2 Commits 9e2045cb6d ... 03581c8e00

Author SHA1 Message Date
  0225216 03581c8e00 feat adddata 2 days ago
  0225216 e86aa18923 feat historycheck 2 days ago
45 changed files with 3980 additions and 656 deletions
  1. 74 0
      src/app/tab1/add-data-modal/add-data-modal.component.html
  2. 0 0
      src/app/tab1/add-data-modal/add-data-modal.component.scss
  3. 61 0
      src/app/tab1/add-data-modal/add-data-modal.component.ts
  4. 78 105
      src/app/tab1/tab1.page.html
  5. 64 0
      src/app/tab1/tab1.page.scss
  6. 348 144
      src/app/tab1/tab1.page.ts
  7. 39 82
      src/app/tab2/tab2.page.html
  8. 271 105
      src/app/tab2/tab2.page.scss
  9. 149 50
      src/app/tab2/tab2.page.ts
  10. 26 29
      src/app/tab3/tab3.page.html
  11. 86 67
      src/app/tab3/tab3.page.scss
  12. 90 1
      src/app/tab3/tab3.page.ts
  13. BIN
      src/assets/images/default-avatar.jpg
  14. BIN
      src/assets/images/doc.jpeg
  15. 119 0
      src/lib/consult-tasks/consult-tasks.ts
  16. 59 0
      src/lib/consult-tasks/symptom-input/symptom-input.modal.ts
  17. 37 0
      src/lib/consult-tasks/triage-result/triage-result.component.ts
  18. 147 0
      src/lib/flow-display/flow-display.component.ts
  19. 125 0
      src/lib/flow.executor.ts
  20. 186 0
      src/lib/flow.task.ts
  21. 9 0
      src/lib/flow.workflow.ts
  22. 116 72
      src/lib/import.data.ts
  23. 59 0
      src/lib/job-workflow/job-workflow.ts
  24. 89 0
      src/lib/job-workflow/task.job.ts
  25. 127 0
      src/lib/mock-tasks/mock-tasks.ts
  26. 1 1
      src/lib/ncloud.ts
  27. 30 0
      src/lib/tasks/task-completion-json/README.md
  28. 219 0
      src/lib/tasks/task-completion-json/task-completion-json.ts
  29. 43 0
      src/lib/tasks/task-completion-text/README.md
  30. 165 0
      src/lib/tasks/task-completion-text/task-completion-text.ts
  31. 269 0
      src/lib/tasks/task-user-form/README.md
  32. 143 0
      src/lib/tasks/task-user-form/form-collector/form-collector.component.html
  33. 17 0
      src/lib/tasks/task-user-form/form-collector/form-collector.component.scss
  34. 94 0
      src/lib/tasks/task-user-form/form-collector/form-collector.component.ts
  35. 59 0
      src/lib/tasks/task-user-form/get-user-form.ts
  36. 75 0
      src/lib/tasks/task-user-form/task-user-form.ts
  37. 29 0
      src/lib/user/modal-user-edit/modal-user-edit.component.html
  38. 0 0
      src/lib/user/modal-user-edit/modal-user-edit.component.scss
  39. 22 0
      src/lib/user/modal-user-edit/modal-user-edit.component.spec.ts
  40. 65 0
      src/lib/user/modal-user-edit/modal-user-edit.component.ts
  41. 36 0
      src/lib/user/modal-user-login/modal-user-login.component.html
  42. 0 0
      src/lib/user/modal-user-login/modal-user-login.component.scss
  43. 22 0
      src/lib/user/modal-user-login/modal-user-login.component.spec.ts
  44. 92 0
      src/lib/user/modal-user-login/modal-user-login.component.ts
  45. 240 0
      src/lib/user/token-guard/token.guard.ts

+ 74 - 0
src/app/tab1/add-data-modal/add-data-modal.component.html

@@ -0,0 +1,74 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>添加健康数据</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="cancel()">取消</ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <ion-segment [(ngModel)]="selectedType">
+    <ion-segment-button value="bloodGlucose">
+      <ion-label>血糖</ion-label>
+    </ion-segment-button>
+    <ion-segment-button value="bloodPressure">
+      <ion-label>血压</ion-label>
+    </ion-segment-button>
+    <ion-segment-button value="heartRate">
+      <ion-label>心率</ion-label>
+    </ion-segment-button>
+  </ion-segment>
+
+  <form (ngSubmit)="save()">
+    <!-- 血糖表单 -->
+    <div *ngIf="selectedType === 'bloodGlucose'">
+      <ion-item>
+        <ion-label position="floating">血糖值 (mmol/L)</ion-label>
+        <ion-input type="number" [(ngModel)]="formData.value" name="glucoseValue" required></ion-input>
+      </ion-item>
+      
+      <ion-item>
+        <ion-label position="floating">测量类型</ion-label>
+        <ion-select [(ngModel)]="formData.measurementType" name="measurementType">
+          <ion-select-option *ngFor="let type of measurementTypes" [value]="type">{{type}}</ion-select-option>
+        </ion-select>
+      </ion-item>
+    </div>
+
+    <!-- 血压表单 -->
+    <div *ngIf="selectedType === 'bloodPressure'">
+      <ion-item>
+        <ion-label position="floating">血压值 (mmHg)</ion-label>
+        <ion-input type="text" [(ngModel)]="formData.value" name="bloodPressure" placeholder="例如: 120/80" required></ion-input>
+      </ion-item>
+      
+      <ion-item>
+        <ion-label position="floating">脉搏 (次/分)</ion-label>
+        <ion-input type="number" [(ngModel)]="formData.pulse" name="pulse"></ion-input>
+      </ion-item>
+    </div>
+
+    <!-- 心率表单 -->
+    <div *ngIf="selectedType === 'heartRate'">
+      <ion-item>
+        <ion-label position="floating">心率值 (次/分)</ion-label>
+        <ion-input type="number" [(ngModel)]="formData.value" name="heartRate" required></ion-input>
+      </ion-item>
+      
+      <ion-item>
+        <ion-label position="floating">测量方法</ion-label>
+        <ion-select [(ngModel)]="formData.measurementMethod" name="measurementMethod">
+          <ion-select-option *ngFor="let method of measurementMethods" [value]="method">{{method}}</ion-select-option>
+        </ion-select>
+      </ion-item>
+    </div>
+
+    <ion-item>
+      <ion-label position="floating">备注</ion-label>
+      <ion-textarea [(ngModel)]="formData.notes" name="notes"></ion-textarea>
+    </ion-item>
+
+    <ion-button expand="block" type="submit" [disabled]="!validateForm()">保存</ion-button>
+  </form>
+</ion-content>

+ 0 - 0
src/app/tab1/add-data-modal/add-data-modal.component.scss


+ 61 - 0
src/app/tab1/add-data-modal/add-data-modal.component.ts

@@ -0,0 +1,61 @@
+import { Component } from '@angular/core';
+import { ModalController } from '@ionic/angular/standalone';
+import { FormsModule } from '@angular/forms';
+import { IonicModule } from '@ionic/angular';
+import { CommonModule } from '@angular/common';
+@Component({
+  selector: 'app-add-data-modal',
+  templateUrl: './add-data-modal.component.html',
+  styleUrls: ['./add-data-modal.component.scss'],
+  standalone: true,
+  imports: [IonicModule, FormsModule,CommonModule]
+})
+export class AddDataModalComponent {
+  selectedType: 'bloodGlucose' | 'bloodPressure' | 'heartRate' = 'bloodGlucose';
+  formData: any = {
+    value: '',
+    measurementType: '空腹',
+    pulse: '',
+    measurementMethod: '手动测量',
+    notes: ''
+  };
+
+  measurementTypes = ['空腹', '餐后', '随机'];
+  measurementMethods = ['手动测量', '设备测量'];
+
+  constructor(private modalCtrl: ModalController) {}
+
+  cancel() {
+    this.modalCtrl.dismiss();
+  }
+
+  save() {
+    if (!this.validateForm()) {
+      return;
+    }
+
+    this.modalCtrl.dismiss({
+      type: this.selectedType,
+      formData: this.formData
+    });
+  }
+
+  validateForm(): boolean {
+    if (!this.formData.value) {
+      return false;
+    }
+
+    if (this.selectedType === 'bloodPressure') {
+      const values = this.formData.value.split('/');
+      if (values.length !== 2 || isNaN(values[0]) || isNaN(values[1])) {
+        return false;
+      }
+    } else {
+      if (isNaN(this.formData.value)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}

+ 78 - 105
src/app/tab1/tab1.page.html

@@ -15,91 +15,34 @@
     <ion-refresher-content></ion-refresher-content>
     <ion-refresher-content></ion-refresher-content>
   </ion-refresher>
   </ion-refresher>
 
 
-  <!-- 用户统计信息卡片 -->
-  <ion-card class="user-card">
-    <ion-grid>
-      <ion-row class="ion-align-items-center">
-        <!-- 用户信息 -->
-        <ion-col size="4">
-          <ion-avatar class="user-avatar">
-            <img src="assets/images/avatar.png" />
-          </ion-avatar>
-          <ion-text class="ion-text-nowrap">
-            <h2>王先生</h2>
-            <ion-chip color="danger" outline>糖尿病</ion-chip>
-          </ion-text>
-        </ion-col>
-
-        <!-- 健康指数 -->
-        <ion-col size="5">
-          <div class="health-index">
-            <ion-text color="light">综合健康指数</ion-text>
-            <div class="index-value">
-              85
-              <ion-icon name="caret-up" color="success"></ion-icon>
-            </div>
-          </div>
-        </ion-col>
-
-        <!-- 健康动态 -->
-        <ion-col size="3">
-          <ion-text color="light" class="dynamic-text">
-            您的血糖控制良好,继续保持!
-            <ion-button fill="clear" size="small">更多</ion-button>
-          </ion-text>
-        </ion-col>
-      </ion-row>
-    </ion-grid>
-  </ion-card>
-
+  
   <!-- 健康数据概览 -->
   <!-- 健康数据概览 -->
-  <ion-card class="data-card">
-    <ion-grid>
-      <ion-row>
-        <ion-col size="6" *ngFor="let metric of metrics">
-          <div class="metric-item" [class.abnormal]="metric.abnormal">
-            <div class="metric-header">
-              <ion-text>{{metric.name}}</ion-text>
-              <ion-icon [name]="metric.trend" [color]="metric.trendColor"></ion-icon>
-            </div>
-            <div class="metric-value">
-              {{metric.value}}
-              <ion-text class="unit">{{metric.unit}}</ion-text>
-            </div>
-            <ion-text color="medium" class="reference">参考: {{metric.reference}}</ion-text>
+<ion-card class="data-card">
+  <ion-grid>
+    <ion-row>
+      <ion-col size="6" *ngFor="let metric of metrics">
+        <div class="metric-item" [class.abnormal]="metric.abnormal">
+          <div class="metric-header">
+            <ion-text>{{metric.name}}</ion-text>
+            <ion-icon [name]="metric.trend" [color]="metric.trendColor"></ion-icon>
           </div>
           </div>
-        </ion-col>
-      </ion-row>
-    </ion-grid>
-    <ion-fab vertical="bottom" horizontal="end" slot="fixed">
-      <ion-fab-button (click)="addData()">
-        <ion-icon name="add"></ion-icon>
-      </ion-fab-button>
-    </ion-fab>
-  </ion-card>
-<!--今日任务-->
-  <ion-card class="task-card">
-    <ion-item lines="none">
-      <ion-label (click)="importData()">今日任务 (3/5)</ion-label>
-      <ion-progress-bar value="0.6" color="primary"></ion-progress-bar>
-    </ion-item>
+          <div class="metric-value">
+            {{metric.value}}
+            <ion-text class="unit">{{metric.unit}}</ion-text>
+          </div>
+          <ion-text color="medium" class="reference">参考: {{metric.reference}}</ion-text>
+          <ion-text color="medium" class="time">测量时间: {{metric.time}}</ion-text>
+        </div>
+      </ion-col>
+    </ion-row>
+  </ion-grid>
+  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
+    <ion-fab-button (click)="addData()">
+      <ion-icon name="add"></ion-icon>
+    </ion-fab-button>
+  </ion-fab>
+</ion-card>
 
 
-    <ion-list lines="none">
-      <ion-item *ngFor="let task of tasks" [class.recommended]="task.recommended">
-        <ion-icon :icon="task.icon" slot="start" color="primary"></ion-icon>
-        <ion-label>
-          <h3>{{task.title}}</h3>
-          <p>{{task.description}}</p>
-          <ion-text color="medium">{{task.time}}</ion-text>
-        </ion-label>
-        <ion-icon 
-          :name="task.completed ? 'checkmark-circle' : 'ellipse'" 
-          slot="end" 
-          [color]="task.completed ? 'success' : 'medium'">
-        </ion-icon>
-      </ion-item>
-    </ion-list>
-  </ion-card>
   
   
   <!-- 健康数据记录 -->
   <!-- 健康数据记录 -->
 <ion-card class="health-data-card">
 <ion-card class="health-data-card">
@@ -180,28 +123,58 @@
     </ion-list>
     </ion-list>
   </ion-card>
   </ion-card>
 </ion-card>
 </ion-card>
-
-<!--对话智能体-->
-  <ion-card class="doctor-card">
-  <div class="card-content">
-    <div class="doctor-avatar">
-      <img src="/assets/images/0.jpg" alt="慢性病医师头像" class="avatar-image">
-    </div>
-    
-    <div class="doctor-info">
-      <h2 class="doctor-name">丁医生</h2>
-      <p class="doctor-title">慢性病管理主任医师</p>
-      <div class="doctor-specialty">
-        <ion-badge color="primary">糖尿病</ion-badge>
-        <ion-badge color="primary">高血压</ion-badge>
-        <ion-badge color="primary">冠心病</ion-badge>
-      </div>
-      <p class="doctor-desc">10年慢性病管理经验。擅长个性化治疗方案制定和长期健康管理。</p>
-    </div>
-  </div>
+<!-- 今日数据模块 -->
+<ion-card class="today-data-card">
+  <ion-item lines="none">
+    <ion-label>今日数据</ion-label>
+    <ion-button fill="clear" size="small" (click)="openAddDataModal()">
+      <ion-icon name="add" slot="start"></ion-icon>
+      添加数据
+    </ion-button>
+  </ion-item>
   
   
-  <ion-button (click)="openConsult()" expand="block" color="success" class="consult-btn">
-    <ion-icon name="medical" slot="start"></ion-icon>
-    立即咨询
-  </ion-button>
+  <ion-list lines="none">
+    <ion-item *ngIf="todayData.bloodGlucose.length === 0 && todayData.bloodPressure.length === 0 && todayData.heartRate.length === 0">
+      <ion-label class="ion-text-center" color="medium">
+        今日暂无数据,点击上方按钮添加
+      </ion-label>
+    </ion-item>
+    
+    <!-- 今日血糖数据 -->
+    <ion-item *ngFor="let item of todayData.bloodGlucose">
+      <ion-icon name="water" slot="start" color="primary"></ion-icon>
+      <ion-label>
+        <h3>{{item.get('glucoseValue')}} mmol/L</h3>
+        <p>{{item.get('measurementType')}} · {{formatTime(item.get('measurementTime'))}}</p>
+      </ion-label>
+      <ion-badge slot="end" [color]="getGlucoseColor(item.get('glucoseValue'))">
+        {{getGlucoseStatus(item.get('glucoseValue'))}}
+      </ion-badge>
+    </ion-item>
+    
+    <!-- 今日血压数据 -->
+    <ion-item *ngFor="let item of todayData.bloodPressure">
+      <ion-icon name="speedometer" slot="start" color="danger"></ion-icon>
+      <ion-label>
+        <h3>{{item.get('systolic')}}/{{item.get('diastolic')}} mmHg</h3>
+        <p>脉搏: {{item.get('pulse')}} · {{formatTime(item.get('measurementTime'))}}</p>
+      </ion-label>
+      <ion-badge slot="end" [color]="getPressureColor(item.get('systolic'), item.get('diastolic'))">
+        {{getPressureStatus(item.get('systolic'), item.get('diastolic'))}}
+      </ion-badge>
+    </ion-item>
+    
+    <!-- 今日心率数据 -->
+    <ion-item *ngFor="let item of todayData.heartRate">
+      <ion-icon name="heart" slot="start" color="danger"></ion-icon>
+      <ion-label>
+        <h3>{{item.get('heartRate')}} 次/分</h3>
+        <p>{{item.get('measurementMethod')}} · {{formatTime(item.get('measurementTime'))}}</p>
+      </ion-label>
+      <ion-badge slot="end" [color]="getHeartRateColor(item.get('heartRate'))">
+        {{getHeartRateStatus(item.get('heartRate'))}}
+      </ion-badge>
+    </ion-item>
+  </ion-list>
 </ion-card>
 </ion-card>
+

+ 64 - 0
src/app/tab1/tab1.page.scss

@@ -160,4 +160,68 @@
   ion-badge {
   ion-badge {
     font-weight: normal;
     font-weight: normal;
   }
   }
+}
+.metric-item {
+  padding: 12px;
+  border-radius: 8px;
+  background: var(--ion-color-light);
+  margin-bottom: 8px;
+  
+  &.abnormal {
+    background: rgba(var(--ion-color-warning-rgb), 0.1);
+  }
+
+  .metric-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 4px;
+  }
+
+  .metric-value {
+    font-size: 24px;
+    font-weight: bold;
+    margin: 8px 0;
+    
+    .unit {
+      font-size: 14px;
+      font-weight: normal;
+      margin-left: 4px;
+    }
+  }
+
+  .reference, .time {
+    display: block;
+    font-size: 12px;
+    margin-top: 4px;
+  }
+
+  .time {
+    color: var(--ion-color-medium-shade);
+  }
+}
+.today-data-card {
+  margin-bottom: 16px;
+  
+  ion-item {
+    --padding-start: 0;
+    --inner-padding-end: 0;
+  }
+  
+  .no-data {
+    padding: 16px;
+    text-align: center;
+    color: var(--ion-color-medium);
+  }
+}
+
+// 添加数据模态框样式
+.add-data-modal {
+  ion-segment {
+    margin-bottom: 16px;
+  }
+  
+  ion-button[type="submit"] {
+    margin-top: 24px;
+  }
 }
 }

+ 348 - 144
src/app/tab1/tab1.page.ts

@@ -1,10 +1,8 @@
 import { Component, OnInit } from '@angular/core';
 import { Component, OnInit } from '@angular/core';
-import{ChatPanelOptions,FmChatModalInput,FmodeChat,FmodeChatMessage,openChatPanelModal}from 'fmode-ng';
-import Parse from "parse";
-import{ModalController}from '@ionic/angular/standalone';
-import {  CloudUser, CloudQuery  } from 'src/lib/ncloud';
+import{ModalController, AlertController}from '@ionic/angular/standalone';
+import {  CloudUser, CloudQuery, CloudObject  } from 'src/lib/ncloud';
 import { importTestData } from 'src/lib/import.data';
 import { importTestData } from 'src/lib/import.data';
