Преглед изворни кода

feat: dedicated training-log page, remove inline form from action cards

17846405080 пре 1 месец
родитељ
комит
27e0b56a74

+ 4 - 0
campus_health_app/frontend/campus-health-app/src/app/app.routes.ts

@@ -46,6 +46,10 @@ export const routes: Routes = [
     path: 'health-journal',
     loadComponent: () => import('./modules/health-journal/health-journal.component').then(m => m.HealthJournalComponent)
   },
+  {
+    path: 'training-log',
+    loadComponent: () => import('./modules/training-log/training-log.component').then(m => m.TrainingLogComponent)
+  },
   {
     path: '',
     redirectTo: 'auth/login',

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

@@ -42,21 +42,10 @@
             <span>🏃</span><span>Keep</span>
           </button>
         </div>
-        <button *ngIf="!logForm[action.id]" (click)="startLog(action.id, action.name)"
+        <button (click)="router.navigate(['/training-log'], { queryParams: { exercise: action.name } })"
                 style="flex-basis:100%;margin-top:8px;padding:8px;background:#e6fffa;color:#0d9488;border:1px solid #0d9488;border-radius:8px;cursor:pointer;font-size:13px;">
           📝 记一组
         </button>
-        <div *ngIf="logForm[action.id]" style="flex-basis:100%;background:#f0fdfa;padding:12px;border-radius:8px;margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
-          <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
-            <input type="number" [(ngModel)]="logForm[action.id].weightKg" placeholder="重量kg" style="width:80px;padding:6px;border:1px solid #ddd;border-radius:4px;">
-            <input type="number" [(ngModel)]="logForm[action.id].reps" placeholder="次数" style="width:70px;padding:6px;border:1px solid #ddd;border-radius:4px;">
-            <input type="number" min="1" max="10" [(ngModel)]="logForm[action.id].rpe" placeholder="RPE" style="width:60px;padding:6px;border:1px solid #ddd;border-radius:4px;">
-            <button (click)="saveLog(action.id)" style="padding:6px 14px;background:#2e7d32;color:#fff;border:none;border-radius:4px;cursor:pointer;">保存</button>
-          </div>
-        </div>
-        <div *ngIf="logsFor(action.name).length > 0" style="flex-basis:100%;margin-top:6px;font-size:12px;color:#666;">
-          <span *ngFor="let l of logsFor(action.name)" style="margin-right:10px;">{{ l.logDate }} {{ l.weightKg }}kg×{{ l.reps }} RPE{{ l.rpe }}</span>
-        </div>
       </div>
 
       <div class="empty-state" *ngIf="filteredActions.length === 0">

+ 45 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/training-log/training-log.component.html

@@ -0,0 +1,45 @@
+<div style="max-width:500px;margin:0 auto;padding:16px;">
+  <button (click)="router.navigate(['/exercise/actions'])" style="background:none;border:none;color:#0d9488;cursor:pointer;font-size:14px;margin-bottom:12px;">← 返回动作库</button>
+
+  <h2 style="margin:0 0 4px;font-size:20px;color:#0f172a;">📝 记录一组</h2>
+  <p style="color:#666;font-size:14px;margin:0 0 20px;">{{ exerciseName }}</p>
+
+  <div style="background:#fff;border-radius:12px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08);">
+    <div style="margin-bottom:14px;">
+      <label style="display:block;font-size:13px;color:#555;margin-bottom:6px;">重量 (kg)</label>
+      <input type="number" [(ngModel)]="weightKg" placeholder="如 20" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;font-size:15px;">
+    </div>
+
+    <div style="display:flex;gap:12px;margin-bottom:14px;">
+      <div style="flex:1;">
+        <label style="display:block;font-size:13px;color:#555;margin-bottom:6px;">次数</label>
+        <input type="number" [(ngModel)]="reps" placeholder="如 12" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;font-size:15px;">
+      </div>
+      <div style="flex:1;">
+        <label style="display:block;font-size:13px;color:#555;margin-bottom:6px;">RPE (1-10)</label>
+        <input type="number" min="1" max="10" [(ngModel)]="rpe" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;font-size:15px;">
+      </div>
+    </div>
+
+    <div style="margin-bottom:14px;">
+      <label style="display:block;font-size:13px;color:#555;margin-bottom:6px;">备注(可选)</label>
+      <input type="text" [(ngModel)]="notes" placeholder="如:左肩有点紧" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;font-size:15px;">
+    </div>
+
+    <button (click)="save" [disabled]="!exerciseName" style="width:100%;padding:14px;background:#0d9488;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;">
+      {{ saved ? '✓ 已保存' : '保存' }}
+    </button>
+  </div>
+
+  <h3 style="margin:24px 0 10px;font-size:16px;">最近记录</h3>
+  <div *ngIf="recentLogs.length===0" style="color:#999;font-size:13px;text-align:center;padding:20px;">还没有记录</div>
+  <div *ngFor="let l of recentLogs" style="background:#fff;border-radius:10px;padding:12px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 1px 2px rgba(0,0,0,.05);">
+    <div>
+      <div style="font-weight:500;">{{ l.logDate }}</div>
+      <div style="font-size:13px;color:#666;margin-top:2px;">
+        {{ l.weightKg ? l.weightKg + 'kg' : '自重' }} × {{ l.reps }} 次 · RPE {{ l.rpe }}
+      </div>
+    </div>
+    <button (click)="remove(l.id)" style="background:none;border:none;color:#c62828;cursor:pointer;font-size:13px;">删除</button>
+  </div>
+</div>

+ 74 - 0
campus_health_app/frontend/campus-health-app/src/app/modules/training-log/training-log.component.ts

@@ -0,0 +1,74 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { HttpClient } from '@angular/common/http';
+import { ActivatedRoute, Router } from '@angular/router';
+
+@Component({
+  selector: 'app-training-log',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './training-log.component.html'
+})
+export class TrainingLogComponent implements OnInit {
+  exerciseName = '';
+  weightKg: number | null = null;
+  reps: number | null = null;
+  rpe: number = 8;
+  notes = '';
+  recentLogs: any[] = [];
+  saved = false;
+
+  constructor(
+    private http: HttpClient,
+    private route: ActivatedRoute,
+    private router: Router
+  ) {}
+
+  ngOnInit() {
+    this.route.queryParams.subscribe(p => {
+      this.exerciseName = p['exercise'] || '';
+      this.loadRecent();
+    });
+  }
+
+  loadRecent() {
+    if (!this.exerciseName) return;
+    this.http.get<any>(`/api/training-logs?days=90`).subscribe({
+      next: (res) => {
+        if (res.success) {
+          this.recentLogs = res.data
+            .filter((l: any) => l.exerciseName === this.exerciseName)
+            .slice(0, 10);
+        }
+      }
+    });
+  }
+
+  save() {
+    this.http.post<any>('/api/training-logs', {
+      logDate: new Date().toISOString().slice(0, 10),
+      exerciseName: this.exerciseName,
+      weightKg: this.weightKg,
+      reps: this.reps,
+      rpe: this.rpe,
+      notes: this.notes
+    }).subscribe({
+      next: () => {
+        this.saved = true;
+        this.weightKg = null;
+        this.reps = null;
+        this.rpe = 8;
+        this.notes = '';
+        this.loadRecent();
+        setTimeout(() => this.saved = false, 2000);
+      }
+    });
+  }
+
+  remove(id: string) {
+    this.http.delete<any>(`/api/training-logs/${id}`).subscribe({
+      next: () => this.loadRecent()
+    });
+  }
+}