瀏覽代碼

Merge branch 'master' of http://git.fmode.cn:3000/18779989085/202226701041

cyx 3 月之前
父節點
當前提交
81fc121c3f
共有 59 個文件被更改,包括 4020 次插入25 次删除
  1. 4 1
      TFPower-app/angular.json
  2. 1 0
      TFPower-app/src/app/app.routes.ts
  3. 1 3
      TFPower-app/src/app/tab1/tab1.page.ts
  4. 19 0
      TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.html
  5. 0 0
      TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.scss
  6. 24 0
      TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.spec.ts
  7. 38 0
      TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.ts
  8. 23 0
      TFPower-app/src/app/tab2/agent/agent.input.ts
  9. 86 0
      TFPower-app/src/app/tab2/agent/agent.json.ts
  10. 21 0
      TFPower-app/src/app/tab2/agent/agent.start.ts
  11. 81 0
      TFPower-app/src/app/tab2/agent/agent.task.ts
  12. 67 0
      TFPower-app/src/app/tab2/agent/tasks/poem/inquiry/1.inquiry-user-story.ts
  13. 80 0
      TFPower-app/src/app/tab2/agent/tasks/poem/inquiry/2.inquiry-doctor-question.ts
  14. 143 0
      TFPower-app/src/app/tab2/agent/tasks/poem/inquiry/3.inquiry-user-answer.ts
  15. 55 0
      TFPower-app/src/app/tab2/agent/tasks/poem/poem-desc.ts
  16. 46 0
      TFPower-app/src/app/tab2/agent/tasks/poem/poem-picture.ts
  17. 52 0
      TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.html
  18. 26 0
      TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.scss
  19. 24 0
      TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.spec.ts
  20. 61 0
      TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.ts
  21. 214 10
      TFPower-app/src/app/tab2/tab2.page.html
  22. 215 0
      TFPower-app/src/app/tab2/tab2.page.scss
  23. 333 7
      TFPower-app/src/app/tab2/tab2.page.ts
  24. 23 0
      TFPower-app/src/app/tab2/tag-input/tag-input.component.html
  25. 36 0
      TFPower-app/src/app/tab2/tag-input/tag-input.component.scss
  26. 24 0
      TFPower-app/src/app/tab2/tag-input/tag-input.component.spec.ts
  27. 39 0
      TFPower-app/src/app/tab2/tag-input/tag-input.component.ts
  28. 9 0
      TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.html
  29. 3 0
      TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.scss
  30. 22 0
      TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.spec.ts
  31. 243 0
      TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.ts
  32. 114 0
      TFPower-app/src/app/tab2/test-page/test-page.component.html
  33. 38 0
      TFPower-app/src/app/tab2/test-page/test-page.component.scss
  34. 22 0
      TFPower-app/src/app/tab2/test-page/test-page.component.spec.ts
  35. 329 0
      TFPower-app/src/app/tab2/test-page/test-page.component.ts
  36. 3 2
      TFPower-app/src/app/tabs/tabs.page.html
  37. 2 1
      TFPower-app/src/app/tabs/tabs.page.ts
  38. 6 0
      TFPower-app/src/app/tabs/tabs.routes.ts
  39. 二進制
      TFPower-app/src/assets/images/ache.jpg
  40. 二進制
      TFPower-app/src/assets/images/action.png
  41. 二進制
      TFPower-app/src/assets/images/coach1.jpg
  42. 二進制
      TFPower-app/src/assets/images/coach2.jpg
  43. 二進制
      TFPower-app/src/assets/images/wisefitness-high-resolution-logo.png
  44. 381 0
      TFPower-app/src/lib/ncloud.ts
  45. 26 0
      TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.html
  46. 0 0
      TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.scss
  47. 22 0
      TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts
  48. 65 0
      TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.ts
  49. 41 0
      TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.html
  50. 0 0
      TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.scss
  51. 22 0
      TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.spec.ts
  52. 92 0
      TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.ts
  53. 1 1
      TFPower-app/src/zone-flags.ts
  54. 2 0
      TFPower-prod/README.md
  55. 258 0
      TFPower-prod/schema/schema.md
  56. 352 0
      TFPower-server/lib/migration/data.js
  57. 60 0
      TFPower-server/lib/migration/import-data.js
  58. 165 0
      TFPower-server/lib/ncloud.js
  59. 6 0
      package-lock.json

+ 4 - 1
TFPower-app/angular.json

@@ -126,7 +126,10 @@
     }
   },
   "cli": {
-    "schematicCollections": ["@ionic/angular-toolkit"]
+    "schematicCollections": [
+      "@ionic/angular-toolkit"
+    ],
+    "analytics": "a63e6579-9007-4bce-b474-6f3b834c3a22"
   },
   "schematics": {
     "@ionic/angular-toolkit:component": {

+ 1 - 0
TFPower-app/src/app/app.routes.ts

@@ -5,4 +5,5 @@ export const routes: Routes = [
     path: '',
     loadChildren: () => import('./tabs/tabs.routes').then((m) => m.routes),
   },
+
 ];

+ 1 - 3
TFPower-app/src/app/tab1/tab1.page.ts

@@ -68,10 +68,8 @@ import { AiplanPageComponent } from '../page/aiplan-page/aiplan-page.component';
     AiplanPageComponent,
   ],
 })
