diff --git a/TUICallKit-Vue3/components/dev-training-entry/index.vue b/TUICallKit-Vue3/components/dev-training-entry/index.vue index 6a1133fb5..22916f9e4 100644 --- a/TUICallKit-Vue3/components/dev-training-entry/index.vue +++ b/TUICallKit-Vue3/components/dev-training-entry/index.vue @@ -1,729 +1,729 @@ - - - - - + + + + + diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json index 66acdbafd..223d8b39f 100644 --- a/TUICallKit-Vue3/pages.json +++ b/TUICallKit-Vue3/pages.json @@ -1,243 +1,243 @@ -{ - "pages": [ - { - "path": "pages/index/index", - "style": { - "navigationBarTitleText": "甄养堂", - "backgroundColor": "#F8F8F8", - "backgroundColorTop": "#F8F8F8" - } - }, - { - "path": "pages/login/login", - "style": { - "navigationBarTitleText": "视频问诊" - } - }, - { - "path": "pages/user/userinfo", - "style": { - "navigationBarTitleText": "个人资料" - } - }, - { - "path": "pages/index/video", - "style": { - "navigationBarTitleText": "甄养堂" - } - }, - { - "path": "pages/order/monad/monad", - "style": { - "navigationBarTitleText": "诊单确认", - "navigationBarHidden": true, - "navigationStyle": "custom" - } - }, - { - "path": "pages/user/user", - "style": { - "navigationBarTitleText": "我的", - "backgroundColor": "#e8eaed", - "backgroundColorTop": "#e8eaed" - } - }, - { - "path": "pages/Card/Card", - "style": { - "navigationBarTitleText": "就诊卡" - } - }, - { - "path": "pages/Card/edit_card", - "style": { - "navigationBarTitleText": "修改就诊卡" - } - }, - { - "path": "pages/Card/contact", - "style": { - "navigationBarTitleText": "服务" - } - }, - { - "path": "pages/order/order", - "style": { - "navigationBarTitleText": "付款" - } - } - ], - "subPackages": [ - { - "root": "TUICallKit/src/Components", - "pages": [ - { - "path": "TUICallKit", - "style": { - "navigationBarTitleText": "视频问诊", - "navigationBarHidden": true, - "navigationStyle": "custom" - } - } - ] - }, - { - "root": "TUIKit", - "pages": [ - { - "path": "pages/chat/chat", - "style": { - "navigationBarTitleText": "问诊" - } - } - ] - }, - { - "root": "doctor", - "pages": [ - { - "path": "pages/doctor/doctor", - "style": { - "navigationBarTitleText": "医生信息" - } - }, - { - "path": "pages/doctor/list/list", - "style": { - "navigationBarTitleText": "执业医生列表" - } - }, - { - "path": "pages/article/article", - "style": { - "navigationBarTitleText": "健康百科" - } - } - ] - }, - { - "root": "training", - "pages": [ - { - "path": "pages/grip-ring", - "style": { - "navigationBarTitleText": "握力环训练", - "navigationBarBackgroundColor": "#f8fafc", - "navigationBarTextStyle": "black", - "backgroundColor": "#f8fafc" - } - }, - { - "path": "pages/pilates-ring", - "style": { - "navigationBarTitleText": "瑜伽环训练", - "navigationBarBackgroundColor": "#f8fafc", - "navigationBarTextStyle": "black", - "backgroundColor": "#f8fafc" - } - }, - { - "path": "pages/foot-pedal", - "style": { - "navigationBarTitleText": "脚蹬器训练", - "navigationBarBackgroundColor": "#f8fafc", - "navigationBarTextStyle": "black", - "backgroundColor": "#f8fafc" - } - }, - { - "path": "pages/dumbbell", - "style": { - "navigationBarTitleText": "哑铃训练", - "navigationBarBackgroundColor": "#f8fafc", - "navigationBarTextStyle": "black", - "backgroundColor": "#f8fafc" - } - }, - { - "path": "pages/metronome", - "style": { - "navigationBarTitleText": "耗糖节拍器", - "navigationBarBackgroundColor": "#f8fafc", - "navigationBarTextStyle": "black", - "backgroundColor": "#f8fafc" - } - } - ] - }, - { - "root": "tongji", - "pages": [ - { - "path": "pages/index", - "style": { - "navigationBarTitleText": "血糖记录", - "navigationBarBackgroundColor": "#204e2b", - "navigationBarTextStyle": "white", - "backgroundColor": "#faf9f5", - "enablePullDownRefresh": true, - "mp-weixin": { - "usingPlugins": { - "WechatSI": { - "version": "0.3.5", - "provider": "wx069ba97219f66d99" - } - } - } - } - }, - { - "path": "pages/weekly", - "style": { - "navigationStyle": "custom", - "navigationBarTitleText": "血糖管理", - "backgroundColor": "#f4fbf4", - "enablePullDownRefresh": true, - "mp-weixin": { - "usingPlugins": { - "WechatSI": { - "version": "0.3.5", - "provider": "wx069ba97219f66d99" - } - } - } - } - }, - { - "path": "pages/more", - "style": { - "navigationStyle": "custom", - "navigationBarTitleText": "日常护理", - "backgroundColor": "#f4fbf4", - "enablePullDownRefresh": true, - "mp-weixin": { - "usingPlugins": { - "WechatSI": { - "version": "0.3.5", - "provider": "wx069ba97219f66d99" - } - } - } - } - }, - { - "path": "endless-game/index", - "style": { - "navigationStyle": "custom", - "navigationBarTitleText": "识糖小课堂", - "backgroundColor": "#eefbf4", - "disableScroll": false, - "enableShareAppMessage": true, - "enableShareTimeline": true - } - } - ] - } - ], - "globalStyle": { - "navigationBarTextStyle": "black", - "navigationBarTitleText": "甄养堂互联网医院", - "navigationBarBackgroundColor": "#F8F8F8", - "backgroundColor": "#F8F8F8" - } -} +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "甄养堂", + "backgroundColor": "#F8F8F8", + "backgroundColorTop": "#F8F8F8" + } + }, + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "视频问诊" + } + }, + { + "path": "pages/user/userinfo", + "style": { + "navigationBarTitleText": "个人资料" + } + }, + { + "path": "pages/index/video", + "style": { + "navigationBarTitleText": "甄养堂" + } + }, + { + "path": "pages/order/monad/monad", + "style": { + "navigationBarTitleText": "诊单确认", + "navigationBarHidden": true, + "navigationStyle": "custom" + } + }, + { + "path": "pages/user/user", + "style": { + "navigationBarTitleText": "我的", + "backgroundColor": "#e8eaed", + "backgroundColorTop": "#e8eaed" + } + }, + { + "path": "pages/Card/Card", + "style": { + "navigationBarTitleText": "就诊卡" + } + }, + { + "path": "pages/Card/edit_card", + "style": { + "navigationBarTitleText": "修改就诊卡" + } + }, + { + "path": "pages/Card/contact", + "style": { + "navigationBarTitleText": "服务" + } + }, + { + "path": "pages/order/order", + "style": { + "navigationBarTitleText": "付款" + } + } + ], + "subPackages": [ + { + "root": "TUICallKit/src/Components", + "pages": [ + { + "path": "TUICallKit", + "style": { + "navigationBarTitleText": "视频问诊", + "navigationBarHidden": true, + "navigationStyle": "custom" + } + } + ] + }, + { + "root": "TUIKit", + "pages": [ + { + "path": "pages/chat/chat", + "style": { + "navigationBarTitleText": "问诊" + } + } + ] + }, + { + "root": "doctor", + "pages": [ + { + "path": "pages/doctor/doctor", + "style": { + "navigationBarTitleText": "医生信息" + } + }, + { + "path": "pages/doctor/list/list", + "style": { + "navigationBarTitleText": "执业医生列表" + } + }, + { + "path": "pages/article/article", + "style": { + "navigationBarTitleText": "健康百科" + } + } + ] + }, + { + "root": "training", + "pages": [ + { + "path": "pages/grip-ring", + "style": { + "navigationBarTitleText": "握力环训练", + "navigationBarBackgroundColor": "#f8fafc", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8fafc" + } + }, + { + "path": "pages/pilates-ring", + "style": { + "navigationBarTitleText": "瑜伽环训练", + "navigationBarBackgroundColor": "#f8fafc", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8fafc" + } + }, + { + "path": "pages/foot-pedal", + "style": { + "navigationBarTitleText": "脚蹬器训练", + "navigationBarBackgroundColor": "#f8fafc", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8fafc" + } + }, + { + "path": "pages/dumbbell", + "style": { + "navigationBarTitleText": "哑铃训练", + "navigationBarBackgroundColor": "#f8fafc", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8fafc" + } + }, + { + "path": "pages/metronome", + "style": { + "navigationBarTitleText": "耗糖节拍器", + "navigationBarBackgroundColor": "#f8fafc", + "navigationBarTextStyle": "black", + "backgroundColor": "#f8fafc" + } + } + ] + }, + { + "root": "tongji", + "pages": [ + { + "path": "pages/index", + "style": { + "navigationBarTitleText": "血糖记录", + "navigationBarBackgroundColor": "#204e2b", + "navigationBarTextStyle": "white", + "backgroundColor": "#faf9f5", + "enablePullDownRefresh": true, + "mp-weixin": { + "usingPlugins": { + "WechatSI": { + "version": "0.3.5", + "provider": "wx069ba97219f66d99" + } + } + } + } + }, + { + "path": "pages/weekly", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "血糖管理", + "backgroundColor": "#f4fbf4", + "enablePullDownRefresh": true, + "mp-weixin": { + "usingPlugins": { + "WechatSI": { + "version": "0.3.5", + "provider": "wx069ba97219f66d99" + } + } + } + } + }, + { + "path": "pages/more", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "日常护理", + "backgroundColor": "#f4fbf4", + "enablePullDownRefresh": true, + "mp-weixin": { + "usingPlugins": { + "WechatSI": { + "version": "0.3.5", + "provider": "wx069ba97219f66d99" + } + } + } + } + }, + { + "path": "endless-game/index", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "识糖小课堂", + "backgroundColor": "#eefbf4", + "disableScroll": false, + "enableShareAppMessage": true, + "enableShareTimeline": true + } + } + ] + } + ], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "甄养堂互联网医院", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + } +} diff --git a/TUICallKit-Vue3/tongji/composables/useDietAi.js b/TUICallKit-Vue3/tongji/composables/useDietAi.js index 477836a73..ba414ed88 100644 --- a/TUICallKit-Vue3/tongji/composables/useDietAi.js +++ b/TUICallKit-Vue3/tongji/composables/useDietAi.js @@ -1,317 +1,317 @@ -import { ref, computed } from 'vue' -import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js' - -const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill' - -const emptyDietAiRecommend = () => ({ - breakfast: '', - drinks: '', - lunch: '', - dinner: '', - tips: '', - avoid: [], - analysis: [], - exercise: null, - disclaimer: '', - rules_summary: '', - source: '', - date: '' -}) - -/** AI 饮食助手(今日推荐 + 能不能吃) */ -export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) { - const dietAiRecommend = ref(emptyDietAiRecommend()) - const dietAiLoading = ref(false) - const dietAiStreaming = ref(false) - const dietAiReplacing = ref(false) - const dietAiStreamField = ref('') - let dietStreamBuffer = '' - const dietAiAskText = ref('') - const dietAiAsking = ref(false) - const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' }) - - const hasDietAiMealContent = computed(() => { - const r = dietAiRecommend.value - return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips) - }) - - function detectDietStreamField(text) { - const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/) - if (!tail) return 'breakfast' - const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' } - return map[tail[1]] || 'breakfast' - } - - function mergePartialDietFromStream() { - const partial = parsePartialDietPlainText(dietStreamBuffer) - const next = { ...dietAiRecommend.value } - for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) { - if (partial[key] != null) next[key] = partial[key] - } - if (Array.isArray(partial.avoid)) next.avoid = partial.avoid - dietAiRecommend.value = next - dietAiStreamField.value = detectDietStreamField(dietStreamBuffer) - } - - function applyDietRecommendPayload(payload) { - if (!payload) return - const prevAnalysis = dietAiRecommend.value.analysis - const prevExercise = dietAiRecommend.value.exercise - dietAiRecommend.value = { - ...emptyDietAiRecommend(), - ...payload, - avoid: Array.isArray(payload.avoid) ? payload.avoid : [], - analysis: Array.isArray(payload.analysis) && payload.analysis.length - ? payload.analysis - : (Array.isArray(prevAnalysis) ? prevAnalysis : []), - exercise: (payload.exercise && typeof payload.exercise === 'object') - ? payload.exercise - : (prevExercise || null) - } - } - - function resolveDietDonePayload(payload) { - if (!payload) return payload - if (payload.source !== 'rule' || !dietStreamBuffer) return payload - - const streamed = parsePartialDietPlainText(dietStreamBuffer) - if (!streamed.breakfast) return payload - - return { - ...emptyDietAiRecommend(), - ...payload, - breakfast: streamed.breakfast, - drinks: streamed.drinks || payload.drinks || '', - lunch: streamed.lunch || payload.lunch || '', - dinner: streamed.dinner || payload.dinner || '', - tips: streamed.tips || payload.tips || '', - avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []), - source: 'ai', - disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。' - } - } - - function onDietAiStreamEvent(event, payload) { - if (event === 'analysis') { - if (Array.isArray(payload?.analysis)) { - dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis } - } - } else if (event === 'exercise') { - if (payload?.exercise && typeof payload.exercise === 'object') { - dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise } - } - } else if (event === 'delta') { - dietAiStreaming.value = true - if (payload.buffer != null) { - dietStreamBuffer = payload.buffer - } else { - dietStreamBuffer += payload.text || '' - } - mergePartialDietFromStream() - } else if (event === 'done') { - applyDietRecommendPayload(resolveDietDonePayload(payload)) - dietStreamBuffer = '' - dietAiStreamField.value = '' - } else if (event === 'error') { - showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败')) - } - } - - async function fetchDietAiRecommendFallback(refresh) { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyDietAiRecommend', - method: 'GET', - timeout: 35000, - data: { - diagnosis_id: diagnosisId.value, - refresh: refresh ? 1 : 0, - ...(refresh ? { _t: Date.now() } : {}) - } - }, false) - if (res?.code === 1 && res.data) { - applyDietRecommendPayload(res.data) - } else if (res?.msg) { - showUserToast(res.msg) - } - } - - async function fetchDietAiRecommend(refresh = false) { - if (!diagnosisId.value || dietAiLoading.value) return - dietAiLoading.value = true - dietAiStreaming.value = false - dietAiReplacing.value = !!refresh - dietStreamBuffer = '' - dietAiStreamField.value = '' - if (refresh) { - applyDietRecommendPayload(emptyDietAiRecommend()) - } - try { - await requestAiStream({ - baseUrl: proxy.$url, - url: '/api/tcm/dailyDietAiRecommendStream', - method: 'GET', - data: { - diagnosis_id: diagnosisId.value, - refresh: refresh ? 1 : 0, - ...(refresh ? { _t: Date.now() } : {}) - }, - onEvent: onDietAiStreamEvent, - fallback: () => fetchDietAiRecommendFallback(refresh) - }) - } catch (e) { - try { - await fetchDietAiRecommendFallback(refresh) - } catch (e2) { - showUserToast('饮食建议加载失败') - } - } finally { - dietAiLoading.value = false - dietAiStreaming.value = false - dietAiReplacing.value = false - dietAiStreamField.value = '' - } - } - - function refreshDietAiRecommend() { - if (dietAiLoading.value) return - fetchDietAiRecommend(true) - } - - async function askDietAiFoodFallback(q) { - const res = await proxy.apiUrl({ - url: '/api/tcm/dailyDietAiAsk', - method: 'POST', - timeout: 35000, - data: { - diagnosis_id: diagnosisId.value, - question: q - } - }, false) - if (res?.code === 1 && res.data) { - dietAiAskResult.value = { - advice: res.data.advice || '', - level: res.data.level || '', - level_label: res.data.level_label || '', - portion: res.data.portion || '', - food: res.data.food || q - } - } else { - showUserToast(formatUserMessage(res?.msg, '咨询失败')) - } - } - - async function askDietAiFood() { - const q = String(dietAiAskText.value || '').trim() - if (!q) { - showUserToast('请输入想咨询的食物') - return - } - if (!diagnosisId.value) return - // 发送后立即清空输入框 - dietAiAskText.value = '' - dietAiAsking.value = true - dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' } - try { - await requestAiStream({ - baseUrl: proxy.$url, - url: '/api/tcm/dailyDietAiAskStream', - method: 'POST', - data: { - diagnosis_id: diagnosisId.value, - question: q - }, - onEvent: (event, payload) => { - if (event === 'start' && !dietAiAskResult.value.advice) { - dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' } - } else if (event === 'delta') { - dietAiAskResult.value = { - ...dietAiAskResult.value, - advice: payload.advice || dietAiAskResult.value.advice - } - } else if (event === 'done') { - dietAiAskResult.value = { - advice: payload.advice || '', - level: payload.level || '', - level_label: payload.level_label || '', - portion: payload.portion || '', - food: payload.food || q - } - } else if (event === 'error') { - showUserToast(formatUserMessage(payload?.message, '咨询失败')) - } - }, - fallback: () => askDietAiFoodFallback(q) - }) - } catch (e) { - try { - await askDietAiFoodFallback(q) - } catch (e2) { - showUserToast('网络异常,请稍后再试') - } - } finally { - dietAiAsking.value = false - } - } - - function onDietAiAskInput(e) { - dietAiAskText.value = e?.detail?.value ?? '' - } - - function saveDietPrefillToStorage() { - const r = dietAiRecommend.value - if (!r.breakfast) return - try { - uni.setStorageSync(DIET_AI_PREFILL_KEY, { - breakfast: r.breakfast, - drinks: r.drinks || '', - lunch: r.lunch, - dinner: r.dinner, - tips: r.tips || '' - }) - } catch (e) {} - } - - function goMoreDietForm(withPrefill = false) { - if (withPrefill) saveDietPrefillToStorage() - uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' }) - } - - async function applyDietAiToForm() { - if (!dietAiRecommend.value.breakfast) { - await fetchDietAiRecommend(false) - } - goMoreDietForm(true) - } - - function openDietForm() { - goMoreDietForm(false) - } - - return { - dietAiRecommend, - dietAiLoading, - dietAiStreaming, - dietAiReplacing, - dietAiStreamField, - dietAiAskText, - dietAiAsking, - dietAiAskResult, - hasDietAiMealContent, - fetchDietAiRecommend, - refreshDietAiRecommend, - askDietAiFood, - onDietAiAskInput, - applyDietAiToForm, - openDietForm - } -} - -/** more 页打开饮食表单时读取并清除预填 */ -export function consumeDietAiPrefill() { - try { - const data = uni.getStorageSync(DIET_AI_PREFILL_KEY) - uni.removeStorageSync(DIET_AI_PREFILL_KEY) - if (data && typeof data === 'object' && data.breakfast) return data - } catch (e) {} - return null -} +import { ref, computed } from 'vue' +import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js' + +const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill' + +const emptyDietAiRecommend = () => ({ + breakfast: '', + drinks: '', + lunch: '', + dinner: '', + tips: '', + avoid: [], + analysis: [], + exercise: null, + disclaimer: '', + rules_summary: '', + source: '', + date: '' +}) + +/** AI 饮食助手(今日推荐 + 能不能吃) */ +export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) { + const dietAiRecommend = ref(emptyDietAiRecommend()) + const dietAiLoading = ref(false) + const dietAiStreaming = ref(false) + const dietAiReplacing = ref(false) + const dietAiStreamField = ref('') + let dietStreamBuffer = '' + const dietAiAskText = ref('') + const dietAiAsking = ref(false) + const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' }) + + const hasDietAiMealContent = computed(() => { + const r = dietAiRecommend.value + return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips) + }) + + function detectDietStreamField(text) { + const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/) + if (!tail) return 'breakfast' + const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' } + return map[tail[1]] || 'breakfast' + } + + function mergePartialDietFromStream() { + const partial = parsePartialDietPlainText(dietStreamBuffer) + const next = { ...dietAiRecommend.value } + for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) { + if (partial[key] != null) next[key] = partial[key] + } + if (Array.isArray(partial.avoid)) next.avoid = partial.avoid + dietAiRecommend.value = next + dietAiStreamField.value = detectDietStreamField(dietStreamBuffer) + } + + function applyDietRecommendPayload(payload) { + if (!payload) return + const prevAnalysis = dietAiRecommend.value.analysis + const prevExercise = dietAiRecommend.value.exercise + dietAiRecommend.value = { + ...emptyDietAiRecommend(), + ...payload, + avoid: Array.isArray(payload.avoid) ? payload.avoid : [], + analysis: Array.isArray(payload.analysis) && payload.analysis.length + ? payload.analysis + : (Array.isArray(prevAnalysis) ? prevAnalysis : []), + exercise: (payload.exercise && typeof payload.exercise === 'object') + ? payload.exercise + : (prevExercise || null) + } + } + + function resolveDietDonePayload(payload) { + if (!payload) return payload + if (payload.source !== 'rule' || !dietStreamBuffer) return payload + + const streamed = parsePartialDietPlainText(dietStreamBuffer) + if (!streamed.breakfast) return payload + + return { + ...emptyDietAiRecommend(), + ...payload, + breakfast: streamed.breakfast, + drinks: streamed.drinks || payload.drinks || '', + lunch: streamed.lunch || payload.lunch || '', + dinner: streamed.dinner || payload.dinner || '', + tips: streamed.tips || payload.tips || '', + avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []), + source: 'ai', + disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。' + } + } + + function onDietAiStreamEvent(event, payload) { + if (event === 'analysis') { + if (Array.isArray(payload?.analysis)) { + dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis } + } + } else if (event === 'exercise') { + if (payload?.exercise && typeof payload.exercise === 'object') { + dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise } + } + } else if (event === 'delta') { + dietAiStreaming.value = true + if (payload.buffer != null) { + dietStreamBuffer = payload.buffer + } else { + dietStreamBuffer += payload.text || '' + } + mergePartialDietFromStream() + } else if (event === 'done') { + applyDietRecommendPayload(resolveDietDonePayload(payload)) + dietStreamBuffer = '' + dietAiStreamField.value = '' + } else if (event === 'error') { + showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败')) + } + } + + async function fetchDietAiRecommendFallback(refresh) { + const res = await proxy.apiUrl({ + url: '/api/tcm/dailyDietAiRecommend', + method: 'GET', + timeout: 35000, + data: { + diagnosis_id: diagnosisId.value, + refresh: refresh ? 1 : 0, + ...(refresh ? { _t: Date.now() } : {}) + } + }, false) + if (res?.code === 1 && res.data) { + applyDietRecommendPayload(res.data) + } else if (res?.msg) { + showUserToast(res.msg) + } + } + + async function fetchDietAiRecommend(refresh = false) { + if (!diagnosisId.value || dietAiLoading.value) return + dietAiLoading.value = true + dietAiStreaming.value = false + dietAiReplacing.value = !!refresh + dietStreamBuffer = '' + dietAiStreamField.value = '' + if (refresh) { + applyDietRecommendPayload(emptyDietAiRecommend()) + } + try { + await requestAiStream({ + baseUrl: proxy.$url, + url: '/api/tcm/dailyDietAiRecommendStream', + method: 'GET', + data: { + diagnosis_id: diagnosisId.value, + refresh: refresh ? 1 : 0, + ...(refresh ? { _t: Date.now() } : {}) + }, + onEvent: onDietAiStreamEvent, + fallback: () => fetchDietAiRecommendFallback(refresh) + }) + } catch (e) { + try { + await fetchDietAiRecommendFallback(refresh) + } catch (e2) { + showUserToast('饮食建议加载失败') + } + } finally { + dietAiLoading.value = false + dietAiStreaming.value = false + dietAiReplacing.value = false + dietAiStreamField.value = '' + } + } + + function refreshDietAiRecommend() { + if (dietAiLoading.value) return + fetchDietAiRecommend(true) + } + + async function askDietAiFoodFallback(q) { + const res = await proxy.apiUrl({ + url: '/api/tcm/dailyDietAiAsk', + method: 'POST', + timeout: 35000, + data: { + diagnosis_id: diagnosisId.value, + question: q + } + }, false) + if (res?.code === 1 && res.data) { + dietAiAskResult.value = { + advice: res.data.advice || '', + level: res.data.level || '', + level_label: res.data.level_label || '', + portion: res.data.portion || '', + food: res.data.food || q + } + } else { + showUserToast(formatUserMessage(res?.msg, '咨询失败')) + } + } + + async function askDietAiFood() { + const q = String(dietAiAskText.value || '').trim() + if (!q) { + showUserToast('请输入想咨询的食物') + return + } + if (!diagnosisId.value) return + // 发送后立即清空输入框 + dietAiAskText.value = '' + dietAiAsking.value = true + dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' } + try { + await requestAiStream({ + baseUrl: proxy.$url, + url: '/api/tcm/dailyDietAiAskStream', + method: 'POST', + data: { + diagnosis_id: diagnosisId.value, + question: q + }, + onEvent: (event, payload) => { + if (event === 'start' && !dietAiAskResult.value.advice) { + dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' } + } else if (event === 'delta') { + dietAiAskResult.value = { + ...dietAiAskResult.value, + advice: payload.advice || dietAiAskResult.value.advice + } + } else if (event === 'done') { + dietAiAskResult.value = { + advice: payload.advice || '', + level: payload.level || '', + level_label: payload.level_label || '', + portion: payload.portion || '', + food: payload.food || q + } + } else if (event === 'error') { + showUserToast(formatUserMessage(payload?.message, '咨询失败')) + } + }, + fallback: () => askDietAiFoodFallback(q) + }) + } catch (e) { + try { + await askDietAiFoodFallback(q) + } catch (e2) { + showUserToast('网络异常,请稍后再试') + } + } finally { + dietAiAsking.value = false + } + } + + function onDietAiAskInput(e) { + dietAiAskText.value = e?.detail?.value ?? '' + } + + function saveDietPrefillToStorage() { + const r = dietAiRecommend.value + if (!r.breakfast) return + try { + uni.setStorageSync(DIET_AI_PREFILL_KEY, { + breakfast: r.breakfast, + drinks: r.drinks || '', + lunch: r.lunch, + dinner: r.dinner, + tips: r.tips || '' + }) + } catch (e) {} + } + + function goMoreDietForm(withPrefill = false) { + if (withPrefill) saveDietPrefillToStorage() + uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' }) + } + + async function applyDietAiToForm() { + if (!dietAiRecommend.value.breakfast) { + await fetchDietAiRecommend(false) + } + goMoreDietForm(true) + } + + function openDietForm() { + goMoreDietForm(false) + } + + return { + dietAiRecommend, + dietAiLoading, + dietAiStreaming, + dietAiReplacing, + dietAiStreamField, + dietAiAskText, + dietAiAsking, + dietAiAskResult, + hasDietAiMealContent, + fetchDietAiRecommend, + refreshDietAiRecommend, + askDietAiFood, + onDietAiAskInput, + applyDietAiToForm, + openDietForm + } +} + +/** more 页打开饮食表单时读取并清除预填 */ +export function consumeDietAiPrefill() { + try { + const data = uni.getStorageSync(DIET_AI_PREFILL_KEY) + uni.removeStorageSync(DIET_AI_PREFILL_KEY) + if (data && typeof data === 'object' && data.breakfast) return data + } catch (e) {} + return null +} diff --git a/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js b/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js index 5b34f7852..c886aaf8f 100644 --- a/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js +++ b/TUICallKit-Vue3/tongji/composables/useTongjiAuth.js @@ -1,115 +1,115 @@ -import { ref } from 'vue' -import { formatUserMessage } from '../utils/tongjiHelpers.js' - -/** 登录 / 就诊卡门禁(index 与 more 共用) */ -export function useTongjiAuth(proxy) { - const authChecking = ref(false) - let gateRedirected = false - let authSessionPromise = null - - function hasAuthToken() { - return !!String(uni.getStorageSync('token') || '').trim() - } - - function clearAuthStorage() { - uni.removeStorageSync('token') - authSessionPromise = null - } - - function wxLoginGetCode() { - return new Promise((resolve, reject) => { - uni.login({ - provider: 'weixin', - success: (res) => { - if (res && res.code) resolve(res.code) - else reject(new Error('微信登录未返回 code')) - }, - fail: reject - }) - }) - } - - async function mnpLoginWithCode(code) { - const res = await proxy.apiUrl({ - url: '/api/login/mnpLogin', - method: 'POST', - data: { code } - }, false) - if (res && res.code === 1 && res.data && res.data.token) { - uni.setStorageSync('token', res.data.token) - uni.setStorageSync('userData', res.data) - return res.data - } - clearAuthStorage() - throw new Error(formatUserMessage(res?.msg, '登录失败')) - } - - async function doWxLogin() { - const code = await wxLoginGetCode() - await mnpLoginWithCode(code) - return hasAuthToken() - } - - async function verifyOrLogin() { - if (!hasAuthToken()) { - return doWxLogin() - } - try { - const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false) - if (res && res.code === 1 && res.data) { - uni.setStorageSync('userData', res.data) - return true - } - } catch (e) { - /* 网络异常走重新登录 */ - } - clearAuthStorage() - try { - return await doWxLogin() - } catch (e2) { - return false - } - } - - async function ensureLoggedIn() { - if (authSessionPromise) { - return authSessionPromise - } - authSessionPromise = verifyOrLogin() - .then((ok) => { - if (!ok) authSessionPromise = null - return !!ok - }) - .catch(() => { - authSessionPromise = null - return false - }) - return authSessionPromise - } - - function redirectToCardEntry(returnPath) { - if (gateRedirected) return - gateRedirected = true - const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index') - uni.redirectTo({ - url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}` - }) - } - - function resetGateRedirect() { - gateRedirected = false - } - - function isGateRedirected() { - return gateRedirected - } - - return { - authChecking, - hasAuthToken, - ensureLoggedIn, - redirectToCardEntry, - resetGateRedirect, - isGateRedirected - } -} +import { ref } from 'vue' +import { formatUserMessage } from '../utils/tongjiHelpers.js' + +/** 登录 / 就诊卡门禁(index 与 more 共用) */ +export function useTongjiAuth(proxy) { + const authChecking = ref(false) + let gateRedirected = false + let authSessionPromise = null + + function hasAuthToken() { + return !!String(uni.getStorageSync('token') || '').trim() + } + + function clearAuthStorage() { + uni.removeStorageSync('token') + authSessionPromise = null + } + + function wxLoginGetCode() { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: (res) => { + if (res && res.code) resolve(res.code) + else reject(new Error('微信登录未返回 code')) + }, + fail: reject + }) + }) + } + + async function mnpLoginWithCode(code) { + const res = await proxy.apiUrl({ + url: '/api/login/mnpLogin', + method: 'POST', + data: { code } + }, false) + if (res && res.code === 1 && res.data && res.data.token) { + uni.setStorageSync('token', res.data.token) + uni.setStorageSync('userData', res.data) + return res.data + } + clearAuthStorage() + throw new Error(formatUserMessage(res?.msg, '登录失败')) + } + + async function doWxLogin() { + const code = await wxLoginGetCode() + await mnpLoginWithCode(code) + return hasAuthToken() + } + + async function verifyOrLogin() { + if (!hasAuthToken()) { + return doWxLogin() + } + try { + const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false) + if (res && res.code === 1 && res.data) { + uni.setStorageSync('userData', res.data) + return true + } + } catch (e) { + /* 网络异常走重新登录 */ + } + clearAuthStorage() + try { + return await doWxLogin() + } catch (e2) { + return false + } + } + + async function ensureLoggedIn() { + if (authSessionPromise) { + return authSessionPromise + } + authSessionPromise = verifyOrLogin() + .then((ok) => { + if (!ok) authSessionPromise = null + return !!ok + }) + .catch(() => { + authSessionPromise = null + return false + }) + return authSessionPromise + } + + function redirectToCardEntry(returnPath) { + if (gateRedirected) return + gateRedirected = true + const returnUrl = encodeURIComponent(returnPath || '/tongji/pages/index') + uni.redirectTo({ + url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}` + }) + } + + function resetGateRedirect() { + gateRedirected = false + } + + function isGateRedirected() { + return gateRedirected + } + + return { + authChecking, + hasAuthToken, + ensureLoggedIn, + redirectToCardEntry, + resetGateRedirect, + isGateRedirected + } +} diff --git a/TUICallKit-Vue3/tongji/pages/more.vue b/TUICallKit-Vue3/tongji/pages/more.vue index e26d77f33..9bf7db1a3 100644 --- a/TUICallKit-Vue3/tongji/pages/more.vue +++ b/TUICallKit-Vue3/tongji/pages/more.vue @@ -1,8515 +1,8515 @@ -