10 Angajamente d02dd8c94b ... a509d0bd9b

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  xukang a509d0bd9b Merge branch 'xk' 10 luni în urmă
  xukang a08d63280f Merge branch 'master' of http://git.fmode.cn:3000/18779989085/202226701041 into xk 10 luni în urmă
  xukang 1dfc65cc50 add come pages 10 luni în urmă
  xukang 10f9c6edb1 feat:update plan page 10 luni în urmă
  xukang 7650ade0c0 feat : add come new pages and components 10 luni în urmă
  xukang 171075115b feat:add a edit component 10 luni în urmă
  xukang e6121fcabd update:问诊页面 10 luni în urmă
  xukang b8f1ebb103 update:some new pages 10 luni în urmă
  xukang d7efc3458e update: 新增加schema文档 10 luni în urmă
  xukang e0be145dce 测试成功,不需要了 10 luni în urmă
59 a modificat fișierele cu 4020 adăugiri și 23 ștergeri
  1. 4 1
      TFPower-app/angular.json
  2. 1 0
      TFPower-app/src/app/app.routes.ts
  3. 1 1
      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. BIN
      TFPower-app/src/assets/images/ache.jpg
  40. BIN
      TFPower-app/src/assets/images/action.png
  41. BIN
      TFPower-app/src/assets/images/coach1.jpg
  42. BIN
      TFPower-app/src/assets/images/coach2.jpg
  43. BIN
      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 - 1
TFPower-app/src/app/tab1/tab1.page.ts

@@ -69,7 +69,7 @@ import { AiplanPageComponent } from '../page/aiplan-page/aiplan-page.component';
   ],
 })
 export class Tab1Page {
-  constructor(private router: Router) {}
+  constructor(private router: Router) { }
   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: () =>

BIN
TFPower-app/src/assets/images/ache.jpg


BIN
TFPower-app/src/assets/images/action.png


BIN
TFPower-app/src/assets/images/coach1.jpg


BIN
TFPower-app/src/assets/images/coach2.jpg


BIN
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": {}
+}