diff --git a/docs/plans/2026-01-27-health-module-implementation.md b/docs/plans/2026-01-27-health-module-implementation.md new file mode 100644 index 0000000..c3a8069 --- /dev/null +++ b/docs/plans/2026-01-27-health-module-implementation.md @@ -0,0 +1,1019 @@ +# 健康模块增强实现计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 为运动、饮食、睡眠三个模块添加健康分析功能,包括 BMI 分析、营养建议区间、睡眠预警提示。 + +**Architecture:** +- 新增 3 个 API 端点提供分析数据 +- 修改 3 个页面 HTML 模板添加新 UI 组件 +- 复用现有 `UserConfig.tdee` 和 `User.bmi` 计算逻辑 + +**Tech Stack:** FastAPI, Chart.js, 原生 HTML/CSS/JS + +--- + +## 任务概览 + +| 任务 | 模块 | 预估步骤 | +|-----|------|---------| +| Task 1 | API: BMI 分析端点 | 5 步 | +| Task 2 | API: 营养建议端点 | 5 步 | +| Task 3 | API: 睡眠评估端点 | 5 步 | +| Task 4 | UI: 运动页面 BMI 卡片 | 5 步 | +| Task 5 | UI: 饮食页面营养建议 | 5 步 | +| Task 6 | UI: 睡眠页面预警卡片 | 5 步 | + +--- + +## Task 1: API - BMI 分析端点 + +**Files:** +- Modify: `src/vitals/web/app.py:1163` (在 `/api/exercises/stats` 后添加) +- Test: `tests/test_web.py` + +### Step 1: 写失败测试 + +在 `tests/test_web.py` 末尾添加: + +```python +def test_bmi_analysis_endpoint(client): + """测试 BMI 分析 API""" + response = client.get("/api/bmi/analysis") + assert response.status_code == 200 + data = response.json() + assert "current_weight" in data + assert "current_bmi" in data + assert "bmi_status" in data + assert "target_weight" in data + assert "estimated_days" in data +``` + +### Step 2: 运行测试确认失败 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_bmi_analysis_endpoint -v +``` +预期: FAIL - 404 Not Found + +### Step 3: 实现 API 端点 + +在 `src/vitals/web/app.py` 第 1163 行后添加: + +```python +@app.get("/api/bmi/analysis") +async def get_bmi_analysis(): + """获取 BMI 分析数据""" + active_user = db.get_active_user() + if not active_user: + raise HTTPException(status_code=400, detail="没有激活的用户") + + config = db.get_config(active_user.id) + + # 获取最新体重记录 + today = date.today() + weight_records = db.get_weight_records( + start_date=today - timedelta(days=30), + end_date=today, + user_id=active_user.id + ) + current_weight = weight_records[0].weight_kg if weight_records else config.weight + + # 计算 BMI + height_m = (config.height or 170) / 100 + current_bmi = round(current_weight / (height_m * height_m), 1) if current_weight else None + + # BMI 状态 + def get_bmi_status(bmi): + if bmi is None: + return None + if bmi < 18.5: + return {"status": "偏瘦", "color": "#60A5FA", "range": "<18.5"} + elif bmi < 24: + return {"status": "正常", "color": "#34D399", "range": "18.5-24"} + elif bmi < 28: + return {"status": "偏重", "color": "#FBBF24", "range": "24-28"} + else: + return {"status": "肥胖", "color": "#F87171", "range": ">28"} + + bmi_info = get_bmi_status(current_bmi) + + # 目标体重(从用户配置或默认 BMI 22 计算) + target_weight = getattr(config, 'target_weight', None) + if not target_weight: + # 默认目标 BMI 22 + target_weight = round(22 * height_m * height_m, 1) + + target_bmi = round(target_weight / (height_m * height_m), 1) + weight_diff = round(current_weight - target_weight, 1) if current_weight else None + + # 计算预估达成天数 + estimated_days = None + daily_deficit = None + calculation_basis = None + + if current_weight and target_weight and config.tdee: + # 获取近30天饮食数据 + meals = db.get_meals( + start_date=today - timedelta(days=30), + end_date=today, + user_id=active_user.id + ) + # 获取近30天运动数据 + exercises = db.get_exercises( + start_date=today - timedelta(days=30), + end_date=today, + user_id=active_user.id + ) + + # 计算平均每日摄入 + total_intake = sum(m.calories for m in meals) + days_with_data = len(set(m.date for m in meals)) or 1 + avg_daily_intake = total_intake / days_with_data + + # 计算平均每日运动消耗 + total_exercise = sum(e.calories for e in exercises) + avg_daily_exercise = total_exercise / 30 + + # 每日净消耗 = TDEE - 摄入 + 运动 + daily_deficit = round(config.tdee - avg_daily_intake + avg_daily_exercise) + + if daily_deficit > 0 and weight_diff > 0: + # 7700 卡 ≈ 1 kg + estimated_days = round(weight_diff * 7700 / daily_deficit) + calculation_basis = { + "tdee": config.tdee, + "avg_intake": round(avg_daily_intake), + "avg_exercise": round(avg_daily_exercise), + "daily_deficit": daily_deficit + } + + return { + "current_weight": current_weight, + "current_bmi": current_bmi, + "bmi_status": bmi_info, + "height": config.height, + "target_weight": target_weight, + "target_bmi": target_bmi, + "weight_diff": weight_diff, + "estimated_days": estimated_days, + "calculation_basis": calculation_basis, + "has_sufficient_data": bool(calculation_basis) + } +``` + +### Step 4: 运行测试确认通过 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_bmi_analysis_endpoint -v +``` +预期: PASS + +### Step 5: 提交 + +```bash +git add src/vitals/web/app.py tests/test_web.py +git commit -m "feat(api): 添加 BMI 分析端点 /api/bmi/analysis" +``` + +--- + +## Task 2: API - 营养建议端点 + +**Files:** +- Modify: `src/vitals/web/app.py` (在 BMI API 后添加) +- Test: `tests/test_web.py` + +### Step 1: 写失败测试 + +```python +def test_nutrition_recommendations_endpoint(client): + """测试营养建议 API""" + response = client.get("/api/nutrition/recommendations") + assert response.status_code == 200 + data = response.json() + assert "daily_targets" in data + assert "today_intake" in data + assert "gaps" in data + assert "suggestions" in data +``` + +### Step 2: 运行测试确认失败 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_nutrition_recommendations_endpoint -v +``` +预期: FAIL - 404 Not Found + +### Step 3: 实现 API 端点 + +```python +@app.get("/api/nutrition/recommendations") +async def get_nutrition_recommendations(): + """获取营养建议数据""" + active_user = db.get_active_user() + if not active_user: + raise HTTPException(status_code=400, detail="没有激活的用户") + + config = db.get_config(active_user.id) + tdee = config.tdee or 1800 + + # 根据目标调整热量目标 + goal_multipliers = { + "lose": 0.8, # 减重: 80% TDEE + "maintain": 1.0, # 维持: 100% TDEE + "gain": 1.15 # 增重: 115% TDEE + } + calorie_target = int(tdee * goal_multipliers.get(config.goal, 1.0)) + + # 营养素目标 (基于 TDEE) + # 蛋白质 25%, 碳水 50%, 脂肪 25% + daily_targets = { + "calories": calorie_target, + "protein": round(calorie_target * 0.25 / 4), # 每克蛋白质 4 卡 + "carbs": round(calorie_target * 0.50 / 4), # 每克碳水 4 卡 + "fat": round(calorie_target * 0.25 / 9), # 每克脂肪 9 卡 + } + + # 建议区间 + recommended_ranges = { + "protein": {"min": round(calorie_target * 0.20 / 4), "max": round(calorie_target * 0.30 / 4)}, + "carbs": {"min": round(calorie_target * 0.45 / 4), "max": round(calorie_target * 0.55 / 4)}, + "fat": {"min": round(calorie_target * 0.20 / 9), "max": round(calorie_target * 0.30 / 9)}, + } + + # 今日摄入 + today = date.today() + today_meals = db.get_meals(start_date=today, end_date=today, user_id=active_user.id) + + today_intake = { + "calories": sum(m.calories for m in today_meals), + "protein": sum(m.protein or 0 for m in today_meals), + "carbs": sum(m.carbs or 0 for m in today_meals), + "fat": sum(m.fat or 0 for m in today_meals), + } + + # 计算差距 + gaps = { + "calories": daily_targets["calories"] - today_intake["calories"], + "protein": daily_targets["protein"] - today_intake["protein"], + "carbs": daily_targets["carbs"] - today_intake["carbs"], + "fat": daily_targets["fat"] - today_intake["fat"], + } + + # 餐次分布 + meal_type_calories = {} + for m in today_meals: + meal_type_calories[m.meal_type] = meal_type_calories.get(m.meal_type, 0) + m.calories + + meal_distribution = { + "早餐": {"actual": meal_type_calories.get("早餐", 0), "recommended_pct": "25-30%"}, + "午餐": {"actual": meal_type_calories.get("午餐", 0), "recommended_pct": "35-40%"}, + "晚餐": {"actual": meal_type_calories.get("晚餐", 0), "recommended_pct": "25-30%"}, + "加餐": {"actual": meal_type_calories.get("加餐", 0), "recommended_pct": "0-10%"}, + } + + # 生成建议 + suggestions = [] + if gaps["protein"] > 20: + suggestions.append({ + "type": "protein", + "message": f"蛋白质缺口较大 ({gaps['protein']}g),建议增加:", + "options": [ + "鸡胸肉 150g (+46g 蛋白质, 165 卡)", + "鸡蛋 3 个 (+18g 蛋白质, 210 卡)", + "豆腐 200g (+16g 蛋白质, 160 卡)", + ] + }) + if gaps["calories"] > 300: + suggestions.append({ + "type": "calories", + "message": f"热量摄入不足 ({gaps['calories']} 卡),注意补充能量", + "options": [] + }) + + # 近 7 天趋势 + week_start = today - timedelta(days=6) + week_meals = db.get_meals(start_date=week_start, end_date=today, user_id=active_user.id) + + weekly_trend = [] + for i in range(7): + day = week_start + timedelta(days=i) + day_meals = [m for m in week_meals if m.date == day] + weekly_trend.append({ + "date": day.isoformat(), + "calories": sum(m.calories for m in day_meals), + "protein": sum(m.protein or 0 for m in day_meals), + "carbs": sum(m.carbs or 0 for m in day_meals), + "fat": sum(m.fat or 0 for m in day_meals), + }) + + return { + "tdee": tdee, + "goal": config.goal, + "daily_targets": daily_targets, + "recommended_ranges": recommended_ranges, + "today_intake": today_intake, + "gaps": gaps, + "meal_distribution": meal_distribution, + "suggestions": suggestions, + "weekly_trend": weekly_trend, + } +``` + +### Step 4: 运行测试确认通过 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_nutrition_recommendations_endpoint -v +``` +预期: PASS + +### Step 5: 提交 + +```bash +git add src/vitals/web/app.py tests/test_web.py +git commit -m "feat(api): 添加营养建议端点 /api/nutrition/recommendations" +``` + +--- + +## Task 3: API - 睡眠评估端点 + +**Files:** +- Modify: `src/vitals/web/app.py` (在营养 API 后添加) +- Test: `tests/test_web.py` + +### Step 1: 写失败测试 + +```python +def test_sleep_assessment_endpoint(client): + """测试睡眠评估 API""" + response = client.get("/api/sleep/assessment") + assert response.status_code == 200 + data = response.json() + assert "avg_duration" in data + assert "status" in data + assert "health_impacts" in data + assert "ideal_days_count" in data +``` + +### Step 2: 运行测试确认失败 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_sleep_assessment_endpoint -v +``` +预期: FAIL - 404 Not Found + +### Step 3: 实现 API 端点 + +```python +@app.get("/api/sleep/assessment") +async def get_sleep_assessment(): + """获取睡眠评估数据""" + active_user = db.get_active_user() + if not active_user: + raise HTTPException(status_code=400, detail="没有激活的用户") + + today = date.today() + week_start = today - timedelta(days=6) + month_start = today - timedelta(days=29) + + # 获取近 7 天和近 30 天睡眠数据 + week_records = db.get_sleep_records(start_date=week_start, end_date=today, user_id=active_user.id) + month_records = db.get_sleep_records(start_date=month_start, end_date=today, user_id=active_user.id) + + # 计算平均睡眠时长 + week_durations = [r.duration for r in week_records] + month_durations = [r.duration for r in month_records] + + avg_week = sum(week_durations) / len(week_durations) if week_durations else 0 + avg_month = sum(month_durations) / len(month_durations) if month_durations else 0 + + # 判断睡眠状态 + def get_sleep_status(avg_hours): + if avg_hours < 5: + return { + "level": "severe", + "label": "严重不足", + "color": "#EF4444", + "icon": "alert-circle" + } + elif avg_hours < 6: + return { + "level": "insufficient", + "label": "睡眠不足", + "color": "#F59E0B", + "icon": "alert-triangle" + } + elif avg_hours <= 9: + return { + "level": "ideal", + "label": "睡眠充足", + "color": "#10B981", + "icon": "check-circle" + } + else: + return { + "level": "excessive", + "label": "睡眠过多", + "color": "#3B82F6", + "icon": "info" + } + + status = get_sleep_status(avg_week) + + # 统计理想天数 + ideal_days = sum(1 for d in week_durations if 7 <= d <= 9) + insufficient_days = sum(1 for d in week_durations if d < 7) + + month_ideal_days = sum(1 for d in month_durations if 7 <= d <= 9) + month_insufficient_days = sum(1 for d in month_durations if d < 7) + + # 健康影响信息 + warning_impacts = { + "cognitive": { + "title": "认知能力", + "effects": ["注意力下降 40%", "记忆力减退", "决策能力受损"] + }, + "physical": { + "title": "身体健康", + "effects": ["免疫力下降", "肥胖风险增加 33%", "心血管疾病风险上升"] + }, + "emotional": { + "title": "情绪状态", + "effects": ["焦虑抑郁风险增加", "情绪波动大", "压力耐受力降低"] + }, + "exercise": { + "title": "运动表现", + "effects": ["反应速度下降", "肌肉恢复减慢", "受伤风险增加"] + } + } + + benefit_impacts = { + "cognitive": { + "title": "认知提升", + "effects": ["记忆巩固增强", "专注力提高", "创造力活跃"] + }, + "physical": { + "title": "身体修复", + "effects": ["免疫系统强化", "肌肉组织修复", "激素分泌平衡"] + }, + "emotional": { + "title": "情绪稳定", + "effects": ["情绪调节能力增强", "压力抵抗力提升", "心态积极"] + }, + "exercise": { + "title": "运动增益", + "effects": ["运动表现提升 15%", "恢复速度加快", "耐力增强"] + } + } + + health_impacts = warning_impacts if status["level"] in ["severe", "insufficient"] else benefit_impacts + + # 建议 + if status["level"] == "severe": + suggestion = "建议立即调整作息,每天提前 1 小时入睡" + elif status["level"] == "insufficient": + suggestion = "尝试提前 30 分钟入睡,逐步调整作息" + elif status["level"] == "ideal": + suggestion = "继续保持,规律作息是健康的基石!" + else: + suggestion = "睡眠时间过长也可能影响健康,建议控制在 7-9 小时" + + # 距理想的差距 + ideal_target = 7.5 + gap_hours = round(ideal_target - avg_week, 1) if avg_week < 7 else 0 + + return { + "avg_duration": round(avg_week, 1), + "avg_duration_month": round(avg_month, 1), + "status": status, + "gap_hours": gap_hours, + "ideal_days_count": ideal_days, + "insufficient_days_count": insufficient_days, + "total_days": len(week_durations), + "month_stats": { + "ideal_days": month_ideal_days, + "insufficient_days": month_insufficient_days, + "total_days": len(month_durations) + }, + "health_impacts": health_impacts, + "suggestion": suggestion, + "thresholds": { + "severe": 5, + "insufficient": 6, + "ideal_min": 7, + "ideal_max": 9 + } + } +``` + +### Step 4: 运行测试确认通过 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/test_web.py::test_sleep_assessment_endpoint -v +``` +预期: PASS + +### Step 5: 提交 + +```bash +git add src/vitals/web/app.py tests/test_web.py +git commit -m "feat(api): 添加睡眠评估端点 /api/sleep/assessment" +``` + +--- + +## Task 4: UI - 运动页面 BMI 分析卡片 + +**Files:** +- Modify: `src/vitals/web/app.py:4104` (`get_exercise_page_html` 函数) + +### Step 1: 定位插入位置 + +在 `get_exercise_page_html()` 函数中,找到统计卡片 grid 结束位置(约第 4404 行 `` 之后),插入 BMI 分析卡片。 + +### Step 2: 添加 BMI 分析卡片 HTML + +在第 4404 行后插入: + +```html + +
+
+

