夜雨聆风学习资料网

ARTICLE · 1194033

【已开源】糖护智伴·AI私人监控系统(Vue3)

【已开源】糖护智伴·AI私人监控系统(Vue3)

糖护智伴·AI私人监控系统

基于 Vue 3、Element Plus、Pinia、ECharts、Axios 和 Express 的前后端一体化项目

功能模块

  • 健康首页:健康评分、关键指标、血糖趋势、今日任务、AI 洞察
  • 血糖监测:趋势分析、达标率、异常提醒、新增记录
  • 智能饮食:热量与营养素分析、低 GI 食谱、饮食记录
  • 用药管理:用药时间表、服药打卡、依从性与安全提醒
  • 运动指导:周目标、运动效果、AI 推荐与记录
  • 健康报告:综合评分、行为表现、医生结论、打印导出
  • AI 问诊:结合模拟健康档案的上下文问答
  • 数据中心:多维 ECharts 健康数据可视化
  • 医患协作:医生档案、在线沟通、数据共享、家庭关怀

环境搭建

  • 前端 Vue3 + Vite + Vue-Router + Pinia + ECharts + Axios + ElementPlus + Dayjs
  • 后端 Nodejs + Experss + Cors + concurrently
文末领取

构建前端环境

npm create vue@latestnpm install axiosnpm install element-plus --savenpm install -D unplugin-vue-components unplugin-auto-importnpm install @element-plus/icons-vuenpm install echartsnpm install dayjs
import { defineConfig } from 'vite'import AutoImport from 'unplugin-auto-import/vite'import Components from 'unplugin-vue-components/vite'import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'export default defineConfig({  // ...  plugins: [    // ...    AutoImport({      resolvers: [ElementPlusResolver()],    }),    Components({      resolvers: [ElementPlusResolver()],    }),  ],})

构建后端环境

npm install expressnpm install corsnpm install --save-dev concurrently
import express from 'express'import cors from 'cors'const app = express()app.use(cors())app.use(express.json())const patients = [  { id: 1, name: '张伟', age: 55, gender: '男', diabetes_type: '2型', diagnosis_date: '2020-03-15', height: 172, weight: 78, phone: '13800138001', medical_history: '高血压5年,血脂偏高' },  { id: 2, name: '李芳', age: 42, gender: '女', diabetes_type: '2型', diagnosis_date: '2022-08-20', height: 160, weight: 62, phone: '13800138002', medical_history: '无其他慢性病' },  { id: 3, name: '王强', age: 68, gender: '男', diabetes_type: '2型', diagnosis_date: '2018-01-10', height: 170, weight: 82, phone: '13800138003', medical_history: '冠心病,糖尿病肾病早期' },  { id: 4, name: '赵敏', age: 35, gender: '女', diabetes_type: '1型', diagnosis_date: '2015-06-05', height: 165, weight: 55, phone: '13800138004', medical_history: '1型糖尿病,使用胰岛素泵' },  { id: 5, name: '陈刚', age: 50, gender: '男', diabetes_type: '2型', diagnosis_date: '2021-11-30', height: 178, weight: 90, phone: '13800138005', medical_history: '肥胖,脂肪肝' }]let bloodSugar = [  [1, 6.8, '空腹', '2026-04-01 07:00:00', '晨起空腹'], [1, 9.2, '餐后2h', '2026-04-01 09:30:00', '早餐后'],  [1, 6.5, '空腹', '2026-04-02 07:00:00', ''], [1, 8.8, '餐后2h', '2026-04-02 09:30:00', '杂粮馒头'],  [1, 7.1, '空腹', '2026-04-03 07:00:00', ''], [1, 10.5, '餐后2h', '2026-04-03 12:30:00', '米饭较多'],  [1, 5.9, '空腹', '2026-04-04 07:00:00', ''], [1, 8.2, '餐后2h', '2026-04-04 12:30:00', ''],  [1, 6.3, '睡前', '2026-04-04 22:00:00', ''], [1, 6.2, '空腹', '2026-04-05 07:00:00', '控制不错']].map((r, i) => ({ id: i + 1, patient_id: r[0], value: r[1], measure_type: r[2], measured_at: r[3], note: r[4] }))let dietRecords = [  ['燕麦粥', 150, 27, 5, 3, 55, 1, '早餐', '2026-04-01 07:30:00'],  ['水煮鸡蛋', 78, 0.6, 6, 5, 0, 1, '早餐', '2026-04-01 07:30:00'],  ['清炒西兰花', 55, 6, 4, 2, 15, 1, '午餐', '2026-04-01 12:00:00'],  ['糙米饭', 220, 46, 5, 2, 56, 1, '午餐', '2026-04-01 12:00:00'],  ['红烧豆腐', 120, 5, 12, 6, 15, 1, '午餐', '2026-04-01 12:00:00']].map((r, i) => ({ id: i + 1, patient_id: 1, food_name: r[0], calories: r[1], carbs: r[2], protein: r[3], fat: r[4], gi_value: r[5], portion: r[6], meal_type: r[7], eaten_at: r[8] }))let medications = [  [1, '二甲双胍', '口服药', '500mg', '每日2次', '餐后服用', '2020-04-01'],  [1, '格列美脲', '口服药', '2mg', '每日1次', '早餐前30分钟', '2021-06-15'],  [1, '阿托伐他汀', '口服药', '20mg', '每日1次', '睡前', '2020-05-01'],  [2, '二甲双胍', '口服药', '850mg', '每日2次', '餐后', '2022-09-01'],  [4, '门冬胰岛素', '胰岛素', '按需', '每日3次', '餐前', '2015-07-01'],  [4, '甘精胰岛素', '胰岛素', '18U', '每日1次', '睡前', '2015-07-01']].map((r, i) => ({ id: i + 1, patient_id: r[0], drug_name: r[1], drug_type: r[2], dosage: r[3], frequency: r[4], timing: r[5], start_date: r[6], is_active: true, takenToday: i === 1 }))let exerciseRecords = [  [1, '快走', 30, '中等', 150, '2026-04-01 18:00:00', 8.5, 6.8],  [1, '太极拳', 45, '低', 120, '2026-04-02 06:30:00', 6.5, 5.8],  [1, '骑自行车', 40, '中等', 200, '2026-04-03 17:30:00', 9, 7.2],  [2, '瑜伽', 60, '低', 180, '2026-04-01 09:00:00', 7, 6.2]].map((r, i) => ({ id: i + 1, patient_id: r[0], exercise_type: r[1], duration_min: r[2], intensity: r[3], calories_burned: r[4], performed_at: r[5], blood_sugar_before: r[6], blood_sugar_after: r[7] }))const plans = [  { meal: '早餐', time: '07:30', foods: '无糖酸奶 + 全麦面包 + 鸡蛋', kcal: 380, tag: '低GI' },  { meal: '午餐', time: '12:00', foods: '糙米饭 + 清蒸鱼 + 时蔬', kcal: 560, tag: '高纤维' },  { meal: '加餐', time: '15:30', foods: '原味坚果 15g', kcal: 90, tag: '控量' },  { meal: '晚餐', time: '18:30', foods: '荞麦面 + 鸡胸肉 + 菌菇', kcal: 470, tag: '均衡' }]const getPatientData = (id) => {  const patient = patients.find(p => p.id === Number(id)) || patients[0]  const glucose = bloodSugar.filter(x => x.patient_id === patient.id)  const avg = glucose.length ? glucose.reduce((a, b) => a + b.value, 0) / glucose.length : 0  return { patient, glucose, avg: Math.round((avg + Number.EPSILON) * 10) / 10 }}app.get('/api/health', (_, res) => res.json({ status: 'ok', time: new Date().toISOString() }))app.get('/api/patients', (_, res) => res.json(patients))app.get('/api/dashboard/:id', (req, res) => {  const { patient, glucose, avg } = getPatientData(req.params.id)  res.json({    patient, glucose, avg,    metrics: { hba1c: 6.7, targetRate: 82, bmi: +(patient.weight / ((patient.height / 100) ** 2)).toFixed(1), streak: 12 },    todayTasks: [      { id: 1, time: '07:00', title: '空腹血糖测量', done: true, type: '血糖' },      { id: 2, time: '07:30', title: '格列美脲 2mg', done: true, type: '用药' },      { id: 3, time: '12:30', title: '午餐后步行 15 分钟', done: false, type: '运动' },      { id: 4, time: '18:30', title: '二甲双胍 500mg', done: false, type: '用药' }    ],    insight: '过去 5 天空腹血糖整体平稳;4 月 3 日午餐后升高明显,与较多米饭摄入高度相关。建议午餐主食减少约 1/4,并在餐后进行 15 分钟快走。'  })})app.get('/api/blood-sugar/:id', (req, res) => res.json(bloodSugar.filter(x => x.patient_id === Number(req.params.id))))app.post('/api/blood-sugar', (req, res) => {  const item = { id: Date.now(), patient_id: 1, ...req.body }  bloodSugar.push(item); res.status(201).json(item)})app.get('/api/diet/:id', (req, res) => res.json({ records: dietRecords.filter(x => x.patient_id === Number(req.params.id)), plans }))app.post('/api/diet', (req, res) => { const item = { id: Date.now(), patient_id: 1, ...req.body }; dietRecords.push(item); res.status(201).json(item) })app.get('/api/medications/:id', (req, res) => res.json(medications.filter(x => x.patient_id === Number(req.params.id))))app.patch('/api/medications/:id/take', (req, res) => { const item = medications.find(x => x.id === Number(req.params.id)); if (item) item.takenToday = !item.takenToday; res.json(item) })app.get('/api/exercises/:id', (req, res) => res.json(exerciseRecords.filter(x => x.patient_id === Number(req.params.id))))app.post('/api/exercises', (req, res) => { const item = { id: Date.now(), patient_id: 1, ...req.body }; exerciseRecords.push(item); res.status(201).json(item) })app.get('/api/report/:id', (req, res) => {  const { patient, avg } = getPatientData(req.params.id)  res.json({ patient, period: '2026年4月1日 — 4月7日', score: 86, avg, hba1c: 6.7, targetRate: 82, lowCount: 0, highCount: 2, medicationRate: 92, exerciseMinutes: 115, calories: 470, doctorNote: '整体控制良好,继续保持规律监测。重点关注午餐后血糖,三个月后复查糖化血红蛋白。' })})app.get('/api/collaboration/:id', (_, res) => res.json({  doctor: { name: '刘颖', title: '内分泌科 副主任医师', hospital: '市中心医院', avatar: '刘' },  nextVisit: '2026-04-18 09:30',  messages: [    { from: 'doctor', name: '刘医生', time: '昨天 16:42', text: '张先生,最近空腹血糖控制得不错,请继续保持。午餐主食注意减量。' },    { from: 'patient', name: '我', time: '昨天 18:05', text: '好的医生,我已经开始把米饭换成糙米,也会坚持饭后走一走。' }  ]}))app.post('/api/ai/chat', (req, res) => {  const q = req.body.message || ''  let answer = '根据您近期的健康数据,血糖整体处于较稳定状态。建议保持规律饮食、按时用药,并继续记录血糖变化。'  if (q.includes('吃') || q.includes('饮食')) answer = '今天的碳水摄入略偏高。晚餐建议选择荞麦面或少量糙米,搭配一掌心优质蛋白和两拳蔬菜,避免粥、甜饮和夜宵。'  if (q.includes('血糖')) answer = '您最近平均血糖约 7.6 mmol/L,达标率 82%。4 月 3 日餐后出现 10.5 mmol/L,可能与米饭摄入较多有关;若连续多次超过 10 mmol/L,请联系医生评估。'  if (q.includes('运动')) answer = '结合您的年龄和近期数据,推荐餐后 30 分钟开始快走 15–30 分钟。运动前低于 5.6 mmol/L 时可先少量加餐;若出现心慌、冷汗或头晕请立即停止。'  setTimeout(() => res.json({ answer, disclaimer: 'AI 建议仅用于健康管理参考,不能替代医生诊断与处方。' }), 350)})app.listen(3001, () => console.log('Mock API running at http://localhost:3001'))
import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'export default defineConfig({  plugins: [vue()],  server: {    port: 5173,    proxy: { '/api': 'http://localhost:3001' }  }})

