Просмотр исходного кода

fix: replace mock sleep gantt with real stages; wire monitoring trend + menstrual to real data

17846405080 1 месяц назад
Родитель
Сommit
74de318801

+ 11 - 53
campus_health_app/frontend/campus-health-app/src/app/modules/dashboard/dashboard.component.html

@@ -179,77 +179,35 @@
 
 
         <!-- 睡眠时间概览 -->
         <!-- 睡眠时间概览 -->
         <div class="sleep-summary-header">
         <div class="sleep-summary-header">
-          <div class="summary-label">{{ sleepTotalLabelTitle }}</div>
+          <div class="summary-label">昨晚睡眠(来自 Apple Watch)</div>
           <div class="summary-time">
           <div class="summary-time">
-            <span class="time-value">{{ sleepTotalLabel.split('小时')[0] }}</span> 
+            <span class="time-value">{{ sleepCardData.sleepHours }}</span>
             <span class="time-unit">小时</span>
             <span class="time-unit">小时</span>
-            <span class="time-value">{{ sleepTotalLabel.split('小时')[1] || '' }}</span>
+            <span class="time-value">{{ sleepCardData.sleepMinutes }}</span>
+            <span class="time-unit">分</span>
           </div>
           </div>
-          <div class="summary-date">{{ currentSleepDateLabel }}</div>
+          <div class="summary-date">评分 {{ sleepCardData.sleepScore }}/100 · 深睡 {{ sleepCardData.deepSleepDuration }}h</div>
         </div>
         </div>
 
 
         <!-- 图表区域 -->
         <!-- 图表区域 -->
         <div class="chart-section">
         <div class="chart-section">
-          <!-- 日视图:自定义甘特图 -->
           <div class="day-chart-container" *ngIf="sleepViewMode === 'day'">
           <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="day-stats-grid">
               <div class="stat-box rem">
               <div class="stat-box rem">
-                <div class="label">快速眼动睡眠</div>
-                <div class="value">{{ sleepData.remSleepDuration }}小时 <span class="percent">16%</span></div>
+                <div class="label">快速眼动 REM</div>
+                <div class="value">{{ sleepData.remSleepDuration }} 小时</div>
               </div>
               </div>
               <div class="stat-box core">
               <div class="stat-box core">
                 <div class="label">核心睡眠</div>
                 <div class="label">核心睡眠</div>
-                <div class="value">{{ sleepData.lightSleepDuration }}小时 <span class="percent">56%</span></div>
+                <div class="value">{{ sleepData.lightSleepDuration }} 小时</div>
               </div>
               </div>
               <div class="stat-box deep">
               <div class="stat-box deep">
                 <div class="label">深度睡眠</div>
                 <div class="label">深度睡眠</div>
-                <div class="value">{{ sleepData.deepSleepDuration }}小时 <span class="percent">28%</span></div>
+                <div class="value">{{ sleepData.deepSleepDuration }} 小时</div>
               </div>
               </div>
               <div class="stat-box awake">
               <div class="stat-box awake">
-                <div class="label">清醒时间</div>
-                <div class="value">{{ sleepData.awakeTime }}分钟 <span class="percent">2%</span></div>
+                <div class="label">总睡眠</div>
+                <div class="value">{{ sleepData.totalSleepTime }} 小时</div>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>

+ 46 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts

@@ -225,6 +225,52 @@ export class MonitoringComponent implements OnInit, OnDestroy {
         }
         }
       }
       }
     });
     });
+    // 近 60 天历史:接趋势图和经期
+    this.http.get<any>('/api/health-metrics/recent?days=60').subscribe({
+      next: (res) => {
+        if (res.success && Array.isArray(res.data)) {
+          const rows = res.data;
+          // 经期:找最近一次 isMenstruating=true 的日期
+          const lastPeriod = [...rows].reverse().find((r: any) => r.isMenstruating);
+          if (lastPeriod) {
+            this.menstrualCycle.startDate = new Date(lastPeriod.metricDate);
+            // 估算周期长度:找相邻两次经期开始的间隔
+            const periodStarts: string[] = [];
+            let prev = false;
+            for (const r of rows) {
+              const cur = !!r.isMenstruating;
+              if (cur && !prev) periodStarts.push(r.metricDate);
+              prev = cur;
+            }
+            if (periodStarts.length >= 2) {
+              const d1 = new Date(periodStarts[periodStarts.length - 2]);
+              const d2 = new Date(periodStarts[periodStarts.length - 1]);
+              this.menstrualCycle.length = Math.round((d2.getTime() - d1.getTime()) / 86400000);
+            }
+            const predicted = new Date(this.menstrualCycle.startDate);
+            predicted.setDate(predicted.getDate() + this.menstrualCycle.length);
+            this.menstrualCycle.predictedNextStartDate = predicted;
+          }
+          // 趋势图:用真实静息心率数据替换心率历史
+          this.heartRateHistory = rows
+            .filter((r: any) => r.restingHeartRate != null)
+            .map((r: any) => ({
+              date: r.metricDate,
+              value: Number(r.restingHeartRate),
+              status: 'normal' as const
+            }));
+          this.bloodOxygenHistory = rows
+            .filter((r: any) => r.avgOxygenSaturation != null)
+            .map((r: any) => ({
+              date: r.metricDate,
+              value: Number(r.avgOxygenSaturation),
+              status: 'normal' as const
+            }));
+          // 重绘趋势图
+          setTimeout(() => this.initializeTrendChart(), 100);
+        }
+      }
+    });
   }
   }
   
   
   ngOnDestroy(): void {
   ngOnDestroy(): void {