Ver código fonte

feat(frontend): refine health modules — dashboard, diet, exercise, monitoring and weight

17846405080 1 mês atrás
pai
commit
d245779d6a
38 arquivos alterados com 6681 adições e 2355 exclusões
  1. 201 169
      campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.html
  2. 603 13
      campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.scss
  3. 256 55
      campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.ts
  4. 42 28
      campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-history/diet-history.component.html
  5. 219 48
      campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-history/diet-history.component.scss
  6. 174 17
      campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-history/diet-history.component.ts
  7. 91 0
      campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-mobile.component.html
  8. 289 0
      campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-mobile.component.scss
  9. 2 2
      campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet.component.ts
  10. 6 5
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.ts
  11. 1 1
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-actions/exercise-actions.component.html
  12. 374 4
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-actions/exercise-actions.component.ts
  13. 123 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-detail/exercise-detail.component.html
  14. 282 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-detail/exercise-detail.component.scss
  15. 26 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-detail/exercise-detail.component.ts
  16. 12 31
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-history/exercise-history.component.html
  17. 2 1
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-history/exercise-history.component.scss
  18. 40 4
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-history/exercise-history.component.ts
  19. 18 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise.routes.ts
  20. 98 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/fitness-summary/fitness-summary.component.html
  21. 295 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/fitness-summary/fitness-summary.component.scss
  22. 160 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/fitness-summary/fitness-summary.component.ts
  23. 117 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/steps-detail/steps-detail.component.html
  24. 257 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/steps-detail/steps-detail.component.scss
  25. 18 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/steps-detail/steps-detail.component.ts
  26. 317 547
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html
  27. 0 171
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.scss
  28. 266 393
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts
  29. 77 0
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-history/weight-history.component.html
  30. 368 0
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-history/weight-history.component.scss
  31. 249 0
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-history/weight-history.component.ts
  32. 214 0
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-mobile.component.html
  33. 539 0
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-mobile.component.scss
  34. 168 162
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.html
  35. 609 614
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.scss
  36. 61 86
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.ts
  37. 6 0
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.routes.ts
  38. 101 4
      campus_health_app/frontend/campus-health-app/src/app/services/weight-data.service.ts

+ 201 - 169
campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.html

@@ -1,6 +1,6 @@
 <div class="dashboard-container">
   <!-- 顶部标题栏 -->
-  <div class="top-header">
+  <div class="top-header" *ngIf="false">
     <header>
       <div class="title-group">
         <h1>校园健康仪表盘</h1>
@@ -22,213 +22,245 @@
 
   <!-- 主内容区域 -->
   <div class="main-content">
-    <!-- 功能卡片网格 -->
-    <section class="cards-grid">
-      <!-- 体重管理 -->
-      <article class="feature-card" (click)="navigateTo('weight')">
-        <div class="feature-header">
-          <div>
-            <div class="feature-title">体重管理</div>
-            <div class="feature-sub">记录体重 / 查看 BMI 和趋势</div>
-          </div>
-          <div class="pill"><span class="pill-dot"></span>已开启目标</div>
-        </div>
-        <div class="feature-main">BMI {{ weightData.bmi }}<span class="unit"> {{ weightData.bmiStatus }}</span></div>
-        <div class="feature-tags">
-          <span class="feature-tag">今日体重:{{ weightData.currentWeight }} kg · 目标 {{ weightData.targetWeight }} kg</span>
-          <span class="feature-tag">一周内变化 {{ weightData.weeklyWeightChange }} kg · 本周已记录 {{ weightData.weeklyRecords }} 天</span>
-          <span class="feature-tag">最近记录:{{ weightData.lastRecord }} · 距离目标 {{ weightData.targetDiff }} kg</span>
+    <div class="home-shell">
+      <header class="home-header">
+        <div>
+          <p class="home-hello">
+            <span>你好,郑同学</span>
+            <span>今天感觉如何?</span>
+          </p>
+          <p class="home-sub">下面是你今天最重要的三个数字:体重 · 运动 · 饮食</p>
         </div>
-        <div class="feature-footer">
-          <span>建议:{{ weightData.suggestion }}</span>
-          <button (click)="navigateTo('weight'); $event.stopPropagation()">进入体重管理</button>
+        <div class="home-avatar" (click)="openAiChatModal()" style="cursor: pointer; padding: 0;">
+          <img src="assets/images/black-cat.png" alt="AI Chat" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">
         </div>
-      </article>
+      </header>
 
-      <!-- 睡眠质量 -->
-      <article class="feature-card" (click)="showSleepDetails()">
-        <div class="feature-header">
-          <div>
-            <div class="feature-title">睡眠质量</div>
-            <div class="feature-sub">入睡时间、深睡时长与睡眠评分</div>
-          </div>
-          <div class="feature-icon">💤</div>
-        </div>
-        <div class="feature-main">{{ sleepCardData.sleepScore }} / 100<span class="unit"> {{ sleepCardData.sleepStatus }}</span></div>
-        <div class="feature-tags">
-          <span class="feature-tag">昨晚睡眠:{{ sleepCardData.sleepHours }} 小时 {{ sleepCardData.sleepMinutes }} 分</span>
-          <span class="feature-tag">近 7 天平均:{{ sleepCardData.avgSleepHours }} 小时</span>
-          <span class="feature-tag">平均入睡:{{ sleepCardData.avgBedtime }}</span>
-          <span class="feature-tag">深睡:{{ sleepCardData.deepSleepDuration }} 小时 · 连续早睡 {{ sleepCardData.earlyBedStreak }} 天</span>
-        </div>
-        <div class="feature-footer">
-          <span>小贴士:{{ sleepCardData.tip }}</span>
-          <button (click)="showSleepDetails(); $event.stopPropagation()">查看睡眠详情</button>
+      <section class="home-section">
+        <p class="home-section-title">关键数字一览</p>
+
+        <div class="home-kpi-row">
+          <article class="home-kpi-card" (click)="navigateTo('weight')">
+            <div class="home-kpi-label">
+              <div class="home-kpi-icon">⚖️</div>
+              <span>今日体重</span>
+            </div>
+            <div class="home-kpi-main">
+              <span>{{ weightData.currentWeight }}</span>
+              <span class="home-kpi-unit">kg</span>
+            </div>
+            <div class="home-kpi-extra">
+              目标 {{ weightData.targetWeight }}kg · 差 {{ weightData.targetDiff }}kg
+            </div>
+          </article>
+
+          <article class="home-kpi-card" (click)="navigateTo('exercise/detail')">
+            <div class="home-kpi-label">
+              <div class="home-kpi-icon">🏃</div>
+              <span>今日运动</span>
+            </div>
+            <div class="home-kpi-main">
+              <span>{{ healthStats.exerciseMinutes }}</span>
+              <span class="home-kpi-unit">分钟</span>
+            </div>
+            <div class="home-kpi-extra">
+              {{ healthStats.steps }} 步 · 完成目标的 {{ healthStats.stepProgress }}%
+            </div>
+          </article>
+
+          <article class="home-kpi-card" (click)="navigateTo('diet')">
+            <div class="home-kpi-label">
+              <div class="home-kpi-icon">🥗</div>
+              <span>饮食热量</span>
+            </div>
+            <div class="home-kpi-main">
+              <span>{{ dietData.calories }}</span>
+              <span class="home-kpi-unit">kcal</span>
+            </div>
+            <div class="home-kpi-extra">
+              目标 {{ dietData.targetCalories }}kcal · 剩余 {{ dietData.remainingCalories }}kcal
+            </div>
+          </article>
         </div>
-      </article>
+      </section>
 
-      <!-- 饮食管理 -->
-      <article class="feature-card" (click)="navigateTo('diet')">
-        <div class="feature-header">
-          <div>
-            <div class="feature-title">饮食管理</div>
-            <div class="feature-sub">记录三餐、热量与营养结构</div>
+      <section class="home-ring-card" (click)="navigateTo('monitoring')" style="cursor: pointer;">
+        <div class="home-ring-container">
+          <div class="home-ring">
+            <div class="home-ring-inner">
+              <div class="home-ring-main">{{ healthStats.stepProgress }}%</div>
+            </div>
           </div>
-          <div class="feature-icon">🥗</div>
+          <div class="home-ring-sub">今日进度</div>
         </div>
-        <div class="feature-main">{{ dietData.calories }} kcal<span class="unit"> 今日已摄入</span></div>
-        <div class="feature-tags">
-          <span class="feature-tag">目标:{{ dietData.targetCalories }} kcal · 剩余 {{ dietData.remainingCalories }} kcal</span>
-          <span class="feature-tag">蛋白质:{{ dietData.protein }} g</span>
-          <span class="feature-tag">碳水:{{ dietData.carbs }} g</span>
-          <span class="feature-tag">脂肪:{{ dietData.fat }} g</span>
-        </div>
-        <div class="feature-footer">
-          <span>建议:{{ dietData.suggestion }}</span>
-          <button (click)="navigateTo('diet'); $event.stopPropagation()">录入今日饮食</button>
+        <div>
+          <div class="home-ring-text-title">今天的整体状态不错</div>
+          <div class="home-ring-text-sub">结合体重 · 运动 · 饮食三个维度计算</div>
+          <p class="home-ring-desc">如果在今晚前再<strong>完成 15 分钟中等强度运动</strong>,就可以点亮本周的健康勋章。</p>
+          <div class="home-chips">
+            <span class="home-chip">昨晚睡眠 {{ sleepCardData.sleepHours }} 小时 {{ sleepCardData.sleepMinutes }} 分</span>
+            <span class="home-chip">饮水略少 · 再喝 400ml</span>
+          </div>
         </div>
-      </article>
+      </section>
 
-      <!-- 运动记录 -->
-      <article class="feature-card" (click)="navigateTo('exercise')">
-        <div class="feature-header">
+      <section class="home-stack-section">
+        <article class="home-stack-card" (click)="showSleepDetails()">
+          <div class="home-stack-icon purple">💤</div>
           <div>
-            <div class="feature-title">运动记录</div>
-            <div class="feature-sub">跑步、骑行、力量训练等统一记录</div>
+            <div class="home-stack-text-title">昨晚睡眠</div>
+            <div class="home-stack-text-sub">
+              {{ sleepCardData.sleepHours }} 小时 {{ sleepCardData.sleepMinutes }} 分 · 评分 {{ sleepCardData.sleepScore }} / 100
+            </div>
           </div>
-          <div class="feature-icon">🏃</div>
-        </div>
-        <div class="feature-main">{{ exerciseData.duration }} 分钟<span class="unit"> 今日运动</span></div>
-        <div class="feature-tags">
-          <span class="feature-tag">步数:{{ exerciseData.steps }} · 已完成目标的 {{ exerciseData.stepProgress }}%</span>
-          <span class="feature-tag">跑步:{{ exerciseData.runningDistance }} 公里</span>
-          <span class="feature-tag">本周完成 {{ exerciseData.weeklyProgress }}</span>
-        </div>
-        <div class="feature-footer">
-          <span>下一次计划:{{ exerciseData.nextPlan }}</span>
-          <button (click)="navigateTo('exercise'); $event.stopPropagation()">添加运动记录</button>
-        </div>
-      </article>
+          <div class="home-stack-main-number">
+            {{ sleepCardData.sleepStatus }}<span>睡眠</span>
+          </div>
+        </article>
 
-      <!-- 健康监测 -->
-      <article class="feature-card" (click)="navigateTo('monitoring')">
-        <div class="feature-header">
+        <article class="home-stack-card" (click)="navigateTo('exercise/fitness-summary')">
+          <div class="home-stack-icon green">🔥</div>
           <div>
-            <div class="feature-title">健康监测</div>
-            <div class="feature-sub">血压、心率、体脂等指标总览</div>
+            <div class="home-stack-text-title">体能训练摘要</div>
+            <div class="home-stack-text-sub">上一次训练消耗约 320 千卡 · 功能性力量训练</div>
           </div>
-          <div class="feature-icon">🩺</div>
-        </div>
-        <div class="feature-main">心率 {{ monitoringData.heartRate }}<span class="unit"> 次/分</span></div>
-        <div class="feature-tags">
-          <span class="feature-tag">血压:{{ monitoringData.bloodPressure }} mmHg</span>
-          <span class="feature-tag">体脂率:{{ monitoringData.bodyFat }}%</span>
-          <span class="feature-tag">最近一次体检:{{ monitoringData.lastCheckup }}</span>
-        </div>
-        <div class="feature-footer">
-          <span>状态:{{ monitoringData.status }}</span>
-          <button (click)="navigateTo('monitoring'); $event.stopPropagation()">打开健康监测</button>
-        </div>
-      </article>
+          <div class="home-stack-main-number">
+            {{ exerciseData.duration }}<span>分钟</span>
+          </div>
+        </article>
 
-      <!-- 校园服务 -->
-      <article class="feature-card" (click)="navigateTo('school-services')">
-        <div class="feature-header">
+        <article class="home-stack-card" (click)="navigateTo('exercise/steps')">
+          <div class="home-stack-icon">
+            <span>👣</span>
+          </div>
           <div>
-            <div class="feature-title">校园服务</div>
-            <div class="feature-sub">餐厅推荐、运动组队与健康资讯</div>
+            <div class="home-stack-text-title">今日步数</div>
+            <div class="home-stack-text-sub">总计步数</div>
           </div>
-          <div class="feature-icon">🏫</div>
-        </div>
-        <div class="feature-main">{{ campusData.restaurantCount }} 个餐厅<span class="unit"> 健康推荐</span></div>
-        <div class="feature-tags">
-          <span class="feature-tag" *ngFor="let restaurant of campusData.restaurants">{{ restaurant }}</span>
-        </div>
-        <div class="feature-footer">
-          <span>{{ campusData.activityInfo }}</span>
-          <button (click)="navigateTo('school-services'); $event.stopPropagation()">浏览校园服务</button>
-        </div>
-      </article>
-    </section>
+          <div class="home-stack-main-number">
+            {{ healthStats.steps }}<span>步</span>
+          </div>
+        </article>
+      </section>
+
+    </div>
   </div>
 
-  <footer>
+  <footer *ngIf="false">
     校园健康管理系统 · 卡片总览布局:适合希望在一屏内快速浏览所有健康模块的用户。
   </footer>
 
   <!-- 睡眠详情模态框 -->
   <div *ngIf="showSleepDetailsModal" class="modal-overlay" (click)="closeSleepDetailsModal()">
-    <div class="modal-content" (click)="$event.stopPropagation()">
+    <div class="modal-content sleep-modal" (click)="$event.stopPropagation()">
       <div class="modal-header">
-        <h3>睡眠质量详情</h3>
-        <button class="close-button" (click)="closeSleepDetailsModal()">×</button>
+        <button class="nav-back" (click)="closeSleepDetailsModal()">❮</button>
+        <h3>睡眠</h3>
+        <button class="close-btn" (click)="closeSleepDetailsModal()">×</button>
       </div>
       
       <div class="modal-body">
-        <!-- 睡眠得分 -->
-        <div class="sleep-score-container">
-          <div class="sleep-score">
-            <span class="score-number">{{ sleepData.sleepScore }}</span>
-            <span class="score-max">/100</span>
+        <!-- 视图切换 Tabs -->
+        <div class="view-tabs">
+          <div class="tab-segment">
+            <button [class.active]="sleepViewMode === 'day'" (click)="switchSleepView('day')">日</button>
+            <button [class.active]="sleepViewMode === 'week'" (click)="switchSleepView('week')">周</button>
+            <button [class.active]="sleepViewMode === 'month'" (click)="switchSleepView('month')">月</button>
           </div>
-          <p class="score-comment">{{ sleepData.comment }}</p>
         </div>
-        
-        <!-- 睡眠概览统计 -->
-        <div class="sleep-overview-stats">
-          <div class="stat-item">
-            <span class="stat-label">总睡眠时间</span>
-            <span class="stat-value">{{ sleepData.totalSleepTime }}小时</span>
-          </div>
-          <div class="stat-item">
-            <span class="stat-label">深度睡眠</span>
-            <span class="stat-value">{{ sleepData.deepSleepDuration }}小时</span>
-          </div>
-          <div class="stat-item">
-            <span class="stat-label">浅度睡眠</span>
-            <span class="stat-value">{{ sleepData.lightSleepDuration }}小时</span>
-          </div>
-          <div class="stat-item">
-            <span class="stat-label">快速眼动睡眠</span>
-            <span class="stat-value">{{ sleepData.remSleepDuration }}小时</span>
-          </div>
-          <div class="stat-item">
-            <span class="stat-label">入睡时长</span>
-            <span class="stat-value">{{ sleepData.timeToFallAsleep }}分钟</span>
-          </div>
-          <div class="stat-item">
-            <span class="stat-label">睡眠效率</span>
-            <span class="stat-value">{{ sleepData.sleepEfficiency }}%</span>
+
+        <!-- 睡眠时间概览 -->
+        <div class="sleep-summary-header">
+          <div class="summary-label">{{ sleepTotalLabelTitle }}</div>
+          <div class="summary-time">
+            <span class="time-value">{{ sleepTotalLabel.split('小时')[0] }}</span> 
+            <span class="time-unit">小时</span>
+            <span class="time-value">{{ sleepTotalLabel.split('小时')[1] || '' }}</span>
           </div>
+          <div class="summary-date">{{ currentSleepDateLabel }}</div>
         </div>
-        
-        <!-- 睡眠阶段明细 - 折线图 -->
-        <div class="sleep-stages-chart">
-          <h4>一周睡眠质量趋势</h4>
-          <div class="chart-container">
+
+        <!-- 图表区域 -->
+        <div class="chart-section">
+          <!-- 日视图:自定义甘特图 -->
+          <div class="day-chart-container" *ngIf="sleepViewMode === 'day'">
+            <div class="hypnogram">
+              <!-- Y轴标签 -->
+              <div class="y-labels">
+                <span>快速眼动</span>
+                <span>核心睡眠</span>
+                <span>深度睡眠</span>
+              </div>
+              
+              <!-- 图表内容 -->
+              <div class="hypno-chart">
+                <!-- 模拟的睡眠阶段块 (使用绝对定位或Flex模拟) -->
+                <!-- 简单起见,这里硬编码几个块来模拟效果,实际应由 *ngFor 生成 -->
+                <div class="chart-grid"></div>
+                
+                <!-- 醒 -->
+                <div class="stage-block awake" style="left: 10%; width: 2%;"></div>
+                <div class="stage-block awake" style="left: 45%; width: 3%;"></div>
+                
+                <!-- REM -->
+                <div class="stage-block rem" style="left: 25%; width: 10%;"></div>
+                <div class="stage-block rem" style="left: 70%; width: 15%;"></div>
+
+                <!-- 核心 -->
+                <div class="stage-block core" style="left: 5%; width: 20%;"></div>
+                <div class="stage-block core" style="left: 35%; width: 35%;"></div>
+                <div class="stage-block core" style="left: 85%; width: 10%;"></div>
+
+                <!-- 深度 -->
+                <div class="stage-block deep" style="left: 15%; width: 10%;"></div>
+                <div class="stage-block deep" style="left: 50%; width: 15%;"></div>
+                
+                <!-- X轴时间 -->
+                <div class="x-labels">
+                  <span>00:00</span>
+                  <span>02:00</span>
+                  <span>04:00</span>
+                  <span>06:00</span>
+                  <span>08:00</span>
+                </div>
+              </div>
+            </div>
+            
+            <div class="day-stats-grid">
+              <div class="stat-box rem">
+                <div class="label">快速眼动睡眠</div>
+                <div class="value">{{ sleepData.remSleepDuration }}小时 <span class="percent">16%</span></div>
+              </div>
+              <div class="stat-box core">
+                <div class="label">核心睡眠</div>
+                <div class="value">{{ sleepData.lightSleepDuration }}小时 <span class="percent">56%</span></div>
+              </div>
+              <div class="stat-box deep">
+                <div class="label">深度睡眠</div>
+                <div class="value">{{ sleepData.deepSleepDuration }}小时 <span class="percent">28%</span></div>
+              </div>
+              <div class="stat-box awake">
+                <div class="label">清醒时间</div>
+                <div class="value">{{ sleepData.awakeTime }}分钟 <span class="percent">2%</span></div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 周/月视图:Chart.js 堆叠柱状图 -->
+          <div class="bar-chart-container" *ngIf="sleepViewMode !== 'day'">
             <canvas baseChart
-              [data]="lineChartData"
-              [options]="lineChartOptions"
-              [type]="lineChartType">
+              [data]="sleepChartData"
+              [options]="sleepChartOptions"
+              [type]="sleepChartType">
             </canvas>
           </div>
         </div>
         
-        <!-- 睡眠改善建议 -->
-        <div class="sleep-improvement-tips">
-          <h4>睡眠改善建议</h4>
-          <ul class="tips-list">
-            <li *ngFor="let tip of sleepImprovementTips">
-              <span class="tip-icon">💡</span>
-              <span class="tip-text">{{ tip }}</span>
-            </li>
-          </ul>
+        <!-- 底部信息 -->
+        <div class="more-info">
+          <button>显示更多睡眠数据</button>
         </div>
       </div>
-      
-      <div class="modal-footer">
-        <button class="close-modal-btn" (click)="closeSleepDetailsModal()">关闭</button>
-      </div>
     </div>
   </div>
 

+ 603 - 13
campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.scss

