Просмотр исходного кода

feat: task-user-form for user input collector

未来全栈 3 дней назад
Родитель
Сommit
1f41200f0e

+ 41 - 14
package-lock.json

@@ -9,10 +9,12 @@
       "version": "0.0.1",
       "dependencies": {
         "@angular/animations": "^18.0.0",
+        "@angular/cdk": "^18.2.14",
         "@angular/common": "^18.0.0",
         "@angular/compiler": "^18.0.0",
         "@angular/core": "^18.0.0",
         "@angular/forms": "^18.0.0",
+        "@angular/material": "^18.2.14",
         "@angular/platform-browser": "^18.0.0",
         "@angular/platform-browser-dynamic": "^18.0.0",
         "@angular/router": "^18.0.0",
@@ -35,8 +37,7 @@
         "@tensorflow/tfjs-core": "^4.22.0",
         "@types/pdf-parse": "^1.1.4",
         "@vladmandic/face-api": "^1.7.14",
-        "fmode-ng": "^0.0.63",
-        "ionicons": "^7.2.1",
+        "fmode-ng": "^0.0.82",
         "langchain": "^0.3.7",
         "leafer": "^1.6.1",
         "leafer-x-connector": "^0.1.3",
@@ -527,7 +528,6 @@
       "resolved": "https://registry.npmmirror.com/@angular/cdk/-/cdk-18.2.14.tgz",
       "integrity": "sha512-vDyOh1lwjfVk9OqoroZAP8pf3xxKUvyl+TVR8nJxL4c5fOfUFkD7l94HaanqKSRwJcI2xiztuu92IVoHn8T33Q==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "tslib": "^2.3.0"
       },
@@ -713,6 +713,24 @@
         "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
       }
     },
+    "node_modules/@angular/material": {
+      "version": "18.2.14",
+      "resolved": "https://registry.npmmirror.com/@angular/material/-/material-18.2.14.tgz",
+      "integrity": "sha512-28pxzJP49Mymt664WnCtPkKeg7kXUsQKTKGf/Kl95rNTEdTJLbnlcc8wV0rT0yQNR7kXgpfBnG7h0ETLv/iu5Q==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/animations": "^18.0.0 || ^19.0.0",
+        "@angular/cdk": "18.2.14",
+        "@angular/common": "^18.0.0 || ^19.0.0",
+        "@angular/core": "^18.0.0 || ^19.0.0",
+        "@angular/forms": "^18.0.0 || ^19.0.0",
+        "@angular/platform-browser": "^18.0.0 || ^19.0.0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
     "node_modules/@angular/platform-browser": {
       "version": "18.2.13",
       "resolved": "https://registry.npmmirror.com/@angular/platform-browser/-/platform-browser-18.2.13.tgz",
@@ -2823,9 +2841,9 @@
       }
     },
     "node_modules/@babylonjs/core": {
-      "version": "7.37.0",
-      "resolved": "https://registry.npmmirror.com/@babylonjs/core/-/core-7.37.0.tgz",
-      "integrity": "sha512-BwwZqRr35V9rasMoApBn7TDubulzL9g6+96HhEqYiRQYqJ5arZTEdhanmYgNDy5zem4zF/k4srCi7HmWH212Aw==",
+      "version": "7.2.3",
+      "resolved": "https://registry.npmmirror.com/@babylonjs/core/-/core-7.2.3.tgz",
+      "integrity": "sha512-SzNVgkSJi4hErSL3+VmAJIoUrN4pc2BNVHU3DZqR7ZxzwmhhaF+c5tO9CQIi4loS3PdvjEdSUY5K5dnPhxQXfQ==",
       "license": "Apache-2.0",
       "peer": true
     },
@@ -9655,6 +9673,13 @@
       "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==",
       "license": "BSD-3-Clause"
     },
