ソースを参照

feat: plan task for list

fmode 16 時間 前
コミット
449a1701d3
6 ファイル変更725 行追加113 行削除
  1. 2 1
      .vscode/settings.json
  2. 47 1
      docs/schema.md
  3. 104 35
      src/app/tab2/tab2.page.html
  4. 135 75
      src/app/tab2/tab2.page.ts
  5. 431 0
      src/lib/import-data.ts
  6. 6 1
      src/lib/ncloud.ts

+ 2 - 1
.vscode/settings.json

@@ -1,3 +1,4 @@
 {
-  "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular"]
+  "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular"],
+   "plantuml.server":"http://www.plantuml.com/plantuml"
 }

+ 47 - 1
docs/schema.md

@@ -13,7 +13,7 @@ Array => JSON Array
 Object => JSON Object
 Date => Date
 File => Parse.File
-Pointer => other Parse.Object
+Pointer => other Parse.Object // 指针类型的字段不需要用xxxId形式命名,直接写xxx即可。
 Relation => Parse.Relation
 Null => null
 GeoPoint => {latitude: 40.0, longitude: -30.0}
@@ -32,4 +32,50 @@ GeoPoint => {latitude: 40.0, longitude: -30.0}
 
 # UML类图
 
+```plantuml
+@startuml
+class _User {
+  +objectId: String [PK]
+  username: String
+  email: String
+  createdAt: Date
+  updatedAt: Date
+  // 其他标准字段...
+}
+
+class TrainingPlan {
+  +objectId: String [PK]
+  planName: String
+  description: String
+  durationInWeeks: Number
+  difficultyLevel: String
+  fitnessGoals: Array
+  schedule: Object
+  user: Pointer<_User>
+  createdAt: Date
+  updatedAt: Date
+  tags: Array
+  isPublic: bool
+}
+
+class TrainingTask {
+  +objectId: String [PK]
+  taskName: String
+  description: String
+  taskType: String
+  estimatedDuration: Number
+  isCompleted: bool
+  orderNumber: Number
+  plan: Pointer<TrainingPlan>
+  createdAt: Date
+  updatedAt: Date
+  attachments: Array<Parse.File>
+  targetMetrics: Object
+}
+
+_User "1" --> "n" TrainingPlan : creates
+TrainingPlan "1" --> "n" TrainingTask : contains
+@enduml
+```
+
 # SQL语句

+ 104 - 35
src/app/tab2/tab2.page.html

@@ -10,47 +10,116 @@
 </ion-header>
 
 <ion-content [fullscreen]="true">
-  <!-- 本周训练概览 -->
-  <ion-card class="weekly-card">
-    <ion-card-header>
-      <ion-card-title>
-        <ion-icon name="calendar-number"></ion-icon>
-        本周训练
-      </ion-card-title>
-      <ion-card-subtitle>已完成 {{completedWorkouts}}/{{totalWorkouts}} 次训练</ion-card-subtitle>
-    </ion-card-header>
-    
-    <ion-card-content>
-      <div class="progress-container">
-        <ion-progress-bar [value]="completionRate" color="primary"></ion-progress-bar>
-        <div class="progress-text">40% 完成度</div>
-      </div>
-      
-      <ion-grid class="day-grid">
-        <ion-row>
-          <ion-col *ngFor="let day of weekDays" [class.active]="day.active">
-            <div class="day-name">{{day.shortName}}</div>
-            <ion-icon 
-              [name]="day.trained ? 'checkmark-circle' : 'ellipse-outline'" 
-              [color]="day.trained ? 'success' : 'medium'">
-            </ion-icon>
-          </ion-col>
-        </ion-row>
-      </ion-grid>
-    </ion-card-content>
-  </ion-card>
 