-
+import { AddDataModalComponent } from 'src/app/tab1/add-data-modal/add-data-modal.component'; 
 @Component({
 @Component({
   selector: 'app-tab1',
   selector: 'app-tab1',
   templateUrl: 'tab1.page.html',
   templateUrl: 'tab1.page.html',
@@ -18,16 +16,237 @@ export class Tab1Page implements OnInit{
     heartRate: [] as any[],
     heartRate: [] as any[],
     loading: true
     loading: true
   };
   };
+ todayData = {
+    bloodGlucose: [] as any[],
+    bloodPressure: [] as any[],
+    heartRate: [] as any[],
+    loading: true}
+  // 更新 metrics 结构以包含时间和时间戳
+  metrics: Array<{
+  name: string;
+  value: string;
+  unit: string;
+  trend: string;
+  trendColor: string;
+  reference: string;
+  abnormal: boolean;
+  time: string;
+  timestamp: Date | null;
+}> = [
+  { 
+    name: '血糖', 
+    value: '--', 
+    unit: 'mmol/L',
+    trend: 'caret-up', 
+    trendColor: 'medium',
+    reference: '3.9-6.1',
+    abnormal: false,
+    time: '暂无数据',
+    timestamp: null
+  },
+  {
+    name: '血压',
+    value: '--/--',
+    unit: 'mmHg',
+    trend: 'caret-down',
+    trendColor: 'medium',
+    reference: '120/80',
+    abnormal: false,
+    time: '暂无数据',
+    timestamp: null
+  },
+  {
+    name: '心率',
+    value: '--',
+    unit: '次/分',
+    trend: 'caret-up',
+    trendColor: 'medium',
+    reference: '60-100',
+    abnormal: false,
+    time: '暂无数据',
+    timestamp: null
+  }
+];
   constructor(
   constructor(
-    private modalCtrl:ModalController
+    private modalCtrl:ModalController,
+    private alertCtrl:AlertController
   ){}
   ){}
 
 
   async ngOnInit() {
   async ngOnInit() {
     await this.loadHealthData();
     await this.loadHealthData();
+    await this.loadTodayData();
   }
   }
   // 加载健康数据
   // 加载健康数据
-  // 在 tab1.page.ts 中更新以下方法
+ async loadTodayData() {
+    try {
+      this.todayData.loading = true;
+      
+      const user = new CloudUser();
+      const currentUser = await user.current();
+      
+      if (!currentUser?.id) {
+        console.warn('未获取到当前用户');
+        return;
+      }
+
+      // 获取今天0点的时间
+      const today = new Date();
+      today.setHours(0, 0, 0, 0);
+
+      const [glucose, pressure, heartRate] = await Promise.all([
+        this.queryTodayData('BloodGlucose', currentUser, today),
+        this.queryTodayData('BloodPressure', currentUser, today),
+        this.queryTodayData('HeartRate', currentUser, today)
+      ]);
+
+      this.todayData = {
+        bloodGlucose: glucose,
+        bloodPressure: pressure,
+        heartRate: heartRate,
+        loading: false
+      };
+    } catch (error) {
+      console.error('加载今日数据失败:', error);
+      this.todayData.loading = false;
+    }
+  }
+
+  private async queryTodayData(className: string, user: CloudUser, date: Date): Promise<any[]> {
+    const query = new CloudQuery(className);
+    query.equalTo('patient', {
+      __type: 'Pointer',
+      className: '_User',
+      objectId: user.id
+    });
+    query.greaterThan('measurementTime', date);
+    query.descending('measurementTime'); // 按时间降序排列
+    return await query.find();
+  }
+
+  // 打开添加数据模态框
+  async openAddDataModal() {
+    const modal = await this.modalCtrl.create({
+      component: AddDataModalComponent
+    });
+
+    await modal.present();
+
+    const { data } = await modal.onWillDismiss();
+    if (data) {
+      await this.saveHealthData(data.type, data.formData);
+      await this.loadTodayData(); // 重新加载今日数据
+      await this.loadHealthData(); // 重新加载所有健康数据
+    }
+  }
+
+  // 保存健康数据
+  async saveHealthData(type: string, formData: any) {
+    try {
+      const user = new CloudUser();
+      const currentUser = await user.current();
+      
+      if (!currentUser?.id) {
+        console.warn('未获取到当前用户');
+        return;
+      }
 
 
+      let className = '';
+      let dataObject: any = {
+        patient: {
+          __type: 'Pointer',
+          className: '_User',
+          objectId: currentUser.id
+        },
+        measurementTime: new Date()
+      };
+
+      switch (type) {
+        case 'bloodGlucose':
+          className = 'BloodGlucose';
+          dataObject.glucoseValue = parseFloat(formData.value);
+          dataObject.measurementType = formData.measurementType || '空腹';
+          if (formData.notes) dataObject.notes = formData.notes;
+          break;
+          
+        case 'bloodPressure':
+          className = 'BloodPressure';
+          const values = formData.value.split('/');
+          dataObject.systolic = parseInt(values[0]);
+          dataObject.diastolic = parseInt(values[1]);
+          dataObject.pulse = parseInt(formData.pulse) || 0;
+          if (formData.notes) dataObject.notes = formData.notes;
+          break;
+          
+        case 'heartRate':
+          className = 'HeartRate';
+          dataObject.heartRate = parseInt(formData.value);
+          dataObject.measurementMethod = formData.measurementMethod || '手动测量';
+          if (formData.notes) dataObject.notes = formData.notes;
+          break;
+      }
+
+      const healthData = new CloudObject(className);
+      healthData.set(dataObject);
+      await healthData.save();
+
+      const alert = await this.alertCtrl.create({
+        header: '成功',
+        message: '数据添加成功',
+        buttons: ['确定']
+      });
+      await alert.present();
+
+    } catch (error) {
+      console.error('保存数据失败:', error);
+      
+      const alert = await this.alertCtrl.create({
+        header: '错误',
+        message: '数据添加失败,请重试',
+        buttons: ['确定']
+      });
+      await alert.present();
+    }
+  }
+
+  // 辅助方法:获取血糖状态
+  getGlucoseStatus(value: number): string {
+    if (value <= 6.1) return '正常';
+    if (value <= 7.0) return '偏高';
+    return '过高';
+  }
+
+  // 辅助方法:获取血糖颜色
+  getGlucoseColor(value: number): string {
+    if (value <= 6.1) return 'success';
+    if (value <= 7.0) return 'warning';
+    return 'danger';
+  }
+
+  // 辅助方法:获取血压状态
+  getPressureStatus(systolic: number, diastolic: number): string {
+    if (systolic <= 120 && diastolic <= 80) return '正常';
+    if ((systolic <= 140 && diastolic <= 90)) return '偏高';
+    return '过高';
+  }
+
+  // 辅助方法:获取血压颜色
+  getPressureColor(systolic: number, diastolic: number): string {
+    if (systolic <= 120 && diastolic <= 80) return 'success';
+    if ((systolic <= 140 && diastolic <= 90)) return 'warning';
+    return 'danger';
+  }
+
+  // 辅助方法:获取心率状态
+  getHeartRateStatus(value: number): string {
+    if (value >= 60 && value <= 100) return '正常';
+    return '异常';
+  }
+
+  // 辅助方法:获取心率颜色
+  getHeartRateColor(value: number): string {
+    if (value >= 60 && value <= 100) return 'success';
+    return 'warning';
+  }
 async loadHealthData() {
 async loadHealthData() {
   try {
   try {
     this.healthData.loading = true;
     this.healthData.loading = true;
@@ -58,12 +277,117 @@ async loadHealthData() {
       heartRate: heartRate,
       heartRate: heartRate,
       loading: false
       loading: false
     };
     };
+     this.updateMetricsOverview();
   } catch (error) {
   } catch (error) {
     console.error('加载失败:', error);
     console.error('加载失败:', error);
     this.healthData.loading = false;
     this.healthData.loading = false;
   }
   }
+  
 }
 }
 
 
+
+
+// 更新概览数据
+  updateMetricsOverview() {
+    // 更新血糖数据
+    if (this.healthData.bloodGlucose.length > 0) {
+      const latestGlucose = this.healthData.bloodGlucose[0];
+      const glucoseValue = latestGlucose.get('glucoseValue');
+      const measureTime = new Date(latestGlucose.get('measurementTime'));
+      
+      this.metrics[0].value = glucoseValue.toFixed(1);
+      this.metrics[0].time = this.formatTime(measureTime);
+      this.metrics[0].timestamp = measureTime;
+      
+      // 设置趋势和异常状态
+      if (glucoseValue > 6.1) {
+        this.metrics[0].abnormal = true;
+        this.metrics[0].trendColor = glucoseValue > 7.0 ? 'danger' : 'warning';
+      } else {
+        this.metrics[0].abnormal = false;
+        this.metrics[0].trendColor = 'success';
+      }
+    }
+
+    // 更新血压数据
+    if (this.healthData.bloodPressure.length > 0) {
+      const latestPressure = this.healthData.bloodPressure[0];
+      const systolic = latestPressure.get('systolic');
+      const diastolic = latestPressure.get('diastolic');
+      const measureTime = new Date(latestPressure.get('measurementTime'));
+      
+      this.metrics[1].value = `${systolic}/${diastolic}`;
+      this.metrics[1].time = this.formatTime(measureTime);
+      this.metrics[1].timestamp = measureTime;
+      
+      // 设置趋势和异常状态
+      if (systolic > 120 || diastolic > 80) {
+        this.metrics[1].abnormal = true;
+        this.metrics[1].trendColor = (systolic > 140 || diastolic > 90) ? 'danger' : 'warning';
+      } else {
+        this.metrics[1].abnormal = false;
+        this.metrics[1].trendColor = 'success';
+      }
+    }
+
+    // 更新心率数据
+    if (this.healthData.heartRate.length > 0) {
+      const latestHeartRate = this.healthData.heartRate[0];
+      const heartRateValue = latestHeartRate.get('heartRate');
+      const measureTime = new Date(latestHeartRate.get('measurementTime'));
+      
+      this.metrics[2].value = heartRateValue.toString();
+      this.metrics[2].time = this.formatTime(measureTime);
+      this.metrics[2].timestamp = measureTime;
+      
+      // 设置趋势和异常状态
+      if (heartRateValue < 60 || heartRateValue > 100) {
+        this.metrics[2].abnormal = true;
+        this.metrics[2].trendColor = 'warning';
+      } else {
+        this.metrics[2].abnormal = false;
+        this.metrics[2].trendColor = 'success';
+      }
+    }
+  }
+
+  // 格式化时间显示
+ formatTime(date: any): string {
+  if (!date) return '暂无数据';
+  
+  let dateObj: Date;
+  
+  if (date instanceof Date) {
+    dateObj = date;
+  } else if (typeof date === 'string') {
+    dateObj = new Date(date);
+  } else if (typeof date === 'number') {
+    dateObj = new Date(date);
+  } else {
+    return '暂无数据';
+  }
+  
+  if (isNaN(dateObj.getTime())) {
+    return '无效日期';
+  }
+  
+  return this.formatDate(dateObj);
+}
+
+private formatDate(date: Date): string {
+  const now = new Date();
+  const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
+  
+  if (diffHours < 24) {
+    return `今天 ${this.formatTwoDigits(date.getHours())}:${this.formatTwoDigits(date.getMinutes())}`;
+  } else if (diffHours < 48) {
+    return `昨天 ${this.formatTwoDigits(date.getHours())}:${this.formatTwoDigits(date.getMinutes())}`;
+  } else {
+    return `${date.getMonth() + 1}月${date.getDate()}日 ${this.formatTwoDigits(date.getHours())}:${this.formatTwoDigits(date.getMinutes())}`;
+  }
+}
+
+// 更新数据查询方法
 private async queryData(className: string, user: CloudUser, date: Date): Promise<any[]> {
 private async queryData(className: string, user: CloudUser, date: Date): Promise<any[]> {
   const query = new CloudQuery(className);
   const query = new CloudQuery(className);
   query.equalTo('patient', {
   query.equalTo('patient', {
@@ -73,9 +397,24 @@ private async queryData(className: string, user: CloudUser, date: Date): Promise
   });
   });
   query.greaterThan('measurementTime', date);
   query.greaterThan('measurementTime', date);
   query.ascending('measurementTime');
   query.ascending('measurementTime');
-  return await query.find();
+  
+  const results = await query.find();
+  
+  // 转换日期格式
+  return results.map(item => {
+    const time = item.get('measurementTime');
+    if (time && !(time instanceof Date)) {
+      item.data['measurementTime'] = new Date(time);
+    }
+    return item;
+  });
 }
 }
 
 
+  // 辅助函数:格式化两位数
+  formatTwoDigits(num: number): string {
+    return num < 10 ? `0${num}` : num.toString();
+  }
+
 async importData() {
 async importData() {
   try {
   try {
     this.healthData = {
     this.healthData = {
@@ -100,144 +439,9 @@ async importData() {
     }, 1000);
     }, 1000);
   }
   }
   
   
-openConsult(chatId?:string){
-    localStorage.setItem("company","E4KpGvTEto")
-    let options:ChatPanelOptions = {
-      roleId:"2DXJkRsjXK",
-      //chatId:chatId,//若存在则恢复对话,不存在则开启新对话1
-      onChatInit:(chat:FmodeChat)=>{
-        console.log("onChatInit");
-        console.log("预设角色",chat.role);
-        // 角色名称
-        chat.role.set("name","丁德文");
-        // 角色称号
-        chat.role.set("title","慢性病医生");
-        // 角色描述
-        chat.role.set("desc","一名亲切和蔼的慢性病医生,丁德文,年龄46岁");
-        // 角色标签
-        chat.role.set("tags",["内科","初诊"]);
-        // 角色头像
-        chat.role.set("avatar","/assets/images/0.jpg")
-        // 角色提示词
-        chat.role.set("prompt",`
-# 角色设定
-您是一名亲切和蔼的专业的慢性病医生,丁德文,年龄46岁,需要完成一次完整且专业的慢性病问诊服务。
-
-# 对话环节
-0.导诊(根据用户基本情况,匹配对应的慢性病病症)
-1.预设的问询方式
-- 打招呼,以用户自述为主
-- 当信息充足时候,确认用户症状慢性病病症,并进入下一个环节
-2.拓展的问询细节
-例如:用户反映呼吸不畅,拓展出:是否咳嗽;是否感觉痛或者痒等其他需要的问题。
-- 当问询细节补充完成后进入下一个环节
-3.初步的诊断结果,并且同时列出检查检验项目
-初步诊断:确定需要有哪些进一步检查
-检查检验:获取医学客观数据
-- 等待用户提交客观数据,进入下一阶段
-4.给出诊断方案并给出处方
-- 完成处方时,请在消息结尾附带: [完成]
-
-# 开始话语
-当您准备好了,可以以一个医生的身份,向来访的用户打招呼。`);
-        // 对话灵感分类
-        let promptCates = [
-          {
-            "img": "/assets/icon/tnb.jpeg",
-            "name": "糖尿病"
-          },
-          {
-            "img": "/assets/icon/gxy.jpeg",
-            "name": "高血压"
-          },
-          {
-            "img": "/assets/icon/gxb.jpeg",
-            "name": "冠心病"
-          }
-        ]
-        setTimeout(() => {
-          chat.role.set("promptCates",promptCates)
-        }, 500);
-        // 对话灵感列表
-        let promptList = [
-          {
-            cate:"糖尿病",img:"/assets/icon/tnb.jpeg",
-            messageList:["异常口渴(多饮","尿频且尿量增多","体重短期内明显下降","饥饿感强烈(多食)","伤口愈合缓慢","皮肤干燥瘙痒","手脚麻木或刺痛感(周围神经病变)"]
-          },
-          {
-            cate:"高血压",img:"/assets/icon/gxy.jpeg",
-            messageList:["持续性头痛(尤其后脑勺部位)","经常性头晕或眩晕","视物模糊(短暂性)","耳鸣(如嗡嗡声)","心悸或心律不齐","疲劳感明显(即使休息后)","夜间多尿或起夜频繁"]
-          },
-          {
-            cate:"冠心病",img:"/assets/icon/gxb.jpeg",
-            messageList:["胸痛或压迫感(心绞痛,可放射至左肩/下颌)","活动后气促(如爬楼梯时)","心悸或心跳漏搏感","突发冷汗伴恶心","不明原因疲劳(尤其女性患者)","下肢水肿(晚期症状)","夜间平卧时呼吸困难(需高枕卧位)"]
-          },
-        ]
-        let ChatPrompt = Parse.Object.extend("ChatPrompt");
-        setTimeout(() => {
-          chat.promptList = promptList.map(item=>{
-            let prompt = new ChatPrompt();
-            prompt.set(item);
-            prompt.img = item.img;
-            return prompt;
-          })
-        }, 500);
-
-        // 功能按钮区域预设
-        chat.leftButtons = [
-          { // 提示 当角色配置预设提示词时 显示
-           title:"话题灵感", // 按钮标题
-           showTitle:true, // 是否显示标题文字
-           icon:"color-wand-outline", // 标题icon图标
-           onClick:()=>{ // 按钮点击事件
-               chat.isPromptModalOpen = true
-           },
-           show:()=>{ // 按钮显示条件
-             return chat?.promptList?.length // 存在话题提示词时显示
-           }
-         },
-      ]
-
-      },
-      onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
-        console.log("onMessage",message)
-        let content:any = message?.content
-        if(typeof content == "string"){
-          if(content?.indexOf("[完成]")>-1){
-            console.log("已完成")
-          }
-        }
-      },
-      onChatSaved:(chat:FmodeChat)=>{
-        // chat?.chatSession?.id 本次会话的 chatId
-        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
-      }
-    }
-    openChatPanelModal(this.modalCtrl,options)
-}
-  
   timeRange = 'week';
   timeRange = 'week';
   
   
-  metrics = [
-    { 
-      name: '血糖', 
-      value: '6.5', 
-      unit: 'mmol/L',
-      trend: 'caret-up', 
-      trendColor: 'success',
-      reference: '3.9-6.1',
-      abnormal: false
-    },
-    {
-      name: '血压',
-      value: '135/85',
-      unit: 'mmHg',
-      trend: 'caret-down',
-      trendColor: 'danger',
-      reference: '120/80',
-      abnormal: true
-    }
-  ];
+
 
 
   tasks = [
   tasks = [
     {
     {

+ 39 - 82
src/app/tab2/tab2.page.html

@@ -21,87 +21,44 @@
     </ion-buttons>
     </ion-buttons>
   </ion-toolbar>
   </ion-toolbar>
 </ion-header>
 </ion-header>
-
-<ion-content [fullscreen]="true" class="ion-padding">
-  <!-- 健康资讯区 -->
-  <ion-list-header class="section-title">
-    <ion-label>健康资讯</ion-label>
-  </ion-list-header>
   
   
-  <ion-card *ngFor="let item of healthNews" class="news-card">
-    <div class="news-tag" [style.background]="item.tagColor">{{item.tag}}</div>
-    <ion-img [src]="item.image" class="news-image"></ion-img>
-    <ion-card-header>
-      <ion-card-title class="news-title">{{item.title}}</ion-card-title>
-    </ion-card-header>
-    <ion-card-content>
-      <p class="news-summary">{{item.summary}}</p>
-      <div class="news-footer">
-        <span class="news-source">{{item.source}}</span>
-        <span class="news-time">{{item.time}}</span>
-      </div>
-    </ion-card-content>
-    <ion-buttons slot="end" class="action-buttons">
-      <ion-button (click)="toggleLike(item)">
-        <ion-icon [name]="item.liked ? 'heart' : 'heart-outline'" 
-                 [color]="item.liked ? 'primary' : 'medium'"></ion-icon>
-      </ion-button>
-      <ion-button>
-        <ion-icon name="chatbubble-outline" color="medium"></ion-icon>
-      </ion-button>
-      <ion-button>
-        <ion-icon name="share-social-outline" color="medium"></ion-icon>
-      </ion-button>
-    </ion-buttons>
-  </ion-card>
-
-  <!-- 互助社区区 -->
-  <ion-list-header class="section-title">
-    <ion-label>互助社区</ion-label>
-  </ion-list-header>
-
-  <ion-card *ngFor="let topic of communityTopics" class="topic-card">
-    <div class="topic-tag">{{topic.tag}}</div>
-    <ion-card-header>
-      <ion-avatar slot="start" class="user-avatar">
-        <img [src]="topic.user.avatar">
-      </ion-avatar>
-      <ion-card-subtitle>{{topic.user.name}} · {{topic.user.condition}}</ion-card-subtitle>
-      <ion-card-title class="topic-title">{{topic.title}}</ion-card-title>
-    </ion-card-header>
-    <ion-card-content>
-      <p class="topic-preview">{{topic.content}}</p>
-      <div class="topic-stats">
-        <ion-badge color="light">
-          <ion-icon name="people-outline"></ion-icon>
-          {{topic.participants}} 人参与
-        </ion-badge>
-        <ion-badge color="light">
-          <ion-icon name="chatbubbles-outline"></ion-icon>
-          {{topic.answers}} 个回答
-        </ion-badge>
+<!--对话智能体-->
+  <ion-card class="doctor-card">
+  <div class="card-content">
+    <div class="doctor-avatar">
+      <img src="/assets/images/doc.jpeg" alt="慢性病医师头像" class="avatar-image">
+    </div>
+    
+    <div class="doctor-info">
+      <h2 class="doctor-name">丁医生</h2>
+      <p class="doctor-title">慢性病管理主任医师</p>
+      <div class="doctor-specialty">
+        <ion-badge color="primary">糖尿病</ion-badge>
+        <ion-badge color="primary">高血压</ion-badge>
+        <ion-badge color="primary">冠心病</ion-badge>
       </div>
       </div>
-    </ion-card-content>
-  </ion-card>
-
-  <!-- 健康工具区 -->
-  <ion-list-header class="section-title">
-    <ion-label>健康工具</ion-label>
-  </ion-list-header>
-
-  <ion-grid class="tools-grid">
-    <ion-row>
-      <ion-col size="6" *ngFor="let tool of healthTools">
-        <ion-card class="tool-card">
-          <ion-icon [name]="tool.icon" class="tool-icon" color="primary"></ion-icon>
-          <ion-card-header>
-            <ion-card-title class="tool-title">{{tool.name}}</ion-card-title>
-          </ion-card-header>
-          <ion-card-content>
-            <p class="tool-description">{{tool.description}}</p>
-          </ion-card-content>
-        </ion-card>
-      </ion-col>
-    </ion-row>
-  </ion-grid>
-</ion-content>
+      <p class="doctor-desc">10年慢性病管理经验。擅长个性化治疗方案制定和长期健康管理。</p>
+    </div>
+  </div>
+  
+  <ion-button (click)="openConsult()" expand="block" color="success" class="consult-btn">
+    <ion-icon name="medical" slot="start"></ion-icon>
+    立即咨询
+  </ion-button>
+  <ion-card-content>
+  @if(consultList?.length){
+    <ul>
+      @for(consult of consultList;track consult){
+        <li (click)="openConsult(consult.get('chatId'))">
+          @if(consult.get("avatar")){
+            <img [src]="consult.get('avatar')"alt="" srcset="">
+          }
+          <span>{{consult.get("name")||'无名氏'}}</span>
+          {{consult.updatedAt}}
+        </li>
+      }
+    </ul>
+  }
+  </ion-card-content>
+<ion-button (click)="loadConsult()">历史咨询记录</ion-button>
+</ion-card>

+ 271 - 105
src/app/tab2/tab2.page.scss

@@ -1,133 +1,299 @@
-// 自定义搜索栏样式
-.custom-searchbar {
-    padding: 0 10px !important;
-    transform: scale(0.9);
-  }
-  
-  .section-title {
-    font-size: 1.2rem;
-    font-weight: bold;
-    color: var(--ion-color-primary);
-    padding-left: 0;
+.user-card {
+    background: linear-gradient(135deg, #4a90e2 0%, #63b8ff 100%);
+    border-radius: 16px;
+    color: white;
+    
+    .user-avatar {
+      width: 48px;
+      height: 48px;
+      margin-bottom: 8px;
+    }
   }
   }
   
   
-  // 资讯卡片样式
-  .news-card {
+  .data-card {
     position: relative;
     position: relative;
-    width: 95%;
-    margin: 10px auto;
-    border-radius: 10px;
-    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
-  
-    .news-tag {
-      position: absolute;
-      top: 10px;
-      left: 10px;
-      padding: 4px 12px;
-      border-radius: 15px;
-      color: white;
-      font-size: 12px;
-      z-index: 2;
+    border-radius: 12px;
+    
+    .metric-item {
+      padding: 12px;
+      
+      &.abnormal {
+        background: #fff3f3;
+        border-radius: 8px;
+      }
     }
     }
-  
-    .news-image {
-      height: 150px;
-      object-fit: cover;
+    
+    .metric-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
     }
     }
-  
-    .news-title {
-      font-size: 1.1rem;
+    
+    .metric-value {
+      font-size: 24px;
       font-weight: bold;
       font-weight: bold;
-      margin-top: 8px;
+      
+      .unit {
+        font-size: 14px;
+        color: var(--ion-color-medium);
+      }
     }
     }
-  
-    .news-summary {
-      color: #666;
-      font-size: 0.9rem;
-      line-height: 1.4;
+    
+    .reference {
+      font-size: 12px;
     }
     }
+  }
   
   
-    .news-footer {
-      margin-top: 10px;
-      font-size: 0.8rem;
-      color: #999;
-      display: flex;
-      justify-content: space-between;
+  .task-card {
+    ion-item {
+      --padding-start: 0;
+      --inner-padding-end: 0;
+    }
+    
+    ion-progress-bar {
+      flex: 1;
+      margin-left: 16px;
+    }
+    
+    .recommended {
+      background: #f5f9ff;
+      border-radius: 8px;
     }
     }
   }
   }
   
   
-  // 工具卡片样式
-  .tools-grid {
-    padding: 0;
+  .chart-container {
+    height: 200px;
+    padding: 16px;
+  }
+
+  .doctor-card {
+  border-radius: 15px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  margin: 16px;
+  max-width: 400px;
+}
+
+.card-content {
+  display: flex;
+  padding: 20px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
+}
+
+.doctor-avatar {
+  margin-right: 16px;
+}
+
+.avatar-image {
+  width: 80px;
+  height: 80px;
+  border-radius: 50%;
+  object-fit: cover;
+  border: 3px solid #fff;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.doctor-info {
+  flex: 1;
+}
+
+.doctor-name {
+  margin: 0 0 4px 0;
+  font-size: 1.2rem;
+  font-weight: bold;
+  color: #2c3e50;
+}
+
+.doctor-title {
+  margin: 0 0 8px 0;
+  font-size: 0.9rem;
+  color: #7f8c8d;
+}
+
+.doctor-specialty {
+  margin-bottom: 8px;
+}
+
+.doctor-desc {
+  margin: 8px 0 0 0;
+  font-size: 0.85rem;
+  color: #34495e;
+  line-height: 1.4;
+}
+
+.consult-btn {
+  --border-radius: 0 0 15px 15px;
+  font-weight: bold;
+  --padding-top: 12px;
+  --padding-bottom: 12px;
+}
+// 健康数据卡片样式
+.health-data-card {
+  margin-top: 16px;
   
   
-    .tool-card {
-      height: 120px;
-      margin: 5px;
-      text-align: center;
+  .data-section {
+    margin: 8px 0;
+    box-shadow: none;
+    
+    ion-card-header {
+      padding-bottom: 0;
       
       
-      .tool-icon {
-        font-size: 2.5rem;
-        margin-top: 10px;
+      ion-card-title {
+        font-size: 1.1rem;
+        display: flex;
+        align-items: center;
+        
+        ion-icon {
+          margin-right: 8px;
+        }
       }
       }
+    }
+  }
   
   
-      .tool-title {
-        font-size: 1rem;
-        margin: 5px 0;
-      }
+  .loading-spinner {
+    display: flex;
+    justify-content: center;
+    padding: 16px;
+  }
+  
+  ion-badge {
+    font-weight: normal;
+  }
+}
+
+/* 医生卡片样式 */
+.doctor-card {
+  border-radius: 16px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+  margin-bottom: 24px;
   
   
-      .tool-description {
-        font-size: 0.8rem;
-        color: #666;
-        margin: 0;
+  .card-content {
+    display: flex;
+    padding: 16px;
+    
+    .doctor-avatar {
+      position: relative;
+      margin-right: 16px;
+      
+      .avatar-image {
+        width: 80px;
+        height: 80px;
+        border-radius: 50%;
+        object-fit: cover;
+        border: 3px solid var(--ion-color-primary);
+      }
+      
+      .verified-badge {
+        position: absolute;
+        bottom: 0;
+        right: 0;
+        background: white;
+        border-radius: 50%;
+        padding: 4px;
+        font-size: 16px;
+      }
+    }
+    
+    .doctor-info {
+      flex: 1;
+      
+      .doctor-name {
+        margin: 0 0 4px 0;
+        font-weight: bold;
+        color: var(--ion-color-primary);
+      }
+      
+      .doctor-title {
+        margin: 0 0 8px 0;
+        font-size: 14px;
+        color: var(--ion-color-medium);
+      }
+      
+      .doctor-specialty {
+        margin-bottom: 8px;
+        
+        ion-chip {
+          margin: 0 4px 4px 0;
+        }
+      }
+      
+      .doctor-desc {
+        margin: 8px 0;
+        font-size: 14px;
+        color: var(--ion-color-dark);
+      }
+      
+      .doctor-stats {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+        
+        ion-badge {
+          padding: 6px 8px;
+          font-weight: normal;
+          
+          ion-icon {
+            margin-right: 4px;
+          }
+        }
       }
       }
     }
     }
   }
   }
   
   
-  // 互助社区样式
-  .topic-card {
-    .topic-tag {
-      background: var(--ion-color-primary);
-      color: white;
-      padding: 4px 12px;
-      border-radius: 0 0 10px 10px;
-      position: absolute;
-      top: 0;
-      left: 20px;
-      font-size: 12px;
+  .consult-btn {
+    margin: 0 16px 16px;
+    --border-radius: 12px;
+    font-weight: bold;
+  }
+}
+
+/* 历史记录列表样式 */
+ion-list {
+  ion-item {
+    --padding-start: 0;
+    --inner-padding-end: 0;
+    --border-radius: 12px;
+    margin-bottom: 8px;
+    
+    ion-avatar {
+      width: 48px;
+      height: 48px;
     }
     }
-  
-    .user-avatar {
-      width: 40px;
-      height: 40px;
+    
+    ion-label {
+      h2 {
+        font-weight: 600;
+      }
+      
+      p {
+        color: var(--ion-color-medium);
+        font-size: 14px;
+      }
     }
     }
+  }
+}
+
+/* 空状态提示样式 */
+.empty-state {
+  text-align: center;
   
   
-    .topic-title {
-      font-size: 1.1rem;
+  .empty-content {
+    padding: 24px 0;
+    
+    ion-icon {
+      font-size: 48px;
+      margin-bottom: 16px;
     }
     }
-  
-    .topic-preview {
-      color: #666;
-      font-size: 0.9rem;
-      line-height: 1.4;
-      margin: 10px 0;
+    
+    h3 {
+      color: var(--ion-color-medium);
+      margin: 8px 0;
     }
     }
-  
-    .topic-stats {
-      ion-badge {
-        margin-right: 10px;
-        padding: 8px;
-        font-weight: normal;
-        
-        ion-icon {
-          vertical-align: middle;
-          margin-right: 4px;
-        }
-      }
+    
+    p {
+      color: var(--ion-color-medium);
+      margin-bottom: 16px;
     }
     }
   }
   }
-  
-  .action-buttons {
-    position: absolute;
-    bottom: 10px;
-    right: 10px;
-  }
+}

+ 149 - 50
src/app/tab2/tab2.page.ts

@@ -1,60 +1,159 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import{ChatPanelOptions,FmChatModalInput,FmodeChat,FmodeChatMessage,openChatPanelModal}from 'fmode-ng';
+import Parse from "parse";
+import{ModalController}from '@ionic/angular/standalone';
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
+import { train } from 'ionicons/icons';
 
 
 @Component({
 @Component({
   selector: 'app-tab2',
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
   templateUrl: 'tab2.page.html',
   styleUrls: ['tab2.page.scss'],
   styleUrls: ['tab2.page.scss'],
+
   standalone: false,
   standalone: false,
 })
 })
 export class Tab2Page {
 export class Tab2Page {
-
-  healthNews = [
-    {
-      tag: '饮食',
-      tagColor: '#4CAF50',
-      title: '糖尿病患者的饮食指南',
-      summary: '科学规划每日膳食结构,掌握健康饮食诀窍...',
-      image: 'assets/food.jpg',
-      source: '健康时报',
-      time: '2小时前',
-      liked: false
-    },
-    // 更多示例数据...
-  ];
-
-  communityTopics = [
-    {
-      tag: '血糖管理',
-      title: '如何稳定控制空腹血糖?',
-      user: {
-        avatar: 'assets/user1.jpg',
-        name: '张先生',
-        condition: 'II型糖尿病'
-      },
-      content: '最近空腹血糖总是偏高,大家有什么好的建议吗?',
-      participants: 45,
-      answers: 28
-    },
-    // 更多示例数据...
-  ];
-
-  healthTools = [
-    {
-      icon: 'calculator-outline',
-      name: '血糖计算器',
-      description: '智能计算每日血糖变化趋势'
-    },
-    {
-      icon: 'restaurant-outline',
-      name: '饮食记录',
-      description: '记录分析每日膳食营养'
-    },
-    // 更多示例数据...
-  ];
-
-  constructor() {}
-
-  toggleLike(item: any) {
-    item.liked = !item.liked;
+ constructor(
+    private modalCtrl:ModalController
+  ){}
+  openConsult(chatId?:string){
+      localStorage.setItem("company","E4KpGvTEto")
+      let options:ChatPanelOptions = {
+        roleId:"2DXJkRsjXK",
+        //chatId:chatId,//若存在则恢复对话,不存在则开启新对话1
+        onChatInit:(chat:FmodeChat)=>{
+          console.log("onChatInit");
+          console.log("预设角色",chat.role);
+          // 角色名称
+          chat.role.set("name","丁德文");
+          // 角色称号
+          chat.role.set("title","慢性病医生");
+          // 角色描述
+          chat.role.set("desc","一名亲切和蔼的慢性病医生,丁德文,年龄46岁");
+          // 角色标签
+          chat.role.set("tags",["内科","初诊"]);
+          // 角色头像
+          chat.role.set("avatar","/assets/images/0.jpg")
+          // 角色提示词
+          chat.role.set("prompt",`
+  # 角色设定
+  您是一名亲切和蔼的专业的慢性病医生,丁德文,年龄46岁,需要完成一次完整且专业的慢性病问诊服务。
+  
+  # 对话环节
+  0.导诊(根据用户基本情况,匹配对应的慢性病病症)
+  1.预设的问询方式
+  - 打招呼,以用户自述为主
+  - 当信息充足时候,确认用户症状慢性病病症,并进入下一个环节
+  2.拓展的问询细节
+  例如:用户反映呼吸不畅,拓展出:是否咳嗽;是否感觉痛或者痒等其他需要的问题。
+  - 当问询细节补充完成后进入下一个环节
+  3.初步的诊断结果,并且同时列出检查检验项目
+  初步诊断:确定需要有哪些进一步检查
+  检查检验:获取医学客观数据
+  - 等待用户提交客观数据,进入下一阶段
+  4.给出诊断方案并给出处方
+  - 完成处方时,请在消息结尾附带: [完成]
+  
+  # 开始话语
+  当您准备好了,可以以一个医生的身份,向来访的用户打招呼。`);
+          // 对话灵感分类
+          let promptCates = [
+            {
+              "img": "/assets/icon/tnb.jpeg",
+              "name": "糖尿病"
+            },
+            {
+              "img": "/assets/icon/gxy.jpeg",
+              "name": "高血压"
+            },
+            {
+              "img": "/assets/icon/gxb.jpeg",
+              "name": "冠心病"
+            }
+          ]
+          setTimeout(() => {
+            chat.role.set("promptCates",promptCates)
+          }, 500);
+          // 对话灵感列表
+          let promptList = [
+            {
+              cate:"糖尿病",img:"/assets/icon/tnb.jpeg",
+              messageList:["异常口渴(多饮","尿频且尿量增多","体重短期内明显下降","饥饿感强烈(多食)","伤口愈合缓慢","皮肤干燥瘙痒","手脚麻木或刺痛感(周围神经病变)"]
+            },
+            {
+              cate:"高血压",img:"/assets/icon/gxy.jpeg",
+              messageList:["持续性头痛(尤其后脑勺部位)","经常性头晕或眩晕","视物模糊(短暂性)","耳鸣(如嗡嗡声)","心悸或心律不齐","疲劳感明显(即使休息后)","夜间多尿或起夜频繁"]
+            },
+            {
+              cate:"冠心病",img:"/assets/icon/gxb.jpeg",
+              messageList:["胸痛或压迫感(心绞痛,可放射至左肩/下颌)","活动后气促(如爬楼梯时)","心悸或心跳漏搏感","突发冷汗伴恶心","不明原因疲劳(尤其女性患者)","下肢水肿(晚期症状)","夜间平卧时呼吸困难(需高枕卧位)"]
+            },
+          ]
+          let ChatPrompt = Parse.Object.extend("ChatPrompt");
+          setTimeout(() => {
+            chat.promptList = promptList.map(item=>{
+              let prompt = new ChatPrompt();
+              prompt.set(item);
+              prompt.img = item.img;
+              return prompt;
+            })
+          }, 500);
+  
+          // 功能按钮区域预设
+          chat.leftButtons = [
+            { // 提示 当角色配置预设提示词时 显示
+             title:"话题灵感", // 按钮标题
+             showTitle:true, // 是否显示标题文字
+             icon:"color-wand-outline", // 标题icon图标
+             onClick:()=>{ // 按钮点击事件
+                 chat.isPromptModalOpen = true
+             },
+             show:()=>{ // 按钮显示条件
+               return chat?.promptList?.length // 存在话题提示词时显示
+             }
+           },
+        ]
+  
+        },
+        onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
+          console.log("onMessage",message)
+          let content:any = message?.content
+          if(typeof content == "string"){
+            if(content?.indexOf("[完成]")>-1){
+              console.log("已完成")
+            }
+          }
+        },
+        onChatSaved:async(chat:FmodeChat)=>{
+          // chat?.chatSession?.id 本次会话的 chatId
+          console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+          let chatId = chat?.chatSession?.id;
+          let query =new CloudQuery("AskConsult");
+          let AskConsult =await query.get(chatId);
+          if(!AskConsult?.id){
+            AskConsult= new CloudObject("AskConsult")
+          }
+          AskConsult.set({
+            "chatId":chatId,
+            "messageList":chat.messageList,
+            "name":chat.role.get("name")
+          })
+          console.log("AskConsult",AskConsult)
+          AskConsult.save();
+      }
+    }
+      openChatPanelModal(this.modalCtrl,options)
   }
   }
+  //加载记录
+  consultList:Array<CloudObject>=[]
+  async loadConsult(){
+  let query =new CloudQuery("AskConsult");
+  this.consultList=await query.find()
+
+}
+
 }
 }
+
+
+
+

+ 26 - 29
src/app/tab3/tab3.page.html

@@ -8,43 +8,40 @@
 
 
 <ion-content [fullscreen]="true">
 <ion-content [fullscreen]="true">
   <!-- 个人信息区 -->
   <!-- 个人信息区 -->
-  <div class="profile-section">
+  <div class="profile-section" [class.logged-in]="isLoggedIn" [class.logged-out]="!isLoggedIn">
     <ion-avatar class="profile-avatar">
     <ion-avatar class="profile-avatar">
-      <img src="https://ionicframework.com/docs/img/demos/avatar.svg" />
+      <img [src]="isLoggedIn ? 'https://ionicframework.com/docs/img/demos/avatar.svg' : 'assets/icon/guest-avatar.png'" />
+      <div class="status-dot" [class.online]="isLoggedIn"></div>
     </ion-avatar>
     </ion-avatar>
+    
     <div class="profile-info">
     <div class="profile-info">
-      <h2 class="nickname">健康达人张先生</h2>
+      <h2 class="nickname">
+        {{ isLoggedIn ? (currentUser?.data?.['nickname'] || currentUser?.data?.['username'] || '健康达人') : '请登录' }}
+      </h2>
+      
       <div class="tag-container">
       <div class="tag-container">
-        <ion-badge color="light" class="disease-tag">糖尿病</ion-badge>
-        <ion-icon name="create-outline" class="edit-icon"></ion-icon>
+        <ion-badge *ngIf="isLoggedIn && currentUser?.data?.['disease']" color="tertiary" class="disease-tag">
+          {{ currentUser?.data?.['disease'] }}
+        </ion-badge>
+        <ion-badge *ngIf="!isLoggedIn" color="medium" class="guest-tag">
+          访客模式
+        </ion-badge>
+        <ion-icon *ngIf="isLoggedIn" name="create-outline" class="edit-icon"></ion-icon>
       </div>
       </div>
     </div>
     </div>
+    
+    <!-- 登录/登出按钮 -->
+    <ion-button *ngIf="!isLoggedIn" shape="round" fill="solid" color="primary" (click)="showLoginAlert()">
+      <ion-icon slot="start" name="log-in-outline"></ion-icon>
+      立即登录
+    </ion-button>
+    
+    <ion-button *ngIf="isLoggedIn" shape="round" fill="outline" color="danger" (click)="logout()">
+      <ion-icon slot="start" name="log-out-outline"></ion-icon>
+      退出登录
+    </ion-button>
   </div>
   </div>
 
 
-  <!-- 常用功能区 -->
-  <ion-list lines="none" class="function-list">
-    <ion-item button detail="true">
-      <ion-icon slot="start" name="fitness-outline" color="primary"></ion-icon>
-      <ion-label>健康档案</ion-label>
-    </ion-item>
-    <ion-item button detail="true">
-      <ion-icon slot="start" name="pulse-outline" color="primary"></ion-icon>
-      <ion-label>监测记录</ion-label>
-    </ion-item>
-    <ion-item button detail="true">
-      <ion-icon slot="start" name="alarm-outline" color="primary"></ion-icon>
-      <ion-label>服药提醒</ion-label>
-    </ion-item>
-    <ion-item button detail="true">
-      <ion-icon slot="start" name="restaurant-outline" color="primary"></ion-icon>
-      <ion-label>饮食管理</ion-label>
-    </ion-item>
-    <ion-item button detail="true">
-      <ion-icon slot="start" name="bicycle-outline" color="primary"></ion-icon>
-      <ion-label>运动计划</ion-label>
-    </ion-item>
-  </ion-list>
-
   <!-- 账户管理区 -->
   <!-- 账户管理区 -->
   <ion-list lines="none">
   <ion-list lines="none">
     <ion-item button detail="true">
     <ion-item button detail="true">

+ 86 - 67
src/app/tab3/tab3.page.scss

@@ -1,79 +1,98 @@
 .profile-section {
 .profile-section {
-    background: var(--ion-color-primary);
-    padding: 20px;
-    display: flex;
-    align-items: center;
-    color: white;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  margin: 15px;
+  border-radius: 12px;
+  background: var(--ion-color-light);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
   
   
-    .profile-avatar {
-      width: 80px;
-      height: 80px;
-      margin-right: 16px;
-    }
+  &.logged-out {
+    background: var(--ion-color-light-tint);
+    border: 1px dashed var(--ion-color-medium);
+  }
   
   
-    .profile-info {
-      flex: 1;
+  &.logged-in {
+    background: var(--ion-color-primary-contrast);
+    border: 1px solid var(--ion-color-primary-tint);
+  }
+
+  .profile-avatar {
+    width: 80px;
+    height: 80px;
+    margin-right: 20px;
+    position: relative;
+    
+    img {
+      border: 3px solid var(--ion-color-light-shade);
+    }
+    
+    .status-dot {
+      position: absolute;
+      bottom: 5px;
+      right: 5px;
+      width: 15px;
+      height: 15px;
+      border-radius: 50%;
+      background: var(--ion-color-medium);
+      border: 2px solid var(--ion-color-light);
       
       
-      .nickname {
-        margin: 0 0 8px 0;
-        font-size: 18px;
-        font-weight: bold;
-      }
-  
-      .tag-container {
-        position: relative;
-        display: inline-block;
-  
-        .disease-tag {
-          color: var(--ion-color-primary);
-          font-size: 12px;
-          padding: 4px 8px;
-          border-radius: 12px;
-        }
-  
-        .edit-icon {
-          position: absolute;
-          right: -8px;
-          top: -8px;
-          background: white;
-          border-radius: 50%;
-          padding: 2px;
-          font-size: 16px;
-          color: var(--ion-color-primary);
-        }
+      &.online {
+        background: var(--ion-color-success);
       }
       }
     }
     }
   }
   }
-  
-  .function-list {
-    ion-item {
-      --padding-start: 16px;
-      --inner-padding-end: 16px;
-      --min-height: 56px;
-  
-      ion-icon {
-        font-size: 24px;
+
+  .profile-info {
+    flex: 1;
+    
+    .nickname {
+      margin: 0 0 5px 0;
+      font-size: 1.3rem;
+      font-weight: 600;
+      color: var(--ion-color-dark);
+    }
+    
+    .tag-container {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      
+      .disease-tag {
+        font-weight: 500;
+      }
+      
+      .guest-tag {
+        opacity: 0.8;
+      }
+      
+      .edit-icon {
+        color: var(--ion-color-primary);
+        font-size: 1.1rem;
       }
       }
     }
     }
   }
   }
   
   
-  ion-list {
-    margin: 16px 0;
-    background: white;
-  
-    ion-item {
-      --padding-start: 16px;
-      --inner-padding-end: 16px;
-      --min-height: 48px;
-  
-      ion-icon {
-        font-size: 20px;
-        margin-right: 12px;
-      }
-  
-      ion-label {
-        font-size: 16px;
-        font-weight: 500;
-      }
+  ion-button {
+    --border-radius: 20px;
+    --padding-start: 16px;
+    --padding-end: 16px;
+    height: 40px;
+    font-weight: 500;
+    
+    ion-icon {
+      font-size: 1.1rem;
     }
     }
-  }
+  }
+}
+
+/* 为未登录状态下的功能列表添加半透明效果 */
+ion-list:not(:first-of-type) {
+  opacity: 0.9;
+  transition: opacity 0.3s ease;
+  
+  ion-item {
+    --background: var(--ion-color-light);
+  }
+}

+ 90 - 1
src/app/tab3/tab3.page.ts

@@ -1,4 +1,6 @@
 import { Component } from '@angular/core';
 import { Component } from '@angular/core';
+import { AlertController } from '@ionic/angular';
+import { CloudUser } from 'src/lib/ncloud';
 
 
 @Component({
 @Component({
   selector: 'app-tab3',
   selector: 'app-tab3',
@@ -7,7 +9,94 @@ import { Component } from '@angular/core';
   standalone: false,
   standalone: false,
 })
 })
 export class Tab3Page {
 export class Tab3Page {
+  currentUser: CloudUser | null = null;
+  isLoggedIn: boolean = false;
 
 
-  constructor() {}
+  constructor(private alertCtrl: AlertController) {
+    this.checkLoginStatus();
+  }
 
 
+  async checkLoginStatus() {
+    this.currentUser = new CloudUser();
+    if (this.currentUser.sessionToken) {
+      try {
+        await this.currentUser.current();
+        this.isLoggedIn = true;
+      } catch (error) {
+        console.error('检查登录状态失败:', error);
+        this.isLoggedIn = false;
+      }
+    } else {
+      this.isLoggedIn = false;
+    }
+  }
+
+async showLoginAlert() {
+  const alert = await this.alertCtrl.create({
+    header: '用户登录',
+    inputs: [
+      {
+        name: 'username',
+        type: 'text',
+        placeholder: '用户名'
+      },
+      {
+        name: 'password',
+        type: 'password',
+        placeholder: '密码'
+      }
+    ],
+    buttons: [
+      {
+        text: '取消',
+        role: 'cancel'
+      },
+      {
+        text: '登录',
+        handler: async (data) => {
+          if (!data.username || !data.password) {
+            this.showErrorAlert('用户名和密码不能为空');
+            return false; // 阻止关闭
+          }
+          
+          try {
+            const user = new CloudUser();
+            const result = await user.login(data.username, data.password);
+            
+            if (result) {
+              this.checkLoginStatus();
+              return true; // 允许关闭
+            } else {
+              this.showErrorAlert('登录失败,请检查用户名和密码');
+              return false; // 阻止关闭
+            }
+          } catch (error) {
+            console.error('登录错误:', error);
+            this.showErrorAlert('登录过程中发生错误');
+            return false; // 阻止关闭
+          }
+        }
+      }
+    ]
+  });
+
+  await alert.present();
 }
 }
+
+  async showErrorAlert(message: string) {
+    const alert = await this.alertCtrl.create({
+      header: '错误',
+      message: message,
+      buttons: ['确定']
+    });
+    await alert.present();
+  }
+
+  async logout() {
+    if (this.currentUser) {
+      await this.currentUser.logout();
+      this.isLoggedIn = false;
+      this.currentUser = null;
+    }
+  }
+}

BIN
src/assets/images/default-avatar.jpg


BIN
src/assets/images/doc.jpeg


+ 119 - 0
src/lib/consult-tasks/consult-tasks.ts

@@ -0,0 +1,119 @@
+
+import { FmodeChatCompletion } from 'fmode-ng';
+import { FlowTask } from '../../lib/flow.task';
+
+
+// triage.task.ts
+import { ModalController } from '@ionic/angular/standalone';
+import { Subject, takeUntil } from 'rxjs';
+import { SymptomInputModalComponent } from './symptom-input/symptom-input.modal';
+import { inject } from '@angular/core';
+
+export class TriageTask extends FlowTask {
+  private modalCtrl: ModalController;
+  private destroy$ = new Subject<void>();
+
+  constructor(options: { modalCtrl: ModalController }) {
+    super({
+      title: '智能分诊',
+      output: [
+        { name: 'department', type: 'string', required: true },
+        { name: 'symptomDescription', type: 'string' },
+        { name: 'triageReason', type: 'string' }
+      ]
+    });
+
+    this.modalCtrl = options?.modalCtrl
+  }
+
+  override async handle() {
+    // 1. 通过模态框获取用户输入
+    const symptom = await this.getSymptomDescription();
+    if (!symptom) throw new Error('症状描述不能为空');
+
+    this.updateData('symptomDescription', symptom);
+
+    // 2. 调用大模型进行分诊
+    await this.generateTriageResult(symptom);
+  }
+
+  onDestroy() {
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+
+  private async getSymptomDescription(): Promise<string> {
+    const modal = await this.modalCtrl.create({
+      component: SymptomInputModalComponent,
+      backdropDismiss: false
+    });
+
+    await modal.present();
+    const { data } = await modal.onDidDismiss();
+    return data || '';
+  }
+
+  private generateTriageResult(symptom: string): Promise<void> {
+    return new Promise((resolve, reject) => {
+      const prompt = [
+        {
+          role: "user",
+          content: `您是一名专业的分诊护士,根据患者描述推荐最合适的科室。严格按照以下JSON格式返回:
+          {
+            "department": "科室名称",
+            "reason": "分诊理由(50字以内)"
+          }`+ `患者主诉:${symptom}`
+        }
+      ];
+
+      let completion = new FmodeChatCompletion(prompt);
+
+      completion.sendCompletion({
+        onComplete: (message: any) => {
+          console.log("onComplete", message)
+        }
+      })
+        // .pipe(takeUntil(this.destroy$))
+        .subscribe((message: any) => {
+          // 处理流式响应
+          if (message.content && typeof message.content === 'string') {
+            this.setProgress(0.5 + (message.content.length / 200) * 0.5); // 模拟进度
+          }
+          // console.log(message)
+          if (message.complete == true) {
+            console.log(message)
+            try {
+              const result = this.parseTriageResponse(message.content);
+              console.log("result", result)
+              this.updateData('department', result.department)
+                .updateData('triageReason', result.reason);
+              this.setProgress(1);
+              resolve();
+            } catch (e) {
+              reject(e);
+            }
+          }
+        });
+    });
+  }
+
+  private parseTriageResponse(response: string): { department: string; reason: string } {
+    try {
+      // 尝试提取JSON部分(处理可能的非JSON前缀)
+      const jsonStart = response.indexOf('{');
+      const jsonEnd = response.lastIndexOf('}') + 1;
+      const jsonStr = response.slice(jsonStart, jsonEnd);
+
+      console.log("jsonStr", jsonStr)
+      const result = JSON.parse(jsonStr);
+      if (!result.department) throw new Error('缺少科室信息');
+
+      return {
+        department: result.department.trim(),
+        reason: result.reason || '根据症状描述判断'
+      };
+    } catch (e) {
+      throw new Error(`分诊结果解析失败: ${response}`);
+    }
+  }
+}

+ 59 - 0
src/lib/consult-tasks/symptom-input/symptom-input.modal.ts

@@ -0,0 +1,59 @@
+// symptom-input.modal.ts
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { IonicModule, ModalController } from '@ionic/angular';
+
+@Component({
+  standalone: true,
+  imports: [IonicModule, FormsModule],
+  template: `
+    <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-textarea
+        [(ngModel)]="symptomDescription"
+        placeholder="请详细描述您的症状(如:头痛3天,伴有恶心呕吐)"
+        autoGrow
+        rows="5"
+      ></ion-textarea>
+    </ion-content>
+
+    <ion-footer>
+      <ion-toolbar>
+        <ion-button
+          expand="block"
+          (click)="submit()"
+          [disabled]="!symptomDescription.trim()">
+          提交
+        </ion-button>
+      </ion-toolbar>
+    </ion-footer>
+  `,
+  styles: [`
+    ion-textarea {
+      background: var(--ion-color-light);
+      border-radius: 8px;
+      margin: 16px;
+      padding: 8px;
+    }
+  `]
+})
+export class SymptomInputModalComponent {
+  symptomDescription = '';
+  constructor(private modalCtrl:ModalController){}
+
+  submit() {
+    this.modalCtrl.dismiss(this.symptomDescription.trim());
+  }
+
+  dismiss() {
+    this.modalCtrl.dismiss(null);
+  }
+}

+ 37 - 0
src/lib/consult-tasks/triage-result/triage-result.component.ts

@@ -0,0 +1,37 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+
+@Component({
+  standalone: true,
+  imports: [CommonModule, IonicModule],
+  template: `
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>分诊结果</ion-card-title>
+      </ion-card-header>
+
+      <ion-card-content>
+        <ion-item>
+          <ion-label>症状描述</ion-label>
+          <ion-text>{{ symptom }}</ion-text>
+        </ion-item>
+
+        <ion-item>
+          <ion-label>推荐科室</ion-label>
+          <ion-badge color="primary">{{ department }}</ion-badge>
+        </ion-item>
+
+        <ion-item>
+          <ion-label>分诊依据</ion-label>
+          <ion-text>{{ reason }}</ion-text>
+        </ion-item>
+      </ion-card-content>
+    </ion-card>
+  `
+})
+export class TriageResultComponent {
+  @Input() symptom = '';
+  @Input() department = '';
+  @Input() reason = '';
+}

+ 147 - 0
src/lib/flow-display/flow-display.component.ts

@@ -0,0 +1,147 @@
+// flow-display.component.ts
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from '@ionic/angular';
+import { ModalController } from '@ionic/angular';
+import { FlowExecutor } from '../flow.executor';
+
+@Component({
+  standalone: true,
+  imports: [CommonModule, IonicModule],
+  selector: 'app-flow-display',
+  template: `
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>{{executor?.workflow?.title}} <ion-button (click)="dismiss()">返回</ion-button></ion-title>
+        <ion-progress-bar [value]="executor?.currentProgress"></ion-progress-bar>
+      </ion-toolbar>
+    </ion-header>
+
+    <ion-content>
+      <ion-list>
+        <ion-item *ngFor="let task of executor?.workflow?.taskList; let i = index">
+          <ion-label (click)="toggleExpand(i)">
+            <h2>{{task.title}}</h2>
+            <p>{{task.getData('description')}}</p>
+
+            <div *ngIf="expandedTasks[i]" class="task-details">
+              <ion-text color="medium">
+                <p>状态: {{task.status}}</p>
+                <p>进度: {{task.progress * 100 | number:'1.0-0'}}%</p>
+                <p>开始时间: {{taskTimers[i]?.start | date:'mediumTime'}}</p>
+                <p>持续时间: {{calculateDuration(i) | number:'1.1-1'}}秒</p>
+                <pre *ngIf="task.output">输出参数{{task.output | json}}</pre>
+                <pre *ngIf="task.data">共享数据{{task.data | json}}</pre>
+              </ion-text>
+            </div>
+          </ion-label>
+
+          <ion-badge slot="end" [color]="getStatusColor(task.status)">
+            {{task.status}} ({{task.progress * 100 | number:'1.0-0'}}%)
+          </ion-badge>
+        </ion-item>
+      </ion-list>
+      <ion-card>共享数据
+      {{executor?.sharedData|json}}
+      </ion-card>
+
+      <ion-footer *ngIf="executor?.status === 'failed'">
+        <ion-toolbar color="danger">
+          <ion-buttons slot="start">
+            <ion-button (click)="retryFailedTask()">
+              <ion-icon slot="start" name="refresh"></ion-icon>
+              重试失败任务
+            </ion-button>
+          </ion-buttons>
+          <ion-title>工作流执行失败</ion-title>
+          <ion-buttons slot="end">
+            <ion-button (click)="retryAll()">
+              <ion-icon slot="start" name="reload"></ion-icon>
+              全部重试
+            </ion-button>
+          </ion-buttons>
+        </ion-toolbar>
+      </ion-footer>
+    </ion-content>
+  `,
+  styles: [`
+    ion-progress-bar {
+      height: 4px;
+    }
+    ion-item {
+      --padding-start: 16px;
+      --inner-padding-end: 16px;
+    }
+    ion-badge {
+      margin-inline-start: 12px;
+    }
+    .task-details {
+      margin-top: 8px;
+      padding: 8px;
+      background: var(--ion-color-light);
+      border-radius: 4px;
+    }
+    pre {
+      white-space: pre-wrap;
+      font-size: 12px;
+    }
+  `]
+})
+export class FlowDisplayComponent {
+  @Input() executor?: FlowExecutor;
+  expandedTasks: boolean[] = [];
+  taskTimers: { start?: Date, end?: Date }[] = [];
+
+  dismiss() {
+    this.modalCtrl.dismiss();
+  }
+  constructor(
+    private modalCtrl: ModalController
+  ) {
+    // 监听任务状态变化
+    this.executor?.taskStart$.subscribe((task) => {
+      const index = this.executor!.workflow!.taskList.indexOf(task);
+      this.taskTimers[index] = { start: new Date() };
+    });
+
+    this.executor?.taskSuccess$.subscribe((task) => {
+      const index = this.executor!.workflow!.taskList.indexOf(task);
+      this.taskTimers[index].end = new Date();
+    });
+  }
+
+  // 切换任务详情展开状态
+  toggleExpand(index: number) {
+    this.expandedTasks[index] = !this.expandedTasks[index];
+  }
+
+  // 计算任务持续时间
+  calculateDuration(index: number): number {
+    const timer = this.taskTimers[index];
+    if (!timer?.start) return 0;
+    const end = timer.end || new Date();
+    return (end.getTime() - timer.start.getTime()) / 1000;
+  }
+
+  // 重试失败的任务
+  async retryFailedTask() {
+    const failedIndex = this.executor?.failedTaskIndex;
+    if (failedIndex !== null && failedIndex !== undefined) {
+      await this.executor?.retryFromTask(failedIndex);
+    }
+  }
+
+  // 重试全部任务
+  async retryAll() {
+    await this.executor?.retryFromTask(0);
+  }
+
+  getStatusColor(status: string): string {
+    switch (status) {
+      case 'success': return 'success';
+      case 'failed': return 'danger';
+      case 'running': return 'primary';
+      default: return 'medium';
+    }
+  }
+}

+ 125 - 0
src/lib/flow.executor.ts

@@ -0,0 +1,125 @@
+// flow-executor.ts
+import { FlowWorkflow, FlowStatus } from './flow.workflow';
+import { FlowTask } from './flow.task';
+import { Subject } from 'rxjs';
+
+export class FlowExecutor {
+  public workflow?: FlowWorkflow;
+  private currentTaskIndex = 0;
+  private _status: FlowStatus = 'idle';
+  private retryCount = 0;
+
+  // 共享数据
+  public sharedData: Record<string, any> = {}; // 共享数据存储
+
+  // 事件系统
+  public taskStart$ = new Subject<FlowTask>();
+  public taskSuccess$ = new Subject<FlowTask>();
+  public taskFailure$ = new Subject<{ task: FlowTask; error: Error }>();
+  public statusChange$ = new Subject<FlowStatus>();
+  public progressUpdate$ = new Subject<number>();
+
+  constructor(
+    public maxRetries = 3,
+    public autoRetry = false
+  ) { }
+
+  setWorkflow(workflow: FlowWorkflow) {
+    this.workflow = workflow;
+
+    // 初始化所有任务的共享数据
+    this.workflow.taskList.forEach(task => {
+      task.setSharedData(this.sharedData);
+    });
+
+    this.reset();
+  }
+
+  // 添加获取共享数据的方法
+  getSharedData(): Record<string, any> {
+    return this.sharedData;
+  }
+  // 添加更新共享数据的方法
+  updateSharedData(data: Record<string, any>): void {
+    this.sharedData = { ...this.sharedData, ...data };
+    // 更新所有任务的引用(以防有新增任务)
+    this.workflow?.taskList.forEach(task => {
+      task.data = this.sharedData;
+    });
+  }
+
+  async start() {
+    if (!this.workflow) throw new Error('工作流未设置');
+    this._status = 'running';
+    this.statusChange$.next(this._status);
+    await this.executeNextTask();
+  }
+
+  // 方法:从指定任务索引重新执行
+  async retryFromTask(taskIndex: number) {
+    if (!this.workflow || taskIndex < 0 || taskIndex >= this.workflow.taskList.length) {
+      throw new Error('无效的任务索引');
+    }
+
+    this.currentTaskIndex = taskIndex;
+    this.retryCount = 0;
+    this._status = 'running';
+    this.statusChange$.next(this._status);
+    await this.executeNextTask();
+  }
+  // 获取当前失败的任务索引
+  get failedTaskIndex(): number | null {
+    return this._status === 'failed' ? this.currentTaskIndex : null;
+  }
+
+  private async executeNextTask() {
+    if (!this.workflow || this.currentTaskIndex >= this.workflow.taskList.length) {
+      this._status = 'success';
+      this.statusChange$.next(this._status);
+      return;
+    }
+
+    const task = this.workflow.taskList[this.currentTaskIndex];
+    task.data = this.sharedData;
+    try {
+      this.taskStart$.next(task);
+      await task.execute();
+      console.log(task.title, task.data, this.sharedData)
+      this.sharedData = task.data;
+      // 只有当任务状态是success时才继续
+      if (task.status === 'success') {
+        this.taskSuccess$.next(task);
+        this.currentTaskIndex++;
+        this.retryCount = 0;
+        await this.executeNextTask();
+      }
+      // 如果是idle状态(用户取消),不继续执行也不标记为失败
+    } catch (error) {
+      console.error('任务执行错误:', error);
+      this.taskFailure$.next({ task, error: error as Error });
+
+      if (this.autoRetry && this.retryCount < this.maxRetries) {
+        this.retryCount++;
+        await this.executeNextTask();
+      } else {
+        this._status = 'failed';
+        this.statusChange$.next(this._status);
+      }
+    }
+  }
+
+  get status() {
+    return this._status;
+  }
+
+  get currentProgress() {
+    if (!this.workflow) return 0;
+    return this.currentTaskIndex / this.workflow.taskList.length;
+  }
+
+  reset() {
+    this.currentTaskIndex = 0;
+    this.retryCount = 0;
+    this._status = 'idle';
+  }
+}

+ 186 - 0
src/lib/flow.task.ts

@@ -0,0 +1,186 @@
+type FieldType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
+
+export interface FieldSchema {
+  name: string;
+  type: FieldType;
+  description?: string;
+  required?: boolean;
+}
+
+export interface FlowTaskOptions {
+  title: string;
+  input?: FieldSchema[];
+  output?: FieldSchema[];
+  initialData?: Record<string, any>;
+}
+
+export class FlowTask {
+  // 核心属性
+  title: string;
+  _status: 'idle' | 'running' | 'success' | 'failed' = 'idle';
+  _progress: number = 0;
+  public data: Record<string, any> = {};
+  // 添加设置共享数据的方法
+  setSharedData(data: Record<string, any>): this {
+    this.data = data;
+    return this;
+  }
+
+
+  // 校验规则
+  inputSchema: FieldSchema[];
+  outputSchema: FieldSchema[];
+
+  // 添加执行时间记录
+  _startTime?: Date;
+  _endTime?: Date;
+  // 添加执行时间信息
+  get executionTime(): number {
+    if (!this._startTime) return 0;
+    const end = this._endTime || new Date();
+    return (end.getTime() - this._startTime.getTime()) / 1000;
+  }
+
+  constructor(options: FlowTaskOptions) {
+    this.title = options.title;
+    this.data = options.initialData || {};
+    this.inputSchema = options.input || [];
+    this.outputSchema = options.output || [];
+  }
+
+  /************************************
+   *          核心执行流程              *
+   ************************************/
+
+  async execute(): Promise<void> {
+
+    try {
+      if (this._status !== 'idle') return;
+      this._startTime = new Date();
+
+      this._status = 'running';
+      this.validateInput();  // 输入校验
+
+      this.beforeExecute();
+      await this.handle();   // 执行用户自定义逻辑
+      this.afterExecute();
+
+      this.validateOutput(); // 输出校验
+      this._status = 'success';
+      this.onSuccess();
+    } catch (error) {
+      this._status = 'failed';
+      this.onFailure(error as Error);
+      throw error; // 重新抛出错误确保执行器能捕获
+    }
+    this._endTime = new Date();
+  }
+
+  /************************************
+   *          用户可覆盖方法            *
+   ************************************/
+
+  // 主处理函数(用户需要覆盖的核心方法)
+  protected async handle(): Promise<void> {
+    // 默认空实现,抛出错误提示需要实现
+    throw new Error('必须实现 handle() 方法');
+  }
+
+  // 生命周期钩子(可选覆盖)
+  protected beforeExecute(): void { }
+  protected afterExecute(): void { }
+  protected onProgress(progress: number): void { }
+  protected onSuccess(): void { }
+  protected onFailure(error: Error): void { }
+
+  /************************************
+   *          数据校验系统              *
+   ************************************/
+
+  validateInput(): void {
+    const errors: string[] = [];
+
+    this.inputSchema.forEach(field => {
+      const value = this.data[field.name];
+
+      // 检查必填字段
+      if (field.required && value === undefined) {
+        errors.push(`缺少必要字段:${field.name}`);
+        return;
+      }
+
+      // 类型校验
+      if (value !== undefined && !this.checkType(value, field.type)) {
+        errors.push(`${field.name} 类型错误,期望 ${field.type},实际 ${this.getType(value)}`);
+      }
+    });
+
+    if (errors.length > 0) {
+      throw new Error(`输入校验失败:\n${errors.join('\n')}`);
+    }
+  }
+
+  validateOutput(): void {
+    const missingFields = this.outputSchema
+      .filter(f => f.required)
+      .filter(f => !(f.name in this.data))
+      .map(f => f.name);
+
+    if (missingFields.length > 0) {
+      throw new Error(`输出校验失败,缺少字段:${missingFields.join(', ')}`);
+    }
+  }
+
+  /************************************
+   *          工具方法                *
+   ************************************/
+
+  // 类型检查
+  checkType(value: any, expected: FieldType): boolean {
+    const actualType = this.getType(value);
+    return expected === 'any' || actualType === expected;
+  }
+
+  getType(value: any): FieldType {
+    if (Array.isArray(value)) return 'array';
+    if (value === null) return 'object';
+    return typeof value as FieldType;
+  }
+
+  /************************************
+   *          公共接口                *
+   ************************************/
+
+  // 更新任务数据
+  updateData(key: string, value: any): this {
+    this.data[key] = value;
+    return this;
+  }
+
+  // 获取任务数据
+  getData<T = any>(key: string): T {
+    return this.data[key];
+  }
+
+  // 进度更新
+  setProgress(value: number): void {
+    if (value < 0 || value > 1) return;
+    this._progress = value;
+    this.onProgress(value);
+  }
+
+  // 状态访问器
+  get progress(): number {
+    return this._progress;
+  }
+
+  get status() {
+    return this._status;
+  }
+
+  get output() {
+    return Object.fromEntries(
+      this.outputSchema.map(f => [f.name, this.data[f.name]])
+    );
+  }
+}

+ 9 - 0
src/lib/flow.workflow.ts

@@ -0,0 +1,9 @@
+import { FlowTask } from './flow.task';
+
+export interface FlowWorkflow {
+  title: string;
+  desc?: string;
+  taskList: FlowTask[];
+}
+
+export type FlowStatus = 'idle' | 'running' | 'paused' | 'success' | 'failed';

+ 116 - 72
src/lib/import.data.ts

@@ -1,97 +1,141 @@
-import { CloudUser, CloudObject,CloudQuery } from './ncloud';
+import { CloudUser, CloudObject, CloudQuery } from './ncloud';
 
 
 // 测试数据导入函数
 // 测试数据导入函数
 export async function importTestData() {
 export async function importTestData() {
   try {
   try {
-    // 1. 创建或登录测试用户
-    const user = new CloudUser();
-    const testUsername = 'test_patient@111.com';
-    const testPassword = 'test123456';
-    
-    // 尝试登录,如果失败则注册新用户
-    let loggedInUser = await user.login(testUsername, testPassword);
-    if (!loggedInUser) {
-      loggedInUser = await user.signUp(testUsername, testPassword, {
+    // 第一个测试账号(原有)
+    const user1 = await createTestUser(
+      '123123', 
+      '123123',
+      {
         firstName: '张',
         firstName: '张',
         lastName: '三',
         lastName: '三',
-        email: testUsername,
+        email: '123123@example.com',
         birthDate: new Date('1970-05-15'),
         birthDate: new Date('1970-05-15'),
         gender: 'male',
         gender: 'male',
         medicalHistory: ['高血压', '2型糖尿病'],
         medicalHistory: ['高血压', '2型糖尿病'],
-        currentMedications: ['二甲双胍', '阿司匹林']
-      });
-      console.log('新用户注册成功:', loggedInUser);
-    } else {
-      console.log('用户登录成功:', loggedInUser);
-    }
-    //用户存在性检查:
-    if (!loggedInUser) {
-     throw new Error('无法创建或登录测试用户');
+        currentMedications: ['二甲双胍', '阿司匹林'],
+        nickname: '张三'
       }
       }
-    //测试删除
-      await deleteUserHealthData(loggedInUser);
-    // 2. 创建血糖测试数据
-    const bloodGlucoseData = [
+    );
+    
+    // 为第一个账号创建测试数据
+    await createHealthData(user1, [
       { glucoseValue: 5.2, measurementType: '空腹', measurementTime: new Date('2025-05-29T08:00:00') },
       { glucoseValue: 5.2, measurementType: '空腹', measurementTime: new Date('2025-05-29T08:00:00') },
       { glucoseValue: 7.8, measurementType: '餐后', measurementTime: new Date('2025-05-29T14:30:00') },
       { glucoseValue: 7.8, measurementType: '餐后', measurementTime: new Date('2025-05-29T14:30:00') },
       { glucoseValue: 6.5, measurementType: '随机', measurementTime: new Date('2025-05-29T10:15:00') }
       { glucoseValue: 6.5, measurementType: '随机', measurementTime: new Date('2025-05-29T10:15:00') }
-    ];
-
-    for (const data of bloodGlucoseData) {
-      const glucose = new CloudObject('BloodGlucose');
-      glucose.set({
-        patient: user.toPointer(),
-        ...data,
-        notes: '早餐后测量',
-        tags: ['早餐后', '正常范围']
-      });
-      await glucose.save();
-      console.log('血糖数据创建成功:', glucose.id);
-    }
-
-    // 3. 创建血压测试数据
-    const bloodPressureData = [
+    ], [
       { systolic: 120, diastolic: 80, pulse: 72, measurementTime: new Date('2025-05-29T09:00:00'), measurementPosition: '坐姿' },
       { systolic: 120, diastolic: 80, pulse: 72, measurementTime: new Date('2025-05-29T09:00:00'), measurementPosition: '坐姿' },
       { systolic: 135, diastolic: 85, pulse: 76, measurementTime: new Date('2025-05-29T09:00:00'), measurementPosition: '坐姿' },
       { systolic: 135, diastolic: 85, pulse: 76, measurementTime: new Date('2025-05-29T09:00:00'), measurementPosition: '坐姿' },
       { systolic: 128, diastolic: 82, pulse: 74, measurementTime: new Date('2025-05-29T09:00:00'), measurementPosition: '坐姿' }
       { systolic: 128, diastolic: 82, pulse: 74, measurementTime: new Date('2025-05-29T09:00:00'), measurementPosition: '坐姿' }
-    ];
-
-    for (const data of bloodPressureData) {
-      const pressure = new CloudObject('BloodPressure');
-      pressure.set({
-        patient: user.toPointer(),
-        ...data,
-        notes: '早晨服药前测量',
-        tags: ['早晨', '服药前']
-      });
-      await pressure.save();
-      console.log('血压数据创建成功:', pressure.id);
-    }
-
-    // 4. 创建心率测试数据
-    const heartRateData = [
+    ], [
       { heartRate: 72, measurementTime: new Date('2025-05-29T08:30:00'), measurementMethod: '设备', rhythm: '规律' },
       { heartRate: 72, measurementTime: new Date('2025-05-29T08:30:00'), measurementMethod: '设备', rhythm: '规律' },
       { heartRate: 68, measurementTime: new Date('2025-05-29T08:30:00'), measurementMethod: '设备', rhythm: '规律' },
       { heartRate: 68, measurementTime: new Date('2025-05-29T08:30:00'), measurementMethod: '设备', rhythm: '规律' },
       { heartRate: 75, measurementTime: new Date('2025-05-29T08:30:00'), measurementMethod: '手动', rhythm: '规律' }
       { heartRate: 75, measurementTime: new Date('2025-05-29T08:30:00'), measurementMethod: '手动', rhythm: '规律' }
-    ];
-
-    for (const data of heartRateData) {
-      const heartRate = new CloudObject('HeartRate');
-      heartRate.set({
-        patient: user.toPointer(),
-        ...data,
-        notes: '静息心率测量',
-        tags: ['静息', '正常']
-      });
-      await heartRate.save();
-      console.log('心率数据创建成功:', heartRate.id);
-    }
+    ]);
+    
+    // 第二个测试账号(新增)
+    const user2 = await createTestUser(
+      '456456',
+      '456456',
+      {
+        firstName: '李',
+        lastName: '四',
+        email: '456456@example.com',
+        birthDate: new Date('1985-08-22'),
+        gender: 'female',
+        medicalHistory: ['高胆固醇', '甲状腺功能减退'],
+        currentMedications: ['左甲状腺素', '阿托伐他汀'],
+        nickname: '李四'
+      }
+    );
+    
+    // 为第二个账号创建测试数据
+    await createHealthData(user2, [
+      { glucoseValue: 4.8, measurementType: '空腹', measurementTime: new Date('2025-05-29T07:30:00') },
+      { glucoseValue: 6.2, measurementType: '餐后', measurementTime: new Date('2025-05-29T13:45:00') }
+    ], [
+      { systolic: 118, diastolic: 77, pulse: 68, measurementTime: new Date('2025-05-29T08:15:00') },
+      { systolic: 122, diastolic: 79, pulse: 70, measurementTime: new Date('2025-05-29T20:30:00') }
+    ], [
+      { heartRate: 65, measurementTime: new Date('2025-05-29T07:45:00') },
+      { heartRate: 72, measurementTime: new Date('2025-05-29T18:20:00') }
+    ]);
 
 
-    console.log('所有测试数据导入完成!');
+    console.log('所有测试数据和账号创建完成!');
   } catch (error) {
   } catch (error) {
     console.error('导入测试数据时出错:', error);
     console.error('导入测试数据时出错:', error);
   }
   }
 }
 }
+
+// 提取的创建用户函数
+async function createTestUser(username: string, password: string, userInfo: any) {
+  const user = new CloudUser();
+  let loggedInUser = await user.login(username, password);
+  
+  if (!loggedInUser) {
+    loggedInUser = await user.signUp(username, password, userInfo);
+    console.log(`新用户 ${username} 注册成功:`, loggedInUser);
+  } else {
+    console.log(`用户 ${username} 登录成功:`, loggedInUser);
+  }
+  
+  if (!loggedInUser) {
+    throw new Error(`无法创建或登录用户 ${username}`);
+  }
+  
+  await deleteUserHealthData(loggedInUser);
+  return loggedInUser;
+}
+
+// 提取的创建健康数据函数
+async function createHealthData(
+  user: CloudUser,
+  glucoseData: any[],
+  pressureData: any[],
+  heartRateData: any[]
+) {
+  // 创建血糖数据
+  for (const data of glucoseData) {
+    const glucose = new CloudObject('BloodGlucose');
+    glucose.set({
+      patient: user.toPointer(),
+      ...data,
+      notes: '常规测量',
+      tags: ['日常']
+    });
+    await glucose.save();
+    console.log(`血糖数据创建成功 (用户 ${user.data['username']}):`, glucose.id);
+  }
+
+  // 创建血压数据
+  for (const data of pressureData) {
+    const pressure = new CloudObject('BloodPressure');
+    pressure.set({
+      patient: user.toPointer(),
+      ...data,
+      notes: '日常监测',
+      tags: ['常规']
+    });
+    await pressure.save();
+    console.log(`血压数据创建成功 (用户 ${user.data['username']}):`, pressure.id);
+  }
+
+  // 创建心率数据
+  for (const data of heartRateData) {
+    const heartRate = new CloudObject('HeartRate');
+    heartRate.set({
+      patient: user.toPointer(),
+      ...data,
+      notes: '静息心率',
+      tags: ['静息']
+    });
+    await heartRate.save();
+    console.log(`心率数据创建成功 (用户 ${user.data['username']}):`, heartRate.id);
+  }
+}
+
+// 删除用户健康数据函数
 async function deleteUserHealthData(user: CloudUser) {
 async function deleteUserHealthData(user: CloudUser) {
   try {
   try {
     const classes = ['BloodGlucose', 'BloodPressure', 'HeartRate'];
     const classes = ['BloodGlucose', 'BloodPressure', 'HeartRate'];
@@ -101,12 +145,11 @@ async function deleteUserHealthData(user: CloudUser) {
       query.equalTo('patient', user.toPointer());
       query.equalTo('patient', user.toPointer());
       const results = await query.find();
       const results = await query.find();
       
       
-      // 逐条删除记录
       if (results.length > 0) {
       if (results.length > 0) {
         for (const record of results) {
         for (const record of results) {
           await record.destroy();
           await record.destroy();
         }
         }
-        console.log(`已删除 ${results.length} 条 ${className} 记录`);
+        console.log(`已删除 ${results.length} 条 ${className} 记录 (用户 ${user.data['username']})`);
       }
       }
     }
     }
     return true;
     return true;
@@ -115,5 +158,6 @@ async function deleteUserHealthData(user: CloudUser) {
     throw error;
     throw error;
   }
   }
 }
 }
+
 // 执行导入
 // 执行导入
 importTestData();
 importTestData();

+ 59 - 0
src/lib/job-workflow/job-workflow.ts

@@ -0,0 +1,59 @@
+// job-application-workflow.ts
+import { UserBasicInfoTask } from './task.job';
+import { JobIntentionTask } from './task.job';
+import { UserInterestsTask } from './task.job';
+import { FlowDisplayComponent } from '../../lib/flow-display/flow-display.component';
+import { FlowExecutor } from '../../lib/flow.executor';
+
+export class JobApplicationWorkflow {
+    private executor: any | FlowExecutor;
+    private modalCtrl: any;
+
+    constructor(modalCtrl: any) {
+        this.modalCtrl = modalCtrl;
+        this.executor = new FlowExecutor();
+    }
+
+    async startWorkflow() {
+        // 创建工作流
+        const workflow = {
+            title: '求职信息收集',
+            desc: '请填写您的求职相关信息',
+            taskList: [
+                new UserBasicInfoTask(this.modalCtrl),
+                new JobIntentionTask(this.modalCtrl),
+                new UserInterestsTask(this.modalCtrl)
+            ]
+        };
+
+        this.executor.setWorkflow(workflow);
+
+        // 打开工作流展示模态框
+        const modal = await this.modalCtrl.create({
+            component: FlowDisplayComponent, // 假设有工作流展示组件
+            componentProps: { executor: this.executor }
+        });
+
+        await modal.present();
+
+        try {
+            await this.executor.start();
+            console.log('工作流完成,收集到的数据:', this.getCollectedData());
+            return this.getCollectedData();
+        } catch (error) {
+            console.error('工作流执行出错:', error);
+            throw error;
+        }
+    }
+
+    getCollectedData() {
+        if (!this.executor?.isCompleted) {
+            return null;
+        }
+
+        // 合并所有任务的数据
+        return this.executor?.taskList.reduce((result: any, task: any) => {
+            return { ...result, ...task.output };
+        }, {});
+    }
+}

+ 89 - 0
src/lib/job-workflow/task.job.ts

@@ -0,0 +1,89 @@
+import { TaskUserForm } from "../../lib/tasks/task-user-form/task-user-form";
+export class UserBasicInfoTask extends TaskUserForm {
+    constructor(modalCtrl: any, initialData: any = {}) {
+        super({
+            title: '基本信息',
+            modalCtrl: modalCtrl,
+            initialData: initialData,
+            fieldList: [
+                { type: "String", key: "name", name: "姓名", required: true },
+                { type: "String", key: "mobile", name: "手机", required: true, validator: 'mobile' },
+                {
+                    type: "Radio", key: "gender", name: "性别", required: true, options: [
+                        { label: "男", value: "male" },
+                        { label: "女", value: "female" },
+                        { label: "其他", value: "other" }
+                    ]
+                },
+                { type: "Number", key: "age", name: "年龄", required: false },
+                { type: "String", key: "email", name: "邮箱", required: false, validator: 'email' }
+            ]
+        });
+    }
+}
+
+
+export class JobIntentionTask extends TaskUserForm {
+    constructor(modalCtrl: any, initialData: any = {}) {
+        super({
+            title: '求职意向',
+            modalCtrl: modalCtrl,
+            initialData: initialData,
+            fieldList: [
+                {
+                    type: "Select", key: "jobType", name: "工作类型", required: true, options: [
+                        { label: "全职", value: "full-time" },
+                        { label: "兼职", value: "part-time" },
+                        { label: "实习", value: "internship" },
+                        { label: "自由职业", value: "freelance" }
+                    ]
+                },
+                // {
+                //     type: "MultiSelect", key: "industries", name: "行业意向", required: true, options: [
+                //         { label: "互联网", value: "internet" },
+                //         { label: "金融", value: "finance" },
+                //         { label: "教育", value: "education" },
+                //         { label: "医疗", value: "medical" },
+                //         { label: "制造业", value: "manufacturing" }
+                //     ]
+                // },
+                { type: "String", key: "position", name: "期望职位", required: true },
+                { type: "Number", key: "expectedSalary", name: "期望薪资(元)", required: false },
+                {
+                    type: "Select", key: "city", name: "工作城市", required: true, options: [
+                        { label: "北京", value: "beijing" },
+                        { label: "上海", value: "shanghai" },
+                        { label: "广州", value: "guangzhou" },
+                        { label: "深圳", value: "shenzhen" },
+                        { label: "其他", value: "other" }
+                    ]
+                }
+            ]
+        });
+    }
+}
+
+
+export class UserInterestsTask extends TaskUserForm {
+    constructor(modalCtrl: any, initialData: any = {}) {
+        super({
+            title: '兴趣喜好',
+            modalCtrl: modalCtrl,
+            initialData: initialData,
+            fieldList: [
+                // {
+                //     type: "MultiSelect", key: "hobbies", name: "兴趣爱好", required: false, options: [
+                //         { label: "阅读", value: "reading" },
+                //         { label: "运动", value: "sports" },
+                //         { label: "音乐", value: "music" },
+                //         { label: "旅行", value: "travel" },
+                //         { label: "美食", value: "food" },
+                //         { label: "摄影", value: "photography" }
+                //     ]
+                // },
+                // { type: "TextArea", key: "selfDescription", name: "自我描述", required: false, placeholder: "简单介绍一下你自己..." },
+                // { type: "TextArea", key: "otherInterests", name: "其他兴趣", required: false, placeholder: "请补充其他兴趣..." }
+            ]
+        });
+    }
+}

+ 127 - 0
src/lib/mock-tasks/mock-tasks.ts

@@ -0,0 +1,127 @@
+// mock-tasks.ts
+import { FlowTask } from '../../lib/flow.task';
+
+export class MockSuccessTask extends FlowTask {
+  constructor() {
+    super({
+      title: '初始化系统',
+      input: [
+        { name: 'config', type: 'object', required: true, description: '系统配置对象' }
+      ],
+      output: [
+        { name: 'result', type: 'string', description: '初始化结果' },
+        { name: 'timestamp', type: 'number', description: '完成时间戳' }
+      ],
+      initialData: {
+        description: '系统初始化配置',
+        config: { env: 'production' } // 提供默认配置
+      }
+    });
+  }
+
+  override async handle() {
+    // 模拟初始化过程
+    await new Promise(resolve => setTimeout(resolve, 1000));
+
+    // 验证输入
+    if (!this.getData('config')) {
+      throw new Error('缺少必要配置');
+    }
+
+    // 设置输出
+    this.updateData('result', '初始化成功')
+       .updateData('timestamp', Date.now());
+    this.setProgress(1);
+  }
+}
+
+export class MockRetryTask extends FlowTask {
+  private static readonly MAX_ATTEMPTS = 3;
+
+  constructor() {
+    super({
+      title: '数据同步',
+      output: [
+        { name: 'syncResult', type: 'object', description: '同步结果数据' }
+      ],
+      initialData: {
+        description: '尝试连接远程服务器',
+        serverUrl: 'https://api.example.com'
+      }
+    });
+  }
+
+  override async handle() {
+    // 模拟连接尝试
+    await new Promise(resolve => setTimeout(resolve, 300));
+
+    // 模拟75%成功率
+    const shouldFail = Math.random() < 0.75;
+    if (shouldFail && this.attempts < MockRetryTask.MAX_ATTEMPTS - 1) {
+      this.attempts++;
+      throw new Error(`第 ${this.attempts} 次连接失败`);
+    }
+
+    // 成功情况
+    this.updateData('syncResult', {
+      connected: true,
+      server: this.getData('serverUrl'),
+      dataReceived: Array(5).fill(0).map((_,i) => `item_${i+1}`)
+    });
+    this.setProgress(1);
+  }
+
+  private get attempts(): number {
+    return this.getData('_attempts') || 0;
+  }
+
+  private set attempts(value: number) {
+    this.updateData('_attempts', value);
+  }
+}
+
+export class MockProgressTask extends FlowTask {
+  constructor() {
+    super({
+      title: '数据处理',
+      output: [
+        { name: 'processedItems', type: 'array' },
+        { name: 'stats', type: 'object' }
+      ],
+      initialData: {
+        description: '批量处理数据记录',
+        batchSize: 100
+      }
+    });
+  }
+
+  override async handle() {
+    const batchSize = this.getData('batchSize') || 100;
+    const results = [];
+
+    for (let i = 1; i <= batchSize; i++) {
+      await new Promise(resolve => setTimeout(resolve, 20));
+
+      // 模拟数据处理
+      results.push({
+        id: i,
+        processedAt: Date.now(),
+        value: Math.random().toString(36).substring(7)
+      });
+
+      // 每10%更新一次进度
+      if (i % (batchSize/10) === 0) {
+        this.setProgress(i / batchSize);
+      }
+    }
+
+    // 最终输出
+    this.updateData('processedItems', results)
+       .updateData('stats', {
+         total: results.length,
+         avgTime: 20,
+         firstId: results[0]?.id,
+         lastId: results[results.length-1]?.id
+       });
+  }
+}

+ 1 - 1
src/lib/ncloud.ts

@@ -7,7 +7,7 @@ if (location.protocol == "http:") {
 
 
 export class CloudObject {
 export class CloudObject {
     className: string;
     className: string;
-    id: string | undefined = undefined;
+    id: string |undefined = undefined;
     createdAt: any;
     createdAt: any;
     updatedAt: any;
     updatedAt: any;
     data: Record<string, any> = {};
     data: Record<string, any> = {};

+ 30 - 0
src/lib/tasks/task-completion-json/README.md

@@ -0,0 +1,30 @@
+# 任务:大模型生成JSON结果 TaskCompletionJson
+
+# 使用示例
+``` ts
+const triageTask = new TaskCompletionJson({
+  title: '智能分诊',
+  promptTemplate: `您是一名专业的分诊护士,根据患者描述推荐最合适的科室。严格按照以下JSON格式返回:
+  {
+    "department": "科室名称",
+    "reason": "分诊理由(50字以内)"
+  }
+  患者主诉:{{userDesc}}`,
+  input: [
+    { name: 'userDesc', type: 'string', required: true }
+  ],
+  output: [
+    { name: 'department', type: 'string', required: true },
+    { name: 'reason', type: 'string' }
+  ]
+});
+
+// Set input data
+triageTask.updateData('userDesc', '患者描述的症状...');
+
+// Execute the task
+await triageTask.execute();
+
+// Get results
+console.log(triageTask.output); // { department: '内科', reason: '根据症状描述判断...' }
+```

+ 219 - 0
src/lib/tasks/task-completion-json/task-completion-json.ts

@@ -0,0 +1,219 @@
+import { FmodeChatCompletion } from 'fmode-ng';
+import { FlowTask, FieldSchema, FlowTaskOptions } from '../../flow.task';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+export interface JsonCompletionOptions extends FlowTaskOptions {
+    promptTemplate: string;
+    modelOptions?: Record<string, any>;
+    strictPromptValidation?: boolean;
+}
+
+export class TaskCompletionJson extends FlowTask {
+    promptTemplate: string;
+    modelOptions: Record<string, any>;
+    strictPromptValidation: boolean;
+    destroy$ = new Subject<void>();
+
+    constructor(options: JsonCompletionOptions) {
+        super({
+            title: options.title || 'JSON Completion Task',
+            output: options.output, // Only output schema is needed
+            initialData: options.initialData
+        });
+
+        this.promptTemplate = options.promptTemplate;
+        this.modelOptions = options.modelOptions || {};
+        this.strictPromptValidation = options.strictPromptValidation ?? true;
+    }
+
+    override async handle(): Promise<void> {
+        // 1. Validate all required prompt variables exist in task.data
+        this.validatePromptVariables();
+
+        // 2. Prepare the prompt with variable substitution
+        const fullPrompt = this.renderPromptTemplate();
+
+        // 3. Call the LLM for completion
+        await this.callModelCompletion(fullPrompt);
+    }
+
+    validatePromptVariables(): void {
+        const requiredVariables = this.extractPromptVariables();
+        const missingVariables: string[] = [];
+        const undefinedVariables: string[] = [];
+
+        requiredVariables.forEach(variable => {
+            if (!(variable in this.data)) {
+                missingVariables.push(variable);
+            } else if (this.data[variable] === undefined) {
+                undefinedVariables.push(variable);
+            }
+        });
+
+        const errors: string[] = [];
+
+        if (missingVariables.length > 0) {
+            errors.push(`Missing required variables in task.data: ${missingVariables.join(', ')}`);
+        }
+
+        if (undefinedVariables.length > 0) {
+            errors.push(`Variables with undefined values: ${undefinedVariables.join(', ')}`);
+        }
+
+        if (errors.length > 0 && this.strictPromptValidation) {
+            throw new Error(`Prompt variable validation failed:\n${errors.join('\n')}`);
+        } else if (errors.length > 0) {
+            console.warn(`Prompt variable warnings:\n${errors.join('\n')}`);
+        }
+    }
+
+    extractPromptVariables(): string[] {
+        const matches = this.promptTemplate.match(/\{\{\w+\}\}/g) || [];
+        const uniqueVariables = new Set<string>();
+
+        matches.forEach(match => {
+            const key = match.replace(/\{\{|\}\}/g, '');
+            uniqueVariables.add(key);
+        });
+
+        return Array.from(uniqueVariables);
+    }
+
+    renderPromptTemplate(): string {
+        let result = this.promptTemplate;
+        const variables = this.extractPromptVariables();
+
+        variables.forEach(variable => {
+            if (this.data[variable] !== undefined) {
+                result = result.replace(new RegExp(`\\{\\{${variable}\\}\\}`, 'g'), this.data[variable]);
+            }
+        });
+
+        return result;
+    }
+
+    async callModelCompletion(prompt: string): Promise<void> {
+        return new Promise((resolve, reject) => {
+            const messages = [{
+                role: "user",
+                content: prompt
+            }];
+
+            const completion = new FmodeChatCompletion(messages);
+
+            let accumulatedContent = '';
+
+            completion.sendCompletion({
+                ...this.modelOptions,
+                onComplete: (message: any) => {
+                    console.log("onComplete", message);
+                }
+            })
+                .pipe(takeUntil(this.destroy$))
+                .subscribe({
+                    next: (message: any) => {
+                        if (message.content && typeof message.content === 'string') {
+                            accumulatedContent = message.content;
+                            this.setProgress(0.3 + (accumulatedContent.length / 1000) * 0.7);
+                        }
+
+                        if (message.complete) {
+                            try {
+                                const parsed = this.parseAndValidateResponse(accumulatedContent);
+                                this.updateOutputData(parsed);
+                                this.setProgress(1);
+                                resolve();
+                            } catch (error) {
+                                this.handleError(error as Error);
+                                reject(error);
+                            }
+                        }
+                    },
+                    error: (error) => {
+                        this.handleError(error);
+                        reject(error);
+                    }
+                });
+        });
+    }
+
+    parseAndValidateResponse(response: string): Record<string, any> {
+        const jsonStart = response.indexOf('{');
+        const jsonEnd = response.lastIndexOf('}') + 1;
+
+        if (jsonStart === -1 || jsonEnd === -1) {
+            throw new Error('Invalid JSON response format');
+        }
+
+        const jsonStr = response.slice(jsonStart, jsonEnd);
+        let parsedData: Record<string, any>;
+
+        try {
+            parsedData = JSON.parse(jsonStr);
+        } catch (e) {
+            throw new Error(`Failed to parse JSON response: ${(e as Error).message}`);
+        }
+
+        // Validate against output schema
+        this.validateOutputData(parsedData);
+
+        return parsedData;
+    }
+
+    validateOutputData(data: Record<string, any>): void {
+        if (!this.outputSchema || this.outputSchema.length === 0) {
+            return; // No validation needed if no output schema defined
+        }
+
+        const errors: string[] = [];
+
+        this.outputSchema.forEach(field => {
+            const value = data[field.name];
+
+            if (field.required && value === undefined) {
+                errors.push(`Missing required field in response: ${field.name}`);
+                return;
+            }
+
+            if (value !== undefined && !this.checkType(value, field.type)) {
+                errors.push(`${field.name} has wrong type, expected ${field.type}, got ${this.getType(value)}`);
+            }
+        });
+
+        if (errors.length > 0) {
+            throw new Error(`Output validation failed:\n${errors.join('\n')}`);
+        }
+    }
+
+    updateOutputData(parsedData: Record<string, any>): void {
+        Object.entries(parsedData).forEach(([key, value]) => {
+            this.updateData(key, value);
+        });
+    }
+
+    handleError(error: Error): void {
+        this.updateData('error', {
+            message: error.message,
+            stack: error.stack,
+            timestamp: new Date().toISOString()
+        });
+        this._status = 'failed';
+    }
+
+    override checkType(value: any, expected: any): boolean {
+        const actualType = this.getType(value);
+        return expected === 'any' || actualType === expected;
+    }
+
+    override getType(value: any): any {
+        if (Array.isArray(value)) return 'array';
+        if (value === null) return 'object';
+        return typeof value as any;
+    }
+
+    onDestroy(): void {
+        this.destroy$.next();
+        this.destroy$.complete();
+    }
+}

+ 43 - 0
src/lib/tasks/task-completion-text/README.md

@@ -0,0 +1,43 @@
+
+# 使用示例
+```ts
+// 1. Initialize the task with required parameters
+const textGenerator = new TaskCompletionText({
+    promptTemplate: "Write a product description for {{productName}} that highlights these features: {{features}}. Target audience: {{audience}}.",
+    outputProperty: "productDescription", // Where to store the result
+    inputVariables: ["productName", "features", "audience"], // Explicitly declare required variables
+    modelOptions: {
+        temperature: 0.7,
+        maxTokens: 500
+    },
+    strictValidation: true // Fail if variables are missing
+});
+
+// 2. Prepare input data (variables referenced in the prompt)
+textGenerator.updateData('productName', 'Smart Coffee Maker');
+textGenerator.updateData('features', [
+    'Voice control',
+    'Custom brew strength',
+    'Scheduled brewing'
+].join(', '));
+textGenerator.updateData('audience', 'tech-savvy home users');
+
+// 3. Execute the task
+try {
+    await textGenerator.handle();
+    
+    // 4. Get the generated text from the specified property
+    const generatedText = textGenerator.getData('productDescription');
+    console.log('Generated Description:', generatedText);
+    
+    // Example output might be:
+    // "Introducing our Smart Coffee Maker, perfect for tech-savvy home users..."
+} catch (error) {
+    console.error('Generation failed:', error);
+    const errorDetails = textGenerator.getData('error');
+    // Handle error...
+}
+
+// 5. Clean up when done
+textGenerator.onDestroy();
+```

+ 165 - 0
src/lib/tasks/task-completion-text/task-completion-text.ts

@@ -0,0 +1,165 @@
+import { FmodeChatCompletion } from 'fmode-ng';
+import { FlowTask, FlowTaskOptions } from '../../flow.task';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+export interface TextCompletionOptions extends FlowTaskOptions {
+    promptTemplate: string;          // Required: Your prompt template with {{variables}}
+    outputProperty: string;          // Required: Where to store the result in task.data
+    inputVariables?: string[];       // Optional: List of required variables (for validation)
+    modelOptions?: Record<string, any>; // Optional: Model configuration
+    strictValidation?: boolean;      // Optional: Whether to fail on missing variables
+}
+
+export class TaskCompletionText extends FlowTask {
+    promptTemplate: string;
+    outputProperty: string;
+    inputVariables: string[];
+    strictPromptValidation: boolean = false;
+    modelOptions: Record<string, any>;
+    strictValidation: boolean;
+    destroy$ = new Subject<void>();
+
+    constructor(options: TextCompletionOptions) {
+        super({
+            title: options.title || 'Text Generation Task',
+            output: options.output,
+            initialData: options.initialData
+        });
+
+        if (!options.promptTemplate) throw new Error('promptTemplate is required');
+        if (!options.outputProperty) throw new Error('outputProperty is required');
+
+        this.promptTemplate = options.promptTemplate;
+        this.outputProperty = options.outputProperty;
+        this.inputVariables = options.inputVariables || this.extractPromptVariables();
+        this.modelOptions = options.modelOptions || {};
+        this.strictValidation = options.strictValidation ?? true;
+    }
+
+
+    override async handle(): Promise<void> {
+        // 1. Validate all required prompt variables exist in task.data
+        this.validatePromptVariables();
+
+        // 2. Prepare the prompt with variable substitution
+        const fullPrompt = this.renderPromptTemplate();
+
+        // 3. Call the LLM for text completion
+        await this.callModelCompletion(fullPrompt);
+    }
+
+    validatePromptVariables(): void {
+        const requiredVariables = this.extractPromptVariables();
+        const missingVariables: string[] = [];
+        const undefinedVariables: string[] = [];
+
+        requiredVariables.forEach(variable => {
+            if (!(variable in this.data)) {
+                missingVariables.push(variable);
+            } else if (this.data[variable] === undefined) {
+                undefinedVariables.push(variable);
+            }
+        });
+
+        const errors: string[] = [];
+
+        if (missingVariables.length > 0) {
+            errors.push(`Missing required variables in task.data: ${missingVariables.join(', ')}`);
+        }
+
+        if (undefinedVariables.length > 0) {
+            errors.push(`Variables with undefined values: ${undefinedVariables.join(', ')}`);
+        }
+
+        if (errors.length > 0 && this.strictPromptValidation) {
+            throw new Error(`Prompt variable validation failed:\n${errors.join('\n')}`);
+        } else if (errors.length > 0) {
+            console.warn(`Prompt variable warnings:\n${errors.join('\n')}`);
+        }
+    }
+
+    extractPromptVariables(): string[] {
+        const matches = this.promptTemplate.match(/\{\{\w+\}\}/g) || [];
+        const uniqueVariables = new Set<string>();
+
+        matches.forEach(match => {
+            const key = match.replace(/\{\{|\}\}/g, '');
+            uniqueVariables.add(key);
+        });
+
+        return Array.from(uniqueVariables);
+    }
+
+    renderPromptTemplate(): string {
+        let result = this.promptTemplate;
+        const variables = this.extractPromptVariables();
+
+        variables.forEach(variable => {
+            if (this.data[variable] !== undefined) {
+                result = result.replace(new RegExp(`\\{\\{${variable}\\}\\}`, 'g'), this.data[variable]);
+            }
+        });
+
+        return result;
+    }
+
+    async callModelCompletion(prompt: string): Promise<void> {
+        return new Promise((resolve, reject) => {
+            const messages = [{
+                role: "user",
+                content: prompt
+            }];
+
+            const completion = new FmodeChatCompletion(messages);
+
+            let accumulatedContent = '';
+
+            completion.sendCompletion({
+                ...this.modelOptions,
+                onComplete: (message: any) => {
+                    console.log("onComplete", message);
+                }
+            })
+                .pipe(takeUntil(this.destroy$))
+                .subscribe({
+                    next: (message: any) => {
+                        if (message.content && typeof message.content === 'string') {
+                            accumulatedContent = message.content;
+                            this.setProgress(0.3 + (accumulatedContent.length / 1000) * 0.7);
+                        }
+
+                        if (message.complete) {
+                            try {
+                                // Store the complete generated text in the specified property
+                                this.updateData(this.outputProperty, accumulatedContent);
+                                this.setProgress(1);
+                                resolve();
+                            } catch (error) {
+                                this.handleError(error as Error);
+                                reject(error);
+                            }
+                        }
+                    },
+                    error: (error) => {
+                        this.handleError(error);
+                        reject(error);
+                    }
+                });
+        });
+    }
+
+    handleError(error: Error): void {
+        this.updateData('error', {
+            message: error.message,
+            stack: error.stack,
+            timestamp: new Date().toISOString()
+        });
+        this._status = 'failed';
+    }
+
+    onDestroy(): void {
+        this.destroy$.next();
+        this.destroy$.complete();
+    }
+}

+ 269 - 0
src/lib/tasks/task-user-form/README.md

@@ -0,0 +1,269 @@
+
+
+# 任务:TaskUserForm
+使用示例
+您可以像下面这样使用 TaskUserForm 类:
+
+```ts
+import { TaskUserForm } from './task-user-form.ts'; // 假设TaskUserForm在同一目录下
+
+async function collectUserInfo(modalCtrl: any) {
+  const fieldList = [
+    { type: "String", key: "name", name: "姓名", required: true },
+    { type: "String", key: "mobile", name: "手机", required: true },
+    // 注意options每个元素是{label,value}形式
+    { type: "Radio", key: "gender", name: "性别", required: false, options: [
+      { label: "男", value: "male" },
+      { label: "女", value: "female" }
+    ]}
+  ];
+
+  const task = new TaskUserForm({
+    title: '用户信息采集',
+    modalCtrl: this.modalCtrl, // 必备属性
+    fieldList: fieldList,
+    initialData: {}, // 可以传入初始数据
+  });
+
+  try {
+    await task.execute();
+    console.log('采集结果:', task.output);
+    return task.output;
+  } catch (error) {
+    console.error('表单采集出错:', error);
+    return null;
+  }
+}
+```
+说明
+构造函数: TaskUserForm 接受 modalCtrl 和 fieldList,并初始化输入输出校验规则。
+handle 方法: 在执行时调用 getUserForm 函数进行表单采集,并将结果保存到 data 中。
+类型映射: mapFieldType 方法将用户定义的字段类型映射为 FieldType 类型,以便进行校验。
+这样,您就可以使用 TaskUserForm 类来进行用户信息的表单采集任务,且具备完整的输入输出校验机制。
+
+
+# 组件:用户表单采集
+
+# 函数形式使用
+``` typescript
+import { getUserForm } from '../lib/tasks/task-user-form/get-user-form';
+
+async function collectUserInfo(modalCtrl: any) {
+  const fieldList = [
+    { type: "String", key: "name", name: "姓名", required: true },
+    { type: "String", key: "mobile", name: "手机", required: true },
+    { type: "Radio", key: "gender", name: "性别", required: false, options: [
+      { label: "男", value: "male" },
+      { label: "女", value: "female" }
+    ]}
+  ];
+
+  try {
+    const result = await getUserForm({ modalCtrl, fieldList });
+    console.log('Collected data:', result);
+    return result;
+  } catch (error) {
+    console.error('Form error:', error);
+    return null;
+  }
+}
+```
+
+# 细节用法说明
+以下是基于 `getUserForm` 函数的完整使用示例和所有支持的字段类型说明:
+
+### 1. 使用示例
+
+```typescript
+import { getUserForm } from '../lib/tasks/task-user-form/get-user-form';
+
+
+async function collectUserInfo(modalCtrl: any) {
+  // 定义表单字段配置
+  const fieldList = [
+    // 文本输入
+    { 
+      type: "String", 
+      key: "name", 
+      name: "姓名", 
+      required: true,
+      placeholder: "请输入真实姓名"
+    },
+    
+    // 数字输入
+    { 
+      type: "Number", 
+      key: "age", 
+      name: "年龄", 
+      required: false,
+      min: 0,
+      max: 120
+    },
+    
+    // 日期选择
+    { 
+      type: "Date", 
+      key: "birthday", 
+      name: "出生日期", 
+      required: true
+    },
+    
+    // 单选按钮
+    { 
+      type: "Radio", 
+      key: "gender", 
+      name: "性别", 
+      required: true,
+      options: [
+        { label: "男", value: "male" },
+        { label: "女", value: "female" },
+        { label: "其他", value: "other" }
+      ]
+    },
+    
+    // 下拉选择
+    { 
+      type: "Select", 
+      key: "education", 
+      name: "学历", 
+      required: false,
+      options: [
+        { label: "高中", value: "high_school" },
+        { label: "大专", value: "college" },
+        { label: "本科", value: "bachelor" },
+        { label: "硕士", value: "master" },
+        { label: "博士", value: "phd" }
+      ]
+    },
+    
+    // 多选框
+    { 
+      type: "Checkbox", 
+      key: "hobbies", 
+      name: "兴趣爱好", 
+      required: false,
+      options: [
+        { label: "阅读", value: "reading" },
+        { label: "运动", value: "sports" },
+        { label: "音乐", value: "music" },
+        { label: "旅行", value: "travel" }
+      ]
+    },
+    
+    // 开关
+    { 
+      type: "Boolean", 
+      key: "subscribe", 
+      name: "订阅通知", 
+      required: false
+    },
+    
+    // 多行文本
+    { 
+      type: "Textarea", 
+      key: "address", 
+      name: "详细地址", 
+      required: false,
+      placeholder: "请输入详细住址"
+    }
+  ];
+
+  try {
+    // 调用表单采集函数
+    const result = await getUserForm({
+      modalCtrl: modalCtrl,  // 传入Ionic的ModalController实例
+      fieldList: fieldList
+    });
+    
+    if (result) {
+      console.log('采集结果:', result);
+      // 结果示例:
+      // {
+      //   name: "张三",
+      //   age: 25,
+      //   birthday: "1998-05-20",
+      //   gender: "male",
+      //   education: "bachelor",
+      //   hobbies: ["reading", "sports"],
+      //   subscribe: true,
+      //   address: "北京市朝阳区..."
+      // }
+      return result;
+    } else {
+      console.log('用户取消了表单');
+      return null;
+    }
+  } catch (error) {
+    console.error('表单采集出错:', error);
+    throw error;
+  }
+}
+```
+
+### 2. 所有支持的字段类型及写法
+
+| 类型        | 描述                  | 必填属性               | 示例配置 |
+|------------|----------------------|----------------------|---------|
+| **String** | 文本输入              | `required`, `pattern`, `placeholder` | `{ type: "String", key: "name", name: "姓名", required: true, placeholder: "请输入姓名" }` |
+| **Number** | 数字输入              | `required`, `min`, `max` | `{ type: "Number", key: "age", name: "年龄", min: 0, max: 120 }` |
+| **Date**   | 日期选择              | `required`           | `{ type: "Date", key: "birthday", name: "出生日期", required: true }` |
+| **Radio**  | 单选按钮组            | `required`, `options` | `{ type: "Radio", key: "gender", name: "性别", options: [{label:"男",value:"male"}] }` |
+| **Select** | 下拉选择              | `required`, `options` | `{ type: "Select", key: "city", name: "城市", options: [{label:"北京",value:"beijing"}] }` |
+| **Checkbox** | 多选框组            | `required`, `options` | `{ type: "Checkbox", key: "hobbies", name: "爱好", options: [{label:"游泳",value:"swimming"}] }` |
+| **Boolean** | 开关                | -                    | `{ type: "Boolean", key: "agree", name: "同意协议" }` |
+| **Textarea** | 多行文本            | `required`, `placeholder` | `{ type: "Textarea", key: "feedback", name: "意见反馈", placeholder: "请输入您的意见" }` |
+
+### 3. 特殊字段属性说明
+
+1. **options** (用于 Radio/Select/Checkbox):
+   ```typescript
+   options: [
+     { label: "显示文本", value: "实际值" },
+     { label: "男", value: "male" }
+   ]
+   ```
+
+2. **验证规则**:
+   - `required: true` - 必填字段
+   - `pattern: "正则表达式"` - 文本格式验证
+   - `min: 数值` - 最小值(用于Number类型)
+   - `max: 数值` - 最大值(用于Number类型)
+
+3. **日期字段**:
+   - 会自动格式化为 ISO 字符串格式 (如 "2023-05-20")
+   - 可通过 `max` 和 `min` 限制日期范围
+
+4. **多选字段(Checkbox)**:
+   - 返回值为数组,包含所有选中的值
+   - 如: `hobbies: ["reading", "music"]`
+
+### 4. 完整调用流程示例
+
+```typescript
+// 在Ionic页面组件中的调用示例
+import { Component } from '@angular/core';
+import { ModalController } from '@ionic/angular';
+
+@Component({
+  selector: 'app-user-info',
+  templateUrl: './user-info.page.html',
+})
+export class UserInfoPage {
+  constructor(private modalCtrl: ModalController) {}
+
+  async openForm() {
+    try {
+      const userData = await collectUserInfo(this.modalCtrl);
+      if (userData) {
+        // 处理采集到的数据
+        console.log('收到用户数据:', userData);
+        // 提交到服务器等操作...
+      }
+    } catch (error) {
+      console.error('表单错误:', error);
+    }
+  }
+}
+```
+
+这个实现支持了常见的所有表单字段类型,并提供了完善的验证机制。您可以根据实际需求组合这些字段类型来构建复杂的表单。

+ 143 - 0
src/lib/tasks/task-user-form/form-collector/form-collector.component.html

@@ -0,0 +1,143 @@
+<ion-header>
+    <ion-toolbar>
+        <ion-title>信息采集</ion-title>
+        <ion-buttons slot="end">
+            <ion-button (click)="onCancel()">取消</ion-button>
+        </ion-buttons>
+    </ion-toolbar>
+</ion-header>
+
+<ion-content>
+    <form [formGroup]="form" (ngSubmit)="onSubmit()">
+        <ion-list>
+            <ng-container *ngFor="let field of fieldList">
+                <!-- 文本输入 -->
+                <ion-item *ngIf="field.type === 'String'" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-input [formControlName]="field.key" type="text" [placeholder]="field.placeholder || ''">
+                    </ion-input>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请输入{{field.name}}
+                    </ion-note>
+                </ion-item>
+
+                <!-- 数字输入 -->
+                <ion-item *ngIf="field.type === 'Number'" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-input [formControlName]="field.key" type="number" [placeholder]="field.placeholder || ''">
+                    </ion-input>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请输入{{field.name}}
+                    </ion-note>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('min') && getFieldControl(field.key)?.touched">
+                        最小值不能小于{{field.min}}
+                    </ion-note>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('max') && getFieldControl(field.key)?.touched">
+                        最大值不能超过{{field.max}}
+                    </ion-note>
+                </ion-item>
+
+                <!-- 日期选择 -->
+                <ion-item *ngIf="field.type === 'Date'" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-datetime [formControlName]="field.key" displayFormat="YYYY-MM-DD" [max]="today">
+                    </ion-datetime>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请选择{{field.name}}
+                    </ion-note>
+                </ion-item>
+
+                <!-- 单选按钮 -->
+                <ion-item *ngIf="field.type === 'Radio' && field.options" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-radio-group [formControlName]="field.key">
+                        <ion-item *ngFor="let option of field.options" lines="none">
+                            <ion-label>{{option.label}}</ion-label>
+                            <ion-radio slot="start" [value]="option.value"></ion-radio>
+                        </ion-item>
+                    </ion-radio-group>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请选择{{field.name}}
+                    </ion-note>
+                </ion-item>
+
+                <!-- 下拉选择 -->
+                <ion-item *ngIf="field.type === 'Select' && field.options" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-select [formControlName]="field.key" interface="action-sheet">
+                        <ion-select-option *ngFor="let option of field.options" [value]="option.value">
+                            {{option.label}}
+                        </ion-select-option>
+                    </ion-select>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请选择{{field.name}}
+                    </ion-note>
+                </ion-item>
+
+                <!-- 多选框 -->
+                <ion-item *ngIf="field.type === 'Checkbox' && field.options" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-checkbox-group [formControlName]="field.key">
+                        <ion-item *ngFor="let option of field.options" lines="none">
+                            <ion-label>{{option.label}}</ion-label>
+                            <ion-checkbox slot="start" [value]="option.value"></ion-checkbox>
+                        </ion-item>
+                    </ion-checkbox-group>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请至少选择一项{{field.name}}
+                    </ion-note>
+                </ion-item>
+
+                <!-- 开关 -->
+                <ion-item *ngIf="field.type === 'Boolean'" lines="full">
+                    <ion-label>{{field.name}}</ion-label>
+                    <ion-toggle [formControlName]="field.key"></ion-toggle>
+                </ion-item>
+
+                <!-- 文本域 -->
+                <ion-item *ngIf="field.type === 'Textarea'||field.type === 'TextArea'" lines="full">
+                    <ion-label position="stacked">
+                        {{field.name}}
+                        <span *ngIf="field.required" class="required">*</span>
+                    </ion-label>
+                    <ion-textarea [formControlName]="field.key" autoGrow="true" rows="4">
+                    </ion-textarea>
+                    <ion-note slot="error"
+                        *ngIf="getFieldControl(field.key)?.hasError('required') && getFieldControl(field.key)?.touched">
+                        请输入{{field.name}}
+                    </ion-note>
+                </ion-item>
+            </ng-container>
+        </ion-list>
+
+        <ion-button type="submit" expand="block" class="ion-margin" [disabled]="!form.valid">
+            提交
+        </ion-button>
+    </form>
+</ion-content>

+ 17 - 0
src/lib/tasks/task-user-form/form-collector/form-collector.component.scss

@@ -0,0 +1,17 @@
+.required {
+    color: var(--ion-color-danger);
+}
+
+ion-item {
+    --padding-start: 0;
+}
+
+ion-note[slot="error"] {
+    color: var(--ion-color-danger);
+    font-size: 14px;
+}
+
+ion-checkbox-group,
+ion-radio-group {
+    width: 100%;
+}

+ 94 - 0
src/lib/tasks/task-user-form/form-collector/form-collector.component.ts

@@ -0,0 +1,94 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { ModalController, IonicModule } from '@ionic/angular';
+import { FormBuilder, FormGroup, Validators, FormArray, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+
+export interface FormField {
+    type: string;
+    key: string;
+    name: string;
+    required?: boolean;
+    options?: { label: string; value: any }[];
+    min?: number;
+    max?: number;
+    pattern?: string;
+    placeholder?: string;
+    default?: any;
+}
+
+@Component({
+    selector: 'app-form-collector',
+    standalone: true,
+    imports: [IonicModule, CommonModule, FormsModule, ReactiveFormsModule],
+    templateUrl: './form-collector.component.html',
+    styleUrls: ['./form-collector.component.scss'],
+})
+export class FormCollectorComponent {
+    @Input() fieldList: FormField[] = [];
+    @Output() formSubmit = new EventEmitter<any>();
+
+    form: FormGroup;
+    today = new Date().toISOString();
+
+    constructor(
+        private fb: FormBuilder,
+        private modalCtrl: ModalController
+    ) {
+        this.form = this.fb.group({});
+    }
+
+    ngOnInit() {
+        this.createForm();
+    }
+
+    createForm() {
+        const group: any = {};
+
+        this.fieldList.forEach(field => {
+            const validators = [];
+            if (field.required) {
+                validators.push(Validators.required);
+            }
+            if (field.pattern) {
+                validators.push(Validators.pattern(field.pattern));
+            }
+            if (field.min !== undefined) {
+                validators.push(Validators.min(field.min));
+            }
+            if (field.max !== undefined) {
+                validators.push(Validators.max(field.max));
+            }
+
+            group[field.key] = [null, validators];
+        });
+
+        this.form = this.fb.group(group);
+    }
+
+    onSubmit() {
+        if (this.form.valid) {
+            this.formSubmit.emit(this.form.value);
+            this.modalCtrl.dismiss(this.form.value);
+        } else {
+            this.markFormGroupTouched(this.form);
+        }
+    }
+
+    onCancel() {
+        this.modalCtrl.dismiss();
+    }
+
+    private markFormGroupTouched(formGroup: FormGroup | FormArray) {
+        Object.values(formGroup.controls).forEach(control => {
+            control.markAsTouched();
+
+            if (control instanceof FormGroup || control instanceof FormArray) {
+                this.markFormGroupTouched(control);
+            }
+        });
+    }
+
+    getFieldControl(key: string): FormControl {
+        return this.form.get(key) as FormControl;
+    }
+}

+ 59 - 0
src/lib/tasks/task-user-form/get-user-form.ts

@@ -0,0 +1,59 @@
+import { FormCollectorComponent } from './form-collector/form-collector.component';
+
+export interface FormField {
+    type: string;
+    key: string;
+    name: string;
+    required?: boolean;
+    options?: { label: string; value: any }[];
+    min?: number;
+    max?: number;
+    pattern?: string;
+    placeholder?: string;
+    validator?: any;
+    default?: any;
+}
+
+/**
+ * 表单采集函数
+ * @param params.modalCtrl Ionic ModalController 实例
+ * @param params.fieldList 表单字段定义
+ * @returns Promise 解析用户提交的数据,如果取消则返回 undefined
+ */
+export async function getUserForm(params: {
+    modalCtrl: any; // Ionic ModalController 类型
+    fieldList: FormField[];
+}): Promise<Record<string, any> | undefined> {
+    const { modalCtrl, fieldList } = params;
+
+    if (!modalCtrl || typeof modalCtrl.create !== 'function') {
+        throw new Error('modalCtrl parameter must be an instance of Ionic ModalController');
+    }
+
+    const modal = await modalCtrl.create({
+        component: FormCollectorComponent,
+        componentProps: {
+            fieldList
+        }
+    });
+
+    await modal.present();
+
+    const { data } = await modal.onWillDismiss();
+    return data;
+}
+
+/**
+ * 表单采集类形式
+ */
+export class FormCollector {
+    constructor(private modalCtrl: any) {
+        if (!modalCtrl || typeof modalCtrl.create !== 'function') {
+            throw new Error('modalCtrl parameter must be an instance of Ionic ModalController');
+        }
+    }
+
+    async getForm(fieldList: FormField[]): Promise<Record<string, any> | undefined> {
+        return getUserForm({ modalCtrl: this.modalCtrl, fieldList });
+    }
+}

+ 75 - 0
src/lib/tasks/task-user-form/task-user-form.ts

@@ -0,0 +1,75 @@
+import { FlowTask, FlowTaskOptions, FieldSchema } from '../../flow.task'; // 假设FlowTask在同一目录下
+import { getUserForm, FormField } from './get-user-form';
+
+interface TaskUserFormOptions extends FlowTaskOptions {
+    modalCtrl: any; // ModalController 类型
+    fieldList: FormField[]; // 表单字段列表
+}
+
+export class TaskUserForm extends FlowTask {
+    private modalCtrl: any;
+    private fieldList: FormField[];
+
+    constructor(options: TaskUserFormOptions) {
+        super(options);
+        this.modalCtrl = options.modalCtrl;
+        this.fieldList = options.fieldList;
+        // this.inputSchema = this.createInputSchema(); // 创建输入校验规则 此刻用户没有任何输入
+        // this.outputSchema = this.createOutputSchema(); // 创建输出校验规则
+    }
+
+    override async handle(): Promise<void> {
+        console.log("handle", this.modalCtrl, this.fieldList)
+        const result = await getUserForm({
+            modalCtrl: this.modalCtrl,
+            fieldList: this.fieldList,
+        });
+
+        if (result) {
+            console.log(result)
+            // 将表单结果保存到任务数据中
+            this.data = { ...this.data, ...result };
+        } else {
+            throw new Error('用户取消了表单');
+        }
+    }
+
+    private createInputSchema(): FieldSchema[] {
+        return this.fieldList.map(field => ({
+            name: field.key,
+            type: this.mapFieldType(field.type),
+            required: field.required,
+            description: field.name,
+        }));
+    }
+
+    private createOutputSchema(): FieldSchema[] {
+        return this.fieldList.map(field => ({
+            name: field.key,
+            type: this.mapFieldType(field.type),
+            required: field.required,
+            description: field.name,
+        }));
+    }
+
+    private mapFieldType(type: string): any {
+        switch (type.toLowerCase()) {
+            case 'string':
+                return 'string';
+            case 'number':
+                return 'number';
+            case 'boolean':
+                return 'boolean';
+            case 'date':
+                return 'object'; // 日期类型通常处理为对象
+            case 'radio':
+            case 'select':
+            case 'checkbox':
+                return 'array'; // 多选和单选可以处理为数组
+            case 'textarea':
+                return 'string'; // 多行文本处理为字符串
+            default:
+                throw new Error(`不支持的字段类型: ${type}`);
+        }
+    }
+}

+ 29 - 0
src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -0,0 +1,29 @@
+<!-- 用户登录状态 -->
+<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-item>
+      <ion-input [value]="userData['avatar']" (ionChange)="userDataChange('avatar',$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
src/lib/user/modal-user-edit/modal-user-edit.component.scss


+ 22 - 0
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
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
+}

+ 36 - 0
src/lib/user/modal-user-login/modal-user-login.component.html

@@ -0,0 +1,36 @@
+<!-- 用户登录状态 -->
+<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
src/lib/user/modal-user-login/modal-user-login.component.scss


+ 22 - 0
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
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
+}

+ 240 - 0
src/lib/user/token-guard/token.guard.ts

@@ -0,0 +1,240 @@
+/**
+@desc
+请您帮我设计一个ionic/angular项目的TokenGuard路由守卫,检查localStorage是否有token值。若不存在,通过dom构建ui交互提示用户填写token(不使用modal和angular逻辑)。若存在token或填写后,则调用接口:
+curl -X GET \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
+  https://YOUR.PARSE-SERVER.HERE/parse/users/me 
+可用fetch请求验证token是否正常。
+若不正常,提示错误请重新填写直到填写了有效token。
+若正常则返回true。
+接口地址为https://server.fmode.cn/parse 应用id为 ncloudmaster。
+
+@example 路由守卫使用
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { TokenGuard } from './guards/token.guard';
+
+const routes: Routes = [
+  {
+    path: 'protected',
+    loadChildren: () => import('./protected/protected.module').then(m => m.ProtectedPageModule),
+    canActivate: [TokenGuard]
+  },
+  // 其他路由...
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes)],
+  exports: [RouterModule]
+})
+export class AppRoutingModule {}
+
+ */
+
+// src/app/guards/token.guard.ts
+import { Injectable } from '@angular/core';
+import { CanActivate, Router } from '@angular/router';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TokenGuard implements CanActivate {
+  private readonly PARSE_SERVER_URL = 'https://server.fmode.cn/parse';
+  private readonly APPLICATION_ID = 'ncloudmaster';
+
+  constructor(
+    private http: HttpClient,
+    private router: Router
+  ) {}
+
+  async canActivate(): Promise<boolean> {
+    let token = localStorage.getItem('parseSessionToken');
+
+    if (!token) {
+      token = await this.showTokenPrompt();
+      if (!token) {
+        this.router.navigate(['/login']);
+        return false;
+      }
+    }
+
+    const isValid = await this.validateToken(token);
+    if (!isValid) {
+      localStorage.removeItem('parseSessionToken');
+      return this.canActivate();
+    }
+
+    localStorage.setItem('parseSessionToken', token);
+    return true;
+  }
+
+  private async validateToken(token: string): Promise<boolean> {
+    const headers = new HttpHeaders({
+      'X-Parse-Application-Id': this.APPLICATION_ID,
+      'X-Parse-Session-Token': token
+    });
+
+    try {
+      const response: any = await this.http.get(
+        `${this.PARSE_SERVER_URL}/users/me`,
+        { headers }
+      ).toPromise();
+      return !!response?.objectId;
+    } catch (error) {
+      return false;
+    }
+  }
+
+  private showTokenPrompt(): Promise<string | null> {
+    return new Promise((resolve) => {
+      // 创建遮罩层
+      const overlay = document.createElement('div');
+      overlay.style.position = 'fixed';
+      overlay.style.top = '0';
+      overlay.style.left = '0';
+      overlay.style.width = '100%';
+      overlay.style.height = '100%';
+      overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
+      overlay.style.display = 'flex';
+      overlay.style.justifyContent = 'center';
+      overlay.style.alignItems = 'center';
+      overlay.style.zIndex = '1000';
+
+      // 创建对话框
+      const dialog = document.createElement('div');
+      dialog.style.backgroundColor = 'white';
+      dialog.style.padding = '20px';
+      dialog.style.borderRadius = '8px';
+      dialog.style.width = '80%';
+      dialog.style.maxWidth = '400px';
+
+      // 创建标题
+      const title = document.createElement('h3');
+      title.textContent = '请输入 Token';
+      title.style.marginTop = '0';
+      title.style.color = "black";
+      dialog.appendChild(title);
+
+      // 创建描述
+      // 使用以下指令获取token
+      
+      const descEl:HTMLElement = document.createElement("div");
+      descEl.innerHTML = `获取token方法:<br>
+      1.登录<a href="https://ai.fmode.cn" target="_blank">https://ai.fmode.cn</a><br>
+      2.按F12进入调试——打开控制台Console<br>
+      3.输入指令:<br>
+      <span style="color:blue;">JSON.parse(localStorage.getItem("Parse/ncloudmaster/currentUser"))?.sessionToken</span><br>
+      4.复制字符串内容,形如:<br>
+      <span style="color:red">r:xxxxxxxxxxxxx</span>
+      `
+      descEl.style.color = "black"
+      dialog.appendChild(descEl);
+
+      // 创建错误消息容器
+      const errorMsg = document.createElement('div');
+      errorMsg.style.color = 'red';
+      errorMsg.style.minHeight = '20px';
+      errorMsg.style.margin = '10px 0';
+      dialog.appendChild(errorMsg);
+
+      // 创建输入框
+      const input = document.createElement('input');
+      input.type = 'text';
+      input.placeholder = '请输入您的 Token';
+      input.style.width = '100%';
+      input.style.padding = '10px';
+      input.style.marginBottom = '15px';
+      input.style.boxSizing = 'border-box';
+      dialog.appendChild(input);
+
+      // 创建按钮容器
+      const buttonContainer = document.createElement('div');
+      buttonContainer.style.display = 'flex';
+      buttonContainer.style.justifyContent = 'flex-end';
+      buttonContainer.style.gap = '10px';
+
+      // 创建提交按钮
+      const submitBtn = document.createElement('button');
+      submitBtn.textContent = '提交';
+      submitBtn.style.padding = '8px 16px';
+      submitBtn.style.backgroundColor = '#3880ff';
+      submitBtn.style.color = 'white';
+      submitBtn.style.border = 'none';
+      submitBtn.style.borderRadius = '4px';
+      submitBtn.disabled = true;
+
+      // 创建取消按钮
+      const cancelBtn = document.createElement('button');
+      cancelBtn.textContent = '取消';
+      cancelBtn.style.padding = '8px 16px';
+      cancelBtn.style.backgroundColor = '#eb445a';
+      cancelBtn.style.color = 'white';
+      cancelBtn.style.border = 'none';
+      cancelBtn.style.borderRadius = '4px';
+
+      // 添加按钮到容器
+      buttonContainer.appendChild(cancelBtn);
+      buttonContainer.appendChild(submitBtn);
+      dialog.appendChild(buttonContainer);
+
+      // 添加到遮罩层
+      overlay.appendChild(dialog);
+      document.body.appendChild(overlay);
+
+      // 自动聚焦输入框
+      input.focus();
+
+      // 输入验证
+      input.addEventListener('input', () => {
+        submitBtn.disabled = !input.value.trim();
+      });
+
+      // 取消按钮事件
+      const cleanup = () => {
+        document.body.removeChild(overlay);
+      };
+
+      cancelBtn.addEventListener('click', () => {
+        cleanup();
+        resolve(null);
+      });
+
+      // 提交按钮事件
+      submitBtn.addEventListener('click', async () => {
+        const token = input.value.trim();
+        if (!token) return;
+
+        const isValid = await this.validateToken(token);
+        if (isValid) {
+          cleanup();
+          resolve(token);
+        } else {
+          errorMsg.textContent = 'Token 无效,请重新输入';
+          input.value = '';
+          submitBtn.disabled = true;
+          input.focus();
+        }
+      });
+
+      // 回车键提交
+      input.addEventListener('keypress', async (e) => {
+        if (e.key === 'Enter' && input.value.trim()) {
+          const token = input.value.trim();
+          const isValid = await this.validateToken(token);
+          
+          if (isValid) {
+            cleanup();
+            resolve(token);
+          } else {
+            errorMsg.textContent = 'Token 无效,请重新输入';
+            input.value = '';
+            submitBtn.disabled = true;
+            input.focus();
+          }
+        }
+      });
+    });
+  }
+}