+    "node_modules/@wecom/jssdk": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/@wecom/jssdk/-/jssdk-2.3.1.tgz",
+      "integrity": "sha512-9XxeY/kljYZF1tKk9v0ZOR/Amz2Y8cxrmZTEBTN/Zqb6WsbpHiDPOWbvpvUBrTsiCW4w7nDnvYYi01ZsOUcUYQ==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/@xmldom/xmldom": {
       "version": "0.8.10",
       "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -13619,9 +13644,9 @@
       "license": "ISC"
     },
     "node_modules/fmode-ng": {
-      "version": "0.0.63",
-      "resolved": "https://registry.npmmirror.com/fmode-ng/-/fmode-ng-0.0.63.tgz",
-      "integrity": "sha512-gTiDZO2CchcTYAmlaweapasqV/8PdhG2vizJNn5dYZyXjgtrjyW+KeW5k2EVyIDvM1+bMGjjhGmr76Fc0TElxw==",
+      "version": "0.0.82",
+      "resolved": "https://registry.npmmirror.com/fmode-ng/-/fmode-ng-0.0.82.tgz",
+      "integrity": "sha512-e+tcSQMR32QPQsRt+UyEb0APNd/isuDEOn3JleJ0R5/7n9BRhKdiNxr/uXR/BGUWRbpPR4ZArPP0TRSmibKsCA==",
       "license": "COPYRIGHT © 未来飞马 未来全栈 www.fmode.cn All RIGHTS RESERVED",
       "dependencies": {
         "tslib": "^2.3.0"
@@ -13632,17 +13657,19 @@
         "@angular/common": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
         "@angular/core": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
         "@angular/forms": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
-        "@awesome-cordova-plugins/diagnostic": "~6.6.0",
+        "@angular/material": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "@awesome-cordova-plugins/diagnostic": "^5.0.0 || ^6.0.0",
         "@awesome-cordova-plugins/media-capture": "^5.0.0 || ^6.0.0 || ^7.0.0",
-        "@babylonjs/core": "^7.2.1",
-        "@babylonjs/loaders": "~7.2.1",
+        "@babylonjs/core": "7.2.3",
+        "@babylonjs/loaders": "7.2.3",
         "@capacitor/camera": "^5.0.0 || ^6.0.0 || ^7.0.0",
-        "@capacitor/clipboard": "^6.0.0 || ^7.0.0",
+        "@capacitor/clipboard": "^5.0.0 || ^6.0.0 || ^7.0.0",
         "@capacitor/filesystem": "^5.0.0 || ^6.0.0 || ^7.0.0",
         "@ionic/angular": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
         "@langchain/core": "^0.3.0 || ^1.0.0",
         "@types/parse": "^3.0.9",
         "@types/spark-md5": "^3.0.4",
+        "@wecom/jssdk": "^2.2.4",
         "esdk-obs-browserjs": "^3.23.5",
         "highlight.js": "^11.0.0",
         "jquery": "^3.7.1",
@@ -13663,7 +13690,7 @@
         "ng-zorro-antd": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
         "parse": "^5.0.0",
         "plantuml-encoder": "^1.4.0",
-        "qiniu-js": "^3.4.1",
+        "qiniu-js": "^3.0.0 || ^2.0.0",
         "recorder-core": "^1.2.23070100",
         "spark-md5": "^3.0.2"
       }

+ 3 - 2
package.json

@@ -14,10 +14,12 @@
   "private": true,
   "dependencies": {
     "@angular/animations": "^18.0.0",
+    "@angular/cdk": "^18.2.14",
     "@angular/common": "^18.0.0",
     "@angular/compiler": "^18.0.0",
     "@angular/core": "^18.0.0",
     "@angular/forms": "^18.0.0",
+    "@angular/material": "^18.2.14",
     "@angular/platform-browser": "^18.0.0",
     "@angular/platform-browser-dynamic": "^18.0.0",
     "@angular/router": "^18.0.0",
@@ -40,8 +42,7 @@
     "@tensorflow/tfjs-core": "^4.22.0",
     "@types/pdf-parse": "^1.1.4",
     "@vladmandic/face-api": "^1.7.14",
-    "fmode-ng": "^0.0.63",
-    "ionicons": "^7.2.1",
+    "fmode-ng": "^0.0.82",
     "langchain": "^0.3.7",
     "leafer": "^1.6.1",
     "leafer-x-connector": "^0.1.3",

+ 11 - 5
src/modules/flow/lib/flow.executor.ts

@@ -19,7 +19,7 @@ export class FlowExecutor {
   constructor(
     public maxRetries = 3,
     public autoRetry = false
-  ) {}
+  ) { }
 
   setWorkflow(workflow: FlowWorkflow) {
     this.workflow = workflow;
@@ -61,11 +61,17 @@ export class FlowExecutor {
     try {
       this.taskStart$.next(task);
       await task.execute();
-      this.taskSuccess$.next(task);
-      this.currentTaskIndex++;
-      this.retryCount = 0;
-      await this.executeNextTask();
+
+      // 只有当任务状态是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) {

+ 14 - 14
src/modules/flow/lib/flow.task.ts

@@ -1,13 +1,13 @@
 type FieldType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
 
-interface FieldSchema {
+export interface FieldSchema {
   name: string;
   type: FieldType;
   description?: string;
   required?: boolean;
 }
 
-interface FlowTaskOptions {
+export interface FlowTaskOptions {
   title: string;
   input?: FieldSchema[];
   output?: FieldSchema[];
@@ -16,18 +16,18 @@ interface FlowTaskOptions {
 
 export class FlowTask {
   // 核心属性
-  readonly title: string;
+  title: string;
   protected data: Record<string, any> = {};
-  private _status: 'idle' | 'running' | 'success' | 'failed' = 'idle';
-  private _progress: number = 0;
+  _status: 'idle' | 'running' | 'success' | 'failed' = 'idle';
+  _progress: number = 0;
 
   // 校验规则
-  private readonly inputSchema: FieldSchema[];
-  private readonly outputSchema: FieldSchema[];
+  inputSchema: FieldSchema[];
+  outputSchema: FieldSchema[];
 
   // 添加执行时间记录
-  private _startTime?: Date;
-  private _endTime?: Date;
+  _startTime?: Date;
+  _endTime?: Date;
   // 添加执行时间信息
   get executionTime(): number {
     if (!this._startTime) return 0;
@@ -81,11 +81,11 @@ export class FlowTask {
   }
 
   // 生命周期钩子(可选覆盖)
-  protected beforeExecute(): void {}
-  protected afterExecute(): void {}
-  protected onProgress(progress: number): void {}
-  protected onSuccess(): void {}
-  protected onFailure(error: Error): void {}
+  protected beforeExecute(): void { }
+  protected afterExecute(): void { }
+  protected onProgress(progress: number): void { }
+  protected onSuccess(): void { }
+  protected onFailure(error: Error): void { }
 
   /************************************
    *          数据校验系统              *

+ 268 - 0
src/modules/flow/lib/tasks/task-user-form/README.md

@@ -0,0 +1,268 @@
+
+
+# 任务: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 },
+    { type: "Radio", key: "gender", name: "性别", required: false, options: [
+      { label: "男", value: "male" },
+      { label: "女", value: "female" }
+    ]}
+  ];
+
+  const task = new TaskUserForm({
+    title: '用户信息采集',
+    modalCtrl: 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/modules/flow/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'" 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/modules/flow/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%;
+}

+ 93 - 0
src/modules/flow/lib/tasks/task-user-form/form-collector/form-collector.component.ts

@@ -0,0 +1,93 @@
+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;
+}
+
+@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;
+    }
+}

+ 58 - 0
src/modules/flow/lib/tasks/task-user-form/get-user-form.ts

@@ -0,0 +1,58 @@
+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;
+}
+
+/**
+ * 表单采集函数
+ * @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/modules/flow/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) {
+            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}`);
+        }
+    }
+}

+ 28 - 23
src/modules/flow/page-flow-test/consult-tasks/consult-tasks.ts

@@ -10,10 +10,10 @@ import { SymptomInputModalComponent } from './symptom-input/symptom-input.modal'
 import { inject } from '@angular/core';
 
 export class TriageTask extends FlowTask {
-  private modalCtrl:ModalController;
+  private modalCtrl: ModalController;
   private destroy$ = new Subject<void>();
 
-  constructor(options:{modalCtrl:ModalController}) {
+  constructor(options: { modalCtrl: ModalController }) {
     super({
       title: '智能分诊',
       output: [
@@ -62,34 +62,38 @@ export class TriageTask extends FlowTask {
           {
             "department": "科室名称",
             "reason": "分诊理由(50字以内)"
-          }`+`患者主诉:${symptom}`
+          }`+ `患者主诉:${symptom}`
         }
       ];
 
-      let fullResponse = '';
-      const completion = new FmodeChatCompletion(prompt);
+      let completion = new FmodeChatCompletion(prompt);
 
-      completion.sendCompletion()
+      completion.sendCompletion({
+        onComplete: (message: any) => {
+          console.log("onComplete", message)
+        }
+      })
         // .pipe(takeUntil(this.destroy$))
-        .subscribe((message) => {
-            // 处理流式响应
-            if (message.content && typeof message.content === 'string') {
-              fullResponse += message.content;
-              this.setProgress(0.5 + (fullResponse.length / 200) * 0.5); // 模拟进度
-            }
+        .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)
-            if(message.complete == true){
-              try {
-                const result = this.parseTriageResponse(fullResponse);
-                this.updateData('department', result.department)
-                   .updateData('triageReason', result.reason);
-                this.setProgress(1);
-                resolve();
-              } catch (e) {
-                reject(e);
-              }
+            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);
             }
-          });
+          }
+        });
     });
   }
 
@@ -100,6 +104,7 @@ export class TriageTask extends FlowTask {
       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('缺少科室信息');
 

+ 59 - 0
src/modules/flow/page-flow-test/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/modules/flow/page-flow-test/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: "请补充其他兴趣..." }
+            ]
+        });
+    }
+}

+ 3 - 1
src/modules/flow/page-flow-test/page-flow-test.component.html

@@ -1,2 +1,4 @@
 <ion-button (click)="startTriage()">开始分诊</ion-button>
-<ion-button (click)="startWorkflow()">启动工作流</ion-button>
+<ion-button (click)="getForm()">获取表单</ion-button>
+<ion-button (click)="jobWorkflow()">求职表单</ion-button>
+<ion-button (click)="startWorkflow()">启动工作流</ion-button>

+ 51 - 5
src/modules/flow/page-flow-test/page-flow-test.component.ts

@@ -7,12 +7,14 @@ import { FlowDisplayComponent } from '../lib/flow-display/flow-display.component
 import { TriageResultComponent } from './consult-tasks/triage-result/triage-result.component';
 import { TriageTask } from './consult-tasks/consult-tasks';
 import { SymptomInputModalComponent } from './consult-tasks/symptom-input/symptom-input.modal';
+import { getUserForm } from '../lib/tasks/task-user-form/get-user-form';
+import { JobIntentionTask, UserBasicInfoTask, UserInterestsTask } from './job-workflow/task.job';
 
 @Component({
   selector: 'app-page-flow-test',
   templateUrl: './page-flow-test.component.html',
   styleUrls: ['./page-flow-test.component.scss'],
-  imports:[IonButton,FlowDisplayComponent,
+  imports: [IonButton, FlowDisplayComponent,
     TriageResultComponent,
     SymptomInputModalComponent
   ],
@@ -25,6 +27,53 @@ export class PageFlowTestComponent {
   constructor(
     private modalCtrl: ModalController
   ) {
+
+  }
+
+  async getForm() {
+    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: this.modalCtrl, fieldList });
+      console.log('Collected data:', result);
+      return result;
+    } catch (error) {
+      console.error('Form error:', error);
+      return null;
+    }
+  }
+
+  async jobWorkflow() {
+    // 创建工作流
+    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();
+    await this.executor.start();
+  }
+  async startWorkflow() {
     // 创建工作流
     const workflow = {
       title: '示例工作流',
@@ -37,9 +86,6 @@ export class PageFlowTestComponent {
     };
 
     this.executor.setWorkflow(workflow);
-  }
-
-  async startWorkflow() {
     const modal = await this.modalCtrl.create({
       component: FlowDisplayComponent,
       componentProps: { executor: this.executor }
@@ -53,7 +99,7 @@ export class PageFlowTestComponent {
   async startTriage() {
     this.executor.setWorkflow({
       title: '医疗分诊流程',
-      taskList: [new TriageTask({modalCtrl:this.modalCtrl})]
+      taskList: [new TriageTask({ modalCtrl: this.modalCtrl })]
     });
 
     this.executor.taskSuccess$.subscribe(task => {