+  <!-- 加载状态 -->
+  <div *ngIf="isLoading" class="loading-container">
+    <ion-skeleton-text animated style="width: 80%; height: 30px;"></ion-skeleton-text>
+    <ion-skeleton-text animated style="width: 60%; height: 20px;"></ion-skeleton-text>
+    <ion-list>
+      <ion-item *ngFor="let item of [1,2,3]">
+        <ion-avatar slot="start">
+          <ion-skeleton-text animated></ion-skeleton-text>
+        </ion-avatar>
+        <ion-label>
+          <h3><ion-skeleton-text animated style="width: 60%"></ion-skeleton-text></h3>
+          <p><ion-skeleton-text animated style="width: 80%"></ion-skeleton-text></p>
+        </ion-label>
+      </ion-item>
+    </ion-list>
+  </div>
+
+  <!-- 内容区域 -->
+  <div *ngIf="!isLoading">
+    <!-- 本周训练概览 -->
+    <ion-card class="weekly-card">
+      <ion-card-header>
+        <ion-card-title>
+          <ion-icon name="calendar-number"></ion-icon>
+          本周训练
+        </ion-card-title>
+        <ion-card-subtitle>已完成 {{completedWorkouts}}/{{totalWorkouts}} 次训练</ion-card-subtitle>
+      </ion-card-header>
+
+      <ion-card-content>
+        <div class="progress-container">
+          <ion-progress-bar [value]="completionRate" color="primary"></ion-progress-bar>
+          <div class="progress-text">{{completionRate * 100 | number:'1.0-0'}}% 完成度</div>
+        </div>
+
+        <ion-grid class="day-grid">
+          <ion-row>
+            <ion-col *ngFor="let day of weekDays" [class.active]="day.active">
+              <div class="day-name">{{day.shortName}}</div>
+              <ion-icon
+                [name]="day.trained ? 'checkmark-circle' : 'ellipse-outline'"
+                [color]="day.trained ? 'success' : 'medium'">
+              </ion-icon>
+            </ion-col>
+          </ion-row>
+        </ion-grid>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 我的计划列表 -->
+    <ion-list-header>
+      <ion-label>我的计划</ion-label>
+      <ion-button fill="clear" size="small" (click)="refreshPlans()">
+        <ion-icon name="refresh" slot="start"></ion-icon>
+        刷新
+      </ion-button>
+    </ion-list-header>
+
+    <ion-list lines="none">
+      <ion-item *ngFor="let item of myPlans" [detail]="true" (click)="openPlan(item)">
+        <ion-avatar slot="start">
+          <ion-icon [name]="item.icon" color="primary"></ion-icon>
+        </ion-avatar>
+        <ion-label>
+          <h3>{{item.name}}</h3>
+          <p>{{item.progress}}% 完成 · 下次训练: {{item.nextTime}}</p>
+          <p *ngIf="item.description" class="plan-description">{{item.description}}</p>
+        </ion-label>
+        <ion-badge slot="end" color="light">{{item.remaining}}天</ion-badge>
+      </ion-item>
+    </ion-list>
+
+    <div class="empty-state" *ngIf="!isLoading && myPlans.length === 0">
+      <ion-icon name="fitness"></ion-icon>
+      <p>暂无训练计划</p>
+      <ion-button fill="outline" (click)="createNewPlan()">创建新计划</ion-button>
+    </div>
+
+
+    <!-- 任务列表 -->
+    <ion-list-header>
+      <ion-label>任务列表</ion-label>
+    </ion-list-header>
+
+    <ion-list lines="none">
+      <ion-item *ngFor="let task of planTasks">
+        <ion-label>
+          <h3>{{task.get('taskName')}}</h3>
+          <p>{{task.get('description')}}</p>
+          <p>类型: {{task.get('taskType')}} | 预计时长: {{task.get('estimatedDuration')}}分钟</p>
+          <p *ngIf="task.get('isCompleted')">状态: 已完成</p>
+          <p *ngIf="!task.get('isCompleted')">状态: 未完成</p>
+          <p>目标心率: {{task.get('targetMetrics').heartRate}}</p>
+        </ion-label>
+      </ion-item>
+    </ion-list>
+
+ </div>
   <!-- AI咨询 -->
   <ion-list-header>
     <ion-label>私教咨询(免费)</ion-label>
-    
+
   </ion-list-header>
 
   <ion-card class="coach-card">
       <div class="avatar-container">
         <img src="/assets/avatars/jiaolian1.jpg" alt="教练头像" class="coach-avatar" />
       </div>
-      
+
       <ion-card-header>
         <ion-card-title class="coach-name">宋珀尔</ion-card-title>
         <ion-card-subtitle class="coach-title">专业教练</ion-card-subtitle>