BMI 健康分析

+ 设置目标 → +
+
+
加载中...
+
+
+``` + +### Step 3: 添加 JavaScript 加载逻辑 + +在 `loadExerciseStats()` 函数后添加 `loadBmiAnalysis()` 函数: + +```javascript + async function loadBmiAnalysis() { + try { + const res = await fetch('/api/bmi/analysis'); + const data = await res.json(); + const container = document.getElementById('bmi-content'); + + if (!data.current_bmi) { + container.innerHTML = ` +
+

⚠️ 数据不足

+

请完善身高、体重信息以启用 BMI 分析

+ 去设置 +
+ `; + return; + } + + const statusColor = data.bmi_status?.color || '#94A3B8'; + const bmiPercent = Math.min(Math.max((data.current_bmi - 15) / 20 * 100, 0), 100); + + container.innerHTML = ` +
+
+
当前体重
+
${data.current_weight || '--'} kg
+
+
+
当前 BMI
+
${data.current_bmi || '--'}
+
+
+
BMI 状态
+
● ${data.bmi_status?.status || '--'}
+
(${data.bmi_status?.range || ''})
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
偏瘦 <18.5
+
正常
+
偏重
+
肥胖 >28
+
+
+ + +
+
🎯 目标达成预估
+
+
目标体重: ${data.target_weight} kg
+
目标 BMI: ${data.target_bmi} (正常)
+
距离目标: ${data.weight_diff > 0 ? '-' : '+'}${Math.abs(data.weight_diff)} kg
+
预估达成: ${data.estimated_days ? '约 ' + data.estimated_days + ' 天' : '数据不足'}
+
+ ${data.calculation_basis ? ` +
+ 计算依据: 近30天平均每日净消耗 ${data.calculation_basis.daily_deficit} 卡
+ (TDEE ${data.calculation_basis.tdee} - 摄入 ${data.calculation_basis.avg_intake} + 运动 ${data.calculation_basis.avg_exercise}) +
+ ` : ''} +
+ +
+ 💡 健康减重建议每周 0.5-1 kg +
+ `; + } catch (e) { + console.error('加载 BMI 分析失败:', e); + } + } +``` + +### Step 4: 在页面加载时调用 + +在 `DOMContentLoaded` 事件中添加调用: + +```javascript + document.addEventListener('DOMContentLoaded', () => { + loadExerciseStats(); + loadExerciseRecords(); + loadTodaySteps(); + loadBmiAnalysis(); // 添加这行 + }); +``` + +### Step 5: 提交 + +```bash +git add src/vitals/web/app.py +git commit -m "feat(ui): 运动页面添加 BMI 健康分析卡片" +``` + +--- + +## Task 5: UI - 饮食页面营养建议 + +**Files:** +- Modify: `src/vitals/web/app.py:4745` (`get_meal_page_html` 函数) + +### Step 1: 添加今日营养状况卡片 + +在饮食页面统计卡片后(约第 5060 行)添加: + +```html + +
+

🍽 今日营养状况

+
+
加载中...
+
+
+``` + +### Step 2: 修改近 7 天趋势图 + +更新趋势图以包含参考线,在 Chart.js 配置中添加 annotation 插件支持。 + +### Step 3: 添加 JavaScript 加载逻辑 + +```javascript + async function loadNutritionRecommendations() { + try { + const res = await fetch('/api/nutrition/recommendations'); + const data = await res.json(); + const container = document.getElementById('nutrition-status-content'); + + const calcPercent = (actual, target) => Math.min(Math.round(actual / target * 100), 100); + const getColor = (percent) => percent >= 80 ? '#10B981' : percent >= 50 ? '#F59E0B' : '#EF4444'; + + const nutrients = [ + { key: 'calories', label: '热量', unit: '卡', target: data.daily_targets.calories, actual: data.today_intake.calories }, + { key: 'protein', label: '蛋白质', unit: 'g', target: data.daily_targets.protein, actual: data.today_intake.protein }, + { key: 'carbs', label: '碳水', unit: 'g', target: data.daily_targets.carbs, actual: data.today_intake.carbs }, + { key: 'fat', label: '脂肪', unit: 'g', target: data.daily_targets.fat, actual: data.today_intake.fat }, + ]; + + container.innerHTML = ` +
+ ${nutrients.map(n => { + const pct = calcPercent(n.actual, n.target); + const color = getColor(pct); + return ` +
+
${n.label}
+
${n.actual}
+
/${n.target}${n.unit}
+
+
+
+
${pct}%
+
+ `; + }).join('')} +
+ + ${data.suggestions.length > 0 ? ` +
+
💡 晚餐建议
+ ${data.suggestions.map(s => ` +
${s.message}
+ ${s.options.length > 0 ? ` + + ` : ''} + `).join('')} +
+ ` : ` +
+ ✅ 今日营养摄入均衡 +
+ `} + `; + + // 更新趋势图参考线 + updateIntakeChartWithReference(data); + } catch (e) { + console.error('加载营养建议失败:', e); + } + } + + function updateIntakeChartWithReference(data) { + // 在 loadMealStats 中的 intake-chart 添加参考线 + const target = data.daily_targets.calories; + window.nutritionTarget = target; + } +``` + +### Step 4: 修改趋势图添加参考线 + +在原有的 `loadMealStats()` 函数中修改 intake-chart 的配置: + +```javascript + // 近 7 天趋势图 - 添加参考线 + const intakeCtx = document.getElementById('intake-chart').getContext('2d'); + const targetLine = window.nutritionTarget || 1800; + new Chart(intakeCtx, { + type: 'line', + data: { + labels: last7Days.map(d => d.slice(5)), + datasets: [ + { + label: '实际摄入', + data: dailyCalories, + borderColor: '#10B981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + fill: true, + tension: 0.3, + }, + { + label: '建议摄入', + data: Array(7).fill(targetLine), + borderColor: '#F59E0B', + borderDash: [5, 5], + pointRadius: 0, + fill: false, + } + ] + }, + options: { + responsive: true, + scales: { y: { beginAtZero: true } }, + plugins: { + annotation: { + annotations: { + targetZone: { + type: 'box', + yMin: targetLine * 0.9, + yMax: targetLine * 1.1, + backgroundColor: 'rgba(16, 185, 129, 0.1)', + borderWidth: 0 + } + } + } + } + } + }); +``` + +### Step 5: 提交 + +```bash +git add src/vitals/web/app.py +git commit -m "feat(ui): 饮食页面添加营养建议区间与配餐建议" +``` + +--- + +## Task 6: UI - 睡眠页面预警卡片 + +**Files:** +- Modify: `src/vitals/web/app.py:5602` (`get_sleep_page_html` 函数) + +### Step 1: 添加睡眠评估卡片 + +在统计卡片后(约第 5835 行)添加: + +```html + +
+

😴 睡眠健康评估

+
+
加载中...
+
+
+ + + +``` + +### Step 2: 添加 JavaScript 加载逻辑 + +```javascript + async function loadSleepAssessment() { + try { + const res = await fetch('/api/sleep/assessment'); + const data = await res.json(); + + // 渲染评估卡片 + const assessmentContainer = document.getElementById('sleep-assessment-content'); + const statusColor = data.status.color; + const avgPercent = Math.min(Math.max((data.avg_duration - 4) / 6 * 100, 0), 100); + + assessmentContainer.innerHTML = ` +
+
近 7 天平均睡眠
+
${data.avg_duration} 小时
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
偏少 <5h
+
不足 5-6h
+
理想 7-9h
+
过多 >9h
+
+
+ +
+
+ ● ${data.status.label} + ${data.gap_hours > 0 ? `距理想还差 ${data.gap_hours} 小时/天` : ''} +
+
+ 本周: 理想 ${data.ideal_days_count}/${data.total_days} 天 +
+
+ `; + + // 渲染影响卡片 + const impactCard = document.getElementById('sleep-impact-card'); + const impactContainer = document.getElementById('sleep-impact-content'); + + const isWarning = data.status.level === 'severe' || data.status.level === 'insufficient'; + const bgColor = isWarning ? 'rgba(127, 29, 29, 0.2)' : 'rgba(6, 95, 70, 0.2)'; + const borderColor = isWarning ? 'rgba(239, 68, 68, 0.3)' : 'rgba(16, 185, 129, 0.3)'; + const titleColor = isWarning ? '#F87171' : '#34D399'; + const title = isWarning ? '⚠️ 睡眠不足警告' : '✅ 睡眠状态良好'; + const intro = isWarning + ? `您近 7 天平均睡眠仅 ${data.avg_duration} 小时,低于健康标准 7 小时。
长期睡眠不足会对身体造成以下影响:` + : `您近 7 天平均睡眠 ${data.avg_duration} 小时,处于健康范围内!
充足睡眠为您带来以下益处:`; + + impactCard.style.display = 'block'; + impactCard.style.background = bgColor; + impactCard.style.border = `1px solid ${borderColor}`; + + const impacts = data.health_impacts; + impactContainer.innerHTML = ` +

${title}

+

${intro}

+ +
+ ${Object.values(impacts).map(impact => ` +
+
${impact.title}
+ +
+ `).join('')} +
+ +
+ 💡 ${data.suggestion} +
+ `; + } catch (e) { + console.error('加载睡眠评估失败:', e); + } + } +``` + +### Step 3: 修改睡眠趋势图添加理想区间 + +在原有的 `loadSleepData()` 函数中修改 duration-chart 配置: + +```javascript + // 添加理想睡眠区间背景 + new Chart(durationCtx, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: '睡眠时长(小时)', + data: durationData, + borderColor: '#6d28d9', + backgroundColor: 'rgba(109, 40, 217, 0.2)', + fill: true, + tension: 0.3, + pointBackgroundColor: durationData.map(d => + d >= 7 && d <= 9 ? '#10B981' : d < 6 ? '#EF4444' : '#F59E0B' + ), + } + ] + }, + options: { + responsive: true, + scales: { y: { beginAtZero: true, max: 12 } }, + plugins: { + annotation: { + annotations: { + idealZone: { + type: 'box', + yMin: 7, + yMax: 9, + backgroundColor: 'rgba(16, 185, 129, 0.1)', + borderWidth: 0, + label: { + content: '理想区间', + display: true, + position: 'start' + } + } + } + } + } + } + }); +``` + +### Step 4: 在页面加载时调用 + +```javascript + document.addEventListener('DOMContentLoaded', () => { + loadSleepData(); + loadSleepAssessment(); // 添加这行 + }); +``` + +### Step 5: 提交 + +```bash +git add src/vitals/web/app.py +git commit -m "feat(ui): 睡眠页面添加健康评估与预警卡片" +``` + +--- + +## 最终验收 + +### 功能测试 + +1. **运动页面**: 访问 `/exercise`,确认 BMI 分析卡片正确显示 +2. **饮食页面**: 访问 `/meal`,确认营养状况卡片和趋势参考线 +3. **睡眠页面**: 访问 `/sleep`,确认睡眠评估和警告/益处卡片 + +### 运行全部测试 + +```bash +cd /Users/rocky/Projects/vitals && python -m pytest tests/ -v +``` + +### 最终提交 + +```bash +git add . +git commit -m "feat: 完成健康模块增强 - BMI分析/营养建议/睡眠预警" +```