-export class Tab1Page implements OnInit {
+export class Tab1Page {
   constructor(private router: Router) {}
-
-  ngOnInit(): any {}
   alertButtons = ['打卡'];
   duringday: number = 10;
   goodday: number = 20;

+ 19 - 0
TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.html

@@ -0,0 +1,19 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-button color="medium" (click)="cancel()">取消</ion-button>
+    </ion-buttons>
+    <ion-title>用户输入</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="confirm()" [strong]="true">确认</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+<ion-content class="ion-padding">
+  @for(field of fieldsArray;track field.name){
+  <ion-item>
+    <ion-input labelPlacement="stacked" label="请输入 {{field.name}}" [(ngModel)]="inputData[field.name]"
+      placeholder="{{field.desc}}"></ion-input>
+  </ion-item>
+  }
+</ion-content>

+ 0 - 0
TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.scss


+ 24 - 0
TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { AgentUserInputComponent } from './agent-user-input.component';
+
+describe('AgentUserInputComponent', () => {
+  let component: AgentUserInputComponent;
+  let fixture: ComponentFixture<AgentUserInputComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [AgentUserInputComponent],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(AgentUserInputComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 38 - 0
TFPower-app/src/app/tab2/agent/agent-user-input/agent-user-input.component.ts

@@ -0,0 +1,38 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { ModalController, IonHeader, IonContent, IonInput, IonToolbar, IonItem, IonButtons, IonButton, IonTitle } from '@ionic/angular/standalone';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+export interface AgentUserInputField {
+  name: string, // 字段名称
+  type: string, // 输入类型
+  desc: string, // 字段说明
+}
+@Component({
+  selector: 'agent-user-input',
+  templateUrl: './agent-user-input.component.html',
+  styleUrls: ['./agent-user-input.component.scss'],
+  standalone: true,
+  imports: [
+    IonHeader, IonContent, IonInput, IonToolbar, IonItem, IonButtons, IonButton,
+    IonTitle,
+    FormsModule, ReactiveFormsModule,
+  ]
+})
+export class AgentUserInputComponent implements OnInit {
+
+  @Input()
+  fieldsArray: AgentUserInputField[] = []
+
+  @Input()
+  inputData: any = {}
+
+  constructor(private modalCtrl: ModalController) { }
+  ngOnInit() {
+  }
+  cancel() {
+    return this.modalCtrl.dismiss(null, 'cancel');
+  }
+
+  confirm() {
+    return this.modalCtrl.dismiss(this.inputData, 'confirm');
+  }
+}

+ 23 - 0
TFPower-app/src/app/tab2/agent/agent.input.ts

@@ -0,0 +1,23 @@
+import { ModalController } from '@ionic/angular/standalone';
+import { AgentUserInputComponent } from './agent-user-input/agent-user-input.component';
+
+/**
+ * 数据搜集Modal
+ * @description
+ * https://ionicframework.com/docs/api/modal#controller-modals
+ */
+export async function getUserInput(modalCtrl: ModalController, options: { fieldsArray: any }) {
+  const modal = await modalCtrl.create({
+    component: AgentUserInputComponent,
+    componentProps: {
+      fieldsArray: options.fieldsArray
+    }
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+}

+ 86 - 0
TFPower-app/src/app/tab2/agent/agent.json.ts

@@ -0,0 +1,86 @@
+/**
+ * 
+ 结果是:
+ {
+     "keshi":"呼吸科",
+     "user":{
+         "name":""
+     }
+ }
+ */
+export function extactAndParseJsonFromString(inputString: string) {
+    let startIndex = inputString.indexOf("{")
+    if (startIndex == -1) return {}
+
+    let count = 0;
+    let endIndex = startIndex;
+
+    // 遍历字符串,计算花括号实现数量平衡
+    for (let i = startIndex; i < inputString.length; i++) {
+        // console.log(i,inputString[i])
+        if (inputString[i] === "{") {
+            count++;
+        } else if (inputString[i] === "}") {
+            count--;
+        }
+        // 当花括号平衡,我们找到了完整的JSON
+        if (count === 0) {
+            endIndex = i;
+            break;
+        }
+    }
+    // 若不平衡
+    if (count != 0) return {};
+
+    // 提取JSON字符串,并解析
+    const jsonString = inputString.slice(startIndex, endIndex + 1);
+    console.log("jsonString", jsonString)
+    try {
+        return JSON.parse(jsonString);
+    } catch (error) {
+        console.error("Failed to parse JSON", error);
+        return {}
+    }
+}
+export function extractAllJsonFromString(inputString: string) {
+    let startIndex = inputString.indexOf("{");
+    let count = 0;
+    let jsonObjects = [];  // 用来存储所有提取的JSON对象
+
+    while (startIndex !== -1) {
+        let endIndex = startIndex;
+        count = 0;
+
+        // 遍历字符串,计算花括号平衡
+        for (let i = startIndex; i < inputString.length; i++) {
+            if (inputString[i] === "{") {
+                count++;
+            } else if (inputString[i] === "}") {
+                count--;
+            }
+            // 找到完整的JSON数据
+            if (count === 0) {
+                endIndex = i;
+                break;
+            }
+        }
+
+        // 如果找到了平衡的花括号,提取JSON并解析
+        if (count === 0) {
+            const jsonString = inputString.slice(startIndex, endIndex + 1);
+            try {
+                const jsonObject = JSON.parse(jsonString);
+                jsonObjects.push(jsonObject);  // 将解析的对象添加到数组中
+            } catch (error) {
+                console.error("Failed to parse JSON:", error);
+            }
+
+            // 更新startIndex,继续查找下一个 JSON
+            startIndex = inputString.indexOf("{", endIndex + 1);
+        } else {
+            break;
+        }
+    }
+
+    return jsonObjects;
+}

+ 21 - 0
TFPower-app/src/app/tab2/agent/agent.start.ts

@@ -0,0 +1,21 @@
+import { AgentTaskStep } from "./agent.task";
+
+/**
+ * 任务执行函数
+ */
+export async function startTask(taskStepList: AgentTaskStep[]) {
+
+    for (let index = 0; index < taskStepList.length; index++) {
+        let step = taskStepList[index];
+        let result = await step.handle()
+        if (result == false) { break; }
+
+        if (step.error) {
+            break;
+        }
+        if (!step.error) { // 任务执行正常,无Error
+
+        }
+    }
+
+}

+ 81 - 0
TFPower-app/src/app/tab2/agent/agent.task.ts

@@ -0,0 +1,81 @@
+
+// 任务步骤初始化参数
+interface AgentTaskStepOptions {
+    title: string
+    shareData?: any
+    handle?(): { (): Promise<any> }
+}
+
+export class AgentTaskStep {
+    // 任务名称
+    title: string = ""
+
+    // 任务数据 共享数据(整个任务集共享传递的数据)
+    shareData: any = {}
+    // 任务数据 私有数据(当前任务私有的处理数据)
+    data: any = {}
+
+    // 父级组件:任务集组件
+    // @example 当前子任务未完成,且当前任务是必须时,需要在当前任务中,终止整个程序
+    parentComp: any | undefined
+    // 当前任务在任务集数组中的项目
+    parentIndex: number | undefined
+
+    /**
+     * 任务生命周期
+     * @constructor // 构造时执行一次
+     * @onInit // 初始化后执行一次
+     * @onProgress // 进度变化时执行一次
+     * @onComplete // 完成前执行一次
+     */
+    constructor(metaData: AgentTaskStepOptions) {
+        this.title = metaData.title
+        this.shareData = metaData.shareData
+        if (metaData.handle) {
+            this.handle = metaData.handle
+        }
+        this.init(metaData)
+    }
+    onInit(step: AgentTaskStep) {
+        this.isInit = true;
+    }
+    beforeHandle(step: AgentTaskStep) {
+
+    }
+    afterHandle(step: AgentTaskStep) {
+
+    }
+    onProgress(step: AgentTaskStep) {
+
+    }
+    onComplete(step: AgentTaskStep) {
+
+    }
+    onError(step: AgentTaskStep) {
+
+    }
+
+    /**
+     * 任务初始化函数
+     */
+    async init(metaData: AgentTaskStepOptions) {
+    }
+    isInit: boolean = false; // 是否初始化
+
+    /** 
+     * 任务执行函数
+     * @desc 函数内必须有明确的完成、结束情形,函数内必须有进度的变化指示 progress从0.00 - 1.00
+     */
+    progress: number = 0;
+    handle(): Promise<any> | any {
+        this.beforeHandle(this)
+        this.onComplete(this)
+        this.onError(this)
+        return true
+    }
+
+    /**
+     * 任务错误提示
+     */
+    error: string = "" // 错误原因
+}

+ 67 - 0
TFPower-app/src/app/tab2/agent/tasks/poem/inquiry/1.inquiry-user-story.ts

@@ -0,0 +1,67 @@
+import { AgentTaskStep } from 'src/app/tab2/agent/agent.task';
+import { getUserInput } from 'src/app/tab2/agent/agent.input';
+import { ModalController } from '@ionic/angular/standalone';
+import { FmodeChatCompletion } from 'fmode-ng';
+import { extactAndParseJsonFromString } from 'src/app/tab2/agent/agent.json';
+
+export function TaskInqueryUserStory(options: {
+    modalCtrl: ModalController
+    shareData: any
+}
+): AgentTaskStep {
+    let task1 = new AgentTaskStep({ title: "根据部位判断是什么引起不适", shareData: options.shareData })
+    task1.handle = () => {
+        return new Promise(async (resolve, reject) => {
+            // 获取用户输入的诗词
+            let userInput = await getUserInput(options.modalCtrl, {
+                fieldsArray: [
+                    { name: "症状口述", type: "text", desc: "描述下感觉到的症状和不适的部位。" }
+                ]
+            });
+
+            if (!userInput?.['症状口述']) {
+                task1.error = "无症状口述,无法判断科室"
+                resolve(false);
+            }
+
+            // 文本生成
+            let PromptTemplate = `您是一名专业的健身医疗恢复专家,在健身房保健室负责导诊服务,请您根据患者的症状口述,判断其造成原因。
+                患者症状口述:${userInput['症状口述']}
+                结果以JSON格式表示:
+                症状名称为具体的症状,症状描述为用户的感受,持续时间若没有直接说,可以写近期即可。
+                {
+                    "reason":"运动项目",
+                    "sympList":[
+                        {"title":"症状名称","desc":"症状描述","duration":"持续时间"}
+                    ]
+                }
+                `
+            let completion = new FmodeChatCompletion([
+                { role: "system", content: "" },
+                { role: "user", content: PromptTemplate }
+            ])
+            completion.sendCompletion().subscribe((message: any) => {
+                if (task1.progress < 0.5) {
+                    task1.progress += 0.1
+                }
+                if (task1.progress >= 0.5 && task1.progress <= 0.9) {
+                    task1.progress += 0.01
+                }
+                if (task1.progress >= 0.9) {
+                    task1.progress += 0.001
+                }
+                // 打印消息体
+                console.log(message.content)
+                // 赋值消息内容给组件内属性
+                if (message.complete) { // 判断message为完成状态,则设置isComplete为完成
+                    options.shareData.userStory = extactAndParseJsonFromString(message.content)
+                    options.shareData.userStory['症状口述'] = userInput['症状口述']
+                    task1.progress = 1
+                    resolve(true)
+                }
+            })
+        })
+
+    }
+    return task1
+}

+ 80 - 0
TFPower-app/src/app/tab2/agent/tasks/poem/inquiry/2.inquiry-doctor-question.ts

@@ -0,0 +1,80 @@
+import { AgentTaskStep } from 'src/app/tab2/agent/agent.task';
+import { getUserInput } from 'src/app/tab2/agent/agent.input';
+import { ModalController } from '@ionic/angular/standalone';
+import { FmodeChatCompletion } from 'fmode-ng';
+import { extactAndParseJsonFromString } from 'src/app/tab2/agent/agent.json';
+
+export function TaskInqueryDoctorQuestion(options: {
+    modalCtrl: ModalController
+    shareData: any
+}
+): AgentTaskStep {
+    /**
+     shareData.userStory // 已经拥有的用户描述数据
+     {
+              "keshi": "神经内科",
+              "sympList": [
+                  {
+                      "title": "偏头痛",
+                      "desc": "持续了2天的偏头疼",
+                      "duration": "2天"
+                  },
+                  {
+                      "title": "发冷",
+                      "desc": "感觉发冷,已经有一天",
+                      "duration": "1天"
+                  }
+              ]
+          }
+     */
+    let task1 = new AgentTaskStep({ title: "问诊:医生根据患者情况,主动询问", shareData: options.shareData })
+    task1.handle = () => {
+        return new Promise(async (resolve, reject) => {
+            // 获取用户输入
+            // let userInput = await getUserInput(options.modalCtrl,{fieldsArray:[
+            //     {name:"症状口述",type:"text",desc:"描述下感觉到的症状或不适。"}
+            // ]});
+
+            // if(!userInput?.['症状口述']){
+            //     task1.error = "无症状口述,无法判断科室"
+            //     resolve(false);
+            // }
+
+            // 文本生成
+            let PromptTemplate = `您是一名专业的健身医疗主任医生,患者简单描述了一下他的情况,请您思考下还有哪些需要询问(问题要准确,不超过3个问题)。
+                症状口述:${options.shareData.userStory['症状口述']}
+                结果以JSON格式表示:
+                {
+                    "questionList":[
+                        {"title":"问题标题","desc":"问题描述"}
+                    ]
+                }
+                `
+            let completion = new FmodeChatCompletion([
+                { role: "system", content: "" },
+                { role: "user", content: PromptTemplate }
+            ])
+            completion.sendCompletion().subscribe((message: any) => {
+                if (task1.progress < 0.5) {
+                    task1.progress += 0.1
+                }
+                if (task1.progress >= 0.5 && task1.progress <= 0.9) {
+                    task1.progress += 0.01
+                }
+                if (task1.progress >= 0.7) {
+                    task1.progress += 0.001
+                }
+                // 打印消息体
+                console.log(message.content)
+                // 赋值消息内容给组件内属性
+                if (message.complete) { // 判断message为完成状态,则设置isComplete为完成
+                    options.shareData.userStory.questionList = extactAndParseJsonFromString(message.content).questionList || []
+                    task1.progress = 1
+                    resolve(true)
+                }
+            })
+        })
+
+    }
+    return task1
+}

+ 143 - 0
TFPower-app/src/app/tab2/agent/tasks/poem/inquiry/3.inquiry-user-answer.ts

@@ -0,0 +1,143 @@
+import { AgentTaskStep } from 'src/app/tab2/agent/agent.task';
+import { getUserInput } from 'src/app/tab2/agent/agent.input';
+import { ModalController } from '@ionic/angular/standalone';
+import { FmodeChatCompletion } from 'fmode-ng';
+import { extactAndParseJsonFromString } from 'src/app/tab2/agent/agent.json';
+
+export function TaskInqueryUserAnswer(options: {
+    modalCtrl: ModalController
+    shareData: any
+}
+): AgentTaskStep {
+    /**
+     shareData.userStory // 已经拥有的医生主动询问
+     {
+        "questionList": [
+            {
+                "title": "头痛的性质",
+                "desc": "请问您的偏头痛是怎样的感觉?是刺痛、跳动还是压迫感?"
+            },
+        ]
+    }
+     */
+    let task1 = new AgentTaskStep({ title: "诊断:患者回答具体内容后,医生给出诊断结果", shareData: options.shareData })
+    task1.handle = () => {
+        return new Promise(async (resolve, reject) => {
+            // 获取用户输入的问题清单
+            let questionList = options.shareData.userStory.questionList.map((item: any) => { return { name: item.title, desc: item.desc, type: "text" } })
+
+            let userInputResult = await getUserInput(options.modalCtrl, { fieldsArray: questionList });
+            console.log(userInputResult)
+            //正好是他的标题
+            questionList.forEach((question: any) => {
+                question.answer = userInputResult[question.name]
+            })
+
+            // 文本生成
+            let qaContent = options.shareData.userStory.questionList.map((item: any) => `医生问:${item.title},${item.desc}\n患者答:${item.answer || "没回答"}`).join("\n")
+            let PromptTemplate = `您是一名专业的健身领域的医疗主治医生,根据具体的询问,给出初步诊断。
+                症状口述:${options.shareData.userStory['症状口述']}
+                具体询问:${qaContent}
+                结果以JSON格式表示:
+                症状名称为具体的症状,症状描述为用户的感受,持续时间若没有直接说,可以写近期即可。
+                {
+                    "title":"病例标题",
+                    "desc":"病情概括",
+                    "content":"给出完整的治疗方案和建议"
+                }
+                `
+            let completion = new FmodeChatCompletion([
+                { role: "system", content: "" },
+                { role: "user", content: PromptTemplate }
+            ])
+            completion.sendCompletion().subscribe((message: any) => {
+                if (task1.progress < 0.5) {
+                    task1.progress += 0.1
+                }
+                if (task1.progress >= 0.5 && task1.progress <= 0.9) {
+                    task1.progress += 0.01
+                }
+                if (task1.progress >= 0.9) {
+                    task1.progress += 0.001
+                }
+                // 打印消息体
+                console.log(message.content)
+                // 赋值消息内容给组件内属性
+                if (message.complete) { // 判断message为完成状态,则设置isComplete为完成
+                    options.shareData.diagResult = extactAndParseJsonFromString(message.content)
+                    task1.progress = 1
+                    resolve(true)
+                }
+            })
+        })
+
+    }
+    return task1
+}
+
+
+const TestShareData = {
+    userStory: {
+        "keshi": "神经内科",
+        "sympList": [
+            {
+                "title": "偏头疼",
+                "desc": "已经持续了2天了",
+                "duration": "2天"
+            },
+            {
+                "title": "发冷",
+                "desc": "感觉已经有一天",
+                "duration": "1天"
+            }
+        ],
+        "症状口述": "偏头疼已经持续了2天了,发冷感觉已经有一天。",
+        "questionList": [
+            {
+                "title": "头痛的性质",
+                "desc": "请描述您的头痛是搏动性、压迫性还是其他类型?",
+                "answer": "压迫性头疼"
+            },
+            {
+                "title": "头痛的部位",
+                "desc": "您的头痛主要集中在头部的哪个区域?",
+                "answer": "左侧头疼"
+            },
+            {
+                "title": "伴随症状",
+                "desc": "除了头痛和发冷,您还有其他的症状吗?例如恶心、呕吐、视力模糊或对光敏感等?",
+                "answer": "有点恶心但是没有呕吐"
+            },
+            {
+                "title": "发冷的性质",
+                "desc": "您感到发冷时是否伴随有发热、出汗或其他症状?",
+                "answer": "没有"
+            },
+            {
+                "title": "既往病史",
+                "desc": "您是否有偏头痛或其他头痛的病史?",
+                "answer": "没有"
+            },
+            {
+                "title": "生活习惯",
+                "desc": "您最近的生活习惯是否有变化?例如睡眠不足、压力增大或饮食不规律?",
+                "answer": "经常熬夜,学习压力大"
+            },
+            {
+                "title": "药物使用",
+                "desc": "您是否有服用任何药物来缓解头痛或其他症状?",
+                "answer": "没有"
+            },
+            {
+                "title": "家族病史",
+                "desc": "您的家族中是否有人有类似的头痛或神经系统疾病史?",
+                "answer": "没有"
+            },
+            {
+                "title": "过敏史",
+                "desc": "您是否有药物或其他物质的过敏史?",
+                "answer": "没有"
+            }
+        ]
+    }
+}

+ 55 - 0
TFPower-app/src/app/tab2/agent/tasks/poem/poem-desc.ts

@@ -0,0 +1,55 @@
+import { AgentTaskStep } from 'src/app/tab2/agent/agent.task';
+
+import { ModalController } from '@ionic/angular/standalone';
+import { FmodeChatCompletion } from 'fmode-ng';
+import { getUserInput } from '../../agent.input';
+
+export function TaskPoemPictureDesc(options: {
+    modalCtrl: ModalController
+    shareData: any
+}
+): AgentTaskStep {
+    let task1 = new AgentTaskStep({ title: "输入动作需求", shareData: options.shareData })
+    task1.handle = () => {
+        return new Promise(async (resolve, reject) => {
+            // 获取用户输入的诗词
+            let userInput = await getUserInput(options.modalCtrl, {
+                fieldsArray: [
+                    { name: "目标部位", type: "text", desc: "请输入目标锻炼的肌肉部位" },
+                    { name: "设备限制", type: "text", desc: "是否需要特定设备限制(如徒手、哑铃等)" }
+                ]
+            });
+            if (!userInput?.['目标部位']) {
+                task1.error = "缺少目标部位,请重新开始"
+                resolve(false);
+            }
+
+            // 文本生成
+            let PromptTemplate = `您是一名专业的健身教练,请根据以下描述生成一个简单易懂的健身动作教学画面,包括用箭头语言标记动作细节等。
+                描述如下:
+                ${userInput['目标部位']}${userInput['设备限制']},请确保动作正确。
+                `
+            let completion = new FmodeChatCompletion([
+                { role: "system", content: "" },
+                { role: "user", content: PromptTemplate }
+            ])
+            completion.sendCompletion().subscribe((message: any) => {
+                if (task1.progress < 0.5) {
+                    task1.progress = Math.min(task1.progress + 0.004, 1); // 保证不超过1
+                } else if (task1.progress < 1) {
+                    task1.progress = Math.min(task1.progress + 0.004, 1); // 保证不超过1
+                }
+                // 打印消息体
+                // console.log(message.content)
+                // 赋值消息内容给组件内属性
+                options.shareData.PictureDescResult = message.content
+                if (message.complete) { // 判断message为完成状态,则设置isComplete为完成
+                    task1.progress = 1
+                    resolve(true)
+                }
+            })
+        })
+
+    }
+    return task1
+}

+ 46 - 0
TFPower-app/src/app/tab2/agent/tasks/poem/poem-picture.ts

@@ -0,0 +1,46 @@
+import { ModalController } from '@ionic/angular/standalone';
+import { AgentTaskStep } from '../../agent.task';
+import { ImagineWork, DalleOptions } from 'fmode-ng';
+import { getUserInput } from '../../agent.input';
+
+
+export function TaskPoemPictureCreate(options: {
+  modalCtrl: ModalController
+  shareData: any
+}): AgentTaskStep {
+  let task2 = new AgentTaskStep({ title: "生成动作图像", shareData: options.shareData })
+  task2.handle = () => {
+    return new Promise(resolve => {
+      // let userInput = await getUserInput(options.modalCtrl, {
+      //   fieldsArray: [
+      //     { name: "绘图要求", type: "text", desc: "画风、构图等等" }
+      //   ]
+      // });
+      // console.log(userInput)
+      // console.log("意境绘制:执行过程")
+
+      if (!options.shareData.PictureDescResult) {
+        task2.error = "缺少古风描述结果,请重新执行。"
+        resolve(false)
+      }
+      let imagineWork = new ImagineWork();
+      let PicturePrompt = `${options.shareData.PictureDescResult}\n特点:动作标准清晰易懂。`
+      let imgOptions: DalleOptions = { prompt: PicturePrompt }
+      let countDownInt = setInterval(() => {
+        task2.progress += 0.01
+      }, 500)
+      imagineWork?.draw(imgOptions).subscribe((work: any) => {
+        console.log("imagineWork", work?.toJSON())
+        console.log("images", work?.get("images"))
+        if (work?.get("images")?.length) {
+          options.shareData.images = work?.get("images");
+          clearInterval(countDownInt);
+          task2.progress = 1
+          resolve(true)
+        }
+      })
+    })
+
+  }
+  return task2
+}

+ 52 - 0
TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.html

@@ -0,0 +1,52 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>编辑计划</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="dismiss()">关闭</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-card>
+    <ion-card-header>
+      <ion-card-title>修改计划内容</ion-card-title>
+    </ion-card-header>
+    <ion-card-content>
+      <!-- 日期输入框 -->
+      <ion-item lines="none">
+        <ion-input label="日期" [value]="plan.get('date')" (ionBlur)="onChange('date', $event)"></ion-input>
+      </ion-item>
+
+      <!-- 部位输入框 -->
+      <ion-item lines="none">
+        <ion-input label="部位" [value]="plan.get('trainingPart')" (ionBlur)="onChange(  'trainingPart', $event)">
+        </ion-input>
+      </ion-item>
+
+      <!-- 训练计划 -->
+      <ion-label>训练计划</ion-label>
+      <ion-row *ngFor="let task of plan.get('trainingItems'); let i = index" class="plan-row">
+        <ion-col size="4" class="plan-column">
+          <ion-item lines="none">
+            <ion-input label="项目{{i + 1}}" [value]="task.item || ''" (ionBlur)="onTaskChange(i, 'item', $event)">
+            </ion-input>
+          </ion-item>
+        </ion-col>
+        <ion-col size="3">
+          <ion-item lines="none">
+            <ion-input label="组数" [value]="task.sets" (ionBlur)="onTaskChange(i, 'sets', $event)"></ion-input>
+          </ion-item>
+        </ion-col>
+        <ion-col size="3">
+          <ion-item lines="none">
+            <ion-input label="次数" [value]="task.reps" (ionBlur)="onTaskChange(i, 'reps', $event)"></ion-input>
+          </ion-item>
+        </ion-col>
+      </ion-row>
+
+      <!-- 保存按钮 -->
+      <ion-button expand="full" color="primary" (click)="saveChanges()">保存修改</ion-button>
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 26 - 0
TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.scss

@@ -0,0 +1,26 @@
+.plan-row {
+  margin-bottom: 15px;
+}
+
+.plan-column {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+
+ion-item {
+  margin-bottom: 5px;
+}
+.horizontal-item {
+  display: flex;
+  align-items: center; /* 垂直居中对齐 */
+  justify-content: space-between; /* 水平分配空间 */
+}
+
+.horizontal-item ion-label {
+  flex: 0 0 auto; /* 标签不扩展 */
+}
+
+.horizontal-item ion-input {
+  flex: 1 1 auto; /* 输入框扩展占满剩余空间 */
+}

+ 24 - 0
TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { EditPlanModalComponent } from './edit-plan-modal.component';
+
+describe('EditPlanModalComponent', () => {
+  let component: EditPlanModalComponent;
+  let fixture: ComponentFixture<EditPlanModalComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [ EditPlanModalComponent ],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(EditPlanModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 61 - 0
TFPower-app/src/app/tab2/edit-plan-modal/edit-plan-modal.component.ts

@@ -0,0 +1,61 @@
+import { Component, Input } from '@angular/core';
+import { ModalController } from '@ionic/angular/standalone';
+import { IonRow, IonCol, IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonContent, IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonItem, IonLabel, IonInput, IonToggle } from '@ionic/angular/standalone';
+import { FormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+@Component({
+  selector: 'app-edit-plan-modal',
+  templateUrl: './edit-plan-modal.component.html',
+  styleUrls: ['./edit-plan-modal.component.scss'],
+  standalone: true,
+  imports: [
+    IonRow,
+    IonCol,
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonButtons,
+    IonButton,
+    IonContent,
+    IonCard,
+    IonCardHeader,
+    IonCardTitle,
+    IonCardContent,
+    IonItem,
+    IonLabel,
+    IonInput,
+    IonToggle,
+    FormsModule,
+    CommonModule
+  ]
+})
+export class EditPlanModalComponent {
+  @Input() plan: any;  // 接收传入的计划数据
+  constructor(private modalCtrl: ModalController) { }
+  // 更新数据字段
+  onChange(field: string, event: any) {
+    const value = event.target.value;
+    console.log('Before update:', this.plan.data);
+    this.plan.set({ [field]: value });  // 使用 set 来更新数据
+  }
+  // 处理任务项的变化
+  onTaskChange(index: number, field: string, event: any) {
+    const value = event.target.value;
+    this.plan.data.trainingItems[index][field] = value;
+    const updatedTrainingItems = this.plan.data.trainingItems;
+    this.plan.set({ "trainingItems": updatedTrainingItems });
+  }
+  // 保存修改并更新数据库
+  async saveChanges() {
+    try {
+      await this.plan.save();
+      console.log('计划修改成功');
+      this.modalCtrl.dismiss(this.plan);
+    } catch (error) {
+      console.error('保存失败:', error);
+    }
+  }
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+}

+ 214 - 10
TFPower-app/src/app/tab2/tab2.page.html

@@ -1,17 +1,221 @@
-<ion-header [translucent]="true">
+<ion-header sticky>
   <ion-toolbar>
-    <ion-title>
-
-    </ion-title>
+    <ion-segment [(ngModel)]="selectedTab" color="primary" class="custom-segment">
+      <ion-segment-button value="checkin">
+        <ion-icon aria-hidden="true" name="calendar"></ion-icon>
+        打卡
+      </ion-segment-button>
+      <ion-segment-button value="plan">
+        <ion-icon name="checkmark-circle" aria-hidden="true"></ion-icon>
+        计划
+      </ion-segment-button>
+      <ion-segment-button value="consultation">
+        <ion-icon name="help-circle" aria-hidden="true"></ion-icon>
+        问诊
+      </ion-segment-button>
+    </ion-segment>
   </ion-toolbar>
 </ion-header>
 
 <ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
-    <ion-toolbar>
-      <ion-title size="large">Tab 2</ion-title>
-    </ion-toolbar>
-  </ion-header>
+  <div style="height: 56px;"></div>
+
+  <!-- 打卡 -->
+  <div *ngIf="selectedTab === 'checkin'" class="checkin-container">
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>今天的打卡任务</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p>在这里打卡,开始你的一天健身之旅吧!</p>
+      </ion-card-content>
+    </ion-card>
+    <ion-card>
+      <!-- 未登录 -->
+      @if(!currentUser?.id){
+      <ion-card-header>
+        <ion-card-title>请登录</ion-card-title>
+        <ion-card-subtitle>暂无信息</ion-card-subtitle>
+      </ion-card-header>
+      }
+      <!-- 未登录 -->
+      @if(currentUser?.id){
+      <ion-card-header>
+        <ion-card-title>{{currentUser?.get("username")}} {{currentUser?.get("realname")}}</ion-card-title>
+        <ion-card-subtitle>性别:{{currentUser?.get("gender")||"-"}} 年龄:{{currentUser?.get("age")||"-"}}
+        </ion-card-subtitle>
+      </ion-card-header>
+      }
+      <ion-card-content>
+        @if(!currentUser?.id){
+        <ion-button expand="block" (click)="signup()">注册</ion-button>
+        <ion-button expand="block" (click)="login()">登录</ion-button>
+        }
+        @if(currentUser?.id){
+        <ion-button expand="block" (click)="editUser()">编辑资料</ion-button>
+        <ion-button expand="block" (click)="logout()" color="light">登出</ion-button>
+        }
+      </ion-card-content>
+    </ion-card>
+  </div>
+
+  <!-- 计划 -->
+  <div *ngIf="selectedTab === 'plan'" class="plan-container">
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>我的本周计划</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <div class="plan-table">
+          <ion-grid class="table">
+            <ion-row>
+              <ion-col size="1" class="grid-header">日期</ion-col>
+              <ion-col size="1" class="grid-header">部位</ion-col>
+              <ion-col size="1.5" class="grid-header">项目1</ion-col>
+              <ion-col size="1.5" class="grid-header">项目2</ion-col>
+              <ion-col size="1.5" class="grid-header">项目3</ion-col>
+              <ion-col size="1.5" class="grid-header">项目4</ion-col>
+              <ion-col size="1.5" class="grid-header">状态</ion-col>
+              <ion-col size="2" class="grid-header">操作</ion-col> <!-- 操作列 -->
+            </ion-row>
+
+            <!-- 绑定计划数据 -->
+            <ion-row *ngFor="let day of planList">
+              <ion-col size="1" class="plan-column">{{ day.get('date') }}</ion-col>
+              <ion-col size="1" class="plan-column">{{ day.get('trainingPart') }}</ion-col>
+
+              <!-- 显示每个训练项目,确保即使为空也占位 -->
+              <ion-col size="1.5" *ngFor="let task of day.get('trainingItems'); let i = index" class="plan-column">
+                {{ task.item || '' }}: {{task.sets}} x {{task.reps}}
+              </ion-col>
+
+              <ion-col size="1.5" class="plan-column">
+                <ion-list>
+                  <ion-item>
+                    <ion-select aria-label="fruit" value="未完成">
+                      <ion-select-option value="未完成">未完成</ion-select-option>
+                      <ion-select-option value="已完成">已完成</ion-select-option>
+                      <ion-select-option value="修改中">修改中</ion-select-option>
+                    </ion-select>
+                  </ion-item>
+                </ion-list>
+              </ion-col>
+
+              <!-- 修改和删除按钮 -->
+              <ion-col size="2" class="ion-text-center">
+                <ion-buttons>
+                  <ion-button color="success" (click)="editPlan(day)">
+                    <ion-icon slot="start" name="create"></ion-icon> 修改
+                  </ion-button>
+                  <ion-button color="danger" (click)="deletePlan(day)">
+                    <ion-icon slot="start" name="trash"></ion-icon> 删除
+                  </ion-button>
+                </ion-buttons>
+              </ion-col>
+            </ion-row>
+          </ion-grid>
+        </div>
+
+        <ion-button expand="full" color="primary" (click)="goToPage('test-page')">重新生成计划</ion-button>
+      </ion-card-content>
+    </ion-card>
+  </div>
+
+  <!-- 问诊 -->
+  <div *ngIf="selectedTab === 'consultation'" class="consult">
+    <!-- 健康问诊卡片 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>健康问诊</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p>有任何不适,可以随时询问!</p>
+
+        <!-- 健身建议和疼痛咨询左右分布 -->
+        <ion-row>
+          <!-- 健身建议 -->
+          <ion-col size="6" class="ion-text-center">
+            <ion-img src="../../assets/images/action.png" alt="健身建议"></ion-img>
+            <ion-button expand="full" color="secondary" (click)="doPoemTask()">生成健身动作</ion-button>
+          </ion-col>
+
+          <!-- 疼痛咨询 -->
+          <ion-col size="6" class="ion-text-center">
+            <ion-img src="../../assets/images/ache.jpg" alt="疼痛咨询"></ion-img>
+            <ion-button expand="full" color="tertiary" (click)="doInqueryTask()">疼?点这里!</ion-button>
+          </ion-col>
+        </ion-row>
+
+        <!-- 任务区域 -->
+        <ion-card>
+          <ion-card-header>
+            <ion-card-title>健康任务</ion-card-title>
+          </ion-card-header>
+          <ion-card-content>
+            <div *ngFor="let step of taskList">
+              <ion-item>
+                <!-- 待开始 -->
+                <ion-icon *ngIf="step.progress === 0 && !step.error" name="radio-button-off-outline"></ion-icon>
+                <!-- 进行中 -->
+                <ion-icon *ngIf="step.progress !== 0 && step.progress !== 1" name="reload-outline"></ion-icon>
+                <!-- 已完成 -->
+                <ion-icon *ngIf="step.progress === 1" name="checkmark-circle-outline"></ion-icon>
+                <!-- 已出错 -->
+                <ion-icon *ngIf="step.error" name="close-circle-outline"></ion-icon>
+                {{ step.title }}
+                <span *ngIf="step.progress">{{ step.progress * 100 | number:'2.0-0' }}%</span>
+                <span *ngIf="step.error" style="color:red;">{{ step.error }}</span>
+              </ion-item>
+            </div>
+          </ion-card-content>
+        </ion-card>
+
+        <!-- 图片展示 -->
+        <ion-card *ngIf="shareData.images">
+          <ion-card-header>
+            <ion-card-title>动作展示</ion-card-title>
+          </ion-card-header>
+          <ion-card-content>
+            <div *ngFor="let imageUrl of shareData.images">
+              <img [src]="imageUrl" alt="诊断图片" style="width: 100%; height: 400px; object-fit: cover;" />
+            </div>
+          </ion-card-content>
+        </ion-card>
+
+        <!-- 诊断结果 -->
+        <ion-card *ngIf="shareData.diagResult">
+          <ion-card-header>
+            <ion-card-title>{{ shareData.diagResult.title }}</ion-card-title>
+          </ion-card-header>
+          <ion-card-content>
+            <h2>{{ shareData.diagResult.desc }}</h2>
+            <fm-markdown-preview class="content-style" [content]=shareData.diagResult.content>
+            </fm-markdown-preview>
+          </ion-card-content>
+        </ion-card>
 
-  <app-explore-container name="Tab 2 page"></app-explore-container>
+        <!-- AI教练互动 -->
+        <ion-card>
+          <ion-card-header>
+            <ion-card-title>顶级教练专区</ion-card-title>
+            <ion-card-subtitle>教练简介</ion-card-subtitle>
+          </ion-card-header>
+          <ion-card-content>
+            <ion-list>
+              <ion-item (click)="openInquiry(coach)" *ngFor="let coach of coachList" lines="none">
+                <ion-thumbnail slot="start">
+                  <img src="../../assets/images/coach1.jpg" alt="coach.get('name')">
+                </ion-thumbnail>
+                <div class="coach-info">
+                  <h3>{{ coach.get('name') }}({{ coach.get('age') }}岁)</h3>
+                  <p>擅长领域:{{ coach.get('specialize')}}</p>
+                  <p>WiseFitness俱乐部</p>
+                </div>
+              </ion-item>
+            </ion-list>
+          </ion-card-content>
+        </ion-card>
+      </ion-card-content>
+    </ion-card>
+  </div>
 </ion-content>

+ 215 - 0
TFPower-app/src/app/tab2/tab2.page.scss

@@ -0,0 +1,215 @@
+//顶部栏部分
+ion-header {
+  position: fixed;
+  z-index: 10; /* 确保它在其他内容之上 */
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 56px; /* 设置固定头部高度 */
+}
+
+.custom-segment {
+  width: 60%; 
+}
+
+ion-icon {
+  --ionicon-stroke-width: 16px;
+  margin-bottom: 5px;
+}
+
+ion-segment-button::part(indicator-background) {
+  background: #719e8c;
+}
+
+ion-segment-button.md::part(native) {
+  color: #000;
+  font-size: 12px;
+}
+
+.segment-button-checked.md::part(native) {
+  color: #08a391;
+  font-size: 20px;
+}
+
+ion-segment-button.md::part(indicator-background) {
+  height: 2px;
+}
+
+ion-content {
+  padding-top: 56px; /* 给内容区域加上与header相等的顶部间距 */
+}
+ion-content {
+  background-color: #f9f9f9;
+  padding-top: 56px;
+}
+// plan部分css
+/* 给表格增加阴影和圆角 */
+.plan-table ion-grid {
+  margin-top: 20px;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+  width: 100%; 
+
+}
+/* 计划表格的列间距和背景 */
+.plan-table ion-row {
+  margin-bottom: 1px;
+}
+
+/* 表格行的底部间距 */
+.plan-table ion-col {
+  margin-bottom: 1px;
+  border-bottom: 1px solid #ddd;
+  
+}
+
+/* 表格的列 */
+.plan-column {
+  background-color: #ffffff;
+  padding: 12px;
+  border-bottom: 1px solid #ddd;
+  text-align: center;
+}
+
+/* 表格的标题 */
+.grid-header {
+  font-weight: bold;
+  background-color: #f0f0f0;
+  text-align: center;
+  padding: 12px;
+  border-bottom: 1px solid #ddd;
+}
+.plan-table ion-select {
+  width: 50%;  /* 调整宽度 */
+  height: 50%;
+  margin: 1px;
+}
+
+/* 修改状态框的选项 */
+.plan-table ion-select .select-interface-option {
+  font-size: 12px;  /* 修改选项字体大小 */
+  color: #555;  /* 修改选项字体颜色 */
+}
+
+/* 修改选中时的背景颜色 */
+.plan-table ion-select .select-placeholder {
+  color: #719e8c;  /* 占位符字体颜色 */
+}
+
+/* 修改下拉箭头的颜色 */
+.plan-table ion-select .select-icon {
+  color: #719e8c;  /* 下拉箭头颜色 */
+}
+
+/* 当选择框被激活时的样式 */
+
+.plan-table ion-button {  
+  margin-right: 10px; 
+  text-align: center; 
+  justify-content: center; 
+}
+.plan-table ion-button[color="success"]::part(native)
+{
+   justify-content: center;
+  align-items: center;
+color: white !important; /* 强制修改文本颜色为黑色 */
+}
+.plan-table ion-button[color="danger"]::part(native)
+{
+   justify-content: center;
+  align-items: center;
+color: white !important; /* 强制修改文本颜色为黑色 */
+}
+/* 按钮 */
+.plan-table ion-button[color="success"] {
+  background-color: #59beec; /* 绿色背景 */
+  border-radius: 4px;
+  padding: 5px 5px;
+  font-weight: bold;
+   justify-content: center;
+  align-items: center;
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+  transition: background-color 0.3s ease, transform 0.3s ease;
+}
+
+.plan-table ion-button[color="success"]:hover {
+  background-color: #48a7cc; /* 悬停时颜色变深 */
+  transform: scale(1.05); /* 悬停时按钮放大 */
+}
+
+.plan-table ion-button[color="danger"] {
+  background-color: #f44336; /* 红色背景 */
+  border-radius: 4px;
+  padding: 5px 5px;
+  font-weight: bold;
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+  transition: background-color 0.3s ease, transform 0.3s ease;
+}
+.plan-table ion-button[color="danger"]:hover {
+  background-color: #d32f2f; /* 悬停时颜色变深 */
+  transform: scale(1.05); /* 悬停时按钮放大 */
+}
+/* 优化按钮在行中的显示位置 */
+.plan-table ion-buttons {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+/* 给每个section加标题 */
+.section-title {
+  font-size: 20px;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20px;
+}
+
+/* 问诊部分的设计 */
+/* 确保图片等大 */
+.consult .ion-text-center ion-img {
+  width: 100%; /* 设置宽度为 100% */
+  height: 200px; /* 设置固定高度,确保两张图片等高 */
+  object-fit: cover; /* 保持图片比例,同时裁剪图片以适应区域 */
+}
+.consult ion-item {
+  margin-bottom: 12px; /* 增加医生卡片之间的间距 */
+  padding: 12px; /* 调整卡片内边距 */
+  border-radius: 8px; /* 圆角设计 */
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* 添加卡片阴影 */
+  background-color: #ffffff; /* 卡片背景色 */
+}
+
+.consult ion-thumbnail {
+  width: 60px; /* 缩略图宽度 */
+  height: 60px; /* 缩略图高度 */
+  border-radius: 50%; /* 圆形头像 */
+}
+
+.consult .coach-info {
+  margin-left: 12px; /* 缩略图与文字的间距 */
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.consult .coach-info h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.consult .coach-info p {
+  margin: 4px 0;
+  font-size: 14px;
+  color: #666; /* 字体颜色 */
+}
+
+
+/* 打卡部分的设计 */
+.checkin-container p {
+  font-size: 18px;
+  color: #555;
+}
+
+
+
+

+ 333 - 7
TFPower-app/src/app/tab2/tab2.page.ts

@@ -1,16 +1,342 @@
-import { Component } from '@angular/core';
-import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular/standalone';
-import { ExploreContainerComponent } from '../explore-container/explore-container.component';
-
+import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
+import { Router } from '@angular/router';
+import { addIcons } from 'ionicons';
+import { checkmarkCircle, trash, calendar, helpCircle, create } from 'ionicons/icons';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { IonSelect, IonThumbnail, IonCardSubtitle, IonImg, IonCard, IonButtons, IonItem, IonList, IonHeader, IonIcon, IonToolbar, IonContent, IonSegment, IonSegmentButton, IonGrid, IonRow, IonCol, IonButton, IonLabel, IonBadge, IonInput, ModalController, IonCardTitle, IonCardContent, IonCardHeader, IonSelectOption } from '@ionic/angular/standalone';
+import { FmodeChatCompletion, ImagineWork, DalleOptions, ChatPanelOptions, FmodeChat, FmodeChatMessage, MarkdownPreviewModule, openChatPanelModal } from "fmode-ng";
+import { AgentTaskStep } from './agent/agent.task';
+import { TaskPoemPictureDesc } from './agent/tasks/poem/poem-desc';
+import { TaskPoemPictureCreate } from './agent/tasks/poem/poem-picture';
+import { startTask } from './agent/agent.start';
+import { TaskInqueryUserStory } from './agent/tasks/poem/inquiry/1.inquiry-user-story';
+import { TaskInqueryDoctorQuestion } from './agent/tasks/poem/inquiry/2.inquiry-doctor-question';
+import { TaskInqueryUserAnswer } from './agent/tasks/poem/inquiry/3.inquiry-user-answer';
+import { CloudObject, CloudQuery, CloudUser } from 'src/lib/ncloud';
+import { EditPlanModalComponent } from './edit-plan-modal/edit-plan-modal.component';
+import { AlertController } from '@ionic/angular';
+import { openUserEditModal } from 'src/lib/user/modal-user-edit/modal-user-edit.component';
+import { openUserLoginModal } from 'src/lib/user/modal-user-login/modal-user-login.component';
 @Component({
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
   styleUrls: ['tab2.page.scss'],
   standalone: true,
-  imports: [IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent]
+  imports: [
+    MarkdownPreviewModule,
+    IonSelectOption,
+    IonSelect,
+    IonThumbnail,
+    IonCardSubtitle,
+    IonImg,
+    IonCard,
+    IonCardTitle,
+    IonCardHeader,
+    IonCardContent,
+    IonButtons,
+    IonItem,
+    IonList,
+    IonIcon,
+    FormsModule,
+    CommonModule,
+    IonHeader,
+    IonToolbar,
+    IonContent,
+    IonSegment,
+    IonSegmentButton,
+    IonGrid,
+    IonRow,
+    IonCol,
+    IonButton,
+    IonLabel,
+    IonBadge,
+    IonInput
+  ]
 })
-export class Tab2Page {
+export class Tab2Page implements OnInit {
+  selectedTab: string = 'checkin';  // 默认选中的tab
+  planList: any[] = [
+  ];
+  coachList: any[] = [
+  ];
+  currentUser: CloudUser | undefined
+  constructor(private router: Router, private modalCtrl: ModalController, private cdr: ChangeDetectorRef, private alertController: AlertController) {
+    addIcons({ checkmarkCircle, calendar, helpCircle, trash, create });
+    this.currentUser = new CloudUser();
+  }
+  async loadPlanList() {
+    let currentUser = new CloudUser();
+    const cloudQuery = new CloudQuery("fitPlan");
+    cloudQuery.equalTo("user", currentUser.toPointer());
+    this.planList = await cloudQuery.find();
+    this.cdr.detectChanges();
+  }
+  async loadCoachList() {
+    let query = new CloudQuery("Coach");
+    this.coachList = await query.find();
+    this.cdr.detectChanges();
+  }
+  ngOnInit() {
+    this.loadPlanList()
+    this.loadCoachList()
+  }
+  ngOnChanges() {
+    this.loadPlanList();
+    this.loadCoachList()
+  }
+  ngAfterViewChecked() {
+    this.loadPlanList();
+    this.loadCoachList()
+  }
+  async login() {
+    let user = await openUserLoginModal(this.modalCtrl);
+    if (user?.id) {
+      this.currentUser = user
+    }
+  }
+  async signup() {
+    // 弹出注册窗口
+    let user = await openUserLoginModal(this.modalCtrl, "signup");
+    if (user?.id) {
+      this.currentUser = user
+    }
+  }
+  logout() {
+    this.currentUser?.logout();
+  }
+
+  editUser() {
+    openUserEditModal(this.modalCtrl)
+  }
+
+  //plan页面
+  editPlan(day: any) {
+    console.log('编辑计划:', day);
+
+    // 创建一个弹出框
+    this.modalCtrl.create({
+      component: EditPlanModalComponent,
+      componentProps: { plan: day }
+    }).then(modal => {
+      modal.present();
+
+      modal.onDidDismiss().then((result) => {
+        if (result.data) {
+          const updatedPlan = result.data;
+          const index = this.planList.findIndex(item => item.id === updatedPlan.id);
+          if (index !== -1) {
+            this.planList[index] = updatedPlan;
+          }
+        }
+      });
+    });
+  }
+  async deletePlan(day: any) {
+    const alert = await this.alertController.create({
+      header: '确认删除',
+      message: '确定要删除此计划吗?',
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel',
+          cssClass: 'secondary',
+          handler: () => {
+            console.log('删除操作被取消');
+          }
+        },
+        {
+          text: '确认',
+          handler: () => {
+            day.destroy()
+              .then(() => {
+                console.log('计划已删除');
+                this.loadPlanList();
+              })
+              .catch((error: any) => {
+                console.error('删除失败:', error);
+              });
+          }
+        }
+      ]
+    });
+
+    await alert.present();
+  }
+
+  //任务链
+  taskList: AgentTaskStep[] = []
+  //一个等待一秒的函数  每经过一秒
+  wait(duration: number = 1000) {
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve(true)
+      }, duration);
+    })
+  }
+  shareData: any = {}
+  // 任务:完成故事意境描述及图像绘制
+  doPoemTask() {
+    let task1 = TaskPoemPictureDesc({ shareData: this.shareData, modalCtrl: this.modalCtrl });
+    let task2 = TaskPoemPictureCreate({ shareData: this.shareData, modalCtrl: this.modalCtrl });
+    let PoemTaskList = [task1, task2]
+    this.taskList = PoemTaskList
+    startTask(PoemTaskList)
+  }
+  doInqueryTask() {
+    let task1 = TaskInqueryUserStory({ shareData: this.shareData, modalCtrl: this.modalCtrl });
+    let task2 = TaskInqueryDoctorQuestion({ shareData: this.shareData, modalCtrl: this.modalCtrl });
+    let task3 = TaskInqueryUserAnswer({ shareData: this.shareData, modalCtrl: this.modalCtrl });
+    // 定义任务集
+    let InquireServiceTaskList = [
+      task1, task2, task3
+    ]
+    // 传递给显示组件
+    this.taskList = InquireServiceTaskList
+    // 开始执行任务
+    startTask(InquireServiceTaskList)
+  }
+  // 聊天页面
+  async openInquiry(coach: CloudObject) {
+    let currentUser = new CloudUser();
+    let userPrompt = ``
+    if (!currentUser?.id) {
+      console.log("用户未登录,请登录后重试");
+      let user = await openUserLoginModal(this.modalCtrl);
+      if (!user?.id) {
+        return
+      }
+      currentUser = user;
+    }
+
+    if (currentUser?.get("realname")) {
+      userPrompt += `当前来访的患者,姓名:${currentUser?.get("realname")}`
+    }
+    if (currentUser?.get("gender")) {
+      userPrompt += `,性别:${currentUser?.get("gender")}`
+    }
+    if (currentUser?.get("age")) {
+      userPrompt += `,年龄:${currentUser?.get("age")}`
+    }
+
+
+    localStorage.setItem("company", "E4KpGvTEto")
+
+    let consult = new CloudObject("fitConsultation")
+    let now = new Date();
+    let dateStr = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`
+    // 对象权限的精确指定
+    let ACL: any = {
+      "*": { read: false, write: false }
+    }
+    if (currentUser?.id) {
+      ACL[currentUser?.id] = { read: true, write: true }
+    }
+    localStorage.setItem("company", "E4KpGvTEto")
+    consult.set({
+      title: `交流记录${dateStr}-${coach?.get("name")}`,
+      coach: coach.toPointer(),
+      user: currentUser.toPointer(),
+      ACL: ACL
+    })
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      onChatInit: (chat: FmodeChat) => {
+        console.log("onChatInit");
+        console.log("预设角色", chat.role);
+        chat.role.set("name", coach?.get("name"));
+        chat.role.set("title", "职业教练");
+        chat.role.set("tags", coach?.get("specialize"));
+        chat.role.set("avatar", "../../assets/images/coach2.jpg")
+        chat.role.set("prompt", `
+# 角色设定
+您是${coach?.get("name")},年龄${coach?.get("age")},特长为${coach?.get("specialize")},要完成一次教练与学员之间的锻炼部位交流。
+
+# 对话环节
+0.导诊(根据用户基本情况,引导选择合适的训练计划) 
+1.预设的问询方式(根据学员自述情况进行引导)
+
+- 打招呼,以学员自述为主
+- 当信息充足时,确认学员的目标与需求,并进入下一个环节 
+2.拓展的问询细节 
+例如:学员反映想要锻炼腹肌,拓展出:目前的锻炼频率;饮食习惯;是否有受伤历史等其他需要的问题。
+- 当问询细节补充完成后进入下一个环节 
+3.初步的训练方案,并且同时列出需要的器械与注意事项 初步方案:确定需要的训练动作与频率 器械与注意事项:获取训练所需的器械信息
+- 等待学员确认并准备器械,进入下一阶段 
+4.给出详细的训练计划并提供指导
+- 完成训练计划时,请在消息结尾附带: [交流完成]
+# 开始话语
+当您准备好了,可以以一个健身教练的身份,向来访的学员打招呼。
+你好!欢迎来到健身房,我是${coach?.get("name")}教练。今天你想要专注锻炼哪个部位呢?或者有什么具体的健身目标吗?`);
+      },
+      onMessage: (chat: FmodeChat, message: FmodeChatMessage) => {
+        console.log("onMessage", message)
+        let content: any = message?.content
+        if (typeof content == "string") {
+          if (content?.indexOf("[交流完成]") > -1) {
+            console.log("交流已完成")
+            consult.set({
+              content: content // 处方内容
+            })
+            consult.save();
+          }
+        }
+      },
+      onChatSaved: (chat: FmodeChat) => {
+        // chat?.chatSession?.id 本次会话的 chatId
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
+      }
+    }
+    openChatPanelModal(this.modalCtrl, options)
+  }
+  openChat() {
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      onChatSaved: (chat: FmodeChat) => {
+        // chat?.chatSession?.id 本次会话的 chatId
+        console.log("onChatSaved", chat, chat?.chatSession, chat?.chatSession?.id)
+      },
+    }
+    openChatPanelModal(this.modalCtrl, options)
+  }
+  restoreChat(chatId: string) {
+    let options: ChatPanelOptions = {
+      roleId: "2DXJkRsjXK",
+      chatId: chatId
+    }
+    openChatPanelModal(this.modalCtrl, options)
+  }
+  goChat() {
+    this.router.navigateByUrl("/chat/session/role/2DXJkRsjXK")
+  }
+  // audioModalHeightPoint:number = 0.35;
+  // async startTalk(){
+  //   // 根据手机兼容性,适配组件弹出高度
+  //   let height = document.body.clientHeight || 960;
+  //   this.audioModalHeightPoint = Number((165/height).toFixed(2));
 
-  constructor() {}
+  //   // 弹出组件
+  //   let modal:any
+  //   let chat:any
+  //   modal = await this.modalCtrl.create({
+  //     component:ModalAudioMessageComponent,
+  //     componentProps:{
+  //       chat:chat,
+  //       modal:modal,
+  //       onBreakPointSet:()=>{
+  //         modal?.setCurrentBreakpoint(this.audioModalHeightPoint)
+  //       }
+  //     }
+  //   })
+  //   modal.present();
+  // }
+  // 页面跳转功能
+  goToPage(page: string) {
+    // 更新选中的tab
+    this.selectedTab = page;
+    this.router.navigate([`/tabs/${page}`]);  // 然后再进行路由跳转
 
+    // 手动检测视图变化,确保数据绑定正确
+    this.cdr.detectChanges();
+  }
 }

+ 23 - 0
TFPower-app/src/app/tab2/tag-input/tag-input.component.html

@@ -0,0 +1,23 @@
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>添加标签</ion-card-title>
+  </ion-card-header>
+
+  <ion-card-content>
+    <div class="input-container">
+      <ion-input [(ngModel)]="tagInput" placeholder="例如:减脂,增肌..." (keydown.enter)="addTag()"></ion-input>
+      <ion-button expand="full" (click)="addTag()">添加标签</ion-button>
+    </div>
+
+    <!-- 标签显示区域 -->
+    <div class="tag-container">
+      <ion-chip *ngFor="let tag of tags" class="tag-chip">
+        <ion-label>{{ tag }}</ion-label>
+        <!-- 删除按钮 -->
+        <ion-icon name="close" (click)="removeTag(tag)" class="close-icon"></ion-icon>
+      </ion-chip>
+    </div>
+
+    <p>{{tagInput}}</p>
+  </ion-card-content>
+</ion-card>

+ 36 - 0
TFPower-app/src/app/tab2/tag-input/tag-input.component.scss

@@ -0,0 +1,36 @@
+.input-container {
+  display: flex;
+  align-items: center;
+  gap: 10px; 
+  margin-bottom: 10px; 
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.tag-chip {
+  display: flex;
+  align-items: center;
+  position: relative; 
+  background-color: var(--ion-color-light);
+}
+
+.close-icon {
+  position: absolute;
+  right: 5px; 
+  top: 50%; 
+  transform: translateY(-50%); 
+  cursor: pointer;
+  font-size: 12px; 
+  color: var(--ion-color-danger); 
+}
+
+ion-chip {
+  --padding-start: 10px;
+  --padding-end: 10px;
+  --border-radius: 20px;
+}

+ 24 - 0
TFPower-app/src/app/tab2/tag-input/tag-input.component.spec.ts

@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { IonicModule } from '@ionic/angular';
+
+import { TagInputComponent } from './tag-input.component';
+
+describe('TagInputComponent', () => {
+  let component: TagInputComponent;
+  let fixture: ComponentFixture<TagInputComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [TagInputComponent],
+      imports: [IonicModule.forRoot()]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(TagInputComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 39 - 0
TFPower-app/src/app/tab2/tag-input/tag-input.component.ts

@@ -0,0 +1,39 @@
+import { Component, Output, EventEmitter } from '@angular/core';
+import { IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonInput, IonButton, IonChip, IonLabel, IonIcon } from '@ionic/angular/standalone';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { addIcons } from 'ionicons';
+import { close, barbellOutline, personOutline, square, alarmOutline } from 'ionicons/icons';
+@Component({
+  selector: 'app-tag-input',
+  templateUrl: './tag-input.component.html',
+  styleUrls: ['./tag-input.component.scss'],
+  standalone: true,
+  imports: [IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonInput, IonButton, IonChip, IonLabel, IonIcon, CommonModule, FormsModule]
+})
+
+export class TagInputComponent {
+  tags: string[] = [];  // 标签列表
+  tagInput: string = '';  // 输入框的值
+
+  // 定义一个事件输出,类型为string[]
+  @Output() tagsChanged: EventEmitter<string[]> = new EventEmitter<string[]>();
+
+  // 添加标签方法
+  addTag() {
+    if (this.tagInput && !this.tags.includes(this.tagInput)) {
+      this.tags.push(this.tagInput);  // 将标签添加到数组
+      this.tagsChanged.emit(this.tags);  // 发射更新后的标签列表
+      this.tagInput = '';  // 清空输入框
+    }
+  }
+
+  // 删除标签方法
+  removeTag(tag: string) {
+    this.tags = this.tags.filter(t => t !== tag);  // 从标签数组中移除
+    this.tagsChanged.emit(this.tags);  // 发射更新后的标签列表
+  }
+  constructor() {
+    addIcons({ close, personOutline, barbellOutline, alarmOutline, square });
+  }
+}

+ 9 - 0
TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.html

@@ -0,0 +1,9 @@
+<app-chat-panel *ngIf="leftButtons?.length&&modelList?.length" #chatComp 
+[roleId]="roleId" 
+[chatId]="chatId" 
+[leftButtons]="leftButtons" 
+[modelList]="modelList" 
+[isDirect]="isDirect"
+[hideModalSelect]="hideModalSelect"
+[hideInputPreview]="hideInputPreview"
+></app-chat-panel>

+ 3 - 0
TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.scss

@@ -0,0 +1,3 @@
+app-chat-panel {
+    height: 100vh;
+}

+ 22 - 0
TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { TestChatPanelComponent } from './test-chat-panel.component';
+
+describe('TestChatPanelComponent', () => {
+  let component: TestChatPanelComponent;
+  let fixture: ComponentFixture<TestChatPanelComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [TestChatPanelComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(TestChatPanelComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 243 - 0
TFPower-app/src/app/tab2/test-chat-panel/test-chat-panel.component.ts

@@ -0,0 +1,243 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { ModalController } from '@ionic/angular/standalone';
+import { ChatPanelComponent } from 'fmode-ng'
+import Parse from "parse";
+import { combineLatest } from 'rxjs';
+
+// 添加Icons
+import { addIcons } from 'ionicons';
+import * as icons from 'ionicons/icons';
+addIcons(icons);
+
+@Component({
+  selector: 'app-test-chat-panel',
+  templateUrl: './test-chat-panel.component.html',
+  styleUrls: ['./test-chat-panel.component.scss'],
+  standalone: true,
+  imports:[
+    CommonModule,
+    ChatPanelComponent,
+  ]
+})
+export class TestChatPanelComponent  implements OnInit {
+  @ViewChild(ChatPanelComponent) chatComp:ChatPanelComponent|undefined
+  leftButtons:any[]=[]
+  modelList:any[]=[]
+  isDirect:boolean=true;
+  hideShare:boolean=true;
+  hideModalSelect:boolean=true;
+  hideInputPreview:boolean = true;
+  chatId:string = ""
+  roleId:string = ""
+  pid:string = ""
+  constructor(
+    private route:ActivatedRoute,
+    private cdRef:ChangeDetectorRef,
+    private modalCtrl:ModalController
+  ) { 
+    combineLatest([this.route.params,this.route.queryParams]).subscribe(async (data:any)=>{
+      let params = data[0] || {}
+
+      this.chatId = params['chatId'] || this.chatId || null;
+      this.roleId = params['roleId'] || this.roleId || null;
+      this.pid = params['pid'] || this.pid || null;
+      console.log("this.pid",this.pid)
+      // 异步加载的后续数据 操作按钮
+      let bint = setInterval(() => {
+        if(this.roleId){
+          clearInterval(bint);
+          return
+        }
+        this.initPanelConfig();
+      }, 2000);
+    })
+  }
+
+
+  ngOnInit() {
+        this.initPanelConfig();
+        // 异步加载的后续数据 提示词
+        let pint = setInterval(() => {
+          if(this.chatComp?.fmodeChat?.promptList?.length){
+            clearInterval(pint);
+            return
+          }
+          this.getChatPrompt();
+        }, 2000);
+
+        // 异步加载的后续数据 采访人物 ChatSession.person
+        let personInt = setInterval(() => {
+          if(this.chatComp?.fmodeChat?.chatSession?.get("person")){
+            clearInterval(personInt)
+          }
+          if(!this.chatComp?.fmodeChat?.chatSession?.get("person")){
+            if(this.pid){
+              this.chatComp?.fmodeChat?.chatSession?.set("person",{type:"Pointer",className:"Person",objectId:this.pid})
+            }
+          }
+        }, 2000);
+  }
+
+  // 初始化聊天面板的设置
+  initPanelConfig(){
+    this.roleId = this.chatComp?.fmodeChat?.chatSession?.get("role")?.id || this.roleId;
+
+    // 按钮自定义
+     this.leftButtons = [
+       // 提示 当角色配置预设提示词时 显示
+       {
+        title:"话题灵感",
+        showTitle:true,
+        icon:"color-wand-outline",
+        onClick:()=>{
+          if(this.chatComp){
+            this.chatComp.fmodeChat.isPromptModalOpen = true
+          }
+        },
+        show:()=>{
+          return this.chatComp?.fmodeChat?.promptList?.length
+        }
+      }
+   ]
+
+      this.leftButtons.push({ // 总结 结束并归档本次对话
+            title:"AI总结对话",
+            showTitle:true,
+            icon:"archive-outline",
+            onClick:()=>{
+              if(this.chatComp){
+                // this.chatComp.fmodeChat.isPromptModalOpen = true
+                if(this.chatComp.fmodeChat){
+                  console.log(JSON.stringify(this.chatComp.fmodeChat.messageList))
+                  // alert("处理对话记录")
+                }
+              }
+              },
+            show:()=>{ 
+              return !this.chatComp?.fmodeChat?.chatSession?.get("story")?.id
+              }
+        })
+
+        this.leftButtons.push({ // 总结 结束并归档本次对话
+          title:"聊天心理分析",
+          showTitle:true,
+          icon:"archive-outline",
+          onClick:()=>{
+            if(this.chatComp){
+              // this.chatComp.fmodeChat.isPromptModalOpen = true
+              if(this.chatComp.fmodeChat){
+                let messageList = JSON.parse(JSON.stringify(this.chatComp.fmodeChat.messageList))
+                messageList = messageList.filter((item:any)=>item.role!="system"&&item?.hidden!=true)
+                let qaContent = messageList.map((item:any)=>{
+                  let roleName = "当前用户"
+                  if(item.role!="user"){
+                    if(this.chatComp&&this.chatComp.fmodeChat.role){
+                      roleName = this.chatComp.fmodeChat.role.get("name");
+                    }else{
+                      roleName = "AI助理"
+                    }
+                  }
+                  return `${roleName}:${item.content}`
+                }
+                ).join("\n")
+                console.log(qaContent)
+                // alert("处理对话记录")
+              }
+            }
+            },
+          show:()=>{ 
+            return !this.chatComp?.fmodeChat?.chatSession?.get("story")?.id
+            }
+      })
+
+    
+
+      setTimeout(()=>{
+          if(this.chatComp&&this.chatComp.fmodeChat){
+            // 自定义左下角操作按钮
+            console.log("左下角操作按钮",this.chatComp.fmodeChat.leftButtons);
+            this.chatComp.fmodeChat.leftButtons = this.leftButtons;
+            
+            // 自定义角色名称
+            console.log("自定义角色",this.chatComp.fmodeChat.role);
+                        this.chatComp.fmodeChat.role.set("name","晓晓");
+            this.chatComp.fmodeChat.role.set("title","心理咨询师");
+            this.chatComp.fmodeChat.role.set("desc","一名亲切和蔼的心理咨询师,晓晓,年龄36岁");
+            this.chatComp.fmodeChat.role.set("tags",["焦虑","抑郁"]);
+            this.chatComp.fmodeChat.role.set("avatar","https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png")
+            this.chatComp.fmodeChat.role.set("prompt",`
+# 角色设定
+您是一名亲切和蔼的心理咨询师,晓晓,年龄36岁,需要完成陪来访者聊聊天,随意轻松一些。
+
+# 对话环节
+0.破冰,互相了解,引导用户介绍自己
+1.拓展话题,根据用户的介绍,拓展一些和其心理状态相关的话题
+- 引导,可深入的点,以用户自述为主
+- 当信息充足时候,确认用户心理状态,并进入下一个环节
+2.引导收尾,委婉引导用户结束本次对话
+- 用户同意结束后,结束本次对话,如果依依不舍,可以再陪聊一会儿`);
+//             this.chatComp.fmodeChat.role.set("name","晓晓");
+//             this.chatComp.fmodeChat.role.set("title","主任医师");
+//             this.chatComp.fmodeChat.role.set("desc","一名专业的全科医生,晓晓,年龄36岁");
+//             this.chatComp.fmodeChat.role.set("tags",["呼吸道","感染科"]);
+//             this.chatComp.fmodeChat.role.set("avatar","https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png")
+//             this.chatComp.fmodeChat.role.set("prompt",`
+// # 角色设定
+// 您是一名专业的全科医生,晓晓,年龄36岁,需要完成一次完整的门诊服务。
+
+// # 对话环节
+// 0.导诊(根据用户基本情况,引导挂号合适的科室)
+// 1.预设的问询方式(感冒问呼吸、肚子疼叩诊)
+// - 打招呼,以用户自述为主
+// - 当信息充足时候,确认用户症状对应的科室,并进入下一个环节
+// 2.拓展的问询细节
+// 例如:用户反映呼吸不畅,拓展出:是否咳嗽;是否感觉痛或者痒等其他需要的问题。
+// - 当问询细节补充完成后进入下一个环节
+// 3.初步的诊断结果,并且同时列出检查检验项目
+// 初步诊断:确定需要有哪些进一步检查
+// 检查检验:获取医学客观数据
+// - 等待用户提交客观数据,进入下一阶段
+// 4.给出诊断方案并给出处方
+
+// # 开始话语
+// 当您准备好了,可以以一个医生的身份,向来访的用户打招呼。
+//             `);
+
+            this.cdRef.detectChanges();
+          }
+      },1000)
+    
+
+   // 模型自定义
+   let ChatModel = Parse.Object.extend("ChatModel");
+   let model1 = new ChatModel();
+   model1.set({
+       name:"语伴4.5-128k",
+       code:"fmode-4.5-128k",
+       model:"gpt-4o-mini",
+       credit:0.096,
+   })
+  this.modelList = [model1]
+
+
+   console.log("initPanelConfig",this.leftButtons,this.modelList)
+ }
+
+ async getChatPrompt(){
+     let query = new Parse.Query('ChatPrompt')
+     query.notEqualTo('isDeleted', true)
+    //  query.equalTo('company', localStorage.getItem("company"))
+     query.equalTo('role', this.chatComp?.fmodeChat?.role)
+     query.include('role')
+     let promptData = await query.find()
+     if(this.chatComp&&this.chatComp.fmodeChat){
+       this.chatComp.fmodeChat.promptList = promptData
+       this.chatComp.fmodeChat.promptList.forEach((item:any)=>{
+         let cate = item.get('role').get('promptCates').filter((cate:any) => cate.name == item.get('cate'))
+         item.img = cate[0].img
+        })
+      }
+   }
+}

+ 114 - 0
TFPower-app/src/app/tab2/test-page/test-page.component.html

@@ -0,0 +1,114 @@
+<ion-header translucent="true">
+
+  <ion-toolbar>
+    <ion-title>制定个人健身计划</ion-title>
+    <ion-back-button></ion-back-button>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content [fullscreen]="true">
+  <div class="content">
+
+    <ion-button (click)="goBack()">
+      <ion-icon slot="icon-only" name="arrow-back"></ion-icon>
+    </ion-button>
+    健身目标选择
+    <div class="module">
+      <h2>请输入您的健身目标</h2>
+      <app-tag-input (tagsChanged)="onTagsChanged($event)"></app-tag-input>
+      <ion-item>
+        <ion-label position="floating">详细描述(可选,越详细计划越清晰哦!)</ion-label>
+        <ion-textarea [value]="goalDescription" (ionInput)="onGoalDescriptionChange($event)"
+          placeholder="例如:我想减脂,增强耐力..." auto-grow="true">
+        </ion-textarea>
+      </ion-item>
+    </div>
+
+    <!-- 偏好设置 -->
+    <div class="module">
+      <h3>偏好设置</h3>
+      <ion-grid>
+        <ion-row>
+          <ion-col size="6">
+            <ion-item>
+              <ion-label>锻炼方式(多选)</ion-label>
+              <ion-select multiple="true" (ionChange)="onExercisePreferenceChange($event)" cancelText="取消" okText="确认">
+                <ion-select-option value="cardio">有氧</ion-select-option>
+                <ion-select-option value="strength">力量</ion-select-option>
+                <ion-select-option value="flexibility">柔韧性</ion-select-option>
+              </ion-select>
+            </ion-item>
+          </ion-col>
+          <ion-col size="6">
+            <ion-item>
+              <ion-label>每周锻炼频率</ion-label>
+              <ion-select (ionChange)="onWorkoutFrequencyChange($event)" cancelText="取消" okText="确认">
+                <ion-select-option value="1">1次</ion-select-option>
+                <ion-select-option value="2">2次</ion-select-option>
+                <ion-select-option value="3">3次</ion-select-option>
+                <ion-select-option value="4">4次</ion-select-option>
+                <ion-select-option value="5">5次</ion-select-option>
+                <ion-select-option value="6">6次</ion-select-option>
+                <ion-select-option value="7">7次</ion-select-option>
+              </ion-select>
+            </ion-item>
+          </ion-col>
+        </ion-row>
+      </ion-grid>
+    </div>
+
+    <!-- 身体数据 -->
+    <div class="module">
+      <h3>身体数据</h3>
+      <ion-grid>
+        <ion-row>
+          <ion-col size="4">
+            <ion-item [class.empty]="!height" [class.filled]="height">
+              <ion-label position="floating">身高(cm)</ion-label>
+              <ion-input type="number" [value]="height" (ionInput)="onHeightChange($event)" placeholder="请输入身高"
+                required>
+              </ion-input>
+            </ion-item>
+          </ion-col>
+          <ion-col size="4">
+            <ion-item [class.empty]="!weight" [class.filled]="weight">
+              <ion-label position="floating">体重(kg)</ion-label>
+              <ion-input type="number" [value]="weight" (ionInput)="onWeightChange($event)" placeholder="请输入体重"
+                required>
+              </ion-input>
+            </ion-item>
+          </ion-col>
+          <ion-col size="4">
+            <ion-item [class.empty]="!age" [class.filled]="age">
+              <ion-label position="floating">年龄(age)</ion-label>
+              <ion-input type="number" [value]="age" (ionInput)="onAgeChange($event)" placeholder="请输入年龄" required>
+              </ion-input>
+            </ion-item>
+          </ion-col>
+        </ion-row>
+      </ion-grid>
+    </div>
+
+    <!-- 生成计划按钮 -->
+    <ion-button expand="full" (click)="generatePlan()" [disabled]="isGenerating">生成健身计划</ion-button>
+
+    <!-- 显示健身计划结果 -->
+    <div *ngIf="generatedPlan" class="result-container">
+      <h3>您的健身计划:</h3>
+      <ion-card>
+        <ion-card-header>
+          <ion-card-title>健身计划内容</ion-card-title>
+        </ion-card-header>
+        <ion-card-content>
+          <div *ngIf="isGenerating">
+            <ion-spinner name="crescent"></ion-spinner>
+            <p>生成中...</p>
+          </div>
+          <div *ngIf="!isGenerating">
+            <fm-markdown-preview class="content-style" [content]="generatedPlan"></fm-markdown-preview>
+          </div>
+        </ion-card-content>
+      </ion-card>
+    </div>
+  </div>
+</ion-content>

+ 38 - 0
TFPower-app/src/app/tab2/test-page/test-page.component.scss

@@ -0,0 +1,38 @@
+.content {
+  padding: 8px;
+}
+h2 {
+  margin-bottom: 8px;
+  font-size: 1.5em;
+}
+h3
+{
+   margin-bottom: 8px;
+  font-size: 1.2em; 
+}
+.module {
+  margin-bottom: 8px;
+  padding: 8px;
+  background-color: #f9f9f9;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+ion-grid {
+  margin-bottom: 8px;
+}
+ion-label{
+    transition: transform 0.2s ease;
+}
+ion-input {
+  --padding-top: 20px;
+  --padding-bottom: 2px; 
+  font-size: 14px;
+}
+
+ion-col {
+  padding: 0 8px;
+}
+ion-button {
+  margin-top: 20px;
+  --background: #3880ff;
+}

+ 22 - 0
TFPower-app/src/app/tab2/test-page/test-page.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { TestPageComponent } from './test-page.component';
+
+describe('TestPageComponent', () => {
+  let component: TestPageComponent;
+  let fixture: ComponentFixture<TestPageComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [TestPageComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(TestPageComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 329 - 0
TFPower-app/src/app/tab2/test-page/test-page.component.ts

@@ -0,0 +1,329 @@
+import { Component, OnInit } from '@angular/core';
+import { IonicModule, ToastController } from '@ionic/angular';
+import { FormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { TagInputComponent } from '../tag-input/tag-input.component';
+import { addIcons } from 'ionicons';
+import { barbellOutline, personOutline, square, alarmOutline, arrowBack } from 'ionicons/icons';
+import { DalleOptions, FmodeChatCompletion, MarkdownPreviewModule } from 'fmode-ng';
+import { LoadingController } from '@ionic/angular';
+import { AlertController, IonThumbnail, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonImg, IonInput, IonItem, IonLabel, IonList, IonRow, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonToolbar, IonTitle, IonSpinner, IonTextarea } from '@ionic/angular/standalone';
+import { NavController } from '@ionic/angular';
+import { Router } from '@angular/router';
+import { CloudObject, CloudQuery, CloudUser } from 'src/lib/ncloud';
+import { extractAllJsonFromString } from '../agent/agent.json';
+import { IonBackButton } from '@ionic/angular/standalone';
+@Component({
+  selector: 'app-test-page',
+  templateUrl: './test-page.component.html',
+  styleUrls: ['./test-page.component.scss'],
+  standalone: true,
+  imports: [
+    IonTextarea,
+    IonSpinner,
+    IonTitle,
+    IonBackButton,
+    MarkdownPreviewModule,
+    IonSelectOption,
+    IonSelect,
+    IonThumbnail,
+    IonCardSubtitle,
+    IonImg,
+    IonCard,
+    IonCardTitle,
+    IonCardHeader,
+    IonCardContent,
+    IonButtons,
+    IonItem,
+    IonList,
+    IonIcon,
+    FormsModule,
+    CommonModule,
+    IonHeader,
+    IonToolbar,
+    IonContent,
+    IonSegment,
+    IonSegmentButton,
+    IonGrid,
+    IonRow,
+    IonCol,
+    IonButton,
+    IonLabel,
+    IonBadge,
+    IonInput,
+    TagInputComponent
+  ]  // 添加 CommonModule
+})
+export class TestPageComponent implements OnInit {
+  selectedTags: string[] = [];
+  exercisePreference: string = '';
+  workoutFrequency: string = '';
+  height: number | null = null;
+  weight: number | null = null;
+  age: number | null = null;
+  goalDescription: string = '';
+  generatedPlan: string = '';  // 用来存储生成的健身计划
+  isGenerating: boolean = false;  // 是否正在生成计划
+  messageList: any[] = [];  // 存储消息列表
+  constructor(private toastController: ToastController,
+    private loadingController: LoadingController,
+    private alertController: AlertController,
+    private navController: NavController,
+    private router: Router,
+    private navCtrl: NavController
+  ) {
+    addIcons({ personOutline, barbellOutline, alarmOutline, square, arrowBack });
+  }
+  ngOnInit() { }
+
+  onTagsChanged(tags: string[]) {
+    this.selectedTags = tags;
+    console.log('当前标签:', this.selectedTags);
+  }
+
+  onGoalDescriptionChange(event: any) {
+    this.goalDescription = event.detail.value;
+  }
+
+  onExercisePreferenceChange(event: any) {
+    this.exercisePreference = event.detail.value;
+  }
+
+  onWorkoutFrequencyChange(event: any) {
+    this.workoutFrequency = event.detail.value;
+  }
+
+  onHeightChange(event: any) {
+    this.height = event.detail.value;
+  }
+
+  onWeightChange(event: any) {
+    this.weight = event.detail.value;
+  }
+
+  onAgeChange(event: any) {
+    this.age = event.detail.value;
+  }
+
+  // 显示toast提示
+  async showToast(message: string) {
+    const toast = await this.toastController.create({
+      message: message,
+      duration: 2000,
+      position: 'top',
+      color: 'danger',
+    });
+    toast.present();
+  }
+
+  async generatePlan() {
+    // 检查身高和体重是否为空
+    if (!this.height || !this.weight) {
+      // 如果为空,显示提示信息
+      await this.showToast('身高或体重为空,无法生成个人健身计划');
+      return; // 退出函数,不继续生成健身计划
+    }
+    const loading = await this.loadingController.create({
+      message: '生成计划中...',
+      duration: 30000 // 加载时长可以根据实际需求调整
+    });
+    await loading.present();
+    this.generatedPlan = '';
+    this.messageList = [];
+    this.isGenerating = true;
+
+    console.log('生成健身计划', {
+      tags: this.selectedTags,
+      exercisePreference: this.exercisePreference,
+      workoutFrequency: this.workoutFrequency,
+      height: this.height,
+      weight: this.weight,
+      age: this.age,
+      goalDescription: this.goalDescription
+    });
+
+    let prompt = `您作为一名专业的健身计划定制大师,请帮我根据以下情况制定健身计划(健身计划请给每天的运动标上序号)。关键词:${this.selectedTags.join(",")},目标描述:${this.goalDescription},运动偏好:${this.exercisePreference},
+    健身频率每周:${this.workoutFrequency},身高:${this.height}cm,体重:${this.weight}kg,年龄:${this.age},注意:只需给出训练日计划,训练日只做四个运动`;
+
+    let messageList = new FmodeChatCompletion([
+      { role: "system", content: '' },
+      { role: "user", content: prompt }
+    ]);
+
+    messageList.sendCompletion().subscribe((message: any) => {
+      console.log(message.content);
+      this.generatedPlan = message.content;
+
+      if (message?.complete) {
+        this.isGenerating = false;
+        this.getJson()
+        loading.dismiss();
+      }
+    });
+  }
+  public gptre = ""
+  title: string = "123"
+  chatID = ""
+  complete: boolean = false
+  planArray: any
+  JSONcomplete: boolean = false
+  JSONdes = ""
+  JSONobject: { [key: string]: string } = {}
+  getJson() {
+    let promt = `请你以以下json格式分每天来整合文段内容:
+    {
+    "date":"周几运动",
+    "srcId": "plan001等等",
+    "trainingPart":"训练部位",
+    "trainingItems":"四个运动项目,以下面形式给出,形式如下"[
+  {
+    "item": "引体向上",
+    "reps": 8,
+    "sets": 4
+  },
+  {
+    "item": "划船",
+    "reps": 10,
+    "sets": 3
+  },
+  {
+    "item": "背部伸展",
+    "reps": 12,
+    "sets": 3
+  },
+  {
+    "item": "拉力器划船",
+    "reps": 12,
+    "sets": 3
+  }
+]"
+}
+    内容如下:${this.generatedPlan}
+`
+
+    let completion = new FmodeChatCompletion([
+      { role: "system", content: "" },
+      { role: "user", content: promt }
+    ])
+    completion.sendCompletion().subscribe((message: any) => {
+      console.log(message.content)
+      this.JSONdes = message.content;
+
+      if (message?.complete) {
+        console.log("json:", this.JSONdes);
+        this.generatedPlan = message.content;
+        this.planArray = extractAllJsonFromString(this.JSONdes);
+        console.log(this.planArray)
+        this.showConfirmationAlert();
+        // planArray.forEach((plan: any) => {
+        //   plan.detail = this.generatedPlan; // 添加额外的计划内容
+        //   console.log(plan);
+
+        //   // 准备插入数据库的数据对象
+        //   let newPlan = {
+        //     "date": plan['date'],
+        //     "srcId": plan['srcId'],
+        //     "trainingPart": plan['trainingPart'],
+        //     "trainingItems": plan['trainingItems'],
+        //     "user": new CloudUser().toPointer(),
+        //   };
+
+        //   // 创建新的 CloudObject 实例
+        //   let newObject = new CloudObject("fitPlan");
+        //   newObject.set(newPlan);
+        //   newObject.save()
+        //     .then(() => {
+        //       console.log("计划保存成功");
+        //     })
+        //     .catch((error: any) => {
+        //       console.error("保存计划时发生错误", error);
+        //     });
+        // });
+        // this.JSONcomplete = true;
+      }
+    });
+  }
+  async showConfirmationAlert() {
+    const alert = await this.alertController.create({
+      header: '确认生成计划',
+      message: ` <div style="white-space: pre-wrap;">
+        <!-- 使用 fm-markdown-preview 渲染 Markdown 格式的内容 -->
+        <fm-markdown-preview class="content-style" [content]="generatedPlan"></fm-markdown-preview>
+      </div>`,
+      buttons: [
+        {
+          text: '放弃',
+          role: 'cancel',
+          handler: () => {
+            console.log('用户选择放弃');
+          }
+        },
+        {
+          text: '确认',
+          handler: () => {
+            // 这里可以调用 API 将数据存到数据库
+            console.log('用户确认', this.generatedPlan);
+            this.savePlanToDatabase(this.generatedPlan);
+          }
+        }
+      ]
+    });
+    await alert.present();
+  }
+  async savePlanToDatabase(plan: string) {
+
+    let currentUser = new CloudUser().toPointer();  // 获取当前用户实例
+    console.log(currentUser)
+    console.log('计划保存到数据库', plan);
+
+    // 查询当前用户的所有计划
+    const cloudQuery = new CloudQuery("fitPlan");
+    // 过滤出与当前用户关联的所有计划
+    cloudQuery.equalTo("user", currentUser)
+    try {
+      const userPlans = await cloudQuery.find();  // 获取当前用户的所有计划
+      console.log("找到的计划数量:", userPlans.length);
+
+      // 删除当前用户的所有计划
+      for (const planObj of userPlans) {
+        await planObj.destroy();  // 删除每个计划
+        console.log("已删除计划:", planObj);
+      }
+      this.planArray.forEach((plan: any) => {
+        plan.detail = this.generatedPlan; // 添加额外的计划内容
+        console.log(plan);
+
+        // 准备插入数据库的数据对象
+        let newPlan = {
+          "date": plan['date'],
+          "srcId": plan['srcId'],
+          "trainingPart": plan['trainingPart'],
+          "trainingItems": plan['trainingItems'],
+          "user": currentUser,  // 将当前用户指针关联到计划
+        };
+        // 创建新的 CloudObject 实例
+        let newObject = new CloudObject("fitPlan");
+        newObject.set(newPlan);
+        newObject.save()
+          .then(() => {
+            console.log("计划保存成功");
+          })
+          .catch((error: any) => {
+            console.error("保存计划时发生错误", error);
+          });
+      });
+      this.JSONcomplete = true;
+    } catch (error) {
+      console.error("查询或删除计划时发生错误", error);
+    }
+    this.goToPage("tab2")
+  }
+
+  goBack() {
+    this.navCtrl.back();
+  }
+  goToPage(page: string) {
+    // 更新选中的tab
+    this.router.navigate([`/tabs/${page}`]);  // 然后再进行路由跳转
+  }
+}

+ 3 - 2
TFPower-app/src/app/tabs/tabs.page.html

@@ -6,14 +6,15 @@
     </ion-tab-button>
 
     <ion-tab-button tab="tab2" href="/tabs/tab2">
-      <ion-icon name="chatbox-ellipses"></ion-icon>
-      <ion-label>社交</ion-label>
+      <ion-icon name="barbell"></ion-icon>
+      <ion-label>运动</ion-label>
     </ion-tab-button>
 
     <ion-tab-button tab="tab3" href="/tabs/tab3">
       <ion-icon aria-hidden="true" name="people"></ion-icon>
       <ion-label>社区</ion-label>
     </ion-tab-button>
+
     <ion-tab-button tab="tab4" href="/tabs/tab4">
       <ion-icon name="home"></ion-icon>
       <ion-label>我的</ion-label>

+ 2 - 1
TFPower-app/src/app/tabs/tabs.page.ts

@@ -7,7 +7,7 @@ import {
   IonLabel,
 } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
-import { people, balloonSharp, home, chatboxEllipses } from 'ionicons/icons';
+import { people, balloonSharp, home, chatboxEllipses, barbell } from 'ionicons/icons';
 
 @Component({
   selector: 'app-tabs',
@@ -25,6 +25,7 @@ export class TabsPage {
       balloonSharp,
       chatboxEllipses,
       home,
+      barbell,
     });
   }
 }

+ 6 - 0
TFPower-app/src/app/tabs/tabs.routes.ts

@@ -16,6 +16,11 @@ export const routes: Routes = [
         loadComponent: () =>
           import('../tab2/tab2.page').then((m) => m.Tab2Page),
       },
+      {
+        path: 'test-page',
+        loadComponent: () =>
+          import('../tab2/test-page/test-page.component').then((m) => m.TestPageComponent),
+      },
       {
         path: 'tab3',
         loadComponent: () =>
@@ -26,6 +31,7 @@ export const routes: Routes = [
         loadComponent: () =>
           import('../tab4/tab4.page').then((m) => m.Tab4Page),
       },
+
       {
         path: 'todolistpage',
         loadComponent: () =>

二進制
TFPower-app/src/assets/images/ache.jpg


二進制
TFPower-app/src/assets/images/action.png


二進制
TFPower-app/src/assets/images/coach1.jpg


二進制
TFPower-app/src/assets/images/coach2.jpg


二進制
TFPower-app/src/assets/images/wisefitness-high-resolution-logo.png


+ 381 - 0
TFPower-app/src/lib/ncloud.ts

@@ -0,0 +1,381 @@
+// CloudObject.ts
+export class CloudObject {
+    className: string;
+    id: string | null = null;
+    createdAt: any;
+    updatedAt: any;
+    data: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    toPointer() {
+        return { "__type": "Pointer", "className": this.className, "objectId": this.id };
+    }
+
+    set(json: Record<string, any>) {
+        Object.keys(json).forEach(key => {
+            if (["objectId", "id", "createdAt", "updatedAt"].indexOf(key) > -1) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save() {
+        let method = "POST";
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}`;
+
+        // 更新
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        const body = JSON.stringify(this.data);
+        const response = await fetch(url, {
+            headers: {
+                "content-type": "application/json;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        return this;
+    }
+
+    async destroy() {
+        if (!this.id) return;
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}/${this.id}`, {
+            headers: {
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "DELETE",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result) {
+            this.id = null;
+        }
+        return true;
+    }
+}
+
+// CloudQuery.ts
+export class CloudQuery {
+    className: string;
+    queryParams: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    include(...fileds: string[]) {
+        this.queryParams["include"] = fileds;
+    }
+    greaterThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gt"] = value;
+    }
+
+    greaterThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gte"] = value;
+    }
+
+    lessThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lt"] = value;
+    }
+
+    lessThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lte"] = value;
+    }
+
+    equalTo(key: string, value: any) {
+        // 确保 this.queryParams["where"] 已经初始化为对象
+        if (!this.queryParams["where"]) {
+            this.queryParams["where"] = {};  // 防止未初始化的情况
+        }
+        this.queryParams["where"][key] = value;
+    }
+
+    async get(id: string) {
+        const url = `http://dev.fmode.cn:1337/parse/classes/${this.className}/${id}?`;
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        return json || {};
+    }
+
+    async find() {
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?`;
+
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key => {
+            let paramStr = JSON.stringify(this.queryParams[key]);
+            if (key == "include") {
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if (queryStr) {
+                url += `${key}=${paramStr}`;
+            } else {
+                url += `&${key}=${paramStr}`;
+            }
+        })
+        // if (Object.keys(this.queryParams["where"]).length) {
+
+        // }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        let list = json?.results || []
+        let objList = list.map((item: any) => this.dataToObj(item))
+        return objList || [];
+    }
+
+    async first() {
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?`;
+
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
+            url += `where=${whereStr}`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        const exists = json?.results?.[0] || null;
+        if (exists) {
+            let existsObject = this.dataToObj(exists)
+            return existsObject;
+        }
+        return null
+    }
+
+    dataToObj(exists: any): CloudObject {
+        let existsObject = new CloudObject(this.className);
+        existsObject.set(exists);
+        existsObject.id = exists.objectId;
+        existsObject.createdAt = exists.createdAt;
+        existsObject.updatedAt = exists.updatedAt;
+        return existsObject;
+    }
+}
+
+// CloudUser.ts
+export class CloudUser extends CloudObject {
+    constructor() {
+        super("_User"); // 假设用户类在Parse中是"_User"
+        // 读取用户缓存信息
+        let userCacheStr = localStorage.getItem("NCloud/dev/User")
+        if (userCacheStr) {
+            let userData = JSON.parse(userCacheStr)
+            // 设置用户信息
+            this.id = userData?.objectId;
+            this.sessionToken = userData?.sessionToken;
+            this.data = userData; // 保存用户数据
+        }
+    }
+
+    sessionToken: string | null = ""
+    /** 获取当前用户信息 */
+    async current() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return null;
+        }
+        return this;
+        // const response = await fetch(`http://dev.fmode.cn:1337/parse/users/me`, {
+        //     headers: {
+        //         "x-parse-application-id": "dev",
+        //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
+        //     },
+        //     method: "GET"
+        // });
+
+        // const result = await response?.json();
+        // if (result?.error) {
+        //     console.error(result?.error);
+        //     return null;
+        // }
+        // return result;
+    }
+
+    /** 登录 */
+    async login(username: string, password: string): Promise<CloudUser | null> {
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/login`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify({ username, password }),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
+        return this;
+    }
+
+    /** 登出 */
+    async logout() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return;
+        }
+
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/logout`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "x-parse-session-token": this.sessionToken
+            },
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return false;
+        }
+
+        // 清除用户信息
+        localStorage.removeItem("NCloud/dev/User")
+        this.id = null;
+        this.sessionToken = null;
+        this.data = {};
+        return true;
+    }
+
+    /** 注册 */
+    async signUp(username: string, password: string, additionalData: Record<string, any> = {}) {
+        const userData = {
+            username,
+            password,
+            ...additionalData // 合并额外的用户数据
+        };
+
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/users`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify(userData),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        return this;
+    }
+
+    override async save() {
+        let method = "POST";
+        let url = `http://dev.fmode.cn:1337/parse/users`;
+
+        // 更新用户信息
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        let data: any = JSON.parse(JSON.stringify(this.data))
+        delete data.createdAt
+        delete data.updatedAt
+        delete data.ACL
+        delete data.objectId
+        const body = JSON.stringify(data);
+        let headersOptions: any = {
+            "content-type": "application/json;charset=UTF-8",
+            "x-parse-application-id": "dev",
+            "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
+        }
+        const response = await fetch(url, {
+            headers: headersOptions,
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(this.data))
+        return this;
+    }
+}

+ 26 - 0
TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -0,0 +1,26 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      用户名:{{currentUser?.get("username")}}
+    </ion-card-title>
+    <ion-card-subtitle>请输入您的详细资料</ion-card-subtitle>
+   </ion-card-header>
+ <ion-card-content>
+
+   <ion-item>
+     <ion-input [value]="userData['realname']" (ionChange)="userDataChange('realname',$event)" label="姓名" placeholder="请您输入真实姓名"></ion-input>
+   </ion-item>
+   <ion-item>
+     <ion-input type="number" [value]="userData['age']" (ionChange)="userDataChange('age',$event)" label="年龄" placeholder="请您输入年龄"></ion-input>
+    </ion-item>
+  <ion-item>
+     <ion-input [value]="userData['gender']" (ionChange)="userDataChange('gender',$event)" label="性别" placeholder="请您输入男/女"></ion-input>
+    </ion-item>
+
+   <ion-button expand="block" (click)="save()">保存</ion-button>
+   <ion-button expand="block" (click)="cancel()">取消</ion-button>
+ 
+
+</ion-card-content>
+</ion-card>

+ 0 - 0
TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.scss


+ 22 - 0
TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { ModalUserEditComponent } from './modal-user-edit.component';
+
+describe('ModalUserEditComponent', () => {
+  let component: ModalUserEditComponent;
+  let fixture: ComponentFixture<ModalUserEditComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [ModalUserEditComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ModalUserEditComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 65 - 0
TFPower-app/src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -0,0 +1,65 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-modal-user-edit',
+  templateUrl: './modal-user-edit.component.html',
+  styleUrls: ['./modal-user-edit.component.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserEditComponent  implements OnInit {
+
+  currentUser:CloudUser|undefined
+  userData:any = {}
+  userDataChange(key:string,ev:any){
+    let value = ev?.detail?.value
+    if(value){
+      this.userData[key] = value
+    }
+  }
+  constructor(private modalCtrl:ModalController) { 
+    this.currentUser = new CloudUser();
+    this.userData = this.currentUser.data;
+  }
+
+  ngOnInit() {}
+
+  async save(){
+    Object.keys(this.userData).forEach(key=>{
+      if(key=="age"){
+        this.userData[key] = Number(this.userData[key])
+      }
+    })
+
+    this.currentUser?.set(this.userData)
+    await this.currentUser?.save()
+    this.modalCtrl.dismiss(this.currentUser,"confirm")
+  }
+  cancel(){
+    this.modalCtrl.dismiss(null,"cancel")
+
+  }
+}
+
+export async function openUserEditModal(modalCtrl:ModalController):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserEditComponent,
+    breakpoints:[0.7,1.0],
+    initialBreakpoint:0.7
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 41 - 0
TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.html

@@ -0,0 +1,41 @@
+<!-- 用户登录状态 -->
+<ion-card>
+  <ion-card-header>
+    <ion-card-title>
+      <ion-segment [value]="type" (ionChange)="typeChange($event)">
+        <ion-segment-button value="login">
+          <ion-label>登录</ion-label>
+        </ion-segment-button>
+        <ion-segment-button value="signup">
+          <ion-label>注册</ion-label>
+        </ion-segment-button>
+      </ion-segment>
+    </ion-card-title>
+    <ion-card-subtitle>请输入账号密码</ion-card-subtitle>
+  </ion-card-header>
+  <ion-card-content>
+
+    <ion-item>
+      <ion-input [value]="username" (ionChange)="usernameChange($event)" label="账号" placeholder="请您输入账号/手机号">
+      </ion-input>
+    </ion-item>
+    <ion-item>
+      <ion-input [value]="password" (ionChange)="passwordChange($event)" label="密码" type="password" value="password">
+      </ion-input>
+    </ion-item>
+    @if(type=="signup"){
+    <ion-item>
+      <ion-input [value]="password2" (ionChange)="password2Change($event)" label="确认密码" type="password"
+        value="password"></ion-input>
+    </ion-item>
+    }
+
+    @if(type=="login"){
+    <ion-button expand="block" (click)="login()">登录</ion-button>
+    }
+    @if(type=="signup"){
+    <ion-button expand="block" (click)="signup()">注册</ion-button>
+    }
+
+  </ion-card-content>
+</ion-card>

+ 0 - 0
TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.scss


+ 22 - 0
TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.spec.ts

@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { ModalUserLoginComponent } from './modal-user-login.component';
+
+describe('ModalUserLoginComponent', () => {
+  let component: ModalUserLoginComponent;
+  let fixture: ComponentFixture<ModalUserLoginComponent>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      imports: [ModalUserLoginComponent],
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ModalUserLoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  }));
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 92 - 0
TFPower-app/src/lib/user/modal-user-login/modal-user-login.component.ts

@@ -0,0 +1,92 @@
+import { Input, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
+import { IonHeader, IonToolbar, IonTitle, IonContent, IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle, ModalController, IonInput, IonItem, IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
+import { CloudUser } from 'src/lib/ncloud';
+
+@Component({
+  selector: 'app-modal-user-login',
+  templateUrl: './modal-user-login.component.html',
+  styleUrls: ['./modal-user-login.component.scss'],
+  standalone: true,
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
+  ],
+})
+export class ModalUserLoginComponent  implements OnInit {
+  @Input()
+  type:"login"|"signup" = "login"
+  typeChange(ev:any){
+    this.type = ev?.detail?.value || ev?.value || 'login'
+  }
+  username:string = ""
+  usernameChange(ev:any){
+    console.log(ev)
+    this.username = ev?.detail?.value
+  }
+  password:string = ""
+  passwordChange(ev:any){
+    this.password = ev?.detail?.value
+  }
+  password2:string = ""
+  password2Change(ev:any){
+    this.password2 = ev?.detail?.value
+  }
+  constructor(private modalCtrl:ModalController) { }
+
+  ngOnInit() {}
+
+  async login(){
+    if(!this.username || !this.password){
+      console.log("请输入完整")
+      return
+    }
+    let user:any = new CloudUser();
+    user = await user.login(this.username,this.password);
+    if(user?.id){
+       this.modalCtrl.dismiss(user,"confirm")
+    }else{
+      console.log("登录失败")
+    }
+  }
+
+  async signup(){
+    if(!this.username || !this.password || !this.password2){
+      console.log("请输入完整")
+      return
+    }
+    if(this.password!=this.password2){
+      console.log("两次密码不符,请修改")
+      return
+    }
+
+    let user:any = new CloudUser();
+    user = await user.signUp(this.username,this.password);
+    if(user){
+      this.type = "login"
+      console.log("注册成功请登录")
+    }
+  }
+
+}
+
+
+export async function openUserLoginModal(modalCtrl:ModalController,type:"login"|"signup"="login"):Promise<CloudUser|null>{
+  const modal = await modalCtrl.create({
+    component: ModalUserLoginComponent,
+    componentProps:{
+      type:type
+    },
+    breakpoints:[0.5,0.7],
+    initialBreakpoint:0.5
+  });
+  modal.present();
+
+  const { data, role } = await modal.onWillDismiss();
+
+  if (role === 'confirm') {
+    return data;
+  }
+  return null
+}

+ 1 - 1
TFPower-app/src/zone-flags.ts

@@ -1,4 +1,4 @@
-/**
+ /**
  * Prevents Angular change detection from
  * running with certain Web Component callbacks
  */

+ 2 - 0
TFPower-prod/README.md

@@ -564,3 +564,5 @@ User -> "社交与社区": 分享成果/加入群组
 # FAQ:项目起名
 
 > 渴望力量
+
+

+ 258 - 0
TFPower-prod/schema/schema.md

@@ -1,3 +1,204 @@
+<<<<<<< HEAD
+# 一、Schema范式设计
+
+## 训练计划及任务执行模块
+
+### 训练计划模块描述
+
+根据用户所需要的健身目标,生成的整个训练计划,训练计划中包含了很多子任务。
+
+### 任务执行模块描述
+
+记录各类型子任务以及每项任务的执行情况
+
+#### 任务类型
+
+- 一次性任务
+- 周期性任务(每日、每周、每月)
+
+#### 任务状态
+
+- 一次性任务,可以在任务数据中直接标注完成
+- 周期性任务,虽然整体有完成状态,但是每次的执行都需要有单独的表来记录。
+
+## 具体范式设计
+
+### 1. 表设计
+
+#### 1.1 训练计划表 (TrainingPlan)
+
+| 字段     | 类型         | 说明             |
+|----------|--------------|------------------|
+| objectId | String       | 唯一标识符       |
+| createdAt | Date        | 创建时间         |
+| user     | Pointer<User> | 关联用户         |
+| goal     | String       | 健身目标         |
+| content  | String       | 训练计划详情     |
+| tasks    | Array<Pointer<Task>> | 关联的子任务 |
+
+#### 1.2 任务表 (Task)
+
+| 字段      | 类型         | 说明                       |
+|-----------|--------------|----------------------------|
+| objectId  | String       | 唯一标识符                 |
+| createdAt | Date         | 创建时间                   |
+| plan      | Pointer<TrainingPlan> | 关联训练计划         |
+| type      | String       | 任务类型(一次性任务/周期性任务) |
+| name      | String       | 任务名称                   |
+| status    | String       | 任务状态(未完成/已完成)  |
+| count     | Number       | 执行次数                   |
+| duration  | Number       | 持续时间                   |
+| frequency | String       | 周期性任务的频率(每日/每周/每月,可选) |
+
+#### 1.3 任务执行记录表 (TaskExecution)
+
+| 字段     | 类型         | 说明                   |
+|----------|--------------|------------------------|
+| objectId | String       | 唯一标识符             |
+| createdAt | Date        | 创建时间               |
+| task     | Pointer<Task> | 关联任务               |
+| execDate | Date         | 执行日期               |
+| status   | String       | 执行状态(完成/未完成) |
+
+#### 1.4 用户表 (User)
+
+| 字段     | 类型         | 说明             |
+|----------|--------------|------------------|
+| objectId | String       | 唯一标识符       |
+| createdAt | Date        | 创建时间         |
+| username | String       | 用户名           |
+| email    | String       | 用户邮箱         |
+| password | String       | 用户密码         |
+
+### 2. PlantUML 类图表示
+
+以下是使用PlantUML表示的类图:
+
+```plantuml
+@startuml
+class User {
+  +objectId: String
+  +createdAt: Date
+  +username: String
+  +email: String
+  +password: String
+}
+
+class TrainingPlan {
+  +objectId: String
+  +createdAt: Date
+  +goal: String
+  +content: String
+}
+
+class Task {
+  +objectId: String
+  +createdAt: Date
+  +type: String
+  +name: String
+  +status: String
+  +count: Number
+  +duration: Number
+  +frequency: String
+}
+
+class TaskExecution {
+  +objectId: String
+  +createdAt: Date
+  +execDate: Date
+  +status: String
+}
+
+User "1" -- "0..*" TrainingPlan : owns
+TrainingPlan "1" -- "0..*" Task : contains
+Task "1" -- "0..*" TaskExecution : executes
+@enduml
+```
+##打卡签到模块
+###模块描述 
+用户表是_User表 用户每日登录后,可以打卡签到,记录用户的签到时间,和日期,且每日只能打卡签到一次。
+### 1. 用户表 (_User)
+
+#### 表名:User
+
+#### 字段:
+- **objectId**:String (用户唯一标识)
+- **username**:String (用户名)
+- **email**:String (用户邮箱)
+- **createdAt**:Date (创建时间)
+- **updatedAt**:Date (更新时间)
+
+---
+
+## 2. 签到记录表 (SignInRecord)
+
+#### 表名:SignInRecord
+
+#### 字段:
+- **objectId**:String (签到记录唯一标识)
+- **user**:Pointer (用户,外键,关联到User表)
+- **signInDate**:Date (签到日期)
+- **createdAt**:Date (创建时间)
+
+---
+
+### 3. 设计注意事项
+
+- 在 **SignInRecord** 表中,`user` 字段使用 `Pointer<User>` 来表示与用户表的关联。
+- 每个用户每天只能有一条签到记录,因此在 **SignInRecord** 表中,`user` 和 `signInDate` 的组合应当是唯一的。
+- `createdAt` 字段用于记录每条记录的创建时间,以便于后续的数据追踪。
+
+#### PlantUML 类图表示
+
+```plantuml
+@startuml
+class User {
+  +objectId: String
+  +username: String
+  +email: String
+  +createdAt: Date
+  +updatedAt: Date
+}
+
+class SignInRecord {
+  +objectId: String
+  +user: Pointer
+  +signInDate: Date
+  +createdAt: Date
+}
+
+User "1" -- "0..*" SignInRecord : has
+@enduml
+```
+# 二、业务逻辑描述
+
+## 训练计划的完整逻辑
+
+### 计划生成逻辑
+
+用户在APP内,通过文本生成整个训练计划。
+
+#### 数据来源
+
+- **用户输入**:用户的健身需求等。
+- **用户体征**:性别、年龄、体重等。
+
+#### 结果存储
+
+- **TrainingPlan**
+  - 计划目标 `goal`:计划需求标题。
+  - 计划详情 `content`:完整计划内容。
+
+### 根据训练计划结果,排期生成任务
+
+#### 数据来源
+
+- **计划详情** `content`。
+
+#### 文本生成
+
+
+=======
 # 一、训练计划及任务执行模块
 
 - 训练计划模块描述
@@ -205,6 +406,7 @@ User "1" -- "0..*" SignInRecord : has
     - 提示词:严格限制 json 格式,输出任务列表(附带当前时- 间,用于时间的生成)
   - 生成结果:taskList
     - 循环数组,向 Task 表逐个插入数据。
+>>>>>>> 538aa7279b9922f4eaea6729b67ae3a16c956e85
 
 ```json
 [
@@ -265,6 +467,61 @@ User "1" -- "0..*" SignInRecord : has
   }
 ]
 ```
+<<<<<<< HEAD
+### 计划执行的逻辑
+
+#### 用户在 TodoList 页面,加载所有计划和子任务。
+
+- 用户主动填写执行情况并提交。
+
+#### 数据存储
+
+- **TaskExecution**:记录每次用户执行情况提交的数据,并在提交后,根据 **Task** 中持续时间和次数条件,来判断 **Task** 本身是否标记完成。
+
+#### 计划的完成和统计分析
+
+- 连表查询,分析用户计划完成的总体情况和进度。
+
+### 训练计划业务逻辑图例
+
+**提示词**:{业务逻辑},您是一名专业的产品经理,请您将上述逻辑的每个过程,用 PlantUML 的时序图表示出来。
+
+### 业务逻辑图例
+
+下面是根据您提供的业务逻辑描述所生成的 **PlantUML** 时序图。这个时序图展示了训练计划的生成、执行和完成统计分析的各个过程:
+
+```plantuml
+@startuml
+actor User
+entity APP
+entity TrainingPlan
+entity Task
+entity TaskExecution
+
+User -> APP: 输入健身需求和体征信息
+APP -> TrainingPlan: 生成训练计划
+APP -> Task: 根据计划生成任务
+User -> APP: 查看任务和子任务
+User -> APP: 提交任务执行情况
+APP -> TaskExecution: 记录执行情况
+APP -> Task: 判断任务是否完成
+APP -> User: 展示任务完成情况
+@enduml
+```
+### 解释
+
+1. **训练计划生成逻辑**:通过用户输入的健身需求和体征信息,生成训练计划,并存储到 **TrainingPlan** 表中。
+2. **任务生成**:根据训练计划的内容生成相关任务,每个任务存储到 **Task** 表中,并关联到相应的训练计划。
+3. **任务执行**:用户在 TodoList 页面查看任务,并提交执行情况。每次执行情况记录在 **TaskExecution** 表中。
+4. **任务完成标记**:通过任务的持续时间和次数条件,判断任务是否标记为完成。
+5. **计划统计分析**:通过连表查询,分析用户任务完成的整体情况和进度。
+
+
+
+
+
+
+=======
 
 ## 计划执行的逻辑
 
@@ -318,3 +575,4 @@ App -> User : 显示统计分析结果
 - 用户在 TodoList 页面查看计划和子任务,并提交执行情况。
 - APP 记录执行情况,并判断任务是否完成。
 - 最后,APP 进行统计分析并将结果展示给用户。
+>>>>>>> 538aa7279b9922f4eaea6729b67ae3a16c956e85

+ 352 - 0
TFPower-server/lib/migration/data.js

@@ -0,0 +1,352 @@
+// module.exports.DoctorList = [
+//     {
+//       "objectId": "doc001",
+//       "name": "张伟",
+//       "title": "主任医师",
+//       "desc": "拥有20年内科临床经验,擅长心血管疾病的治疗",
+//       "gender": "男",
+//       "age": 45,
+//       "specialty": "内科",
+//       "qualifications": ["医学博士,内科专科医生"],
+
+//     },
+//     {
+//       "objectId": "doc002",
+//       "avatar":"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png",
+//       "name": "李娜",
+//       "title": "主任医师",
+//       "desc": "外科领域专家,擅长微创手术",
+//       "gender": "女",
+//       "age": 50,
+//       "specialty": "外科",
+//       "qualifications": ["外科专科医生,硕士研究生"],
+//       "depart": {
+//         "objectId": "dept002"
+//       }
+//     },
+//     {
+//       "objectId": "doc003",
+//       "name": "王芳",
+//       "title": "主任医师",
+//       "desc": "儿童健康专家,擅长儿童生长发育",
+//       "gender": "女",
+//       "age": 40,
+//       "specialty": "儿科",
+//       "qualifications": ["儿科专科医生,医学硕士"],
+//       "depart": {
+//         "objectId": "dept003"
+//       }
+//     },
+//     {
+//       "objectId": "doc004",
+//       "name": "刘强",
+//       "title": "主任医师",
+//       "desc": "妇产科专家,专注于高危妊娠管理",
+//       "gender": "男",
+//       "age": 48,
+//       "specialty": "妇产科",
+//       "qualifications": ["妇产科专科医生,博士研究生"],
+//       "depart": {
+//         "objectId": "dept004"
+//       }
+//     },
+//     {
+//       "objectId": "doc005",
+//       "name": "陈静",
+//       "title": "主任医师",
+//       "desc": "神经科专家,擅长癫痫和头痛的治疗",
+//       "gender": "女",
+//       "age": 42,
+//       "specialty": "神经科",
+//       "qualifications": ["神经科专科医生,医学博士"],
+//       "depart": {
+//         "objectId": "dept005"
+//       }
+//     }
+//   ]
+
+// module.exports.DepartList = [
+
+//       {
+//         "objectId": "dept001",
+//         "name": "内科",
+//         "desc": "负责内科疾病的诊断和治疗"
+//       },
+//       {
+//         "objectId": "dept002",
+//         "name": "外科",
+//         "desc": "负责外科手术和相关疾病的治疗"
+//       },
+//       {
+//         "objectId": "dept003",
+//         "name": "儿科",
+//         "desc": "专注于儿童疾病的预防和治疗"
+//       },
+//       {
+//         "objectId": "dept004",
+//         "name": "妇产科",
+//         "desc": "负责女性生殖系统及相关疾病的治疗"
+//       },
+//       {
+//         "objectId": "dept005",
+//         "name": "神经科",
+//         "desc": "专注于神经系统疾病的诊断和治疗"
+//       }
+//     ]
+// module.exports.DoctorList = [
+//     {
+//       "objectId": "doc001",
+//       "name": "张伟",
+//       "title": "主任医师",
+//       "desc": "拥有20年内科临床经验,擅长心血管疾病的治疗",
+//       "gender": "男",
+//       "age": 45,
+//       "specialty": "内科",
+//       "qualifications": ["医学博士,内科专科医生"],
+//       "depart": {
+//         "objectId": "dept001"
+//       }
+//     },
+//     {
+//       "objectId": "doc002",
+//       "avatar":"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/Q4Zif7fTbK-0.png",
+//       "name": "李娜",
+//       "title": "主任医师",
+//       "desc": "外科领域专家,擅长微创手术",
+//       "gender": "女",
+//       "age": 50,
+//       "specialty": "外科",
+//       "qualifications": ["外科专科医生,硕士研究生"],
+//       "depart": {
+//         "objectId": "dept002"
+//       }
+//     },
+//     {
+//       "objectId": "doc003",
+//       "name": "王芳",
+//       "title": "主任医师",
+//       "desc": "儿童健康专家,擅长儿童生长发育",
+//       "gender": "女",
+//       "age": 40,
+//       "specialty": "儿科",
+//       "qualifications": ["儿科专科医生,医学硕士"],
+//       "depart": {
+//         "objectId": "dept003"
+//       }
+//     },
+//     {
+//       "objectId": "doc004",
+//       "name": "刘强",
+//       "title": "主任医师",
+//       "desc": "妇产科专家,专注于高危妊娠管理",
+//       "gender": "男",
+//       "age": 48,
+//       "specialty": "妇产科",
+//       "qualifications": ["妇产科专科医生,博士研究生"],
+//       "depart": {
+//         "objectId": "dept004"
+//       }
+//     },
+//     {
+//       "objectId": "doc005",
+//       "name": "陈静",
+//       "title": "主任医师",
+//       "desc": "神经科专家,擅长癫痫和头痛的治疗",
+//       "gender": "女",
+//       "age": 42,
+//       "specialty": "神经科",
+//       "qualifications": ["神经科专科医生,医学博士"],
+//       "depart": {
+//         "objectId": "dept005"
+//       }
+//     }
+//   ]
+
+// module.exports.DepartList = [
+
+//       {
+//         "objectId": "dept001",
+//         "name": "内科",
+//         "desc": "负责内科疾病的诊断和治疗"
+//       },
+//       {
+//         "objectId": "dept002",
+//         "name": "外科",
+//         "desc": "负责外科手术和相关疾病的治疗"
+//       },
+//       {
+//         "objectId": "dept003",
+//         "name": "儿科",
+//         "desc": "专注于儿童疾病的预防和治疗"
+//       },
+//       {
+//         "objectId": "dept004",
+//         "name": "妇产科",
+//         "desc": "负责女性生殖系统及相关疾病的治疗"
+//       },
+//       {
+//         "objectId": "dept005",
+//         "name": "神经科",
+//         "desc": "专注于神经系统疾病的诊断和治疗"
+//       }
+//     ]
+module.exports.UserList = [
+  {
+    "objectId": "user001",
+    "name": "张伟",
+    "gender": "男",
+    "age": 30,
+    "height": 175, // 身高(厘米)  
+    "weight": 70,  // 体重(千克)  
+    "bmi": 22.86,  // 体重指数  
+    "fitnessGoals": ["增肌", "提高耐力"], // 健身目标  
+    "phone": "13800000001",
+    "email": "zhangwei@example.com",
+    "depart": {
+      "objectId": "plan001"
+    }
+  },
+  {
+    "objectId": "user002",
+    "name": "李娜",
+    "gender": "女",
+    "age": 28,
+    "height": 160,
+    "weight": 55,
+    "bmi": 21.48,
+    "fitnessGoals": ["减脂", "塑形"],
+    "phone": "13800000002",
+    "email": "lina@example.com",
+    "depart": {
+      "objectId": "plan002"
+    }
+  },
+  {
+    "objectId": "user003",
+    "name": "王芳",
+    "gender": "女",
+    "age": 35,
+    "height": 165,
+    "weight": 60,
+    "bmi": 22.04,
+    "fitnessGoals": ["增强力量"],
+    "phone": "13800000003",
+    "email": "wangfang@example.com",
+    "depart": {
+      "objectId": "plan003"
+    }
+  },
+  {
+    "objectId": "user004",
+    "name": "刘强",
+    "gender": "男",
+    "age": 40,
+    "height": 180,
+    "weight": 80,
+    "bmi": 24.69,
+    "fitnessGoals": ["提高灵活性", "增强耐力"],
+    "phone": "13800000004",
+    "email": "liuqiang@example.com",
+    "depart": {
+      "objectId": "plan004"
+    }
+  },
+  {
+    "objectId": "user005",
+    "name": "陈静",
+    "gender": "女",
+    "age": 25,
+    "height": 158,
+    "weight": 50,
+    "bmi": 20.03,
+    "fitnessGoals": ["保持健康"],
+    "phone": "13800000005",
+    "email": "chenjing@example.com",
+    "depart": {
+      "objectId": "plan005"
+    }
+  }
+];
+module.exports.PlanList = [
+  {
+    "objectId": "plan001",
+    "date": "2024-12-20", // 计划日期  
+    "trainingPart": "胸部", // 训练部位  
+    "trainingItems": [
+      { "item": "卧推", "sets": 4, "reps": 10 }, // 训练项目1  
+      { "item": "哑铃飞鸟", "sets": 3, "reps": 12 }, // 训练项目2  
+      { "item": "俯卧撑", "sets": 3, "reps": 15 }, // 训练项目3  
+      { "item": "拉力器扩胸", "sets": 3, "reps": 12 } // 训练项目4  
+    ]
+  },
+  {
+    "objectId": "plan002",
+    "date": "2024-12-22",
+    "trainingPart": "下肢",
+    "trainingItems": [
+      { "item": "深蹲", "sets": 4, "reps": 10 },
+      { "item": "腿举", "sets": 3, "reps": 12 },
+      { "item": "弓步", "sets": 3, "reps": 10 },
+      { "item": "小腿提踵", "sets": 3, "reps": 15 }
+    ]
+  },
+  {
+    "objectId": "plan003",
+    "date": "2024-12-25",
+    "trainingPart": "背部",
+    "trainingItems": [
+      { "item": "引体向上", "sets": 4, "reps": 8 },
+      { "item": "划船", "sets": 3, "reps": 10 },
+      { "item": "背部伸展", "sets": 3, "reps": 12 },
+      { "item": "拉力器划船", "sets": 3, "reps": 12 }
+    ]
+  },
+  {
+    "objectId": "plan004",
+    "date": "2024-12-28",
+    "trainingPart": "全身",
+    "trainingItems": [
+      { "item": "HIIT训练", "sets": 4, "reps": 30 }, // 30秒高强度  
+      { "item": "核心训练", "sets": 3, "reps": 15 },
+      { "item": "有氧运动", "sets": 1, "reps": 20 }, // 20分钟  
+      { "item": "拉伸", "sets": 1, "reps": 10 } // 10分钟  
+    ]
+  },
+  {
+    "objectId": "plan005",
+    "date": "2024-12-30",
+    "trainingPart": "上肢",
+    "trainingItems": [
+      { "item": "哑铃弯举", "sets": 4, "reps": 10 },
+      { "item": "三头肌下压", "sets": 3, "reps": 12 },
+      { "item": "肩推", "sets": 3, "reps": 10 },
+      { "item": "侧平举", "sets": 3, "reps": 12 }
+    ]
+  }
+];
+module.exports.CoachList = [
+  {
+    "objectId": "coach001",
+    "qualification": "国家认证健身教练",
+    "gender": "男",
+    "name": "李明",
+    "specialize": "力量训练",
+    "age": 32
+  },
+  {
+    "objectId": "coach002",
+    "qualification": "国际健身教练认证",
+    "gender": "女",
+    "name": "张婷",
+    "specialize": "灵活性训练",
+    "age": 29
+  },
+  {
+    "objectId": "coach003",
+    "qualification": "高级私人教练",
+    "gender": "男",
+    "name": "王强",
+    "specialize": "综合训练",
+    "age": 38
+  }
+];

+ 60 - 0
TFPower-server/lib/migration/import-data.js

@@ -0,0 +1,60 @@
+const { CloudQuery, CloudObject } = require("../ncloud");
+const { UserList, PlanList, CoachList } = require("./data");
+inportDapartAndDoctor()
+
+DataMap = {
+    fitUser: {},
+    fitPlan: {},
+    Coach: {}
+}
+
+async function inportDapartAndDoctor() {
+    // 导入科室数据
+    let departList = CoachList
+    for (let index = 0; index < departList.length; index++) {
+        let depart = departList[index];
+        depart = await importObject("Coach", depart)
+    }
+
+    // 导入医生数据
+    // let doctorList = PlanList
+    // for (let index = 0; index < doctorList.length; index++) {
+    //     let doctor = doctorList[index];
+    //     doctor = await importObject("fitPlan", doctor)
+    // }
+    // console.log(DataMap["Doctor"])
+}
+
+async function importObject(className, data) {
+
+    // 查重 srcId 数据源列表中的objectId并非数据库生成的唯一ID,因此需要有一个srcId字段进行记录,并查重
+    let query = new CloudQuery(className)
+    let srcId = data.objectId
+    query.equalTo("srcId", srcId)
+    let importObj = await query.first()
+    console.log(importObj)
+
+    // 导入
+    // 导入前批量处理Pointer类型数据,进行重定向
+    Object.keys(data)?.forEach(key => {
+        let field = data[key]
+        let srcId = field?.objectId
+        if (srcId) { // 是数组字段
+            if (key == "depart") {
+                data[key] = DataMap?.["Department"]?.[srcId]?.toPointer();
+            }
+        }
+    })
+
+    // 若未添加,则创建新对象并保存
+    if (!importObj?.id) {
+        importObj = new CloudObject(className)
+    }
+
+    // 保存或更新数据
+    data.srcId = srcId;
+    importObj.set(data);
+    importObj = await importObj.save();
+
+    DataMap[className][srcId] = importObj
+}

+ 165 - 0
TFPower-server/lib/ncloud.js

@@ -0,0 +1,165 @@
+
+
+class CloudObject{
+    id
+    className
+    data = {}
+    constructor(className){
+        this.className = className
+    }
+    toPointer(){
+        return {"__type":"Pointer","className":this.className,"objectId":this.id}
+    }
+    set(json){
+        Object.keys(json).forEach(key=>{
+            if(["objectId","id","createdAt","updatedAt","ACL"].indexOf(key)>-1){
+                return
+            }
+            this.data[key] = json[key]
+        })
+    }
+    get(key){
+        return this.data[key] || null
+    }
+    async save(){
+        let method = "POST"
+        let url = "http://dev.fmode.cn:1337/parse/classes/" + this.className
+        // 更新
+        if(this.id){
+            url += "/"+this.id
+            method = "PUT"
+        } 
+        let body = JSON.stringify(this.data)
+        let response = await fetch(url, {
+            "headers": {
+              "content-type": "application/json;charset=UTF-8",
+              "x-parse-application-id": "dev"
+            },
+            "body": body,
+            "method": method,
+            "mode": "cors",
+            "credentials": "omit"
+          });
+          let result = await response?.json();
+          if(result?.error){
+            console.error(result?.error)
+          }
+          if(result?.objectId){this.id = result?.objectId}
+          return this
+    }
+    async destory(){
+        if(!this.id) return
+        let response = await fetch("http://dev.fmode.cn:1337/parse/classes/Doctor/"+this.id, {
+            "headers": {
+              "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "DELETE",
+            "mode": "cors",
+            "credentials": "omit"
+          });
+          let result = await response?.json();
+          if(result){
+            this.id = null
+        }
+        return true
+    }
+}
+
+class CloudQuery{
+    className
+    constructor(className){
+        this.className = className
+    }
+
+    whereOptions = {}
+    greaterThan(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+        this.whereOptions[key]["$gt"] = value
+    }
+    greaterThanAndEqualTo(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+        this.whereOptions[key]["$gte"] = value
+    }
+    lessThan(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+        this.whereOptions[key]["$lt"] = value
+    }
+    lessThanAndEqualTo(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+        this.whereOptions[key]["$lte"] = value
+    }
+    equalTo(key,value){
+        this.whereOptions[key] = value
+    }
+
+    async get(id){
+        let url = "http://dev.fmode.cn:1337/parse/classes/"+this.className+"/"+id+"?"
+
+        let response = await fetch(url, {
+            "headers": {
+            "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+            "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let json = await response?.json();
+        return json || {}
+    }
+    async find(){
+        let url = "http://dev.fmode.cn:1337/parse/classes/"+this.className+"?"
+        
+        if(Object.keys(this.whereOptions)?.length){
+            let whereStr = JSON.stringify(this.whereOptions)
+            url += `where=${whereStr}`
+        }
+
+        let response = await fetch(url, {
+            "headers": {
+            "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+            "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let json = await response?.json();
+        return json?.results || []
+    }
+    async first(){
+        let url = "http://dev.fmode.cn:1337/parse/classes/"+this.className+"?"
+        
+        if(Object.keys(this.whereOptions)?.length){
+            let whereStr = JSON.stringify(this.whereOptions)
+            url += `where=${whereStr}`
+        }
+
+        let response = await fetch(url, {
+            "headers": {
+            "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+            "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+        });
+        let json = await response?.json();
+        let exists = json?.results?.[0] || null
+        if(exists){
+            let existsObject = new CloudObject(this.className)
+            existsObject.set(exists)
+            existsObject.id = exists.objectId
+            existsObject.createdAt = exists.createdAt
+            existsObject.updatedAt = exists.updatedAt
+            return existsObject
+        }
+    }
+}
+
+module.exports.CloudObject = CloudObject
+module.exports.CloudQuery = CloudQuery

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "202226701041",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}