@@ -65,7 +134,7 @@
             </ion-chip>
           </div>
         </div>
-        
+
         <ion-button (click)="openConsult()" expand="block" shape="round" class="consult-btn">
           <ion-icon name="chatbubble-ellipses" slot="start"></ion-icon>
           咨询
@@ -75,11 +144,11 @@
 
    <!--<div class="deepseek-input-container">
     <div class="deepseek-input">
-       <ion-textarea 
+       <ion-textarea
         rows="1"
         placeholder="输入您的训练问题..."
         class="deepseek-textarea"
-      ></ion-textarea> 
+      ></ion-textarea>
       <ion-button fill="clear" expand="block">
          健身教练
         <ion-icon name="send" color="primary"></ion-icon>
@@ -122,4 +191,4 @@
       <ion-icon name="add"></ion-icon>
     </ion-fab-button>
   </ion-fab>
-</ion-content>
+</ion-content>

+ 135 - 75
src/app/tab2/tab2.page.ts

@@ -1,23 +1,25 @@
 import { CommonModule } from '@angular/common';
 import { Component } from '@angular/core';
 import { IonTextarea } from '@ionic/angular/standalone';
-import { 
+import {
   ModalController,
   IonHeader,IonNote,IonChip, IonToolbar, IonTitle, IonButtons, IonButton, IonIcon,
   IonContent, IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle,
   IonCardContent, IonProgressBar, IonGrid, IonRow, IonCol, IonListHeader,
-  IonLabel, IonList, IonItem, IonAvatar, IonBadge, IonFab, IonFabButton
+  IonLabel, IonList, IonItem, IonAvatar, IonBadge, IonFab, IonFabButton,IonSkeletonText
 } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
-import { 
+import {
   notificationsOutline, calendarNumber, checkmarkCircle, ellipseOutline,
   timeOutline, barbell, body, walk, add, refresh, fitness,
-  flame, bicycle, trophy
+  flame, bicycle, trophy,
+  ellipsisHorizontal
 } from 'ionicons/icons';
 
 // 引用fmode-ng智能体组件
-import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
+import { ChatPanelOptions, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
 import Parse from "parse";
+import { CloudObject, CloudQuery, CloudUser } from 'src/lib/ncloud';
 
 @Component({
   selector: 'app-tab2',
@@ -31,7 +33,7 @@ import Parse from "parse";
     IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonIcon,
     IonContent, IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle,
     IonCardContent, IonProgressBar, IonGrid, IonRow, IonCol, IonListHeader,
-    IonLabel, IonList, IonItem, IonAvatar, IonBadge, IonFab, IonFabButton
+    IonLabel, IonList, IonItem, IonAvatar, IonBadge, IonFab, IonFabButton,IonSkeletonText
   ]
 })
 export class Tab2Page {
@@ -83,46 +85,46 @@ export class Tab2Page {
           {
             cate:"有氧",img:"/assets/icon/yy.jpg",
             messageList:[
-            "有氧运动多久才能有效减脂?",  
-            "跑步和游泳哪个减肥效果更好?",  
-            "空腹有氧真的更燃脂吗?",  
-            "有氧运动会不会掉肌肉?",  
-            "心率控制在多少才能高效燃脂?",  
-            "每天做有氧运动会不会过度疲劳?",  
-            "有氧运动前要不要吃东西?",  
-            "椭圆机和跑步机哪个更适合新手?",  
-            "跳绳会不会伤膝盖?",  
-            "有氧运动后怎么补充能量?"  
+            "有氧运动多久才能有效减脂?",
+            "跑步和游泳哪个减肥效果更好?",
+            "空腹有氧真的更燃脂吗?",
+            "有氧运动会不会掉肌肉?",
+            "心率控制在多少才能高效燃脂?",
+            "每天做有氧运动会不会过度疲劳?",
+            "有氧运动前要不要吃东西?",
+            "椭圆机和跑步机哪个更适合新手?",
+            "跳绳会不会伤膝盖?",
+            "有氧运动后怎么补充能量?"
           ]
           },
           {
             cate:"减脂",img:"/assets/icon/jz.jpg",
             messageList:[
-              "减脂一定要做有氧吗?",  
-              "为什么体重没变但看起来瘦了?",  
-              "局部减脂(如瘦肚子)真的存在吗?",  
-              "减脂期每天应该吃多少热量?",  
-              "低碳饮食和低脂饮食哪个更适合减脂?",  
-              "为什么运动后体重反而增加了?",  
-              "减脂期可以吃零食吗?",  
-              "平台期怎么突破?",  
-              "晚上吃东西会不会更容易长胖?",  
-              "减脂期要不要计算蛋白质摄入?"  
+              "减脂一定要做有氧吗?",
+              "为什么体重没变但看起来瘦了?",
+              "局部减脂(如瘦肚子)真的存在吗?",
+              "减脂期每天应该吃多少热量?",
+              "低碳饮食和低脂饮食哪个更适合减脂?",
+              "为什么运动后体重反而增加了?",
+              "减脂期可以吃零食吗?",
+              "平台期怎么突破?",
+              "晚上吃东西会不会更容易长胖?",
+              "减脂期要不要计算蛋白质摄入?"
             ]
           },
           {
             cate:"增肌",img:"/assets/icon/zj.jpg",
             messageList: [
-            "增肌一定要喝蛋白粉吗?",  
-            "为什么练了很久肌肉不长?",  
-            "增肌期可以同时减脂吗?",  
-            "训练后多久补充蛋白质最有效?",  
-            "增肌需要每天练同一个部位吗?",  
-            "徒手训练(如俯卧撑)能有效增肌吗?",  
-            "增肌期体重不增长是怎么回事?",  
-            "肌肉酸痛还能继续练吗?",  
-            "增肌训练每组做多少次最合适?",  
-            "睡眠对增肌的影响有多大?"  
+            "增肌一定要喝蛋白粉吗?",
+            "为什么练了很久肌肉不长?",
+            "增肌期可以同时减脂吗?",
+            "训练后多久补充蛋白质最有效?",
+            "增肌需要每天练同一个部位吗?",
+            "徒手训练(如俯卧撑)能有效增肌吗?",
+            "增肌期体重不增长是怎么回事?",
+            "肌肉酸痛还能继续练吗?",
+            "增肌训练每组做多少次最合适?",
+            "睡眠对增肌的影响有多大?"
           ]
           },
         ]
@@ -183,23 +185,13 @@ export class Tab2Page {
     constructor(
     private modalCtrl:ModalController,
   ) {
-    addIcons({ 
+    addIcons({
       notificationsOutline, calendarNumber, checkmarkCircle, ellipseOutline,
       timeOutline, barbell, body, walk, add, refresh, fitness,
-      flame, bicycle,  trophy
+      flame, bicycle,  trophy, ellipsisHorizontal
     });
   }
 
-  // 周训练数据
-  weekDays = [
-    { shortName: '周一', name: 'Monday', trained: true, active: false },
-    { shortName: '周二', name: 'Tuesday', trained: false, active: true },
-    { shortName: '周三', name: 'Wednesday', trained: true, active: false },
-    { shortName: '周四', name: 'Thursday', trained: false, active: false },
-    { shortName: '周五', name: 'Friday', trained: false, active: false },
-    { shortName: '周六', name: 'Saturday', trained: false, active: false },
-    { shortName: '周日', name: 'Sunday', trained: false, active: false }
-  ];
 
   // 计算属性
   get completedWorkouts(): number {
@@ -239,41 +231,109 @@ export class Tab2Page {
     }
   ];
 
-  // 我的计划
-  myPlans = [
-    {
-      name: '30天减脂挑战',
-      progress: 65,
-      nextTime: '明天 7:00',
-      remaining: 12,
-      icon: 'flame'
-    },
-    {
-      name: '上肢力量训练',
-      progress: 30,
-      nextTime: '后天 19:30',
-      remaining: 5,
-      icon: 'barbell'
-    },
-    {
-      name: '晨跑计划',
-      progress: 80,
-      nextTime: '今天 6:00',
-      remaining: 3,
-      icon: 'walk'
-    }
+
+
+  // 动态加载数据
+  async ngOnInit() {
+    await this.loadUserData();
+  }
+  isLoading = true;
+  currentUser: CloudUser | null = null;
+  myPlans: any[] = [];
+  planTasks: any[] = [];
+
+  // 周训练数据
+  weekDays = [
+    { shortName: '周一', name: 'Monday', trained: true, active: false },
+    { shortName: '周二', name: 'Tuesday', trained: false, active: true },
+    { shortName: '周三', name: 'Wednesday', trained: true, active: false },
+    { shortName: '周四', name: 'Thursday', trained: false, active: false },
+    { shortName: '周五', name: 'Friday', trained: false, active: false },
+    { shortName: '周六', name: 'Saturday', trained: false, active: false },
+    { shortName: '周日', name: 'Sunday', trained: false, active: false }
   ];
+  async loadUserData() {
+    this.isLoading = true;
+
+    try {
+      // 获取当前用户
+      this.currentUser = new CloudUser();
+
+      if (this.currentUser.id) {
+        // 加载用户的训练计划
+        await this.loadTrainingPlans();
+      }
+    } catch (error) {
+      console.error('加载用户数据失败:', error);
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
+  async loadTrainingPlans() {
+    if (!this.currentUser?.id) return;
+
+    const query = new CloudQuery('TrainingPlan');
+    query.equalTo('user', this.currentUser.toPointer());
+
+    const plans = await query.find();
+    console.log("this.loadTrainingPlans()",plans)
+    this.myPlans = plans.map(plan => ({
+      id: plan.id,
+      name: plan.get('planName') || '未命名计划',
+      progress: 0, // 这里可以根据实际完成情况计算
+      nextTime: this.getNextTrainingTime(plan),
+      remaining: this.getRemainingDays(plan),
+      icon: this.getPlanIcon(plan.get('fitnessGoals')),
+      description: plan.get('description'),
+      durationInWeeks: plan.get('durationInWeeks'),
+      difficultyLevel: plan.get('difficultyLevel')
+    }));
 
+    // 加载每个计划的任务
+    for (const plan of plans) {
+      if(typeof plan.id == "string"){
+        await this.loadPlanTasks(plan.id);
+      }
+    }
+  }
+
+  async loadPlanTasks(planId: string) {
+    const query = new CloudQuery('TrainingTask');
+    query.equalTo('plan', planId);
+
+    const tasks = await query.find();
+    console.log("tasks",tasks)
+    this.planTasks = tasks
+  }
+
+  getNextTrainingTime(plan: CloudObject): string {
+    // 简单实现 - 实际应根据计划schedule计算
+    return '明天 7:00';
+  }
+
+  getRemainingDays(plan: CloudObject): number {
+    // 简单实现 - 实际应根据计划duration和开始日期计算
+    return Math.floor(Math.random() * 30) + 1;
+  }
+
+  getPlanIcon(goals: string[] = []): string {
+    if (goals.includes('减脂')) return 'flame';
+    if (goals.includes('增肌')) return 'barbell';
+    if (goals.includes('跑步')) return 'walk';
+    return 'fitness';
+  }
 
-  refreshPlans() {
-    console.log('刷新计划数据');
+  async refreshPlans() {
+    await this.loadUserData();
   }
 
   openPlan(plan: any) {
-    console.log('打开计划:', plan.name);
+    console.log('打开计划:', plan);
+    // 这里可以导航到计划详情页,传入plan和this.planTasks[plan.id]
   }
 
   createNewPlan() {
     console.log('创建新计划');
   }
-}
+}

+ 431 - 0
src/lib/import-data.ts

@@ -0,0 +1,431 @@
+import { CloudObject, CloudUser } from './ncloud';
+
+// 创建测试用户(如果不存在)
+async function createTestUser() {
+  const user = new CloudUser();
+  const existingUser = await user.login('fitnesstest@example.com', 'test1234');
+
+  if (!existingUser) {
+    await user.signUp(
+      'fitnesstest@example.com',
+      'test1234',
+      {
+        email: 'fitnesstest@example.com',
+        name: '健身测试用户'
+      }
+    );
+    console.log('测试用户创建成功');
+  } else {
+    console.log('测试用户已存在');
+  }
+  return user;
+}
+
+// 创建减脂训练计划
+async function createFatLossPlan(user: CloudUser) {
+  const plan = new CloudObject('TrainingPlan');
+
+  plan.set({
+    planName: '4周高效减脂计划',
+    description: '针对初学者的科学减脂训练方案,结合有氧和力量训练',
+    durationInWeeks: 4,
+    difficultyLevel: '初级',
+    fitnessGoals: ['减脂', '提高耐力', '基础力量'],
+    schedule: {
+      monday: true,
+      wednesday: true,
+      friday: true,
+      saturday: true
+    },
+    user: user.toPointer(),
+    tags: ['减脂', '新手友好', '家庭训练'],
+    isPublic: true
+  });
+
+  await plan.save();
+  console.log('训练计划创建成功:', plan.id);
+  return plan;
+}
+
+// 创建减脂训练任务
+async function createFatLossTasks(plan: CloudObject) {
+  const tasks = [
+    {
+      taskName: '热身 - 动态拉伸',
+      description: '5分钟全身动态拉伸,提高心率',
+      taskType: '热身',
+      estimatedDuration: 5,
+      isCompleted: false,
+      orderNumber: 1,
+      targetMetrics: {
+        heartRate: '100-120bpm'
+      }
+    },
+    {
+      taskName: '高强度间歇训练(HIIT)',
+      description: '20秒高强度运动+40秒休息,重复8组',
+      taskType: '有氧',
+      estimatedDuration: 15,
+      isCompleted: false,
+      orderNumber: 2,
+      targetMetrics: {
+        heartRate: '140-160bpm',
+        calories: '150-200kcal'
+      }
+    },
+    {
+      taskName: '全身循环训练',
+      description: '深蹲+俯卧撑+平板支撑,每个动作45秒,休息15秒,3轮',
+      taskType: '力量',
+      estimatedDuration: 20,
+      isCompleted: false,
+      orderNumber: 3,
+      targetMetrics: {
+        reps: '12-15/组'
+      }
+    },
+    {
+      taskName: '核心训练',
+      description: '仰卧卷腹+俄罗斯转体+平板支撑,每个动作30秒',
+      taskType: '核心',
+      estimatedDuration: 10,
+      isCompleted: false,
+      orderNumber: 4,
+      targetMetrics: {
+        sets: '3组'
+      }
+    },
+    {
+      taskName: '放松拉伸',
+      description: '10分钟静态拉伸,重点下肢',
+      taskType: '恢复',
+      estimatedDuration: 10,
+      isCompleted: false,
+      orderNumber: 5,
+      targetMetrics: {
+        duration: '每个部位30秒'
+      }
+    }
+  ];
+
+  for (const taskData of tasks) {
+    const task = new CloudObject('TrainingTask');
+    task.set({
+      ...taskData,
+      plan: plan.toPointer()
+    });
+    await task.save();
+    console.log(`任务创建成功: ${taskData.taskName}`);
+  }
+}
+
+// 主执行函数
+export async function importFatLossTrainingData() {
+  try {
+    console.log('开始导入减脂训练测试数据...');
+
+    // 1. 创建/获取测试用户
+    const user = await createTestUser();
+
+    // 2. 创建训练计划
+    const plan = await createFatLossPlan(user);
+
+    // 3. 创建训练任务
+    await createFatLossTasks(plan);
+
+    console.log('减脂训练测试数据导入完成!');
+  } catch (error) {
+    console.error('导入数据时出错:', error);
+  }
+}
+
+
+
+
+// 创建增肌测试用户(如果不存在)
+async function createMuscleGainUser() {
+  const user = new CloudUser();
+  const existingUser = await user.login('muscletest@example.com', 'gain1234');
+
+  if (!existingUser) {
+    await user.signUp(
+      'muscletest@example.com',
+      'gain1234',
+      {
+        email: 'muscletest@example.com',
+        name: '增肌测试用户',
+        bodyWeight: 70,  // 公斤
+        height: 175      // 厘米
+      }
+    );
+    console.log('增肌测试用户创建成功');
+  } else {
+    console.log('增肌测试用户已存在');
+  }
+  return user;
+}
+
+// 创建增肌训练计划
+async function createMuscleGainPlan(user: CloudUser) {
+  const plan = new CloudObject('TrainingPlan');
+
+  plan.set({
+    planName: '6周科学增肌计划',
+    description: '针对中级训练者的肌肉增长方案,采用分化训练',
+    durationInWeeks: 6,
+    difficultyLevel: '中级',
+    fitnessGoals: ['增肌', '力量提升', '肌肥大'],
+    schedule: {
+      monday: '胸+三头',
+      tuesday: '背+二头',
+      wednesday: '休息',
+      thursday: '腿',
+      friday: '肩+腹',
+      saturday: '全身功能性',
+      sunday: '休息'
+    },
+    userId: user.toPointer(),
+    tags: ['增肌', '分化训练', '健身房'],
+    isPublic: true,
+    recommendedNutrition: {
+      protein: '2g/kg体重',
+      carbs: '4g/kg体重',
+      fats: '1g/kg体重'
+    }
+  });
+
+  await plan.save();
+  console.log('增肌训练计划创建成功:', plan.id);
+  return plan;
+}
+
+// 创建增肌训练任务(按训练日分组)
+async function createMuscleGainTasks(plan: CloudObject) {
+  // 周一:胸+三头
+  await createTrainingDayTasks(plan, '胸+三头', 1, [
+    {
+      taskName: '平板杠铃卧推',
+      description: '4组×8-12次,组间休息90秒',
+      taskType: '力量',
+      estimatedDuration: 15,
+      targetMetrics: {
+        weight: '渐进超负荷',
+        rpe: '7-8'
+      }
+    },
+    {
+      taskName: '上斜哑铃卧推',
+      description: '3组×10-12次',
+      taskType: '力量',
+      estimatedDuration: 10,
+      targetMetrics: {
+        rpe: '7-8'
+      }
+    },
+    {
+      taskName: '双杠臂屈伸(负重)',
+      description: '3组×最大次数',
+      taskType: '力量',
+      estimatedDuration: 10,
+      targetMetrics: {
+        addedWeight: '5-10kg'
+      }
+    },
+    {
+      taskName: '绳索下压',
+      description: '4组×12-15次,递减组',
+      taskType: '力量',
+      estimatedDuration: 12,
+      targetMetrics: {
+        technique: '顶峰收缩2秒'
+      }
+    }
+  ]);
+
+  // 周二:背+二头
+  await createTrainingDayTasks(plan, '背+二头', 2, [
+    {
+      taskName: '引体向上',
+      description: '4组×力竭,可辅助',
+      taskType: '力量',
+      estimatedDuration: 12,
+      targetMetrics: {
+        reps: '每组8+'
+      }
+    },
+    {
+      taskName: '杠铃划船',
+      description: '4组×8-12次,严格姿势',
+      taskType: '力量',
+      estimatedDuration: 15,
+      targetMetrics: {
+        weight: '渐进超负荷'
+      }
+    },
+    {
+      taskName: '哑铃单臂划船',
+      description: '3组×10-12次/侧',
+      taskType: '力量',
+      estimatedDuration: 10,
+      targetMetrics: {
+        mindMuscle: '强调背肌收缩'
+      }
+    },
+    {
+      taskName: '杠铃弯举',
+      description: '21响礼炮(7次下半程+7次上半程+7次全程)',
+      taskType: '力量',
+      estimatedDuration: 10,
+      targetMetrics: {
+        technique: '控制离心'
+      }
+    }
+  ]);
+
+  // 周四:腿
+  await createTrainingDayTasks(plan, '腿', 3, [
+    {
+      taskName: '深蹲',
+      description: '5组×5次,大重量',
+      taskType: '力量',
+      estimatedDuration: 20,
+      targetMetrics: {
+        weight: '85% 1RM'
+      }
+    },
+    {
+      taskName: '罗马尼亚硬拉',
+      description: '4组×8次',
+      taskType: '力量',
+      estimatedDuration: 15,
+      targetMetrics: {
+        stretch: '强调腘绳肌拉伸'
+      }
+    },
+    {
+      taskName: '腿举',
+      description: '3组×12-15次,递减组',
+      taskType: '力量',
+      estimatedDuration: 12,
+      targetMetrics: {
+        footPosition: '不同站距'
+      }
+    },
+    {
+      taskName: '坐姿提踵',
+      description: '4组×15-20次',
+      taskType: '力量',
+      estimatedDuration: 8,
+      targetMetrics: {
+        range: '全程动作'
+      }
+    }
+  ]);
+
+  // 周五:肩+腹
+  await createTrainingDayTasks(plan, '肩+腹', 4, [
+    {
+      taskName: '站姿杠铃推举',
+      description: '4组×6-8次',
+      taskType: '力量',
+      estimatedDuration: 12,
+      targetMetrics: {
+        strictForm: '避免借力'
+      }
+    },
+    {
+      taskName: '哑铃侧平举',
+      description: '5组×12-15次,渐降组',
+      taskType: '力量',
+      estimatedDuration: 15,
+      targetMetrics: {
+        weight: '递减重量'
+      }
+    },
+    {
+      taskName: '面拉',
+      description: '4组×15次',
+      taskType: '力量',
+      estimatedDuration: 10,
+      targetMetrics: {
+        focus: '后束激活'
+      }
+    },
+    {
+      taskName: '悬挂举腿',
+      description: '4组×12次',
+      taskType: '核心',
+      estimatedDuration: 8,
+      targetMetrics: {
+        control: '避免摆动'
+      }
+    }
+  ]);
+
+  // 周六:全身功能性
+  await createTrainingDayTasks(plan, '全身功能性', 5, [
+    {
+      taskName: '农夫行走',
+      description: '3组×30秒',
+      taskType: '功能性',
+      estimatedDuration: 10,
+      targetMetrics: {
+        weight: '挑战性重量'
+      }
+    },
+    {
+      taskName: '药球砸地',
+      description: '4组×15次',
+      taskType: '爆发力',
+      estimatedDuration: 8,
+      targetMetrics: {
+        power: '最大爆发'
+      }
+    },
+    {
+      taskName: '战绳训练',
+      description: '30秒全力+30秒休息,5轮',
+      taskType: '心肺',
+      estimatedDuration: 10,
+      targetMetrics: {
+        intensity: '全力输出'
+      }
+    }
+  ]);
+}
+
+// 辅助函数:为特定训练日创建任务
+async function createTrainingDayTasks(plan: CloudObject, dayName: string, dayOrder: number, tasks: any[]) {
+  for (const [index, taskData] of tasks.entries()) {
+    const task = new CloudObject('TrainingTask');
+    task.set({
+      ...taskData,
+      planId: plan.toPointer(),
+      orderNumber: dayOrder * 100 + index + 1, // 确保排序正确
+      trainingDay: dayName,
+      isCompleted: false
+    });
+    await task.save();
+    console.log(`[${dayName}] 任务创建成功: ${taskData.taskName}`);
+  }
+}
+
+// 主执行函数
+export async function importMuscleGainTrainingData() {
+  try {
+    console.log('开始导入增肌训练测试数据...');
+
+    // 1. 创建/获取测试用户
+    const user = await createMuscleGainUser();
+
+    // 2. 创建训练计划
+    const plan = await createMuscleGainPlan(user);
+
+    // 3. 创建训练任务(按训练日分组)
+    await createMuscleGainTasks(plan);
+
+    console.log('增肌训练测试数据导入完成!');
+  } catch (error) {
+    console.error('导入数据时出错:', error);
+  }
+}

+ 6 - 1
src/lib/ncloud.ts

@@ -186,7 +186,7 @@ export class CloudQuery {
 
         if (Object.keys(this.queryParams["where"]).length) {
             const whereStr = JSON.stringify(this.queryParams["where"]);
-            url += `where=${whereStr}`;
+            url += `where=${whereStr}&limit=1`;
         }
 
         const response = await fetch(url, {
@@ -211,6 +211,11 @@ export class CloudQuery {
 
     dataToObj(exists:any):CloudObject{
         let existsObject = new CloudObject(this.className);
+        Object.keys(exists).forEach(key=>{
+          if(exists[key]?.__type =="Object"){
+            exists[key] = this.dataToObj(exists[key])
+          }
+        })
         existsObject.set(exists);
         existsObject.id = exists.objectId;
         existsObject.createdAt = exists.createdAt;