diff --git a/TUICallKit-Vue3/tongji/composables/useDietAi.js b/TUICallKit-Vue3/tongji/composables/useDietAi.js new file mode 100644 index 00000000..154ed2ab --- /dev/null +++ b/TUICallKit-Vue3/tongji/composables/useDietAi.js @@ -0,0 +1,312 @@ +import { ref, computed } from 'vue' +import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js' + +const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill' + +const emptyDietAiRecommend = () => ({ + breakfast: '', + 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.lunch || r.dinner || r.tips) + }) + + function detectDietStreamField(text) { + const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/) + if (!tail) return 'breakfast' + const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' } + return map[tail[1]] || 'breakfast' + } + + function mergePartialDietFromStream() { + const partial = parsePartialDietPlainText(dietStreamBuffer) + const next = { ...dietAiRecommend.value } + for (const key of ['breakfast', '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, + 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 + 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, + 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/pages/index.vue b/TUICallKit-Vue3/tongji/pages/index.vue index dbb64924..457f4fcd 100644 --- a/TUICallKit-Vue3/tongji/pages/index.vue +++ b/TUICallKit-Vue3/tongji/pages/index.vue @@ -365,11 +365,236 @@ 查看更多 - 健康日历 · 饮食运动 · 家人互动 + 健康日历 · 运动 · 家人互动 + + + + + + + AI 饮食建议 + 读您近 7 天 · 30 天血糖,定制今日三餐 + + + + + {{ dietAiLoading ? '生成中' : '换一换' }} + + + + + + + + + + 正在生成今日饮食建议… + + + + + 今日三餐 + + + + + + + 早餐 + + + {{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }} + + + + + + 午餐 + + + {{ dietAiRecommend.lunch || (dietAiLoading ? '…' : '—') }} + + + + + + 晚餐 + + + {{ dietAiRecommend.dinner || (dietAiLoading ? '…' : '—') }} + + + + + + + + 小贴士 + + + {{ dietAiRecommend.tips || '…' }} + + + + + + + 今日尽量少碰 + + + {{ item }} + + + + + + 填入今日饮食 + + + 手动记录 + + + + + + + + + + 运动降糖 + {{ dietExercise.level_label }} + + + {{ dietExercise.headline }} + + + {{ idx + 1 }} + {{ it }} + + + + + {{ dietExercise.intensity }} + + {{ dietExercise.note }} + + + + + 能不能吃? + 输入食物名称,结合您的档案判断 + + + + + + {{ dietAiAsking ? '…' : '问 AI' }} + + + + + {{ dietAiAskResult.level_label }} + + + {{ dietAiAskResult.advice || (dietAiAsking ? '正在想…' : '') }} + + 建议份量:{{ dietAiAskResult.portion }} + + + + + + + + 以上建议依据 · 血糖回顾 + + + + {{ s.label }} + + + + {{ s.compliance }} + % + + 达标率 + + + + 偏高 + + {{ s.high_days }} + + + + 空腹均 + {{ dietAvgText(s.fasting_avg) }} + + + 餐后均 + {{ dietAvgText(s.postprandial_avg) }} + + + {{ s.record_days }} 天有记录 + + + + 暂无记录 + + + + + + + {{ dietAiRecommend.rules_summary }} + {{ dietAiRecommend.disclaimer }} + + + 数据仅供参考,请遵医嘱 @@ -531,6 +756,7 @@ import { ref, computed, getCurrentInstance, nextTick, watch } from 'vue' import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage } from '@dcloudio/uni-app' import SugarTreeGraphic from '../components/SugarTreeGraphic.vue' import TongjiIcon from '../components/TongjiIcon.vue' +import { useDietAi } from '../composables/useDietAi.js' const { proxy } = getCurrentInstance() @@ -573,6 +799,53 @@ const patientName = ref('') const age = ref(0) const gender = ref(0) +const { + dietAiRecommend, + dietAiLoading, + dietAiStreaming, + dietAiReplacing, + dietAiStreamField, + dietAiAskText, + dietAiAsking, + dietAiAskResult, + hasDietAiMealContent, + fetchDietAiRecommend, + refreshDietAiRecommend, + askDietAiFood, + onDietAiAskInput, + applyDietAiToForm, + openDietForm +} = useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) + +/** AI 饮食建议依据:近7天 / 近30天 血糖统计 */ +const dietAnalysisList = computed(() => { + const list = dietAiRecommend.value && Array.isArray(dietAiRecommend.value.analysis) + ? dietAiRecommend.value.analysis + : [] + return list.filter((s) => s && typeof s === 'object') +}) + +const hasDietAnalysis = computed(() => dietAnalysisList.value.length > 0) + +/** AI 运动降糖建议(按血糖统计调整强度) */ +const dietExercise = computed(() => { + const ex = dietAiRecommend.value && dietAiRecommend.value.exercise + return ex && typeof ex === 'object' && Array.isArray(ex.items) && ex.items.length ? ex : null +}) + +/** 达标率 → 颜色档位(绿/橙/红) */ +function dietRateClass(rate) { + const n = Number(rate) + if (!Number.isFinite(n)) return 'rate-mid' + if (n >= 80) return 'rate-good' + if (n >= 50) return 'rate-mid' + return 'rate-low' +} + +function dietAvgText(v) { + return v == null || v === '' ? '—' : v +} + /** 登录 / 就诊卡门禁校验中(避免未登录时闪屏) */ const authChecking = ref(false) @@ -699,7 +972,8 @@ function toNumber(v) { function getBloodSugarThresholds(ageValue) { const a = Number(ageValue) - if (!Number.isFinite(a) || a <= 0) return null + // 年龄未知时用通用糖尿病控制目标兜底,避免高血糖不被标记 + if (!Number.isFinite(a) || a <= 0) return { fasting: 7, postprandial: 10 } if (a < 50) return { fasting: 7, postprandial: 9 } return { fasting: 8, postprandial: 10 } } @@ -1305,6 +1579,7 @@ async function fetchAll() { } else { trackingNotes.value = [] } + await fetchDietAiRecommend(false) } catch (e) { uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' }) } finally { @@ -5842,4 +6117,700 @@ async function onFamilyLike() { } @import '../styles/daily-theme.scss'; + +.ai-diet-card { + margin: 24rpx 24rpx 0; + padding: 0; + overflow: hidden; + border-radius: 28rpx; + border: 1rpx solid var(--border-soft); + box-shadow: var(--shadow-premium); +} +.ai-diet-hero { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16rpx; + padding: 28rpx 24rpx 24rpx; + background: linear-gradient(135deg, var(--primary-light) 0%, #f0faf4 55%, #ffffff 100%); + border-bottom: 1rpx solid var(--border-soft); +} +.ai-diet-hero-main { + display: flex; + align-items: flex-start; + gap: 16rpx; + flex: 1; + min-width: 0; +} +.ai-diet-hero-icon { + flex-shrink: 0; + width: 72rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 20rpx; + background: var(--primary-grad); + box-shadow: var(--shadow-glow-primary); +} +.ai-diet-hero-copy { + flex: 1; + min-width: 0; +} +.ai-diet-title { + display: block; + font-size: 34rpx; + font-weight: 800; + color: var(--text-primary); + letter-spacing: 0.5rpx; + line-height: 1.3; +} +.ai-diet-sub { + display: block; + margin-top: 8rpx; + font-size: 24rpx; + line-height: 1.5; + color: var(--text-secondary); +} +.ai-diet-refresh { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 8rpx; + min-height: 64rpx; + padding: 0 20rpx; + border-radius: 999rpx; + background: #ffffff; + border: 2rpx solid var(--border-soft); + color: var(--primary); + font-size: 24rpx; + font-weight: 700; + box-shadow: 0 4rpx 12rpx rgba(32, 78, 43, 0.06); + transition: opacity 0.2s ease, background 0.2s ease; +} +.ai-diet-refresh.disabled { + opacity: 0.55; +} + +/* ===== 建议依据:近7天 / 近30天 血糖回顾 ===== */ +.ai-diet-analysis { + padding: 22rpx 24rpx 24rpx; + background: linear-gradient(180deg, #ffffff 0%, #f6fbf7 100%); + border-top: 1rpx solid var(--border-soft); +} +.ai-diet-analysis-head { + display: flex; + align-items: center; + gap: 8rpx; + margin-bottom: 18rpx; +} +.ai-diet-analysis-title { + font-size: 26rpx; + font-weight: 700; + color: var(--primary-container); + letter-spacing: 0.5rpx; +} +.ai-diet-analysis-grid { + display: flex; + gap: 16rpx; +} +.ai-diet-analysis-col { + flex: 1; + min-width: 0; + padding: 20rpx 18rpx; + border-radius: 20rpx; + background: var(--surface); + border: 1rpx solid var(--border-soft); + box-shadow: 0 2rpx 10rpx rgba(32, 78, 43, 0.04); +} +.ai-diet-analysis-range { + display: block; + font-size: 24rpx; + font-weight: 700; + color: var(--text-secondary); + margin-bottom: 14rpx; +} +.ai-diet-analysis-rate { + display: flex; + align-items: flex-end; + gap: 10rpx; + padding-bottom: 14rpx; + margin-bottom: 14rpx; + border-bottom: 1rpx dashed var(--border-soft); +} +.ai-diet-analysis-rate-val { + display: flex; + align-items: baseline; +} +.ai-diet-analysis-rate-num { + font-size: 56rpx; + font-weight: 800; + line-height: 1; + color: var(--primary); +} +.ai-diet-analysis-rate-pct { + font-size: 26rpx; + font-weight: 700; + color: var(--primary); + margin-left: 2rpx; +} +.ai-diet-analysis-rate-label { + font-size: 24rpx; + color: var(--text-muted); + padding-bottom: 6rpx; +} +.ai-diet-analysis-rate.rate-good .ai-diet-analysis-rate-num, +.ai-diet-analysis-rate.rate-good .ai-diet-analysis-rate-pct { + color: var(--success); +} +.ai-diet-analysis-rate.rate-mid .ai-diet-analysis-rate-num, +.ai-diet-analysis-rate.rate-mid .ai-diet-analysis-rate-pct { + color: var(--warning); +} +.ai-diet-analysis-rate.rate-low .ai-diet-analysis-rate-num, +.ai-diet-analysis-rate.rate-low .ai-diet-analysis-rate-pct { + color: var(--danger); +} +.ai-diet-analysis-metrics { + display: flex; + justify-content: space-between; + gap: 8rpx; +} +.ai-diet-analysis-metric { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6rpx; +} +.ai-diet-analysis-m-label { + font-size: 22rpx; + color: var(--text-muted); +} +.ai-diet-analysis-m-value { + font-size: 30rpx; + font-weight: 700; + color: var(--text-primary); + line-height: 1.1; +} +.ai-diet-analysis-m-value.is-warn { + color: var(--danger); +} +.ai-diet-analysis-m-unit { + font-size: 20rpx; + font-weight: 600; + color: var(--text-muted); + margin-left: 2rpx; +} +.ai-diet-analysis-days { + display: block; + margin-top: 14rpx; + font-size: 22rpx; + color: var(--text-muted); +} +.ai-diet-analysis-empty { + display: flex; + align-items: center; + gap: 8rpx; + padding: 18rpx 0; + font-size: 24rpx; + color: var(--text-muted); +} + +/* ===== 今日三餐分组标题 ===== */ +.ai-diet-section-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14rpx; +} +.ai-diet-section-title { + font-size: 28rpx; + font-weight: 800; + color: var(--text-primary); + letter-spacing: 0.5rpx; +} +.ai-diet-section-tag { + font-size: 22rpx; + font-weight: 600; + color: var(--primary-container); + padding: 4rpx 16rpx; + border-radius: 999rpx; + background: var(--primary-light); +} + +.ai-diet-skeleton { + padding: 24rpx; +} +.ai-diet-skeleton-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12rpx; + margin-bottom: 20rpx; +} +.ai-diet-skeleton-tag { + width: 88rpx; + height: 40rpx; + border-radius: 12rpx; + background: linear-gradient(90deg, var(--slate-100) 25%, var(--slate-200) 50%, var(--slate-100) 75%); + background-size: 200% 100%; + animation: ai-diet-shimmer 1.2s ease-in-out infinite; +} +.ai-diet-skeleton-line { + flex: 1; + min-width: 60%; + height: 28rpx; + border-radius: 8rpx; + background: linear-gradient(90deg, var(--slate-100) 25%, var(--slate-200) 50%, var(--slate-100) 75%); + background-size: 200% 100%; + animation: ai-diet-shimmer 1.2s ease-in-out infinite; +} +.ai-diet-skeleton-line.short { + flex-basis: 100%; + min-width: 40%; + max-width: 72%; + animation-delay: 0.15s; +} +.ai-diet-skeleton-hint { + display: block; + margin-top: 8rpx; + text-align: center; + font-size: 26rpx; + color: var(--primary-container); +} +@keyframes ai-diet-shimmer { + 0% { background-position: 100% 0; } + 100% { background-position: -100% 0; } +} +@media (prefers-reduced-motion: reduce) { + .ai-diet-skeleton-tag, + .ai-diet-skeleton-line { + animation: none; + background: var(--slate-100); + } +} +.ai-diet-body { + padding: 20rpx 24rpx 8rpx; +} +.ai-diet-body.is-streaming .ai-diet-meal-text { + transition: color 0.15s ease; +} +.ai-diet-meal-list { + display: flex; + flex-direction: column; + gap: 12rpx; +} +.ai-diet-meal-card { + padding: 20rpx; + border-radius: 20rpx; + background: var(--surface); + border: 1rpx solid var(--border-soft); + border-left-width: 6rpx; +} +.ai-diet-meal-card.meal-breakfast { + border-left-color: #d97706; + background: linear-gradient(90deg, rgba(217, 119, 6, 0.06) 0%, #ffffff 28%); +} +.ai-diet-meal-card.meal-lunch { + border-left-color: var(--primary); + background: linear-gradient(90deg, var(--primary-glow) 0%, #ffffff 28%); +} +.ai-diet-meal-card.meal-dinner { + border-left-color: #4f46e5; + background: linear-gradient(90deg, rgba(79, 70, 229, 0.06) 0%, #ffffff 28%); +} +.ai-diet-meal-head { + display: flex; + align-items: center; + gap: 10rpx; + margin-bottom: 10rpx; +} +.ai-diet-meal-icon { + width: 44rpx; + height: 44rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12rpx; + background: var(--primary-light); +} +.ai-diet-meal-label { + font-size: 26rpx; + font-weight: 800; + color: var(--primary); + letter-spacing: 1rpx; +} +.ai-diet-meal-text { + display: block; + font-size: 30rpx; + line-height: 1.6; + color: var(--text-primary); +} +.ai-diet-tip-box { + margin-top: 16rpx; + padding: 18rpx 20rpx; + border-radius: 16rpx; + background: var(--primary-light); + border: 1rpx solid rgba(32, 78, 43, 0.12); +} +.ai-diet-tip-head { + display: flex; + align-items: center; + gap: 8rpx; + margin-bottom: 8rpx; +} +.ai-diet-tip-label { + font-size: 24rpx; + font-weight: 700; + color: var(--primary-container); +} +.ai-diet-tip-text { + display: block; + font-size: 28rpx; + line-height: 1.55; + color: var(--text-secondary); +} +.ai-diet-avoid { + margin-top: 16rpx; + padding: 18rpx 20rpx; + border-radius: 16rpx; + background: var(--danger-light); + border: 1rpx solid rgba(220, 38, 38, 0.15); +} +.ai-diet-avoid-head { + display: flex; + align-items: center; + gap: 8rpx; + margin-bottom: 12rpx; +} +.ai-diet-avoid-label { + font-size: 26rpx; + color: #b91c1c; + font-weight: 700; +} +.ai-diet-avoid-tags { + display: flex; + flex-wrap: wrap; + gap: 10rpx; +} +.ai-diet-avoid-tag { + padding: 8rpx 16rpx; + border-radius: 999rpx; + font-size: 24rpx; + color: #991b1b; + background: #ffffff; + border: 1rpx solid rgba(220, 38, 38, 0.2); +} +.ai-diet-actions { + display: flex; + gap: 16rpx; + margin-top: 24rpx; + padding-bottom: 8rpx; +} +.ai-diet-btn { + flex: 1; + min-height: 88rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 20rpx; + font-size: 28rpx; + font-weight: 700; + transition: opacity 0.2s ease; +} +.ai-diet-btn.primary { + background: var(--primary-grad); + color: #ffffff; + box-shadow: var(--shadow-glow-primary); + border: none; +} +.ai-diet-btn.ghost { + background: #ffffff; + border: 2rpx solid var(--border-soft); + color: var(--primary-container); +} + +/* ===== 运动降糖建议 ===== */ +.ai-diet-exercise { + margin: 8rpx 24rpx 24rpx; + padding: 24rpx; + border-radius: 20rpx; + background: linear-gradient(180deg, #f3f9f5 0%, #ffffff 60%); + border: 1rpx solid var(--border-soft); + border-left: 6rpx solid var(--primary); +} +.ai-diet-exercise.lv-high { + border-left-color: var(--danger); + background: linear-gradient(180deg, #fef4f3 0%, #ffffff 60%); +} +.ai-diet-exercise.lv-medium { + border-left-color: var(--warning); + background: linear-gradient(180deg, #fff8f0 0%, #ffffff 60%); +} +.ai-diet-exercise-head { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 14rpx; +} +.ai-diet-exercise-icon { + flex-shrink: 0; + width: 52rpx; + height: 52rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 14rpx; + background: var(--primary-grad); + box-shadow: var(--shadow-glow-primary); +} +.ai-diet-exercise.lv-high .ai-diet-exercise-icon { + background: linear-gradient(160deg, #dc2626 0%, #ef4444 100%); + box-shadow: 0 8rpx 20rpx rgba(220, 38, 38, 0.18); +} +.ai-diet-exercise.lv-medium .ai-diet-exercise-icon { + background: linear-gradient(160deg, #d97706 0%, #f59e0b 100%); + box-shadow: 0 8rpx 20rpx rgba(217, 119, 6, 0.18); +} +.ai-diet-exercise-head-text { + display: flex; + align-items: center; + gap: 12rpx; + flex: 1; + min-width: 0; +} +.ai-diet-exercise-title { + font-size: 30rpx; + font-weight: 800; + color: var(--text-primary); + letter-spacing: 0.5rpx; +} +.ai-diet-exercise-tag { + font-size: 22rpx; + font-weight: 700; + padding: 4rpx 16rpx; + border-radius: 999rpx; + color: var(--primary); + background: var(--primary-light); +} +.ai-diet-exercise-tag.lv-high { + color: var(--danger); + background: var(--danger-light); +} +.ai-diet-exercise-tag.lv-medium { + color: var(--warning); + background: var(--warning-light); +} +.ai-diet-exercise-headline { + display: block; + font-size: 28rpx; + line-height: 1.55; + color: var(--text-secondary); + margin-bottom: 16rpx; +} +.ai-diet-exercise-list { + display: flex; + flex-direction: column; + gap: 12rpx; +} +.ai-diet-exercise-item { + display: flex; + align-items: flex-start; + gap: 14rpx; +} +.ai-diet-exercise-bullet { + flex-shrink: 0; + width: 40rpx; + height: 40rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--primary-light); + color: var(--primary); + font-size: 24rpx; + font-weight: 800; +} +.ai-diet-exercise.lv-high .ai-diet-exercise-bullet { + background: var(--danger-light); + color: var(--danger); +} +.ai-diet-exercise.lv-medium .ai-diet-exercise-bullet { + background: var(--warning-light); + color: var(--warning); +} +.ai-diet-exercise-item-text { + flex: 1; + min-width: 0; + font-size: 30rpx; + line-height: 1.55; + color: var(--text-primary); +} +.ai-diet-exercise-meta { + display: flex; + align-items: center; + gap: 8rpx; + margin-top: 18rpx; + padding: 12rpx 18rpx; + border-radius: 14rpx; + background: var(--surface-muted); + font-size: 24rpx; + font-weight: 600; + color: var(--text-secondary); +} +.ai-diet-exercise-note { + display: block; + margin-top: 14rpx; + font-size: 24rpx; + line-height: 1.55; + color: var(--text-muted); +} + +.ai-diet-ask-panel { + margin: 8rpx 24rpx 24rpx; + padding: 24rpx; + border-radius: 20rpx; + background: var(--slate-50); + border: 1rpx solid var(--border-soft); +} +.ai-diet-ask-head { + margin-bottom: 16rpx; +} +.ai-diet-ask-title { + display: block; + font-size: 30rpx; + font-weight: 800; + color: var(--primary); + line-height: 1.35; +} +.ai-diet-ask-hint { + display: block; + margin-top: 6rpx; + font-size: 24rpx; + color: var(--text-muted); + line-height: 1.45; +} +.ai-diet-ask-row { + display: flex; + gap: 12rpx; + align-items: stretch; +} +.ai-diet-ask-input { + flex: 1; + min-height: 88rpx; + padding: 0 24rpx; + border-radius: 16rpx; + background: #ffffff; + border: 2rpx solid var(--border-soft); + font-size: 30rpx; + color: var(--text-primary); + box-sizing: border-box; +} +.ai-diet-ask-btn { + flex-shrink: 0; + min-width: 148rpx; + min-height: 88rpx; + display: flex; + align-items: center; + justify-content: center; + gap: 6rpx; + padding: 0 20rpx; + border-radius: 16rpx; + background: var(--primary-grad); + color: #ffffff; + font-size: 28rpx; + font-weight: 700; + box-shadow: var(--shadow-glow-primary); +} +.ai-diet-ask-btn.disabled { + opacity: 0.55; +} +.ai-diet-ask-result { + margin-top: 16rpx; + padding: 20rpx; + border-radius: 16rpx; + background: #ffffff; + border: 1rpx solid var(--border-soft); +} +.ai-diet-ask-result.lv-high { + border-color: rgba(220, 38, 38, 0.25); + background: var(--danger-light); +} +.ai-diet-ask-result.lv-medium { + border-color: rgba(199, 125, 10, 0.3); + background: var(--warning-light); +} +.ai-diet-ask-badge { + display: inline-block; + padding: 6rpx 18rpx; + border-radius: 999rpx; + font-size: 22rpx; + font-weight: 700; + margin-bottom: 12rpx; + background: var(--primary-light); + border: 2rpx solid var(--primary); + color: var(--primary); +} +.ai-diet-ask-badge.lv-high { + border-color: #dc2626; + background: #ffffff; + color: #dc2626; +} +.ai-diet-ask-badge.lv-medium { + border-color: #c77d0a; + background: #ffffff; + color: #c77d0a; +} +.ai-diet-ask-badge.lv-low { + border-color: var(--primary); + background: #ffffff; + color: var(--primary); +} +.ai-diet-ask-advice { + display: block; + font-size: 30rpx; + line-height: 1.6; + color: var(--text-primary); +} +.ai-stream-cursor { + display: inline; + color: var(--primary-container); + animation: ai-cursor-blink 0.9s step-end infinite; +} +@keyframes ai-cursor-blink { + 50% { opacity: 0; } +} +@media (prefers-reduced-motion: reduce) { + .ai-stream-cursor { + animation: none; + opacity: 0.6; + } +} +.ai-diet-ask-portion { + display: block; + margin-top: 12rpx; + padding-top: 12rpx; + border-top: 1rpx dashed var(--border-soft); + font-size: 26rpx; + color: var(--primary-container); + font-weight: 600; +} +.ai-diet-footer { + padding: 0 24rpx 24rpx; +} +.ai-diet-rules { + display: block; + font-size: 24rpx; + color: var(--primary); + line-height: 1.5; + font-weight: 600; +} +.ai-diet-disclaimer { + display: block; + margin-top: 10rpx; + font-size: 22rpx; + color: var(--text-muted); + line-height: 1.45; +} diff --git a/TUICallKit-Vue3/tongji/pages/more.vue b/TUICallKit-Vue3/tongji/pages/more.vue index 71350dae..1f0cd174 100644 --- a/TUICallKit-Vue3/tongji/pages/more.vue +++ b/TUICallKit-Vue3/tongji/pages/more.vue @@ -33,90 +33,6 @@ - - - - - AI 饮食助手 - 午饭多蛋白 · 淀粉不重复 - - - - {{ dietAiLoading ? '生成中' : '换一换' }} - - - - - 正在生成今日饮食建议… - - - - - 早餐 - - {{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }} - - - - 午餐 - - {{ dietAiRecommend.lunch || (dietAiLoading ? '…' : '—') }} - - - - 晚餐 - - {{ dietAiRecommend.dinner || (dietAiLoading ? '…' : '—') }} - - - - {{ dietAiRecommend.tips || '…' }} - - - 今日尽量少碰: - {{ dietAiRecommend.avoid.join('、') }} - - - - 填入今日饮食 - - - 手动记录 - - - - - - 能不能吃? - - - - {{ dietAiAsking ? '…' : '问 AI' }} - - - - - {{ dietAiAskResult.level_label }} - - - {{ dietAiAskResult.advice || (dietAiAsking ? '正在想…' : '') }} - - 建议:{{ dietAiAskResult.portion }} - - - {{ dietAiRecommend.rules_summary }} - {{ dietAiRecommend.disclaimer }} - - @@ -745,9 +661,9 @@ 加载中… - + AI 推荐 - 早 {{ dietAiRecommend.breakfast }} · 午 {{ dietAiRecommend.lunch }} · 晚 {{ dietAiRecommend.dinner }} + 早 {{ dietAiPrefill.breakfast }} · 午 {{ dietAiPrefill.lunch }} · 晚 {{ dietAiPrefill.dinner }} 一键填入 @@ -882,7 +798,7 @@ import SugarTreeGraphic from '../components/SugarTreeGraphic.vue' import TongjiIcon from '../components/TongjiIcon.vue' import CelebrateBurst from '../components/CelebrateBurst.vue' import { TREE_LEVELS, TREE_MAX_LEVEL, calcTreeFromPoints, pickTreeWhisper } from '../utils/treeLevels.js' -import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js' +import { consumeDietAiPrefill } from '../composables/useDietAi.js' const { proxy } = getCurrentInstance() @@ -1529,7 +1445,6 @@ async function fetchAll() { // 拉取游戏化积分/小树状态 await fetchGamifyState() await loadFamilyLikeSummary() - await fetchDietAiRecommend(false) } catch (e) { uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' }) } finally { @@ -1981,239 +1896,11 @@ const emptyDietForm = () => ({ note: '' }) const dietForm = ref(emptyDietForm()) - -const emptyDietAiRecommend = () => ({ - breakfast: '', - lunch: '', - dinner: '', - tips: '', - avoid: [], - disclaimer: '', - rules_summary: '', - source: '', - date: '' -}) -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.lunch || r.dinner || r.tips) -}) - -function detectDietStreamField(text) { - const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/) - if (!tail) return 'breakfast' - const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' } - return map[tail[1]] || 'breakfast' -} - -function mergePartialDietFromStream() { - const partial = parsePartialDietPlainText(dietStreamBuffer) - const next = { ...dietAiRecommend.value } - for (const key of ['breakfast', '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 - dietAiRecommend.value = { - ...emptyDietAiRecommend(), - ...payload, - avoid: Array.isArray(payload.avoid) ? payload.avoid : [] - } -} - -/** 流式已展示 AI 内容时,避免 done 用规则模板覆盖 */ -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, - 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 === '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 - 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 ?? '' -} +const dietAiPrefill = ref(null) function fillDietFormFromAi() { - const r = dietAiRecommend.value + const r = dietAiPrefill.value + if (!r) return if (r.breakfast) dietForm.value.breakfast_foods = r.breakfast if (r.lunch) dietForm.value.lunch_foods = r.lunch if (r.dinner) dietForm.value.dinner_foods = r.dinner @@ -2223,19 +1910,15 @@ function fillDietFormFromAi() { } } -async function applyDietAiToForm() { - if (!dietAiRecommend.value.breakfast) { - await fetchDietAiRecommend(false) - } - await openDietForm() - fillDietFormFromAi() -} - function onDietField(field, e) { dietForm.value[field] = e?.detail?.value ?? '' } -async function openDietForm() { +async function openDietForm(options = {}) { + const autoFill = !!options.autoFill + if (!dietAiPrefill.value) { + dietAiPrefill.value = consumeDietAiPrefill() + } if (!diagnosisId.value) { showUserToast('请先选择就诊卡') return @@ -2264,12 +1947,16 @@ async function openDietForm() { } catch (e) {} finally { dietForm.value.loading = false + if (autoFill && dietAiPrefill.value?.breakfast) { + fillDietFormFromAi() + } } } function closeDietForm() { if (dietForm.value.submitting) return dietForm.value.visible = false + dietAiPrefill.value = null nextTick(() => {}) } @@ -3165,6 +2852,9 @@ async function bootstrap(options) { authChecking.value = false await fetchAll() + if (options.openDiet === '1' || options.openDiet === 1) { + await openDietForm({ autoFill: true }) + } } onLoad((options) => { @@ -6983,217 +6673,6 @@ function onFamilyLikeStripTap() { @import '../styles/daily-theme.scss'; -.ai-diet-card { - margin: 24rpx 24rpx 0; -} -.ai-diet-head { - align-items: flex-start; -} -.ai-diet-refresh { - display: flex; - align-items: center; - gap: 8rpx; - padding: 12rpx 20rpx; - border-radius: 999rpx; - border: 2rpx solid rgba(32, 78, 43, 0.25); - background: #fff; - color: var(--primary); - font-size: 24rpx; - font-weight: 600; -} -.ai-diet-refresh.disabled { - opacity: 0.5; -} -.ai-diet-loading { - padding: 32rpx 0; - text-align: center; - color: var(--primary-container); - font-size: 28rpx; -} -.ai-diet-meals { - padding-top: 8rpx; -} -.ai-diet-meals.is-streaming .ai-diet-meal-text { - transition: color 0.15s ease; -} -.ai-diet-meal { - display: flex; - gap: 16rpx; - align-items: flex-start; - padding: 16rpx 0; - border-bottom: 1rpx solid rgba(32, 78, 43, 0.08); -} -.ai-diet-meal:last-of-type { - border-bottom: none; -} -.ai-diet-meal-tag { - flex-shrink: 0; - min-width: 72rpx; - padding: 6rpx 12rpx; - border-radius: 12rpx; - background: #fff; - border: 2rpx solid var(--primary); - color: var(--primary); - font-size: 24rpx; - font-weight: 700; - text-align: center; -} -.ai-diet-meal-text { - flex: 1; - font-size: 28rpx; - line-height: 1.55; - color: #1b1c1a; -} -.ai-diet-tips { - display: block; - margin-top: 16rpx; - font-size: 26rpx; - line-height: 1.5; - color: var(--primary-container); -} -.ai-diet-avoid { - margin-top: 12rpx; - padding: 16rpx; - border-radius: 16rpx; - background: rgba(220, 38, 38, 0.06); - border: 1rpx solid rgba(220, 38, 38, 0.12); -} -.ai-diet-avoid-label { - font-size: 24rpx; - color: #b91c1c; - font-weight: 600; -} -.ai-diet-avoid-text { - font-size: 26rpx; - color: #991b1b; -} -.ai-diet-actions { - display: flex; - gap: 16rpx; - margin-top: 24rpx; -} -.ai-diet-btn { - flex: 1; - min-height: 80rpx; - display: flex; - align-items: center; - justify-content: center; - border-radius: 20rpx; - background: #fff; - border: 2rpx solid var(--primary); - color: var(--primary); - font-size: 28rpx; - font-weight: 700; -} -.ai-diet-btn.outline { - border-color: rgba(32, 78, 43, 0.25); - color: var(--primary-container); -} -.ai-diet-ask { - margin-top: 28rpx; - padding-top: 24rpx; - border-top: 1rpx solid rgba(32, 78, 43, 0.1); -} -.ai-diet-ask-title { - display: block; - font-size: 28rpx; - font-weight: 700; - color: var(--primary); - margin-bottom: 16rpx; -} -.ai-diet-ask-row { - display: flex; - gap: 12rpx; - align-items: center; -} -.ai-diet-ask-input { - flex: 1; - height: 80rpx; - padding: 0 24rpx; - border-radius: 16rpx; - background: #fff; - border: 2rpx solid rgba(32, 78, 43, 0.2); - font-size: 28rpx; - color: #1b1c1a; -} -.ai-diet-ask-btn { - flex-shrink: 0; - min-width: 128rpx; - height: 80rpx; - display: flex; - align-items: center; - justify-content: center; - border-radius: 16rpx; - background: #fff; - border: 2rpx solid var(--primary); - color: var(--primary); - font-size: 28rpx; - font-weight: 700; -} -.ai-diet-ask-btn.disabled { - opacity: 0.5; -} -.ai-diet-ask-result { - margin-top: 16rpx; - padding: 20rpx; - border-radius: 16rpx; - background: rgba(32, 78, 43, 0.04); - border: 1rpx solid rgba(32, 78, 43, 0.12); -} -.ai-diet-ask-badge { - display: inline-block; - padding: 4rpx 16rpx; - border-radius: 999rpx; - font-size: 22rpx; - font-weight: 700; - margin-bottom: 12rpx; - background: #fff; - border: 2rpx solid var(--primary); - color: var(--primary); -} -.ai-diet-ask-badge.lv-high { - border-color: #dc2626; - color: #dc2626; -} -.ai-diet-ask-badge.lv-medium { - border-color: #c77d0a; - color: #c77d0a; -} -.ai-diet-ask-advice { - display: block; - font-size: 28rpx; - line-height: 1.55; - color: #1b1c1a; -} -.ai-stream-cursor { - display: inline; - color: #386641; - animation: ai-cursor-blink 0.9s step-end infinite; -} -@keyframes ai-cursor-blink { - 50% { opacity: 0; } -} -.ai-diet-ask-portion { - display: block; - margin-top: 8rpx; - font-size: 24rpx; - color: var(--primary-container); -} -.ai-diet-rules { - display: block; - margin-top: 16rpx; - font-size: 24rpx; - color: var(--primary); - line-height: 1.45; - font-weight: 600; -} -.ai-diet-disclaimer { - display: block; - margin-top: 20rpx; - font-size: 22rpx; - color: #727970; - line-height: 1.4; -} .input-modal-ai-tip { margin-bottom: 20rpx; padding: 20rpx; diff --git a/admin/src/components/tcm-prescription/index.vue b/admin/src/components/tcm-prescription/index.vue index 4a6aba21..0b677816 100644 --- a/admin/src/components/tcm-prescription/index.vue +++ b/admin/src/components/tcm-prescription/index.vue @@ -518,6 +518,13 @@
+ + + +
已作废 已通过 @@ -597,49 +604,76 @@
-
药房联
+
{{ prescriptionTabType === 'internal' ? '药房联' : '处方联' }}
Rp.
用药 (单剂) - 总量 + {{ prescriptionTabType === 'user' ? '用量' : '总量' }}
用药 (单剂) - 总量 + {{ prescriptionTabType === 'user' ? '用量' : '总量' }}
-