@@ -10,7 +10,7 @@
 
 // 顶部标题栏样式
 .top-header {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  background: linear-gradient(135deg, #01847F 0%, #016b67 100%);
   color: white;
   padding:14px  24px;
   border-radius: 0 0 16px 16px;
@@ -111,16 +111,306 @@ header {
 
   &.primary {
     background: white;
-    color: #4f46e5;
+    color: #01847F;
     box-shadow: 0 4px 12px rgba(255, 255, 255, 0.3);
   }
 }
 
 // 主内容区域样式
 .main-content {
-  max-width: 1200px;
+  max-width: 480px;
   margin: 0 auto;
-  padding: 0 24px 24px;
+  padding: 16px 16px 24px;
+}
+
+.home-shell {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.home-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.home-hello {
+  font-size: 20px;
+  font-weight: 700;
+  color: #0f172a;
+  line-height: 1.25;
+
+  span {
+    display: block;
+  }
+}
+
+.home-sub {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #6b7280;
+}
+
+.home-avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 999px;
+  background: radial-gradient(circle at 30% 20%, #F9D2E4, #01847F);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #f9fafb;
+  font-size: 16px;
+  font-weight: 600;
+  box-shadow: 0 6px 18px rgba(1, 132, 127, 0.55);
+}
+
+.home-section {
+  margin-top: 4px;
+}
+
+.home-section-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #0f172a;
+  letter-spacing: 0.02em;
+}
+
+.home-section-sub {
+  margin-top: 2px;
+  font-size: 11px;
+  color: #6b7280;
+}
+
+.home-kpi-row {
+  margin-top: 10px;
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 10px;
+}
+
+.home-kpi-card {
+  border-radius: 16px;
+  background: #ffffff;
+  box-shadow: 0 10px 25px rgba(15, 23, 42, 0.06);
+  padding: 8px 9px;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  cursor: pointer;
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 12px 30px rgba(15, 23, 42, 0.1);
+  }
+}
+
+.home-kpi-label {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 11px;
+  color: #6b7280;
+}
+
+.home-kpi-icon {
+  width: 18px;
+  height: 18px;
+  border-radius: 999px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 11px;
+  background: #E6F7F6;
+  color: #01847F;
+}
+
+.home-kpi-main {
+  display: flex;
+  align-items: baseline;
+  gap: 2px;
+  font-size: 18px;
+  font-weight: 700;
+  color: #0f172a;
+}
+
+.home-kpi-unit {
+  font-size: 11px;
+  color: #6b7280;
+}
+
+.home-kpi-extra {
+  font-size: 10px;
+  color: #6b7280;
+}
+
+.home-ring-card {
+  margin-top: 4px;
+  border-radius: 20px;
+  background: linear-gradient(145deg, #01847F 0%, #016b67 100%);
+  padding: 14px 14px 12px;
+  display: grid;
+  grid-template-columns: auto 1fr;
+  gap: 12px;
+  align-items: center;
+  box-shadow: 0 14px 36px rgba(1, 132, 127, 0.55);
+}
+
+.home-ring-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px; /* Space between ring and text */
+}
+
+.home-ring {
+  width: 92px;
+  height: 92px;
+  border-radius: 50%;
+  background: conic-gradient(from 220deg, #F9D2E4 0 140deg, rgba(255,255,255,0.1) 140deg 360deg);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.home-ring::after {
+  content: '';
+  position: absolute;
+  inset: 18px;
+  border-radius: 50%;
+  background: #017571; /* Slightly darker than card bg */
+}
+
+.home-ring-inner {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #e5e7eb;
+  gap: 2px;
+}
+
+.home-ring-main {
+  font-size: 20px;
+  font-weight: 700;
+}
+
+.home-ring-sub {
+  font-size: 10px;
+  color: #9ca3af;
+}
+
+.home-ring-text-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #f9fafb;
+  margin-bottom: 4px;
+}
+
+.home-ring-text-sub {
+  font-size: 11px;
+  color: #cbd5f5;
+  margin-bottom: 6px;
+}
+
+.home-ring-desc {
+  font-size: 11px;
+  color: #e5e7eb;
+
+  strong {
+    color: #facc15;
+  }
+}
+
+.home-chips {
+  margin-top: 6px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.home-chip {
+  border-radius: 999px;
+  padding: 3px 8px;
+  font-size: 10px;
+  background: rgba(15, 23, 42, 0.55);
+  color: #e5e7eb;
+  border: 1px solid rgba(148, 163, 184, 0.4);
+}
+
+.home-stack-section {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.home-stack-card {
+  border-radius: 18px;
+  background: #ffffff;
+  padding: 10px 12px;
+  display: grid;
+  grid-template-columns: auto 1fr auto;
+  gap: 10px;
+  align-items: center;
+  box-shadow: 0 10px 26px rgba(15, 23, 42, 0.08);
+}
+
+.home-stack-icon {
+  width: 32px;
+  height: 32px;
+  border-radius: 999px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  background: radial-gradient(circle at 30% 20%, #F9D2E4, #01847F);
+  color: #ffffff;
+
+  &.green {
+    background: radial-gradient(circle at 30% 20%, #F9D2E4, #01847F);
+  }
+
+  &.purple {
+    background: radial-gradient(circle at 30% 20%, #F9D2E4, #01847F);
+  }
+}
+
+.home-stack-text-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #0f172a;
+}
+
+.home-stack-text-sub {
+  margin-top: 2px;
+  font-size: 11px;
+  color: #6b7280;
+}
+
+.home-stack-main-number {
+  font-size: 20px;
+  font-weight: 700;
+  color: #0f172a;
+  text-align: right;
+
+  span {
+    font-size: 11px;
+    color: #6b7280;
+    margin-left: 2px;
+  }
+}
+
+.home-footer-hint {
+  margin-top: 8px;
+  font-size: 10px;
+  color: #9ca3af;
+  text-align: center;
 }
 
 // 快速统计卡片样式
@@ -173,7 +463,7 @@ header {
 .stat-trend {
   margin-top: 8px;
   font-size: 12px;
-  color: #4f46e5;
+  color: #01847F;
   font-weight: 500;
 }
 
@@ -205,7 +495,7 @@ header {
     left: 0;
     right: 0;
     height: 4px;
-    background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 100%);
+    background: linear-gradient(90deg, #01847F 0%, #016b67 100%);
     transform: scaleX(0);
     transition: transform 0.3s ease;
   }
@@ -270,8 +560,8 @@ header {
   font-size: 11px;
   padding: 4px 10px;
   border-radius: 999px;
-  background: rgba(79, 70, 229, 0.1);
-  color: #4f46e5;
+  background: #E6F7F6;
+  color: #01847F;
   font-weight: 500;
 }
 
@@ -279,7 +569,7 @@ header {
   width: 6px;
   height: 6px;
   border-radius: 999px;
-  background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+  background: linear-gradient(135deg, #01847F 0%, #016b67 100%);
 }
 
 .feature-main {
@@ -324,14 +614,314 @@ header {
     padding: 8px 14px;
     font-size: 12px;
     cursor: pointer;
-    background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+    background: linear-gradient(135deg, #01847F 0%, #016b67 100%);
     color: white;
     font-weight: 500;
     transition: all 0.3s ease;
 
     &:hover {
       transform: translateY(-2px);
-      box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
+      box-shadow: 0 6px 20px rgba(1, 132, 127, 0.4);
+    }
+  }
+}
+
+// 模态框样式重构
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.6);
+  backdrop-filter: blur(8px);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+  animation: fadeIn 0.3s ease-out;
+}
+
+.modal-content.sleep-modal {
+  background: #f2f2f7; // iOS 风格背景灰
+  border-radius: 20px;
+  width: 90%;
+  max-width: 500px; // 模拟手机宽度
+  height: 80vh;
+  max-height: 800px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  padding: 0;
+  border: none;
+  
+  .modal-header {
+    background: #fff;
+    padding: 1rem;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #e5e5ea;
+    border-radius: 0;
+    
+    h3 {
+      font-size: 17px;
+      font-weight: 600;
+      color: #000;
+      margin: 0;
+      background: none;
+      -webkit-text-fill-color: #000;
+    }
+    
+    button {
+      background: none;
+      border: none;
+      font-size: 1.5rem;
+      color: #007aff;
+      padding: 0;
+      width: auto;
+      height: auto;
+      cursor: pointer;
+      box-shadow: none;
+      
+      &:hover {
+        transform: none;
+        box-shadow: none;
+        opacity: 0.7;
+      }
+    }
+    
+    .nav-back { font-size: 1.2rem; font-weight: 600; }
+    .close-btn { font-size: 1.8rem; font-weight: 400; line-height: 1; color: #666; }
+  }
+  
+  .modal-body {
+    padding: 1rem;
+    background: #f2f2f7;
+    overflow-y: auto;
+  }
+  
+  // 视图切换 Tabs (Segmented Control)
+  .view-tabs {
+    margin-bottom: 1.5rem;
+    
+    .tab-segment {
+      background: #e3e3e8;
+      padding: 2px;
+      border-radius: 9px;
+      display: flex;
+      
+      button {
+        flex: 1;
+        border: none;
+        background: transparent;
+        padding: 6px 0;
+        font-size: 13px;
+        font-weight: 500;
+        color: #000;
+        border-radius: 7px;
+        cursor: pointer;
+        transition: all 0.2s;
+        
+        &.active {
+          background: #fff;
+          box-shadow: 0 1px 3px rgba(0,0,0,0.12);
+          font-weight: 600;
+        }
+        
+        &:disabled {
+          color: #999;
+          cursor: default;
+        }
+      }
+    }
+  }
+  
+  // 睡眠概要
+  .sleep-summary-header {
+    margin-bottom: 1.5rem;
+    padding: 0 0.5rem;
+    
+    .summary-label {
+      font-size: 13px;
+      color: #666;
+      margin-bottom: 4px;
+    }
+    
+    .summary-time {
+      display: flex;
+      align-items: baseline;
+      gap: 4px;
+      margin-bottom: 4px;
+      
+      .time-value {
+        font-size: 32px;
+        font-weight: 600;
+        color: #000;
+        font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+      }
+      
+      .time-unit {
+        font-size: 16px;
+        font-weight: 600;
+        color: #000;
+      }
+    }
+    
+    .summary-date {
+      font-size: 13px;
+      color: #666;
+    }
+  }
+  
+  // 图表区域
+  .chart-section {
+    background: #fff;
+    border-radius: 12px;
+    padding: 1rem;
+    margin-bottom: 1rem;
+    min-height: 300px;
+    
+    .day-chart-container {
+      // 甘特图样式
+      .hypnogram {
+        display: flex;
+        height: 200px;
+        margin-bottom: 1.5rem;
+        position: relative;
+        
+        .y-labels {
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          padding-right: 10px;
+          padding-bottom: 20px; // space for x-axis
+          width: 60px;
+          
+          span {
+            font-size: 11px;
+            color: #999;
+            text-align: right;
+            height: 20px;
+          }
+        }
+        
+        .hypno-chart {
+          flex: 1;
+          position: relative;
+          border-left: 1px solid #eee;
+          border-bottom: 1px solid #eee;
+          padding-bottom: 20px; // space for x-axis
+          
+          .chart-grid {
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 20px;
+            background-image: linear-gradient(to right, #f5f5f5 1px, transparent 1px);
+            background-size: 20% 100%; // 5 columns
+            z-index: 0;
+          }
+          
+          .x-labels {
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            display: flex;
+            justify-content: space-between;
+            
+            span {
+              font-size: 11px;
+              color: #999;
+              transform: translateX(-50%);
+              
+              &:first-child { transform: translateX(0); }
+              &:last-child { transform: translateX(0); }
+            }
+          }
+          
+          // 阶段块
+          .stage-block {
+            position: absolute;
+            height: 20px; // height of bars
+            border-radius: 4px;
+            z-index: 1;
+            
+            &.awake { 
+              background: #ef4444; 
+              top: 0; // Top
+            }
+            &.rem { 
+              background: #06b6d4; 
+              top: 33%; // Middle Top
+            }
+            &.core { 
+              background: #3b82f6; 
+              top: 66%; // Middle Bottom
+            }
+            &.deep { 
+              background: #312e81; 
+              bottom: 20px; // Bottom
+            }
+          }
+        }
+      }
+      
+      // 详细数据统计网格
+      .day-stats-grid {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        gap: 12px;
+        
+        .stat-box {
+          padding: 8px 0 8px 12px;
+          border-left: 4px solid #ccc;
+          
+          .label {
+            font-size: 12px;
+            color: #666;
+            margin-bottom: 2px;
+          }
+          
+          .value {
+            font-size: 15px;
+            font-weight: 600;
+            color: #000;
+            
+            .percent {
+              font-size: 12px;
+              color: #999;
+              margin-left: 4px;
+              font-weight: 400;
+            }
+          }
+          
+          &.rem { border-color: #06b6d4; }
+          &.core { border-color: #3b82f6; }
+          &.deep { border-color: #312e81; }
+          &.awake { border-color: #ef4444; }
+        }
+      }
+    }
+    
+    .bar-chart-container {
+      height: 300px;
+    }
+  }
+  
+  .more-info {
+    text-align: center;
+    margin-top: 1rem;
+    
+    button {
+      background: none;
+      border: none;
+      color: #007aff;
+      font-size: 14px;
+      font-weight: 600;
+      cursor: pointer;
     }
   }
 }
@@ -411,7 +1001,7 @@ header {
   font-size: 1.5rem;
   color: #1e293b;
   font-weight: 700;
-  background: linear-gradient(135deg, #1e293b 0%, #4f46e5 100%);
+  background: linear-gradient(135deg, #1e293b 0%, #01847F 100%);
   -webkit-background-clip: text;
   -webkit-text-fill-color: transparent;
   background-clip: text;
@@ -482,7 +1072,7 @@ header {
 .score-number {
   font-size: 3rem;
   font-weight: bold;
-  color: #1a73e8;
+  color: #01847F;
 }
 
 .score-max {

+ 256 - 55
campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.ts

@@ -258,68 +258,269 @@ export class DashboardComponent {
     '避免睡前摄入咖啡因和大量食物',
     '白天进行适量运动,但避免在睡前3小时内进行剧烈运动'
   ];
+
+  // 睡眠详情视图控制
+  sleepViewMode: 'day' | 'week' | 'month' = 'day';
+  currentSleepDateLabel: string = '2025年12月6日';
+  sleepTotalLabel: string = '7小时 30分钟';
+  sleepTotalLabelTitle: string = '睡眠时间';
   
-  // 图表配置
-  public lineChartData: ChartData<'line'> = {
-    labels: this.weeklySleepData.map(item => item.date),
-    datasets: [
-      {
-        data: this.weeklySleepData.map(item => item.deepSleep),
-        label: '深度睡眠(小时)',
-        borderColor: '#2e7d32',
-        backgroundColor: 'rgba(46, 125, 50, 0.1)',
-        tension: 0.3,
-        fill: false
-      },
-      {
-        data: this.weeklySleepData.map(item => item.lightSleep),
-        label: '浅度睡眠(小时)',
-        borderColor: '#1565c0',
-        backgroundColor: 'rgba(21, 101, 192, 0.1)',
-        tension: 0.3,
-        fill: false
+  // 统一图表配置
+  public sleepChartData: ChartData<'bar' | 'line'> = { labels: [], datasets: [] };
+  public sleepChartOptions: ChartConfiguration['options'] = {};
+  public sleepChartType: ChartType = 'bar';
+
+  // 月度睡眠数据 (模拟)
+  monthlySleepData: WeeklySleepData[] = [];
+
+  constructor(private router: Router) {
+    this.initMonthlyData();
+    // 初始化图表为日视图
+    this.updateSleepChart();
+  }
+
+  private initMonthlyData() {
+    // 生成30天模拟数据
+    for (let i = 1; i <= 30; i++) {
+      this.monthlySleepData.push({
+        date: `${i}日`,
+        deepSleep: 1.5 + Math.random(),
+        lightSleep: 3.5 + Math.random(),
+        remSleep: 1.0 + Math.random() * 0.5,
+        totalSleep: 0,
+        score: 70 + Math.floor(Math.random() * 30)
+      });
+      // 计算总睡眠
+      const last = this.monthlySleepData[this.monthlySleepData.length - 1];
+      last.totalSleep = last.deepSleep + last.lightSleep + last.remSleep;
+    }
+  }
+
+  // 切换睡眠视图
+  switchSleepView(mode: 'day' | 'week' | 'month'): void {
+    this.sleepViewMode = mode;
+    this.updateSleepChart();
+  }
+
+  updateSleepChart(): void {
+    if (this.sleepViewMode === 'day') {
+      this.currentSleepDateLabel = '2025年12月6日';
+      this.sleepTotalLabel = '7小时 30分钟';
+      this.sleepTotalLabelTitle = '睡眠时间';
+      this.setupDayChart();
+    } else if (this.sleepViewMode === 'week') {
+      this.currentSleepDateLabel = '2025年11月30日至12月6日';
+      this.sleepTotalLabel = '7小时 18分钟';
+      this.sleepTotalLabelTitle = '平均睡眠时间';
+      this.setupWeekChart();
+    } else {
+      this.currentSleepDateLabel = '2025年11月6日至12月6日';
+      this.sleepTotalLabel = '7小时 2分钟';
+      this.sleepTotalLabelTitle = '平均睡眠时间';
+      this.setupMonthChart();
+    }
+  }
+
+  // 配置日视图 (甘特图风格 - 水平条形图)
+  private setupDayChart(): void {
+    // 转换时间为数值 (小时)
+    // 假设基准时间是 22:00 (前一天) = 0, 06:00 = 8
+    // 简单起见,我们构造 Floating Bar 数据: [start, end]
+    // Y轴: 清醒, 快速眼动, 浅度睡眠, 深度睡眠 (Categories)
+    
+    // 构造数据
+    const stages = this.sleepData.sleepStages;
+    // 映射类型到 Y 轴索引
+    const typeMap: {[key: string]: string} = {
+      '清醒': '清醒',
+      '快速眼动': '快速眼动',
+      '浅度睡眠': '核心睡眠', // 对应图示的核心睡眠
+      '深度睡眠': '深度睡眠'
+    };
+
+    // 辅助函数:将时间字符串 "HH:MM" 转为相对时间轴的数值(例如从 22:00 开始的分钟数)
+    const parseTime = (timeStr: string) => {
+      const [h, m] = timeStr.split(':').map(Number);
+      let minutes = h * 60 + m;
+      // 如果是次日凌晨 (00:00 - 12:00),加 24小时
+      if (h < 12) minutes += 24 * 60;
+      // 归一化:减去 22:00 (22 * 60 = 1320)
+      // 但为了显示 X 轴标签正确,我们最好用真实时间戳或直接用 hours (float)
+      // Chart.js floating bars use [min, max]
+      return h + m / 60 + (h < 18 ? 24 : 0); // 偏移以便排序: 22->22, 00->24, 06->30
+    };
+    
+    // 我们需要构建 datasets,每个 stage 一段,但为了颜色统一,我们可以按类型分组 dataset
+    // 或者使用 parsing
+    
+    // 简化的 Chart.js 数据结构
+    // X轴: 时间
+    // Y轴: Category
+    
+    this.sleepChartType = 'bar';
+    this.sleepChartData = {
+      labels: ['清醒', '快速眼动', '核心睡眠', '深度睡眠'],
+      datasets: [
+        {
+          label: '睡眠阶段',
+          data: [
+            // 清醒 (示例数据,对应图示)
+            // {x: [22.5, 22.75], y: '清醒'}, // 22:30-22:45
+             // Chart.js data format for floating bars depends on structure.
+             // simpler to map manually
+             [0, 0], [0,0], [0,0], [0,0] // placeholder
+          ],
+          backgroundColor: [
+            '#ef4444', // 清醒 - 红
+            '#06b6d4', // REM - 蓝绿
+            '#3b82f6', // 核心 - 蓝
+            '#312e81'  // 深度 - 深蓝
+          ]
+        }
+      ]
+    };
+    
+    // 由于 Chart.js 处理 Gantt 需要复杂配置,这里为了演示,我们将使用一种简化的方式:
+    // 在HTML中使用 CSS Grid/Flex 手动绘制日视图的 Hypnogram,因为 Chart.js 对此支持较繁琐
+    // 见 dashboard.component.html 修改
+  }
+
+  // 配置周视图 (堆叠柱状图)
+  private setupWeekChart(): void {
+    this.sleepChartType = 'bar';
+    this.sleepChartData = {
+      labels: this.weeklySleepData.map(d => d.date),
+      datasets: [
+        {
+          label: '深度睡眠',
+          data: this.weeklySleepData.map(d => d.deepSleep),
+          backgroundColor: '#312e81', // 深蓝
+          stack: 'stack1',
+          barThickness: 20,
+          borderRadius: 4
+        },
+        {
+          label: '核心睡眠', // 浅度
+          data: this.weeklySleepData.map(d => d.lightSleep),
+          backgroundColor: '#3b82f6', // 蓝
+          stack: 'stack1',
+          barThickness: 20,
+          borderRadius: 4
+        },
+        {
+          label: '快速眼动',
+          data: this.weeklySleepData.map(d => d.remSleep),
+          backgroundColor: '#06b6d4', // 浅蓝
+          stack: 'stack1',
+          barThickness: 20,
+          borderRadius: 4
+        },
+        // 可选:添加清醒时间顶端
+        {
+          label: '清醒',
+          data: this.weeklySleepData.map(d => Math.max(0, 8 - d.totalSleep)), // 模拟清醒时间
+          backgroundColor: '#ef4444', // 红
+          stack: 'stack1',
+          barThickness: 20,
+          borderRadius: 4
+        }
+      ]
+    };
+
+    this.sleepChartOptions = {
+      responsive: true,
+      maintainAspectRatio: false,
+      plugins: {
+        legend: { display: false }, // 隐藏图例以符合设计
+        tooltip: { mode: 'index', intersect: false }
       },
-      {
-        data: this.weeklySleepData.map(item => item.remSleep),
-        label: '快速眼动(小时)',
-        borderColor: '#ef6c00',
-        backgroundColor: 'rgba(239, 108, 0, 0.1)',
-        tension: 0.3,
-        fill: false
+      scales: {
+        x: { 
+          grid: { display: true, drawOnChartArea: false },
+          ticks: { color: '#94a3b8' }
+        },
+        y: { 
+          display: true,
+          position: 'right',
+          grid: { color: '#f1f5f9' },
+          ticks: { 
+            stepSize: 4,
+            callback: (v) => v + ':00' // 简单模拟时间轴标签
+          },
+          min: 0,
+          max: 12
+        }
       }
-    ]
-  };
-  
-  public lineChartOptions: ChartConfiguration<'line'>['options'] = {
-    responsive: true,
-    maintainAspectRatio: false,
-    plugins: {
-      legend: {
-        position: 'top',
-      },
-      tooltip: {
-        mode: 'index',
-        intersect: false,
+    };
+  }
+
+  // 配置月视图 (堆叠柱状图)
+  private setupMonthChart(): void {
+    this.sleepChartType = 'bar';
+    this.sleepChartData = {
+      labels: this.monthlySleepData.map(d => d.date), // 1日, 2日...
+      datasets: [
+        {
+          label: '深度睡眠',
+          data: this.monthlySleepData.map(d => d.deepSleep),
+          backgroundColor: '#312e81',
+          stack: 'stack1',
+          barThickness: 6, // 变细
+          borderRadius: 2
+        },
+        {
+          label: '核心睡眠',
+          data: this.monthlySleepData.map(d => d.lightSleep),
+          backgroundColor: '#3b82f6',
+          stack: 'stack1',
+          barThickness: 6,
+          borderRadius: 2
+        },
+        {
+          label: '快速眼动',
+          data: this.monthlySleepData.map(d => d.remSleep),
+          backgroundColor: '#06b6d4',
+          stack: 'stack1',
+          barThickness: 6,
+          borderRadius: 2
+        },
+        {
+          label: '清醒',
+          data: this.monthlySleepData.map(d => 0.5), // 模拟
+          backgroundColor: '#ef4444',
+          stack: 'stack1',
+          barThickness: 6,
+          borderRadius: 2
+        }
+      ]
+    };
+
+    this.sleepChartOptions = {
+      responsive: true,
+      maintainAspectRatio: false,
+      plugins: {
+        legend: { display: false },
+        tooltip: { mode: 'index', intersect: false }
       },
-      title: {
-        display: true,
-        text: '一周睡眠质量趋势'
-      }
-    },
-    scales: {
-      y: {
-        beginAtZero: true,
-        title: {
+      scales: {
+        x: { 
+          grid: { display: false },
+          ticks: { 
+            color: '#94a3b8',
+            maxTicksLimit: 6, // 限制X轴标签数量
+            autoSkip: true
+          }
+        },
+        y: { 
           display: true,
-          text: '时长(小时)'
+          position: 'right',
+          grid: { color: '#f1f5f9' },
+          min: 0,
+          max: 12
         }
       }
-    }
-  };
-  
-  public lineChartType: 'line' = 'line';
-
-  constructor(private router: Router) {}
+    };
+  }
 
   // 退出登录
   logout(): void {

+ 42 - 28
campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-history/diet-history.component.html

@@ -4,43 +4,43 @@
   <main class="history-main">
     <!-- 时期选择器 -->
     <section class="period-section">
-      <div class="period-tabs">
-        <button 
-          class="period-tab" 
-          [class.active]="selectedPeriod === 'week'"
-          (click)="changePeriod('week')">
-          周
-        </button>
-        <button 
-          class="period-tab" 
-          [class.active]="selectedPeriod === 'month'"
-          (click)="changePeriod('month')">
-          月
-        </button>
-        <button 
-          class="period-tab" 
-          [class.active]="selectedPeriod === '6months'"
-          (click)="changePeriod('6months')">
-          6个月
-        </button>
+      <div class="period-header">
+        <div class="period-tabs">
+          <button 
+            class="period-tab" 
+            [class.active]="selectedPeriod === 'week'"
+            (click)="changePeriod('week')">
+            周
+          </button>
+          <button 
+            class="period-tab" 
+            [class.active]="selectedPeriod === 'month'"
+            (click)="changePeriod('month')">
+            月
+          </button>
+        </div>
+
       </div>
 
       <!-- 统计信息 -->
       <div class="period-stats">
-        <div class="stats-main">
-          <span class="stats-label">日均摄入</span>
-          <span class="stats-value">{{ periodStats.avgCalories }}</span>
-          <span class="stats-unit">千卡/天</span>
+        <div class="stat-item">
+          <span class="stat-value">{{ periodStats.totalMeals }}</span>
+          <span class="stat-label">总餐数</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-value">{{ periodStats.avgCalories }}</span>
+          <span class="stat-label">日均摄入(千卡)</span>
         </div>
-        <div class="stats-secondary">
-          <span>共 {{ periodStats.totalMeals }} 餐</span>
-          <span class="stats-range">{{ periodStats.dateRange }}</span>
+        <div class="stat-item">
+          <span class="stat-value highlight">{{ periodStats.totalCalories }}</span>
+          <span class="stat-label">总消耗热量(千卡)</span>
         </div>
       </div>
     </section>
 
-    <!-- 饮食记录列表 -->
-    <section class="records-section">
+    <!-- 饮食记录列表 (周视图) -->
+    <section class="records-section" *ngIf="selectedPeriod === 'week'">
       <div class="day-group" *ngFor="let group of groupedRecords">
         <div class="day-header">
           <span class="day-date">{{ group.displayDate }}</span>
@@ -75,5 +75,19 @@
         <span class="no-data-text">暂无饮食记录</span>
       </div>
     </section>
+
+    <!-- 日历视图 (月视图) -->
+    <section class="calendar-section" *ngIf="selectedPeriod === 'month'">
+      <app-calendar-grid
+        [currentDate]="currentViewDate"
+        [dayDataMap]="calendarDataMap"
+        [selectedDate]="selectedDate"
+        [themeColor]="'blue'"
+        (dateSelected)="onDateSelected($event)"
+        (previousMonth)="previousMonth()"
+        (nextMonth)="nextMonth()"
+        (goToCurrentMonth)="goToCurrentMonth()">
+      </app-calendar-grid>
+    </section>
   </main>
 </div>

+ 219 - 48
campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-history/diet-history.component.scss

@@ -13,87 +13,258 @@
 
 // 时期选择区域
 .period-section {
-  background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
-  border-radius: 20px;
+  background: #fff;
+  border-radius: 8px;
   padding: 1.5rem;
   margin-bottom: 1.5rem;
-  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
-  border: 1px solid rgba(226, 232, 240, 0.5);
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+  .period-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 1.5rem;
+  }
 }
 
 .period-tabs {
   display: flex;
-  background: #f3f4f6;
-  border-radius: 12px;
-  padding: 4px;
-  margin-bottom: 1rem;
+  gap: 1rem;
 }
 
 .period-tab {
-  flex: 1;
-  padding: 10px 16px;
+  padding: 0.5rem 0;
   border: none;
   background: transparent;
-  border-radius: 8px;
-  font-size: 14px;
+  font-size: 1rem;
   font-weight: 500;
-  color: #6b7280;
+  color: #64748b;
   cursor: pointer;
+  position: relative;
   transition: all 0.2s ease;
 
   &:hover {
-    color: #374151;
+    color: #3b82f6;
   }
 
   &.active {
-    background: #ffffff;
-    color: #4f46e5;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    color: #3b82f6;
     font-weight: 600;
-  }
-}
 
-.period-stats {
-  padding: 1rem;
-  background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
-  border-radius: 12px;
-  border: 1px solid #e9d5ff;
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -4px;
+      left: 0;
+      width: 100%;
+      height: 2px;
+      background-color: #3b82f6;
+      border-radius: 2px;
+    }
+  }
 }
 
-.stats-main {
+// 月份导航
+.month-nav {
   display: flex;
-  align-items: baseline;
-  gap: 6px;
-  margin-bottom: 8px;
-}
-
-.stats-label {
-  font-size: 13px;
-  color: #6b7280;
-}
+  align-items: center;
+  gap: 12px;
+  background: #f8fafc;
+  padding: 4px 12px;
+  border-radius: 20px;
+  border: 1px solid #e2e8f0;
+
+  .nav-btn {
+    width: 24px;
+    height: 24px;
+    border-radius: 50%;
+    border: none;
+    background: white;
+    color: #64748b;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 18px;
+    line-height: 1;
+    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+    
+    &:hover {
+      color: #3b82f6;
+      background: #eff6ff;
+    }
+  }
 
-.stats-value {
-  font-size: 28px;
-  font-weight: 700;
-  color: #7c3aed;
-}
+  .current-month {
+    font-weight: 600;
+    color: #1e293b;
+    font-size: 14px;
+    min-width: 80px;
+    text-align: center;
+  }
 
-.stats-unit {
-  font-size: 12px;
-  color: #9ca3af;
+  .today-btn {
+    font-size: 12px;
+    color: #3b82f6;
+    background: none;
+    border: none;
+    cursor: pointer;
+    margin-left: 8px;
+    padding: 2px 6px;
+    border-radius: 4px;
+    
+    &:hover {
+      background: #eff6ff;
+    }
+  }
 }
 
-.stats-secondary {
+.period-stats {
   display: flex;
-  justify-content: space-between;
-  font-size: 12px;
-  color: #6b7280;
+  gap: 1rem;
+  margin-top: 1rem;
+  
+  .stat-item {
+    flex: 1;
+    background: #f8fafc;
+    padding: 1rem;
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 0.5rem;
+    border: 1px solid #f1f5f9;
+
+    .stat-value {
+      font-size: 1.5rem;
+      font-weight: 700;
+      color: #0f172a; // 深色,类似绿色但更通用,或用主题色
+
+      &.highlight {
+        color: #3b82f6; // 蓝色高亮
+      }
+    }
+
+    .stat-label {
+      font-size: 0.875rem;
+      color: #64748b;
+    }
+  }
 }
 
-.stats-range {
-  color: #9ca3af;
+// 日历视图样式
+.calendar-section {
+  background: #fff;
+  border-radius: 8px;
+  padding: 1.5rem;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+
+  .week-header {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    margin-bottom: 1rem;
+    text-align: center;
+    
+    .week-day {
+      color: #64748b;
+      font-size: 0.875rem;
+      font-weight: 500;
+      padding: 0.5rem;
+    }
+  }
+
+  .calendar-grid {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    gap: 1px;
+    background: #e2e8f0; // 网格线颜色
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    overflow: hidden;
+  }
+
+  .calendar-day {
+    background: #fff;
+    min-height: 100px;
+    padding: 0.75rem;
+    position: relative;
+    transition: all 0.2s;
+
+    &:hover {
+      background: #f8fafc;
+    }
+
+    &.other-month {
+      background: #f9fafb;
+      
+      .day-number {
+        color: #cbd5e1;
+      }
+    }
+
+    &.today {
+      background: #fff;
+      
+      .day-number {
+        background: #3b82f6;
+        color: white;
+        width: 24px;
+        height: 24px;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+
+    &.has-record {
+      background: #ecfdf5; // 浅绿色背景表示有记录
+
+      &:hover {
+        background: #d1fae5;
+      }
+      
+      &.other-month {
+        background: #f0fdf4;
+        opacity: 0.6;
+      }
+    }
+
+    .day-number {
+      font-size: 0.875rem;
+      color: #64748b;
+      margin-bottom: 0.5rem;
+      display: inline-block;
+    }
+
+    .day-content {
+      .record-indicator {
+        color: #10b981;
+        font-size: 12px;
+        margin-bottom: 4px;
+        font-weight: bold;
+      }
+
+      .record-summary {
+        .meal-types {
+          font-size: 11px;
+          color: #1e293b;
+          margin-bottom: 2px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .total-cal {
+          font-size: 10px;
+          color: #64748b;
+        }
+      }
+    }
+  }
 }
 
+
 // 记录区域
 .records-section {
   display: flex;

+ 174 - 17
campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-history/diet-history.component.ts

@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
 import { PageHeaderComponent } from '../../../shared/page-header/page-header.component';
+import { CalendarGridComponent, CalendarDayData } from '../../../shared/components/calendar-grid/calendar-grid.component';
 
 interface MealRecord {
   id: number;
@@ -12,21 +13,31 @@ interface MealRecord {
   time: string;
 }
 
+interface CalendarDay {
+  date: Date;
+  isCurrentMonth: boolean;
+  isToday: boolean;
+  hasRecord: boolean;
+  totalCalories?: number;
+  mealTypes?: string;
+}
+
 @Component({
   selector: 'app-diet-history',
   standalone: true,
-  imports: [CommonModule, RouterModule, PageHeaderComponent],
+  imports: [CommonModule, RouterModule, PageHeaderComponent, CalendarGridComponent],
   templateUrl: './diet-history.component.html',
   styleUrl: './diet-history.component.scss'
 })
 export class DietHistoryComponent implements OnInit {
   // 时期选择
-  selectedPeriod: 'week' | 'month' | '6months' = 'week';
+  selectedPeriod: 'week' | 'month' = 'week';
   
   // 统计数据
   periodStats = {
     avgCalories: 0,
     totalMeals: 0,
+    totalCalories: 0,
     dateRange: ''
   };
 
@@ -37,6 +48,14 @@ export class DietHistoryComponent implements OnInit {
   // 按日期分组的记录
   groupedRecords: { date: string; displayDate: string; meals: MealRecord[]; totalCalories: number }[] = [];
 
+  // 日历相关
+  currentViewDate: Date = new Date();
+  weekDays: string[] = ['一', '二', '三', '四', '五', '六', '日'];
+  calendarDays: CalendarDay[] = [];
+  calendarDataMap: Map<string, CalendarDayData> = new Map();
+  isCurrentMonth: boolean = true;
+  selectedDate: Date | null = null;
+
   constructor(private router: Router) {}
 
   ngOnInit(): void {
@@ -45,11 +64,138 @@ export class DietHistoryComponent implements OnInit {
   }
 
   // 切换时期
-  changePeriod(period: 'week' | 'month' | '6months'): void {
+  changePeriod(period: 'week' | 'month'): void {
     this.selectedPeriod = period;
+    if (period === 'month') {
+      this.currentViewDate = new Date(); // Reset to current month when switching
+      this.generateCalendar();
+      this.buildCalendarDataMap();
+    }
+    this.updatePeriodData();
+  }
+
+  // 构建日历数据映射
+  buildCalendarDataMap(): void {
+    this.calendarDataMap.clear();
+    this.calendarDays.forEach(day => {
+      if (day.isCurrentMonth && day.hasRecord) {
+        const dateKey = this.formatDateKey(day.date);
+        this.calendarDataMap.set(dateKey, {
+          date: day.date,
+          hasData: day.hasRecord,
+          completed: false,
+          primaryText: day.mealTypes || '',
+          secondaryText: day.totalCalories ? `${day.totalCalories}千卡` : '',
+          badge: day.hasRecord ? '✓' : ''
+        });
+      }
+    });
+  }
+
+  private formatDateKey(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}-${month}-${day}`;
+  }
+
+  onDateSelected(day: CalendarDayData | null): void {
+    if (!day) return;
+    this.selectedDate = day.date;
+  }
+
+  previousMonth(): void {
+    this.navigateMonth(-1);
+  }
+
+  nextMonth(): void {
+    this.navigateMonth(1);
+  }
+
+  goToCurrentMonth(): void {
+    this.resetToToday();
+  }
+
+  // 切换月份
+  navigateMonth(delta: number): void {
+    this.currentViewDate = new Date(this.currentViewDate.getFullYear(), this.currentViewDate.getMonth() + delta, 1);
+    this.checkIfCurrentMonth();
+    this.generateCalendar();
+    this.updatePeriodData(); // Recalculate stats for the new month
+  }
+
+  // 回到本月
+  resetToToday(): void {
+    this.currentViewDate = new Date();
+    this.checkIfCurrentMonth();
+    this.generateCalendar();
     this.updatePeriodData();
   }
 
+  private checkIfCurrentMonth(): void {
+    const now = new Date();
+    this.isCurrentMonth = now.getMonth() === this.currentViewDate.getMonth() && now.getFullYear() === this.currentViewDate.getFullYear();
+    this.buildCalendarDataMap();
+  }
+
+  // 生成日历数据
+  private generateCalendar(): void {
+    this.calendarDays = [];
+    const year = this.currentViewDate.getFullYear();
+    const month = this.currentViewDate.getMonth(); // 0-indexed
+
+    // First day of the month
+    const firstDayOfMonth = new Date(year, month, 1);
+    // Last day of the month
+    const lastDayOfMonth = new Date(year, month + 1, 0);
+
+    // Get the day of the week for the 1st (0 = Sunday, 1 = Monday, ...)
+    let startDayOfWeek = firstDayOfMonth.getDay();
+    // Adjust to make Monday = 0, Sunday = 6 (since our header starts with Monday)
+    startDayOfWeek = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1;
+
+    // Days from previous month to fill the first row
+    for (let i = startDayOfWeek; i > 0; i--) {
+      const date = new Date(year, month, 1 - i);
+      this.calendarDays.push(this.createCalendarDay(date, false));
+    }
+
+    // Days of current month
+    for (let i = 1; i <= lastDayOfMonth.getDate(); i++) {
+      const date = new Date(year, month, i);
+      this.calendarDays.push(this.createCalendarDay(date, true));
+    }
+
+    // Days from next month to complete the grid (up to 42 days total for 6 rows, or just fill the last week)
+    const remainingDays = 42 - this.calendarDays.length;
+    for (let i = 1; i <= remainingDays; i++) {
+      const date = new Date(year, month + 1, i);
+      this.calendarDays.push(this.createCalendarDay(date, false));
+    }
+  }
+
+  private createCalendarDay(date: Date, isCurrentMonth: boolean): CalendarDay {
+    const dateStr = date.toISOString().split('T')[0];
+    const meals = this.allMealRecords.filter(r => r.date === dateStr);
+    const hasRecord = meals.length > 0;
+    const totalCalories = meals.reduce((sum, r) => sum + r.calories, 0);
+    const mealTypes = [...new Set(meals.map(r => r.type))].join('、');
+    
+    const today = new Date();
+    const isToday = date.getDate() === today.getDate() && 
+                    date.getMonth() === today.getMonth() && 
+                    date.getFullYear() === today.getFullYear();
+
+    return {
+      date,
+      isCurrentMonth,
+      isToday,
+      hasRecord,
+      totalCalories: hasRecord ? totalCalories : undefined,
+      mealTypes: hasRecord ? mealTypes : undefined
+    };
+  }
+
   // 返回饮食管理
   backToDiet(): void {
     this.router.navigate(['/diet']);
@@ -59,32 +205,42 @@ export class DietHistoryComponent implements OnInit {
   private updatePeriodData(): void {
     const now = new Date();
     let startDate: Date;
+    let endDate: Date;
 
-    switch (this.selectedPeriod) {
-      case 'week':
-        startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
-        break;
-      case 'month':
-        startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
-        break;
-      case '6months':
-        startDate = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000);
-        break;
+    if (this.selectedPeriod === 'week') {
+      endDate = now;
+      startDate = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000); // Last 7 days including today
+    } else {
+      // Month view uses the selected month
+      startDate = new Date(this.currentViewDate.getFullYear(), this.currentViewDate.getMonth(), 1);
+      endDate = new Date(this.currentViewDate.getFullYear(), this.currentViewDate.getMonth() + 1, 0);
     }
 
     // 筛选记录
     this.filteredRecords = this.allMealRecords.filter(r => {
       const recordDate = new Date(r.date);
-      return recordDate >= startDate && recordDate <= now;
+      // Strip time for accurate date comparison
+      const rDate = new Date(recordDate.getFullYear(), recordDate.getMonth(), recordDate.getDate());
+      const sDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
+      const eDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
+      
+      return rDate >= sDate && rDate <= eDate;
     });
 
-    // 按日期分组
-    this.groupRecordsByDate();
+    // 按日期分组 (仅周视图需要)
+    if (this.selectedPeriod === 'week') {
+      this.groupRecordsByDate();
+    } else {
+      // For month view, calculate stats directly
+      this.generateCalendar(); // Re-generate to update calendar data
+    }
 
     // 计算统计
-    this.calculateStats(startDate, now);
+    this.calculateStats(startDate, endDate);
   }
 
+  // ... (rest of the file remains same, need to be careful with existing methods)
+  
   // 按日期分组记录
   private groupRecordsByDate(): void {
     const groups: { [key: string]: MealRecord[] } = {};
@@ -130,6 +286,7 @@ export class DietHistoryComponent implements OnInit {
     this.periodStats = {
       avgCalories: uniqueDays > 0 ? Math.round(totalCalories / uniqueDays) : 0,
       totalMeals: this.filteredRecords.length,
+      totalCalories: totalCalories,
       dateRange: `${this.formatDate(startDate)}至${this.formatDate(endDate)}`
     };
   }

+ 91 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-mobile.component.html

@@ -0,0 +1,91 @@
+<div class="diet-mobile-container">
+  <!-- 顶部标题栏 -->
+  <div class="mobile-header">
+    <app-page-header [title]="'饮食管理'" (back)="backToDashboard()"></app-page-header>
+  </div>
+
+  <div class="scroll-content">
+    <!-- 仪表盘网格 -->
+    <div class="dashboard-grid">
+      <!-- 剩余热量大卡片 -->
+      <div class="stat-card large">
+        <div class="stat-label">剩余热量</div>
+        <div class="stat-value">400 <span class="stat-unit">千卡</span></div>
+        <div class="stat-footer">
+          <span>总预算 2,000</span>
+          <span>已摄入 1,600</span>
+        </div>
+        <div class="progress-bg">
+          <div class="progress-fill" style="width: 80%;"></div>
+        </div>
+      </div>
+
+      <!-- 营养素卡片 -->
+      <div class="stat-card">
+        <div class="stat-label">蛋白质</div>
+        <div class="stat-value">60<span class="stat-unit">g</span></div>
+        <div class="stat-sub">目标 80g</div>
+        <div class="stat-progress">
+          <div class="progress-fill protein" style="width: 75%;"></div>
+        </div>
+      </div>
+      
+      <div class="stat-card">
+        <div class="stat-label">碳水</div>
+        <div class="stat-value">170<span class="stat-unit">g</span></div>
+        <div class="stat-sub">目标 200g</div>
+        <div class="stat-progress">
+          <div class="progress-fill carbs" style="width: 85%;"></div>
+        </div>
+      </div>
+
+      <div class="stat-card">
+        <div class="stat-label">脂肪</div>
+        <div class="stat-value">40<span class="stat-unit">g</span></div>
+        <div class="stat-sub">目标 67g</div>
+        <div class="stat-progress">
+          <div class="progress-fill fat" style="width: 60%;"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 餐食记录标题 -->
+    <div class="meal-grid-header">
+      <span>今日记录</span>
+      <span class="meal-count">3 餐</span>
+    </div>
+
+    <!-- 餐食网格 -->
+    <div class="meal-grid">
+      <div class="meal-item" *ngFor="let meal of todayMeals">
+        <div class="meal-img-placeholder">
+          <span *ngIf="meal.type === '早餐'">🍳</span>
+          <span *ngIf="meal.type === '午餐'">🍱</span>
+          <span *ngIf="meal.type === '晚餐'">🥗</span>
+        </div>
+        <div class="meal-info">
+          <div class="meal-type">
+            {{ meal.type }}
+            <span class="meal-cal-badge">{{ meal.calories }}</span>
+          </div>
+          <div class="meal-desc">
+            <span *ngFor="let food of meal.foods; last as isLast">{{ food }}{{ isLast ? '' : ', ' }}</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 添加按钮卡片 -->
+      <div class="add-meal-card" (click)="navigateToAddMeal()">
+        <div class="add-icon">+</div>
+        <div class="add-text">记录新餐食</div>
+      </div>
+    </div>
+    
+    <!-- 底部额外操作区 -->
+    <div class="bottom-actions">
+      <button class="action-btn secondary" (click)="navigateToDietHistory()">
+        <span>📊</span> 查看历史记录
+      </button>
+    </div>
+  </div>
+</div>

+ 289 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet-mobile.component.scss

@@ -0,0 +1,289 @@
+// 移动端适配样式 - 基于方案 C (网格/相册) 适配系统风格
+.diet-mobile-container {
+  min-height: 100vh;
+  background-color: #f5f7fa;
+  color: #1e293b;
+  padding-bottom: 40px;
+  max-width: 480px;
+  margin: 0 auto;
+  position: relative;
+}
+
+// 顶部导航
+.mobile-header {
+  background: #fff;
+  padding: 0;
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: block;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+
+  app-page-header {
+    width: 100%;
+    display: block;
+  }
+}
+
+.scroll-content {
+  padding: 16px;
+}
+
+// 仪表盘网格
+.dashboard-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12px;
+  margin-bottom: 24px;
+}
+
+.stat-card {
+  background: #fff;
+  border-radius: 16px;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  height: 110px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+  position: relative;
+  overflow: hidden;
+  border: 1px solid #e2e8f0;
+
+  &.large {
+    grid-column: span 2;
+    background: linear-gradient(135deg, #01847F 0%, #016b67 100%); // Mars Green gradient
+    color: white;
+    border: none;
+    height: 130px;
+    box-shadow: 0 8px 20px rgba(1, 132, 127, 0.3);
+
+    .stat-label { color: rgba(255,255,255,0.9); }
+    .stat-value { color: #fff; }
+    .stat-unit { color: rgba(255,255,255,0.9); }
+    
+    .stat-footer {
+      display: flex;
+      justify-content: space-between;
+      font-size: 12px;
+      opacity: 0.85;
+      margin-top: auto;
+      margin-bottom: 8px;
+      position: relative;
+      z-index: 1;
+    }
+
+    .progress-bg {
+      height: 6px;
+      background: rgba(0,0,0,0.2);
+      border-radius: 3px;
+      overflow: hidden;
+      position: relative;
+      z-index: 1;
+      
+      .progress-fill {
+        height: 100%;
+        background: #fff;
+        border-radius: 3px;
+        opacity: 0.9;
+      }
+    }
+    
+    // 装饰背景
+    &::after {
+      content: "";
+      position: absolute;
+      right: -10px;
+      top: -10px;
+      width: 100px;
+      height: 100px;
+      background: rgba(255,255,255,0.1);
+      border-radius: 50%;
+    }
+  }
+
+  .stat-label { 
+    font-size: 12px; 
+    font-weight: 600; 
+    color: #64748b; 
+    margin-bottom: 4px;
+  }
+  
+  .stat-value { 
+    font-size: 24px; 
+    font-weight: 800; 
+    color: #1e293b;
+    line-height: 1.2;
+  }
+  
+  .stat-unit { 
+    font-size: 14px; 
+    font-weight: 500; 
+    margin-left: 2px;
+  }
+  
+  .stat-sub {
+    font-size: 11px;
+    color: #94a3b8;
+    margin-bottom: 8px;
+  }
+
+  .stat-progress {
+    height: 6px;
+    background: #f1f5f9;
+    border-radius: 3px;
+    margin-top: auto;
+    overflow: hidden;
+    
+    .progress-fill {
+      height: 100%;
+      border-radius: 3px;
+      
+      &.protein { background: #f59e0b; } // 橙色
+      &.carbs { background: #3b82f6; } // 蓝色
+      &.fat { background: #ef4444; } // 红色
+    }
+  }
+}
+
+// 餐食网格标题
+.meal-grid-header {
+  padding: 0 4px;
+  margin-bottom: 12px;
+  font-size: 16px;
+  font-weight: 700;
+  color: #1e293b;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  
+  .meal-count {
+    font-size: 12px;
+    color: #64748b;
+    background: #f1f5f9;
+    padding: 2px 8px;
+    border-radius: 10px;
+  }
+}
+
+// 餐食网格
+.meal-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.meal-item {
+  background: #fff;
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.04);
+  border: 1px solid #e2e8f0;
+  display: flex;
+  flex-direction: column;
+  transition: transform 0.2s;
+  
+  &:active {
+    transform: scale(0.98);
+  }
+
+  .meal-img-placeholder {
+    height: 100px;
+    background: #f8fafc;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 36px;
+    border-bottom: 1px solid #f1f5f9;
+  }
+
+  .meal-info {
+    padding: 12px;
+  }
+
+  .meal-type {
+    font-size: 14px;
+    font-weight: 700;
+    color: #1e293b;
+    margin-bottom: 6px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .meal-cal-badge {
+    font-size: 10px;
+    background: #E6F7F6;
+    color: #01847F;
+    padding: 2px 6px;
+    border-radius: 4px;
+    font-weight: 600;
+  }
+
+  .meal-desc {
+    font-size: 11px;
+    color: #64748b;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    line-height: 1.5;
+    height: 2.8em; // 固定两行高度
+  }
+}
+
+.add-meal-card {
+  border: 2px dashed #cbd5e1;
+  border-radius: 16px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #64748b;
+  background: transparent;
+  min-height: 180px;
+  cursor: pointer;
+  transition: all 0.2s;
+  
+  &:active {
+    background: #f1f5f9;
+    border-color: #94a3b8;
+  }
+  
+  .add-icon {
+    font-size: 32px;
+    margin-bottom: 8px;
+    color: #94a3b8;
+  }
+  
+  .add-text {
+    font-size: 13px;
+    font-weight: 600;
+  }
+}
+
+// 底部操作区
+.bottom-actions {
+  display: flex;
+  justify-content: center;
+  margin-top: 10px;
+  
+  .action-btn {
+    background: #fff;
+    border: 1px solid #e2e8f0;
+    padding: 10px 20px;
+    border-radius: 20px;
+    color: #64748b;
+    font-size: 13px;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    box-shadow: 0 2px 6px rgba(0,0,0,0.03);
+    
+    &:active {
+      background: #f8fafc;
+    }
+  }
+}

+ 2 - 2
campus_health_app/frontend/campus-health-app/src/app/modules/diet/diet.component.ts

@@ -7,8 +7,8 @@ import { PageHeaderComponent } from '../../shared/page-header/page-header.compon
   selector: 'app-diet',
   standalone: true,
   imports: [CommonModule, RouterModule, PageHeaderComponent],
-  templateUrl: './diet.component.html',
-  styleUrl: './diet.component.scss'
+  templateUrl: './diet-mobile.component.html',
+  styleUrl: './diet-mobile.component.scss'
 })
 export class DietComponent {
   // 今日饮食记录

+ 6 - 5
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.ts

@@ -1,5 +1,5 @@
 import { Component } from '@angular/core';
-import { CommonModule } from '@angular/common';
+import { CommonModule, Location } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
 import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
 import { PageHeaderComponent } from '../../../shared/page-header/page-header.component';
@@ -68,7 +68,8 @@ export class AddExerciseComponent {
   
   constructor(
     private router: Router,
-    private formBuilder: FormBuilder
+    private formBuilder: FormBuilder,
+    private location: Location
   ) {
     // 初始化表单
     this.exerciseForm = this.formBuilder.group({
@@ -86,9 +87,9 @@ export class AddExerciseComponent {
     this.exerciseDateTimeString = this.formatDateTimeForInput(this.exerciseDateTime);
   }
   
-  // 返回运动记录页面
+  // 返回上一页(运动详情页面)
   backToExerciseList(): void {
-    this.router.navigate(['/exercise']);
+    this.location.back();
   }
   
   // 表单提交
@@ -112,7 +113,7 @@ export class AddExerciseComponent {
       console.log('运动记录提交成功:', exerciseData);
       
       // 提交成功后返回运动记录页面
-      this.router.navigate(['/exercise']);
+      this.router.navigate(['/exercise/detail']);
       
       this.isSubmitting = false;
     }, 1000);

+ 1 - 1
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-actions/exercise-actions.component.html

@@ -1,5 +1,5 @@
 <div class="actions-container">
-  <app-page-header [title]="'动作库'" (back)="backToExercise()"></app-page-header>
+  <app-page-header [title]="'动作库'" [showBack]="false"></app-page-header>
 
   <main class="actions-main">
     <!-- 搜索栏 -->

+ 374 - 4
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-actions/exercise-actions.component.ts

@@ -29,11 +29,13 @@ export class ExerciseActionsComponent {
   searchQuery = '';
 
   categories = [
-    { name: '胸部训练', icon: '💪' },
-    { name: '背部训练', icon: '🔙' },
+    { name: '胸部训练', icon: '🦍' },
+    { name: '背部训练', icon: '🦇' },
     { name: '腿部训练', icon: '🦵' },
-    { name: '核心训练', icon: '🎯' },
-    { name: '有氧训练', icon: '🏃' }
+    { name: '肩部训练', icon: '🛡️' },
+    { name: '手臂训练', icon: '💪' },
+    { name: '核心训练', icon: '🍫' },
+    { name: '有氧训练', icon: '🫀' }
   ];
 
   allActions: ExerciseAction[] = [
@@ -53,6 +55,22 @@ export class ExerciseActionsComponent {
       keepUrl: 'https://www.gotokeep.com/training',
       description: '使用哑铃进行卧推,增加胸肌训练自由度'
     },
+    {
+      id: 11, name: '上斜哑铃卧推', category: '胸部训练',
+      targetMuscles: ['胸大肌上束', '三角肌前束'],
+      difficulty: 'intermediate', equipment: '哑铃、上斜凳', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对上胸部的卧推训练,打造饱满胸型'
+    },
+    {
+      id: 12, name: '绳索夹胸', category: '胸部训练',
+      targetMuscles: ['胸大肌', '胸中缝'],
+      difficulty: 'intermediate', equipment: '龙门架', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '利用绳索持续张力刺激胸肌,修饰胸部线条'
+    },
     {
       id: 3, name: '引体向上', category: '背部训练',
       targetMuscles: ['背阔肌', '肱二头肌'],
@@ -69,6 +87,22 @@ export class ExerciseActionsComponent {
       keepUrl: 'https://www.gotokeep.com/training',
       description: '单侧训练背部肌肉,纠正左右不平衡'
     },
+    {
+      id: 13, name: '高位下拉', category: '背部训练',
+      targetMuscles: ['背阔肌', '大圆肌'],
+      difficulty: 'beginner', equipment: '高位下拉机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '模拟引体向上的器械动作,适合初学者建立背部发力感'
+    },
+    {
+      id: 14, name: '坐姿绳索划船', category: '背部训练',
+      targetMuscles: ['背阔肌', '斜方肌中下束', '菱形肌'],
+      difficulty: 'beginner', equipment: '划船机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '增加背部厚度的经典动作'
+    },
     {
       id: 5, name: '深蹲', category: '腿部训练',
       targetMuscles: ['股四头肌', '臀大肌', '腘绳肌'],
@@ -85,6 +119,70 @@ export class ExerciseActionsComponent {
       keepUrl: 'https://www.gotokeep.com/training',
       description: '单腿训练动作,提高下肢稳定性'
     },
+    {
+      id: 15, name: '腿举 (倒蹬)', category: '腿部训练',
+      targetMuscles: ['股四头肌', '臀大肌'],
+      difficulty: 'intermediate', equipment: '倒蹬机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '大重量腿部训练,安全性高于深蹲'
+    },
+    {
+      id: 16, name: '坐姿腿屈伸', category: '腿部训练',
+      targetMuscles: ['股四头肌'],
+      difficulty: 'beginner', equipment: '腿屈伸机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '孤立刺激大腿前侧肌肉,刻画线条'
+    },
+    {
+      id: 17, name: '俯身腿弯举', category: '腿部训练',
+      targetMuscles: ['腘绳肌'],
+      difficulty: 'beginner', equipment: '腿弯举机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对大腿后侧肌肉的孤立训练'
+    },
+    {
+      id: 18, name: '站姿杠铃推举', category: '肩部训练',
+      targetMuscles: ['三角肌前束', '三角肌中束'],
+      difficulty: 'intermediate', equipment: '杠铃', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '打造宽肩的王牌动作,同时也锻炼核心稳定性'
+    },
+    {
+      id: 19, name: '哑铃侧平举', category: '肩部训练',
+      targetMuscles: ['三角肌中束'],
+      difficulty: 'beginner', equipment: '哑铃', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '孤立训练中束,增加肩部宽度'
+    },
+    {
+      id: 20, name: '面拉', category: '肩部训练',
+      targetMuscles: ['三角肌后束', '斜方肌', '外旋肌群'],
+      difficulty: 'intermediate', equipment: '龙门架', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '改善圆肩驼背,强化肩后束和肩袖肌群'
+    },
+    {
+      id: 21, name: '杠铃弯举', category: '手臂训练',
+      targetMuscles: ['肱二头肌'],
+      difficulty: 'beginner', equipment: '杠铃', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '能够使用大重量的基础二头肌训练动作'
+    },
+    {
+      id: 22, name: '绳索下压', category: '手臂训练',
+      targetMuscles: ['肱三头肌'],
+      difficulty: 'beginner', equipment: '龙门架', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '持续张力刺激三头肌,适合收尾充血'
+    },
     {
       id: 7, name: '平板支撑', category: '核心训练',
       targetMuscles: ['腹直肌', '腹横肌', '竖脊肌'],
@@ -101,6 +199,14 @@ export class ExerciseActionsComponent {
       keepUrl: 'https://www.gotokeep.com/training',
       description: '经典腹肌训练动作'
     },
+    {
+      id: 23, name: '悬垂举腿', category: '核心训练',
+      targetMuscles: ['腹直肌下部', '髂腰肌'],
+      difficulty: 'intermediate', equipment: '单杠', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对下腹部的高强度训练'
+    },
     {
       id: 9, name: '开合跳', category: '有氧训练',
       targetMuscles: ['全身'],
@@ -116,6 +222,270 @@ export class ExerciseActionsComponent {
       bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
       keepUrl: 'https://www.gotokeep.com/training',
       description: '高强度全身训练动作'
+    },
+    {
+      id: 24, name: '椭圆机', category: '有氧训练',
+      targetMuscles: ['全身', '心肺功能'],
+      difficulty: 'beginner', equipment: '椭圆机', thumbnail: '🏃',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '低冲击有氧器械,保护膝盖'
+    },
+    {
+      id: 25, name: '划船机', category: '有氧训练',
+      targetMuscles: ['背部', '腿部', '心肺功能'],
+      difficulty: 'intermediate', equipment: '划船机', thumbnail: '🏃',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '全身参与度极高的有氧器械训练'
+    },
+    {
+      id: 26, name: '器械夹胸 (蝴蝶机)', category: '胸部训练',
+      targetMuscles: ['胸大肌', '胸中缝'],
+      difficulty: 'beginner', equipment: '蝴蝶机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '固定器械孤立刺激胸肌,安全性高,适合新手寻找发力感'
+    },
+    {
+      id: 27, name: 'T杠划船', category: '背部训练',
+      targetMuscles: ['背阔肌', '斜方肌', '大圆肌'],
+      difficulty: 'intermediate', equipment: 'T杠/地雷管', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '增加背部厚度的强力动作,允许使用较大重量'
+    },
+    {
+      id: 28, name: '史密斯深蹲', category: '腿部训练',
+      targetMuscles: ['股四头肌', '臀大肌'],
+      difficulty: 'intermediate', equipment: '史密斯机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '固定轨迹的深蹲,无需过多担心平衡,专注于腿部发力'
+    },
+    {
+      id: 29, name: '坐姿提踵', category: '腿部训练',
+      targetMuscles: ['比目鱼肌'],
+      difficulty: 'beginner', equipment: '提踵机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对小腿肌肉的孤立训练'
+    },
+    {
+      id: 30, name: '哑铃反向飞鸟', category: '肩部训练',
+      targetMuscles: ['三角肌后束', '菱形肌'],
+      difficulty: 'intermediate', equipment: '哑铃', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '强化肩部后侧,改善圆肩体态'
+    },
+    {
+      id: 31, name: '牧师凳弯举', category: '手臂训练',
+      targetMuscles: ['肱二头肌'],
+      difficulty: 'beginner', equipment: '牧师凳、曲杆杠铃/哑铃', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '固定大臂,孤立刺激二头肌短头,防止借力'
+    },
+    {
+      id: 32, name: '双杠臂屈伸', category: '手臂训练',
+      targetMuscles: ['肱三头肌', '胸大肌下束', '三角肌前束'],
+      difficulty: 'advanced', equipment: '双杠', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '自重训练动作,根据身体倾斜角度不同侧重胸或三头肌'
+    },
+    {
+      id: 33, name: '绳索卷腹', category: '核心训练',
+      targetMuscles: ['腹直肌'],
+      difficulty: 'intermediate', equipment: '龙门架', thumbnail: '🎯',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '利用绳索负重进行卷腹,增加腹肌厚度'
+    },
+    {
+      id: 34, name: '登山机', category: '有氧训练',
+      targetMuscles: ['臀大肌', '腿部', '心肺功能'],
+      difficulty: 'intermediate', equipment: '登山机', thumbnail: '🏃',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '模拟登山的高强度有氧器械,对臀部刺激显著'
+    },
+    {
+      id: 35, name: '坐姿髋外展', category: '腿部训练',
+      targetMuscles: ['臀中肌', '臀小肌'],
+      difficulty: 'beginner', equipment: '髋外展机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对臀部外侧的孤立训练,改善臀部凹陷'
+    },
+    {
+      id: 36, name: '坐姿髋内收', category: '腿部训练',
+      targetMuscles: ['大腿内收肌群'],
+      difficulty: 'beginner', equipment: '髋内收机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '紧致大腿内侧,改善腿部线条'
+    },
+    {
+      id: 37, name: '绳索侧平举', category: '肩部训练',
+      targetMuscles: ['三角肌中束'],
+      difficulty: 'intermediate', equipment: '龙门架', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '利用绳索持续张力刺激中束,相比哑铃效果更佳'
+    },
+    {
+      id: 38, name: '山羊挺身', category: '核心训练',
+      targetMuscles: ['竖脊肌', '臀大肌', '腘绳肌'],
+      difficulty: 'beginner', equipment: '罗马椅', thumbnail: '🎯',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '强化下背部核心力量,预防腰痛'
+    },
+    {
+      id: 39, name: '俄罗斯转体', category: '核心训练',
+      targetMuscles: ['腹斜肌', '腹直肌'],
+      difficulty: 'intermediate', equipment: '药球/哑铃', thumbnail: '🎯',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对侧腹部的旋转训练,雕刻腹肌线条'
+    },
+    {
+      id: 40, name: '动感单车', category: '有氧训练',
+      targetMuscles: ['腿部', '心肺功能'],
+      difficulty: 'intermediate', equipment: '动感单车', thumbnail: '🏃',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '高燃脂的有氧课程,伴随音乐节奏骑行'
+    },
+    {
+      id: 41, name: '哈克深蹲', category: '腿部训练',
+      targetMuscles: ['股四头肌', '臀大肌'],
+      difficulty: 'intermediate', equipment: '哈克深蹲机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '支撑背部的深蹲变式,更专注于腿部发力,减轻腰部压力'
+    },
+    {
+      id: 42, name: '杠铃卧推', category: '胸部训练',
+      targetMuscles: ['胸大肌', '三角肌前束', '肱三头肌'],
+      difficulty: 'intermediate', equipment: '杠铃、卧推凳', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '上半身力量训练的黄金动作,打造厚实胸肌'
+    },
+    {
+      id: 43, name: '传统硬拉', category: '背部训练',
+      targetMuscles: ['背阔肌', '竖脊肌', '臀大肌', '腘绳肌'],
+      difficulty: 'advanced', equipment: '杠铃', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '全身力量训练之王,强化后链肌群'
+    },
+    {
+      id: 44, name: '坐姿哑铃推举', category: '肩部训练',
+      targetMuscles: ['三角肌前束', '三角肌中束'],
+      difficulty: 'intermediate', equipment: '哑铃、训练凳', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '相比站姿更孤立刺激肩部,减少核心借力'
+    },
+    {
+      id: 45, name: '锤式弯举', category: '手臂训练',
+      targetMuscles: ['肱二头肌', '肱肌', '肱桡肌'],
+      difficulty: 'beginner', equipment: '哑铃', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对手臂外侧线条和前臂的训练'
+    },
+    {
+      id: 46, name: '哑铃颈后臂屈伸', category: '手臂训练',
+      targetMuscles: ['肱三头肌'],
+      difficulty: 'intermediate', equipment: '哑铃', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '充分拉伸三头肌长头,消除拜拜肉'
+    },
+    {
+      id: 47, name: '跑步机', category: '有氧训练',
+      targetMuscles: ['腿部', '心肺功能'],
+      difficulty: 'beginner', equipment: '跑步机', thumbnail: '🏃',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '最基础的有氧器械,可调节坡度增加强度'
+    },
+    {
+      id: 48, name: '坐姿推胸', category: '胸部训练',
+      targetMuscles: ['胸大肌', '肱三头肌'],
+      difficulty: 'beginner', equipment: '推胸机', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '固定轨迹的推胸动作,安全性高,适合大重量刺激'
+    },
+    {
+      id: 49, name: '杠铃臀推', category: '腿部训练',
+      targetMuscles: ['臀大肌', '腘绳肌'],
+      difficulty: 'intermediate', equipment: '杠铃、训练凳', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '发展臀部力量和维度的最佳动作之一'
+    },
+    {
+      id: 50, name: '保加利亚分腿蹲', category: '腿部训练',
+      targetMuscles: ['股四头肌', '臀大肌'],
+      difficulty: 'advanced', equipment: '哑铃/徒手、训练凳', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '单腿训练动作,极佳地刺激臀腿,同时挑战平衡性'
+    },
+    {
+      id: 51, name: '阿诺德推举', category: '肩部训练',
+      targetMuscles: ['三角肌前束', '三角肌中束'],
+      difficulty: 'intermediate', equipment: '哑铃', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '施瓦辛格发明的肩部训练动作,刺激范围更广'
+    },
+    {
+      id: 52, name: '直臂下压', category: '背部训练',
+      targetMuscles: ['背阔肌'],
+      difficulty: 'beginner', equipment: '龙门架', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '孤立训练背阔肌,消除腋下赘肉,建立背部连接感'
+    },
+    {
+      id: 53, name: '仰卧臂屈伸', category: '手臂训练',
+      targetMuscles: ['肱三头肌'],
+      difficulty: 'intermediate', equipment: '曲杆杠铃/哑铃', thumbnail: '💪',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '针对三头肌长头的经典动作,通常被称为碎颅者'
+    },
+    {
+      id: 54, name: '健腹轮', category: '核心训练',
+      targetMuscles: ['腹直肌', '核心整体'],
+      difficulty: 'advanced', equipment: '健腹轮', thumbnail: '🎯',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '极高强度的核心抗伸展训练'
+    },
+    {
+      id: 55, name: '绳索伐木', category: '核心训练',
+      targetMuscles: ['腹斜肌', '核心抗旋'],
+      difficulty: 'intermediate', equipment: '龙门架', thumbnail: '🎯',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '模拟伐木动作,强化核心旋转爆发力'
+    },
+    {
+      id: 56, name: '哑铃耸肩', category: '背部训练',
+      targetMuscles: ['斜方肌上束'],
+      difficulty: 'beginner', equipment: '哑铃', thumbnail: '🏋️',
+      bilibiliUrl: 'https://www.bilibili.com/video/BV1xx411c7BF',
+      keepUrl: 'https://www.gotokeep.com/training',
+      description: '改善溜肩,打造强壮的斜方肌'
     }
   ];
 

+ 123 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-detail/exercise-detail.component.html

@@ -0,0 +1,123 @@
+<div class="phone-inner">
+  <header class="nav-header">
+    <button class="back-btn" (click)="goBack()">❮</button>
+    <div class="page-title">运动详情</div>
+  </header>
+
+  <div class="content-area">
+    <!-- Today's Status -->
+    <section class="hero-card">
+      <div class="hero-bg-icon">🏃</div>
+      <div class="hero-label">今日运动总时长</div>
+      <div class="hero-main-stat">
+        <span class="hero-number">55</span>
+        <span class="hero-unit">分钟</span>
+      </div>
+      <div class="hero-stats-row">
+        <div class="stat-item">
+          <span class="stat-val">320</span>
+          <span class="stat-lbl">千卡消耗</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-val">2</span>
+          <span class="stat-lbl">运动次数</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-val">84</span>
+          <span class="stat-lbl">今日评分</span>
+        </div>
+      </div>
+    </section>
+
+    <!-- Today's Activities -->
+    <section>
+      <div class="section-header">
+        <div class="section-title">今日记录</div>
+        <div class="section-action" (click)="navigateToAddExercise()">+ 补录</div>
+      </div>
+      <div class="activity-list">
+        <article class="activity-card">
+          <div class="activity-icon">🏃</div>
+          <div class="activity-info">
+            <div class="activity-name">晨跑</div>
+            <div class="activity-time">07:30 - 08:00 · 户外</div>
+          </div>
+          <div class="activity-stat">
+            <div class="activity-kcal">200</div>
+            <div class="activity-kcal-unit">kcal</div>
+          </div>
+        </article>
+
+        <article class="activity-card">
+          <div class="activity-icon" style="color:#01847F; background:#E6F7F6;">🧘</div>
+          <div class="activity-info">
+            <div class="activity-name">功能性力量训练</div>
+            <div class="activity-time">18:00 - 18:25 · 健身房</div>
+          </div>
+          <div class="activity-stat">
+            <div class="activity-kcal">120</div>
+            <div class="activity-kcal-unit">kcal</div>
+          </div>
+        </article>
+      </div>
+    </section>
+
+    <!-- Weekly History -->
+    <section>
+      <div class="section-header">
+        <div class="section-title">本周趋势</div>
+        <div class="section-action" (click)="viewHistory()" style="cursor: pointer;">查看全部 &gt;</div>
+      </div>
+      <div class="chart-card">
+        <div class="bar-chart">
+          <!-- Monday -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 60%"></div></div>
+            <div class="bar-label">一</div>
+          </div>
+          <!-- Tuesday -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 40%"></div></div>
+            <div class="bar-label">二</div>
+          </div>
+          <!-- Wednesday -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 0%"></div></div>
+            <div class="bar-label">三</div>
+          </div>
+          <!-- Thursday -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 80%"></div></div>
+            <div class="bar-label">四</div>
+          </div>
+          <!-- Friday -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 30%"></div></div>
+            <div class="bar-label">五</div>
+          </div>
+          <!-- Saturday -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 90%; background: #F9D2E4;"></div></div>
+            <div class="bar-label">六</div>
+          </div>
+          <!-- Sunday (Today) -->
+          <div class="bar-group">
+            <div class="bar-track"><div class="bar-fill" style="height: 55%; box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--accent);"></div></div>
+            <div class="bar-label">日</div>
+          </div>
+        </div>
+        
+        <div style="margin-top: 16px;">
+          <div class="history-item">
+            <span class="history-date">本周总计</span>
+            <span class="history-val">245 分钟 / 1500 kcal</span>
+          </div>
+          <div class="history-item">
+            <span class="history-date">日均</span>
+            <span class="history-val">35 分钟</span>
+          </div>
+        </div>
+      </div>
+    </section>
+  </div>
+</div>

+ 282 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-detail/exercise-detail.component.scss

@@ -0,0 +1,282 @@
+:host {
+  display: block;
+  min-height: 100vh;
+  background: #f9fafb;
+
+  --bg: #e5e7eb;
+  --shell: #f9fafb;
+  --card: #ffffff;
+  --accent: #01847F; /* Mars Green */
+  --accent-soft: #F9D2E4; /* Rose Pink */
+  --text-main: #111827;
+  --text-muted: #6b7280;
+  --danger: #ef4444;
+  --success: #22c55e;
+}
+
+/* 局部变量定义,确保组件内生效 */
+.phone-inner {
+  flex: 1;
+  background: #f9fafb;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  min-height: 100vh;
+}
+
+/* Header */
+.nav-header {
+  padding: 16px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  background: #fff;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  border-bottom: 1px solid #f3f4f6;
+}
+
+.back-btn {
+  border: none;
+  background: none;
+  font-size: 20px;
+  color: var(--text-main);
+  cursor: pointer;
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  &:hover { background: #f3f4f6; }
+}
+
+.page-title {
+  font-size: 17px;
+  font-weight: 600;
+  color: var(--text-main);
+}
+
+.content-area {
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+/* Today's Overview Card */
+.hero-card {
+  border-radius: 24px;
+  background: linear-gradient(135deg, #01847F 0%, #016b67 100%);
+  padding: 24px;
+  color: white;
+  box-shadow: 0 10px 25px rgba(1, 132, 127, 0.25);
+  position: relative;
+  overflow: hidden;
+}
+
+.hero-bg-icon {
+  position: absolute;
+  right: -20px;
+  bottom: -20px;
+  font-size: 120px;
+  opacity: 0.1;
+  pointer-events: none;
+}
+
+.hero-label {
+  font-size: 13px;
+  font-weight: 500;
+  opacity: 0.9;
+  margin-bottom: 8px;
+}
+
+.hero-main-stat {
+  display: flex;
+  align-items: baseline;
+  gap: 4px;
+  margin-bottom: 16px;
+}
+
+.hero-number {
+  font-size: 42px;
+  font-weight: 800;
+  line-height: 1;
+}
+
+.hero-unit {
+  font-size: 16px;
+  font-weight: 600;
+  opacity: 0.9;
+}
+
+.hero-stats-row {
+  display: flex;
+  justify-content: space-between;
+  padding-top: 20px;
+  margin-top: 8px;
+  border-top: 1px solid rgba(255,255,255,0.2);
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start; /* 保持左对齐或改为 center */
+  flex: 1;
+  background: transparent; /* 确保无背景 */
+  color: white;
+}
+
+.stat-val {
+  font-size: 20px;
+  font-weight: 700;
+  line-height: 1.2;
+}
+
+.stat-lbl {
+  font-size: 12px;
+  opacity: 0.9;
+  margin-top: 2px;
+}
+
+/* Section Title */
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 8px;
+  margin-bottom: 4px;
+}
+
+.section-title {
+  font-size: 15px;
+  font-weight: 700;
+  color: var(--text-main);
+}
+
+.section-action {
+  font-size: 12px;
+  color: var(--accent);
+  font-weight: 500;
+  cursor: pointer;
+}
+
+/* Activity List */
+.activity-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.activity-card {
+  background: white;
+  border-radius: 16px;
+  padding: 12px 16px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.03);
+}
+
+.activity-icon {
+  width: 44px;
+  height: 44px;
+  border-radius: 12px;
+  background: var(--accent-soft);
+  color: var(--accent);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22px;
+}
+
+.activity-info {
+  flex: 1;
+}
+
+.activity-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-main);
+}
+
+.activity-time {
+  font-size: 11px;
+  color: var(--text-muted);
+  margin-top: 2px;
+}
+
+.activity-stat {
+  text-align: right;
+}
+
+.activity-kcal {
+  font-size: 14px;
+  font-weight: 700;
+  color: var(--text-main);
+}
+
+.activity-kcal-unit {
+  font-size: 10px;
+  color: var(--text-muted);
+}
+
+/* Weekly Chart (Simple CSS Bar Chart) */
+.chart-card {
+  background: white;
+  border-radius: 20px;
+  padding: 20px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.03);
+}
+
+.bar-chart {
+  display: flex;
+  justify-content: space-between;
+  height: 120px;
+  padding-top: 20px;
+}
+
+.bar-group {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  flex: 1;
+  height: 100%; /* Ensure full height */
+}
+
+.bar-track {
+  width: 12px;
+  height: 100%;
+  background: #f3f4f6;
+  border-radius: 6px;
+  position: relative;
+  display: flex;
+  align-items: flex-end;
+}
+
+.bar-fill {
+  width: 100%;
+  background: var(--accent);
+  border-radius: 6px;
+  transition: height 0.5s ease;
+}
+
+.bar-label {
+  font-size: 10px;
+  color: var(--text-muted);
+  font-weight: 500;
+}
+
+.history-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 12px 0;
+  border-bottom: 1px solid #f3f4f6;
+}
+.history-item:last-child { border-bottom: none; }
+
+.history-date { font-size: 13px; color: var(--text-main); font-weight: 500; }
+.history-val { font-size: 13px; color: var(--text-muted); }

+ 26 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-detail/exercise-detail.component.ts

@@ -0,0 +1,26 @@
+import { Component } from '@angular/core';
+import { CommonModule, Location } from '@angular/common';
+import { Router } from '@angular/router';
+
+@Component({
+  selector: 'app-exercise-detail',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './exercise-detail.component.html',
+  styleUrl: './exercise-detail.component.scss'
+})
+export class ExerciseDetailComponent {
+  constructor(private location: Location, private router: Router) {}
+
+  goBack(): void {
+    this.location.back();
+  }
+
+  viewHistory(): void {
+    this.router.navigate(['/exercise/history']);
+  }
+
+  navigateToAddExercise(): void {
+    this.router.navigate(['/exercise/add']);
+  }
+}

+ 12 - 31
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-history/exercise-history.component.html

@@ -2,14 +2,6 @@
   <app-page-header [title]="'历史记录'" (back)="backToExercise()"></app-page-header>
 
   <main class="history-main">
-    <!-- 月份导航 -->
-    <div class="month-nav">
-      <button class="nav-btn" (click)="previousMonth()">‹</button>
-      <span class="month-title">{{ currentYearMonth }}</span>
-      <button class="nav-btn" (click)="nextMonth()">›</button>
-      <button class="back-today-btn" (click)="goToCurrentMonth()">回到本月 →</button>
-    </div>
-
     <!-- 工具栏 -->
     <div class="toolbar">
       <div class="view-toggle">
@@ -43,29 +35,18 @@
     </div>
 
     <!-- 日历网格 -->
-    <div class="calendar">
-      <div class="calendar-header">
-        <span class="weekday" *ngFor="let day of weekDays">{{ day }}</span>
-      </div>
-      <div class="calendar-grid">
-        <div *ngFor="let day of calendarDays" class="calendar-day"
-          [class.empty]="!day"
-          [class.has-record]="day && day.exercises.length > 0"
-          [class.completed]="day?.completed"
-          [class.today]="day && isToday(day.date)"
-          [class.selected]="day && isSelected(day.date)"
-          (click)="selectDate(day)">
-          <ng-container *ngIf="day">
-            <span class="day-number">{{ day.date.getDate() }}</span>
-            <div class="day-content" *ngIf="day.exercises.length > 0">
-              <span class="completion-badge" *ngIf="day.completed">✓</span>
-              <span class="exercise-types">{{ getExerciseTypes(day) }}</span>
-              <span class="day-duration">{{ day.totalDuration }}分钟</span>
-            </div>
-          </ng-container>
-        </div>
-      </div>
-    </div>
+    <app-calendar-grid
+      [currentDate]="currentDate"
+      [dayDataMap]="calendarDataMap"
+      [selectedDate]="selectedDate"
+      [isManageMode]="isManageMode"
+      [selectedRecords]="selectedRecords"
+      [themeColor]="'green'"
+      (dateSelected)="onDateSelected($event)"
+      (previousMonth)="previousMonth()"
+      (nextMonth)="nextMonth()"
+      (goToCurrentMonth)="goToCurrentMonth()">
+    </app-calendar-grid>
 
     <!-- 选中日期详情 -->
     <div class="day-detail" *ngIf="selectedDayRecord && !isManageMode">

+ 2 - 1
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-history/exercise-history.component.scss

@@ -159,7 +159,7 @@
 }
 
 .calendar-day {
-  min-height: 70px;
+  height: 100px;
   padding: 0.35rem;
   border-radius: 8px;
   background: #f8fafc;
@@ -168,6 +168,7 @@
   display: flex;
   flex-direction: column;
   font-size: 0.7rem;
+  overflow: hidden;
 
   &.empty { background: transparent; cursor: default; }
   &.has-record { background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); border: 1px solid #a7f3d0; }

+ 40 - 4
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise-history/exercise-history.component.ts

@@ -1,8 +1,9 @@
 import { Component, OnInit } from '@angular/core';
-import { CommonModule } from '@angular/common';
+import { CommonModule, Location } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
 import { FormsModule } from '@angular/forms';
 import { PageHeaderComponent } from '../../../shared/page-header/page-header.component';
+import { CalendarGridComponent, CalendarDayData } from '../../../shared/components/calendar-grid/calendar-grid.component';
 
 interface ExerciseItem {
   type: string;
@@ -22,7 +23,7 @@ interface DayRecord {
 @Component({
   selector: 'app-exercise-history',
   standalone: true,
-  imports: [CommonModule, RouterModule, FormsModule, PageHeaderComponent],
+  imports: [CommonModule, RouterModule, FormsModule, PageHeaderComponent, CalendarGridComponent],
   templateUrl: './exercise-history.component.html',
   styleUrl: './exercise-history.component.scss'
 })
@@ -36,14 +37,16 @@ export class ExerciseHistoryComponent implements OnInit {
   calendarDays: (DayRecord | null)[] = [];
   weekDays = ['一', '二', '三', '四', '五', '六', '日'];
   historyRecords: Map<string, DayRecord> = new Map();
+  calendarDataMap: Map<string, CalendarDayData> = new Map();
 
   monthStats = { totalDays: 0, completedDays: 0, totalDuration: 0, totalCalories: 0 };
 
-  constructor(private router: Router) {}
+  constructor(private router: Router, private location: Location) {}
 
   ngOnInit(): void {
     this.generateMockData();
     this.buildCalendar();
+    this.buildCalendarDataMap();
     this.calculateMonthStats();
   }
 
@@ -56,6 +59,21 @@ export class ExerciseHistoryComponent implements OnInit {
     return this.historyRecords.get(this.formatDateKey(this.selectedDate)) || null;
   }
 
+  buildCalendarDataMap(): void {
+    this.calendarDataMap.clear();
+    this.historyRecords.forEach((record, key) => {
+      const exerciseTypes = record.exercises.map(e => e.type).slice(0, 2).join('、');
+      this.calendarDataMap.set(this.formatDateKeyPadded(record.date), {
+        date: record.date,
+        hasData: record.exercises.length > 0,
+        completed: record.completed,
+        primaryText: exerciseTypes,
+        secondaryText: `${record.totalDuration}分钟`,
+        badge: record.completed ? '✓' : ''
+      });
+    });
+  }
+
   buildCalendar(): void {
     const year = this.currentDate.getFullYear();
     const month = this.currentDate.getMonth();
@@ -93,6 +111,17 @@ export class ExerciseHistoryComponent implements OnInit {
     this.calculateMonthStats();
   }
 
+  onDateSelected(day: CalendarDayData | null): void {
+    if (!day) return;
+    if (this.isManageMode) {
+      const key = this.formatDateKeyPadded(day.date);
+      if (this.selectedRecords.has(key)) this.selectedRecords.delete(key);
+      else this.selectedRecords.add(key);
+    } else {
+      this.selectedDate = day.date;
+    }
+  }
+
   selectDate(day: DayRecord | null): void {
     if (!day) return;
     if (this.isManageMode) {
@@ -136,7 +165,7 @@ export class ExerciseHistoryComponent implements OnInit {
   }
 
   backToExercise(): void {
-    this.router.navigate(['/exercise']);
+    this.location.back();
   }
 
   formatDuration(mins: number): string {
@@ -175,6 +204,13 @@ export class ExerciseHistoryComponent implements OnInit {
     return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
   }
 
+  private formatDateKeyPadded(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}-${month}-${day}`;
+  }
+
   private generateMockData(): void {
     const exerciseTypes = [
       { type: '晨跑', duration: 30, calories: 250 },

+ 18 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise.routes.ts

@@ -3,6 +3,9 @@ import { ExerciseComponent } from './exercise.component';
 import { AddExerciseComponent } from './add-exercise/add-exercise.component';
 import { ExerciseActionsComponent } from './exercise-actions/exercise-actions.component';
 import { ExerciseHistoryComponent } from './exercise-history/exercise-history.component';
+import { ExerciseDetailComponent } from './exercise-detail/exercise-detail.component';
+import { StepsDetailComponent } from './steps-detail/steps-detail.component';
+import { FitnessSummaryComponent } from './fitness-summary/fitness-summary.component';
 
 export const exerciseRoutes: Routes = [
   {
@@ -10,6 +13,16 @@ export const exerciseRoutes: Routes = [
     component: ExerciseComponent,
     title: '运动记录 - 数智健调系统'
   },
+  {
+    path: 'detail',
+    component: ExerciseDetailComponent,
+    title: '运动详情 - 数智健调系统'
+  },
+  {
+    path: 'steps',
+    component: StepsDetailComponent,
+    title: '步数详情 - 数智健调系统'
+  },
   {
     path: 'add',
     component: AddExerciseComponent,
@@ -24,5 +37,10 @@ export const exerciseRoutes: Routes = [
     path: 'history',
     component: ExerciseHistoryComponent,
     title: '历史记录 - 数智健调系统'
+  },
+  {
+    path: 'fitness-summary',
+    component: FitnessSummaryComponent,
+    title: '体能训练摘要 - 数智健调系统'
   }
 ];

+ 98 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/fitness-summary/fitness-summary.component.html

@@ -0,0 +1,98 @@
+<div class="fitness-summary-container">
+  <app-page-header [title]="'体能训练摘要'" (back)="goBack()"></app-page-header>
+
+  <main class="main-content">
+    <!-- 问候语和头像 -->
+    <section class="greeting-section">
+      <div class="greeting-text">
+        <h2>你好,郑同学</h2>
+        <p>今天感觉如何?</p>
+      </div>
+      <div class="avatar">
+        <div class="avatar-icon">🐱</div>
+      </div>
+    </section>
+
+
+
+    <!-- 体能训练状态卡片 -->
+    <section class="training-status-card">
+      <div class="status-header">
+        <h3>今天的体能训练状态</h3>
+        <div class="completion-badge">{{ fitnessData.trainingStatus.level }}</div>
+      </div>
+      
+      <div class="status-content">
+        <div class="progress-ring">
+          <svg width="80" height="80" viewBox="0 0 80 80">
+            <circle class="progress-bg" cx="40" cy="40" r="35"/>
+            <circle class="progress-fill" cx="40" cy="40" r="35" 
+                    [style.stroke-dashoffset]="getProgressOffset(fitnessData.trainingStatus.completionRate)"/>
+          </svg>
+          <div class="progress-value">
+            <span class="percentage">{{ fitnessData.trainingStatus.completionRate }}%</span>
+          </div>
+        </div>
+        
+        <div class="status-info">
+          <p class="status-description">{{ fitnessData.trainingStatus.description }}</p>
+          <p class="status-recommendation">{{ fitnessData.trainingStatus.recommendation }}</p>
+          
+          <div class="status-details">
+            <div class="detail-item">
+              <span class="detail-label">本周训练</span>
+              <span class="detail-value">{{ fitnessData.weeklyStats.workoutDays }}天</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">总时长</span>
+              <span class="detail-value">{{ formatDuration(fitnessData.weeklyStats.totalDuration) }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">平均心率</span>
+              <span class="detail-value">{{ fitnessData.weeklyStats.avgHeartRate }}bpm</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <!-- 最近训练记录 -->
+    <section class="recent-workouts">
+      <div class="section-header">
+        <h3>最近训练</h3>
+        <button class="view-all-btn" (click)="goToExerciseHistory()">查看全部</button>
+      </div>
+      
+      <div class="workout-list">
+        @for (workout of fitnessData.recentWorkouts; track workout.date) {
+          <div class="workout-item">
+            <div class="workout-icon">
+              @switch (workout.type) {
+                @case ('跑步') { <span>🏃</span> }
+                @case ('力量训练') { <span>💪</span> }
+                @case ('瑜伽') { <span>🧘</span> }
+                @case ('游泳') { <span>🏊</span> }
+                @default { <span>🏃</span> }
+              }
+            </div>
+            <div class="workout-info">
+              <div class="workout-header">
+                <span class="workout-type">{{ workout.type }}</span>
+                <span class="workout-date">{{ formatDate(workout.date) }}</span>
+              </div>
+              <div class="workout-stats">
+                <span class="duration">{{ workout.duration }}分钟</span>
+                <span class="calories">{{ workout.calories }}kcal</span>
+                <span class="intensity" [class]="getIntensityClass(workout.intensity)">
+                  {{ getIntensityLabel(workout.intensity) }}
+                </span>
+              </div>
+            </div>
+          </div>
+        }
+      </div>
+    </section>
+
+
+  </main>
+</div>

+ 295 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/fitness-summary/fitness-summary.component.scss

@@ -0,0 +1,295 @@
+// 体能训练摘要页面样式
+.fitness-summary-container {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background: #f5f5f5;
+  color: #0f172a;
+}
+
+.main-content {
+  flex: 1;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding-bottom: 80px;
+}
+
+// ==================== 问候语区域 ====================
+.greeting-section {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 0;
+}
+
+.greeting-text {
+  h2 {
+    margin: 0 0 4px 0;
+    font-size: 24px;
+    font-weight: 700;
+    color: #0f172a;
+  }
+
+  p {
+    margin: 0;
+    font-size: 14px;
+    color: #6b7280;
+  }
+}
+
+.avatar {
+  width: 60px;
+  height: 60px;
+  background: linear-gradient(135deg, #01847F 0%, #016c68 100%);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4px 12px rgba(1, 132, 127, 0.3);
+}
+
+.avatar-icon {
+  font-size: 28px;
+}
+
+
+
+// ==================== 体能训练状态卡片 ====================
+.training-status-card {
+  background: linear-gradient(135deg, #01847F 0%, #016c68 100%);
+  border-radius: 20px;
+  padding: 24px;
+  color: white;
+  box-shadow: 0 8px 24px rgba(1, 132, 127, 0.3);
+}
+
+.status-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+
+  h3 {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 700;
+  }
+}
+
+.completion-badge {
+  background: rgba(255, 255, 255, 0.2);
+  padding: 6px 14px;
+  border-radius: 20px;
+  font-size: 14px;
+  font-weight: 600;
+  backdrop-filter: blur(10px);
+}
+
+.status-content {
+  display: flex;
+  gap: 20px;
+  align-items: center;
+}
+
+.progress-ring {
+  position: relative;
+  flex-shrink: 0;
+
+  svg {
+    transform: rotate(-90deg);
+  }
+}
+
+.progress-bg {
+  fill: none;
+  stroke: rgba(255, 255, 255, 0.2);
+  stroke-width: 6;
+}
+
+.progress-fill {
+  fill: none;
+  stroke: white;
+  stroke-width: 6;
+  stroke-linecap: round;
+  stroke-dasharray: 220;
+  transition: stroke-dashoffset 0.5s ease;
+}
+
+.progress-value {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  text-align: center;
+
+  .percentage {
+    font-size: 20px;
+    font-weight: 800;
+    line-height: 1;
+  }
+}
+
+.status-info {
+  flex: 1;
+}
+
+.status-description {
+  margin: 0 0 8px 0;
+  font-size: 14px;
+  line-height: 1.5;
+  opacity: 0.9;
+}
+
+.status-recommendation {
+  margin: 0 0 16px 0;
+  font-size: 13px;
+  opacity: 0.8;
+  font-style: italic;
+}
+
+.status-details {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.detail-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 13px;
+}
+
+.detail-label {
+  opacity: 0.8;
+}
+
+.detail-value {
+  font-weight: 600;
+}
+
+// ==================== 最近训练记录 ====================
+.recent-workouts {
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 700;
+      color: #0f172a;
+    }
+  }
+
+  .view-all-btn {
+    background: none;
+    border: none;
+    color: #01847F;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    padding: 4px 8px;
+    border-radius: 6px;
+
+    &:hover {
+      background: rgba(1, 132, 127, 0.1);
+    }
+  }
+}
+
+.workout-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.workout-item {
+  background: #ffffff;
+  border-radius: 14px;
+  padding: 16px;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+}
+
+.workout-icon {
+  width: 44px;
+  height: 44px;
+  background: rgba(1, 132, 127, 0.1);
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+}
+
+.workout-info {
+  flex: 1;
+}
+
+.workout-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 6px;
+}
+
+.workout-type {
+  font-size: 16px;
+  font-weight: 600;
+  color: #0f172a;
+}
+
+.workout-date {
+  font-size: 12px;
+  color: #6b7280;
+}
+
+.workout-stats {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.duration,
+.calories {
+  font-size: 13px;
+  color: #6b7280;
+}
+
+.intensity {
+  font-size: 11px;
+  font-weight: 600;
+  padding: 2px 8px;
+  border-radius: 8px;
+
+  &.intensity-low {
+    background: #dbeafe;
+    color: #1e40af;
+  }
+
+  &.intensity-medium {
+    background: #fef3c7;
+    color: #b45309;
+  }
+
+  &.intensity-high {
+    background: #fecaca;
+    color: #991b1b;
+  }
+}
+
+
+
+// ==================== 响应式设计 ====================
+@media (min-width: 768px) {
+  .status-content {
+    gap: 32px;
+  }
+}

+ 160 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/fitness-summary/fitness-summary.component.ts

@@ -0,0 +1,160 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { Location } from '@angular/common';
+import { PageHeaderComponent } from '../../../shared/page-header/page-header.component';
+
+// 体能数据接口
+export interface FitnessData {
+  trainingStatus: {
+    completionRate: number;
+    level: string;
+    description: string;
+    recommendation: string;
+  };
+  weeklyStats: {
+    workoutDays: number;
+    totalDuration: number; // 分钟
+    avgHeartRate: number;
+    caloriesBurned: number;
+  };
+  recentWorkouts: Array<{
+    date: Date;
+    type: string;
+    duration: number;
+    calories: number;
+    intensity: 'low' | 'medium' | 'high';
+  }>;
+}
+
+@Component({
+  selector: 'app-fitness-summary',
+  standalone: true,
+  imports: [CommonModule, PageHeaderComponent],
+  templateUrl: './fitness-summary.component.html',
+  styleUrl: './fitness-summary.component.scss'
+})
+export class FitnessSummaryComponent implements OnInit {
+  
+  // 暴露Math对象给模板使用
+  Math = Math;
+  
+  fitnessData: FitnessData = {
+    trainingStatus: {
+      completionRate: 82,
+      level: '良好',
+      description: '本周完成4次训练,达到预期目标的82%',
+      recommendation: '建议增加1-2次有氧运动'
+    },
+    weeklyStats: {
+      workoutDays: 4,
+      totalDuration: 240, // 4小时
+      avgHeartRate: 145,
+      caloriesBurned: 1580
+    },
+    recentWorkouts: [
+      {
+        date: new Date('2025-12-13'),
+        type: '跑步',
+        duration: 30,
+        calories: 280,
+        intensity: 'medium'
+      },
+      {
+        date: new Date('2025-12-12'),
+        type: '力量训练',
+        duration: 45,
+        calories: 320,
+        intensity: 'high'
+      },
+      {
+        date: new Date('2025-12-11'),
+        type: '瑜伽',
+        duration: 60,
+        calories: 180,
+        intensity: 'low'
+      },
+      {
+        date: new Date('2025-12-10'),
+        type: '游泳',
+        duration: 40,
+        calories: 350,
+        intensity: 'high'
+      }
+    ]
+  };
+
+  constructor(
+    private router: Router,
+    private location: Location
+  ) {}
+
+  ngOnInit(): void {
+    // 可以在这里加载实际数据
+  }
+
+  // 返回上一页
+  goBack(): void {
+    this.location.back();
+  }
+
+
+
+  // 获取强度标签
+  getIntensityLabel(intensity: string): string {
+    switch (intensity) {
+      case 'low': return '低强度';
+      case 'medium': return '中强度';
+      case 'high': return '高强度';
+      default: return '';
+    }
+  }
+
+  // 获取强度颜色类
+  getIntensityClass(intensity: string): string {
+    switch (intensity) {
+      case 'low': return 'intensity-low';
+      case 'medium': return 'intensity-medium';
+      case 'high': return 'intensity-high';
+      default: return '';
+    }
+  }
+
+  // 格式化日期
+  formatDate(date: Date): string {
+    const today = new Date();
+    const diffTime = today.getTime() - date.getTime();
+    const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+    
+    if (diffDays === 0) return '今天';
+    if (diffDays === 1) return '昨天';
+    if (diffDays === 2) return '前天';
+    return `${diffDays}天前`;
+  }
+
+  // 格式化时长
+  formatDuration(minutes: number): string {
+    if (minutes < 60) return `${minutes}分钟`;
+    const hours = Math.floor(minutes / 60);
+    const mins = minutes % 60;
+    return mins > 0 ? `${hours}小时${mins}分钟` : `${hours}小时`;
+  }
+
+  // 跳转到运动记录
+  goToExerciseRecord(): void {
+    this.router.navigate(['/exercise/add']);
+  }
+
+  // 跳转到运动历史
+  goToExerciseHistory(): void {
+    this.router.navigate(['/exercise/history']);
+  }
+
+
+
+  // 获取进度环偏移量
+  getProgressOffset(percentage: number): number {
+    // 220 是圆周长 (2 * PI * 35)
+    return 220 * (1 - percentage / 100);
+  }
+}

+ 117 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/steps-detail/steps-detail.component.html

@@ -0,0 +1,117 @@
+<div class="phone-inner">
+  <header class="nav-header">
+    <button class="back-btn" (click)="goBack()">❮</button>
+    <div class="page-title">步数详情</div>
+  </header>
+
+  <div class="content-area">
+    <!-- Hero Card -->
+    <section class="hero-card">
+      <div class="goal-pill">目标 10,000 步</div>
+      <div class="ring-container">
+        <div class="progress-ring"></div>
+        <div class="ring-content">
+          <span class="step-count">8,230</span>
+          <span class="step-label">今日步数</span>
+        </div>
+      </div>
+      
+      <div class="hero-stats-row">
+        <div class="stat-item">
+          <span class="stat-val">5.8</span>
+          <span class="stat-lbl">公里</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-val">342</span>
+          <span class="stat-lbl">千卡</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-val">8</span>
+          <span class="stat-lbl">楼层</span>
+        </div>
+      </div>
+    </section>
+
+    <!-- Hourly Trend -->
+    <section class="chart-section">
+      <div class="chart-header">今日分布</div>
+      <div class="hourly-chart">
+        <!-- Simulated hourly data -->
+        <div class="hour-bar"><div class="hour-fill" style="height: 10%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 5%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 20%"></div></div> <!-- 6am -->
+        <div class="hour-bar"><div class="hour-fill" style="height: 60%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 30%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 15%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 10%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 20%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 40%"></div></div> <!-- 12pm -->
+        <div class="hour-bar"><div class="hour-fill" style="height: 20%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 10%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 15%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 50%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 80%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 30%"></div></div> <!-- 6pm -->
+        <div class="hour-bar"><div class="hour-fill" style="height: 10%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 5%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+        <div class="hour-bar"><div class="hour-fill" style="height: 0%"></div></div>
+
+        <div class="x-axis">
+          <span>00:00</span>
+          <span>06:00</span>
+          <span>12:00</span>
+          <span>18:00</span>
+          <span>23:59</span>
+        </div>
+      </div>
+    </section>
+
+    <!-- History List -->
+    <section>
+      <div class="chart-header" style="margin: 0 0 8px 4px;">过去 7 天</div>
+      <div class="history-list">
+        <div class="history-row">
+          <div class="h-date">今天</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 82%"></div></div>
+          <div class="h-steps">8,230</div>
+        </div>
+        <div class="history-row">
+          <div class="h-date">周六</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 100%"></div></div>
+          <div class="h-steps">11,402</div>
+        </div>
+        <div class="history-row">
+          <div class="h-date">周五</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 65%"></div></div>
+          <div class="h-steps">6,521</div>
+        </div>
+        <div class="history-row">
+          <div class="h-date">周四</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 90%"></div></div>
+          <div class="h-steps">9,200</div>
+        </div>
+        <div class="history-row">
+          <div class="h-date">周三</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 45%"></div></div>
+          <div class="h-steps">4,500</div>
+        </div>
+        <div class="history-row">
+          <div class="h-date">周二</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 75%"></div></div>
+          <div class="h-steps">7,800</div>
+        </div>
+        <div class="history-row">
+          <div class="h-date">周一</div>
+          <div class="h-bar-bg"><div class="h-bar-fill" style="width: 85%"></div></div>
+          <div class="h-steps">8,600</div>
+        </div>
+      </div>
+    </section>
+  </div>
+</div>

+ 257 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/steps-detail/steps-detail.component.scss

@@ -0,0 +1,257 @@
+:host {
+  display: block;
+  min-height: 100vh;
+  background: #f9fafb;
+
+  --bg: #e5e7eb;
+  --shell: #f9fafb;
+  --card: #ffffff;
+  --accent: #01847F; /* Mars Green */
+  --accent-soft: #F9D2E4; /* Rose Pink */
+  --text-main: #111827;
+  --text-muted: #6b7280;
+}
+
+.phone-inner {
+  flex: 1;
+  background: #f9fafb;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  min-height: 100vh;
+}
+
+/* Header */
+.nav-header {
+  padding: 16px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  background: #fff;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  border-bottom: 1px solid #f3f4f6;
+}
+
+.back-btn {
+  border: none;
+  background: none;
+  font-size: 20px;
+  color: var(--text-main);
+  cursor: pointer;
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  &:hover { background: #f3f4f6; }
+}
+
+.page-title {
+  font-size: 17px;
+  font-weight: 600;
+  color: var(--text-main);
+}
+
+.content-area {
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+/* Hero Card */
+.hero-card {
+  border-radius: 24px;
+  background: linear-gradient(135deg, #01847F 0%, #016b67 100%);
+  padding: 24px 20px;
+  color: white;
+  box-shadow: 0 10px 25px rgba(1, 132, 127, 0.3);
+  position: relative;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.ring-container {
+  position: relative;
+  width: 160px;
+  height: 160px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16px;
+}
+
+/* Simple CSS Ring using conic-gradient */
+.progress-ring {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  background: conic-gradient(
+    #ffffff 82%, 
+    rgba(255,255,255,0.2) 82% 100%
+  );
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  /* Cutout center */
+  mask: radial-gradient(transparent 58%, black 60%);
+  -webkit-mask: radial-gradient(transparent 58%, black 60%);
+}
+
+.ring-content {
+  position: absolute;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.step-count {
+  font-size: 32px;
+  font-weight: 800;
+  line-height: 1;
+}
+
+.step-label {
+  font-size: 12px;
+  opacity: 0.8;
+  margin-top: 4px;
+}
+
+.hero-stats-row {
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  padding-top: 16px;
+  border-top: 1px solid rgba(255,255,255,0.15);
+  background: transparent; /* Ensure transparent */
+}
+
+.stat-item {
+  text-align: center;
+  flex: 1;
+  background: transparent; /* Ensure transparent */
+  color: white; /* Ensure white text */
+}
+
+.stat-val {
+  display: block;
+  font-size: 16px;
+  font-weight: 700;
+  color: white;
+}
+
+.stat-lbl {
+  font-size: 11px;
+  opacity: 0.8;
+  color: white;
+}
+
+/* Hourly Chart */
+.chart-section {
+  background: white;
+  border-radius: 20px;
+  padding: 16px;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.03);
+}
+
+.chart-header {
+  font-size: 14px;
+  font-weight: 600;
+  margin-bottom: 12px;
+  color: var(--text-main);
+}
+
+.hourly-chart {
+  display: flex;
+  align-items: flex-end;
+  gap: 4px;
+  height: 100px;
+  padding-bottom: 20px; /* space for x-axis */
+  position: relative;
+}
+
+.hour-bar {
+  flex: 1;
+  background: var(--accent-soft);
+  border-radius: 2px;
+  height: 100%; /* max height container */
+  position: relative;
+}
+
+.hour-fill {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  background: var(--accent);
+  border-radius: 2px;
+  min-height: 4px;
+}
+
+.x-axis {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  font-size: 10px;
+  color: var(--text-muted);
+}
+
+/* Weekly List */
+.history-list {
+  background: white;
+  border-radius: 20px;
+  padding: 0 16px;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.03);
+}
+
+.history-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 0;
+  border-bottom: 1px solid #f3f4f6;
+}
+.history-row:last-child { border-bottom: none; }
+
+.h-date {
+  font-size: 14px;
+  color: var(--text-main);
+  font-weight: 500;
+}
+
+.h-steps {
+  font-size: 15px;
+  font-weight: 700;
+  color: var(--text-main);
+}
+
+.h-bar-bg {
+  flex: 1;
+  height: 6px;
+  background: #f3f4f6;
+  border-radius: 99px;
+  margin: 0 16px;
+  overflow: hidden;
+}
+
+.h-bar-fill {
+  height: 100%;
+  background: var(--accent);
+  border-radius: 99px;
+}
+
+.goal-pill {
+  display: inline-block;
+  padding: 4px 10px;
+  background: rgba(255,255,255,0.2);
+  border-radius: 99px;
+  font-size: 11px;
+  margin-bottom: 12px;
+}

+ 18 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/steps-detail/steps-detail.component.ts

@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+import { CommonModule, Location } from '@angular/common';
+import { Router } from '@angular/router';
+
+@Component({
+  selector: 'app-steps-detail',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './steps-detail.component.html',
+  styleUrl: './steps-detail.component.scss'
+})
+export class StepsDetailComponent {
+  constructor(private location: Location, private router: Router) {}
+
+  goBack(): void {
+    this.location.back();
+  }
+}

+ 317 - 547
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html

@@ -2,343 +2,180 @@
   <app-page-header [title]="'健康监测'" (back)="backToDashboard()"></app-page-header>
 
   <main class="main-content">
-    <div class="monitoring-content">
-      <!-- 综合健康状态卡片 - 重新设计 -->
-      <section class="health-status-section">
-        <div class="health-status-card-redesigned">
-          <div class="status-left-panel">
-            <h3 class="section-title">综合健康状态</h3>
-            <div class="health-score-display">
-              <div class="score-circle">
-                <div class="score-number">85</div>
-                <div class="score-label">健康评分</div>
-              </div>
-              <div class="status-badge-large good">
-                <span class="status-icon">✓</span>
-                <span class="status-text">良好</span>
-              </div>
-            </div>
-            <p class="status-description-compact">心率、血压和血氧指标均在正常范围内</p>
-            <button class="view-report-btn" (click)="openDetailModal('comprehensive')">
-              <span>查看详细报告</span>
-              <span class="btn-arrow">→</span>
-            </button>
-          </div>
-          
-          <div class="status-right-panel">
-            <div class="metrics-grid">
-              <div class="metric-card heart-rate clickable" (click)="openMetricModal('heartRate')">
-                <div class="metric-card-top">
-                  <div class="metric-icon">💗</div>
-                  <div class="metric-info">
-                    <div class="metric-label">心率</div>
-                    <div class="metric-value-large">{{ heartRate.value }} <span class="unit">{{ heartRate.unit }}</span></div>
-                    <div class="metric-status status-{{ heartRate.status }}">{{ getStatusText(heartRate.status) }}</div>
-                  </div>
-                </div>
-                <div class="metric-actions">
-                  <button class="metric-action-btn record" (click)="$event.stopPropagation(); openRecordModal('heartRate')" title="记录">
-                    <span>记录</span>
-                  </button>
-                  <button class="metric-action-btn detail" (click)="$event.stopPropagation(); openDetailModal('heartRate')" title="详情">
-                    <span>详情</span>
-                  </button>
-                </div>
-              </div>
-              
-              <div class="metric-card blood-pressure clickable" (click)="openMetricModal('bloodPressure')">
-                <div class="metric-card-top">
-                  <div class="metric-icon">🩺</div>
-                  <div class="metric-info">
-                    <div class="metric-label">血压</div>
-                    <div class="metric-value-large">{{ bloodPressure.systolic.value }}/{{ bloodPressure.diastolic.value }} <span class="unit">mmHg</span></div>
-                    <div class="metric-status status-{{ bloodPressure.systolic.status }}">{{ getStatusText(bloodPressure.systolic.status) }}</div>
-                  </div>
-                </div>
-                <div class="metric-actions">
-                  <button class="metric-action-btn record" (click)="$event.stopPropagation(); openRecordModal('bloodPressure')" title="记录">
-                    <span>记录</span>
-                  </button>
-                  <button class="metric-action-btn detail" (click)="$event.stopPropagation(); openDetailModal('bloodPressure')" title="详情">
-                    <span>详情</span>
-                  </button>
-                </div>
-              </div>
-              
-              <div class="metric-card blood-oxygen clickable" (click)="openMetricModal('bloodOxygen')">
-                <div class="metric-card-top">
-                  <div class="metric-icon">🫁</div>
-                  <div class="metric-info">
-                    <div class="metric-label">血氧</div>
-                    <div class="metric-value-large">{{ bloodOxygen.value }}<span class="unit">%</span></div>
-                    <div class="metric-status status-{{ bloodOxygen.status }}">{{ getStatusText(bloodOxygen.status) }}</div>
-                  </div>
-                </div>
-                <div class="metric-actions">
-                  <button class="metric-action-btn record" (click)="$event.stopPropagation(); openRecordModal('bloodOxygen')" title="记录">
-                    <span>记录</span>
-                  </button>
-                  <button class="metric-action-btn detail" (click)="$event.stopPropagation(); openDetailModal('bloodOxygen')" title="详情">
-                    <span>详情</span>
-                  </button>
-                </div>
-              </div>
-            </div>
-          </div>
+    <!-- 健康评分卡片 -->
+    <section class="health-score-card">
+      <div class="score-ring">
+        <svg width="90" height="90" viewBox="0 0 90 90">
+          <circle class="score-ring-bg" cx="45" cy="45" r="40"/>
+          <circle class="score-ring-progress" cx="45" cy="45" r="40" [style.stroke-dashoffset]="getScoreOffset()"/>
+        </svg>
+        <div class="score-value">
+          <div class="score-number">{{ healthScore }}</div>
+          <div class="score-label">健康分</div>
         </div>
-      </section>
-
-      <!-- 数据可视化区域 -->
-      <section class="data-visualization">
-        <div class="section-header">
-          <h2>数据趋势分析</h2>
-          <div class="time-range-selector">
-            <select [(ngModel)]="selectedTimeRange" (change)="onTimeRangeChange()">
-              <option value="week">最近7天</option>
-              <option value="month">最近30天</option>
-              <option value="threeMonths">最近3个月</option>
-            </select>
-          </div>
+      </div>
+      <div class="score-info">
+        <div class="score-status" [class.good]="healthScore >= 80" [class.warning]="healthScore >= 60 && healthScore < 80" [class.danger]="healthScore < 60">
+          <span>{{ healthScore >= 80 ? '✓' : healthScore >= 60 ? '!' : '✗' }}</span>
+          <span>{{ healthScore >= 80 ? '状态良好' : healthScore >= 60 ? '需要关注' : '需要改善' }}</span>
         </div>
+        <p class="score-desc">{{ getHealthSummary() }}</p>
+      </div>
+    </section>
 
-        <div class="charts-container">
-          <!-- 心率图表 -->
-          <div class="chart-card">
-            <h3>心率趋势</h3>
-            <div class="chart-wrapper">
-              <div id="heartRateChart" style="width: 100%; height: 100%;"></div>
-            </div>
-          </div>
-
-          <!-- 血压图表 -->
-          <div class="chart-card">
-            <h3>血压趋势</h3>
-            <div class="chart-wrapper">
-              <div id="bloodPressureChart" style="width: 100%; height: 100%;"></div>
-            </div>
-          </div>
+    <!-- 核心指标网格 -->
+    <section class="metrics-grid">
+      <div class="metric-item" (click)="openMetricModal('heartRate')">
+        <div class="metric-icon">💗</div>
+        <div class="metric-value">{{ heartRate.value }}<span class="metric-unit">{{ heartRate.unit }}</span></div>
+        <div class="metric-label">心率</div>
+        <span class="metric-status" [class.status-normal]="heartRate.status === 'normal'" [class.status-warning]="heartRate.status === 'warning'" [class.status-danger]="heartRate.status === 'danger'">{{ getStatusText(heartRate.status) }}</span>
+      </div>
+      <div class="metric-item" (click)="openMetricModal('bloodPressure')">
+        <div class="metric-icon">🩺</div>
+        <div class="metric-value">{{ bloodPressure.systolic.value }}<span class="metric-unit">/{{ bloodPressure.diastolic.value }}</span></div>
+        <div class="metric-label">血压</div>
+        <span class="metric-status" [class.status-normal]="bloodPressure.systolic.status === 'normal'" [class.status-warning]="bloodPressure.systolic.status === 'warning'" [class.status-danger]="bloodPressure.systolic.status === 'danger'">{{ getStatusText(bloodPressure.systolic.status) }}</span>
+      </div>
+      <div class="metric-item" (click)="openMetricModal('bloodOxygen')">
+        <div class="metric-icon">🫁</div>
+        <div class="metric-value">{{ bloodOxygen.value }}<span class="metric-unit">%</span></div>
+        <div class="metric-label">血氧</div>
+        <span class="metric-status" [class.status-normal]="bloodOxygen.status === 'normal'" [class.status-warning]="bloodOxygen.status === 'warning'" [class.status-danger]="bloodOxygen.status === 'danger'">{{ getStatusText(bloodOxygen.status) }}</span>
+      </div>
+    </section>
 
-          <!-- 血氧图表 -->
-          <div class="chart-card">
-            <h3>血氧趋势</h3>
-            <div class="chart-wrapper">
-              <div id="bloodOxygenChart" style="width: 100%; height: 100%;"></div>
-            </div>
-          </div>
+    <!-- 快捷操作 -->
+    <section class="quick-actions">
+      <button class="quick-btn primary" (click)="openMetricSelectionModal()">📝 记录数据</button>
+      <button class="quick-btn" (click)="openDetailModal('comprehensive')">📊 详细报告</button>
+    </section>
 
-          <!-- 健康指标关联分析图表 -->
-          <div class="chart-card correlation-chart">
-            <h3>健康指标关联分析</h3>
-            <div class="chart-wrapper">
-              <div id="correlationChart" style="width: 100%; height: 100%;"></div>
-            </div>
-            <p class="chart-description">心率与收缩压的相关性分析,帮助了解身体状况的整体表现</p>
+    <!-- 趋势图 -->
+    <section class="trend-section">
+      <div class="section-header">
+        <span class="section-title">数据趋势</span>
+        <select class="time-select" [(ngModel)]="selectedTimeRange" (change)="onTimeRangeChange()">
+          <option value="week">最近7天</option>
+          <option value="month">最近30天</option>
+          <option value="threeMonths">最近3个月</option>
+        </select>
+      </div>
+      <div class="trend-card">
+        <div class="trend-tabs">
+          <button class="trend-tab" [class.active]="activeTrendTab === 'heartRate'" (click)="setActiveTrendTab('heartRate')">心率</button>
+          <button class="trend-tab" [class.active]="activeTrendTab === 'bloodPressure'" (click)="setActiveTrendTab('bloodPressure')">血压</button>
+          <button class="trend-tab" [class.active]="activeTrendTab === 'bloodOxygen'" (click)="setActiveTrendTab('bloodOxygen')">血氧</button>
+        </div>
+        <div class="chart-wrapper">
+          <div id="trendChart" style="width: 100%; height: 100%;"></div>
+          <div class="chart-loading" *ngIf="!trendChart">
+            <div class="loading-spinner"></div>
+            <span>加载图表中...</span>
           </div>
         </div>
-      </section>
+      </div>
+    </section>
 
-      <!-- 慢性病监测区域 -->
-      @if (chronicConditions.length > 0) {
-        <section class="chronic-conditions-section">
-          <h2>慢性病监测与锻炼注意事项</h2>
-          <div class="chronic-conditions-list">
-            @for (condition of chronicConditions; track condition.name) {
-              <div class="chronic-condition-card">
-                <div class="condition-header">
-                  <h3>{{ condition.name }}</h3>
-                  <span class="severity-badge severity-{{ condition.severity }}">
-                    {{ condition.severity === 'mild' ? '轻度' : condition.severity === 'moderate' ? '中度' : '重度' }}
-                  </span>
-                </div>
-                <div class="condition-content">
-                  <h4>锻炼注意事项:</h4>
-                  <ul class="exercise-restrictions">
-                    @for (restriction of condition.exerciseRestrictions; track restriction) {
-                      <li>{{ restriction }}</li>
-                    }
-                  </ul>
-                  <div class="checkup-info">
-                    <p><strong>上次检查:</strong> {{ formatDate(condition.lastCheckup) }}</p>
-                    @if (condition.nextCheckupReminder) {
-                      <p class="reminder"><strong>下次检查提醒:</strong> {{ formatDate(condition.nextCheckupReminder) }}</p>
-                    }
-                  </div>
-                </div>
-              </div>
-            }
-          </div>
-        </section>
-      }
 
-      <!-- 生活方式建议区域 -->
-      <section class="lifestyle-tips-section">
-        <h2>生活方式建议</h2>
-        <div class="lifestyle-tips-cards">
-          <div class="lifestyle-tip-card exercise">
-            <h3>运动建议</h3>
-            <p>建议每日进行30分钟中等强度有氧运动,如快走、游泳或骑行,可以有效提升心肺功能,维持健康体重。</p>
-            <div class="tip-highlight">
-              运动前记得热身,运动后进行拉伸,避免剧烈运动导致受伤。
-            </div>
-          </div>
-          <div class="lifestyle-tip-card diet">
-            <h3>饮食建议</h3>
-            <p>保持饮食均衡,多摄入新鲜蔬菜和水果,减少盐分和脂肪摄入,选择全谷物和优质蛋白质来源。</p>
-            <div class="tip-highlight">
-              建议每日盐摄入量不超过5克,避免加工食品和高糖饮料。
-            </div>
-          </div>
-          <div class="lifestyle-tip-card sleep">
-            <h3>睡眠建议</h3>
-            <p>保证每日7-8小时的充足睡眠,建立规律的睡眠习惯,避免熬夜和睡前使用电子设备。</p>
-            <div class="tip-highlight">
-              睡前放松身心,可尝试冥想或听轻音乐帮助入睡。
-            </div>
-          </div>
-          <div class="lifestyle-tip-card hydration">
-            <h3>水分补充</h3>
-            <p>每天饮水约2000毫升,保持身体水分充足,促进新陈代谢和废物排出。</p>
-            <div class="tip-highlight">
-              避免等到口渴才喝水,养成定时饮水的好习惯。
+    <!-- 慢性病监测 -->
+    @if (chronicConditions.length > 0) {
+      <section class="chronic-section">
+        <div class="section-header">
+          <span class="section-title">健康提醒</span>
+        </div>
+        @for (condition of chronicConditions; track condition.name) {
+          <div class="chronic-card">
+            <div class="chronic-header">
+              <span class="chronic-name">{{ condition.name }}</span>
+              <span class="chronic-badge" [class.mild]="condition.severity === 'mild'" [class.moderate]="condition.severity === 'moderate'" [class.severe]="condition.severity === 'severe'">
+                {{ condition.severity === 'mild' ? '轻度' : condition.severity === 'moderate' ? '中度' : '重度' }}
+              </span>
             </div>
+            <ul class="chronic-tips">
+              @for (restriction of condition.exerciseRestrictions; track restriction) {
+                <li>{{ restriction }}</li>
+              }
+            </ul>
+            @if (condition.nextCheckupReminder) {
+              <div class="chronic-reminder">📅 下次检查:{{ formatDate(condition.nextCheckupReminder) }}</div>
+            }
           </div>
-        </div>
+        }
       </section>
+    }
 
-      <!-- AI健康建议区域 -->
-      <section class="health-tips-section">
-        <h2>AI健康建议</h2>
-        <div class="health-tips-cards">
-          <div class="health-tip-card">
-            <h3>心率建议</h3>
-            <p>{{ getHealthAdvice('heartRate') }}</p>
-          </div>
-          <div class="health-tip-card">
-            <h3>血压建议</h3>
-            <p>{{ getHealthAdvice('bloodPressure') }}</p>
-          </div>
-          <div class="health-tip-card">
-            <h3>血氧建议</h3>
-            <p>{{ getHealthAdvice('bloodOxygen') }}</p>
-          </div>
-          @if (isFemaleUser) {
-            <div class="health-tip-card">
-              <h3>经期建议</h3>
-              <p>{{ getHealthAdvice('menstrualCycle') }}</p>
-            </div>
-          }
+    <!-- 经期监测 -->
+    @if (isFemaleUser) {
+      <section class="menstrual-section">
+        <div class="section-header">
+          <span class="section-title">经期监测</span>
         </div>
-        <button class="report-btn" (click)="openDetailModal('comprehensive')">查看完整健康报告</button>
-      </section>
-
-      <!-- 经期监测区域 (仅女性用户显示) -->
-      @if (isFemaleUser) {
-        <section class="menstrual-cycle-section">
-          <h2>经期监测与健康管理</h2>
-          
-          <!-- 当前周期状态卡片 -->
-          <div class="menstrual-status-card">
-            <div class="status-overview">
-              <div class="status-item">
-                <span class="status-label">当前阶段</span>
-                <span class="status-value phase-{{ getCurrentPhase().toLowerCase() }}">{{ getCurrentPhase() }}</span>
-              </div>
-              <div class="status-item">
-                <span class="status-label">距离下次月经</span>
-                <span class="status-value">{{ getDaysUntilNextPeriod() }} 天</span>
+        <div class="menstrual-card">
+          <div class="menstrual-card-content" (click)="openDetailModal('menstrualCycle')">
+            <div class="menstrual-header">
+              <span class="menstrual-title">生理周期</span>
+              <span class="phase-badge">{{ getCurrentPhase() }}</span>
+            </div>
+            <div class="menstrual-info">
+              <div class="menstrual-stat">
+                <div class="menstrual-stat-value">{{ getDaysUntilNextPeriod() }}</div>
+                <div class="menstrual-stat-label">距下次月经(天)</div>
               </div>
-              <div class="status-item">
-                <span class="status-label">周期长度</span>
-                <span class="status-value">{{ menstrualCycle.length }} 天</span>
+              <div class="menstrual-stat">
+                <div class="menstrual-stat-value">{{ menstrualCycle.length }}</div>
+                <div class="menstrual-stat-label">周期长度(天)</div>
               </div>
             </div>
-          </div>
-
-          <!-- 周期可视化图表 -->
-          <div class="menstrual-cycle-chart">
-            <h3>生理周期阶段图</h3>
-            <div class="cycle-timeline">
-              @for (phase of cyclePhases; track phase.name) {
-                <div class="phase-block" 
-                     [style.width.%]="phase.duration / menstrualCycle.length * 100"
-                     [class.active]="phase.name === getCurrentPhase()">
-                  <div class="phase-header">
-                    <span class="phase-icon">{{ phase.icon }}</span>
-                    <span class="phase-name">{{ phase.name }}</span>
-                  </div>
-                  <div class="phase-info">
-                    <span class="phase-days">{{ phase.startDay }}-{{ phase.endDay }} 天</span>
-                    <span class="phase-dates">{{ formatDate(phase.startDate) }} - {{ formatDate(phase.endDate) }}</span>
-                  </div>
-                </div>
+            <div class="phase-bar">
+              @for (phase of cyclePhases; track phase.name; let i = $index) {
+                <div class="phase-segment" [class.active]="phase.name === getCurrentPhase()"></div>
               }
             </div>
           </div>
-
-          <!-- 各阶段详细建议 -->
-          <div class="phase-advice-grid">
-            @for (phase of cyclePhases; track phase.name) {
-              <div class="phase-advice-card" [class.current]="phase.name === getCurrentPhase()">
-                <div class="phase-advice-header">
-                  <span class="phase-icon-large">{{ phase.icon }}</span>
-                  <div>
-                    <h4>{{ phase.name }}</h4>
-                    <p class="phase-duration">第 {{ phase.startDay }}-{{ phase.endDay }} 天</p>
-                  </div>
-                  @if (phase.name === getCurrentPhase()) {
-                    <span class="current-badge">当前阶段</span>
-                  }
-                </div>
-                
-                <div class="phase-advice-content">
-                  <div class="advice-section">
-                    <h5>🏃‍♀️ 运动建议</h5>
-                    <ul>
-                      @for (tip of phase.exerciseAdvice; track tip) {
-                        <li>{{ tip }}</li>
-                      }
-                    </ul>
-                  </div>
-                  
-                  <div class="advice-section">
-                    <h5>🥗 健康建议</h5>
-                    <ul>
-                      @for (tip of phase.healthAdvice; track tip) {
-                        <li>{{ tip }}</li>
-                      }
-                    </ul>
-                  </div>
-                  
-                  @if (phase.symptoms && phase.symptoms.length > 0) {
-                    <div class="advice-section symptoms">
-                      <h5>⚠️ 常见症状</h5>
-                      <div class="symptom-tags">
-                        @for (symptom of phase.symptoms; track symptom) {
-                          <span class="symptom-tag">{{ symptom }}</span>
-                        }
-                      </div>
-                    </div>
-                  }
-                </div>
-              </div>
-            }
-          </div>
-
-          <!-- 经期记录按钮 -->
-          <div class="menstrual-actions">
-            <button class="action-btn primary" (click)="openDetailModal('menstrualCycle')">
-              <span>📝 记录经期数据</span>
-            </button>
-            <button class="action-btn secondary" (click)="openDetailModal('menstrualCycle')">
-              <span>📊 查看历史记录</span>
-            </button>
-          </div>
-        </section>
-      }
-    </div>
+          <button class="menstrual-btn" (click)="openMenstrualRecordModal($event)">📝 记录经期</button>
+        </div>
+      </section>
+    }
   </main>
 </div>
 
+<!-- 指标选择模态框 -->
+<app-modal 
+  [(isOpen)]="showMetricSelectionModal"
+  [config]="{
+    title: '选择记录指标',
+    icon: '📝',
+    size: 'small',
+    theme: 'system'
+  }">
+  <div class="metric-selection-content">
+    <div class="metric-selection-grid">
+      <button class="metric-selection-btn" (click)="selectMetricAndRecord('heartRate')">
+        <div class="metric-selection-icon">💗</div>
+        <div class="metric-selection-info">
+          <div class="metric-selection-label">心率</div>
+          <div class="metric-selection-desc">记录心跳频率</div>
+        </div>
+      </button>
+      <button class="metric-selection-btn" (click)="selectMetricAndRecord('bloodPressure')">
+        <div class="metric-selection-icon">🩺</div>
+        <div class="metric-selection-info">
+          <div class="metric-selection-label">血压</div>
+          <div class="metric-selection-desc">记录收缩压/舒张压</div>
+        </div>
+      </button>
+      <button class="metric-selection-btn" (click)="selectMetricAndRecord('bloodOxygen')">
+        <div class="metric-selection-icon">🫁</div>
+        <div class="metric-selection-info">
+          <div class="metric-selection-label">血氧</div>
+          <div class="metric-selection-desc">记录血氧饱和度</div>
+        </div>
+      </button>
+    </div>
+  </div>
+</app-modal>
+
 <!-- 数据记录模态框 -->
 <app-modal 
   [(isOpen)]="showRecordModal"
@@ -350,57 +187,33 @@
     showFooter: false
   }">
   <div class="record-modal-content">
-      <form (ngSubmit)="submitRecord()">
-        <div class="form-group">
-          <label for="recordValue">{{ recordForm.type === 'heartRate' ? '心率值' : recordForm.type === 'bloodPressure' ? '收缩压' : '血氧饱和度' }}:</label>
-          <input 
-            type="number" 
-            id="recordValue" 
-            [(ngModel)]="recordForm.value" 
-            name="recordValue"
-            placeholder="请输入数值"
-            min="0"
-            required
-          >
+    <form (ngSubmit)="submitRecord()">
+      <div class="form-group">
+        <label for="recordValue">{{ recordForm.type === 'heartRate' ? '心率值' : recordForm.type === 'bloodPressure' ? '收缩压' : '血氧饱和度' }}:</label>
+        <div class="input-with-unit">
+          <input type="number" id="recordValue" [(ngModel)]="recordForm.value" name="recordValue" placeholder="请输入数值" min="0" required>
           <span class="unit">{{ recordForm.type === 'heartRate' ? 'bpm' : recordForm.type === 'bloodPressure' ? 'mmHg' : '%' }}</span>
         </div>
-        
-        <div class="form-group" *ngIf="recordForm.type === 'bloodPressure'">
-          <label for="diastolicValue">舒张压:</label>
-          <input 
-            type="number" 
-            id="diastolicValue" 
-            [(ngModel)]="recordForm.diastolic" 
-            name="diastolicValue"
-            placeholder="请输入舒张压"
-            min="0"
-            required
-          >
+      </div>
+      
+      <div class="form-group" *ngIf="recordForm.type === 'bloodPressure'">
+        <label for="diastolicValue">舒张压:</label>
+        <div class="input-with-unit">
+          <input type="number" id="diastolicValue" [(ngModel)]="recordForm.diastolic" name="diastolicValue" placeholder="请输入舒张压" min="0" required>
           <span class="unit">mmHg</span>
         </div>
-        
-        <div class="form-group datetime-wrapper">
-          <label for="recordDate">记录时间:</label>
-          <input 
-            type="datetime-local" 
-            id="recordDate" 
-            [(ngModel)]="recordFormDateTimeString" 
-            (ngModelChange)="onRecordDateChange($event)"
-            name="recordDate"
-            required
-            class="datetime-picker-input"
-          >
-          <div class="datetime-preview" *ngIf="recordForm.date">
-            <span class="preview-label">已选择:</span>
-            <span class="preview-value">{{ getFormattedRecordDate() }}</span>
-          </div>
-        </div>
-        
-        <div class="form-actions">
-          <button type="button" class="cancel-btn" (click)="closeRecordModal()">取消</button>
-          <button type="submit" class="submit-btn">保存</button>
-        </div>
-      </form>
+      </div>
+      
+      <div class="form-group">
+        <label for="recordDate">记录时间:</label>
+        <input type="datetime-local" id="recordDate" [(ngModel)]="recordFormDateTimeString" (ngModelChange)="onRecordDateChange($event)" name="recordDate" required>
+      </div>
+      
+      <div class="form-actions">
+        <button type="button" class="cancel-btn" (click)="closeRecordModal()">取消</button>
+        <button type="submit" class="submit-btn">保存</button>
+      </div>
+    </form>
   </div>
 </app-modal>
 
@@ -411,208 +224,165 @@
     title: selectedMetric === 'heartRate' ? '心率详情' : 
            selectedMetric === 'bloodPressure' ? '血压详情' : 
            selectedMetric === 'bloodOxygen' ? '血氧详情' : 
-           selectedMetric === 'menstrualCycle' ? '经期详情' : '健康综合报告',
-    icon: '📊',
+           selectedMetric === 'menstrualCycle' ? '经期详情' : '',
+    icon: selectedMetric === 'comprehensive' ? '' : '📊',
     size: selectedMetric === 'comprehensive' ? 'large' : 'medium',
-    theme: 'gradient'
+    theme: 'system',
+    showHeader: selectedMetric !== 'comprehensive'
   }">
   <div class="detail-modal-content">
-      @if (selectedMetric === 'comprehensive') {
-        <div class="report-content-new">
-          <div class="report-header">
+    @if (selectedMetric === 'comprehensive') {
+      <div class="report-content">
+        <div class="report-header">
+          <div class="report-header-content">
             <h2>📋 健康状况报告</h2>
             <p class="report-date">{{ today | date: 'yyyy年MM月dd日' }}</p>
           </div>
-          
-          <div class="health-metrics-grid">
-            <!-- 心率卡片 -->
-            <div class="metric-report-card">
-              <div class="metric-card-header">
-                <span class="metric-icon-large">💗</span>
-                <h3>心率状况</h3>
-              </div>
-              <div class="metric-card-body">
-                <div class="metric-main-value">
-                  <span class="value-number">{{ heartRate.value }}</span>
-                  <span class="value-unit">{{ heartRate.unit }}</span>
-                  <span class="status-badge badge-{{ heartRate.status }}">{{ getStatusText(heartRate.status) }}</span>
-                </div>
-                <div class="metric-trend">
-                  <span class="trend-label">趋势:</span>
-                  <span class="trend-value trend-{{ heartRate.trend }}">{{ getTrendIcon(heartRate.trend) }} {{ getTrendText(heartRate.trend) }}</span>
-                </div>
-              </div>
-            </div>
-            
-            <!-- 血压卡片 -->
-            <div class="metric-report-card">
-              <div class="metric-card-header">
-                <span class="metric-icon-large">🩺</span>
-                <h3>血压状况</h3>
-              </div>
-              <div class="metric-card-body">
-                <div class="metric-main-value">
-                  <span class="value-number">{{ bloodPressure.systolic.value }}/{{ bloodPressure.diastolic.value }}</span>
-                  <span class="value-unit">{{ bloodPressure.systolic.unit }}</span>
-                  <span class="status-badge badge-{{ bloodPressure.systolic.status }}">{{ getStatusText(bloodPressure.systolic.status) }}</span>
-                </div>
-                <div class="metric-trend">
-                  <span class="trend-label">趋势:</span>
-                  <span class="trend-value trend-{{ bloodPressure.systolic.trend }}">{{ getTrendIcon(bloodPressure.systolic.trend) }} {{ getTrendText(bloodPressure.systolic.trend) }}</span>
-                </div>
-              </div>
-            </div>
-            
-            <!-- 血氧卡片 -->
-            <div class="metric-report-card">
-              <div class="metric-card-header">
-                <span class="metric-icon-large">🫁</span>
-                <h3>血氧状况</h3>
-              </div>
-              <div class="metric-card-body">
-                <div class="metric-main-value">
-                  <span class="value-number">{{ bloodOxygen.value }}</span>
-                  <span class="value-unit">{{ bloodOxygen.unit }}</span>
-                  <span class="status-badge badge-{{ bloodOxygen.status }}">{{ getStatusText(bloodOxygen.status) }}</span>
-                </div>
-                <div class="metric-trend">
-                  <span class="trend-label">趋势:</span>
-                  <span class="trend-value trend-{{ bloodOxygen.trend }}">{{ getTrendIcon(bloodOxygen.trend) }} {{ getTrendText(bloodOxygen.trend) }}</span>
-                </div>
-              </div>
+          <button class="report-close-btn" (click)="closeDetailModal()">×</button>
+        </div>
+        
+        <div class="health-metrics-list">
+          <div class="metric-report-item">
+            <div class="metric-report-icon">💗</div>
+            <div class="metric-report-info">
+              <div class="metric-report-label">心率</div>
+              <div class="metric-report-value">{{ heartRate.value }} {{ heartRate.unit }}</div>
             </div>
+            <span class="metric-report-status" [class.normal]="heartRate.status === 'normal'" [class.warning]="heartRate.status === 'warning'" [class.danger]="heartRate.status === 'danger'">{{ getStatusText(heartRate.status) }}</span>
           </div>
-          
-          <!-- 综合建议 -->
-          <div class="advice-section-new">
-            <div class="advice-header">
-              <span class="advice-icon">💡</span>
-              <h3>综合建议</h3>
-            </div>
-            <div class="advice-list">
-              <div class="advice-item">
-                <span class="advice-item-icon">🏃</span>
-                <span class="advice-text">保持每日30分钟中等强度有氧运动</span>
-              </div>
-              <div class="advice-item">
-                <span class="advice-item-icon">🥗</span>
-                <span class="advice-text">饮食清淡,减少盐分和脂肪摄入</span>
-              </div>
-              <div class="advice-item">
-                <span class="advice-item-icon">😴</span>
-                <span class="advice-text">保证充足睡眠,避免熬夜</span>
-              </div>
-              <div class="advice-item">
-                <span class="advice-item-icon">😊</span>
-                <span class="advice-text">保持良好心态,避免长期精神压力</span>
-              </div>
-              @if (bloodPressure.systolic.status !== 'normal' || bloodPressure.diastolic.status !== 'normal') {
-                <div class="advice-item warning">
-                  <span class="advice-item-icon">⚠️</span>
-                  <span class="advice-text">建议每日测量血压并记录,如有异常及时就医</span>
-                </div>
-              }
+          <div class="metric-report-item">
+            <div class="metric-report-icon">🩺</div>
+            <div class="metric-report-info">
+              <div class="metric-report-label">血压</div>
+              <div class="metric-report-value">{{ bloodPressure.systolic.value }}/{{ bloodPressure.diastolic.value }} mmHg</div>
             </div>
+            <span class="metric-report-status" [class.normal]="bloodPressure.systolic.status === 'normal'" [class.warning]="bloodPressure.systolic.status === 'warning'" [class.danger]="bloodPressure.systolic.status === 'danger'">{{ getStatusText(bloodPressure.systolic.status) }}</span>
           </div>
-        </div>
-      } @else if (selectedMetric === 'heartRate') {
-        <div class="metric-details">
-          <div class="detail-item">
-            <span class="label">当前心率:</span>
-            <span class="value">{{ heartRate.value }} {{ heartRate.unit }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">状态:</span>
-            <span class="value" [ngClass]="heartRate.status">{{ getStatusText(heartRate.status) }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">记录时间:</span>
-            <span class="value">{{ heartRate.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">趋势:</span>
-            <span class="value trend" [ngClass]="heartRate.trend">
-              {{ getTrendText(heartRate.trend) }}
-            </span>
-          </div>
-          <div class="advice-section">
-            <h4>健康建议</h4>
-            <p>{{ getHealthAdvice('heartRate') }}</p>
-          </div>
-        </div>
-      } @else if (selectedMetric === 'bloodPressure') {
-        <div class="metric-details">
-          <div class="detail-item">
-            <span class="label">收缩压:</span>
-            <span class="value">{{ bloodPressure.systolic.value }} {{ bloodPressure.systolic.unit }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">舒张压:</span>
-            <span class="value">{{ bloodPressure.diastolic.value }} {{ bloodPressure.diastolic.unit }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">状态:</span>
-            <span class="value" [ngClass]="bloodPressure.systolic.status">{{ getStatusText(bloodPressure.systolic.status) }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">记录时间:</span>
-            <span class="value">{{ bloodPressure.systolic.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
-          </div>
-          <div class="advice-section">
-            <h4>健康建议</h4>
-            <p>{{ getHealthAdvice('bloodPressure') }}</p>
+          <div class="metric-report-item">
+            <div class="metric-report-icon">🫁</div>
+            <div class="metric-report-info">
+              <div class="metric-report-label">血氧</div>
+              <div class="metric-report-value">{{ bloodOxygen.value }}%</div>
+            </div>
+            <span class="metric-report-status" [class.normal]="bloodOxygen.status === 'normal'" [class.warning]="bloodOxygen.status === 'warning'" [class.danger]="bloodOxygen.status === 'danger'">{{ getStatusText(bloodOxygen.status) }}</span>
           </div>
         </div>
-      } @else if (selectedMetric === 'bloodOxygen') {
-        <div class="metric-details">
-          <div class="detail-item">
-            <span class="label">血氧饱和度:</span>
-            <span class="value">{{ bloodOxygen.value }} {{ bloodOxygen.unit }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">状态:</span>
-            <span class="value" [ngClass]="bloodOxygen.status">{{ getStatusText(bloodOxygen.status) }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">记录时间:</span>
-            <span class="value">{{ bloodOxygen.timestamp | date: 'yyyy-MM-dd HH:mm:ss' }}</span>
-          </div>
-          <div class="advice-section">
-            <h4>健康建议</h4>
-            <p>{{ getHealthAdvice('bloodOxygen') }}</p>
+        
+        <div class="advice-section">
+          <h3>💡 综合建议</h3>
+          <div class="advice-list">
+            <div class="advice-item"><span class="advice-icon">🏃</span><span>保持每日30分钟中等强度有氧运动</span></div>
+            <div class="advice-item"><span class="advice-icon">🥗</span><span>饮食清淡,减少盐分和脂肪摄入</span></div>
+            <div class="advice-item"><span class="advice-icon">😴</span><span>保证充足睡眠,避免熬夜</span></div>
+            <div class="advice-item"><span class="advice-icon">😊</span><span>保持良好心态,避免长期精神压力</span></div>
           </div>
         </div>
-      } @else if (selectedMetric === 'menstrualCycle') {
-        <div class="metric-details">
-          <div class="detail-item">
-            <span class="label">上次经期开始:</span>
-            <span class="value">{{ menstrualCycle.startDate | date: 'yyyy-MM-dd' }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">上次经期结束:</span>
-            <span class="value">{{ menstrualCycle.endDate | date: 'yyyy-MM-dd' }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">下次经期预计:</span>
-            <span class="value">{{ menstrualCycle.predictedNextStartDate | date: 'yyyy-MM-dd' }}</span>
-          </div>
-          <div class="detail-item">
-            <span class="label">月经周期:</span>
-            <span class="value">{{ menstrualCycle.length }}天</span>
-          </div>
-          <div class="symptoms-section">
+      </div>
+    } @else if (selectedMetric === 'heartRate') {
+      <div class="metric-details">
+        <div class="detail-item"><span class="label">当前心率:</span><span class="value">{{ heartRate.value }} {{ heartRate.unit }}</span></div>
+        <div class="detail-item"><span class="label">状态:</span><span class="value" [class.normal]="heartRate.status === 'normal'" [class.warning]="heartRate.status === 'warning'" [class.danger]="heartRate.status === 'danger'">{{ getStatusText(heartRate.status) }}</span></div>
+        <div class="detail-item"><span class="label">记录时间:</span><span class="value">{{ heartRate.timestamp | date: 'yyyy-MM-dd HH:mm' }}</span></div>
+        <div class="detail-item"><span class="label">趋势:</span><span class="value">{{ getTrendIcon(heartRate.trend) }} {{ getTrendText(heartRate.trend) }}</span></div>
+        <div class="advice-box"><h4>健康建议</h4><p>{{ getHealthAdvice('heartRate') }}</p></div>
+      </div>
+    } @else if (selectedMetric === 'bloodPressure') {
+      <div class="metric-details">
+        <div class="detail-item"><span class="label">收缩压:</span><span class="value">{{ bloodPressure.systolic.value }} mmHg</span></div>
+        <div class="detail-item"><span class="label">舒张压:</span><span class="value">{{ bloodPressure.diastolic.value }} mmHg</span></div>
+        <div class="detail-item"><span class="label">状态:</span><span class="value" [class.normal]="bloodPressure.systolic.status === 'normal'" [class.warning]="bloodPressure.systolic.status === 'warning'" [class.danger]="bloodPressure.systolic.status === 'danger'">{{ getStatusText(bloodPressure.systolic.status) }}</span></div>
+        <div class="detail-item"><span class="label">记录时间:</span><span class="value">{{ bloodPressure.systolic.timestamp | date: 'yyyy-MM-dd HH:mm' }}</span></div>
+        <div class="advice-box"><h4>健康建议</h4><p>{{ getHealthAdvice('bloodPressure') }}</p></div>
+      </div>
+    } @else if (selectedMetric === 'bloodOxygen') {
+      <div class="metric-details">
+        <div class="detail-item"><span class="label">血氧饱和度:</span><span class="value">{{ bloodOxygen.value }}%</span></div>
+        <div class="detail-item"><span class="label">状态:</span><span class="value" [class.normal]="bloodOxygen.status === 'normal'" [class.warning]="bloodOxygen.status === 'warning'" [class.danger]="bloodOxygen.status === 'danger'">{{ getStatusText(bloodOxygen.status) }}</span></div>
+        <div class="detail-item"><span class="label">记录时间:</span><span class="value">{{ bloodOxygen.timestamp | date: 'yyyy-MM-dd HH:mm' }}</span></div>
+        <div class="advice-box"><h4>健康建议</h4><p>{{ getHealthAdvice('bloodOxygen') }}</p></div>
+      </div>
+    } @else if (selectedMetric === 'menstrualCycle') {
+      <div class="metric-details">
+        <div class="detail-item"><span class="label">上次经期开始:</span><span class="value">{{ menstrualCycle.startDate | date: 'yyyy-MM-dd' }}</span></div>
+        <div class="detail-item"><span class="label">上次经期结束:</span><span class="value">{{ menstrualCycle.endDate | date: 'yyyy-MM-dd' }}</span></div>
+        <div class="detail-item"><span class="label">下次经期预计:</span><span class="value">{{ menstrualCycle.predictedNextStartDate | date: 'yyyy-MM-dd' }}</span></div>
+        <div class="detail-item"><span class="label">月经周期:</span><span class="value">{{ menstrualCycle.length }}天</span></div>
+        @if (menstrualCycle.symptoms && menstrualCycle.symptoms.length > 0) {
+          <div class="symptoms-box">
             <h4>记录的症状</h4>
-            <ul>
+            <div class="symptom-tags">
               @for (symptom of menstrualCycle.symptoms; track symptom) {
-            <li>{{ symptom }}</li>
-          }
-            </ul>
-          </div>
-          <div class="advice-section">
-            <h4>健康建议</h4>
-            <p>{{ getHealthAdvice('menstrualCycle') }}</p>
+                <span class="symptom-tag">{{ symptom }}</span>
+              }
+            </div>
           </div>
+        }
+        <div class="advice-box"><h4>健康建议</h4><p>{{ getHealthAdvice('menstrualCycle') }}</p></div>
+      </div>
+    }
+  </div>
+</app-modal>
+
+<!-- 经期记录模态框 -->
+<app-modal 
+  [(isOpen)]="showMenstrualRecordModal"
+  [config]="{
+    title: '记录经期',
+    icon: '📝',
+    size: 'medium',
+    theme: 'system'
+  }">
+  <div class="menstrual-record-content">
+    <form (ngSubmit)="submitMenstrualRecord()">
+      <div class="form-group">
+        <label for="startDate">开始日期:</label>
+        <input type="date" id="startDate" [(ngModel)]="menstrualRecordForm.startDate" name="startDate" required>
+      </div>
+      
+      <div class="form-group">
+        <label for="endDate">结束日期 (可选):</label>
+        <input type="date" id="endDate" [(ngModel)]="menstrualRecordForm.endDate" name="endDate">
+      </div>
+      
+      <div class="form-group">
+        <label>经量:</label>
+        <div class="flow-options">
+          <label class="flow-option">
+            <input type="radio" [(ngModel)]="menstrualRecordForm.flow" name="flow" value="light">
+            <span>偏少</span>
+          </label>
+          <label class="flow-option">
+            <input type="radio" [(ngModel)]="menstrualRecordForm.flow" name="flow" value="normal">
+            <span>正常</span>
+          </label>
+          <label class="flow-option">
+            <input type="radio" [(ngModel)]="menstrualRecordForm.flow" name="flow" value="heavy">
+            <span>偏多</span>
+          </label>
         </div>
-      }
+      </div>
+      
+      <div class="form-group">
+        <label>症状 (可多选):</label>
+        <div class="symptoms-grid">
+          @for (symptom of availableSymptoms; track symptom) {
+            <label class="symptom-option" [class.selected]="menstrualRecordForm.symptoms.includes(symptom)">
+              <input type="checkbox" [checked]="menstrualRecordForm.symptoms.includes(symptom)" (change)="toggleSymptom(symptom)">
+              <span>{{ symptom }}</span>
+            </label>
+          }
+        </div>
+      </div>
+      
+      <div class="form-group">
+        <label for="notes">备注 (可选):</label>
+        <textarea id="notes" [(ngModel)]="menstrualRecordForm.notes" name="notes" placeholder="记录其他症状或注意事项" rows="3"></textarea>
+      </div>
+      
+      <div class="form-actions">
+        <button type="button" class="cancel-btn" (click)="closeMenstrualRecordModal()">取消</button>
+        <button type="submit" class="submit-btn">保存记录</button>
+      </div>
+    </form>
   </div>
-</app-modal>
+</app-modal>

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 171
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.scss


+ 266 - 393
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts

@@ -144,11 +144,7 @@ export class MonitoringComponent implements OnInit, OnDestroy {
   // 经期阶段数据
   cyclePhases: MenstrualPhase[] = [];
 
-  // ECharts 图表实例
-  heartRateChart: any = null;
-  bloodPressureChart: any = null;
-  bloodOxygenChart: any = null;
-  correlationChart: any = null;
+  // ECharts 图表实例 (旧的已移除,使用 trendChart)
 
   // 历史数据
   heartRateHistory: HealthMetricHistory[] = [];
@@ -171,20 +167,47 @@ export class MonitoringComponent implements OnInit, OnDestroy {
   // 模态框控制
   showRecordModal: boolean = false;
   showDetailModal: boolean = false;
+  showMetricSelectionModal: boolean = false;
+  showMenstrualRecordModal: boolean = false;
   selectedMetric: string = 'heartRate';
   
   // 日期时间字符串(用于datetime-local输入框)
   recordFormDateTimeString: string = '';
+  
+  // 经期记录表单
+  menstrualRecordForm = {
+    startDate: '',
+    endDate: '',
+    symptoms: [] as string[],
+    flow: 'normal' as 'light' | 'normal' | 'heavy',
+    notes: ''
+  };
+  
+  // 可选症状列表
+  availableSymptoms = [
+    '腹痛', '腰酸', '疲劳', '情绪波动', '乳房胀痛', 
+    '头痛', '恶心', '食欲变化', '失眠', '皮肤变化'
+  ];
+  
+  // 健康评分
+  healthScore: number = 85;
+  
+  // 当前趋势图标签
+  activeTrendTab: 'heartRate' | 'bloodPressure' | 'bloodOxygen' = 'heartRate';
+  
+  // 合并后的趋势图表
+  trendChart: any = null;
 
   constructor(private router: Router) {}
 
   ngOnInit(): void {
     this.generateMockHistoryData();
     this.initializeCyclePhases();
-    // 延迟初始化图表,确保DOM已加载
+    this.calculateHealthScore();
+    // 延迟初始化图表,确保DOM和ECharts CDN已加载
     setTimeout(() => {
-      this.initializeCharts();
-    }, 100);
+      this.initializeTrendChart();
+    }, 500);
     
     // 添加窗口调整大小事件监听器,使图表自适应
     window.addEventListener('resize', this.handleResize.bind(this));
@@ -193,18 +216,167 @@ export class MonitoringComponent implements OnInit, OnDestroy {
   ngOnDestroy(): void {
     // 移除事件监听器并销毁图表
     window.removeEventListener('resize', this.handleResize.bind(this));
-    if (this.heartRateChart) this.heartRateChart.dispose();
-    if (this.bloodPressureChart) this.bloodPressureChart.dispose();
-    if (this.bloodOxygenChart) this.bloodOxygenChart.dispose();
-    if (this.correlationChart) this.correlationChart.dispose();
+    if (this.trendChart) this.trendChart.dispose();
   }
   
   // 处理窗口调整大小
   handleResize(): void {
-    if (this.heartRateChart) this.heartRateChart.resize();
-    if (this.bloodPressureChart) this.bloodPressureChart.resize();
-    if (this.bloodOxygenChart) this.bloodOxygenChart.resize();
-    if (this.correlationChart) this.correlationChart.resize();
+    if (this.trendChart) this.trendChart.resize();
+  }
+  
+  // 计算健康评分
+  calculateHealthScore(): void {
+    let score = 100;
+    // 根据各项指标状态扣分
+    if (this.heartRate.status === 'warning') score -= 10;
+    if (this.heartRate.status === 'danger') score -= 20;
+    if (this.bloodPressure.systolic.status === 'warning') score -= 10;
+    if (this.bloodPressure.systolic.status === 'danger') score -= 20;
+    if (this.bloodOxygen.status === 'warning') score -= 10;
+    if (this.bloodOxygen.status === 'danger') score -= 20;
+    this.healthScore = Math.max(0, score);
+  }
+  
+  // 获取健康评分环形进度条偏移量
+  getScoreOffset(): number {
+    // 251.2 是圆周长 (2 * PI * 40)
+    return 251.2 * (1 - this.healthScore / 100);
+  }
+  
+  // 获取健康摘要
+  getHealthSummary(): string {
+    const statuses = [this.heartRate.status, this.bloodPressure.systolic.status, this.bloodOxygen.status];
+    const allNormal = statuses.every(s => s === 'normal');
+    const hasWarning = statuses.some(s => s === 'warning');
+    const hasDanger = statuses.some(s => s === 'danger');
+    
+    if (allNormal) return '心率、血压、血氧指标均在正常范围内,继续保持!';
+    if (hasDanger) return '部分指标异常,建议及时就医检查。';
+    if (hasWarning) return '部分指标需要关注,请注意调整生活方式。';
+    return '请定期监测健康指标。';
+  }
+  
+  // 设置当前趋势图标签
+  setActiveTrendTab(tab: 'heartRate' | 'bloodPressure' | 'bloodOxygen'): void {
+    this.activeTrendTab = tab;
+    this.initializeTrendChart();
+  }
+  
+  // 初始化趋势图表
+  initializeTrendChart(): void {
+    const chartDom = document.getElementById('trendChart');
+    if (!chartDom) {
+      console.warn('Chart DOM element not found');
+      return;
+    }
+
+    if (this.trendChart) {
+      this.trendChart.dispose();
+    }
+
+    // 检查ECharts是否已加载,如果没有则等待
+    if (typeof echarts === 'undefined') {
+      console.warn('ECharts is not loaded yet, retrying...');
+      setTimeout(() => {
+        this.initializeTrendChart();
+      }, 200);
+      return;
+    }
+
+    try {
+      this.trendChart = echarts.init(chartDom);
+    } catch (error) {
+      console.error('Failed to initialize ECharts:', error);
+      return;
+    }
+    const filteredData = this.getFilteredHistoryData();
+    
+    let data: HealthMetricHistory[];
+    let color: string;
+    let name: string;
+    let unit: string;
+    let minVal: number;
+    let maxVal: number;
+    
+    switch (this.activeTrendTab) {
+      case 'heartRate':
+        data = filteredData.heartRate;
+        color = '#ef4444';
+        name = '心率';
+        unit = 'bpm';
+        minVal = data.length > 0 ? Math.max(50, Math.min(...data.map(d => d.value)) - 10) : 50;
+        maxVal = data.length > 0 ? Math.min(120, Math.max(...data.map(d => d.value)) + 10) : 120;
+        break;
+      case 'bloodPressure':
+        data = filteredData.bloodPressure.systolic;
+        color = '#3b82f6';
+        name = '收缩压';
+        unit = 'mmHg';
+        minVal = data.length > 0 ? Math.max(80, Math.min(...data.map(d => d.value)) - 10) : 80;
+        maxVal = data.length > 0 ? Math.min(180, Math.max(...data.map(d => d.value)) + 10) : 180;
+        break;
+      case 'bloodOxygen':
+        data = filteredData.bloodOxygen;
+        color = '#8b5cf6';
+        name = '血氧';
+        unit = '%';
+        minVal = 90;
+        maxVal = 100;
+        break;
+      default:
+        data = filteredData.heartRate;
+        color = '#ef4444';
+        name = '心率';
+        unit = 'bpm';
+        minVal = 50;
+        maxVal = 120;
+    }
+
+    const option = {
+      tooltip: {
+        trigger: 'axis',
+        formatter: (params: any) => `${params[0].axisValue}<br/>${name}: ${params[0].value} ${unit}`
+      },
+      grid: {
+        left: '12%',
+        right: '4%',
+        bottom: '15%',
+        top: '10%'
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: data.map(item => item.date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })),
+        axisLabel: { rotate: 45, fontSize: 10 }
+      },
+      yAxis: {
+        type: 'value',
+        name: unit,
+        nameTextStyle: { fontSize: 11 },
+        min: minVal,
+        max: maxVal
+      },
+      series: [{
+        name: name,
+        type: 'line',
+        smooth: true,
+        data: data.map(item => item.value),
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0, y: 0, x2: 0, y2: 1,
+            colorStops: [
+              { offset: 0, color: color + '4D' }, // 30% opacity
+              { offset: 1, color: color + '0D' }  // 5% opacity
+            ]
+          }
+        },
+        lineStyle: { color: color, width: 2 },
+        itemStyle: { color: color }
+      }]
+    };
+
+    this.trendChart.setOption(option);
   }
 
   // 生成模拟历史数据
@@ -244,23 +416,7 @@ export class MonitoringComponent implements OnInit, OnDestroy {
     }
   }
 
-  // 初始化图表
-  initializeCharts(): void {
-    // 根据选择的时间范围获取数据
-    const filteredData = this.getFilteredHistoryData();
-    
-    // 心率图表
-    this.initializeHeartRateChart(filteredData.heartRate);
-    
-    // 血压图表
-    this.initializeBloodPressureChart(filteredData.bloodPressure);
-    
-    // 血氧图表
-    this.initializeBloodOxygenChart(filteredData.bloodOxygen);
-    
-    // 心率-血压关联图表
-    this.initializeCorrelationChart(filteredData.heartRate, filteredData.bloodPressure.systolic);
-  }
+
 
   // 获取过滤后的历史数据
   getFilteredHistoryData() {
@@ -291,389 +447,106 @@ export class MonitoringComponent implements OnInit, OnDestroy {
     };
   }
 
-  // 初始化心率图表 (使用 ECharts)
-  initializeHeartRateChart(data: HealthMetricHistory[]): void {
-    const chartDom = document.getElementById('heartRateChart');
-    if (!chartDom) return;
 
-    // 销毁现有图表
-    if (this.heartRateChart) {
-      this.heartRateChart.dispose();
-    }
 
-    // 初始化 ECharts 实例
-    this.heartRateChart = echarts.init(chartDom);
+  // 时间范围改变
+  onTimeRangeChange(): void {
+    this.initializeTrendChart();
+  }
 
-    const option = {
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'cross'
-        }
-      },
-      grid: {
-        left: '8%',
-        right: '4%',
-        bottom: '15%',
-        top: '10%',
-        containLabel: false
-      },
-      xAxis: {
-        type: 'category',
-        boundaryGap: false,
-        data: data.map(item => item.date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })),
-        axisLabel: {
-          rotate: 45,
-          fontSize: 10,
-          margin: 10
-        }
-      },
-      yAxis: {
-        type: 'value',
-        name: 'bpm',
-        nameTextStyle: {
-          fontSize: 12,
-          padding: [0, 0, 0, 10]
-        },
-        min: Math.max(50, Math.min(...data.map(d => d.value)) - 10),
-        max: Math.min(120, Math.max(...data.map(d => d.value)) + 10)
-      },
-      series: [{
-        name: '心率',
-        type: 'line',
-        smooth: true,
-        data: data.map(item => item.value),
-        areaStyle: {
-          color: {
-            type: 'linear',
-            x: 0,
-            y: 0,
-            x2: 0,
-            y2: 1,
-            colorStops: [{
-              offset: 0,
-              color: 'rgba(239, 68, 68, 0.3)'
-            }, {
-              offset: 1,
-              color: 'rgba(239, 68, 68, 0.05)'
-            }]
-          }
-        },
-        lineStyle: {
-          color: '#ef4444',
-          width: 2
-        },
-        itemStyle: {
-          color: '#ef4444'
-        }
-      }]
+  // 打开数据记录模态框
+  openRecordModal(type: 'heartRate' | 'bloodPressure' | 'bloodOxygen'): void {
+    const now = new Date();
+    this.recordForm = {
+      type,
+      value: 0,
+      diastolic: type === 'bloodPressure' ? 0 : undefined,
+      date: now
     };
+    // 初始化datetime-local输入框的值
+    this.recordFormDateTimeString = this.formatDateTimeForInput(now);
+    this.showRecordModal = true;
+  }
 
-    this.heartRateChart.setOption(option);
+  // 关闭数据记录模态框
+  closeRecordModal(): void {
+    this.showRecordModal = false;
   }
 
-  // 初始化血压图表 (使用 ECharts)
-  initializeBloodPressureChart(data: { systolic: HealthMetricHistory[]; diastolic: HealthMetricHistory[] }): void {
-    const chartDom = document.getElementById('bloodPressureChart');
-    if (!chartDom) return;
+  // 打开指标选择模态框
+  openMetricSelectionModal(): void {
+    this.showMetricSelectionModal = true;
+  }
 
-    // 销毁现有图表
-    if (this.bloodPressureChart) {
-      this.bloodPressureChart.dispose();
-    }
+  // 关闭指标选择模态框
+  closeMetricSelectionModal(): void {
+    this.showMetricSelectionModal = false;
+  }
 
-    // 初始化 ECharts 实例
-    this.bloodPressureChart = echarts.init(chartDom);
+  // 选择指标并打开记录模态框
+  selectMetricAndRecord(type: 'heartRate' | 'bloodPressure' | 'bloodOxygen'): void {
+    this.closeMetricSelectionModal();
+    this.openRecordModal(type);
+  }
 
-    const option = {
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'cross'
-        }
-      },
-      legend: {
-        data: ['收缩压', '舒张压'],
-        top: 0,
-        textStyle: {
-          fontSize: 12
-        }
-      },
-      grid: {
-        left: '8%',
-        right: '4%',
-        bottom: '15%',
-        top: '15%',
-        containLabel: false
-      },
-      xAxis: {
-        type: 'category',
-        boundaryGap: false,
-        data: data.systolic.map(item => item.date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })),
-        axisLabel: {
-          rotate: 45,
-          fontSize: 10,
-          margin: 10
-        }
-      },
-      yAxis: {
-        type: 'value',
-        name: 'mmHg',
-        nameTextStyle: {
-          fontSize: 12,
-          padding: [0, 0, 0, 10]
-        }
-      },
-      series: [
-        {
-          name: '收缩压',
-          type: 'line',
-          smooth: true,
-          data: data.systolic.map(item => item.value),
-          lineStyle: {
-            color: '#3b82f6',
-            width: 2
-          },
-          itemStyle: {
-            color: '#3b82f6'
-          },
-          areaStyle: {
-            color: {
-              type: 'linear',
-              x: 0,
-              y: 0,
-              x2: 0,
-              y2: 1,
-              colorStops: [{
-                offset: 0,
-                color: 'rgba(59, 130, 246, 0.2)'
-              }, {
-                offset: 1,
-                color: 'rgba(59, 130, 246, 0.05)'
-              }]
-            }
-          }
-        },
-        {
-          name: '舒张压',
-          type: 'line',
-          smooth: true,
-          data: data.diastolic.map(item => item.value),
-          lineStyle: {
-            color: '#10b981',
-            width: 2
-          },
-          itemStyle: {
-            color: '#10b981'
-          },
-          areaStyle: {
-            color: {
-              type: 'linear',
-              x: 0,
-              y: 0,
-              x2: 0,
-              y2: 1,
-              colorStops: [{
-                offset: 0,
-                color: 'rgba(16, 185, 129, 0.2)'
-              }, {
-                offset: 1,
-                color: 'rgba(16, 185, 129, 0.05)'
-              }]
-            }
-          }
-        }
-      ]
+  // 打开经期记录模态框
+  openMenstrualRecordModal(event: Event): void {
+    event.stopPropagation(); // 阻止事件冒泡,避免触发卡片点击
+    const today = new Date();
+    this.menstrualRecordForm = {
+      startDate: this.formatDateForInput(today),
+      endDate: '',
+      symptoms: [],
+      flow: 'normal',
+      notes: ''
     };
-
-    this.bloodPressureChart.setOption(option);
+    this.showMenstrualRecordModal = true;
   }
 
-  // 初始化血氧图表 (使用 ECharts)
-  initializeBloodOxygenChart(data: HealthMetricHistory[]): void {
-    const chartDom = document.getElementById('bloodOxygenChart');
-    if (!chartDom) return;
+  // 关闭经期记录模态框
+  closeMenstrualRecordModal(): void {
+    this.showMenstrualRecordModal = false;
+  }
 
-    // 销毁现有图表
-    if (this.bloodOxygenChart) {
-      this.bloodOxygenChart.dispose();
+  // 切换症状选择
+  toggleSymptom(symptom: string): void {
+    const index = this.menstrualRecordForm.symptoms.indexOf(symptom);
+    if (index > -1) {
+      this.menstrualRecordForm.symptoms.splice(index, 1);
+    } else {
+      this.menstrualRecordForm.symptoms.push(symptom);
     }
-
-    // 初始化 ECharts 实例
-    this.bloodOxygenChart = echarts.init(chartDom);
-
-    const option = {
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: {
-          type: 'cross'
-        }
-      },
-      grid: {
-        left: '8%',
-        right: '4%',
-        bottom: '15%',
-        top: '10%',
-        containLabel: false
-      },
-      xAxis: {
-        type: 'category',
-        boundaryGap: false,
-        data: data.map(item => item.date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })),
-        axisLabel: {
-          rotate: 45,
-          fontSize: 10,
-          margin: 10
-        }
-      },
-      yAxis: {
-        type: 'value',
-        name: '%',
-        nameTextStyle: {
-          fontSize: 12,
-          padding: [0, 0, 0, 10]
-        },
-        min: 90,
-        max: 100
-      },
-      series: [{
-        name: '血氧饱和度',
-        type: 'line',
-        smooth: true,
-        data: data.map(item => item.value),
-        areaStyle: {
-          color: {
-            type: 'linear',
-            x: 0,
-            y: 0,
-            x2: 0,
-            y2: 1,
-            colorStops: [{
-              offset: 0,
-              color: 'rgba(139, 92, 246, 0.3)'
-            }, {
-              offset: 1,
-              color: 'rgba(139, 92, 246, 0.05)'
-            }]
-          }
-        },
-        lineStyle: {
-          color: '#8b5cf6',
-          width: 2
-        },
-        itemStyle: {
-          color: '#8b5cf6'
-        }
-      }]
-    };
-
-    this.bloodOxygenChart.setOption(option);
   }
 
-  // 初始化心率-血压关联图表 (使用 ECharts)
-  initializeCorrelationChart(heartRateData: HealthMetricHistory[], bloodPressureData: HealthMetricHistory[]): void {
-    const chartDom = document.getElementById('correlationChart');
-    if (!chartDom) return;
-
-    // 销毁现有图表
-    if (this.correlationChart) {
-      this.correlationChart.dispose();
+  // 提交经期记录
+  submitMenstrualRecord(): void {
+    if (!this.menstrualRecordForm.startDate) {
+      alert('请选择开始日期');
+      return;
     }
 
-    // 初始化 ECharts 实例
-    this.correlationChart = echarts.init(chartDom);
-
-    // 确保数据点数量一致
-    const dataLength = Math.min(heartRateData.length, bloodPressureData.length);
-    const scatterData: any[] = [];
+    // 前端模拟保存
+    const startDate = new Date(this.menstrualRecordForm.startDate);
+    const endDate = this.menstrualRecordForm.endDate ? new Date(this.menstrualRecordForm.endDate) : null;
     
-    for (let i = 0; i < dataLength; i++) {
-      scatterData.push({
-        value: [heartRateData[i].value, bloodPressureData[i].value],
-        date: heartRateData[i].date.toLocaleDateString('zh-CN')
-      });
-    }
-
-    const option = {
-      tooltip: {
-        trigger: 'item',
-        formatter: (params: any) => {
-          return `心率: ${params.value[0]} bpm<br/>收缩压: ${params.value[1]} mmHg<br/>日期: ${params.data.date}`;
-        }
-      },
-      grid: {
-        left: '10%',
-        right: '4%',
-        bottom: '15%',
-        top: '5%',
-        containLabel: false
-      },
-      xAxis: {
-        type: 'value',
-        name: '心率 (bpm)',
-        nameLocation: 'center',
-        nameGap: 35,
-        nameTextStyle: {
-          fontSize: 13,
-          fontWeight: 500
-        }
-      },
-      yAxis: {
-        type: 'value',
-        name: '收缩压 (mmHg)',
-        nameLocation: 'center',
-        nameGap: 50,
-        nameTextStyle: {
-          fontSize: 13,
-          fontWeight: 500
-        }
-      },
-      series: [{
-        name: '心率与收缩压',
-        type: 'scatter',
-        symbolSize: 8,
-        data: scatterData,
-        itemStyle: {
-          color: '#ef4444',
-          opacity: 0.7
-        },
-        emphasis: {
-          itemStyle: {
-            color: '#dc2626',
-            borderColor: '#fff',
-            borderWidth: 2,
-            shadowBlur: 10,
-            shadowColor: 'rgba(239, 68, 68, 0.5)'
-          }
-        }
-      }]
+    // 更新经期数据
+    this.menstrualCycle = {
+      ...this.menstrualCycle,
+      startDate: startDate,
+      endDate: endDate || new Date(startDate.getTime() + 5 * 24 * 60 * 60 * 1000), // 默认5天
+      symptoms: [...this.menstrualRecordForm.symptoms]
     };
 
-    this.correlationChart.setOption(option);
-  }
-
-  // 时间范围改变
-  onTimeRangeChange(): void {
-    this.initializeCharts();
-  }
-
-  // 打开数据记录模态框
-  openRecordModal(type: 'heartRate' | 'bloodPressure' | 'bloodOxygen'): void {
-    const now = new Date();
-    this.recordForm = {
-      type,
-      value: 0,
-      diastolic: type === 'bloodPressure' ? 0 : undefined,
-      date: now
-    };
-    // 初始化datetime-local输入框的值
-    this.recordFormDateTimeString = this.formatDateTimeForInput(now);
-    this.showRecordModal = true;
+    alert('经期记录保存成功');
+    this.closeMenstrualRecordModal();
   }
 
-  // 关闭数据记录模态框
-  closeRecordModal(): void {
-    this.showRecordModal = false;
+  // 格式化日期为输入框格式 (yyyy-MM-dd)
+  formatDateForInput(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}-${month}-${day}`;
   }
   
   // 将Date对象转换为datetime-local输入框所需的格式 (yyyy-MM-ddTHH:mm)
@@ -776,7 +649,7 @@ export class MonitoringComponent implements OnInit, OnDestroy {
     }
 
     // 更新图表
-    this.initializeCharts();
+    this.initializeTrendChart();
     
     // 关闭模态框
     this.closeRecordModal();

+ 77 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-history/weight-history.component.html

@@ -0,0 +1,77 @@
+<div class="history-container">
+  <app-page-header [title]="'体重历史'" (back)="backToWeight()"></app-page-header>
+
+  <main class="history-main">
+    <!-- 工具栏 -->
+    <div class="toolbar">
+      <div class="view-toggle">
+        <button class="toggle-btn" [class.active]="viewMode === 'month'" (click)="viewMode = 'month'">月</button>
+        <span class="divider">/</span>
+        <button class="toggle-btn" [class.active]="viewMode === 'year'" (click)="viewMode = 'year'">年报</button>
+      </div>
+      <button class="manage-btn" (click)="toggleManageMode()">{{ isManageMode ? '取消' : '管理' }}</button>
+    </div>
+
+    <!-- 管理操作 -->
+    <div class="manage-actions" *ngIf="isManageMode">
+      <button class="action-btn" (click)="selectAll()">全选</button>
+      <button class="action-btn delete" (click)="batchDelete()">删除({{ selectedRecords.size }})</button>
+    </div>
+
+    <!-- 月统计 -->
+    <div class="month-stats">
+      <div class="stat-card">
+        <span class="stat-value">{{ monthStats.totalDays }}</span>
+        <span class="stat-label">记录天数</span>
+      </div>
+      <div class="stat-card">
+        <span class="stat-value">{{ monthStats.avgWeight > 0 ? monthStats.avgWeight.toFixed(1) : '--' }} <small>kg</small></span>
+        <span class="stat-label">平均体重</span>
+      </div>
+      <div class="stat-card">
+        <span class="stat-value" [class.positive]="monthStats.weightChange < 0" [class.negative]="monthStats.weightChange > 0">
+          {{ monthStats.weightChange > 0 ? '+' : '' }}{{ monthStats.weightChange !== 0 ? monthStats.weightChange.toFixed(1) : '--' }} <small>kg</small>
+        </span>
+        <span class="stat-label">月变化</span>
+      </div>
+    </div>
+
+    <!-- 日历网格 -->
+    <app-calendar-grid
+      [currentDate]="currentDate"
+      [dayDataMap]="calendarDataMap"
+      [selectedDate]="selectedDate"
+      [isManageMode]="isManageMode"
+      [selectedRecords]="selectedRecords"
+      [themeColor]="'purple'"
+      (dateSelected)="onDateSelected($event)"
+      (previousMonth)="previousMonth()"
+      (nextMonth)="nextMonth()"
+      (goToCurrentMonth)="goToCurrentMonth()">
+    </app-calendar-grid>
+
+    <!-- 选中日期详情 -->
+    <div class="day-detail" *ngIf="selectedDayRecord && selectedDayRecord.hasData && !isManageMode">
+      <div class="detail-header">
+        <h3>{{ selectedDate | date:'M月d日' }} 体重详情</h3>
+        <button class="close-btn" (click)="selectedDate = null">×</button>
+      </div>
+      <div class="detail-records">
+        <div class="record-item" *ngFor="let rec of selectedDayRecord.records">
+          <div class="record-info">
+            <span class="record-time">{{ rec.measurementTime }}</span>
+            <span class="record-condition">{{ getConditionLabel(rec.measurementCondition || 'fasting') }}</span>
+          </div>
+          <div class="record-metrics">
+            <span class="metric-weight">{{ rec.weight.toFixed(1) }} kg</span>
+            <span class="metric-bodyfat">体脂 {{ rec.bodyFat.toFixed(1) }}%</span>
+            <span class="metric-muscle" *ngIf="rec.muscleMass">肌肉 {{ rec.muscleMass.toFixed(1) }}kg</span>
+          </div>
+        </div>
+      </div>
+      <div class="detail-summary">
+        平均:{{ selectedDayRecord.avgWeight.toFixed(1) }} kg / 体脂 {{ selectedDayRecord.avgBodyFat.toFixed(1) }}%
+      </div>
+    </div>
+  </main>
+</div>

+ 368 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-history/weight-history.component.scss

@@ -0,0 +1,368 @@
+.history-container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
+}
+
+.history-main {
+  padding: 16px;
+  max-width: 600px;
+  margin: 0 auto;
+}
+
+.month-nav {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+  margin-bottom: 16px;
+  
+  .nav-btn {
+    width: 32px;
+    height: 32px;
+    border: none;
+    background: white;
+    border-radius: 50%;
+    font-size: 18px;
+    cursor: pointer;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+    
+    &:hover {
+      background: #f3f4f6;
+    }
+  }
+  
+  .month-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+  
+  .back-today-btn {
+    padding: 6px 12px;
+    border: 1px solid #d1d5db;
+    background: white;
+    border-radius: 16px;
+    font-size: 12px;
+    color: #6b7280;
+    cursor: pointer;
+    
+    &:hover {
+      background: #f9fafb;
+    }
+  }
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  
+  .view-toggle {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    .toggle-btn {
+      padding: 6px 12px;
+      border: none;
+      background: transparent;
+      color: #9ca3af;
+      font-size: 14px;
+      cursor: pointer;
+      
+      &.active {
+        color: #7c3aed;
+        font-weight: 600;
+      }
+    }
+    
+    .divider {
+      color: #d1d5db;
+    }
+  }
+  
+  .manage-btn {
+    padding: 6px 16px;
+    border: 1px solid #d1d5db;
+    background: white;
+    border-radius: 16px;
+    font-size: 13px;
+    color: #6b7280;
+    cursor: pointer;
+    
+    &:hover {
+      background: #f9fafb;
+    }
+  }
+}
+
+.manage-actions {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 16px;
+  
+  .action-btn {
+    padding: 8px 16px;
+    border: none;
+    border-radius: 8px;
+    font-size: 13px;
+    cursor: pointer;
+    background: #f3f4f6;
+    color: #374151;
+    
+    &.delete {
+      background: #fee2e2;
+      color: #dc2626;
+    }
+  }
+}
+
+.month-stats {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 20px;
+  
+  .stat-card {
+    flex: 1;
+    background: white;
+    border-radius: 12px;
+    padding: 16px 12px;
+    text-align: center;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+    
+    .stat-value {
+      display: block;
+      font-size: 20px;
+      font-weight: 700;
+      color: #7c3aed;
+      
+      small {
+        font-size: 12px;
+        font-weight: 400;
+      }
+      
+      &.positive {
+        color: #10b981;
+      }
+      
+      &.negative {
+        color: #ef4444;
+      }
+    }
+    
+    .stat-label {
+      display: block;
+      font-size: 12px;
+      color: #9ca3af;
+      margin-top: 4px;
+    }
+  }
+}
+
+.calendar {
+  background: white;
+  border-radius: 16px;
+  padding: 16px;
+  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
+  
+  .calendar-header {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    margin-bottom: 8px;
+    
+    .weekday {
+      text-align: center;
+      font-size: 13px;
+      font-weight: 500;
+      color: #9ca3af;
+      padding: 8px 0;
+    }
+  }
+  
+  .calendar-grid {
+    display: grid;
+    grid-template-columns: repeat(7, 1fr);
+    gap: 4px;
+  }
+  
+  .calendar-day {
+    aspect-ratio: 1;
+    min-height: 70px;
+    border-radius: 8px;
+    padding: 4px;
+    cursor: pointer;
+    transition: all 0.2s;
+    display: flex;
+    flex-direction: column;
+    background: #f8fafc;
+    
+    &.empty {
+      background: transparent;
+      cursor: default;
+    }
+    
+    &.has-record {
+      background: linear-gradient(135deg, #dbeafe 0%, #c7d2fe 100%);
+      
+      .day-number {
+        color: #1e40af;
+      }
+    }
+    
+    &.today {
+      border: 2px solid #7c3aed;
+      
+      .day-number {
+        color: #7c3aed;
+        font-weight: 700;
+      }
+    }
+    
+    &.selected {
+      background: #7c3aed !important;
+      
+      .day-number,
+      .day-content span {
+        color: white !important;
+      }
+    }
+    
+    &:hover:not(.empty) {
+      transform: scale(1.02);
+      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+    }
+    
+    .day-number {
+      font-size: 12px;
+      font-weight: 500;
+      color: #6b7280;
+    }
+    
+    .day-content {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      gap: 2px;
+      
+      .completion-badge {
+        font-size: 10px;
+        color: #10b981;
+      }
+      
+      .weight-value {
+        font-size: 11px;
+        font-weight: 600;
+        color: #1e40af;
+      }
+      
+      .bodyfat-value {
+        font-size: 9px;
+        color: #6b7280;
+      }
+    }
+  }
+}
+
+.day-detail {
+  margin-top: 20px;
+  background: white;
+  border-radius: 16px;
+  padding: 20px;
+  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
+  
+  .detail-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    
+    h3 {
+      font-size: 16px;
+      font-weight: 600;
+      color: #1f2937;
+      margin: 0;
+    }
+    
+    .close-btn {
+      width: 28px;
+      height: 28px;
+      border: none;
+      background: #f3f4f6;
+      border-radius: 50%;
+      font-size: 16px;
+      cursor: pointer;
+      color: #6b7280;
+      
+      &:hover {
+        background: #e5e7eb;
+      }
+    }
+  }
+  
+  .detail-records {
+    .record-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 0;
+      border-bottom: 1px solid #f3f4f6;
+      
+      &:last-child {
+        border-bottom: none;
+      }
+      
+      .record-info {
+        display: flex;
+        flex-direction: column;
+        gap: 4px;
+        
+        .record-time {
+          font-size: 14px;
+          font-weight: 500;
+          color: #374151;
+        }
+        
+        .record-condition {
+          font-size: 12px;
+          color: #10b981;
+          background: #d1fae5;
+          padding: 2px 8px;
+          border-radius: 10px;
+          display: inline-block;
+          width: fit-content;
+        }
+      }
+      
+      .record-metrics {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-end;
+        gap: 2px;
+        
+        .metric-weight {
+          font-size: 18px;
+          font-weight: 700;
+          color: #7c3aed;
+        }
+        
+        .metric-bodyfat,
+        .metric-muscle {
+          font-size: 12px;
+          color: #6b7280;
+        }
+      }
+    }
+  }
+  
+  .detail-summary {
+    margin-top: 16px;
+    padding-top: 16px;
+    border-top: 1px solid #e5e7eb;
+    font-size: 14px;
+    color: #6b7280;
+    text-align: center;
+  }
+}

+ 249 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-history/weight-history.component.ts

@@ -0,0 +1,249 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule, Location } from '@angular/common';
+import { Router, RouterModule } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { PageHeaderComponent } from '../../../shared/page-header/page-header.component';
+import { CalendarGridComponent, CalendarDayData } from '../../../shared/components/calendar-grid/calendar-grid.component';
+import { WeightDataService } from '../../../services/weight-data.service';
+import { WeightRecord } from '../models/weight.models';
+
+interface DayRecord {
+  date: Date;
+  records: WeightRecord[];
+  avgWeight: number;
+  avgBodyFat: number;
+  hasData: boolean;
+}
+
+@Component({
+  selector: 'app-weight-history',
+  standalone: true,
+  imports: [CommonModule, RouterModule, FormsModule, PageHeaderComponent, CalendarGridComponent],
+  templateUrl: './weight-history.component.html',
+  styleUrl: './weight-history.component.scss'
+})
+export class WeightHistoryComponent implements OnInit {
+  currentDate = new Date();
+  selectedDate: Date | null = null;
+  viewMode: 'month' | 'year' = 'month';
+  isManageMode = false;
+  selectedRecords: Set<string> = new Set();
+
+  calendarDays: (DayRecord | null)[] = [];
+  weekDays = ['一', '二', '三', '四', '五', '六', '日'];
+  historyRecords: Map<string, DayRecord> = new Map();
+  calendarDataMap: Map<string, CalendarDayData> = new Map();
+  allRecords: WeightRecord[] = [];
+
+  monthStats = { totalDays: 0, avgWeight: 0, minWeight: 0, maxWeight: 0, weightChange: 0 };
+
+  constructor(
+    private router: Router,
+    private location: Location,
+    private weightDataService: WeightDataService
+  ) {}
+
+  ngOnInit(): void {
+    this.weightDataService.records$.subscribe(records => {
+      this.allRecords = records;
+      this.processRecords();
+      this.buildCalendar();
+      this.buildCalendarDataMap();
+      this.calculateMonthStats();
+    });
+  }
+
+  buildCalendarDataMap(): void {
+    this.calendarDataMap.clear();
+    this.historyRecords.forEach((record, key) => {
+      this.calendarDataMap.set(key, {
+        date: record.date,
+        hasData: record.hasData,
+        completed: false,
+        primaryText: `${record.avgWeight.toFixed(1)}kg`,
+        secondaryText: `${record.avgBodyFat.toFixed(1)}%`,
+        badge: record.hasData ? '✓' : ''
+      });
+    });
+  }
+
+  onDateSelected(day: CalendarDayData | null): void {
+    if (!day) return;
+    if (this.isManageMode) {
+      const key = this.formatDateKeyFromDate(day.date);
+      if (this.selectedRecords.has(key)) this.selectedRecords.delete(key);
+      else this.selectedRecords.add(key);
+    } else {
+      this.selectedDate = day.date;
+    }
+  }
+
+  get currentYearMonth(): string {
+    return `${this.currentDate.getFullYear()}年${this.currentDate.getMonth() + 1}月`;
+  }
+
+  get selectedDayRecord(): DayRecord | null {
+    if (!this.selectedDate) return null;
+    return this.historyRecords.get(this.formatDateKey(this.selectedDate)) || null;
+  }
+
+
+  private processRecords(): void {
+    this.historyRecords.clear();
+    
+    this.allRecords.forEach(record => {
+      const dateKey = record.date;
+      const date = new Date(record.date);
+      
+      if (!this.historyRecords.has(dateKey)) {
+        this.historyRecords.set(dateKey, {
+          date,
+          records: [],
+          avgWeight: 0,
+          avgBodyFat: 0,
+          hasData: true
+        });
+      }
+      
+      const dayRecord = this.historyRecords.get(dateKey)!;
+      dayRecord.records.push(record);
+      
+      // 计算平均值
+      const weights = dayRecord.records.map(r => r.weight);
+      const bodyFats = dayRecord.records.map(r => r.bodyFat);
+      dayRecord.avgWeight = weights.reduce((a, b) => a + b, 0) / weights.length;
+      dayRecord.avgBodyFat = bodyFats.reduce((a, b) => a + b, 0) / bodyFats.length;
+    });
+  }
+
+  buildCalendar(): void {
+    const year = this.currentDate.getFullYear();
+    const month = this.currentDate.getMonth();
+    const firstDay = new Date(year, month, 1);
+    const lastDay = new Date(year, month + 1, 0);
+    let startWeekDay = firstDay.getDay();
+    startWeekDay = startWeekDay === 0 ? 6 : startWeekDay - 1;
+
+    this.calendarDays = [];
+    for (let i = 0; i < startWeekDay; i++) this.calendarDays.push(null);
+
+    for (let day = 1; day <= lastDay.getDate(); day++) {
+      const date = new Date(year, month, day);
+      const dateKey = this.formatDateKeyFromDate(date);
+      const record = this.historyRecords.get(dateKey);
+      this.calendarDays.push(record || { date, records: [], avgWeight: 0, avgBodyFat: 0, hasData: false });
+    }
+  }
+
+  previousMonth(): void {
+    this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 1);
+    this.buildCalendar();
+    this.calculateMonthStats();
+  }
+
+  nextMonth(): void {
+    this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 1);
+    this.buildCalendar();
+    this.calculateMonthStats();
+  }
+
+  goToCurrentMonth(): void {
+    this.currentDate = new Date();
+    this.buildCalendar();
+    this.calculateMonthStats();
+  }
+
+  selectDate(day: DayRecord | null): void {
+    if (!day) return;
+    if (this.isManageMode) {
+      const key = this.formatDateKeyFromDate(day.date);
+      if (this.selectedRecords.has(key)) this.selectedRecords.delete(key);
+      else this.selectedRecords.add(key);
+    } else {
+      this.selectedDate = day.date;
+    }
+  }
+
+  toggleManageMode(): void {
+    this.isManageMode = !this.isManageMode;
+    if (!this.isManageMode) this.selectedRecords.clear();
+  }
+
+  batchDelete(): void {
+    if (this.selectedRecords.size === 0) return;
+    if (confirm(`确定删除 ${this.selectedRecords.size} 条记录吗?`)) {
+      this.selectedRecords.forEach(key => {
+        const dayRecord = this.historyRecords.get(key);
+        if (dayRecord) {
+          dayRecord.records.forEach(record => {
+            if (record.id) {
+              this.weightDataService.deleteRecord(record.id);
+            }
+          });
+        }
+      });
+      this.selectedRecords.clear();
+      this.isManageMode = false;
+    }
+  }
+
+  selectAll(): void {
+    this.calendarDays.forEach(day => {
+      if (day && day.hasData) this.selectedRecords.add(this.formatDateKeyFromDate(day.date));
+    });
+  }
+
+  isToday(date: Date): boolean {
+    const today = new Date();
+    return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
+  }
+
+  isSelected(date: Date): boolean {
+    return this.selectedRecords.has(this.formatDateKeyFromDate(date));
+  }
+
+  backToWeight(): void {
+    this.location.back();
+  }
+
+  getConditionLabel(condition: string): string {
+    return condition === 'fasting' ? '空腹' : '餐后';
+  }
+
+  private calculateMonthStats(): void {
+    const year = this.currentDate.getFullYear();
+    const month = this.currentDate.getMonth();
+    let totalDays = 0;
+    const weights: number[] = [];
+
+    this.historyRecords.forEach((record, key) => {
+      const recordDate = new Date(key);
+      if (recordDate.getFullYear() === year && recordDate.getMonth() === month && record.hasData) {
+        totalDays++;
+        weights.push(record.avgWeight);
+      }
+    });
+
+    if (weights.length > 0) {
+      const avgWeight = weights.reduce((a, b) => a + b, 0) / weights.length;
+      const minWeight = Math.min(...weights);
+      const maxWeight = Math.max(...weights);
+      const weightChange = weights.length > 1 ? weights[weights.length - 1] - weights[0] : 0;
+      
+      this.monthStats = { totalDays, avgWeight, minWeight, maxWeight, weightChange };
+    } else {
+      this.monthStats = { totalDays: 0, avgWeight: 0, minWeight: 0, maxWeight: 0, weightChange: 0 };
+    }
+  }
+
+  private formatDateKey(date: Date): string {
+    return this.formatDateKeyFromDate(date);
+  }
+
+  private formatDateKeyFromDate(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    return `${year}-${month}-${day}`;
+  }
+}

+ 214 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-mobile.component.html

@@ -0,0 +1,214 @@
+<div class="weight-mobile-container">
+  <!-- 顶部标题栏 -->
+  <div class="mobile-header">
+    <app-page-header [title]="'体重管理'" (back)="backToDashboard()">
+      <div header-actions>
+        <button class="goal-btn" (click)="openGoalDialog()">
+          🎯 目标
+        </button>
+      </div>
+    </app-page-header>
+  </div>
+
+  <div class="scroll-content">
+    <!-- 核心指标卡片 -->
+    <div class="summary-card" *ngIf="stats">
+      <div class="summary-top">
+        <div class="summary-lbl">当前体重 {{ records[0]?.date | date:'MM/dd' }}</div>
+        <div class="summary-bmi" *ngIf="records[0]">BMI {{ (records[0].weight / ((1.75)*(1.75))).toFixed(1) }}</div>
+      </div>
+      
+      <div class="summary-main-val">
+        {{ stats.currentWeight.toFixed(1) }} <span class="summary-unit">kg</span>
+      </div>
+      
+      <div class="summary-badges">
+        <span class="stat-badge" [ngClass]="{'positive': stats.weightChange < 0, 'negative': stats.weightChange > 0}">
+          较上一次 {{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange.toFixed(1) }} kg
+        </span>
+      </div>
+      
+      <div class="summary-stats-row">
+        <div class="stat-item">
+          <div class="stat-lbl">目标</div>
+          <div class="stat-val">{{ goal?.targetWeight || '--' }}</div>
+        </div>
+        <div class="stat-item">
+          <div class="stat-lbl">剩余</div>
+          <div class="stat-val">{{ goal ? (stats.currentWeight - goal.targetWeight | number:'1.1-1') : '--' }}</div>
+        </div>
+        <div class="stat-item">
+          <div class="stat-lbl">周变化</div>
+          <div class="stat-val">{{ stats.avgWeeklyChange > 0 ? '+' : '' }}{{ stats.avgWeeklyChange.toFixed(1) }}</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 趋势图表 -->
+    <section class="chart-section">
+      <div class="section-header">
+        <div class="section-title">体重趋势</div>
+        <div class="period-selector">
+          <button [class.active]="trendPeriod === 'week'" (click)="changeTrendPeriod('week')">周</button>
+          <button [class.active]="trendPeriod === 'month'" (click)="changeTrendPeriod('month')">月</button>
+          <button [class.active]="trendPeriod === '6months'" (click)="changeTrendPeriod('6months')">半年</button>
+        </div>
+      </div>
+      <div class="chart-container-wrapper">
+        <div id="trendChart" class="chart"></div>
+      </div>
+    </section>
+
+    <!-- 详细指标网格 -->
+    <div class="metrics-grid" *ngIf="records.length > 0 && stats">
+      <div class="metric-card">
+        <div class="metric-icon blue">💧</div>
+        <div class="metric-label">体脂率</div>
+        <div class="metric-value">{{ records[0].bodyFat.toFixed(1) }}%</div>
+        <div class="metric-status">
+           {{ stats?.bodyFatChange && stats!.bodyFatChange < 0 ? '↓' : '↑' }} {{ stats?.bodyFatChange ? (Math.abs(stats!.bodyFatChange).toFixed(1)) : '0.0' }}%
+        </div>
+      </div>
+      <div class="metric-card">
+        <div class="metric-icon purple">💪</div>
+        <div class="metric-label">肌肉量</div>
+        <div class="metric-value">{{ records[0].muscleMass.toFixed(1) }} <small>kg</small></div>
+        <div class="metric-status">--</div>
+      </div>
+      <div class="metric-card">
+        <div class="metric-icon orange">📅</div>
+        <div class="metric-label">已坚持</div>
+        <div class="metric-value">{{ stats?.daysTracked }} <small>天</small></div>
+        <div class="metric-status">继续加油</div>
+      </div>
+      <div class="metric-card">
+        <div class="metric-icon green">📊</div>
+        <div class="metric-label">总减重</div>
+        <div class="metric-value">{{ (goal?.startWeight || 0) - stats.currentWeight | number:'1.1-1' }} <small>kg</small></div>
+        <div class="metric-status good">成效显著</div>
+      </div>
+    </div>
+
+    <!-- 历史记录列表 -->
+    <div class="history-section">
+      <div class="section-header">
+        <div class="section-title">最近记录</div>
+        <button class="view-all-btn" (click)="goToHistory()">全部 ></button>
+      </div>
+      
+      <div class="history-list">
+        <div class="list-item" *ngFor="let rec of records | slice:0:5">
+          <div class="item-left">
+            <div class="item-date">{{ rec.date | date:'MM月dd日' }} <small>{{ rec.measurementTime }}</small></div>
+            <div class="item-tags">
+              <span class="tag condition">{{ rec.measurementCondition === 'fasting' ? '空腹' : '餐后' }}</span>
+              <span class="tag" *ngFor="let tag of rec.tags">{{ tag }}</span>
+            </div>
+          </div>
+          <div class="item-right">
+            <div class="item-weight">{{ rec.weight.toFixed(1) }} kg</div>
+            <div class="item-sub">体脂 {{ rec.bodyFat.toFixed(1) }}%</div>
+          </div>
+        </div>
+        
+        <div class="empty-state" *ngIf="records.length === 0">
+          暂无记录,点击右下角按钮添加
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 悬浮添加按钮 -->
+  <button class="fab" (click)="openAddRecordDialog()">
+    <span>+</span>
+  </button>
+
+  <!-- 添加记录对话框 -->
+  <app-modal 
+    [(isOpen)]="showAddRecordDialog"
+    [config]="{
+      title: '录入数据',
+      icon: '⚖️',
+      size: 'medium',
+      theme: 'default',
+      showFooter: false
+    }">
+    <div class="weight-record-form">
+      <div class="form-group">
+        <label>日期</label>
+        <input type="date" [(ngModel)]="newRecord.date" max="{{ today }}" />
+      </div>
+      <div class="form-group">
+        <label>时间</label>
+        <input type="time" [(ngModel)]="newRecord.measurementTime" />
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>体重 (kg)</label>
+          <input type="number" [(ngModel)]="newRecord.weight" step="0.1" />
+        </div>
+        <div class="form-group">
+          <label>体脂率 (%)</label>
+          <input type="number" [(ngModel)]="newRecord.bodyFat" step="0.1" />
+        </div>
+      </div>
+      <div class="form-group">
+        <label>肌肉量 (kg)</label>
+        <input type="number" [(ngModel)]="newRecord.muscleMass" step="0.1" />
+      </div>
+      <div class="form-group">
+        <label>状态</label>
+        <div class="radio-group">
+          <label class="radio-label">
+            <input type="radio" name="condition" [(ngModel)]="newRecord.measurementCondition" value="fasting"> 空腹
+          </label>
+          <label class="radio-label">
+            <input type="radio" name="condition" [(ngModel)]="newRecord.measurementCondition" value="after_meal"> 餐后
+          </label>
+        </div>
+      </div>
+      <div class="dialog-actions">
+        <button class="btn btn-secondary" (click)="showAddRecordDialog = false">取消</button>
+        <button class="btn btn-primary" (click)="addRecord()">保存</button>
+      </div>
+    </div>
+  </app-modal>
+
+  <!-- 目标设置对话框 -->
+  <app-modal 
+    [(isOpen)]="showGoalDialog"
+    [config]="{
+      title: '设置目标',
+      icon: '🎯',
+      size: 'small',
+      theme: 'gradient',
+      showFooter: false
+    }">
+    <div class="goal-setting-form">
+      <div class="form-group">
+        <label>目标体重 (kg)</label>
+        <input type="number" [(ngModel)]="newGoal.targetWeight" step="0.1" />
+      </div>
+      <div class="form-group">
+        <label>目标体脂率 (%)</label>
+        <input type="number" [(ngModel)]="newGoal.targetBodyFat" step="0.1" />
+      </div>
+      <div class="form-group">
+        <label>达成日期</label>
+        <input type="date" [(ngModel)]="newGoal.targetDate" [min]="tomorrow" />
+      </div>
+      <div class="dialog-actions">
+        <button class="btn btn-secondary" (click)="showGoalDialog = false">取消</button>
+        <button class="btn btn-primary" (click)="setGoal()">保存</button>
+      </div>
+    </div>
+  </app-modal>
+
+  <!-- 调试:一键生成90天模拟数据 -->
+  <div class="debug-section" *ngIf="records.length === 0">
+    <button class="debug-btn" (click)="generateMockData()">
+      📊 生成90天模拟数据
+    </button>
+    <p class="debug-text">点击上方按钮生成测试数据以查看图表效果</p>
+  </div>
+</div>

+ 539 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight-mobile.component.scss

@@ -0,0 +1,539 @@
+// 移动端适配样式 - 基于方案 A
+.weight-mobile-container {
+  min-height: 100vh;
+  background-color: #f5f7fa;
+  color: #1e293b;
+  padding-bottom: 80px; /* FAB space */
+  max-width: 480px;
+  margin: 0 auto;
+  position: relative;
+}
+
+// 顶部导航
+.mobile-header {
+  background: transparent;
+  padding: 0;
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: block; // Change to block to let page-header fill width
+  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+
+  app-page-header {
+    width: 100%;
+    display: block;
+    
+    // Inject styles into projected content if possible, or assume global styles
+  }
+
+  // Header actions are now projected inside app-page-header, 
+  // styling depends on where app-page-header places them.
+  // Assuming app-page-header puts them in a flex container on the right.
+  
+  .goal-btn {
+    background: rgba(255,255,255,0.25);
+    border: 1px solid rgba(255,255,255,0.4);
+    color: #fff;
+    font-size: 13px;
+    font-weight: 500;
+    padding: 6px 14px;
+    border-radius: 20px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    white-space: nowrap;
+    backdrop-filter: blur(4px);
+
+    &:hover {
+      background: rgba(255,255,255,0.35);
+    }
+  }
+}
+
+.scroll-content {
+  padding: 16px;
+}
+
+// 核心指标卡片 (Summary Card)
+.summary-card {
+  background: linear-gradient(135deg, #01847F 0%, #016c68 100%);
+  border-radius: 20px;
+  padding: 24px;
+  color: white;
+  margin-bottom: 20px;
+  box-shadow: 0 10px 25px rgba(1, 132, 127, 0.3);
+  position: relative;
+  overflow: hidden;
+
+  &::after {
+    content: "";
+    position: absolute;
+    right: -20px;
+    top: -20px;
+    width: 120px;
+    height: 120px;
+    background: rgba(255,255,255,0.1);
+    border-radius: 50%;
+  }
+
+  .summary-top {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+  }
+
+  .summary-lbl {
+    font-size: 13px;
+    opacity: 0.9;
+    color: rgba(255,255,255,0.9);
+  }
+
+  .summary-bmi {
+    background: rgba(255,255,255,0.2);
+    padding: 2px 10px;
+    border-radius: 10px;
+    font-size: 11px;
+    font-weight: 600;
+  }
+
+  .summary-main-val {
+    font-size: 46px;
+    font-weight: 700;
+    line-height: 1.1;
+    margin-bottom: 8px;
+    color: #fff;
+    
+    .summary-unit {
+      font-size: 18px;
+      font-weight: 500;
+      opacity: 0.9;
+      margin-left: 2px;
+    }
+  }
+
+  .summary-badges {
+    margin-bottom: 24px;
+    
+    .stat-badge {
+      font-size: 12px;
+      background: rgba(255,255,255,0.15);
+      padding: 4px 10px;
+      border-radius: 6px;
+      display: inline-block;
+      color: #fff;
+      
+      &.positive { color: #d1fae5; background: rgba(16, 185, 129, 0.2); } 
+      &.negative { color: #fee2e2; background: rgba(239, 68, 68, 0.2); }
+    }
+  }
+
+  .summary-stats-row {
+    display: flex;
+    justify-content: space-between;
+    padding-top: 18px;
+    border-top: 1px solid rgba(255,255,255,0.15);
+
+    .stat-item {
+      text-align: center;
+      flex: 1;
+      background: transparent !important;
+      padding: 0 !important;
+      border: none !important;
+      box-shadow: none !important;
+      
+      .stat-lbl {
+        font-size: 11px;
+        opacity: 0.8;
+        margin-bottom: 4px;
+        color: rgba(255,255,255,0.8);
+      }
+      
+      .stat-val {
+        font-size: 16px;
+        font-weight: 600;
+        color: #fff;
+        background: transparent !important;
+      }
+    }
+  }
+}
+
+// 图表区域
+.chart-section {
+  background: #fff;
+  border-radius: 16px;
+  padding: 16px 20px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 12px rgba(0,0,0,0.03);
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    
+    .section-title {
+      font-size: 15px;
+      font-weight: 700;
+      color: #1e293b;
+    }
+  }
+
+  .period-selector {
+    background: #f1f5f9;
+    padding: 3px;
+    border-radius: 8px;
+    display: flex;
+    
+    button {
+      border: none;
+      background: none;
+      padding: 6px 14px;
+      font-size: 12px;
+      border-radius: 6px;
+      color: #64748b;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.2s;
+      
+      &.active {
+        background: white;
+        color: #0f172a;
+        font-weight: 600;
+        box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+      }
+    }
+  }
+
+  .chart-container-wrapper {
+    height: 240px;
+    width: 100%;
+    
+    .chart {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+
+// 详细指标网格
+.metrics-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12px;
+  margin-bottom: 24px;
+
+  .metric-card {
+    background: #fff;
+    padding: 16px;
+    border-radius: 16px;
+    box-shadow: 0 2px 6px rgba(0,0,0,0.04);
+    display: flex;
+    flex-direction: column;
+    
+    .metric-icon {
+      width: 32px;
+      height: 32px;
+      border-radius: 8px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 16px;
+      margin-bottom: 12px;
+      
+      &.blue { background: #e0f2fe; color: #0ea5e9; }
+      &.purple { background: #f3e8ff; color: #a855f7; }
+      &.green { background: #dcfce7; color: #22c55e; }
+      &.orange { background: #ffedd5; color: #f97316; }
+    }
+    
+    .metric-label {
+      font-size: 12px;
+      color: #64748b;
+      margin-bottom: 4px;
+    }
+    
+    .metric-value {
+      font-size: 18px;
+      font-weight: 700;
+      color: #0f172a;
+      
+      small {
+        font-size: 12px;
+        font-weight: 400;
+        color: #94a3b8;
+      }
+    }
+    
+    .metric-status {
+      font-size: 11px;
+      margin-top: 4px;
+      color: #64748b;
+      
+      &.good { color: #22c55e; }
+    }
+  }
+}
+
+// 历史记录列表
+.history-section {
+  background: #fff;
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+  
+  .section-header {
+    padding: 16px;
+    border-bottom: 1px solid #f1f5f9;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    
+    .section-title {
+      font-size: 15px;
+      font-weight: 600;
+    }
+    
+    .view-all-btn {
+      background: none;
+      border: none;
+      color: #01847F;
+      font-size: 13px;
+      font-weight: 500;
+      cursor: pointer;
+    }
+  }
+}
+
+.history-list {
+  .list-item {
+    padding: 16px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #f8fafc;
+    
+    &:last-child {
+      border-bottom: none;
+    }
+    
+    .item-left {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+    }
+    
+    .item-date {
+      font-size: 14px;
+      font-weight: 500;
+      color: #334155;
+      
+      small {
+        color: #94a3b8;
+        margin-left: 6px;
+      }
+    }
+    
+    .item-tags {
+      display: flex;
+      gap: 6px;
+      
+      .tag {
+        font-size: 10px;
+        padding: 2px 6px;
+        background: #f1f5f9;
+        color: #64748b;
+        border-radius: 4px;
+        
+        &.condition {
+          background: rgba(1, 132, 127, 0.1);
+          color: #01847F;
+        }
+      }
+    }
+    
+    .item-right {
+      text-align: right;
+    }
+    
+    .item-weight {
+      font-size: 16px;
+      font-weight: 600;
+      color: #0f172a;
+    }
+    
+    .item-sub {
+      font-size: 11px;
+      color: #94a3b8;
+    }
+  }
+  
+  .empty-state {
+    padding: 30px;
+    text-align: center;
+    color: #94a3b8;
+    font-size: 13px;
+  }
+}
+
+// 悬浮按钮 (FAB)
+.fab {
+  position: fixed;
+  bottom: 24px;
+  right: 24px;
+  width: 56px;
+  height: 56px;
+  background: linear-gradient(135deg, #01847F 0%, #016c68 100%);
+  color: white;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  box-shadow: 0 4px 12px rgba(1, 132, 127, 0.4);
+  border: none;
+  cursor: pointer;
+  z-index: 900;
+  transition: transform 0.2s;
+  
+  &:active {
+    transform: scale(0.95);
+  }
+}
+
+// Modal Styles overrides or helpers
+.weight-record-form, .goal-setting-form {
+  padding: 10px 0;
+  
+  .form-group {
+    margin-bottom: 16px;
+    
+    label {
+      display: block;
+      font-size: 13px;
+      font-weight: 600;
+      color: #475569;
+      margin-bottom: 6px;
+    }
+    
+    input, select, textarea {
+      width: 100%;
+      padding: 10px;
+      border: 1px solid #e2e8f0;
+      border-radius: 8px;
+      font-size: 14px;
+      background: #f8fafc;
+      
+      &:focus {
+        outline: none;
+        border-color: #01847F;
+        background: #fff;
+      }
+    }
+  }
+  
+  .form-row {
+    display: flex;
+    gap: 12px;
+    .form-group { flex: 1; }
+  }
+  
+  .radio-group {
+    display: flex;
+    gap: 20px;
+    
+    .radio-label {
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-weight: 400;
+    }
+  }
+  
+  .dialog-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+    margin-top: 24px;
+    
+    button {
+      padding: 8px 20px;
+      border-radius: 8px;
+      border: none;
+      font-size: 14px;
+      font-weight: 500;
+      cursor: pointer;
+      
+      &.btn-secondary {
+        background: #f1f5f9;
+        color: #64748b;
+      }
+      
+      &.btn-primary {
+        background: #01847F;
+        color: white;
+      }
+    }
+  }
+}
+
+// Tag Selector
+.tag-selector {
+  .available-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+  
+  .tag-btn {
+    font-size: 12px;
+    padding: 4px 10px;
+    border-radius: 20px;
+    border: 1px solid #e2e8f0;
+    background: #fff;
+    cursor: pointer;
+    
+    &.selected {
+      background: rgba(1, 132, 127, 0.1);
+      border-color: #01847F;
+      color: #01847F;
+    }
+  }
+}
+
+// 调试区域
+.debug-section {
+  padding: 24px 16px;
+  text-align: center;
+  background: #fff;
+  margin: 16px;
+  border-radius: 16px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+  
+  .debug-btn {
+    background: linear-gradient(135deg, #01847F 0%, #016c68 100%);
+    color: white;
+    border: none;
+    padding: 12px 24px;
+    border-radius: 12px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    margin-bottom: 12px;
+    box-shadow: 0 4px 12px rgba(1, 132, 127, 0.3);
+    
+    &:active {
+      transform: scale(0.98);
+    }
+  }
+  
+  .debug-text {
+    font-size: 12px;
+    color: #6b7280;
+    margin: 0;
+    line-height: 1.4;
+  }
+}

+ 168 - 162
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.html

@@ -6,31 +6,64 @@
     </div>
   </app-page-header>
 
-  <!-- 趋势主导视图主布局 -->
-  <div class="weight-main-grid" *ngIf="stats">
-    <!-- 左侧:体重趋势 + 关键数字 -->
-    <div class="left-column">
-      <section class="panel">
-        <div class="panel-header">
-          <span class="panel-title">体重趋势总览</span>
+  <div class="main-content">
+    <!-- 主要体重卡片 -->
+    <div class="main-weight-card" *ngIf="stats">
+      <div class="weight-display">
+        <div class="weight-value">{{ stats.currentWeight.toFixed(1) }}</div>
+        <div class="weight-unit">kg</div>
+      </div>
+      <div class="weight-info">
+        <div class="weight-status" *ngIf="goal">
+          目标 {{ goal.targetWeight }}kg
         </div>
-        
+        <div class="weight-status" *ngIf="!goal">
+          建议设置目标体重
+        </div>
+        <div class="weight-stats">
+          <div class="stat-item">
+            <span class="stat-label">变化</span>
+            <span class="stat-value" [ngClass]="{ 'positive': stats.weightChange > 0, 'negative': stats.weightChange < 0 }">
+              {{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange.toFixed(1) }}
+            </span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">周均</span>
+            <span class="stat-value" [ngClass]="{ 'positive': stats.avgWeeklyChange > 0, 'negative': stats.avgWeeklyChange < 0 }">
+              {{ stats.avgWeeklyChange > 0 ? '+' : '' }}{{ stats.avgWeeklyChange.toFixed(2) }}
+            </span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">天数</span>
+            <span class="stat-value">{{ stats.daysTracked }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 体重趋势图表 -->
+    <div class="trend-section" *ngIf="stats">
+      <div class="section-header">
+        <h3 class="section-title">体重趋势</h3>
+      </div>
+      
+      <div class="trend-card">
         <!-- 周期切换标签 -->
-        <div class="chart-period-tabs">
+        <div class="trend-tabs">
           <button 
-            class="period-tab" 
+            class="trend-tab" 
             [class.active]="trendPeriod === 'week'"
             (click)="changeTrendPeriod('week')">
             周
           </button>
           <button 
-            class="period-tab" 
+            class="trend-tab" 
             [class.active]="trendPeriod === 'month'"
             (click)="changeTrendPeriod('month')">
             月
           </button>
           <button 
-            class="period-tab" 
+            class="trend-tab" 
             [class.active]="trendPeriod === '6months'"
             (click)="changeTrendPeriod('6months')">
             6个月
@@ -38,186 +71,159 @@
         </div>
         
         <!-- 统计信息 -->
-        <div class="trend-stats" *ngIf="trendStats">
+        <div class="trend-summary" *ngIf="trendStats">
           <div class="trend-avg">
-            <span class="avg-label">平均</span>
+            <span class="avg-label">平均变化</span>
             <span class="avg-value" [ngClass]="{'positive': trendStats.avgChange > 0, 'negative': trendStats.avgChange < 0}">
-              {{ trendStats.avgChange > 0 ? '+' : '' }}{{ trendStats.avgChange.toFixed(2) }}
+              {{ trendStats.avgChange > 0 ? '+' : '' }}{{ trendStats.avgChange.toFixed(2) }}kg
             </span>
-            <span class="avg-unit">kg(距离基线)</span>
           </div>
           <div class="trend-range">{{ trendStats.dateRange }}</div>
         </div>
         
-        <div class="trend-chart-wrapper">
+        <div class="chart-wrapper">
           <div id="trendChart" class="chart" style="width: 100%; height: 100%;"></div>
         </div>
-        <div class="kpi-strip">
-          <div class="kpi-item">
-            <div class="kpi-label">当前体重</div>
-            <div class="kpi-main">
-              <span class="kpi-value">{{ stats.currentWeight.toFixed(1) }}</span>
-              <span class="kpi-unit">kg</span>
-            </div>
-            <div class="kpi-desc" *ngIf="goal">
-              目标 {{ goal.targetWeight }}kg · 剩余
-              {{ (stats.currentWeight - goal.targetWeight) | number:'1.1-1' }}kg
-            </div>
-            <div class="kpi-desc" *ngIf="!goal">
-              建议先设置目标体重,便于评估进度
-            </div>
-          </div>
-
-          <div class="kpi-item">
-            <div class="kpi-label">最近体重变化</div>
-            <div class="kpi-main">
-              <span
-                class="kpi-value"
-                [ngClass]="{ 'positive': stats.weightChange > 0, 'negative': stats.weightChange < 0 }"
-              >
-                {{ stats.weightChange > 0 ? '+' : '' }}{{ stats.weightChange.toFixed(1) }}
-              </span>
-              <span class="kpi-unit">kg</span>
-            </div>
-            <div class="kpi-desc">从所选时间范围起点到当前的总体变化</div>
-          </div>
-
-          <div class="kpi-item">
-            <div class="kpi-label">平均周变化</div>
-            <div class="kpi-main">
-              <span
-                class="kpi-value"
-                [ngClass]="{ 'positive': stats.avgWeeklyChange > 0, 'negative': stats.avgWeeklyChange < 0 }"
-              >
-                {{ stats.avgWeeklyChange > 0 ? '+' : '' }}{{ stats.avgWeeklyChange.toFixed(2) }}
-              </span>
-              <span class="kpi-unit">kg/周</span>
-            </div>
-            <div class="kpi-desc">
-              已跟踪 {{ stats.daysTracked }} 天 · {{ stats.goalETA || '预计达成时间计算中' }}
-            </div>
-          </div>
-        </div>
-      </section>
+      </div>
     </div>
 
-    <!-- 右侧:体脂率与最近记录 -->
-    <div class="right-column" *ngIf="records.length > 0">
-      <section class="panel">
-        <div class="panel-header">
-          <span class="panel-title">体脂率与身体成分</span>
-          <span class="panel-extra">目标体脂率 {{ goal?.targetBodyFat ?? '—' }}%</span>
+    <!-- 体脂率与身体成分 -->
+    <div class="composition-section" *ngIf="records.length > 0">
+      <div class="section-header">
+        <h3 class="section-title">身体成分</h3>
+        <span class="section-extra">目标体脂率 {{ goal?.targetBodyFat ?? '—' }}%</span>
+      </div>
+      
+      <div class="composition-card">
+        <div class="composition-chart">
+          <div id="progressChart" class="chart-small"></div>
         </div>
-        <div class="composition-body">
-          <div class="composition-ring">
-            <div id="progressChart" class="chart-small"></div>
+        <div class="composition-stats">
+          <div class="composition-item">
+            <span class="composition-label">当前体脂率</span>
+            <span class="composition-value">{{ records[0].bodyFat.toFixed(1) }}%</span>
           </div>
-          <div class="composition-info">
-            <ul class="list">
-              <li class="list-item">
-                <span>当前体脂率</span>
-                <span><strong>{{ records[0].bodyFat.toFixed(1) }}%</strong></span>
-              </li>
-              <li class="list-item" *ngIf="stats">
-                <span>最近 30 天变化</span>
-                <span
-                  class="kpi-value"
-                  [ngClass]="{ 'negative': stats.bodyFatChange < 0, 'positive': stats.bodyFatChange > 0 }"
-                >
-                  {{ stats.bodyFatChange > 0 ? '+' : ''}}{{ stats.bodyFatChange.toFixed(1) }}%
-                </span>
-              </li>
-              <li class="list-item">
-                <span>估算去脂体重</span>
-                <span>{{ (records[0].weight * (1 - records[0].bodyFat / 100)).toFixed(1) }} kg</span>
-              </li>
-              <li class="list-item">
-                <span>肌肉量</span>
-                <span>{{ records[0].muscleMass.toFixed(1) }} kg</span>
-              </li>
-            </ul>
-            <div class="tag-row">
-              <span class="tag" *ngIf="stats.bodyFatChange <= 0">体重趋势与体脂率整体同步下降</span>
-              <span class="tag" *ngIf="stats.bodyFatChange > 0">近期可能存在"掉秤不掉脂"情况</span>
-            </div>
+          <div class="composition-item" *ngIf="stats">
+            <span class="composition-label">30天变化</span>
+            <span class="composition-value" [ngClass]="{ 'negative': stats.bodyFatChange < 0, 'positive': stats.bodyFatChange > 0 }">
+              {{ stats.bodyFatChange > 0 ? '+' : ''}}{{ stats.bodyFatChange.toFixed(1) }}%
+            </span>
+          </div>
+          <div class="composition-item">
+            <span class="composition-label">去脂体重</span>
+            <span class="composition-value">{{ (records[0].weight * (1 - records[0].bodyFat / 100)).toFixed(1) }}kg</span>
+          </div>
+          <div class="composition-item">
+            <span class="composition-label">肌肉量</span>
+            <span class="composition-value">{{ records[0].muscleMass.toFixed(1) }}kg</span>
           </div>
         </div>
-      </section>
-
-      <section class="panel">
-        <div class="panel-header">
-          <span class="panel-title">最近关键记录</span>
-          <span class="panel-extra">近 7 天</span>
+      </div>
+      
+      <div class="composition-tips" *ngIf="stats">
+        <div class="tip-item" *ngIf="stats.bodyFatChange <= 0">
+          ✅ 体重趋势与体脂率整体同步下降
+        </div>
+        <div class="tip-item warning" *ngIf="stats.bodyFatChange > 0">
+          ⚠️ 近期可能存在"掉秤不掉脂"情况
         </div>
-        <ul class="list">
-          <li class="list-item" *ngFor="let rec of records | slice:0:3">
-            <span>{{ rec.date }} · {{ rec.weight.toFixed(1) }}kg · {{ rec.bodyFat.toFixed(1) }}%</span>
-            <span class="badge">
+      </div>
+    </div>
+
+    <!-- 最近记录 -->
+    <div class="recent-records" *ngIf="records.length > 0">
+      <div class="section-header">
+        <h3 class="section-title">最近记录</h3>
+        <button class="view-all-btn" (click)="updateTimePeriod('7days')">查看全部</button>
+      </div>
+      
+      <div class="records-list">
+        <div class="record-item" *ngFor="let rec of records | slice:0:3">
+          <div class="record-date">{{ rec.date }}</div>
+          <div class="record-data">
+            <span class="record-weight">{{ rec.weight.toFixed(1) }}kg</span>
+            <span class="record-bodyfat">{{ rec.bodyFat.toFixed(1) }}%</span>
+            <span class="record-badge">
               {{ rec.measurementCondition === 'fasting' ? '空腹' : '餐后' }}
             </span>
-          </li>
-        </ul>
-        <div class="cta-row">
-          <button class="cta-btn primary" (click)="openAddRecordDialog()">+ 录入体重与体脂</button>
-          <button class="cta-btn" (click)="updateTimePeriod('7days')">仅看近 7 天</button>
+          </div>
         </div>
-      </section>
+      </div>
+      
+      <div class="quick-actions">
+        <button class="quick-btn primary" (click)="openAddRecordDialog()">+ 录入数据</button>
+        <button class="quick-btn" (click)="openGoalDialog()">设置目标</button>
+      </div>
     </div>
   </div>
 
-  <div class="filter-bar">
-    <button class="filter-btn" [class.active]="filter.timePeriod === '7days'" (click)="updateTimePeriod('7days')">7天</button>
-    <button class="filter-btn" [class.active]="filter.timePeriod === '30days'" (click)="updateTimePeriod('30days')">30天</button>
-    <button class="filter-btn" [class.active]="filter.timePeriod === '90days'" (click)="updateTimePeriod('90days')">90天</button>
-    <button class="filter-btn" [class.active]="filter.timePeriod === 'all'" (click)="updateTimePeriod('all')">全部</button>
-    <button class="filter-btn" [class.active]="filter.timePeriod === 'custom'" (click)="showCustomDatePicker = !showCustomDatePicker">自定义</button>
-    <select class="filter-select" [(ngModel)]="filter.condition" (change)="applyFilter(); updateCharts();">
-      <option value="all">全部条件</option>
-      <option value="fasting">空腹</option>
-      <option value="after_meal">餐后</option>
-    </select>
-  </div>
-
-  <!-- 自定义日期范围选择器 -->
-  <div class="custom-date-picker" *ngIf="showCustomDatePicker">
-    <div class="date-range-inputs">
-      <div class="date-input-group">
-        <label>开始日期</label>
-        <input type="date" [(ngModel)]="filter.startDate" />
+    <!-- 筛选栏 -->
+    <div class="filter-section">
+      <div class="section-header">
+        <h3 class="section-title">数据筛选</h3>
       </div>
-      <div class="date-input-group">
-        <label>结束日期</label>
-        <input type="date" [(ngModel)]="filter.endDate" />
+      
+      <div class="filter-bar">
+        <button class="filter-btn" [class.active]="filter.timePeriod === '7days'" (click)="updateTimePeriod('7days')">7天</button>
+        <button class="filter-btn" [class.active]="filter.timePeriod === '30days'" (click)="updateTimePeriod('30days')">30天</button>
+        <button class="filter-btn" [class.active]="filter.timePeriod === '90days'" (click)="updateTimePeriod('90days')">90天</button>
+        <button class="filter-btn" [class.active]="filter.timePeriod === 'all'" (click)="updateTimePeriod('all')">全部</button>
+        <button class="filter-btn" [class.active]="filter.timePeriod === 'custom'" (click)="showCustomDatePicker = !showCustomDatePicker">自定义</button>
+        <select class="filter-select" [(ngModel)]="filter.condition" (change)="applyFilter(); updateCharts();">
+          <option value="all">全部条件</option>
+          <option value="fasting">空腹</option>
+          <option value="after_meal">餐后</option>
+        </select>
       </div>
-      <button class="btn btn-primary" (click)="applyCustomDateRange()">应用</button>
-    </div>
-  </div>
 
-  <!-- 标签筛选 -->
-  <div class="tag-filter" *ngIf="availableTags.length > 0">
-    <div class="tag-filter-title">按标签筛选:</div>
-    <div class="tag-filter-buttons">
-      <button *ngFor="let tag of availableTags" class="tag-filter-btn" [class.active]="filter.tags?.includes(tag)" (click)="toggleFilterTag(tag)">
-        {{ tag }}
-      </button>
-    </div>
-  </div>
+      <!-- 自定义日期范围选择器 -->
+      <div class="custom-date-picker" *ngIf="showCustomDatePicker">
+        <div class="date-range-inputs">
+          <div class="date-input-group">
+            <label>开始日期</label>
+            <input type="date" [(ngModel)]="filter.startDate" />
+          </div>
+          <div class="date-input-group">
+            <label>结束日期</label>
+            <input type="date" [(ngModel)]="filter.endDate" />
+          </div>
+          <button class="btn btn-primary" (click)="applyCustomDateRange()">应用</button>
+        </div>
+      </div>
 
-  <!-- 体重变化与体脂率分析(横向布局) -->
-  <div class="analysis-charts-container">
-    <div class="change-chart-section">
-      <div class="chart-header">
-        <h3 class="chart-title">体重变化趋势</h3>
-        <button class="toggle-btn" (click)="toggleChartPeriod()">切换至{{ chartPeriod === 'weekly' ? '月' : '周' }}视图</button>
+      <!-- 标签筛选 -->
+      <div class="tag-filter" *ngIf="availableTags.length > 0">
+        <div class="tag-filter-title">按标签筛选</div>
+        <div class="tag-filter-buttons">
+          <button *ngFor="let tag of availableTags" class="tag-filter-btn" [class.active]="filter.tags?.includes(tag)" (click)="toggleFilterTag(tag)">
+            {{ tag }}
+          </button>
+        </div>
       </div>
-      <div id="changeChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
     </div>
 
-    <div class="scatter-chart-section">
-      <div class="chart-header">
-        <h3 class="chart-title">体重体脂率关系</h3>
+    <!-- 分析图表 -->
+    <div class="analysis-section">
+      <div class="section-header">
+        <h3 class="section-title">数据分析</h3>
+      </div>
+      
+      <div class="analysis-charts-container">
+        <div class="change-chart-section">
+          <div class="chart-header">
+            <h4 class="chart-title">体重变化趋势</h4>
+            <button class="toggle-btn" (click)="toggleChartPeriod()">{{ chartPeriod === 'weekly' ? '月视图' : '周视图' }}</button>
+          </div>
+          <div id="changeChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
+        </div>
+
+        <div class="scatter-chart-section">
+          <div class="chart-header">
+            <h4 class="chart-title">体重体脂率关系</h4>
+          </div>
+          <div id="scatterChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
+        </div>
       </div>
-      <div id="scatterChart" class="chart-medium" style="width: 100%; height: 100%;"></div>
     </div>
   </div>
 

+ 609 - 614
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.scss

@@ -1,112 +1,194 @@
+// 体重管理页面 - 移动端优化样式
 .weight-container {
+  display: flex;
+  flex-direction: column;
   min-height: 100vh;
-  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
-  padding-bottom: 30px;
+  background: #f5f5f5;
+  color: #0f172a;
 }
 
-// 趋势主导视图主布局
-.weight-main-grid {
-  padding: 18px 24px 0;
-  display: grid;
-  grid-template-columns: 1.9fr 1.1fr;
-  gap: 18px;
+.main-content {
+  flex: 1;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding-bottom: 80px;
 }
 
-.left-column,
-.right-column {
+// ==================== 主要体重卡片 ====================
+.main-weight-card {
+  background: linear-gradient(135deg, #01847F 0%, #016c68 100%);
+  border-radius: 20px;
+  padding: 24px;
+  color: white;
   display: flex;
-  flex-direction: column;
-  gap: 16px;
+  align-items: center;
+  gap: 24px;
+  box-shadow: 0 8px 24px rgba(1, 132, 127, 0.3);
+  margin-bottom: 20px;
 }
 
-.panel {
-  background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
-  border-radius: 16px;
-  padding: 14px 16px;
-  border: 1px solid #e2e8f0;
-  box-shadow: 0 4px 18px rgba(15, 23, 42, 0.08);
+.weight-display {
+  display: flex;
+  align-items: baseline;
+  gap: 4px;
+}
+
+.weight-value {
+  font-size: 48px;
+  font-weight: 800;
+  line-height: 1;
+}
+
+.weight-unit {
+  font-size: 18px;
+  font-weight: 600;
+  opacity: 0.9;
+}
+
+.weight-info {
+  flex: 1;
+}
+
+.weight-status {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 16px;
+  opacity: 0.9;
+}
+
+.weight-stats {
+  display: flex;
+  gap: 20px;
+}
+
+.stat-item {
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  align-items: center;
+  text-align: center;
+}
+
+.stat-label {
+  font-size: 12px;
+  opacity: 0.8;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 18px;
+  font-weight: 700;
+  
+  &.positive {
+    color: #fecaca;
+  }
+  
+  &.negative {
+    color: #d1fae5;
+  }
 }
 
-.panel-header {
+// ==================== 区块标题 ====================
+.section-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  font-size: 13px;
-  color: #64748b;
+  margin-bottom: 12px;
+}
+
+.section-title {
+  font-size: 18px;
+  font-weight: 700;
+  color: #0f172a;
+}
+
+.section-extra {
+  font-size: 12px;
+  color: #6b7280;
 }
 
-.panel-title {
+.view-all-btn {
+  background: none;
+  border: none;
+  color: #01847F;
   font-size: 14px;
   font-weight: 600;
-  color: #1e293b;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 6px;
+
+  &:hover {
+    background: rgba(1, 132, 127, 0.1);
+  }
 }
 
-.panel-extra {
-  font-size: 12px;
+// ==================== 趋势图表区域 ====================
+.trend-section {
+  margin-bottom: 0;
+}
+
+.trend-card {
+  background: #ffffff;
+  border-radius: 16px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 }
 
-// 周期切换标签
-.chart-period-tabs {
+.trend-tabs {
   display: flex;
-  gap: 0;
-  background: #f3f4f6;
-  border-radius: 12px;
-  padding: 4px;
-  margin-top: 12px;
+  gap: 8px;
+  margin-bottom: 16px;
 }
 
-.period-tab {
-  flex: 1;
+.trend-tab {
   padding: 8px 16px;
+  border-radius: 20px;
+  font-size: 14px;
   border: none;
-  background: transparent;
-  border-radius: 8px;
-  font-size: 13px;
-  font-weight: 500;
+  background: #f5f5f5;
   color: #6b7280;
   cursor: pointer;
+  font-weight: 600;
   transition: all 0.2s ease;
 
   &:hover {
-    color: #374151;
+    background: rgba(1, 132, 127, 0.1);
+    color: #01847F;
   }
 
   &.active {
-    background: #ffffff;
-    color: #111827;
-    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    background: rgba(1, 132, 127, 0.1);
+    color: #01847F;
   }
 }
 
-// 趋势统计信息
-.trend-stats {
-  margin-top: 14px;
+.trend-summary {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
   padding: 12px 16px;
-  background: #faf5ff;
-  border-radius: 10px;
-  border: 1px solid #e9d5ff;
+  background: rgba(1, 132, 127, 0.05);
+  border-radius: 12px;
+  margin-bottom: 16px;
 }
 
 .trend-avg {
   display: flex;
-  align-items: baseline;
-  gap: 6px;
-  margin-bottom: 6px;
+  align-items: center;
+  gap: 8px;
 }
 
 .avg-label {
-  font-size: 12px;
+  font-size: 13px;
   color: #6b7280;
-  font-weight: 500;
+  font-weight: 600;
 }
 
 .avg-value {
-  font-size: 24px;
-  font-weight: 700;
-  color: #111827;
+  font-size: 20px;
+  font-weight: 800;
+  color: #0f172a;
 
   &.positive {
     color: #ef4444;
@@ -117,383 +199,389 @@
   }
 }
 
-.avg-unit {
-  font-size: 11px;
-  color: #9ca3af;
-}
-
 .trend-range {
-  font-size: 11px;
+  font-size: 12px;
   color: #6b7280;
 }
 
-.trend-chart-wrapper {
-  margin-top: 12px;
+.chart-wrapper {
+  height: 200px;
   border-radius: 12px;
   overflow: hidden;
-  min-height: 260px;
 }
 
-.trend-chart-wrapper .chart {
+.chart {
   width: 100%;
-  height: 260px;
+  height: 100%;
 }
 
-// 趋势下方关键指标条
-.kpi-strip {
-  margin-top: 10px;
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 10px;
+// ==================== 身体成分区域 ====================
+.composition-section {
+  margin-bottom: 0;
+}
+
+.composition-card {
+  background: #ffffff;
+  border-radius: 16px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+  display: flex;
+  gap: 20px;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.composition-chart {
+  flex: 0 0 120px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.chart-small {
+  width: 120px;
+  height: 120px;
+}
+
+.composition-stats {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.composition-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.composition-label {
+  font-size: 14px;
+  color: #6b7280;
+  font-weight: 500;
+}
+
+.composition-value {
+  font-size: 16px;
+  font-weight: 700;
+  color: #0f172a;
+
+  &.positive {
+    color: #ef4444;
+  }
+
+  &.negative {
+    color: #10b981;
+  }
+}
+
+.composition-tips {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
 }
 
-.kpi-item {
-  background: rgba(248, 250, 252, 0.9);
+.tip-item {
+  background: #ffffff;
   border-radius: 12px;
-  padding: 8px 10px;
-  border: 1px solid #e5e7eb;
+  padding: 12px 16px;
+  font-size: 13px;
+  color: #0f172a;
+  border-left: 4px solid #10b981;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+
+  &.warning {
+    border-left-color: #f59e0b;
+    background: #fffbeb;
+    color: #92400e;
+  }
+}
+
+// ==================== 最近记录区域 ====================
+.recent-records {
+  margin-bottom: 0;
+}
+
+.records-list {
   display: flex;
   flex-direction: column;
-  gap: 4px;
+  gap: 8px;
+  margin-bottom: 16px;
 }
 
-.kpi-label {
-  font-size: 11px;
-  color: #64748b;
+.record-item {
+  background: #ffffff;
+  border-radius: 12px;
+  padding: 16px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+}
+
+.record-date {
+  font-size: 14px;
+  color: #6b7280;
+  font-weight: 600;
 }
 
-.kpi-main {
+.record-data {
   display: flex;
-  align-items: baseline;
-  gap: 4px;
+  align-items: center;
+  gap: 12px;
 }
 
-.kpi-value {
-  font-size: 20px;
+.record-weight {
+  font-size: 16px;
   font-weight: 700;
-  color: #111827;
+  color: #0f172a;
 }
 
-.kpi-unit {
-  font-size: 11px;
-  color: #9ca3af;
+.record-bodyfat {
+  font-size: 14px;
+  font-weight: 600;
+  color: #6b7280;
 }
 
-.kpi-desc {
+.record-badge {
   font-size: 11px;
-  color: #6b7280;
+  font-weight: 600;
+  padding: 4px 10px;
+  border-radius: 12px;
+  background: rgba(1, 132, 127, 0.1);
+  color: #01847F;
 }
 
-.kpi-value.positive {
-  color: #ef4444;
+// ==================== 快捷操作 ====================
+.quick-actions {
+  display: flex;
+  gap: 10px;
 }
 
-.kpi-value.negative {
-  color: #10b981;
+.quick-btn {
+  flex: 1;
+  background: #ffffff;
+  border: none;
+  border-radius: 12px;
+  padding: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #0f172a;
+  cursor: pointer;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+  transition: transform 0.2s;
+
+  &.primary {
+    background: #01847F;
+    color: white;
+  }
+
+  &:active {
+    transform: scale(0.98);
+  }
 }
 
+// ==================== 按钮样式 ====================
 .btn {
   padding: 10px 20px;
   border: none;
-  border-radius: 10px;
+  border-radius: 12px;
   font-size: 14px;
   font-weight: 600;
   cursor: pointer;
-  transition: all 0.3s ease;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 
   &.btn-primary {
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    background: #01847F;
     color: #fff;
     
     &:hover {
-      transform: translateY(-2px);
-      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+      background: #016c68;
     }
   }
   
   &.btn-secondary {
     background: #fff;
-    color: #374151;
-    border: 2px solid #e5e7eb;
+    color: #0f172a;
+    border: 1px solid #e5e7eb;
     
     &:hover {
       background: #f9fafb;
       border-color: #d1d5db;
-      transform: translateY(-2px);
     }
   }
 }
 
-.stats-cards {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 16px;
-  padding: 20px 24px;
-  margin-top: 0;
-}
-
-.stat-card {
-  background: linear-gradient(135deg, #fff 0%, #fafbff 100%);
-  padding: 20px;
+// ==================== 筛选栏 ====================
+.filter-bar {
+  display: flex;
+  gap: 8px;
+  padding: 16px;
+  background: #fff;
   border-radius: 12px;
-  text-align: center;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  transition: all 0.3s ease;
-  border: 1px solid #e5e7eb;
-
-  &:hover {
-    transform: translateY(-4px);
-    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
-  }
-
-  .stat-label {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+  flex-wrap: wrap;
+  
+  .filter-btn {
+    flex: 1;
+    padding: 10px 12px;
+    border: 1px solid #e5e7eb;
+    background: #fff;
+    border-radius: 8px;
+    cursor: pointer;
     font-size: 13px;
-    color: #6b7280;
-    margin-bottom: 10px;
     font-weight: 600;
-    text-transform: uppercase;
-    letter-spacing: 0.5px;
-  }
-  
-  .stat-value {
-    font-size: 28px;
-    font-weight: 800;
-    color: #111827;
-    line-height: 1.2;
+    transition: all 0.2s ease;
+    color: #6b7280;
     
-    span {
-      font-size: 14px;
-      color: #9ca3af;
-      font-weight: 500;
-      margin-left: 4px;
+    &.active {
+      background: #01847F;
+      color: #fff;
+      border-color: #01847F;
+    }
+    
+    &:hover:not(.active) {
+      background: #f9fafb;
+      border-color: #d1d5db;
     }
   }
   
-  .positive { color: #ef4444; }
-  .negative { color: #10b981; }
-}
-
-.chart-section {
-  display: grid;
-  grid-template-columns: 2fr 1fr;
-  gap: 20px;
-  padding: 0 24px;
-  margin-top: 0;
-  margin-bottom: 20px;
-}
-
-.trend-chart-container {
-  background: #fff;
-  border-radius: 12px;
-  padding: 20px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
-  min-height: 450px;
-  display: flex;
-  flex-direction: column;
-  
-  .chart {
-    width: 100%;
-    height: 380px;
-    flex-shrink: 0;
-  }
-  
-  .trend-summary {
-    margin-top: 16px;
-    padding-top: 16px;
-    border-top: 2px solid #f0f0f0;
-    text-align: center;
+  .filter-select {
+    padding: 10px 12px;
+    border: 1px solid #e5e7eb;
+    border-radius: 8px;
+    background: #fff;
+    min-width: 120px;
+    font-size: 13px;
+    font-weight: 600;
+    cursor: pointer;
     color: #6b7280;
-    font-size: 14px;
-    line-height: 1.6;
-    flex-shrink: 0;
     
-    p {
-      margin: 0;
+    &:focus {
+      outline: none;
+      border-color: #01847F;
     }
   }
 }
 
-.progress-chart-container {
+// ==================== 自定义日期选择器 ====================
+.custom-date-picker {
   background: #fff;
+  padding: 16px;
   border-radius: 12px;
-  padding: 20px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
-  min-height: 450px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
   
-  .chart-small {
-    width: 100%;
-    height: 350px;
-  }
-}
-
-// 右侧体脂率与记录区
-.composition-body {
-  display: flex;
-  gap: 16px;
-  align-items: center;
-}
-
-.composition-ring {
-  flex: 0 0 170px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.composition-ring .chart-small {
-  width: 100%;
-  height: 170px;
-}
-
-.composition-info {
-  flex: 1;
-}
-
-.list {
-  list-style: none;
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-  font-size: 12px;
-  color: #64748b;
-}
-
-.list-item {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.badge {
-  border-radius: 999px;
-  padding: 2px 8px;
-  font-size: 11px;
-  background: rgba(79, 70, 229, 0.08);
-  color: #4f46e5;
-}
-
-.tag-row {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 6px;
-  margin-top: 4px;
-}
-
-.tag {
-  border-radius: 999px;
-  border: 1px solid #e2e8f0;
-  padding: 2px 8px;
-  font-size: 11px;
-  color: #64748b;
-  background: #ffffff;
-}
-
-.cta-row {
-  display: flex;
-  gap: 8px;
-  flex-wrap: wrap;
-  margin-top: 8px;
-}
-
-.cta-btn {
-  border-radius: 999px;
-  border: 1px solid #e2e8f0;
-  padding: 6px 12px;
-  background: #ffffff;
-  font-size: 12px;
-  cursor: pointer;
-  color: #64748b;
-}
-
-.cta-btn.primary {
-  background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
-  color: #ffffff;
-  border: none;
+  .date-range-inputs {
+    display: flex;
+    gap: 12px;
+    align-items: flex-end;
+    flex-wrap: wrap;
+    
+    .date-input-group {
+      flex: 1;
+      min-width: 140px;
+      
+      label {
+        display: block;
+        font-size: 13px;
+        font-weight: 600;
+        margin-bottom: 6px;
+        color: #0f172a;
+      }
+      
+      input {
+        width: 100%;
+        padding: 10px 12px;
+        border: 1px solid #e5e7eb;
+        border-radius: 8px;
+        font-size: 14px;
+        transition: all 0.2s ease;
+        
+        &:focus {
+          outline: none;
+          border-color: #01847F;
+        }
+      }
+    }
+    
+    .btn {
+      margin-top: 0;
+    }
+  }
 }
 
-.filter-bar {
-  display: flex;
-  gap: 10px;
-  padding: 18px 20px;
+// ==================== 标签筛选 ====================
+.tag-filter {
   background: #fff;
-  margin: 0 24px 20px;
+  padding: 16px;
   border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
   
-  .filter-btn {
-    flex: 1;
-    padding: 12px;
-    border: 2px solid #e5e7eb;
-    background: #fff;
-    border-radius: 10px;
-    cursor: pointer;
+  .tag-filter-title {
     font-size: 14px;
-    font-weight: 600;
-    transition: all 0.3s ease;
-    
-    &.active {
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      color: #fff;
-      border-color: transparent;
-      transform: translateY(-2px);
-      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
-    }
-    
-    &:hover:not(.active) {
-      background: #f9fafb;
-      border-color: #d1d5db;
-      transform: translateY(-1px);
-    }
+    font-weight: 700;
+    margin-bottom: 12px;
+    color: #0f172a;
   }
   
-  .filter-select {
-    padding: 12px;
-    border: 2px solid #e5e7eb;
-    border-radius: 10px;
+  .tag-filter-buttons {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+  
+  .tag-filter-btn {
+    padding: 6px 12px;
+    border: 1px solid #e5e7eb;
     background: #fff;
-    min-width: 140px;
-    font-size: 14px;
-    font-weight: 600;
+    border-radius: 16px;
     cursor: pointer;
-    transition: all 0.3s ease;
+    font-size: 13px;
+    font-weight: 600;
+    transition: all 0.2s ease;
+    color: #6b7280;
     
     &:hover {
+      background: #f9fafb;
       border-color: #d1d5db;
     }
     
-    &:focus {
-      outline: none;
-      border-color: #667eea;
-      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+    &.active {
+      background: #01847F;
+      color: #fff;
+      border-color: #01847F;
     }
   }
 }
 
-// 分析图表横向布局容器
+// ==================== 筛选区域 ====================
+.filter-section {
+  margin-bottom: 0;
+}
+
+// ==================== 分析区域 ====================
+.analysis-section {
+  margin-bottom: 0;
+}
+
 .analysis-charts-container {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 20px;
-  margin: 0 24px 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
 }
 
-.change-chart-section, .scatter-chart-section {
+.change-chart-section, 
+.scatter-chart-section {
   background: #fff;
-  border-radius: 12px;
+  border-radius: 16px;
   padding: 20px;
-  margin: 0; // 移除外边距,由容器gap控制
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
-  min-height: 400px;
-  display: flex;
-  flex-direction: column;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
   
   .chart-header {
     display: flex;
@@ -504,372 +592,279 @@
     .chart-title {
       font-size: 16px;
       font-weight: 700;
-      color: #1f2937;
+      color: #0f172a;
       margin: 0;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-      -webkit-background-clip: text;
-      -webkit-text-fill-color: transparent;
-      background-clip: text;
     }
   }
   
   .toggle-btn {
-    background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
-    border: 1px solid #d1d5db;
-    padding: 8px 16px;
+    background: #f5f5f5;
+    border: 1px solid #e5e7eb;
+    padding: 6px 12px;
     border-radius: 8px;
     cursor: pointer;
-    font-size: 13px;
+    font-size: 12px;
     font-weight: 600;
-    transition: all 0.3s ease;
+    color: #6b7280;
+    transition: all 0.2s ease;
     
     &:hover {
-      background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
-      transform: translateY(-2px);
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+      background: rgba(1, 132, 127, 0.1);
+      color: #01847F;
+      border-color: #01847F;
     }
   }
   
   .chart-medium {
     width: 100%;
-    flex: 1;
-    min-height: 320px;
+    height: 200px;
   }
 }
 
-.anomaly-section {
-  background: #fff;
-  border-radius: 12px;
+// ==================== 模态框样式 ====================
+.dialog-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 50;
   padding: 20px;
-  margin: 0 24px 20px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
-  
-  h3 {
-    font-size: 17px;
-    font-weight: 700;
-    margin: 0 0 16px 0;
-    color: #1f2937;
-  }
+}
+
+.dialog {
+  background: #fff;
+  border-radius: 16px;
+  padding: 24px;
+  max-width: 480px;
+  width: 100%;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+}
+
+.dialog h2 {
+  font-size: 18px;
+  font-weight: 700;
+  margin: 0 0 20px 0;
+  color: #0f172a;
+}
+
+.form-group {
+  margin-bottom: 16px;
   
-  .anomaly-list {
-    display: flex;
-    flex-direction: column;
-    gap: 12px;
+  label {
+    display: block;
+    font-size: 14px;
+    font-weight: 600;
+    margin-bottom: 6px;
+    color: #0f172a;
   }
   
-  .anomaly-item {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    padding: 14px 16px;
-    border-radius: 10px;
+  input, select, textarea {
+    width: 100%;
+    padding: 12px;
+    border: 1px solid #e5e7eb;
+    border-radius: 8px;
     font-size: 14px;
-    line-height: 1.6;
-    transition: all 0.3s ease;
+    transition: border-color 0.2s ease;
+    box-sizing: border-box;
     
-    &:hover {
-      transform: translateX(4px);
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    &:focus {
+      outline: none;
+      border-color: #01847F;
     }
   }
   
-  .anomaly-item.info {
-    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
-    color: #1e40af;
-    border-left: 4px solid #3b82f6;
-  }
-  
-  .anomaly-item.warning {
-    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-    color: #92400e;
-    border-left: 4px solid #f59e0b;
-  }
-  
-  .anomaly-item.danger {
-    background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
-    color: #991b1b;
-    border-left: 4px solid #ef4444;
-  }
-  
-  .anomaly-icon {
-    font-size: 20px;
-    flex-shrink: 0;
+  textarea {
+    resize: vertical;
+    min-height: 80px;
   }
 }
 
-.dialog-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 50; padding: 20px; }
-.dialog { background: #fff; border-radius: 12px; padding: 24px; max-width: 520px; width: 100%; max-height: 90vh; overflow-y: auto; }
-.dialog h2 { font-size: 18px; font-weight: bold; margin: 0 0 16px 0; }
-.form-group { margin-bottom: 14px; }
-.form-group label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; color: #374151; }
-.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; }
-.dialog-actions { display: flex; gap: 12px; margin-top: 18px; }
-
-// 标签选择器
-.tag-selector {
-  .available-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
-  .tag-btn { padding: 6px 12px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;
-    &:hover { background: #f3f4f6; }
-    &.selected { background: #3b82f6; color: #fff; border-color: #3b82f6; }
-  }
-  .selected-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
-  .selected-tag { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; background: #dbeafe; color: #1e40af; border-radius: 6px; font-size: 13px;
-    .remove-tag { background: none; border: none; color: #1e40af; font-size: 18px; cursor: pointer; padding: 0 4px; line-height: 1; }
-  }
-  .custom-tag-input { display: flex; gap: 8px; margin-top: 12px;
-    input { flex: 1; }
-    .btn-sm { padding: 6px 12px; font-size: 13px; }
-  }
-}
-
-// 自定义日期选择器
-.custom-date-picker {
-  background: #fff;
-  padding: 20px;
-  margin: 0 24px 20px;
-  border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
+.dialog-actions {
+  display: flex;
+  gap: 12px;
+  margin-top: 24px;
   
-  .date-range-inputs {
-    display: flex;
-    gap: 16px;
-    align-items: flex-end;
-    
-    .date-input-group {
-      flex: 1;
-      
-      label {
-        display: block;
-        font-size: 13px;
-        font-weight: 600;
-        margin-bottom: 8px;
-        color: #374151;
-        text-transform: uppercase;
-        letter-spacing: 0.5px;
-      }
-      
-      input {
-        width: 100%;
-        padding: 12px;
-        border: 2px solid #e5e7eb;
-        border-radius: 10px;
-        font-size: 14px;
-        transition: all 0.3s ease;
-        
-        &:hover {
-          border-color: #d1d5db;
-        }
-        
-        &:focus {
-          outline: none;
-          border-color: #667eea;
-          box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
-        }
-      }
-    }
+  .btn {
+    flex: 1;
   }
 }
 
-// 标签筛选
-.tag-filter {
-  background: #fff;
-  padding: 20px;
-  margin: 0 24px 20px;
-  border-radius: 12px;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  border: 1px solid #e5e7eb;
-  
-  .tag-filter-title {
-    font-size: 14px;
-    font-weight: 700;
-    margin-bottom: 14px;
-    color: #1f2937;
-    text-transform: uppercase;
-    letter-spacing: 0.5px;
-  }
-  
-  .tag-filter-buttons {
+// ==================== 标签选择器 ====================
+.tag-selector {
+  .available-tags {
     display: flex;
     flex-wrap: wrap;
-    gap: 10px;
+    gap: 8px;
+    margin-bottom: 12px;
   }
   
-  .tag-filter-btn {
-    padding: 8px 16px;
-    border: 2px solid #e5e7eb;
+  .tag-btn {
+    padding: 6px 12px;
+    border: 1px solid #e5e7eb;
     background: #fff;
-    border-radius: 8px;
+    border-radius: 16px;
     cursor: pointer;
     font-size: 13px;
     font-weight: 600;
-    transition: all 0.3s ease;
+    transition: all 0.2s;
+    color: #6b7280;
     
     &:hover {
       background: #f9fafb;
       border-color: #d1d5db;
-      transform: translateY(-1px);
     }
     
-    &.active {
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    &.selected {
+      background: #01847F;
       color: #fff;
-      border-color: transparent;
-      transform: translateY(-2px);
-      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+      border-color: #01847F;
     }
   }
-}
-
-@media (max-width: 1024px) {
-  .chart-section {
-    grid-template-columns: 1fr;
-    gap: 20px;
-  }
-
-  .weight-main-grid {
-    grid-template-columns: 1fr;
+  
+  .selected-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    margin-bottom: 12px;
   }
   
-  .progress-chart-container {
-    min-height: 400px;
+  .selected-tag {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    padding: 4px 8px;
+    background: rgba(1, 132, 127, 0.1);
+    color: #01847F;
+    border-radius: 12px;
+    font-size: 13px;
+    font-weight: 600;
     
-    .chart-small {
-      height: 300px;
+    .remove-tag {
+      background: none;
+      border: none;
+      color: #01847F;
+      font-size: 16px;
+      cursor: pointer;
+      padding: 0 2px;
+      line-height: 1;
+      
+      &:hover {
+        color: #016c68;
+      }
     }
   }
   
-  .analysis-charts-container {
-    grid-template-columns: 1fr;
-    gap: 16px;
-  }
-  
-  .change-chart-section, .scatter-chart-section {
-    min-height: 350px;
+  .custom-tag-input {
+    display: flex;
+    gap: 8px;
+    margin-top: 12px;
     
-    .chart-medium {
-      min-height: 280px;
+    input {
+      flex: 1;
+    }
+    
+    .btn-sm {
+      padding: 8px 16px;
+      font-size: 13px;
     }
   }
 }
 
-@media (max-width: 768px) {
-  .weight-main-grid {
-    padding: 16px;
+// ==================== 响应式设计 ====================
+@media (min-width: 768px) {
+  .composition-card {
+    gap: 32px;
   }
 
-  .kpi-strip {
-    grid-template-columns: 1fr;
+  .chart-small {
+    width: 140px;
+    height: 140px;
   }
 
-  .stats-cards {
-    grid-template-columns: 1fr;
-    gap: 12px;
-    padding: 16px;
+  .quick-actions {
+    max-width: 400px;
+    margin: 0 auto;
   }
-  
-  .stat-card {
-    padding: 16px;
+}
+
+@media (max-width: 480px) {
+  .main-content {
+    padding: 12px;
+    gap: 16px;
   }
-  
-  .chart-section {
-    grid-template-columns: 1fr;
-    padding: 0 16px;
+
+  .main-weight-card {
+    flex-direction: column;
+    text-align: center;
+    gap: 16px;
+    padding: 20px;
   }
-  
-  .progress-chart-container {
-    order: -1;
+
+  .weight-display {
+    justify-content: center;
   }
-  
-  .trend-chart-container {
-    .chart {
-      height: 300px;
-    }
+
+  .weight-value {
+    font-size: 40px;
   }
-  
-  .header {
-    flex-wrap: wrap;
-    padding: 16px;
-    
-    h1 {
-      flex: 1 0 100%;
-      text-align: center;
-      margin: 8px 0;
-      font-size: 20px;
-    }
-    
-    .back-btn {
-      order: -1;
-      flex: 0 0 auto;
-    }
-    
-    .header-actions {
-      flex: 1;
-      justify-content: center;
-      width: 100%;
-      margin-top: 12px;
-    }
+
+  .weight-unit {
+    font-size: 16px;
   }
-  
+
+  .weight-stats {
+    justify-content: center;
+    gap: 16px;
+  }
+
+  .composition-card {
+    flex-direction: column;
+    text-align: center;
+    gap: 16px;
+  }
+
+  .composition-chart {
+    flex: none;
+  }
+
+  .chart-wrapper {
+    height: 160px;
+  }
+
   .filter-bar {
-    flex-wrap: wrap;
-    padding: 14px 16px;
-    margin: 0 16px 16px;
-    
     .filter-btn {
-      flex: 1 1 calc(50% - 5px);
-      padding: 10px;
-      font-size: 13px;
+      flex: 1 1 calc(50% - 4px);
+      padding: 8px;
+      font-size: 12px;
     }
     
     .filter-select {
       flex: 1 0 100%;
       margin-top: 8px;
+      padding: 8px;
+      font-size: 12px;
     }
   }
-  
-  .change-chart-section, .scatter-chart-section {
-    margin: 0 16px 16px;
-    padding: 16px;
-    
-    .chart-medium {
-      height: 250px;
-    }
-  }
-  
-  .anomaly-section {
-    margin: 0 16px 16px;
-    padding: 16px;
-  }
-  
+
   .custom-date-picker {
-    margin: 0 16px 16px;
-    padding: 16px;
-    
     .date-range-inputs {
       flex-direction: column;
-      align-items: stretch;
+      gap: 12px;
       
-      button {
-        margin-top: 12px;
+      .date-input-group {
+        min-width: auto;
       }
     }
   }
-  
-  .tag-filter {
-    margin: 0 16px 16px;
-    padding: 16px;
-    
-    .tag-filter-buttons {
-      gap: 8px;
-    }
-    
-    .tag-filter-btn {
-      flex: 1 1 auto;
-      min-width: fit-content;
-      font-size: 12px;
-      padding: 6px 12px;
-    }
+
+  .dialog {
+    margin: 10px;
+    padding: 20px;
+    max-width: none;
+    width: calc(100% - 20px);
   }
 }

+ 61 - 86
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.ts

@@ -23,8 +23,8 @@ import { PageHeaderComponent } from '../../shared/page-header/page-header.compon
   selector: 'app-weight',
   standalone: true,
   imports: [CommonModule, FormsModule, RouterModule, ModalComponent, PageHeaderComponent],
-  templateUrl: './weight.component.html',
-  styleUrl: './weight.component.scss'
+  templateUrl: './weight-mobile.component.html',
+  styleUrl: './weight-mobile.component.scss'
 })
 export class WeightComponent implements OnInit, OnDestroy {
   private destroy$ = new Subject<void>();
@@ -169,6 +169,13 @@ export class WeightComponent implements OnInit, OnDestroy {
     this.updateCharts();
   }
 
+  /**
+   * 跳转到历史记录页面
+   */
+  goToHistory(): void {
+    this.router.navigate(['/weight/history']);
+  }
+
   /**
    * 添加记录
    */
@@ -283,29 +290,34 @@ export class WeightComponent implements OnInit, OnDestroy {
   private updateTrendChart(): void {
     if (!this.trendChart) return;
 
-    // 根据周期筛选数据
-    const now = new Date();
-    let startDate: Date;
-    
+    let daysCount = 7;
     switch (this.trendPeriod) {
-      case 'week':
-        startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
-        break;
-      case 'month':
-        startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
-        break;
-      case '6months':
-        startDate = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000);
-        break;
+      case 'week': daysCount = 7; break;
+      case 'month': daysCount = 30; break;
+      case '6months': daysCount = 180; break;
     }
 
-    const periodRecords = this.records.filter(r => {
-      const recordDate = new Date(r.date);
-      return recordDate >= startDate && recordDate <= now;
-    }).reverse(); // 从旧到新排序
+    const dates: string[] = [];
+    const weights: (number | null)[] = [];
+    const now = new Date();
 
-    // 如果数据不足,显示提示信息
-    if (periodRecords.length === 0) {
+    // 生成连续的日期序列(到今天为止)
+    for (let i = daysCount - 1; i >= 0; i--) {
+      const d = new Date();
+      d.setDate(now.getDate() - i);
+      const year = d.getFullYear();
+      const month = String(d.getMonth() + 1).padStart(2, '0');
+      const day = String(d.getDate()).padStart(2, '0');
+      const dateStr = `${year}-${month}-${day}`;
+      dates.push(dateStr);
+
+      const record = this.records.find(r => r.date === dateStr);
+      weights.push(record ? record.weight : null);
+    }
+
+    const hasData = weights.some(w => w !== null);
+
+    if (!hasData) {
       this.trendChart.setOption({
         graphic: {
           type: 'text',
@@ -319,28 +331,21 @@ export class WeightComponent implements OnInit, OnDestroy {
           }
         },
         xAxis: { show: false },
-        yAxis: { show: false }
-      });
+        yAxis: { show: false },
+        series: []
+      }, true);
       return;
     }
 
-    // 准备数据
-    const dates = periodRecords.map(r => r.date);
-    const weights = periodRecords.map(r => r.weight);
-    
-    // 基线数据(如果有目标体重)
-    const baseline = this.trendStats?.baseline || weights[0];
+    const baseline = this.goal?.targetWeight || weights.find(w => w !== null) || 0;
     const baselineData = new Array(dates.length).fill(baseline);
 
-    // X轴标签格式化
     const formatXAxis = (value: string) => {
       const date = new Date(value);
       if (this.trendPeriod === 'week') {
-        // 周视图显示周几
         const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
         return weekDays[date.getDay()];
       } else {
-        // 月/6个月视图显示日期
         return `${date.getMonth() + 1}/${date.getDate()}`;
       }
     };
@@ -354,20 +359,23 @@ export class WeightComponent implements OnInit, OnDestroy {
         textStyle: { color: '#374151' },
         formatter: (params: any) => {
           const index = params[0].dataIndex;
-          const record = periodRecords[index];
-          const changeFromBaseline = record.weight - baseline;
+          const weight = weights[index];
+          if (weight === null) return '';
+          
+          const record = this.records.find(r => r.date === dates[index]);
+          const changeFromBaseline = weight - baseline;
           return `
             <div style="padding: 10px; font-size: 13px;">
-              <div style="font-weight: bold; margin-bottom: 8px; color: #111827;">${record.date}</div>
+              <div style="font-weight: bold; margin-bottom: 8px; color: #111827;">${dates[index]}</div>
               <div style="margin-bottom: 4px;">
-                <span style="color: #a855f7;">●</span> 体重: <strong>${record.weight} kg</strong>
+                <span style="color: #a855f7;">●</span> 体重: <strong>${weight} kg</strong>
               </div>
               <div style="margin-bottom: 4px;">
                 距基线: <span style="color: ${changeFromBaseline >= 0 ? '#ef4444' : '#10b981'}">
                   ${changeFromBaseline > 0 ? '+' : ''}${changeFromBaseline.toFixed(2)} kg
                 </span>
               </div>
-              <div style="color: #6b7280;">体脂率: ${record.bodyFat}%</div>
+              ${record ? `<div style="color: #6b7280;">体脂率: ${record.bodyFat}%</div>` : ''}
             </div>
           `;
         }
@@ -383,12 +391,8 @@ export class WeightComponent implements OnInit, OnDestroy {
         type: 'category',
         data: dates,
         boundaryGap: false,
-        axisLine: {
-          lineStyle: { color: '#e5e7eb' }
-        },
-        axisTick: {
-          show: false
-        },
+        axisLine: { lineStyle: { color: '#e5e7eb' } },
+        axisTick: { show: false },
         axisLabel: {
           formatter: formatXAxis,
           fontSize: 11,
@@ -396,73 +400,44 @@ export class WeightComponent implements OnInit, OnDestroy {
         },
         splitLine: {
           show: true,
-          lineStyle: {
-            color: '#f3f4f6',
-            type: 'dashed'
-          }
+          lineStyle: { color: '#f3f4f6', type: 'dashed' }
         }
       },
       yAxis: {
         type: 'value',
-        axisLine: {
-          show: false
-        },
-        axisTick: {
-          show: false
-        },
-        axisLabel: {
-          formatter: '{value}',
-          fontSize: 11,
-          color: '#9ca3af'
-        },
-        splitLine: {
-          lineStyle: {
-            color: '#f3f4f6',
-            type: 'dashed'
-          }
-        }
+        scale: true,
+        min: (value: any) => Math.floor(value.min - 1),
+        max: (value: any) => Math.ceil(value.max + 1),
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: { formatter: '{value}', fontSize: 11, color: '#9ca3af' },
+        splitLine: { lineStyle: { color: '#f3f4f6', type: 'dashed' } }
       },
       series: [
-        // 基线
         {
           name: '基线',
           type: 'line',
           data: baselineData,
-          lineStyle: {
-            color: '#d1d5db',
-            width: 2,
-            type: 'dashed'
-          },
+          lineStyle: { color: '#d1d5db', width: 2, type: 'dashed' },
           symbol: 'none',
           silent: true
         },
-        // 体重趋势线
         {
           name: '体重',
           type: 'line',
           data: weights,
+          connectNulls: true,
           smooth: false,
-          itemStyle: { 
-            color: '#a855f7',
-            borderColor: '#fff',
-            borderWidth: 2
-          },
-          lineStyle: { 
-            width: 3,
-            color: '#a855f7'
-          },
+          itemStyle: { color: '#a855f7', borderColor: '#fff', borderWidth: 2 },
+          lineStyle: { width: 3, color: '#a855f7' },
           symbol: 'circle',
           symbolSize: 8,
           emphasis: {
-            itemStyle: {
-              borderWidth: 3,
-              shadowBlur: 10,
-              shadowColor: 'rgba(168, 85, 247, 0.5)'
-            }
+            itemStyle: { borderWidth: 3, shadowBlur: 10, shadowColor: 'rgba(168, 85, 247, 0.5)' }
           }
         }
       ]
-    });
+    }, true);
   }
 
   /**

+ 6 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.routes.ts

@@ -1,10 +1,16 @@
 import { Routes } from '@angular/router';
 import { WeightComponent } from './weight.component';
+import { WeightHistoryComponent } from './weight-history/weight-history.component';
 
 export const weightRoutes: Routes = [
   {
     path: '',
     component: WeightComponent,
     title: '体重管理 - 数智健调系统'
+  },
+  {
+    path: 'history',
+    component: WeightHistoryComponent,
+    title: '体重历史 - 数智健调系统'
   }
 ];

+ 101 - 4
campus_health_app/frontend/campus-health-app/src/app/services/weight-data.service.ts

@@ -29,15 +29,112 @@ export class WeightDataService {
   }
 
   /**
-   * 初始化数据:从 localStorage 加载并从后端同步
+   * 初始化数据:从 localStorage 加载,如果没有数据则生成模拟数据
    */
   private initializeData(): void {
-    const localData = this.getLocalData();
+    let localData = this.getLocalData();
+    
+    // 如果没有记录,自动生成模拟数据
+    if (localData.records.length === 0) {
+      localData = this.generateMockData();
+      this.saveLocalData(localData);
+    }
+    
     this.recordsSubject.next(localData.records);
     this.goalSubject.next(localData.goal);
+  }
+
+  /**
+   * 生成模拟体重数据(从2025-10-01到今天)
+   */
+  private generateMockData(): WeightLocalData {
+    const startDate = new Date('2025-10-01');
+    const endDate = new Date();
+    const startWeight = 72;
+    const targetWeight = 65;
+    const startBodyFat = 25;
+    const targetBodyFat = 18;
+    
+    const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
+    const weightPerDay = (startWeight - targetWeight) / totalDays;
+    const bodyFatPerDay = (startBodyFat - targetBodyFat) / totalDays;
+    
+    const records: WeightRecord[] = [];
+    const tags = ['开始运动', '目标调整', '饮食改变', '假期', '压力期'];
+    const conditions: Array<'fasting' | 'after_meal'> = ['fasting', 'after_meal'];
+    const hours = ['07', '08', '09', '20', '21', '22'];
+    
+    // 随机选择几天添加标签
+    const tagDates: { [key: string]: string[] } = {};
+    for (let i = 0; i < 5; i++) {
+      const randomDay = Math.floor(Math.random() * totalDays);
+      const tagDate = new Date(startDate);
+      tagDate.setDate(tagDate.getDate() + randomDay);
+      const dateStr = tagDate.toISOString().split('T')[0];
+      tagDates[dateStr] = [tags[i]];
+    }
+    
+    for (let i = 0; i < totalDays; i++) {
+      const currentDate = new Date(startDate);
+      currentDate.setDate(currentDate.getDate() + i);
+      const dateStr = currentDate.toISOString().split('T')[0];
+      
+      // 计算基准值
+      const baseWeight = startWeight - (weightPerDay * i);
+      const baseBodyFat = startBodyFat - (bodyFatPerDay * i);
+      
+      // 添加随机波动
+      const weightVariation = (Math.random() - 0.5) * 0.6;
+      const bodyFatVariation = (Math.random() - 0.5) * 0.3;
+      
+      // 周末可能会有小幅反弹
+      const dayOfWeek = currentDate.getDay();
+      const weekendBonus = (dayOfWeek === 0 || dayOfWeek === 6) ? 0.2 : 0;
+      
+      const weight = Math.round((baseWeight + weightVariation + weekendBonus) * 100) / 100;
+      const bodyFat = Math.round((baseBodyFat + bodyFatVariation) * 100) / 100;
+      const muscleMass = Math.round((28 + Math.random() * 3) * 100) / 100;
+      
+      const hour = hours[Math.floor(Math.random() * hours.length)];
+      const minute = String(Math.floor(Math.random() * 60)).padStart(2, '0');
+      
+      records.push({
+        id: `mock_${currentDate.getTime()}_${i}`,
+        date: dateStr,
+        measurementTime: `${hour}:${minute}`,
+        weight: weight,
+        bodyFat: bodyFat,
+        muscleMass: muscleMass,
+        measurementCondition: conditions[Math.floor(Math.random() * conditions.length)],
+        tags: tagDates[dateStr] || [],
+        createdAt: currentDate.getTime(),
+        syncStatus: 'synced'
+      });
+    }
+    
+    // 按日期倒序排列(最新在前)
+    records.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+    
+    // 创建目标
+    const goal: WeightGoal = {
+      id: `goal_${Date.now()}`,
+      targetWeight: targetWeight,
+      targetBodyFat: targetBodyFat,
+      targetDate: new Date(endDate.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
+      startWeight: startWeight,
+      startBodyFat: startBodyFat,
+      startDate: startDate.toISOString().split('T')[0],
+      weeklyTarget: weightPerDay * 7,
+      createdAt: Date.now(),
+      syncStatus: 'synced'
+    };
     
-    // 从后端同步最新数据(如果需要)
-    // this.syncFromBackend();
+    return {
+      records,
+      goal,
+      pendingSync: { records: [], goal: null },
+      lastSyncTime: Date.now()
+    };
   }
 
   /**

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff