Browse Source

feat: 初始化数智健调系统后端项目结构

- 添加后端项目的基本文件结构,包括 TypeScript 配置、数据库脚本和环境变量管理
- 创建 RESTful API 相关的控制器、服务和路由
- 实现体重管理模块的核心功能,包括体重记录、目标管理和标签管理
- 配置中间件以处理请求日志和错误处理
- 添加数据库初始化和种子数据脚本,确保开发环境的快速搭建
- 更新 README 文档,提供项目概述和使用说明
17846405080 2 months ago
parent
commit
4804317072
36 changed files with 756 additions and 248 deletions
  1. 1 0
      campus_health_app/backend/.gitignore
  2. 1 0
      campus_health_app/backend/database/README.md
  3. 1 0
      campus_health_app/backend/database/init.sql
  4. 1 0
      campus_health_app/backend/database/schema.sql
  5. 1 0
      campus_health_app/backend/database/seed.sql
  6. 1 0
      campus_health_app/backend/package.json
  7. 0 67
      campus_health_app/backend/src/config/database.ts
  8. 11 0
      campus_health_app/backend/src/controllers/WeightController.ts
  9. 11 0
      campus_health_app/backend/src/dto/tag.dto.ts
  10. 11 0
      campus_health_app/backend/src/dto/weight-goal.dto.ts
  11. 11 0
      campus_health_app/backend/src/dto/weight-record.dto.ts
  12. 11 0
      campus_health_app/backend/src/entities/AnomalyLog.ts
  13. 11 0
      campus_health_app/backend/src/entities/Tag.ts
  14. 11 0
      campus_health_app/backend/src/entities/User.ts
  15. 11 0
      campus_health_app/backend/src/entities/WeightGoal.ts
  16. 11 0
      campus_health_app/backend/src/entities/WeightRecord.ts
  17. 11 0
      campus_health_app/backend/src/entities/WeightRecordTag.ts
  18. 11 0
      campus_health_app/backend/src/middlewares/auth.middleware.ts
  19. 11 0
      campus_health_app/backend/src/middlewares/error.middleware.ts
  20. 11 0
      campus_health_app/backend/src/middlewares/logger.middleware.ts
  21. 11 0
      campus_health_app/backend/src/routes/index.ts
  22. 11 0
      campus_health_app/backend/src/routes/weight.routes.ts
  23. 11 0
      campus_health_app/backend/src/server.ts
  24. 11 0
      campus_health_app/backend/src/services/StatsService.ts
  25. 11 0
      campus_health_app/backend/src/services/TagService.ts
  26. 11 0
      campus_health_app/backend/src/services/WeightGoalService.ts
  27. 11 0
      campus_health_app/backend/src/services/WeightRecordService.ts
  28. 1 0
      campus_health_app/backend/tsconfig.json
  29. 16 19
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.html
  30. 45 0
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.scss
  31. 53 7
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.ts
  32. 7 6
      campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise.component.scss
  33. 182 136
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html
  34. 0 0
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.scss
  35. 209 1
      campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts
  36. 17 12
      campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.ts

+ 1 - 0
campus_health_app/backend/.gitignore

@@ -39,3 +39,4 @@ tmp/
 temp/
 *.tmp
 
+

+ 1 - 0
campus_health_app/backend/database/README.md

@@ -353,3 +353,4 @@ ERROR 1062: Duplicate entry
 
 如有问题或建议,请联系开发团队。
 
+

+ 1 - 0
campus_health_app/backend/database/init.sql

@@ -24,3 +24,4 @@ SELECT
   @@collation_database AS collation,
   NOW() AS created_at;
 
+

+ 1 - 0
campus_health_app/backend/database/schema.sql

@@ -178,3 +178,4 @@ GROUP BY u.id, u.nickname;
 -- 表结构创建完成
 -- ========================================
 
+

+ 1 - 0
campus_health_app/backend/database/seed.sql

@@ -198,3 +198,4 @@ WHERE user_id = 'test-user-001'
 ORDER BY record_date DESC
 LIMIT 10;
 
+

+ 1 - 0
campus_health_app/backend/package.json

@@ -61,3 +61,4 @@
   }
 }
 
+

+ 0 - 67
campus_health_app/backend/src/config/database.ts

@@ -1,67 +0,0 @@
-import 'reflect-metadata';
-import { DataSource } from 'typeorm';
-import * as dotenv from 'dotenv';
-import * as path from 'path';
-
-// 加载环境变量
-dotenv.config();
-
-// 实体导入
-import { User } from '../entities/User';
-import { WeightRecord } from '../entities/WeightRecord';
-import { WeightGoal } from '../entities/WeightGoal';
-import { Tag } from '../entities/Tag';
-import { WeightRecordTag } from '../entities/WeightRecordTag';
-import { AnomalyLog } from '../entities/AnomalyLog';
-
-export const AppDataSource = new DataSource({
-  type: 'mysql',
-  host: process.env.DB_HOST || 'localhost',
-  port: parseInt(process.env.DB_PORT || '3306'),
-  username: process.env.DB_USERNAME || 'root',
-  password: process.env.DB_PASSWORD || '',
-  database: process.env.DB_DATABASE || 'campus_health',
-  synchronize: process.env.DB_SYNCHRONIZE === 'true',
-  logging: process.env.DB_LOGGING === 'true',
-  entities: [
-    User,
-    WeightRecord,
-    WeightGoal,
-    Tag,
-    WeightRecordTag,
-    AnomalyLog
-  ],
-  migrations: [path.join(__dirname, '../migrations/*.ts')],
-  subscribers: [],
-  charset: 'utf8mb4',
-  timezone: '+08:00',
-  extra: {
-    connectionLimit: 10
-  }
-});
-
-/**
- * 初始化数据库连接
- */
-export const initializeDatabase = async (): Promise<void> => {
-  try {
-    await AppDataSource.initialize();
-    console.log('✅ 数据库连接成功');
-  } catch (error) {
-    console.error('❌ 数据库连接失败:', error);
-    process.exit(1);
-  }
-};
-
-/**
- * 关闭数据库连接
- */
-export const closeDatabase = async (): Promise<void> => {
-  try {
-    await AppDataSource.destroy();
-    console.log('✅ 数据库连接已关闭');
-  } catch (error) {
-    console.error('❌ 关闭数据库连接失败:', error);
-  }
-};
-

+ 11 - 0
campus_health_app/backend/src/controllers/WeightController.ts

@@ -364,3 +364,14 @@ export class WeightController {
   };
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/dto/tag.dto.ts