运行配置

”scripts”: {    ”dev”: ”concurrently -k -n API,WEB -c yellow,cyan \”npm:server\” \”npm:client\””,    ”client”: ”vite --host 0.0.0.0”,    ”server”: ”node server/server.js”,    ”build”: ”vite build”,    ”preview”: ”vite preview --host 0.0.0.0”}
-k: 参数,表示kill other processes when one exits-n API,WEB: 给进程命名,用逗号分隔,这里有两个名字API和WEB-c yellow,cyan: 设置颜色,对应命名进程的输出颜色,yellow给API,cyan给WEB。

实战篇_糖护智伴 · 侧边栏

公共样式配置

:root{--green:#13a884;--green-dark:#0b3b36;--green-soft:#e8f7f2;--ink:#173c37;--muted:#7d918d;--line:#e7eeec;--bg:#f4f7f6;--amber:#edaa48;--blue:#5d88ef;--purple:#9475e8;--el-color-primary:#13a884;--el-color-success:#13a884;--el-border-radius-base:9px;font-family:'DM Sans','Noto Sans SC',sans-serif;color:var(--ink);background:var(--bg);font-synthesis:none}*{box-sizing:border-box}html,body,#app{margin:0;min-height:100%;background:var(--bg)}button,input{font:inherit}button{cursor:pointer}.app-shell{min-height:100vh}.sidebar{width:248px;height:100vh;position:fixed;z-index:30;inset:0 auto 0 0;background:#0b3934;color:white;padding:0 17px;display:flex;flex-direction:column;transition:.25s;box-shadow:8px 0 34px rgba(6,39,35,.07)}.sidebar.collapsed{width:82px}.brand{height:86px;display:flex;align-items:center;gap:12px;padding:0 8px;cursor:pointer}.brand-mark{width:39px;height:39px;border:1px solid rgba(255,255,255,.35);border-radius:13px;display:grid;place-items:center;background:linear-gradient(145deg,#22cda1,#0e906f);box-shadow:0 6px 14px rgba(9,29,27,.3);flex:none}.brand-mark span{font-size:28px;font-weight:300;line-height:1}.brand-copy{display:flex;flex-direction:column;white-space:nowrap}.brand-copy b{font-size:18px;letter-spacing:1px}.brand-copy small{font-size:8px;letter-spacing:2.1px;color:#83b7ac;margin-top:2px}.side-section{font-size:10px;letter-spacing:1.5px;color:#729e96;margin:12px 12px 8px;text-transform:uppercase}.sidebar nav{display:flex;flex-direction:column;gap:5px}.sidebar nav button{height:47px;border:0;background:transparent;color:#b2cbc6;border-radius:11px;display:flex;align-items:center;gap:13px;padding:0 14px;text-align:left;position:relative;transition:.2s}.sidebar nav button .el-icon{font-size:19px;flex:none}.sidebar nav button span{font-size:14px;white-space:nowrap}.sidebar nav button em{font-size:9px;font-style:normal;background:#35caa6;color:#072f2a;border-radius:5px;padding:2px 5px;margin-left:auto}.sidebar nav button:hover{background:rgba(255,255,255,.06);color:#fff}.sidebar nav button.active{background:linear-gradient(90deg,rgba(36,185,148,.27),rgba(36,185,148,.1));color:#fff}.sidebar nav button.active:before{content:””;position:absolute;left:-17px;top:11px;width:3px;height:25px;border-radius:0 4px 4px 0;background:#2bcea4}.care-card{margin:auto 3px 20px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.09);border-radius:14px;padding:15px;display:grid;grid-template-columns:auto 1fr;gap:9px}.care-card>.el-icon{font-size:22px;color:#4bd6b3}.care-card div{display:flex;flex-direction:column}.care-card b{font-size:12px}.care-card small{font-size:10px;color:#83a8a0;margin-top:3px}.care-card button{grid-column:1/3;margin-top:5px;height:29px;border:0;border-radius:7px;background:rgba(43,206,164,.15);color:#68ddbf;font-size:11px}.collapse-btn{position:absolute;right:-11px;bottom:33px;border:1px solid #dce7e4;background:#fff;color:#45706a;border-radius:50%;width:24px;height:24px;display:grid;place-items:center;padding:0}.main{padding-left:248px;min-height:100vh;transition:.25s}.main.wide{padding-left:82px}.topbar{height:74px;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 32px;position:sticky;top:0;z-index:20;backdrop-filter:blur(10px)}.breadcrumb{display:flex;align-items:center;gap:12px;font-size:13px}.breadcrumb span,.breadcrumb i{color:#9aaba7;font-style:normal}.breadcrumb b{font-weight:600}.top-actions{display:flex;align-items:center;gap:18px}.sync{font-size:11px;color:#6a827d;display:flex;gap:7px;align-items:center}.sync i,.ai-online i,.online-text i{display:block;width:7px;height:7px;background:#21bc91;border-radius:50%;box-shadow:0 0 0 4px rgba(33,188,145,.12)}.icon-btn{position:relative;border:0;background:#f2f6f5;width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:#45635e}.icon-btn .dot{position:absolute;right:5px;top:4px;width:6px;height:6px;background:#f06d65;border:2px solid white;border-radius:50%}.profile{display:flex;align-items:center;gap:9px;cursor:pointer;outline:none}.avatar{width:37px;height:37px;display:grid;place-items:center;border-radius:12px;background:linear-gradient(145deg,#b7e5d7,#65c3a9);color:#0c5547;font-weight:700}.profile-copy{display:flex;flex-direction:column;min-width:90px}.profile-copy b{font-size:13px}.profile-copy small{font-size:10px;color:#879a96;margin-top:2px}.profile>.el-icon{font-size:11px}.page-wrap{padding:29px 32px 50px;max-width:1600px;margin:auto}.page{animation:pageIn .35s ease}@keyframes pageIn{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}.page-title{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:25px}.page-title h1{font-size:27px;letter-spacing:-.7px;margin:4px 0 5px}.page-title p{margin:0;color:var(--muted);font-size:13px}.eyebrow,.panel-kicker{font-size:9px;letter-spacing:2px;font-weight:700;color:#58d4b5}.eyebrow.dark,.panel-kicker{color:#7b9e96}.panel{background:#fff;border:1px solid var(--line);border-radius:16px;padding:22px;box-shadow:0 7px 25px rgba(24,61,55,.035)}.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.panel-head h3{margin:3px 0 0;font-size:16px;letter-spacing:-.2px}.panel-head p{margin:4px 0 0}.subtle{color:#91a19e;font-size:11px}.el-button{font-weight:600}.el-button--large{padding-left:20px;padding-right:20px}.text-btn{border:0;background:transparent;color:var(--green);display:flex;align-items:center;gap:4px;font-size:12px}.content-grid{display:grid;gap:18px}.two-col{display:grid;grid-template-columns:2fr 1fr;gap:18px}.green-text{color:var(--green)!important}.amber-text{color:var(--amber)!important}/* Home */.welcome-banner{height:218px;border-radius:19px;background:radial-gradient(circle at 85% 35%,rgba(47,199,162,.25),transparent 27%),linear-gradient(118deg,#0c403a,#116b5b);color:white;padding:35px 42px;display:flex;justify-content:space-between;align-items:center;overflow:hidden;position:relative;box-shadow:0 12px 30px rgba(12,64,58,.15)}.welcome-banner:after{content:””;position:absolute;right:-70px;top:-155px;width:390px;height:390px;border:1px solid rgba(255,255,255,.08);border-radius:50%}.welcome-copy{position:relative;z-index:2}.welcome-copy h1{font-size:29px;margin:8px 0}.welcome-copy h1 span{font-size:24px}.welcome-copy>p{font-size:13px;color:#b8d8d1;margin:0 0 20px}.banner-status{display:flex;gap:25px;font-size:11px}.banner-status span{display:flex;align-items:center;gap:6px;color:#c7dfda}.banner-status i{width:7px;height:7px;border-radius:50%;background:#50ddb7}.banner-status b{color:white}.banner-orbit{position:relative;width:250px;height:180px;z-index:2}.orbit-ring{position:absolute;right:35px;top:9px;width:150px;height:150px;border:1px solid rgba(255,255,255,.23);border-radius:50%;display:grid;place-items:center;box-shadow:0 0 0 24px rgba(255,255,255,.025),0 0 0 50px rgba(255,255,255,.018)}.health-score{width:105px;height:105px;border-radius:50%;background:rgba(10,55,50,.65);border:4px solid #47d3ad;display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 22px rgba(64,211,173,.15)}.health-score strong{font-size:30px}.health-score small{font-size:10px;color:#add8ce}.orbit-tag{position:absolute;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.11);backdrop-filter:blur(8px);border-radius:20px;font-size:10px;padding:6px 10px}.orbit-tag.one{right:0;top:15px}.orbit-tag.two{left:4px;bottom:20px}.metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:17px;margin:18px 0}.metric-card{background:#fff;border:1px solid var(--line);border-radius:15px;padding:17px 18px;position:relative;overflow:hidden}.metric-card:after{content:””;position:absolute;right:-25px;top:-25px;width:80px;height:80px;border-radius:50%;background:var(--green-soft)}.metric-top{display:flex;justify-content:space-between;align-items:center;position:relative;z-index:1}.metric-icon{width:35px;height:35px;border-radius:10px;background:var(--green-soft);color:var(--green);display:grid;place-items:center;font-size:17px}.metric-trend{font-size:9px;color:#6f8983;background:#f3f6f5;border-radius:12px;padding:4px 7px}.metric-value{font-size:25px;font-weight:700;margin-top:13px;line-height:1}.metric-value small{font-size:10px;color:#8ba09b;font-weight:500}.metric-label{font-size:11px;color:#7d918d;margin-top:8px}.tone-blue .metric-icon,.tone-blue:after{background:#edf3ff}.tone-blue .metric-icon{color:var(--blue)}.tone-amber .metric-icon,.tone-amber:after{background:#fff6e8}.tone-amber .metric-icon{color:var(--amber)}.tone-purple .metric-icon,.tone-purple:after{background:#f3efff}.tone-purple .metric-icon{color:var(--purple)}.main-grid{grid-template-columns:1.7fr 1fr}.chart-legend{display:flex;align-items:center;gap:15px;font-size:10px;color:#718681;margin:8px}.legend-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px}.legend-dot.green{background:var(--green)}.target-pill{background:#f1f6f4;border-radius:10px;padding:3px 8px}.progress-ring{width:40px;height:40px;border:4px solid #dceee9;border-top-color:var(--green);border-radius:50%;display:grid;place-items:center;font-size:10px;font-weight:700}.task-list{display:flex;flex-direction:column;margin-top:5px}.task-item{display:flex;align-items:center;gap:12px;padding:13px 2px;border-bottom:1px solid #edf1f0}.task-item:last-child{border:0}.task-check{width:22px;height:22px;border:1px solid #cad8d5;border-radius:7px;display:grid;place-items:center;color:white}.task-item.done .task-check{background:var(--green);border-color:var(--green)}.task-item>div{display:flex;flex-direction:column;flex:1}.task-item b{font-size:12px}.task-item.done b{text-decoration:line-through;color:#8ca09c}.task-item small{font-size:9px;color:#94a4a1;margin-top:3px}.task-arrow{color:#abb9b6}.lower-grid{grid-template-columns:1.5fr 1fr;margin-top:18px}.ai-insight{border-radius:16px;padding:25px 28px;display:flex;gap:18px;background:linear-gradient(120deg,#e6f7f2,#f6fbf9);border:1px solid #cfe9e2}.ai-spark{width:48px;height:48px;border-radius:14px;display:grid;place-items:center;background:#fff;color:var(--green);font-size:22px;box-shadow:0 7px 20px rgba(19,168,132,.12);flex:none}.ai-insight h3{margin:4px 0 7px;font-size:16px}.ai-insight p{font-size:11px;line-height:1.8;color:#54716b;margin:0}.ai-insight button{border:0;background:none;color:#0b8c6d;font-size:11px;font-weight:700;padding:10px 0 0;display:flex;align-items:center;gap:4px}.quick-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.quick-actions button{border:0;background:transparent;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ink)}.quick-actions span{width:39px;height:39px;border-radius:12px;display:grid;place-items:center;font-size:17px}.quick-actions b{font-size:10px}.q-green{background:#e7f7f2;color:var(--green)}.q-amber{background:#fff4e3;color:var(--amber)}.q-blue{background:#edf3ff;color:var(--blue)}.q-purple{background:#f3efff;color:var(--purple)}/* Glucose / Diet */.summary-strip{background:#fff;border:1px solid var(--line);border-radius:15px;padding:20px 30px;display:flex;align-items:center;justify-content:space-around;margin-bottom:18px}.summary-strip>div{display:grid;grid-template-columns:auto auto;align-items:end;column-gap:5px}.summary-strip span{grid-column:1/3;color:#7e918d;font-size:11px;margin-bottom:6px}.summary-strip strong{font-size:27px}.summary-strip small{font-size:9px;color:#8ba09c;margin-bottom:4px}.summary-strip>i{height:40px;border-left:1px solid var(--line)}.large-panel{margin-bottom:18px}.analysis-stack{display:flex;flex-direction:column;gap:12px}.insight-card{padding:18px;border-radius:14px;display:flex;gap:12px;border:1px solid}.insight-card>span{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;flex:none}.insight-card b{font-size:12px}.insight-card p{font-size:10px;line-height:1.65;margin:5px 0 0;color:#6e827e}.insight-card.warning{background:#fff8ee;border-color:#f5dfbc}.insight-card.warning>span{background:#ffedcf;color:#db932b}.insight-card.success{background:#edf9f5;border-color:#cfece4}.insight-card.success>span{background:#d9f1ea;color:var(--green)}.tip-card{background:#153f39;color:#fff;border-radius:14px;padding:18px}.tip-card b{font-size:12px}.tip-card p{font-size:10px;color:#bad0cb;line-height:1.65;margin:7px 0 0}.table-panel{overflow:hidden}.table-panel .el-table{--el-table-border-color:#edf1f0;--el-table-header-bg-color:#f7f9f8;--el-table-row-hover-bg-color:#f4fbf8;font-size:11px}.table-panel .el-table th.el-table__cell{font-size:10px;color:#748783;font-weight:600}.diet-hero{background:linear-gradient(110deg,#0d403a,#12695a);border-radius:17px;color:white;padding:23px 30px;display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.diet-hero>div:first-child{width:58%}.diet-hero span{font-size:11px;color:#a8d1c8}.diet-hero strong{font-size:26px;display:block;margin:7px 0 11px}.diet-hero strong small{font-size:10px;color:#98bcb5}.diet-hero p{font-size:10px;color:#a8c8c2;margin:9px 0 0}.diet-hero p b{color:#fff}.water{display:flex;align-items:center;gap:12px;background:rgba(255,255,255,.08);padding:15px 18px;border-radius:13px}.water>span{font-size:24px}.water div{display:flex;flex-direction:column}.water b{font-size:11px}.water small{font-size:9px;color:#95bbb3;margin-top:4px}.water .el-button{margin-left:10px}.diet-grid{grid-template-columns:1.8fr 1fr;margin-bottom:18px}.meal-list{display:flex;flex-direction:column}.meal-row{display:grid;grid-template-columns:42px 1fr auto 65px;align-items:center;gap:11px;padding:13px 0;border-bottom:1px solid var(--line)}.meal-row:last-child{border:0}.meal-index{font-size:12px;color:#aac0bb;font-weight:700}.meal-info b{font-size:12px}.meal-info b small{font-weight:400;color:#91a29e;margin-left:5px}.meal-info p{font-size:10px;color:#697f7a;margin:4px 0 0}.meal-row>strong{text-align:right;font-size:12px}.meal-row>strong small{font-size:8px;color:#96a6a3}.macro-note{font-size:10px;line-height:1.55;color:#617772;background:#f4f8f7;border-radius:8px;padding:9px 11px}/* medication */.med-banner{background:linear-gradient(110deg,#e5f7f1,#f6fbf9);border:1px solid #cfe8e1;border-radius:17px;padding:20px 28px;display:flex;align-items:center;gap:20px;margin-bottom:18px}.med-progress{width:72px;height:72px;border:6px solid #cfe8e1;border-top-color:var(--green);border-right-color:var(--green);border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;flex:none}.med-progress strong{font-size:22px}.med-progress span{font-size:10px}.med-progress small{font-size:7px;color:#76918b}.med-banner>div:nth-child(2){flex:1}.med-banner h3{font-size:16px;margin:0 0 5px}.med-banner p{font-size:11px;color:#728984;margin:0}.med-timeline{margin-bottom:18px}.medicine-card{display:flex;align-items:center;gap:18px;padding:17px 10px;border-top:1px solid var(--line)}.medicine-card.taken{opacity:.72}.medicine-time{width:58px;font-size:17px;font-weight:700}.medicine-time small{display:block;font-size:9px;color:#8da09c;font-weight:400;margin-top:3px}.pill-icon{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;background:#e9f7f3;color:var(--green);font-size:19px}.pill-icon.blue{background:#edf3ff;color:var(--blue)}.medicine-info{flex:1}.medicine-info>span{font-size:8px;background:#f1f5f4;color:#79908b;border-radius:8px;padding:3px 7px}.medicine-info h3{font-size:13px;margin:7px 0 3px}.medicine-info h3 small{font-weight:500;color:#66807a}.medicine-info p{font-size:10px;color:#8ca09c;margin:0}.med-bottom{grid-template-columns:1.1fr 1fr}.adherence{display:flex;align-items:center;justify-content:space-around;padding:14px}.score-ring{width:110px;height:110px;border:11px solid #dcefe9;border-top-color:var(--green);border-right-color:var(--green);border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center}.score-ring strong{font-size:24px}.score-ring small{font-size:8px;color:#899c98}.adherence-stats p{display:flex;justify-content:space-between;gap:50px;font-size:10px}.adherence-stats span{color:#849792}.safety-card{background:#fff7eb;border:1px solid #f3dfbe;border-radius:16px;padding:25px;display:flex;gap:16px;color:#6f552c}.safety-card>.el-icon{font-size:26px;color:var(--amber);flex:none}.safety-card h3{font-size:15px;margin:2px 0 9px}.safety-card p{font-size:11px;line-height:1.8;margin:0;color:#806b4b}.safety-card button{border:0;background:none;padding:15px 0 0;color:#c98729;font-size:10px;font-weight:700}/* Exercise */.exercise-hero{border-radius:17px;background:linear-gradient(112deg,#0c4039,#136e5c);padding:25px 35px;color:#fff;display:flex;align-items:center;margin-bottom:18px}.weekly-ring{width:115px;height:115px;border:10px solid rgba(255,255,255,.15);border-top-color:#50dab6;border-right-color:#50dab6;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center}.weekly-ring strong{font-size:28px}.weekly-ring span{font-size:9px;color:#a9cdc5}.weekly-ring small{font-size:8px;color:#79b5a7;margin-top:4px}.exercise-metrics{display:grid;grid-template-columns:repeat(3,1fr);flex:1;margin-left:55px}.exercise-metrics>div{border-left:1px solid rgba(255,255,255,.12);padding-left:38px}.exercise-metrics>div>span{font-size:22px}.exercise-metrics b{display:block;font-size:21px;margin:8px 0 4px}.exercise-metrics b small{font-size:9px;color:#9bc0b8}.exercise-metrics p{margin:0;color:#8fb6ad;font-size:10px}.exercise-grid{grid-template-columns:1.45fr 1fr;margin-bottom:18px}.coach-card{background:linear-gradient(135deg,#edf9f5,#e3f4ef);border:1px solid #cfeae2;border-radius:16px;padding:24px;position:relative;overflow:hidden}.coach-card:after{content:””;position:absolute;width:160px;height:160px;border-radius:50%;background:rgba(255,255,255,.4);right:-60px;top:-60px}.coach-icon{width:42px;height:42px;border-radius:13px;display:grid;place-items:center;background:white;color:var(--green);font-size:20px;margin-bottom:12px}.coach-card h2{font-size:18px;margin:6px 0 7px}.coach-card>p{font-size:10px;line-height:1.65;color:#607b75}.coach-card>div{display:flex;gap:18px;margin:13px 0}.coach-card>div span{font-size:9px;color:#7b918c}.coach-card>div b{font-size:13px;color:#315d54}.coach-card .el-button{background:#103f38;color:white;border:0}.down-arrow{color:#a0b1ad;margin:0 8px}.exercise-notice{display:flex;align-items:center;gap:13px;background:#fff8ee;color:#775b35;border:1px solid #f0dfc4;border-radius:13px;padding:15px 20px;margin-top:18px}.exercise-notice>.el-icon{color:var(--amber);font-size:20px}.exercise-notice b{font-size:11px}.exercise-notice p{font-size:9px;margin:3px 0 0;color:#967c59}/* Report */.report-paper{background:#fff;border:1px solid var(--line);box-shadow:0 15px 40px rgba(30,61,56,.06);border-radius:3px;padding:38px 45px;max-width:1100px;margin:auto}.report-paper>header{display:flex;align-items:center;justify-content:space-between;border-bottom:2px solid var(--green-dark);padding-bottom:20px}.report-brand{display:flex;align-items:center;gap:10px}.report-brand .brand-mark{color:white;width:35px;height:35px}.report-brand>div:last-child{display:flex;flex-direction:column}.report-brand b{font-size:16px}.report-brand small{font-size:8px;color:#849792}.report-period{text-align:right}.report-period span{display:block;font-size:18px;font-weight:700}.report-period b{font-size:9px;color:#839692}.report-profile{display:flex;align-items:center;gap:45px;background:#f5f8f7;margin:22px 0;padding:17px 22px;border-radius:10px}.profile-main{display:flex;align-items:center;gap:12px;flex:1}.profile-main>span{width:46px;height:46px;border-radius:12px;display:grid;place-items:center;background:#bce6da;color:#146853;font-weight:700}.profile-main h2{font-size:16px;margin:0 0 4px}.profile-main p{font-size:9px;color:#829590;margin:0}.report-profile>div:not(:first-child){display:flex;flex-direction:column}.report-profile small{font-size:8px;color:#8a9b98}.report-profile b{font-size:11px;margin-top:5px}.report-profile .score-text{color:var(--green);font-size:19px}.score-text em{font-style:normal;font-size:8px;color:#849692}.report-summary{display:grid;grid-template-columns:2fr repeat(3,1fr);border:1px solid var(--line);border-radius:12px;overflow:hidden}.report-summary>div{padding:20px;border-right:1px solid var(--line)}.report-summary>div:last-child{border:0}.report-score{display:flex;align-items:center;gap:15px;background:#f0faf7}.report-score>div{width:65px;height:65px;border:6px solid #a9e1d2;border-top-color:var(--green);border-radius:50%;display:grid;place-items:center;flex:none}.report-score strong{font-size:20px}.report-score small{font-size:7px;margin-top:-14px;color:#718983}.report-score p{display:flex;flex-direction:column;margin:0}.report-score p b{font-size:12px}.report-score p span{font-size:8px;line-height:1.6;color:#778c87;margin-top:4px}.mini-stat{display:flex;flex-direction:column}.mini-stat span{font-size:8px;color:#7d908c}.mini-stat strong{font-size:21px;margin:8px 0 4px}.mini-stat small{font-size:8px;color:#9aa9a6}.report-section{margin-top:25px}.report-section>h3{font-size:13px;display:flex;gap:9px;align-items:center;border-bottom:1px solid var(--line);padding-bottom:10px}.report-section>h3 span{font-size:9px;color:white;background:var(--green);border-radius:4px;padding:3px 5px}.report-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.report-kpis>div{background:#f7f9f8;border-radius:8px;padding:13px;display:flex;flex-direction:column}.report-kpis small{font-size:8px;color:#839591}.report-kpis b{font-size:14px;margin-top:5px}.behavior-bars{display:grid;grid-template-columns:repeat(3,1fr);gap:25px}.behavior-bars label{display:flex;justify-content:space-between;font-size:9px;margin-bottom:7px}.doctor-conclusion{display:flex;gap:13px;align-items:center;background:#eef8f5;border-left:3px solid var(--green);padding:16px;margin-top:25px}.doctor-avatar{width:39px;height:39px;border-radius:50%;display:grid;place-items:center;background:#a7ddd0;color:#185f50}.doctor-conclusion small{font-size:8px;color:#79918b}.doctor-conclusion p{font-size:10px;line-height:1.6;margin:4px 0 0}.report-paper>footer{text-align:center;font-size:8px;color:#9aa8a5;margin-top:26px;padding-top:15px;border-top:1px solid var(--line)}/* AI */.ai-online{display:flex;align-items:center;gap:8px;background:#e8f7f2;color:#287867;border-radius:20px;padding:8px 13px;font-size:10px}.chat-layout{display:grid;grid-template-columns:260px 1fr;gap:18px;height:690px}.chat-side{padding:0;overflow:hidden}.ai-doctor{text-align:center;padding:28px 15px 22px;background:linear-gradient(#eaf8f4,#fff)}.ai-avatar{margin:auto;width:67px;height:67px;border-radius:21px;background:linear-gradient(145deg,#1fc39c,#0b745d);color:#fff;display:grid;place-items:center;font-size:28px;position:relative;box-shadow:0 10px 25px rgba(19,168,132,.2)}.ai-avatar i{position:absolute;right:-2px;bottom:-2px;width:13px;height:13px;border-radius:50%;background:#2fd29e;border:3px solid white}.ai-doctor h3{font-size:15px;margin:13px 0 4px}.ai-doctor p{font-size:9px;color:#839793}.ai-doctor>span{font-size:8px;color:#2e8874;background:#e6f5f1;border-radius:10px;padding:4px 8px}.data-context{padding:18px 20px}.data-context h4{font-size:10px;color:#879a96;margin:0 0 13px}.data-context p{display:flex;align-items:center;gap:8px;font-size:9px;color:#647c76;padding:9px 0;margin:0;border-bottom:1px solid var(--line)}.data-context p b{margin-left:auto;color:#365a52}.privacy-note{display:flex;gap:8px;margin:10px 17px;padding:11px;background:#f5f8f7;border-radius:9px;color:#829590;font-size:8px;line-height:1.5}.chat-main{padding:0;display:flex;flex-direction:column;overflow:hidden}.chat-header{height:65px;border-bottom:1px solid var(--line);padding:0 20px;display:flex;align-items:center;justify-content:space-between}.chat-header>div{display:flex;flex-direction:column}.chat-header b{font-size:13px}.chat-header span{font-size:8px;color:#879a96;margin-top:3px}.chat-body{flex:1;overflow:auto;padding:20px 25px;scrollbar-width:thin}.date-divider{text-align:center;font-size:8px;color:#a1afac;margin-bottom:20px}.message{display:flex;gap:9px;margin-bottom:17px;align-items:flex-start}.message.user{justify-content:flex-end}.msg-avatar,.user-avatar{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex:none}.msg-avatar{background:#e3f5ef;color:var(--green)}.user-avatar{background:#addfd2;color:#185e50;font-size:10px;font-weight:700}.message>div{max-width:70%}.bubble{padding:12px 14px;border-radius:4px 13px 13px 13px;background:#f1f6f4}.bubble p{font-size:11px;line-height:1.75;margin:0;white-space:pre-line}.bubble small{display:block;border-top:1px solid #dde7e4;margin-top:10px;padding-top:8px;color:#91a19e;font-size:8px}.message.user .bubble{background:var(--green);color:#fff;border-radius:13px 4px 13px 13px}.message time{font-size:7px;color:#9dacaa;margin-top:4px;display:block}.message.user time{text-align:right}.typing{display:flex;gap:4px;padding:15px}.typing i{width:5px;height:5px;background:#79a89d;border-radius:50%;animation:typing 1s infinite}.typing i:nth-child(2){animation-delay:.15s}.typing i:nth-child(3){animation-delay:.3s}@keyframes typing{50%{transform:translateY(-4px)}}.quick-prompts{display:flex;gap:7px;padding:8px 20px;overflow:auto}.quick-prompts button{border:1px solid #dce8e5;color:#53706a;background:#fff;border-radius:15px;padding:7px 10px;font-size:9px;white-space:nowrap}.quick-prompts button:hover{border-color:var(--green);color:var(--green)}.chat-input{padding:10px 20px 5px;display:flex;gap:8px}.chat-input .el-input__wrapper{border-radius:20px}.chat-disclaimer{text-align:center;font-size:7px;color:#a2afad;margin:5px 0 10px}/* Visualization */.viz-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:18px}.viz-kpis>div{display:flex;align-items:center;gap:12px;background:white;border:1px solid var(--line);border-radius:14px;padding:17px}.viz-icon{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;font-size:18px;flex:none}.viz-icon.green{background:#e6f7f2;color:var(--green)}.viz-icon.blue{background:#edf3ff;color:var(--blue)}.viz-icon.amber{background:#fff4e4;color:var(--amber)}.viz-icon.purple{background:#f3efff;color:var(--purple)}.viz-kpis p{margin:0;display:grid;grid-template-columns:auto auto;align-items:end;column-gap:8px;font-size:9px;color:#7e918d}.viz-kpis p>b{grid-column:1/2;font-size:20px;color:var(--ink);margin-top:4px}.viz-kpis b small{font-size:8px;color:#879a96}.viz-kpis em{font-style:normal;color:var(--green);font-size:8px}.viz-kpis em.neutral{color:#7e918d}.viz-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.viz-grid .span-2{grid-column:span 2}.chart-tabs{display:flex;background:#f3f6f5;border-radius:7px;padding:3px}.chart-tabs button{border:0;background:transparent;font-size:9px;padding:5px 10px;border-radius:5px;color:#7b8f8a}.chart-tabs .active{background:#fff;color:var(--green);box-shadow:0 2px 7px rgba(20,50,45,.08)}.score-panel>p{text-align:center;color:#859793;font-size:9px;margin:-15px 0 0}.score-panel>p b{color:var(--green)}.correlation-card{background:linear-gradient(135deg,#0c413a,#106655);color:#fff;border:0}.correlation-card .panel-kicker{color:#5bdab9}.correlation-card h3{font-size:15px}.correlation{margin:21px 0 13px}.correlation b{font-size:37px}.correlation span{font-size:10px;color:#9fc7bf;margin-left:5px}.correlation p{font-size:9px;color:#8fb8af;margin:4px 0}.correlation-copy{font-size:10px;line-height:1.8;color:#bad5cf;border-top:1px solid rgba(255,255,255,.12);padding-top:15px}/* Collaboration */.collab-grid{display:grid;grid-template-columns:1fr 1.8fr;gap:18px;margin-bottom:18px}.doctor-profile{padding:0;overflow:hidden}.doctor-cover{height:77px;background:linear-gradient(115deg,#0c443c,#16816a);position:relative}.doctor-cover:after{content:””;position:absolute;right:-20px;top:-55px;width:160px;height:160px;border:1px solid rgba(255,255,255,.12);border-radius:50%}.doctor-main{display:flex;gap:14px;padding:0 22px;margin-top:-25px;position:relative}.doctor-big-avatar{width:67px;height:67px;border-radius:16px;display:grid;place-items:center;background:#b7e5da;border:4px solid white;color:#145f50;font-size:20px;font-weight:700;position:relative}.doctor-big-avatar i{position:absolute;right:-2px;bottom:-2px;width:12px;height:12px;border-radius:50%;background:#2bc99a;border:3px solid white}.doctor-main>div{padding-top:31px}.doctor-main h2{font-size:15px;margin:0 0 4px}.doctor-main p{font-size:10px;margin:0;color:#57716b}.doctor-main small{font-size:8px;color:#8a9c98}.doctor-stats{display:grid;grid-template-columns:repeat(3,1fr);margin:23px 22px 17px;border-block:1px solid var(--line);padding:14px 0}.doctor-stats>div{display:flex;flex-direction:column;align-items:center;border-right:1px solid var(--line)}.doctor-stats>div:last-child{border:0}.doctor-stats b{font-size:14px}.doctor-stats span{font-size:8px;color:#8b9d99;margin-top:3px}.visit-card{margin:0 20px 20px;background:#eef8f5;border-radius:10px;padding:12px;display:flex;align-items:center;gap:10px}.visit-card>.el-icon{color:var(--green)}.visit-card>div{display:flex;flex-direction:column;flex:1}.visit-card span{font-size:8px;color:#829590}.visit-card b{font-size:10px;margin-top:3px}.visit-card .el-button{font-size:9px}.message-panel{padding:0;display:flex;flex-direction:column;height:420px}.message-panel>.panel-head{height:62px;border-bottom:1px solid var(--line);padding:0 20px;margin:0}.online-text{font-size:8px;color:#839792;display:flex;gap:7px;align-items:center;margin-top:5px}.doctor-messages{flex:1;padding:18px 20px;overflow:auto}.doctor-message{display:flex;gap:8px;margin-bottom:18px}.doctor-message.patient{justify-content:flex-end}.doctor-message>span{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;background:#b2e2d6;color:#176353;font-size:10px;font-weight:700;flex:none}.doctor-message>div{max-width:75%}.doctor-message small{font-size:7px;color:#91a19e}.doctor-message p{background:#f2f6f5;padding:10px 12px;border-radius:3px 11px 11px 11px;font-size:10px;line-height:1.6;margin:4px 0}.doctor-message.patient p{background:#e4f5f0;border-radius:11px 3px 11px 11px}.doctor-message.patient small{display:block;text-align:right}.doctor-input{display:flex;gap:8px;border-top:1px solid var(--line);padding:12px 18px}.collab-lower{display:grid;grid-template-columns:1.3fr 1fr;gap:18px}.shared-list>div{display:flex;align-items:center;gap:12px;border-top:1px solid var(--line);padding:13px 0}.shared-list>div>div{display:flex;flex-direction:column;flex:1}.shared-list b,.family-person b{font-size:11px}.shared-list small,.family-person small{font-size:8px;color:#8ca09b;margin-top:3px}.family-person{display:flex;gap:11px;align-items:center;padding:17px 0;border-block:1px solid var(--line)}.family-person>span{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;background:#f5d9c8;color:#7d4b32;font-weight:700}.family-person>div{display:flex;flex-direction:column;flex:1}.family-alert{display:flex;gap:10px;margin-top:15px;background:#fff7eb;color:#be7c20;border-radius:9px;padding:11px}.family-alert p{display:flex;flex-direction:column;margin:0}.family-alert b{font-size:10px}.family-alert span{font-size:8px;color:#8f7650;margin-top:3px}.mobile-menu,.mobile-mask{display:none}@media(max-width:1280px){.collab-grid{grid-template-columns:1fr 1.35fr}.doctor-main{padding-inline:16px}.doctor-stats{margin-inline:16px}.visit-card{margin-inline:16px}}@media(max-width:1100px){.metric-grid,.viz-kpis{grid-template-columns:repeat(2,1fr)}.main-grid,.lower-grid,.diet-grid,.exercise-grid{grid-template-columns:1fr}.viz-grid{grid-template-columns:repeat(2,1fr)}.viz-grid .span-2{grid-column:span 2}.collab-grid{grid-template-columns:1fr}.quick-panel{min-height:150px}}@media(max-width:780px){.sidebar{transform:translateX(-110%);box-shadow:10px 0 30px rgba(0,0,0,.25)}.sidebar.mobile-open{transform:translateX(0)}.collapse-btn{display:none}.mobile-mask{display:block;position:fixed;inset:0;background:rgba(4,28,24,.45);z-index:29}.main,.main.wide{padding-left:0}.topbar{padding:0 16px;height:64px}.mobile-menu{display:grid;place-items:center;border:0;background:#eef4f2;width:35px;height:35px;border-radius:9px}.breadcrumb,.sync,.profile-copy{display:none}.top-actions{gap:8px}.page-wrap{padding:20px 14px 40px}.page-title{align-items:flex-start;gap:15px}.page-title h1{font-size:22px}.page-title p{font-size:11px}.page-title .el-button--large{padding:10px 12px;font-size:11px}.welcome-banner{height:auto;min-height:230px;padding:28px 24px}.banner-orbit{display:none}.metric-grid,.viz-kpis{grid-template-columns:1fr 1fr;gap:10px}.content-grid,.two-col,.med-bottom,.collab-grid,.collab-lower,.chat-layout{grid-template-columns:1fr}.summary-strip{padding:15px 10px;overflow:auto;gap:20px;justify-content:flex-start}.summary-strip>div{min-width:85px}.summary-strip>i{display:none}.diet-hero,.exercise-hero{align-items:flex-start;flex-direction:column;gap:20px}.diet-hero>div:first-child{width:100%}.water{width:100%}.exercise-metrics{margin:0;width:100%;gap:8px}.exercise-metrics>div{padding-left:9px}.medicine-card{gap:9px;flex-wrap:wrap}.medicine-time{width:48px}.medicine-info{min-width:160px}.report-paper{padding:22px 16px;overflow:hidden}.report-profile{gap:10px;flex-wrap:wrap}.report-summary{grid-template-columns:1fr 1fr}.report-summary .report-score{grid-column:span 2}.report-kpis,.behavior-bars{grid-template-columns:repeat(2,1fr)}.chat-layout{height:auto}.chat-side{display:none}.chat-main{height:680px}.viz-grid{grid-template-columns:1fr}.viz-grid .span-2{grid-column:auto}.collab-grid{grid-template-columns:1fr}.message-panel{height:440px}}@media(max-width:480px){.metric-grid,.viz-kpis{grid-template-columns:1fr}.welcome-copy h1{font-size:24px}.banner-status{flex-direction:column;gap:8px}.quick-actions{grid-template-columns:repeat(2,1fr);row-gap:15px}.meal-row{grid-template-columns:32px 1fr auto}.meal-row>.el-tag{display:none}.meal-row>strong{grid-column:2/4;text-align:left}.exercise-metrics b{font-size:15px}.exercise-metrics>div>span{font-size:16px}.report-summary{grid-template-columns:1fr}.report-summary .report-score{grid-column:auto}.report-kpis,.behavior-bars{grid-template-columns:1fr}.report-paper>header{align-items:flex-start}.report-period span{font-size:13px}.collab-lower{grid-template-columns:1fr}}@media print{.sidebar,.topbar,.no-print{display:none!important}.main{padding:0}.page-wrap{padding:0}.report-paper{box-shadow:none;border:0}.report-page{background:white}}

图标配置

import { createApp } from 'vue'import { createPinia } from 'pinia'import ElementPlus from 'element-plus'import 'element-plus/dist/index.css'import * as ElementPlusIconsVue from '@element-plus/icons-vue'import App from './App.vue'import './style.css'const app = createApp(App)Object.entries(ElementPlusIconsVue).forEach(([key, component]) => app.component(key, component))app.use(createPinia()).use(ElementPlus).mount('#app')

侧边栏实现

<template><divclass="app-shell"><divv-if="mobileOpen"class="mobile-mask" @click="mobileOpen = false"></div><asideclass="sidebar":class="{ collapsed, 'mobile-open': mobileOpen }"><divclass="brand"><divclass="brand-mark"><span>+</span></div> <divv-if="!collapsed"class="brand-copy"><b>糖护智伴</b><small>AI HEALTH CARE</small></div></div><divclass="side-section">{{ collapsed ? '·' : '健康管理' }}</div><nav><buttonv-for="item in menus":key="item.key":class="{ active: active === item.key }"><el-icon><component:is="item.icon" /></el-icon><spanv-if="!collapsed">{{ item.label }}</span><emv-if="item.badge && !collapsed">{{ item.badge }}</em></button></nav><divv-if="!collapsed"class="care-card"><el-icon><Service /></el-icon><div><b>需要帮助?</b><small>健康专员在线服务</small></div><button>联系专员</button></div><buttonclass="collapse-btn" @click="collapsed = !collapsed"><el-icon><DArrowLeftv-if="!collapsed"/><DArrowRightv-else/></el-icon></button></aside></div></template><scriptsetup>import { computed, onMounted, ref } from 'vue'const active = ref('home')const collapsed = ref(false)const mobileOpen = ref(false)const menus = [{ key: 'home', label: '健康首页', icon: 'House' },{ key: 'glucose', label: '血糖监测', icon: 'Odometer' },{ key: 'diet', label: '饮食管理', icon: 'Bowl' },{ key: 'medication', label: '用药管理', icon: 'FirstAidKit' },{ key: 'exercise', label: '运动指导', icon: 'Bicycle' },{ key: 'report', label: '健康报告', icon: 'Document' },{ key: 'ai', label: 'AI 智能问诊', icon: 'ChatDotRound', badge: 'AI' },{ key: 'visualization', label: '数据中心', icon: 'DataAnalysis' },{ key: 'collaboration', label: '医患协作', icon: 'Connection' }]</script><stylescoped></style>

实战篇_糖护智伴 · 顶部导航

<template><divclass="app-shell"><divv-if="mobileOpen"class="mobile-mask" @click="mobileOpen = false"></div><asideclass="sidebar":class="{ collapsed, 'mobile-open': mobileOpen }"><divclass="brand"><divclass="brand-mark"><span>+</span></div><divv-if="!collapsed"class="brand-copy"><b>糖护智伴</b><small>AI HEALTH CARE</small></div></div><divclass="side-section">{{ collapsed ? '·' : '健康管理' }}</div><nav><buttonv-for="item in menus":key="item.key":class="{ active: active === item.key }" @click="navigate(item.key)"><el-icon><component:is="item.icon" /></el-icon><spanv-if="!collapsed">{{ item.label }}</span><emv-if="item.badge && !collapsed">{{ item.badge }}</em></button></nav><divv-if="!collapsed"class="care-card"><el-icon><Service /></el-icon><div><b>需要帮助?</b><small>健康专员在线服务</small></div><button>联系专员</button></div><buttonclass="collapse-btn" @click="collapsed = !collapsed"><el-icon><DArrowLeftv-if="!collapsed"/><DArrowRightv-else/></el-icon></button></aside><mainclass="main":class="{ wide: collapsed }"> <headerclass="topbar"><divclass="breadcrumb"><span>健康中心</span><i>/</i><b>{{ title }}</b></div><divclass="top-actions"><divclass="sync"><i></i><span>设备已同步</span></div><buttonclass="icon-btn"><el-icon><Bell /></el-icon><iclass="dot"></i></button><el-dropdown><divclass="profile"><spanclass="avatar">{{'张' }}</span><spanclass="profile-copy"><b>张伟</b><small>糖尿病 · 23岁</small></span><el-icon><ArrowDown /></el-icon></div></el-dropdown></div></header></main></div></template><scriptsetup>import { computed, onMounted, ref } from 'vue'const active = ref('home')const collapsed = ref(false)const mobileOpen = ref(false)const menus = [{ key: 'home', label: '健康首页', icon: 'House' }, { key: 'glucose', label: '血糖监测', icon: 'Odometer' },{ key: 'diet', label: '饮食管理', icon: 'Bowl' },{ key: 'medication', label: '用药管理', icon: 'FirstAidKit' },{ key: 'exercise', label: '运动指导', icon: 'Bicycle' },{ key: 'report', label: '健康报告', icon: 'Document' },{ key: 'ai', label: 'AI 智能问诊', icon: 'ChatDotRound', badge: 'AI' },{ key: 'visualization', label: '数据中心', icon: 'DataAnalysis' },{ key: 'collaboration', label: '医患协作', icon: 'Connection' }]const title = computed(() => menus.find(m => m.key === active.value)?.label)</script>

实战篇_糖护智伴 · 导航页面

页面管理

  • 健康首页 HomePage.vue 
  • 血糖监测 GlucosePage.vue
  • 饮食管理 DietPage.vue
  • 用药管理 MedicationPage.vue
  • 运动管理 ExercisePage.vue
  • 健康报告 ReportPage.vue
  • AI 智能问诊 AiPage.vue
  • 数据中心 VisualizationPage.vue
  • 医患协作 CollaborationPage.vue
<template><divclass="app-shell"><divv-if="mobileOpen"class="mobile-mask" @click="mobileOpen = false"></div><asideclass="sidebar":class="{ collapsed, 'mobile-open': mobileOpen }"><divclass="brand"><divclass="brand-mark"><span>+</span></div><divv-if="!collapsed"class="brand-copy"><b>糖护智伴</b><small>AI HEALTH CARE</small></div></div><divclass="side-section">{{ collapsed ? '·' : '健康管理' }}</div><nav><buttonv-for="item in menus":key="item.key":class="{ active: active === item.key }" @click="navigate(item.key)"><el-icon><component:is="item.icon" /></el-icon><spanv-if="!collapsed">{{ item.label }}</span><emv-if="item.badge && !collapsed">{{ item.badge }}</em></button></nav><divv-if="!collapsed"class="care-card"><el-icon><Service /></el-icon><div><b>需要帮助?</b><small>健康专员在线服务</small></div><button>联系专员</button></div><buttonclass="collapse-btn" @click="collapsed = !collapsed"> <el-icon><DArrowLeftv-if="!collapsed"/><DArrowRightv-else/></el-icon></button></aside><mainclass="main":class="{ wide: collapsed }"><headerclass="topbar"><divclass="breadcrumb"><span>健康中心</span><i>/</i><b>{{ title }}</b></div><divclass="top-actions"><divclass="sync"><i></i><span>设备已同步</span></div><buttonclass="icon-btn"><el-icon><Bell /></el-icon><iclass="dot"></i></button><el-dropdown><divclass="profile"><spanclass="avatar">{{'张' }}</span><spanclass="profile-copy"><b>张伟</b><small>糖尿病 · 23岁</small></span><el-icon><ArrowDown /></el-icon></div></el-dropdown></div></header><divclass="page-wrap"><component:is="currentView" @navigate="navigate" /></div></main></div></template><scriptsetup>import { computed, onMounted, ref } from 'vue'import HomePage from './views/HomePage.vue'import GlucosePage from './views/GlucosePage.vue'import DietPage from './views/DietPage.vue'import MedicationPage from './views/MedicationPage.vue'import ExercisePage from './views/ExercisePage.vue'import ReportPage from './views/ReportPage.vue'import AiPage from './views/AiPage.vue'import VisualizationPage from './views/VisualizationPage.vue'import CollaborationPage from './views/CollaborationPage.vue'const active = ref('home')const collapsed = ref(false)const mobileOpen = ref(false)const menus = [{ key: 'home', label: '健康首页', icon: 'House' },{ key: 'glucose', label: '血糖监测', icon: 'Odometer' },{ key: 'diet', label: '饮食管理', icon: 'Bowl' },{ key: 'medication', label: '用药管理', icon: 'FirstAidKit' },{ key: 'exercise', label: '运动指导', icon: 'Bicycle' },{ key: 'report', label: '健康报告', icon: 'Document' },{ key: 'ai', label: 'AI 智能问诊', icon: 'ChatDotRound', badge: 'AI' },{ key: 'visualization', label: '数据中心', icon: 'DataAnalysis' },{ key: 'collaboration', label: '医患协作', icon: 'Connection' }]const pages = {home: HomePage,glucose: GlucosePage,diet: DietPage,medication: MedicationPage,exercise: ExercisePage,report: ReportPage,ai: AiPage,visualization: VisualizationPage,collaboration: CollaborationPage}const currentView = computed(() => pages[active.value])function navigate(key) {active.value = key;mobileOpen.value = false;window.scrollTo({ top: 0, behavior: 'smooth' })}const title = computed(() => menus.find(m => m.key === active.value)?.label)</script><stylescoped></style>

实战篇_糖护智伴 · 数据获取

接口配置

import axios from 'axios'const api = axios.create({ baseURL: '/api', timeout: 8000 })api.interceptors.response.use(response => response.data)export const healthApi = {  patients: () => api.get('/patients'),  dashboard: id => api.get(`/dashboard/${id}`),  glucose: id => api.get(`/blood-sugar/${id}`),  addGlucose: data => api.post('/blood-sugar', data),  diet: id => api.get(`/diet/${id}`),  addDiet: data => api.post('/diet', data),  medications: id => api.get(`/medications/${id}`),  takeMedication: id => api.patch(`/medications/${id}/take`),  exercises: id => api.get(`/exercises/${id}`),  addExercise: data => api.post('/exercises', data),  report: id => api.get(`/report/${id}`),  collaboration: id => api.get(`/collaboration/${id}`),  chat: message => api.post('/ai/chat', { message })}

状态管理

import { defineStore } from 'pinia'import { computed, ref } from 'vue'import { healthApi } from '../api'export const useHealthStore = defineStore('health', () => {  const patientId = ref(1)  const patients = ref([])  const dashboard = ref(null)  const loading = ref(false)  const currentPatient = computed(() => patients.value.find(p => p.id === patientId.value) || dashboard.value?.patient)  async function init() {    loading.value = true    try {      patients.value = await healthApi.patients()      dashboard.value = await healthApi.dashboard(patientId.value)    } finally { loading.value = false }  }  async function changePatient(id) {    patientId.value = id    dashboard.value = await healthApi.dashboard(id)  }  async function refreshDashboard() { dashboard.value = await healthApi.dashboard(patientId.value) }  return { patientId, patients, dashboard, currentPatient, loading, init, changePatient, refreshDashboard }})

初始化数据

onMounted(store.init)

浏览器状态数据查看

Chrome浏览器安装Vue插件:vue-devtools

实战篇_糖护智伴 · 用户数据切换

加载仓库切换用户数据

<template><divclass="app-shell"v-loading="store.loading"><el-dropdown @command="store.changePatient"><divclass="profile"><spanclass="avatar">{{ store.currentPatient?.name?.slice(0, 1) || '张' }}</span><spanclass="profile-copy"><b>{{ store.currentPatient?.name || '加载中' }}</b><small>{{ store.currentPatient?.diabetes_type }}糖尿病 · {{ store.currentPatient?.age }}岁</small></span><el-icon><ArrowDown /></el-icon></div><template #dropdown><el-dropdown-menu><el-dropdown-itemv-for="p in store.patients":key="p.id":command="p.id">{{ p.name }} · {{ p.diabetes_type }}</el-dropdown-item> </el-dropdown-menu></template></el-dropdown></div></template><scriptsetup>import { useHealthStore } from './stores/health'const store = useHealthStore()onMounted(store.init)</script>

实战篇_糖护智伴 · 健康首页卡片

数据获取

import { ref,reactive,computed } from ”vue”import { useHealthStore } from '../stores/health'import MetricCard from '../components/MetricCard.vue'const store = useHealthStore()const data = computed(() => store.dashboard)const greeting = new Date().getHours() < 12 ? '上午好' : new Date().getHours() < 18 ? '下午好' : '晚上好'

视图渲染

<sectionv-if="data"class="page home-page"><divclass="welcome-banner"><divclass="welcome-copy"><spanclass="eyebrow">PERSONAL HEALTH OVERVIEW</span><h1>{{ greeting }},{{ data.patient.name }} <span>👋</span></h1><p>今天也要好好照顾自己。您的血糖状态整体平稳,继续保持。</p><divclass="banner-status"><span><i></i> 今日风险等级:<b>低风险</b></span><span>连续记录 <b>{{ data.metrics.streak }}天</b></span> </div></div><divclass="banner-orbit"><divclass="orbit-ring"><divclass="health-score"><strong>86</strong><small>健康指数</small></div></div><spanclass="orbit-tag one">血糖稳定</span><spanclass="orbit-tag two">按时服药</span></div></div><divclass="metric-grid"><MetricCardlabel="平均血糖":value="data.avg"unit="mmol/L"trend="较上周 ↓ 0.4"icon="Odometer" /><MetricCardlabel="糖化血红蛋白":value="data.metrics.hba1c"unit="%"trend="目标 < 7.0%"icon="DataLine"tone="blue" /><MetricCardlabel="血糖达标率":value="data.metrics.targetRate"unit="%"trend="较上周 ↑ 6%"icon="CircleCheck"tone="amber" /><MetricCardlabel="身体质量指数":value="data.metrics.bmi"unit="BMI"trend="轻度超重"icon="ScaleToOriginal"tone="purple" /></div></section>

组件化

<scriptsetup>defineProps({label: String, value: [String, Number],unit: String,trend: String,icon: String,tone: {type: String,default: 'green'}})</script><template><divclass="metric-card":class="`tone-${tone}`"><divclass="metric-top"><spanclass="metric-icon"><el-icon><component:is="icon" /></el-icon></span><spanv-if="trend"class="metric-trend">{{ trend }}</span></div><divclass="metric-value">{{ value }}<small>{{ unit }}</small></div><divclass="metric-label">{{ label }}</div></div></template>

实战篇_糖护智伴 · 数据可视化

组件化

<scriptsetup>import * as echarts from 'echarts'import { onBeforeUnmount, onMounted, ref, watch } from 'vue'const props = defineProps({option: { type: Object, required: true },height: { type: String, default: '300px' }})const el = ref()let chartlet observeronMounted(() => {chart = echarts.init(el.value)chart.setOption(props.option)observer = new ResizeObserver(() => chart?.resize())observer.observe(el.value)})watch(() => props.option, value => chart?.setOption(value, true), { deep: true })onBeforeUnmount(() => { observer?.disconnect(); chart?.dispose() })</script><template><divref="el"class="chart":style="{ height }"></div></template>

视图渲染

<divclass="content-grid main-grid"><divclass="panel trend-panel"><divclass="panel-head"> <div><spanclass="panel-kicker">GLUCOSE TREND</span><h3>近期血糖趋势</h3></div><buttonclass="text-btn" @click="emit('navigate', 'glucose')">查看详情<el-icon><ArrowRight /></el-icon></button></div><divclass="chart-legend"><span><iclass="legend-dot green"></i>血糖值</span><spanclass="target-pill">目标范围4.4–7.8</span></div><EChart:option="chartOption"height="280px" /></div></div>

数据定义

const chartOption = computed(() => ({  tooltip: { trigger: 'axis', backgroundColor: '#123c37', borderWidth: 0, textStyle: { color: '#fff' } },  grid: { left: 36, right: 20, top: 36, bottom: 32 },  xAxis: { type: 'category', boundaryGap: false, data: data.value?.glucose.map(x => x.measured_at.slice(5, 10)) || [], axisLine: { lineStyle: { color: '#dbe5e2' } }, axisLabel: { color: '#83918e' } },  yAxis: { type: 'value', min: 4, max: 12, splitLine: { lineStyle: { color: '#eef2f1', type: 'dashed' } }, axisLabel: { color: '#83918e' } },  series: [{ type: 'line', smooth: true, data: data.value?.glucose.map(x => x.value) || [], symbolSize: 8, lineStyle: { color: '#13a884', width: 3 }, itemStyle: { color: '#13a884', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(19,168,132,.28)' }, { offset: 1, color: 'rgba(19,168,132,0)' }] } }, markArea: { silent: true, itemStyle: { color: 'rgba(19,168,132,.055)' }, data: [[{ yAxis: 4.4 }, { yAxis: 7.8 }]] } }]}))

实战篇_糖护智伴 · 完善视图

视图渲染

<divclass="panel tasks-panel"><divclass="panel-head"><div><spanclass="panel-kicker">TODAY'S PLAN</span><h3>今日健康任务</h3> </div><spanclass="progress-ring">2/4</span></div><divclass="task-list"><divv-for="task in data.todayTasks":key="task.id"class="task-item":class="{ done: task.done }"><spanclass="task-check"><el-icon><Checkv-if="task.done" /></el-icon></span><div><b>{{ task.title }}</b><small>{{ task.time }} · {{ task.type }}</small></div><spanv-if="!task.done"class="task-arrow">›</span></div></div></div></div><divclass="content-grid lower-grid"><divclass="ai-insight"><spanclass="ai-spark"><el-icon><MagicStick /> </el-icon></span><div><spanclass="panel-kicker">AI HEALTH INSIGHT</span><h3>今日 AI 健康洞察</h3><p>{{ data.insight }}</p><button @click="emit('navigate', 'ai')">继续咨询 AI<el-icon><Right /></el-icon></button></div></div><divclass="panel quick-panel"><divclass="panel-head"><h3>快捷操作</h3></div><divclass="quick-actions"><button @click="emit('navigate', 'glucose')"><spanclass="q-green"><el-icon><Plus /></el-icon></span><b>记录血糖</b></button><button @click="emit('navigate', 'diet')"> <spanclass="q-amber"><el-icon><Bowl /></el-icon></span><b>记录饮食</b></button><button @click="emit('navigate', 'exercise')"><spanclass="q-blue"><el-icon><Bicycle /></el-icon></span><b>运动打卡</b></button><button @click="emit('navigate', 'report')"><spanclass="q-purple"><el-icon><Document /></el-icon></span><b>查看报告</b></button></div></div></div>

获取方式

公众号后台回复:Vue3项目 即可获取~

相关学习资料