From df65344d8f73e02b82fdb606002f325d0a2e3f1c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 3 Jun 2026 14:20:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TUICallKit-Vue3/tongji/pages/weekly.vue | 167 +++++++++- .../tongji/styles/weekly-stitch.scss | 309 +++++++++++++++++- .../tongji/utils/aiStreamRequest.js | 43 +++ server/app/api/controller/TcmController.php | 42 +++ server/app/api/logic/tcm/DailyDietAiLogic.php | 10 + server/app/api/logic/tcm/GiKnowledge.php | 64 ++++ 6 files changed, 619 insertions(+), 16 deletions(-) diff --git a/TUICallKit-Vue3/tongji/pages/weekly.vue b/TUICallKit-Vue3/tongji/pages/weekly.vue index b92012a8..f396e879 100644 --- a/TUICallKit-Vue3/tongji/pages/weekly.vue +++ b/TUICallKit-Vue3/tongji/pages/weekly.vue @@ -56,27 +56,55 @@ - + 血糖趋势 {{ rangeText }} · mmol/L - - - - - 空腹 + + + + + 空腹 + + + + 餐后 + - - 餐后 + + + + + {{ + bloodCareLoading + ? '生成中' + : (bloodCarePanelOpen ? '收起建议' : '照护建议') + }} + @@ -98,6 +126,88 @@ 示例数据,累计记录满 7 天后显示您的真实趋势 + + + + + AI 照护建议 + + + + {{ bloodCareLoading ? '生成中' : '换一换' }} + + + 收起 + + + + 基于您累计 7 天血糖 · 监测与中医调理方向(非处方) + + + {{ bloodCareControlLabel }} + + + + + + + + + + + 总评 + + + {{ bloodCarePlan.summary || '…' }} + + + + + 血糖管理 + + + {{ bloodCarePlan.blood_advice || '…' }} + + + + + 中医调理 + + + {{ bloodCarePlan.tcm_plan || '…' }} + + + + 留意 + + · + {{ item }} + + + + + 复诊提醒 + + + {{ bloodCarePlan.next_steps }} + + + + + + + {{ s.label }} + 记录 {{ s.record_days }} 天 · 达标 {{ s.compliance }}% + + + + {{ bloodCareDisclaimer }} + + @@ -109,6 +219,7 @@ + @@ -414,6 +525,7 @@ import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage import SugarTreeGraphic from '../components/SugarTreeGraphic.vue' import TongjiIcon from '../components/TongjiIcon.vue' import { useDietAi } from '../composables/useDietAi.js' +import { useBloodCareAi } from '../composables/useBloodCareAi.js' import { useSpeechToText } from '../composables/useSpeechToText.js' import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue' import DevTrainingEntry from '@/components/dev-training-entry/index.vue' @@ -924,6 +1036,27 @@ const chartHasDisplayData = computed(() => { return bloodHasData.value }) +const { + bloodCarePlan, + bloodCareLoading, + bloodCareStreamField, + bloodCareEligible, + bloodCarePanelOpen, + hasBloodCareContent, + bloodCareAnalysis, + bloodCareControlLabel, + bloodCareDisclaimer, + toggleBloodCareFromChart, + closeBloodCarePanel, + refreshBloodCareAi, + resetBloodCarePlan +} = useBloodCareAi(proxy, { + diagnosisId, + chartUseMockData, + showUserToast, + formatUserMessage +}) + const bloodHighDayCount = computed(() => { return chartDates.value.filter((date) => { const b = bloodByDate.value[date] @@ -1380,6 +1513,7 @@ async function onSelectCard(card) { exerciseRecords.value = [] trackingNotes.value = [] hoverInfo.value = null + resetBloodCarePlan() await applyCard(card) fetchAll() } @@ -1436,6 +1570,9 @@ async function fetchAll() { trackingNotes.value = [] } await fetchDietAiRecommend(false) + if (!bloodCareEligible.value) { + resetBloodCarePlan() + } } catch (e) { uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' }) } finally { diff --git a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss index c7d1160d..dd2240cc 100644 --- a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss +++ b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss @@ -244,11 +244,110 @@ } .weekly-page .st-chart-head { + display: flex; + flex-direction: column; + align-items: stretch; + margin-bottom: 12rpx; + gap: 12rpx; + padding: 0 20rpx; +} + +.weekly-page .st-chart-head-text { + width: 100%; +} + +/* 图例 + AI 胶囊同一行,避免大块图标压过标题 */ +.weekly-page .st-chart-toolbar { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 12rpx; gap: 16rpx; + width: 100%; +} + +.weekly-page .st-chart-ai-chip { + flex-shrink: 0; + min-height: 88rpx; + display: flex; + align-items: center; +} + +.weekly-page .st-chart-ai-chip-inner { + display: flex; + align-items: center; + gap: 10rpx; + min-height: 64rpx; + padding: 10rpx 22rpx 10rpx 12rpx; + border-radius: 999rpx; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(236, 253, 245, 0.92) 100%); + border: 1rpx solid rgba(0, 108, 73, 0.14); + box-shadow: + 0 2rpx 6rpx rgba(15, 23, 42, 0.04), + inset 0 1rpx 0 rgba(255, 255, 255, 0.9); + transition: + background 0.22s ease, + border-color 0.22s ease, + box-shadow 0.22s ease, + transform 0.15s ease; +} + +.weekly-page .st-chart-ai-chip-icon { + width: 44rpx; + height: 44rpx; + border-radius: 50%; + background: rgba(0, 108, 73, 0.08); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.weekly-page .st-chart-ai-chip-label { + font-size: 26rpx; + font-weight: 700; + color: #0f3d2e; + letter-spacing: 0.02em; + white-space: nowrap; +} + +.weekly-page .st-chart-ai-chip.hint .st-chart-ai-chip-inner { + border-color: rgba(0, 108, 73, 0.28); + box-shadow: + 0 2rpx 8rpx rgba(0, 108, 73, 0.1), + 0 0 0 2rpx rgba(78, 222, 163, 0.22); +} + +.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-inner { + background: linear-gradient(135deg, #1a8f5c 0%, #006c49 100%); + border-color: rgba(0, 108, 73, 0.35); + box-shadow: 0 6rpx 18rpx rgba(0, 108, 73, 0.2); +} + +.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-icon { + background: rgba(255, 255, 255, 0.18); +} + +.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-label { + color: #ffffff; +} + +.weekly-page .st-chart-ai-chip.loading .st-chart-ai-chip-inner { + opacity: 0.82; +} + +.weekly-page .st-chart-ai-chip:active .st-chart-ai-chip-inner { + transform: scale(0.98); + opacity: 0.92; +} + +@media (prefers-reduced-motion: reduce) { + .weekly-page .st-chart-ai-chip-inner { + transition: background 0.01s, border-color 0.01s, opacity 0.01s; + } + + .weekly-page .st-chart-ai-chip:active .st-chart-ai-chip-inner { + transform: none; + } } .weekly-page .st-chart-title { @@ -285,7 +384,107 @@ .weekly-page .st-chart-legend { display: flex; + flex-wrap: wrap; + align-items: center; gap: var(--st-inline-gap); + flex: 1; + min-width: 0; +} + +/* 趋势卡内展开的 AI 建议 */ +.weekly-page .st-chart-care-panel { + margin: 12rpx 20rpx 0; + padding: 28rpx 24rpx 24rpx; + border-radius: 28rpx; + background: rgba(255, 255, 255, 0.72); + border: 1rpx solid rgba(221, 228, 221, 0.65); + box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.95); +} + +.weekly-page .st-chart-care-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12rpx; + margin-bottom: 8rpx; +} + +.weekly-page .st-chart-care-title { + font-size: 30rpx; + font-weight: 700; + color: var(--on-surface); +} + +.weekly-page .st-chart-care-actions { + display: flex; + align-items: center; + gap: 12rpx; + flex-shrink: 0; +} + +.weekly-page .st-chart-care-refresh { + display: flex; + align-items: center; + gap: 6rpx; + padding: 10rpx 18rpx; + border-radius: 999rpx; + background: rgba(255, 255, 255, 0.7); + font-size: 22rpx; + font-weight: 600; + color: var(--primary); +} + +.weekly-page .st-chart-care-refresh.disabled { + opacity: 0.5; +} + +.weekly-page .st-chart-care-close { + min-height: 64rpx; + padding: 10rpx 18rpx; + display: flex; + align-items: center; + border-radius: 999rpx; + background: rgba(255, 255, 255, 0.55); + font-size: 22rpx; + font-weight: 600; + color: var(--on-surface-variant); +} + +.weekly-page .st-chart-care-close:active { + opacity: 0.75; +} + +.weekly-page .st-chart-care-sub { + display: block; + margin-bottom: 16rpx; + font-size: 24rpx; + line-height: 1.4; + color: var(--on-surface-variant); +} + +.weekly-page .st-care-badge--in-chart { + margin-bottom: 16rpx; +} + +.weekly-page .st-skeleton--in-chart { + padding: 8rpx 0; +} + +.weekly-page .st-care-cards--in-chart { + gap: 16rpx; +} + +.weekly-page .st-care-cards--in-chart .st-meal-glass { + padding: 24rpx; +} + +.weekly-page .st-care-stats--in-chart { + margin-top: 16rpx; +} + +.weekly-page .st-care-disclaimer--in-chart { + margin-top: 16rpx; + font-size: 22rpx; } .weekly-page .st-legend-item { @@ -693,6 +892,114 @@ text-align: center; } +.weekly-page .st-skeleton-line.short { + width: 55%; +} + +/* ===== AI 血糖照护(嵌在趋势卡内) ===== */ +.weekly-page .st-care-badge { + position: relative; + z-index: 1; + display: inline-flex; + margin-bottom: 24rpx; + padding: 8rpx 20rpx; + border-radius: 999rpx; + background: rgba(255, 255, 255, 0.65); + font-size: 24rpx; + font-weight: 600; + color: var(--primary); +} + +.weekly-page .st-care-cards { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.weekly-page .st-care-block--tcm { + background: rgba(254, 243, 199, 0.55); + border-color: rgba(217, 119, 6, 0.12); +} + +.weekly-page .st-care-block--next { + background: rgba(224, 242, 254, 0.55); + border-color: rgba(14, 165, 233, 0.12); +} + +.weekly-page .st-care-watch { + padding: 24rpx 32rpx; + border-radius: var(--st-radius-2xl); + background: rgba(255, 255, 255, 0.75); + border: 1rpx solid rgba(255, 255, 255, 0.9); +} + +.weekly-page .st-care-watch-title { + display: block; + margin-bottom: 12rpx; + font-size: 28rpx; + font-weight: 700; + color: var(--on-surface); +} + +.weekly-page .st-care-watch-item { + display: flex; + align-items: flex-start; + gap: 8rpx; + margin-bottom: 8rpx; +} + +.weekly-page .st-care-watch-dot { + font-size: 32rpx; + line-height: 1.2; + color: var(--primary); +} + +.weekly-page .st-care-watch-text { + flex: 1; + font-size: 28rpx; + line-height: 1.43; + color: var(--on-surface-variant); +} + +.weekly-page .st-care-stats { + position: relative; + z-index: 1; + display: flex; + flex-wrap: wrap; + gap: 12rpx; + margin-top: 20rpx; +} + +.weekly-page .st-care-stat-chip { + padding: 12rpx 20rpx; + border-radius: 16rpx; + background: rgba(255, 255, 255, 0.55); +} + +.weekly-page .st-care-stat-label { + display: block; + font-size: 22rpx; + color: var(--outline); +} + +.weekly-page .st-care-stat-val { + font-size: 24rpx; + font-weight: 600; + color: var(--on-surface-variant); +} + +.weekly-page .st-care-disclaimer { + position: relative; + z-index: 1; + display: block; + margin-top: 20rpx; + font-size: 24rpx; + line-height: 1.4; + color: var(--outline); +} + /* ===== 底部浮动输入 ===== */ .weekly-page .st-float-ask { position: fixed; diff --git a/TUICallKit-Vue3/tongji/utils/aiStreamRequest.js b/TUICallKit-Vue3/tongji/utils/aiStreamRequest.js index 414861bd..ea63314d 100644 --- a/TUICallKit-Vue3/tongji/utils/aiStreamRequest.js +++ b/TUICallKit-Vue3/tongji/utils/aiStreamRequest.js @@ -90,6 +90,49 @@ export function parsePartialDietPlainText(text) { return out } +/** 从流式纯文本中提取血糖照护建议(支持未写完的最后一行) */ +export function parsePartialBloodCarePlainText(text) { + const out = {} + if (!text) return out + + const lineRe = /^(总评|血糖|中医|留意|复诊)[::]\s*(.*)$/gm + let match + while ((match = lineRe.exec(text)) !== null) { + const key = { + '总评': 'summary', + '血糖': 'blood_advice', + '中医': 'tcm_plan', + '留意': 'watch_points', + '复诊': 'next_steps' + }[match[1]] + const val = (match[2] || '').trim() + if (key === 'watch_points') { + out.watch_points = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean) + } else { + out[key] = val + } + } + + const tail = text.match(/(?:^|\n)(总评|血糖|中医|留意|复诊)[::]\s*([^\n]*)$/) + if (tail) { + const key = { + '总评': 'summary', + '血糖': 'blood_advice', + '中医': 'tcm_plan', + '留意': 'watch_points', + '复诊': 'next_steps' + }[tail[1]] + const val = (tail[2] || '').trim() + if (key === 'watch_points') { + if (val) out.watch_points = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean) + } else { + out[key] = val + } + } + + return out +} + /** @deprecated 使用 parsePartialDietPlainText */ export function parsePartialDietJson(text) { return parsePartialDietPlainText(text) diff --git a/server/app/api/controller/TcmController.php b/server/app/api/controller/TcmController.php index 9306658d..897ee098 100755 --- a/server/app/api/controller/TcmController.php +++ b/server/app/api/controller/TcmController.php @@ -21,6 +21,7 @@ use app\api\logic\tcm\DailyFamilyLikeLogic; use app\api\logic\tcm\DailyShareLogic; use app\api\logic\tcm\DailyPhoneLogic; use app\api\logic\tcm\DailyDietAiLogic; +use app\api\logic\tcm\DailyBloodCareAiLogic; use app\adminapi\logic\ConfigLogic; use app\common\model\tcm\Diagnosis; use app\common\model\tcm\BloodRecord; @@ -929,6 +930,47 @@ class TcmController extends BaseApiController }); } + /** + * @notes 日常记录-AI 血糖照护建议(累计 7 天有记录) + * + * 路由:GET /api/tcm/dailyBloodCareAiRecommend?diagnosis_id=:id&refresh=0|1 + */ + public function dailyBloodCareAiRecommend() + { + $diagnosisId = (int) $this->request->get('diagnosis_id', 0); + $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); + if (!$check['ok']) { + return $this->fail($check['error']); + } + + $refresh = (int) $this->request->get('refresh', 0) === 1; + $result = DailyBloodCareAiLogic::getDailyCarePlan($diagnosisId, $refresh); + if (empty($result['ok'])) { + return $this->fail($result['error'] ?? '获取照护建议失败'); + } + + return $this->data($result['data']); + } + + /** + * @notes 日常记录-AI 血糖照护建议(SSE 流式) + * + * 路由:GET /api/tcm/dailyBloodCareAiRecommendStream?diagnosis_id=:id&refresh=0|1 + */ + public function dailyBloodCareAiRecommendStream() + { + $diagnosisId = (int) $this->request->get('diagnosis_id', 0); + $check = $this->ensurePatientOwnsDiagnosis($diagnosisId); + if (!$check['ok']) { + return $this->fail($check['error']); + } + + $refresh = (int) $this->request->get('refresh', 0) === 1; + $this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $refresh): void { + DailyBloodCareAiLogic::streamDailyCarePlan($diagnosisId, $refresh, $emit); + }); + } + /** * @param callable(callable(string, array):void):void $runner */ diff --git a/server/app/api/logic/tcm/DailyDietAiLogic.php b/server/app/api/logic/tcm/DailyDietAiLogic.php index d331d896..4771e280 100644 --- a/server/app/api/logic/tcm/DailyDietAiLogic.php +++ b/server/app/api/logic/tcm/DailyDietAiLogic.php @@ -273,6 +273,16 @@ class DailyDietAiLogic return $idx; } + /** + * 供血糖照护 AI 等模块复用:患者档案 + 近 7/30 天血糖统计 + * + * @return array{ok:bool,error?:string,data?:array} + */ + public static function getPatientContext(int $diagnosisId): array + { + return self::buildPatientContext($diagnosisId); + } + /** * @return array{ok:bool,error?:string,data?:array} */ diff --git a/server/app/api/logic/tcm/GiKnowledge.php b/server/app/api/logic/tcm/GiKnowledge.php index b999eb15..33b75b36 100644 --- a/server/app/api/logic/tcm/GiKnowledge.php +++ b/server/app/api/logic/tcm/GiKnowledge.php @@ -320,4 +320,68 @@ class GiKnowledge $q = trim($q); return $q !== '' ? $q : $question; } + + /** + * 血糖照护 AI 关闭时的规则化方案(监测 + 中医调理方向) + * + * @param array $stats7 + * @param array $stats30 + * @param array $extra syndrome_type, symptoms 等 + * @return array + */ + public static function fallbackBloodCarePlan(array $stats7 = [], array $stats30 = [], array $extra = []): array + { + $exercise = self::exercisePlan($stats7, $stats30); + $level = (string) ($exercise['level'] ?? 'unknown'); + $compliance = (int) (($stats30['record_days'] ?? 0) > 0 ? ($stats30['compliance'] ?? 0) : ($stats7['compliance'] ?? 0)); + $syndrome = trim((string) ($extra['syndrome_type'] ?? '')); + $symptoms = trim((string) ($extra['symptoms'] ?? '')); + + $summary = match ($level) { + 'high' => '近阶段血糖偏高天数偏多,要把监测、饮食和运动一起抓紧,别只靠感觉。', + 'medium' => '血糖大体可控,但还有波动,坚持记录和餐后活动,能更稳。', + 'good' => '近期血糖控制不错,保持现在的节奏,定期复查别松懈。', + default => '先坚持记血糖、看趋势,再按下面几条慢慢调整生活方式。', + }; + + $bloodAdvice = match ($level) { + 'high' => '空腹、餐后都测,偏高那天记下吃了啥、动了没。主食换杂粮、每餐先吃菜和蛋白,甜饮糕点先停。三餐后按上面运动建议动起来,头晕心慌马上停、吃点糖。', + 'medium' => '每周至少 4 天空腹+餐后配对记录。主食定量、晚餐别过饱,少喝粥配咸菜。餐后散步 20 分钟,睡眠规律,情绪紧张也会抬糖。', + 'good' => '保持每周 3–4 次监测即可,注意节假日和串门聚餐别放松。继续低 GI 家常饭,运动别停,体重稳一点更好。', + default => '先把血糖记满 7 天再看趋势。吃饭定时定量,多喝白开水,少甜少油,饭后多走动。', + }; + + $tcmBase = '消渴病多与肺脾肾气虚、阴虚燥热或痰湿瘀血相关,需结合面诊脉象。'; + if ($syndrome !== '') { + $tcmBase = '就诊辨证倾向「' . $syndrome . '」。'; + } elseif ($symptoms !== '') { + $tcmBase = '结合您登记的症状(' . mb_substr($symptoms, 0, 40) . '…),宜辨证论治。'; + } + + $tcmPlan = match ($level) { + 'high' => $tcmBase . '调理思路:益气养阴、清热生津为主,可配合健脾化湿;食疗如山药、麦冬、桑叶代茶饮方向;艾灸可选足三里、胰俞等穴(需医师指导)。勿自行加大西药或乱用偏方,务必复诊调方。', + 'medium' => $tcmBase . '调理思路:健脾益气、滋阴润燥,少食肥甘,宜清淡易消化;可配合八段锦、太极拳;中药汤剂需医师面诊后开具,此处不给具体方名剂量。', + 'good' => $tcmBase . '调理思路:顾护脾胃、养阴防燥,季节更替注意保暖;维持规律作息与情志舒畅,按医嘱定期复诊即可。', + default => $tcmBase . '建议到中医科面诊,明确证型后再定汤药、针灸方案;平时可饮食清淡、作息规律,配合记录血糖供医师参考。', + }; + + $watchPoints = match ($level) { + 'high' => ['空腹≥7 或餐后明显升高要联系村医', '脚麻脚痛、视力模糊及时就诊', '用药时间饭点别乱改'], + 'medium' => ['外出走亲也别忘了测糖', '感冒发烧会让血糖乱,多监测', '睡前别加餐太多'], + 'good' => ['每 3–6 个月复查糖化血红蛋白', '血压血脂一并管', '家人提醒别擅自停药'], + default => ['坚持每日或隔日记录', '记录与饮食运动一起给大夫看', '不适及时线下就诊'], + }; + + return [ + 'summary' => $summary, + 'blood_advice' => $bloodAdvice, + 'tcm_plan' => $tcmPlan, + 'watch_points' => $watchPoints, + 'next_steps' => '下周继续记血糖;饮食可参考本页「AI 饮食建议」;若连续 3 天偏高或出现胸闷脚肿,请尽快线下就诊或视频问诊。', + 'control_level' => $level, + 'control_label' => (string) ($exercise['level_label'] ?? ''), + 'disclaimer' => '本建议供日常参考,不能替代医师面诊、开方与调药。', + 'source' => 'rule', + ]; + } }