@@ -15,3 +15,14 @@ export class CreateTagDto {
   color?: string;
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/dto/weight-goal.dto.ts

@@ -40,3 +40,14 @@ export class UpdateWeightGoalDto {
   targetDate?: string;
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/dto/weight-record.dto.ts

@@ -111,3 +111,14 @@ export class WeightRecordQueryDto {
   limit?: number;
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/entities/AnomalyLog.ts

@@ -69,3 +69,14 @@ export class AnomalyLog {
   user!: User;
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/entities/Tag.ts

@@ -56,3 +56,14 @@ export class Tag {
   weightRecordTags!: WeightRecordTag[];
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/entities/User.ts

@@ -58,3 +58,14 @@ export class User {
   anomalyLogs!: AnomalyLog[];
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/entities/WeightGoal.ts

@@ -76,3 +76,14 @@ export class WeightGoal {
   user!: User;
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/entities/WeightRecord.ts

@@ -74,3 +74,14 @@ export class WeightRecord {
   weightRecordTags!: WeightRecordTag[];
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/entities/WeightRecordTag.ts

@@ -41,3 +41,14 @@ export class WeightRecordTag {
   tag!: Tag;
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/middlewares/auth.middleware.ts

@@ -30,3 +30,14 @@ declare global {
   }
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/middlewares/error.middleware.ts

@@ -36,3 +36,14 @@ export const notFoundMiddleware = (
   });
 };
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/middlewares/logger.middleware.ts

@@ -23,3 +23,14 @@ export const loggerMiddleware = (
   next();
 };
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/routes/index.ts

@@ -17,3 +17,14 @@ router.get('/health', (req, res) => {
 
 export default router;
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/routes/weight.routes.ts

@@ -56,3 +56,14 @@ router.get('/stats', weightController.getStats);
 
 export default router;
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/server.ts

@@ -116,3 +116,14 @@ startServer();
 
 export default app;
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/services/StatsService.ts

@@ -98,3 +98,14 @@ export class StatsService {
   }
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/services/TagService.ts

@@ -72,3 +72,14 @@ export class TagService {
   }
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/services/WeightGoalService.ts

@@ -125,3 +125,14 @@ export class WeightGoalService {
   }
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
campus_health_app/backend/src/services/WeightRecordService.ts

@@ -231,3 +231,14 @@ export class WeightRecordService {
   }
 }
 
+
+
+
+
+
+
+
+
+
+
+

+ 1 - 0
campus_health_app/backend/tsconfig.json

@@ -45,3 +45,4 @@
   ]
 }
 
+

+ 16 - 19
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.html

@@ -7,25 +7,22 @@
   <main class="add-exercise-main">
     <form class="exercise-form" [formGroup]="exerciseForm" (ngSubmit)="onSubmit()">
       <!-- 日期和时间选择 -->
-      <div class="form-group">
-        <div class="form-row">
-          <div class="form-col">
-            <label for="date">日期</label>
-            <input
-              type="date"
-              id="date"
-              formControlName="date"
-              class="form-control"
-            />
-          </div>
-          <div class="form-col">
-            <label for="time">时间</label>
-            <input
-              type="time"
-              id="time"
-              formControlName="time"
-              class="form-control"
-            />
+      <div class="form-group datetime-wrapper">
+        <label for="exerciseDateTime">日期和时间 <span class="required">*</span></label>
+        <div class="datetime-input-container">
+          <input
+            type="datetime-local"
+            #dateTimeInput
+            id="exerciseDateTime"
+            [(ngModel)]="exerciseDateTimeString"
+            name="exerciseDateTime"
+            [ngModelOptions]="{standalone: true}"
+            (ngModelChange)="onDateTimeChange($event)"
+            class="form-control datetime-picker-native"
+            required
+          >
+          <div class="datetime-display" (click)="dateTimeInput.showPicker()">
+            {{ getFormattedDateTime() }}
           </div>
         </div>
       </div>

+ 45 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.scss

@@ -100,6 +100,51 @@ textarea.form-control {
   min-height: 80px;
 }
 
+/* 日期时间选择器样式 */
+.datetime-wrapper {
+  position: relative;
+}
+
+.datetime-input-container {
+  position: relative;
+}
+
+.datetime-picker-native {
+  opacity: 0;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 2;
+  cursor: pointer;
+}
+
+.datetime-display {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid #dddddd;
+  border-radius: 4px;
+  font-size: 14px;
+  background-color: #ffffff;
+  cursor: pointer;
+  transition: border-color 0.2s;
+  display: flex;
+  align-items: center;
+  min-height: 40px;
+  position: relative;
+  z-index: 1;
+}
+
+.datetime-display:hover {
+  border-color: #3498db;
+}
+
+.datetime-picker-native:focus + .datetime-display {
+  border-color: #3498db;
+  outline: none;
+}
+
 /* 表单按钮样式 */
 .form-actions {
   display: flex;

+ 53 - 7
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/add-exercise/add-exercise.component.ts

@@ -1,7 +1,7 @@
 import { Component } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router, RouterModule } from '@angular/router';
-import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
 
 // 运动方式类型
 interface ExerciseType {
@@ -20,7 +20,7 @@ interface ExerciseEquipment {
 @Component({
   selector: 'app-add-exercise',
   standalone: true,
-  imports: [CommonModule, RouterModule, ReactiveFormsModule],
+  imports: [CommonModule, RouterModule, ReactiveFormsModule, FormsModule],
   templateUrl: './add-exercise.component.html',
   styleUrl: './add-exercise.component.scss'
 })
@@ -59,6 +59,12 @@ export class AddExerciseComponent {
   // 表单提交状态
   isSubmitting: boolean = false;
   
+  // 日期时间字符串(用于datetime-local输入框)
+  exerciseDateTimeString: string = '';
+  
+  // 实际的日期对象
+  exerciseDateTime: Date = new Date();
+  
   constructor(
     private router: Router,
     private formBuilder: FormBuilder
@@ -71,10 +77,12 @@ export class AddExerciseComponent {
       equipment: [''],
       sets: ['', [Validators.min(1)]],
       reps: ['', [Validators.min(1)]],
-      description: [''],
-      date: [new Date(), Validators.required],
-      time: ['', Validators.required]
+      description: ['']
     });
+    
+    // 初始化日期时间
+    this.exerciseDateTime = new Date();
+    this.exerciseDateTimeString = this.formatDateTimeForInput(this.exerciseDateTime);
   }
   
   // 返回运动记录页面
@@ -84,7 +92,8 @@ export class AddExerciseComponent {
   
   // 表单提交
   onSubmit(): void {
-    if (this.exerciseForm.invalid) {
+    if (this.exerciseForm.invalid || !this.exerciseDateTime) {
+      alert('请填写所有必填项');
       return;
     }
     
@@ -92,8 +101,14 @@ export class AddExerciseComponent {
     
     // 模拟表单提交
     setTimeout(() => {
+      // 合并表单数据和日期时间
+      const exerciseData = {
+        ...this.exerciseForm.value,
+        dateTime: this.exerciseDateTime
+      };
+      
       // 这里应该是实际的表单提交逻辑
-      console.log('运动记录提交成功:', this.exerciseForm.value);
+      console.log('运动记录提交成功:', exerciseData);
       
       // 提交成功后返回运动记录页面
       this.router.navigate(['/exercise']);
@@ -125,4 +140,35 @@ export class AddExerciseComponent {
   getEquipmentByCategory(category: string): ExerciseEquipment[] {
     return this.exerciseEquipment.filter(equipment => equipment.category === category);
   }
+  
+  // 将Date对象转换为datetime-local输入框所需的格式 (yyyy-MM-ddTHH:mm)
+  formatDateTimeForInput(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    return `${year}-${month}-${day}T${hours}:${minutes}`;
+  }
+  
+  // 获取格式化的日期时间(中文格式显示)
+  getFormattedDateTime(): string {
+    if (!this.exerciseDateTime) {
+      return 'yyyy年mm月dd日 HH:mm';
+    }
+    const date = this.exerciseDateTime;
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    return `${year}年${month}月${day}日 ${hours}:${minutes}`;
+  }
+  
+  // 当日期时间改变时更新exerciseDateTime
+  onDateTimeChange(dateTimeString: string): void {
+    if (dateTimeString) {
+      this.exerciseDateTime = new Date(dateTimeString);
+    }
+  }
 }

+ 7 - 6
campus_health_app/frontend/campus-health-app/src/app/modules/exercise/exercise.component.scss

@@ -9,16 +9,17 @@
 }
 
 // 头部导航
-.exercise-header {
+.exercise-container > .exercise-header {
   display: flex;
   align-items: center;
   padding: 1.5rem 2rem;
   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: (135deg, #667eea 0%, #764ba2 100%);
+  color: white;
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+  margin-bottom: 0;
 }
 
-.back-button {
+.exercise-container > .exercise-header .back-button {
   background: rgba(255, 255, 255, 0.1);
   border: 1px solid rgba(255, 255, 255, 0.2);
   border-radius: 12px;
@@ -37,7 +38,7 @@
   }
 }
 
-.exercise-header h1 {
+.exercise-container > .exercise-header h1 {
   margin: 0;
   font-size: 1.8rem;
   color: white;
@@ -264,7 +265,7 @@
   box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 }
 
-.exercise-header {
+.exercise-card .exercise-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -274,7 +275,7 @@
   padding: 0;
 }
 
-.exercise-header h3 {
+.exercise-card .exercise-header h3 {
   margin: 0;
   font-size: 1.1rem;
   color: #202124;

+ 182 - 136
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.html

@@ -6,155 +6,91 @@
 
   <main class="main-content">
     <div class="monitoring-content">
-      <!-- 健康指标概览 -->
-      <section class="health-overview">
-        <div class="section-header">
-          <h2>健康指标概览</h2>
-          <button class="add-record-btn" (click)="openRecordModal('heartRate')">+ 记录数据</button>
-        </div>
-        <div class="health-cards">
-          <!-- 心率监测卡片 -->
-          <div class="health-card" (click)="openDetailModal('heartRate')">
-            <div class="card-header">
-              <h3>心率</h3>
-              <span class="status-indicator" [ngClass]="heartRate.status" title="{{ getStatusText(heartRate.status) }}"></span>
-              <span class="last-updated">最后更新: {{ formatDate(heartRate.timestamp) }}</span>
-            </div>
-            <div class="card-content">
-              <div class="metric-value {{ getStatusClass(heartRate.status) }}">
-                {{ heartRate.value }}<span class="metric-unit">{{ heartRate.unit }}</span>
-                <span class="trend-icon">{{ getTrendIcon(heartRate.trend) }}</span>
+      <!-- 综合健康状态卡片 - 重新设计 -->
+      <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="health-progress">
-                <div class="progress-bar">
-                  <div class="progress-fill {{ getStatusClass(heartRate.status) }}" style="width: 60%;"></div>
-                </div>
-                <div class="progress-range">
-                  <span>60</span>
-                  <span>100</span>
-                </div>
+              <div class="status-badge-large good">
+                <span class="status-icon">✓</span>
+                <span class="status-text">良好</span>
               </div>
-              <p class="health-advice">{{ getHealthAdvice('heartRate') }}</p>
-            </div>
-            <div class="card-actions">
-              <button class="card-button primary" (click)="$event.stopPropagation(); openRecordModal('heartRate')">记录</button>
-              <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('heartRate')">详情</button>
             </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="health-card" (click)="openDetailModal('bloodPressure')">
-            <div class="card-header">
-              <h3>血压</h3>
-              <span class="status-indicator" [ngClass]="bloodPressure.systolic.status" title="{{ getStatusText(bloodPressure.systolic.status) }}"></span>
-              <span class="last-updated">最后更新: {{ formatDate(bloodPressure.systolic.timestamp) }}</span>
-            </div>
-            <div class="card-content">
-              <div class="blood-pressure-values">
-                <div class="bp-value systolic {{ getStatusClass(bloodPressure.systolic.status) }}">
-                  {{ bloodPressure.systolic.value }}
+          
+          <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>
-                <span class="bp-separator">/</span>
-                <div class="bp-value diastolic {{ getStatusClass(bloodPressure.diastolic.status) }}">
-                  {{ bloodPressure.diastolic.value }}
+                <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>
-                <span class="metric-unit">{{ bloodPressure.systolic.unit }}</span>
               </div>
-              <p class="health-advice">{{ getHealthAdvice('bloodPressure') }}</p>
-            </div>
-            <div class="card-actions">
-              <button class="card-button primary" (click)="$event.stopPropagation(); openRecordModal('bloodPressure')">记录</button>
-              <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('bloodPressure')">详情</button>
-            </div>
-          </div>
-
-          <!-- 血氧监测卡片 -->
-          <div class="health-card" (click)="openDetailModal('bloodOxygen')">
-            <div class="card-header">
-              <h3>血氧</h3>
-              <span class="status-indicator" [ngClass]="bloodOxygen.status" title="{{ getStatusText(bloodOxygen.status) }}"></span>
-              <span class="last-updated">最后更新: {{ formatDate(bloodOxygen.timestamp) }}</span>
-            </div>
-            <div class="card-content">
-              <div class="metric-value {{ getStatusClass(bloodOxygen.status) }}">
-                {{ bloodOxygen.value }}<span class="metric-unit">{{ bloodOxygen.unit }}</span>
-                <span class="trend-icon">{{ getTrendIcon(bloodOxygen.trend) }}</span>
+              
+              <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="health-progress">
-                <div class="progress-bar">
-                  <div class="progress-fill {{ getStatusClass(bloodOxygen.status) }}" style="width: 90%;"></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="progress-range">
-                  <span>90</span>
-                  <span>100</span>
+                <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>
-              <p class="health-advice">{{ getHealthAdvice('bloodOxygen') }}</p>
-            </div>
-            <div class="card-actions">
-              <button class="card-button primary" (click)="$event.stopPropagation(); openRecordModal('bloodOxygen')">记录</button>
-              <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('bloodOxygen')">详情</button>
             </div>
           </div>
         </div>
       </section>
 
-      <!-- 综合健康状态卡片 -->
-      <section class="health-status-section">
-        <div class="health-status-card">
-          <div class="health-status-header">
-            <h3>综合健康状态</h3>
-            <div class="overall-status good">良好</div>
-          </div>
-          <p class="status-description">您的整体健康状况良好,心率、血压和血氧指标均在正常范围内。建议继续保持当前的生活方式和健康习惯。</p>
-          <div class="key-metrics-summary">
-            <div class="key-metric-item">
-              <span class="metric-label">健康评分:</span>
-              <span class="metric-value">85/100</span>
-            </div>
-            <div class="key-metric-item">
-              <span class="metric-label">心率:</span>
-              <span class="metric-value">{{ heartRate.value }} {{ heartRate.unit }}</span>
-            </div>
-            <div class="key-metric-item">
-              <span class="metric-label">血压:</span>
-              <span class="metric-value">{{ bloodPressure.systolic.value }}/{{ bloodPressure.diastolic.value }}</span>
-            </div>
-            <div class="key-metric-item">
-              <span class="metric-label">血氧:</span>
-              <span class="metric-value">{{ bloodOxygen.value }}%</span>
-            </div>
-          </div>
-          <div class="card-actions">
-            <button class="card-button secondary" (click)="openDetailModal('comprehensive')">查看综合报告</button>
-          </div>
-        </div>
-      </section>
-
-      <!-- 经期监测卡片 (仅女性用户显示) -->
-      @if (isFemaleUser) {
-        <div class="health-card" (click)="openDetailModal('menstrualCycle')">
-          <div class="card-header">
-            <h3>经期监测</h3>
-            <span class="status-indicator normal" title="规律"></span>
-          </div>
-          <div class="card-content">
-            <div class="menstrual-info">
-              <p><strong>上次经期:</strong> {{ formatDate(menstrualCycle.startDate) }} - {{ formatDate(menstrualCycle.endDate) }}</p>
-              <p><strong>预计下次经期:</strong> {{ formatDate(menstrualCycle.predictedNextStartDate || today) }}</p>
-              <p><strong>距离下次经期:</strong> {{ getDaysUntilNextPeriod() }} 天</p>
-              @if (menstrualCycle.symptoms && menstrualCycle.symptoms.length > 0) {
-                <p><strong>常见症状:</strong> {{ menstrualCycle.symptoms.join('、') }}</p>
-              }
-            </div>
-            <p class="health-advice">{{ getHealthAdvice('menstrualCycle') }}</p>
-          </div>
-          <div class="card-actions">
-            <button class="card-button secondary" (click)="$event.stopPropagation(); openDetailModal('menstrualCycle')">详情</button>
-          </div>
-        </div>
-      }
-
       <!-- 数据可视化区域 -->
       <section class="data-visualization">
         <div class="section-header">
@@ -297,6 +233,111 @@
         </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>
+              <div class="status-item">
+                <span class="status-label">周期长度</span>
+                <span class="status-value">{{ menstrualCycle.length }} 天</span>
+              </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>
+          </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>
   </main>
 </div>
@@ -339,16 +380,21 @@
           <span class="unit">mmHg</span>
         </div>
         
-        <div class="form-group">
+        <div class="form-group datetime-wrapper">
           <label for="recordDate">记录时间:</label>
           <input 
             type="datetime-local" 
             id="recordDate" 
-            [(ngModel)]="recordForm.date" 
+            [(ngModel)]="recordFormDateTimeString" 
+            (ngModelChange)="onRecordDateChange($event)"
             name="recordDate"
-            format="yyyy年MM月dd日 HH:mm"
             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">

File diff suppressed because it is too large
+ 0 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.scss


+ 209 - 1
campus_health_app/frontend/campus-health-app/src/app/modules/monitoring/monitoring.component.ts

@@ -42,6 +42,20 @@ export interface MenstrualCycle {
   symptoms?: string[];
 }
 
+// 经期阶段接口
+export interface MenstrualPhase {
+  name: string;
+  icon: string;
+  startDay: number;
+  endDay: number;
+  duration: number;
+  startDate: Date;
+  endDate: Date;
+  exerciseAdvice: string[];
+  healthAdvice: string[];
+  symptoms?: string[];
+}
+
 // 慢性病监测数据接口
 export interface ChronicCondition {
   name: string;
@@ -126,6 +140,9 @@ export class MonitoringComponent implements OnInit {
   // 是否为女性用户 (用于显示经期监测)
   isFemaleUser: boolean = true;
 
+  // 经期阶段数据
+  cyclePhases: MenstrualPhase[] = [];
+
   // 图表实例
   heartRateChart: Chart | null = null;
   bloodPressureChart: Chart | null = null;
@@ -154,11 +171,15 @@ export class MonitoringComponent implements OnInit {
   showRecordModal: boolean = false;
   showDetailModal: boolean = false;
   selectedMetric: string = 'heartRate';
+  
+  // 日期时间字符串(用于datetime-local输入框)
+  recordFormDateTimeString: string = '';
 
   constructor(private router: Router) {}
 
   ngOnInit(): void {
     this.generateMockHistoryData();
+    this.initializeCyclePhases();
     // 延迟初始化图表,确保DOM已加载
     setTimeout(() => {
       this.initializeCharts();
@@ -474,12 +495,15 @@ export class MonitoringComponent implements OnInit {
 
   // 打开数据记录模态框
   openRecordModal(type: 'heartRate' | 'bloodPressure' | 'bloodOxygen'): void {
+    const now = new Date();
     this.recordForm = {
       type,
       value: 0,
       diastolic: type === 'bloodPressure' ? 0 : undefined,
-      date: new Date()
+      date: now
     };
+    // 初始化datetime-local输入框的值
+    this.recordFormDateTimeString = this.formatDateTimeForInput(now);
     this.showRecordModal = true;
   }
 
@@ -487,6 +511,37 @@ export class MonitoringComponent implements OnInit {
   closeRecordModal(): void {
     this.showRecordModal = false;
   }
+  
+  // 将Date对象转换为datetime-local输入框所需的格式 (yyyy-MM-ddTHH:mm)
+  formatDateTimeForInput(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    return `${year}-${month}-${day}T${hours}:${minutes}`;
+  }
+  
+  // 获取格式化的记录日期(中文格式显示)
+  getFormattedRecordDate(): string {
+    if (!this.recordForm.date) {
+      return 'yyyy年mm月dd日 HH:mm';
+    }
+    const date = this.recordForm.date;
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    return `${year}年${month}月${day}日 ${hours}:${minutes}`;
+  }
+  
+  // 当记录日期改变时更新recordForm.date
+  onRecordDateChange(dateTimeString: string): void {
+    if (dateTimeString) {
+      this.recordForm.date = new Date(dateTimeString);
+    }
+  }
 
   // 提交数据记录
   submitRecord(): void {
@@ -568,6 +623,11 @@ export class MonitoringComponent implements OnInit {
     this.selectedMetric = metric;
     this.showDetailModal = true;
   }
+  
+  // 打开指标模态框(点击卡片时)
+  openMetricModal(metric: string): void {
+    this.openDetailModal(metric);
+  }
 
   // 关闭详情模态框
   closeDetailModal(): void {
@@ -743,4 +803,152 @@ export class MonitoringComponent implements OnInit {
       default: return '未知';
     }
   }
+
+  // 初始化经期阶段数据
+  initializeCyclePhases(): void {
+    if (!this.menstrualCycle.predictedNextStartDate) {
+      return;
+    }
+
+    const cycleStartDate = new Date(this.menstrualCycle.predictedNextStartDate);
+    cycleStartDate.setDate(cycleStartDate.getDate() - this.menstrualCycle.length);
+
+    // 月经期 (1-5天)
+    const menstruationStart = new Date(cycleStartDate);
+    const menstruationEnd = new Date(cycleStartDate);
+    menstruationEnd.setDate(menstruationEnd.getDate() + 4);
+
+    // 卵泡期 (6-13天)
+    const follicularStart = new Date(cycleStartDate);
+    follicularStart.setDate(follicularStart.getDate() + 5);
+    const follicularEnd = new Date(cycleStartDate);
+    follicularEnd.setDate(follicularEnd.getDate() + 12);
+
+    // 排卵期 (14-16天)
+    const ovulationStart = new Date(cycleStartDate);
+    ovulationStart.setDate(ovulationStart.getDate() + 13);
+    const ovulationEnd = new Date(cycleStartDate);
+    ovulationEnd.setDate(ovulationEnd.getDate() + 15);
+
+    // 黄体期 (17-28天)
+    const lutealStart = new Date(cycleStartDate);
+    lutealStart.setDate(lutealStart.getDate() + 16);
+    const lutealEnd = new Date(cycleStartDate);
+    lutealEnd.setDate(lutealEnd.getDate() + this.menstrualCycle.length - 1);
+
+    this.cyclePhases = [
+      {
+        name: '月经期',
+        icon: '🌙',
+        startDay: 1,
+        endDay: 5,
+        duration: 5,
+        startDate: menstruationStart,
+        endDate: menstruationEnd,
+        exerciseAdvice: [
+          '避免剧烈运动,可进行轻度瑜伽或散步',
+          '推荐进行轻柔的拉伸运动',
+          '避免腹部剧烈运动和倒立动作',
+          '根据身体状况调整运动强度'
+        ],
+        healthAdvice: [
+          '注意保暖,避免受凉',
+          '多喝温水,补充铁元素',
+          '保证充足睡眠,避免熬夜',
+          '饮食清淡,避免生冷辛辣食物',
+          '可以适当补充红枣、枸杞等温补食材'
+        ],
+        symptoms: ['腹痛', '腰酸', '疲劳', '情绪波动']
+      },
+      {
+        name: '卵泡期',
+        icon: '🌱',
+        startDay: 6,
+        endDay: 13,
+        duration: 8,
+        startDate: follicularStart,
+        endDate: follicularEnd,
+        exerciseAdvice: [
+          '适合进行中高强度有氧运动',
+          '可进行跑步、游泳、骑行等运动',
+          '推荐进行力量训练,提升肌肉力量',
+          '这是运动减脂的黄金期'
+        ],
+        healthAdvice: [
+          '能量充沛,适合学习和工作',
+          '均衡饮食,多摄入蛋白质和新鲜蔬果',
+          '保持积极心态,适当社交活动',
+          '可以尝试新的运动项目',
+          '注意补充钙质和维生素D'
+        ],
+        symptoms: []
+      },
+      {
+        name: '排卵期',
+        icon: '✨',
+        startDay: 14,
+        endDay: 16,
+        duration: 3,
+        startDate: ovulationStart,
+        endDate: ovulationEnd,
+        exerciseAdvice: [
+          '体能达到峰值,适合高强度训练',
+          '可进行HIIT高强度间歇训练',
+          '推荐团队运动,如篮球、排球',
+          '注意运动后及时补充水分和营养'
+        ],
+        healthAdvice: [
+          '精力旺盛,工作效率高',
+          '注意个人卫生,预防感染',
+          '多喝水,促进新陈代谢',
+          '保持愉悦心情,适度社交',
+          '饮食以清淡为主,多吃蔬菜水果'
+        ],
+        symptoms: ['腹部轻微胀痛', '体温略升高', '分泌物增多']
+      },
+      {
+        name: '黄体期',
+        icon: '🍂',
+        startDay: 17,
+        endDay: 28,
+        duration: 12,
+        startDate: lutealStart,
+        endDate: lutealEnd,
+        exerciseAdvice: [
+          '适合中低强度有氧运动',
+          '推荐瑜伽、普拉提等舒缓运动',
+          '避免过度疲劳,注意休息',
+          '可进行游泳、慢跑等温和运动'
+        ],
+        healthAdvice: [
+          '注意情绪管理,保持心情愉悦',
+          '减少盐分摄入,预防水肿',
+          '避免高糖高脂食物',
+          '保证充足睡眠,早睡早起',
+          '适当补充维生素B6,缓解经前症状',
+          '可以喝玫瑰花茶舒缓情绪'
+        ],
+        symptoms: ['乳房胀痛', '情绪烦躁', '轻微水肿', '食欲变化']
+      }
+    ];
+  }
+
+  // 获取当前所处的经期阶段
+  getCurrentPhase(): string {
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+
+    for (const phase of this.cyclePhases) {
+      const phaseStart = new Date(phase.startDate);
+      phaseStart.setHours(0, 0, 0, 0);
+      const phaseEnd = new Date(phase.endDate);
+      phaseEnd.setHours(23, 59, 59, 999);
+
+      if (today >= phaseStart && today <= phaseEnd) {
+        return phase.name;
+      }
+    }
+
+    return '未知阶段';
+  }
 }

+ 17 - 12
campus_health_app/frontend/campus-health-app/src/app/modules/weight/weight.component.ts

@@ -649,18 +649,23 @@ export class WeightComponent implements OnInit, OnDestroy {
       });
 
       // 计算每月变化
-      const monthKeys = Object.keys(months).sort().slice(-6); // 最近6个月
-      monthKeys.forEach((monthKey, index) => {
-        if (index > 0) {
-          const prevMonth = monthKeys[index - 1];
-          const prevAvg = this.average(months[prevMonth].map(r => r.weight));
-          const currAvg = this.average(months[monthKey].map(r => r.weight));
-          
-          const [, month] = monthKey.split('-');
-          labels.push(`${month}月`);
-          values.push(currAvg - prevAvg);
-        }
-      });
+      const monthKeys = Object.keys(months).sort(); // 先获取所有月份并排序
+      if (monthKeys.length >= 2) {
+        // 确保有足够的数据点来计算变化
+        const recentMonthKeys = monthKeys.slice(-6); // 最近6个月
+        recentMonthKeys.forEach((monthKey, index) => {
+          if (index > 0) {
+            const prevMonth = recentMonthKeys[index - 1];
+            const prevAvg = this.average(months[prevMonth].map(r => r.weight));
+            const currAvg = this.average(months[monthKey].map(r => r.weight));
+            
+            // 使用更完整的月份标签格式
+            const [year, month] = monthKey.split('-');
+            labels.push(`${year}年${month}月`);
+            values.push(currAvg - prevAvg);
+          }
+        });
+      }
     }
 
     return { labels, values };

Some files were not shown because too many files changed in this diff