From ef3e34b85dda7fe0715d66d1a0215743b36b41ec Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 25 Oct 2025 00:20:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A1=E6=89=B9=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FormDesigner/FORM_PREVIEW_GUIDE.md | 361 +++++++++++++ .../FormDesigner/LIFECYCLE_HOOKS.md | 508 ++++++++++++++++++ .../src/components/FormDesigner/Renderer.tsx | 391 ++++++++------ .../src/components/FormDesigner/index.tsx | 6 +- .../src/domain/dataSource/presets/README.md | 138 +++++ frontend/src/pages/FormDesigner/index.tsx | 185 ++++++- .../components/FormPreviewModal.README.md | 128 +++++ 7 files changed, 1542 insertions(+), 175 deletions(-) create mode 100644 frontend/src/components/FormDesigner/FORM_PREVIEW_GUIDE.md create mode 100644 frontend/src/components/FormDesigner/LIFECYCLE_HOOKS.md create mode 100644 frontend/src/domain/dataSource/presets/README.md create mode 100644 frontend/src/pages/Workflow/Design/components/FormPreviewModal.README.md diff --git a/frontend/src/components/FormDesigner/FORM_PREVIEW_GUIDE.md b/frontend/src/components/FormDesigner/FORM_PREVIEW_GUIDE.md new file mode 100644 index 00000000..7de42c79 --- /dev/null +++ b/frontend/src/components/FormDesigner/FORM_PREVIEW_GUIDE.md @@ -0,0 +1,361 @@ +# FormPreview 使用指南 + +## 📖 概述 + +`FormPreview` 是一个独立的表单预览组件,从 `FormDesigner` 中提取出来,可在任何地方使用。它支持完整的表单交互、验证和联动规则。 + +## ✨ 特点 + +- ✅ **完全独立**:可在任何页面使用,不依赖 FormDesigner +- ✅ **支持所有字段类型**:input, select, textarea, date, grid, cascader 等 +- ✅ **支持验证规则**:required, pattern, min/max, email, phone 等 +- ✅ **支持联动规则**:显示/隐藏、禁用/启用、必填控制、值联动 +- ✅ **支持动态数据源**:API 数据源、预定义数据源 +- ✅ **非受控组件**:内部管理状态,无冲突 +- ✅ **简单易用**:只需传入 `fields` 和 `formConfig` + +## 📦 导入 + +```typescript +import { FormPreview, FormPreviewRef } from '@/components/FormDesigner'; +import type { FieldConfig, FormConfig } from '@/components/FormDesigner'; +``` + +## 🚀 基础用法 + +### 1. 最简单的使用 + +```tsx +import React, { useRef } from 'react'; +import { FormPreview, FormPreviewRef } from '@/components/FormDesigner'; + +const MyComponent = () => { + const previewRef = useRef(null); + + // 从后端获取表单定义 + const formSchema = await getFormDefinitionById(id); + + return ( + + ); +}; +``` + +### 2. 在 Modal 中使用 + +```tsx +import React from 'react'; +import { Modal } from 'antd'; +import { FormPreview } from '@/components/FormDesigner'; + +const FormPreviewModal = ({ open, onClose, formDefinition }) => { + if (!formDefinition) return null; + + const { schema } = formDefinition; + + return ( + + + + ); +}; +``` + +### 3. 使用 Ref 方法 + +```tsx +import React, { useRef } from 'react'; +import { Button } from 'antd'; +import { FormPreview, FormPreviewRef } from '@/components/FormDesigner'; + +const MyComponent = () => { + const previewRef = useRef(null); + + const handleSubmit = async () => { + // 触发表单验证和提交 + await previewRef.current?.submit(); + }; + + const handleReset = () => { + // 重置表单 + previewRef.current?.reset(); + }; + + return ( +
+ + +
+ + +
+
+ ); +}; +``` + +## 📝 Props 说明 + +### FormPreviewProps + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `fields` | `FieldConfig[]` | ✅ | 字段列表 | +| `formConfig` | `FormConfig` | ✅ | 表单配置 | + +### FormPreviewRef 方法 + +| 方法 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `submit()` | 无 | `Promise` | 提交表单(触发验证) | +| `reset()` | 无 | `void` | 重置表单 | + +## 🔧 数据格式 + +### FormConfig 示例 + +```typescript +const formConfig: FormConfig = { + title: "部署申请表单", // 表单标题(可选) + formWidth: 600, // 表单弹窗宽度,单位px + labelAlign: "right", // 标签对齐:left | right | top + size: "middle" // 表单尺寸:small | middle | large +}; +``` + +### Fields 示例 + +```typescript +const fields: FieldConfig[] = [ + // 基础输入框 + { + id: "field_1", + type: "input", + label: "应用名称", + name: "appName", + placeholder: "请输入应用名称", + required: true, + validationRules: [ + { type: "required", message: "应用名称不能为空" }, + { type: "maxLength", value: 50, message: "不能超过50个字符" } + ] + }, + + // 下拉选择(静态选项) + { + id: "field_2", + type: "select", + label: "环境选择", + name: "environment", + required: true, + dataSourceType: "static", + options: [ + { label: "开发环境", value: "dev" }, + { label: "测试环境", value: "test" }, + { label: "生产环境", value: "prod" } + ] + }, + + // 下拉选择(API 数据源) + { + id: "field_3", + type: "select", + label: "Jenkins服务器", + name: "jenkinsServer", + required: true, + dataSourceType: "api", + apiDataSource: { + url: "/api/v1/jenkins-servers/list", + method: "GET", + params: { enabled: true }, + labelField: "name", + valueField: "id" + } + }, + + // 下拉选择(预定义数据源) + { + id: "field_4", + type: "select", + label: "项目组", + name: "projectGroup", + required: true, + dataSourceType: "predefined", + predefinedDataSource: { + sourceType: "PROJECT_GROUPS" + } + }, + + // 栅格布局 + { + id: "field_5", + type: "grid", + label: "基础信息", + name: "grid_basic", + columns: 2, + columnSpans: [12, 12], + gutter: 16, + children: [ + // 第一列 + [ + { + id: "field_6", + type: "input", + label: "创建人", + name: "creator", + required: true + } + ], + // 第二列 + [ + { + id: "field_7", + type: "date", + label: "创建日期", + name: "createDate", + required: true + } + ] + ] + } +]; +``` + +## 🌟 实际应用场景 + +### 场景1:工作流设计页面预览启动表单 + +```tsx +// src/pages/Workflow/Design/components/FormPreviewModal.tsx +import { FormPreview } from '@/components/FormDesigner'; + +const FormPreviewModal = ({ formDefinition, open, onClose }) => { + const { schema } = formDefinition; + + return ( + + + + ); +}; +``` + +### 场景2:表单设计器内部预览 + +```tsx +// src/components/FormDesigner/Designer.tsx +const FormDesigner = () => { + const [previewVisible, setPreviewVisible] = useState(false); + const [fields, setFields] = useState([]); + const [formConfig, setFormConfig] = useState({}); + + return ( + <> + + + setPreviewVisible(false)}> + + + + ); +}; +``` + +### 场景3:独立的表单预览页面 + +```tsx +// src/pages/Form/Preview/index.tsx +import { useParams } from 'react-router-dom'; +import { FormPreview } from '@/components/FormDesigner'; + +const FormPreviewPage = () => { + const { id } = useParams(); + const [formSchema, setFormSchema] = useState(null); + + useEffect(() => { + const loadForm = async () => { + const data = await getFormDefinitionById(id); + setFormSchema(data.schema); + }; + loadForm(); + }, [id]); + + if (!formSchema) return
加载中...
; + + return ( +
+

表单预览

+ +
+ ); +}; +``` + +## ⚠️ 重要说明 + +### 1. 非受控组件 + +`FormPreview` 是**非受控组件**,内部通过 `Ant Design Form` 管理状态: + +- ✅ **不要**传递 `value` 和 `onChange`(会导致冲突) +- ✅ **不要**尝试从外部控制字段值 +- ✅ 使用 `ref` 方法获取表单值或提交表单 + +### 2. 数据格式统一 + +无论在哪里使用,数据格式都是一致的: + +```typescript + +``` + +### 3. 与 FormRenderer 的区别 + +| 特性 | FormPreview | FormRenderer | +|------|-------------|--------------| +| 推荐使用 | ✅ 是 | ❌ 已废弃 | +| 受控模式 | ❌ 非受控 | ⚠️ 部分受控(有问题) | +| 下拉框可选中 | ✅ 正常 | ❌ 有bug | +| 使用场景 | 预览、交互 | 已废弃 | + +**建议**:所有新代码都使用 `FormPreview`,不要再使用 `FormRenderer`。 + +## 📚 相关文档 + +- [FormDesigner 文档](./README.md) +- [字段类型定义](./types.ts) +- [工作流扩展字段](./extensions/workflow/README.md) + diff --git a/frontend/src/components/FormDesigner/LIFECYCLE_HOOKS.md b/frontend/src/components/FormDesigner/LIFECYCLE_HOOKS.md new file mode 100644 index 00000000..10347c83 --- /dev/null +++ b/frontend/src/components/FormDesigner/LIFECYCLE_HOOKS.md @@ -0,0 +1,508 @@ +# FormRenderer 生命周期钩子指南 + +## 概述 + +`FormRenderer` 提供了完整的表单生命周期钩子,允许你在表单提交的各个阶段插入自定义逻辑。 + +## 钩子执行顺序 + +``` +用户点击提交 + ↓ +1. 表单验证 (Form.validateFields) + ↓ (验证失败) + onError ← 触发 + ↓ (验证通过) +2. beforeSubmit 钩子 + ↓ (返回 false 或抛出错误) + onError ← 触发 (可选) + ↓ (返回修改后的数据或继续) +3. onSubmit 钩子 (执行实际提交) + ↓ (提交失败) + onError ← 触发 + ↓ (提交成功) +4. afterSubmit 钩子 + ↓ +完成 +``` + +## 钩子详解 + +### 1. beforeSubmit + +**触发时机**: 表单验证通过后,实际提交前 + +**作用**: +- 预处理表单数据(格式化、转换等) +- 添加额外字段(如时间戳、用户 ID) +- 条件判断,取消提交 + +**签名**: +```typescript +beforeSubmit?: (values: Record) => + Record | false | Promise | false> +``` + +**返回值**: +- `Record` - 修改后的数据,将传递给 `onSubmit` +- `false` - 取消提交,不执行 `onSubmit` 和 `afterSubmit` +- 抛出异常 - 触发 `onError`,中断提交流程 + +**示例**: +```tsx +const handleBeforeSubmit = async (values) => { + // 数据预处理 + return { + ...values, + email: values.email.toLowerCase(), + submitTime: new Date().toISOString(), + userId: getCurrentUserId(), + }; +}; + +// 条件取消提交 +const handleBeforeSubmitWithCondition = async (values) => { + if (!values.agreeToTerms) { + message.warning('请先同意服务条款'); + return false; // 取消提交 + } + return values; +}; + + +``` + +### 2. onSubmit + +**触发时机**: `beforeSubmit` 执行完成后(如果有) + +**作用**: +- 执行实际的提交逻辑 +- 调用后端 API +- 返回提交结果 + +**签名**: +```typescript +onSubmit?: (values: Record) => void | Promise +``` + +**参数**: +- `values` - 如果有 `beforeSubmit`,则是其返回的数据;否则是原始表单数据 + +**返回值**: +- 返回的结果会传递给 `afterSubmit` + +**示例**: +```tsx +const handleSubmit = async (data) => { + try { + const response = await api.createUser(data); + message.success('用户创建成功!'); + return response; // 返回结果给 afterSubmit + } catch (error) { + // 错误会被捕获,触发 onError + throw error; + } +}; + + +``` + +### 3. afterSubmit + +**触发时机**: `onSubmit` 成功完成后 + +**作用**: +- 提交成功后的后续操作 +- 页面跳转 +- 刷新列表 +- 显示成功提示 + +**签名**: +```typescript +afterSubmit?: (response?: any) => void | Promise +``` + +**参数**: +- `response` - `onSubmit` 的返回值 + +**注意**: +- `afterSubmit` 的错误不会中断提交流程,只会记录日志 +- 即使 `afterSubmit` 抛出异常,提交仍然被视为成功 + +**示例**: +```tsx +const handleAfterSubmit = async (response) => { + // 跳转到详情页 + if (response?.id) { + navigate(`/users/${response.id}`); + } + + // 刷新列表 + await refreshUserList(); + + // 关闭弹窗 + setModalVisible(false); +}; + + +``` + +### 4. onError + +**触发时机**: +- 表单验证失败 +- `beforeSubmit` 抛出异常 +- `onSubmit` 抛出异常 + +**作用**: +- 统一的错误处理 +- 错误上报 +- 自定义错误提示 + +**签名**: +```typescript +onError?: (error: any) => void | Promise +``` + +**参数**: +- `error` - 错误对象(可能是验证错误、网络错误等) + +**示例**: +```tsx +const handleError = async (error) => { + // 错误上报 + reportError({ + type: 'form_submit_error', + error: error.message, + stack: error.stack, + }); + + // 自定义错误提示 + if (error.code === 'DUPLICATE_EMAIL') { + message.error('该邮箱已被注册,请使用其他邮箱'); + } else { + message.error(`提交失败: ${error.message}`); + } +}; + + +``` + +## 完整示例 + +```tsx +import React from 'react'; +import { FormRenderer } from '@/components/FormDesigner'; +import { message } from 'antd'; +import { useNavigate } from 'react-router-dom'; + +const UserCreateForm: React.FC = () => { + const navigate = useNavigate(); + + // 1️⃣ beforeSubmit: 数据预处理 + const handleBeforeSubmit = async (values: Record) => { + console.log('📝 原始表单数据:', values); + + // 数据格式化 + const processedValues = { + ...values, + email: values.email?.toLowerCase(), + phone: values.phone?.replace(/\s/g, ''), + submitTime: new Date().toISOString(), + submittedBy: getCurrentUserId(), + }; + + // 条件验证 + if (values.age < 18) { + message.warning('用户年龄必须大于18岁'); + return false; // 取消提交 + } + + console.log('✅ 处理后的数据:', processedValues); + return processedValues; + }; + + // 2️⃣ onSubmit: 执行提交 + const handleSubmit = async (data: Record) => { + console.log('📤 提交到服务器:', data); + + // 调用 API + const response = await fetch('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + console.log('✅ 服务器响应:', result); + message.success('用户创建成功!'); + + return result; + }; + + // 3️⃣ afterSubmit: 提交成功后的处理 + const handleAfterSubmit = async (response: any) => { + console.log('🎉 提交完成,开始后续处理:', response); + + // 延迟跳转 + await new Promise(resolve => setTimeout(resolve, 500)); + + // 跳转到用户详情页 + navigate(`/users/${response.data.id}`); + }; + + // 4️⃣ onError: 错误处理 + const handleError = async (error: any) => { + console.error('❌ 提交出错:', error); + + // 上报错误 + await reportError({ + type: 'user_create_error', + message: error.message, + timestamp: Date.now(), + }); + + // 自定义错误提示 + if (error.message.includes('duplicate')) { + message.error('该邮箱已被注册'); + } else if (error.message.includes('network')) { + message.error('网络错误,请稍后重试'); + } else { + message.error(`创建失败: ${error.message}`); + } + }; + + return ( + + ); +}; +``` + +## 常见场景 + +### 场景1: 添加额外字段 + +```tsx +const handleBeforeSubmit = async (values) => { + return { + ...values, + createdBy: getCurrentUser().id, + createdAt: Date.now(), + source: 'web', + }; +}; +``` + +### 场景2: 数据格式转换 + +```tsx +const handleBeforeSubmit = async (values) => { + return { + ...values, + // 日期对象转 ISO 字符串 + startDate: values.startDate?.toISOString(), + endDate: values.endDate?.toISOString(), + // 数组转逗号分隔字符串 + tags: values.tags?.join(','), + // 金额转分(后端存储) + amount: Math.round(values.amount * 100), + }; +}; +``` + +### 场景3: 二次确认 + +```tsx +const handleBeforeSubmit = async (values) => { + const confirmed = await new Promise((resolve) => { + Modal.confirm({ + title: '确认提交', + content: `确定要创建用户 "${values.username}" 吗?`, + onOk: () => resolve(true), + onCancel: () => resolve(false), + }); + }); + + return confirmed ? values : false; +}; +``` + +### 场景4: 提交后跳转或刷新 + +```tsx +const handleAfterSubmit = async (response) => { + // 方式1: 跳转到列表页 + navigate('/users'); + + // 方式2: 跳转到详情页 + navigate(`/users/${response.id}`); + + // 方式3: 刷新当前页面数据 + await refreshData(); + + // 方式4: 关闭弹窗 + setModalVisible(false); +}; +``` + +### 场景5: 错误分类处理 + +```tsx +const handleError = async (error) => { + if (error.code === 400) { + message.error('请求参数错误,请检查输入'); + } else if (error.code === 401) { + message.error('登录已过期,请重新登录'); + navigate('/login'); + } else if (error.code === 403) { + message.error('没有权限执行此操作'); + } else if (error.code === 409) { + message.error('数据冲突,请刷新后重试'); + } else if (error.code >= 500) { + message.error('服务器错误,请联系管理员'); + } else { + message.error(`提交失败: ${error.message}`); + } +}; +``` + +## 最佳实践 + +### ✅ 推荐 + +1. **明确职责分离** + ```tsx + // ✅ beforeSubmit 只做数据处理 + beforeSubmit: (values) => ({ ...values, email: values.email.toLowerCase() }) + + // ✅ onSubmit 只做网络请求 + onSubmit: (data) => api.createUser(data) + + // ✅ afterSubmit 只做后续操作 + afterSubmit: () => navigate('/success') + ``` + +2. **使用 async/await** + ```tsx + // ✅ 清晰的异步处理 + const handleSubmit = async (data) => { + const response = await api.createUser(data); + return response; + }; + ``` + +3. **统一错误处理** + ```tsx + // ✅ 在 onError 中统一处理 + onError: (error) => { + reportError(error); + showErrorMessage(error); + } + ``` + +### ❌ 避免 + +1. **不要在 beforeSubmit 中做网络请求** + ```tsx + // ❌ 不推荐 + beforeSubmit: async (values) => { + await api.checkDuplicate(values.email); // 应该在 onSubmit 中做 + return values; + } + ``` + +2. **不要在 afterSubmit 中处理错误** + ```tsx + // ❌ 错误应该在 onError 中处理 + afterSubmit: (response) => { + if (!response.success) { + message.error('失败'); // 应该在 onError 中做 + } + } + ``` + +3. **避免循环依赖** + ```tsx + // ❌ 不要在钩子中再次触发提交 + afterSubmit: () => { + form.submit(); // 会导致无限循环 + } + ``` + +## 调试技巧 + +```tsx +const handleBeforeSubmit = async (values) => { + console.group('🎣 beforeSubmit'); + console.log('原始数据:', values); + const processed = processData(values); + console.log('处理后:', processed); + console.groupEnd(); + return processed; +}; + +const handleSubmit = async (data) => { + console.group('📤 onSubmit'); + console.log('提交数据:', data); + try { + const response = await api.submit(data); + console.log('响应:', response); + return response; + } catch (error) { + console.error('错误:', error); + throw error; + } finally { + console.groupEnd(); + } +}; + +const handleAfterSubmit = async (response) => { + console.log('🎉 afterSubmit:', response); +}; + +const handleError = async (error) => { + console.error('❌ onError:', error); +}; +``` + +## 总结 + +生命周期钩子让你能够在表单提交的各个阶段插入自定义逻辑: + +- **beforeSubmit**: 预处理数据,可取消提交 +- **onSubmit**: 执行实际提交 +- **afterSubmit**: 提交成功后的处理 +- **onError**: 统一的错误处理 + +合理使用这些钩子,可以构建健壮、灵活的表单提交流程。 + diff --git a/frontend/src/components/FormDesigner/Renderer.tsx b/frontend/src/components/FormDesigner/Renderer.tsx index 3d494250..8388969f 100644 --- a/frontend/src/components/FormDesigner/Renderer.tsx +++ b/frontend/src/components/FormDesigner/Renderer.tsx @@ -1,13 +1,41 @@ /** * 表单渲染器组件(运行时) - * 根据 Schema 渲染可交互的表单 + * 用于真实业务场景的表单渲染和提交 + * + * 特点: + * - ✅ 支持所有表单字段类型 + * - ✅ 支持验证规则和联动规则 + * - ✅ 支持动态数据源(API、预定义) + * - ✅ 非受控组件,内部管理状态 + * - ✅ 支持生命周期钩子(后续补充) + * - ✅ 用于生产环境的真实表单提交 + * + * @example + * ```tsx + * // 使用示例 + * import { FormRenderer } from '@/components/FormDesigner'; + * + * const MyComponent = () => { + * const formSchema = await getFormDefinitionById(id); + * + * return ( + * { + * await api.submitForm(values); + * }} + * /> + * ); + * }; + * ``` */ -import React, { useState, useEffect, useMemo } from 'react'; -import { Form, Button, message } from 'antd'; +import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; +import { Form, message } from 'antd'; import type { Rule } from 'antd/es/form'; import dayjs from 'dayjs'; -import type { FieldConfig, FormSchema, ValidationRule, LinkageRule } from './types'; +import type { FieldConfig, FormConfig, ValidationRule, LinkageRule, FormSchema } from './types'; import type { ComponentMeta } from './config'; import { COMPONENT_LIST } from './config'; import { ComponentsContext } from './Designer'; @@ -15,136 +43,179 @@ import FieldRenderer from './components/FieldRenderer'; import GridFieldPreview from './components/GridFieldPreview'; import './styles.css'; +/** + * 表单渲染器组件的 Props + */ export interface FormRendererProps { - schema: FormSchema; // 表单 Schema - extraComponents?: ComponentMeta[]; // 🆕 扩展组件(如工作流字段) - value?: Record; // 表单值(受控) - onChange?: (values: Record) => void; // 值变化回调 - onSubmit?: (values: Record) => void | Promise; // 提交回调 - onCancel?: () => void; // 取消回调 - readonly?: boolean; // 只读模式 - showSubmit?: boolean; // 是否显示提交按钮 - showCancel?: boolean; // 是否显示取消按钮 - submitText?: string; // 提交按钮文本 - cancelText?: string; // 取消按钮文本 - // 🎣 生命周期钩子 - beforeSubmit?: (values: Record) => Record | false | Promise | false>; // 提交前钩子 - afterSubmit?: (response?: any) => void | Promise; // 提交成功后钩子 - onError?: (error: any) => void | Promise; // 提交失败钩子 + /** 表单 Schema(包含 fields 和 formConfig) */ + schema?: FormSchema; + /** 字段列表(与 schema 二选一) */ + fields?: FieldConfig[]; + /** 表单配置(与 schema 二选一) */ + formConfig?: FormConfig; + /** 🆕 扩展组件(如工作流字段) */ + extraComponents?: ComponentMeta[]; + /** 表单初始值(仅用于初始化,不要作为受控属性使用) */ + value?: Record; + /** 值变化回调(父组件可监听,但不应将值传回 value) */ + onChange?: (values: Record) => void; + /** 提交回调 */ + onSubmit?: (values: Record) => void | Promise; + /** 取消回调 */ + onCancel?: () => void; + /** 只读模式 */ + readonly?: boolean; + /** 是否显示提交按钮 */ + showSubmit?: boolean; + /** 是否显示取消按钮 */ + showCancel?: boolean; + /** 提交按钮文本 */ + submitText?: string; + /** 取消按钮文本 */ + cancelText?: string; + // 🎣 生命周期钩子(后续实现) + beforeSubmit?: (values: Record) => Record | false | Promise | false>; + afterSubmit?: (response?: any) => void | Promise; + onError?: (error: any) => void | Promise; } -const FormRenderer: React.FC = ({ - schema, - extraComponents = [], - value, - onChange, - onSubmit, - onCancel, - readonly = false, - showSubmit = true, - showCancel = false, - submitText = '提交', - cancelText = '取消', - beforeSubmit, - afterSubmit, - onError -}) => { - const { fields, formConfig } = schema; - const [form] = Form.useForm(); - +/** + * 表单渲染器组件暴露的方法 + */ +export interface FormRendererRef { + /** 提交表单(触发验证) */ + submit: () => Promise; + /** 重置表单 */ + reset: () => void; +} + +const FormRenderer = forwardRef((props, ref) => { + const { + schema, + fields: propFields, + formConfig: propFormConfig, + extraComponents = [], + onSubmit, + showSubmit = false, + showCancel = false, + submitText = '提交', + cancelText = '取消', + onCancel, + } = props; + // 🔌 合并核心组件和扩展组件 - const allComponents = useMemo(() => { + const allComponents = React.useMemo(() => { return [...COMPONENT_LIST, ...extraComponents]; }, [extraComponents]); - const [formData, setFormData] = useState>(value || {}); + + // 从 schema 或直接从 props 获取 fields 和 formConfig + const fields = schema?.fields || propFields || []; + const formConfig = schema?.formConfig || propFormConfig || { + labelAlign: 'right', + size: 'middle', + }; + + const [form] = Form.useForm(); + const [formData, setFormData] = useState>({}); const [fieldStates, setFieldStates] = useState>({}); - // 同步外部值到表单 - useEffect(() => { - if (value) { - form.setFieldsValue(value); - setFormData(value); - } - }, [value, form]); - const handleSubmit = async () => { - if (readonly) return; - try { - // 1️⃣ 表单验证 - let values = await form.validateFields(); + // 1. 表单验证 + const values = await form.validateFields(); + console.log('📋 表单验证通过:', values); + setFormData(values); - // 2️⃣ beforeSubmit 钩子 - 数据转换或阻止提交 - if (beforeSubmit) { - const result = await beforeSubmit(values); - if (result === false) { - // 返回 false 阻止提交 + // 2. beforeSubmit 钩子 - 允许修改提交数据或中断提交 + let finalValues = values; + if (props.beforeSubmit) { + try { + const result = await props.beforeSubmit(values); + if (result === false) { + console.log('⚠️ beforeSubmit 返回 false,提交已取消'); + message.warning('提交已取消'); + return; + } + if (result && typeof result === 'object') { + finalValues = result; + console.log('🔄 beforeSubmit 修改了提交数据:', finalValues); + } + } catch (error) { + console.error('❌ beforeSubmit 钩子执行失败:', error); + message.error('提交前处理失败'); + if (props.onError) { + await props.onError(error); + } return; } - if (result && typeof result === 'object') { - // 使用转换后的数据 - values = result; - } } - setFormData(values); - - // 3️⃣ 提交 - let response: any; + // 3. 执行提交 + let submitResult: any; if (onSubmit) { - response = await onSubmit(values); + try { + submitResult = await onSubmit(finalValues); + console.log('✅ 表单提交成功:', submitResult); + } catch (error) { + console.error('❌ 表单提交失败:', error); + // 提交失败时触发 onError 钩子 + if (props.onError) { + await props.onError(error); + } else { + message.error('提交失败,请稍后重试'); + } + throw error; // 重新抛出,不执行 afterSubmit + } } else { - console.log('表单提交数据:', values); message.success('表单提交成功!'); } - // 4️⃣ afterSubmit 钩子 - 提交成功后处理 - if (afterSubmit) { + // 4. afterSubmit 钩子 - 提交成功后的处理 + if (props.afterSubmit) { try { - await afterSubmit(response); - } catch (err) { - console.error('afterSubmit error:', err); + await props.afterSubmit(submitResult); + console.log('✅ afterSubmit 钩子执行完成'); + } catch (error) { + console.error('⚠️ afterSubmit 钩子执行失败:', error); + // afterSubmit 失败不影响整体提交流程,只记录日志 } } } catch (error) { - // 5️⃣ onError 钩子 - 错误处理 - if (onError) { - try { - await onError(error); - } catch (err) { - console.error('onError hook error:', err); - } - } else { - message.error('请填写必填项'); + // 验证失败 + console.error('❌ 表单验证失败:', error); + message.error('请填写必填项'); + + // 触发 onError 钩子(验证失败也算错误) + if (props.onError) { + await props.onError(error); } } }; const handleReset = () => { - if (readonly) return; - form.resetFields(); - const resetValues = {}; - setFormData(resetValues); - - if (onChange) { - onChange(resetValues); - } - + setFormData({}); message.info('表单已重置'); }; + // 监听表单值变化 const handleValuesChange = (_: any, allValues: Record) => { setFormData(allValues); - if (onChange) { - onChange(allValues); + if (props.onChange) { + props.onChange(allValues); } }; + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + submit: handleSubmit, + reset: handleReset, + })); + // 将验证规则转换为Ant Design的Rule数组 const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => { if (!validationRules || validationRules.length === 0) return []; @@ -153,14 +224,11 @@ const FormRenderer: React.FC = ({ const antdRule: Rule = { type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined, message: rule.message || `请输入正确的${rule.type}`, - // 注意:不设置 trigger,或者不仅仅设置为 blur,这样提交时也会触发验证 - // 如果用户指定了 trigger,我们也在提交时触发 }; switch (rule.type) { case 'required': antdRule.required = true; - // 必填验证始终在提交时触发,不管用户选择了什么 trigger break; case 'pattern': if (rule.value) { @@ -243,7 +311,6 @@ const FormRenderer: React.FC = ({ allFields.forEach(field => { if (field.linkageRules && field.linkageRules.length > 0) { field.linkageRules.forEach((rule: LinkageRule) => { - // 检查所有条件是否都满足 const allConditionsMet = rule.conditions.every(condition => evaluateCondition(condition, formData) ); @@ -264,7 +331,6 @@ const FormRenderer: React.FC = ({ newFieldStates[field.name].required = rule.action; break; case 'value': - // 值联动需要直接设置表单值 form.setFieldValue(field.name, rule.action); break; } @@ -283,7 +349,6 @@ const FormRenderer: React.FC = ({ const collectDefaultValues = (fieldList: FieldConfig[]) => { fieldList.forEach(field => { if (field.defaultValue !== undefined && field.name) { - // 🔧 日期字段需要转换为 dayjs 对象 if (field.type === 'date' && typeof field.defaultValue === 'string') { defaultValues[field.name] = dayjs(field.defaultValue); } else { @@ -303,10 +368,9 @@ const FormRenderer: React.FC = ({ setFormData(prev => ({ ...prev, ...defaultValues })); }, [fields, form]); - // 递归渲染字段(包括栅格布局内的字段) + // 递归渲染字段 const renderFields = (fieldList: FieldConfig[]): React.ReactNode => { return fieldList.map((field) => { - // 布局组件:文本、分割线 if (field.type === 'text' || field.type === 'divider') { return (
@@ -315,43 +379,28 @@ const FormRenderer: React.FC = ({ ); } - // 栅格布局:使用专门的预览组件,自动处理内部字段的 Form.Item if (field.type === 'grid') { return ( setFormData(prev => ({ ...prev, [name]: value }))} formConfig={formConfig} /> ); } - // 获取字段状态(联动规则影响) const fieldState = fieldStates[field.name] || {}; - const isVisible = fieldState.visible !== false; // 默认显示 + const isVisible = fieldState.visible !== false; const isDisabled = fieldState.disabled || field.disabled || false; const isRequired = fieldState.required !== undefined ? fieldState.required : field.required; - // 如果字段被隐藏,不渲染 if (!isVisible) { return null; } - // 合并基础验证规则和自定义验证规则 const customRules = convertValidationRules(field.validationRules); - - // 🐛 调试:打印验证规则 - if (field.validationRules && field.validationRules.length > 0) { - console.log(`📋 字段 "${field.label}" 的验证规则:`, field.validationRules); - console.log(`✅ 转换后的规则:`, customRules); - } - - // 检查自定义验证规则中是否已经包含必填验证 const hasRequiredRule = field.validationRules?.some(rule => rule.type === 'required'); - // 基础规则:只有在没有自定义必填验证时,才使用字段属性中的"是否必填" const baseRules: Rule[] = []; if (isRequired && !hasRequiredRule) { baseRules.push({ @@ -361,13 +410,7 @@ const FormRenderer: React.FC = ({ } const allRules = [...baseRules, ...customRules]; - - // 🐛 调试:打印最终规则 - if (allRules.length > 0) { - console.log(`🎯 字段 "${field.label}" 最终验证规则:`, allRules); - } - // 普通表单组件使用 Form.Item 包裹 return ( = ({ > setFormData(prev => ({ ...prev, [field.name]: value }))} isPreview={true} /> @@ -403,52 +444,70 @@ const FormRenderer: React.FC = ({
- {formConfig.title && ( -

{formConfig.title}

- )} + form={form} + layout={formLayout} + size={formConfig.size} + colon={true} + labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign} + labelCol={labelColSpan ? { span: labelColSpan } : undefined} + wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined} + onValuesChange={handleValuesChange} + > + {formConfig.title && ( +

{formConfig.title}

+ )} - {renderFields(fields)} + {renderFields(fields)} - {!readonly && (showSubmit || showCancel) && ( - -
- {showSubmit && ( - - )} - {showCancel && ( - - )} - {!showCancel && ( - - )} -
-
- )} -
+ {(showSubmit || showCancel) && ( + +
+ {showSubmit && ( + + )} + {showCancel && ( + + )} +
+
+ )} +
); -}; +}); + +FormRenderer.displayName = 'FormRenderer'; export default FormRenderer; - diff --git a/frontend/src/components/FormDesigner/index.tsx b/frontend/src/components/FormDesigner/index.tsx index bde1db9c..7b61085b 100644 --- a/frontend/src/components/FormDesigner/index.tsx +++ b/frontend/src/components/FormDesigner/index.tsx @@ -3,8 +3,8 @@ * * 提供三个核心组件: * 1. FormDesigner - 设计时组件,用于可视化设计表单 - * 2. FormPreview - 预览组件,用于预览和交互表单(推荐) - * 3. FormRenderer - 运行时组件,用于渲染和提交表单(已废弃,建议使用 FormPreview) + * 2. FormPreview - 预览组件,用于预览和交互表单(推荐用于纯预览场景) + * 3. FormRenderer - 运行时组件,用于真实业务场景的表单渲染和提交(推荐用于生产环境) */ export { default as FormDesigner } from './Designer'; @@ -14,7 +14,7 @@ export { default as FormPreview } from './Preview'; export type { FormPreviewProps, FormPreviewRef } from './Preview'; export { default as FormRenderer } from './Renderer'; -export type { FormRendererProps } from './Renderer'; +export type { FormRendererProps, FormRendererRef } from './Renderer'; // 导出类型定义 export type { diff --git a/frontend/src/domain/dataSource/presets/README.md b/frontend/src/domain/dataSource/presets/README.md new file mode 100644 index 00000000..dbffd231 --- /dev/null +++ b/frontend/src/domain/dataSource/presets/README.md @@ -0,0 +1,138 @@ +# 数据源预设配置 + +本目录包含所有预定义的数据源配置。 + +## 已注册的数据源 + +### 部署相关 (deploy.ts) + +#### 1. ENVIRONMENTS - 环境列表 +```typescript +import { DataSourceType, loadDataSource } from '@/domain/dataSource'; + +// 使用示例 +const options = await loadDataSource(DataSourceType.ENVIRONMENTS); +``` + +**接口:** `GET /api/v1/environments/list?enabled=true` + +**返回格式:** +```typescript +{ + label: string; // 环境名称 + value: number; // 环境 ID + code: string; // 环境代码 + type: string; // 环境类型 + description: string;// 描述 +} +``` + +#### 2. PROJECT_GROUPS - 项目组列表 +```typescript +const options = await loadDataSource(DataSourceType.PROJECT_GROUPS); +``` + +**接口:** `GET /api/v1/project-groups/list?enabled=true` + +**返回格式:** +```typescript +{ + label: string; // 项目组名称 + value: number; // 项目组 ID + code: string; // 项目组代码 + description: string;// 描述 +} +``` + +#### 3. APPLICATIONS - 应用列表 +```typescript +const options = await loadDataSource(DataSourceType.APPLICATIONS); +``` + +**接口:** `GET /api/v1/applications/list?enabled=true` + +**返回格式:** +```typescript +{ + label: string; // 应用名称 + value: number; // 应用 ID + code: string; // 应用代码 + projectGroupId: number; // 所属项目组 ID + description: string; // 描述 +} +``` + +### 外部系统 (jenkins.ts, k8s.ts, git.ts, docker.ts) + +- **JENKINS_SERVERS** - Jenkins 服务器列表 +- **K8S_CLUSTERS** - K8s 集群列表 +- **GIT_REPOSITORIES** - Git 仓库列表 +- **DOCKER_REGISTRIES** - Docker 镜像仓库列表 + +### 通知相关 (notification.ts) + +- **NOTIFICATION_CHANNEL_TYPES** - 通知渠道类型 +- **NOTIFICATION_CHANNELS** - 通知渠道列表 + +### 用户相关 (user.ts) + +- **USERS** - 用户列表 +- **ROLES** - 角色列表 +- **DEPARTMENTS** - 部门列表 + +## 使用示例 + +### 在 React 组件中使用 + +```typescript +import { useState, useEffect } from 'react'; +import { DataSourceType, loadDataSource } from '@/domain/dataSource'; + +function MyComponent() { + const [environments, setEnvironments] = useState([]); + const [projectGroups, setProjectGroups] = useState([]); + const [applications, setApplications] = useState([]); + + useEffect(() => { + // 加载环境列表 + loadDataSource(DataSourceType.ENVIRONMENTS).then(setEnvironments); + + // 加载项目组列表 + loadDataSource(DataSourceType.PROJECT_GROUPS).then(setProjectGroups); + + // 加载应用列表 + loadDataSource(DataSourceType.APPLICATIONS).then(setApplications); + }, []); + + return ( + + ); +} +``` + +### 在表单设计器中使用 + +这些数据源可以直接在表单设计器的下拉框、单选框等组件中使用: + +```typescript +{ + type: 'select', + label: '选择环境', + dataSource: DataSourceType.ENVIRONMENTS +} +``` + +## 添加新数据源 + +1. 在 `types.ts` 中添加枚举类型 +2. 在 `presets/` 目录创建配置文件 +3. 在 `DataSourceRegistry.ts` 中注册 + +详见主 README.md。 + diff --git a/frontend/src/pages/FormDesigner/index.tsx b/frontend/src/pages/FormDesigner/index.tsx index 7aa034bf..5e1e8f08 100644 --- a/frontend/src/pages/FormDesigner/index.tsx +++ b/frontend/src/pages/FormDesigner/index.tsx @@ -38,7 +38,6 @@ const FormDesignerExamplesPage: React.FC = () => { // ==================== 示例 2: 表单渲染器(静态) ==================== const [previewSchema, setPreviewSchema] = useState(); - const [staticFormData, setStaticFormData] = useState>({}); const handleStaticFormSubmit = async (data: Record) => { console.log('📤 静态表单提交数据:', data); @@ -145,7 +144,6 @@ const FormDesignerExamplesPage: React.FC = () => { }; // ==================== 示例 5: 复杂表单(带栅格布局) ==================== - const [complexFormData, setComplexFormData] = useState>({}); const complexFormSchema: FormSchema = { version: '1.0', @@ -210,6 +208,98 @@ const FormDesignerExamplesPage: React.FC = () => { message.success('表单提交成功!'); }; + // ==================== 示例 6: 带生命周期钩子的表单 ==================== + const lifecycleFormSchema: FormSchema = { + version: '1.0', + formConfig: { + labelAlign: 'right', + size: 'middle', + formWidth: 600 + }, + fields: [ + { + id: 'lc_field_1', + type: 'input', + label: '用户名', + name: 'username', + placeholder: '请输入用户名', + required: true + }, + { + id: 'lc_field_2', + type: 'input', + label: '邮箱', + name: 'email', + placeholder: '请输入邮箱', + required: true, + validationRules: [ + { type: 'email', message: '请输入有效的邮箱地址' } + ] + }, + { + id: 'lc_field_3', + type: 'select', + label: '用户角色', + name: 'role', + placeholder: '请选择角色', + required: true, + dataSourceType: 'static', + options: [ + { label: '管理员', value: 'admin' }, + { label: '普通用户', value: 'user' }, + { label: '访客', value: 'guest' } + ] + }, + ], + }; + + const handleLifecycleBeforeSubmit = async (values: Record) => { + console.log('🎣 beforeSubmit 钩子被调用:', values); + + // 模拟数据处理 + const processedValues = { + ...values, + email: values.email?.toLowerCase(), // 邮箱转小写 + submitTime: new Date().toISOString(), // 添加提交时间 + }; + + message.info('正在处理表单数据...'); + await new Promise(resolve => setTimeout(resolve, 500)); + + console.log('✅ beforeSubmit 处理完成,返回修改后的数据:', processedValues); + return processedValues; + }; + + const handleLifecycleSubmit = async (data: Record) => { + console.log('📤 提交到服务器:', data); + + // 模拟 API 调用 + await new Promise(resolve => setTimeout(resolve, 1000)); + + const response = { id: Math.random(), success: true, data }; + console.log('✅ 服务器响应:', response); + message.success('用户创建成功!'); + + return response; + }; + + const handleLifecycleAfterSubmit = async (response: any) => { + console.log('🎣 afterSubmit 钩子被调用,服务器响应:', response); + + // 模拟后续操作(如跳转、刷新列表等) + await new Promise(resolve => setTimeout(resolve, 300)); + + message.success(`用户 ID: ${response.id.toFixed(0)} 已成功创建`); + console.log('✅ afterSubmit 完成,可以执行跳转或刷新操作'); + }; + + const handleLifecycleError = async (error: any) => { + console.error('🎣 onError 钩子被调用:', error); + + // 可以在这里做错误上报、记录等 + message.error(`操作失败: ${error.message || '未知错误'}`); + }; + // ==================== Tab 配置 ==================== const tabItems = [ { @@ -295,12 +385,18 @@ const FormDesignerExamplesPage: React.FC = () => { 根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。

+ {previewSchema ? ( ) : ( @@ -327,6 +423,13 @@ const FormDesignerExamplesPage: React.FC = () => { showIcon style={{ marginBottom: 16 }} /> + {workflowPreviewSchema ? ( { console.log('🔄 工作流表单提交数据:', data); message.success('工作流表单提交成功!'); }} + showSubmit showCancel /> ) : ( @@ -427,14 +531,83 @@ const FormDesignerExamplesPage: React.FC = () => { ), }, + { + key: '6', + label: '🎣 示例 6: 生命周期钩子', + children: ( + +

生命周期钩子(beforeSubmit / afterSubmit / onError)

+

+ 演示表单提交的完整生命周期钩子,支持数据预处理、提交后处理和错误处理。 +

+ +

beforeSubmit: 提交前处理,可修改数据或取消提交(返回 false)

+

onSubmit: 执行实际提交操作

+

afterSubmit: 提交成功后的处理(跳转、刷新等)

+

onError: 错误处理(验证失败、提交失败等)

+
+ } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> +
+ 使用方式:
+ {` + {` schema={schema}`}
+ {` beforeSubmit={async (values) => {`}
+ {` // 数据预处理`}
+ {` return modifiedValues;`}
+ {` }}`}
+ {` onSubmit={async (data) => {`}
+ {` // 提交到服务器`}
+ {` return response;`}
+ {` }}`}
+ {` afterSubmit={async (response) => {`}
+ {` // 提交后处理`}
+ {` }}`}
+ {` onError={async (error) => {`}
+ {` // 错误处理`}
+ {` }}`}
+ {`/>`} +
+ + + + + ), + }, ]; return ( diff --git a/frontend/src/pages/Workflow/Design/components/FormPreviewModal.README.md b/frontend/src/pages/Workflow/Design/components/FormPreviewModal.README.md new file mode 100644 index 00000000..57279eb4 --- /dev/null +++ b/frontend/src/pages/Workflow/Design/components/FormPreviewModal.README.md @@ -0,0 +1,128 @@ +# FormPreviewModal 组件 + +## 概述 + +表单预览弹窗组件,用于在工作流设计页面预览与工作流关联的启动表单。 + +## 功能特性 + +- ✅ 自动读取工作流关联的表单定义数据 +- ✅ 使用 FormRenderer 组件渲染表单 +- ✅ 只读预览模式(不显示提交/取消按钮) +- ✅ 显示表单基本信息(名称、描述、标识、版本、状态) +- ✅ 响应式弹窗尺寸(max-w-5xl) +- ✅ 支持滚动查看长表单 + +## 使用方式 + +### 1. 在工作流设计页面自动加载 + +当进入工作流设计页面时,如果工作流关联了启动表单(`formDefinitionId`),系统会自动: + +1. 调用 `/api/v1/form-definitions/{id}` 接口获取表单定义数据 +2. 将数据存储在 `formDefinition` 状态中 +3. 在工具栏显示"预览表单"按钮(只有关联表单时才显示) + +### 2. 点击预览按钮 + +用户点击"预览表单"按钮后,会打开弹窗显示表单预览。 + +## 数据流 + +```typescript +// 1. 加载工作流时自动查询表单数据 +if (data.definition?.formDefinitionId) { + const formDef = await getFormDefinitionById(data.definition.formDefinitionId); + setFormDefinition(formDef); +} + +// 2. 点击预览按钮 +const handlePreviewForm = () => { + if (!formDefinition) { + message.warning('当前工作流未关联启动表单'); + return; + } + setFormPreviewVisible(true); +}; + +// 3. 渲染预览弹窗 + setFormPreviewVisible(false)} + formDefinition={formDefinition} +/> +``` + +## Props + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `open` | `boolean` | 是 | 控制弹窗显示/隐藏 | +| `onClose` | `() => void` | 是 | 关闭弹窗的回调函数 | +| `formDefinition` | `FormDefinitionResponse \| null` | 是 | 表单定义数据 | + +## 表单定义数据结构 + +```typescript +interface FormDefinitionResponse { + id: number; + name: string; // 表单名称 + key: string; // 表单标识 + formVersion: number; // 表单版本号 + categoryId?: number; // 分类ID + description?: string; // 表单描述 + schema: FormSchema; // 表单Schema(包含fields和formConfig) + status: 'DRAFT' | 'PUBLISHED' | 'DISABLED'; // 表单状态 + isTemplate: boolean; // 是否为模板 + // ... 其他基础字段 +} +``` + +## FormRenderer 配置 + +预览模式下的 FormRenderer 配置: + +```typescript + +``` + +## UI 展示 + +弹窗包含以下部分: + +1. **标题栏**:显示表单名称和描述 +2. **表单内容区**:使用 FormRenderer 渲染的表单 +3. **底部信息栏**:显示表单标识、版本、状态等元数据 + +## 样式特性 + +- 弹窗最大宽度:`max-w-5xl`(适配大表单) +- 弹窗最大高度:`max-h-[90vh]`(适配屏幕) +- 内容区可滚动:`overflow-y-auto` +- 状态颜色: + - 已发布:绿色(`text-green-600`) + - 草稿:黄色(`text-yellow-600`) + - 其他:灰色(`text-gray-600`) + +## 未来扩展 + +可能的功能扩展方向: + +- [ ] 支持表单数据填写和本地保存 +- [ ] 支持导出表单数据为 JSON +- [ ] 支持全屏预览模式 +- [ ] 支持打印表单 +- [ ] 支持分享表单预览链接 + +## 注意事项 + +1. 只有在工作流关联了启动表单时,"预览表单"按钮才会显示 +2. 表单数据在进入页面时自动加载,无需手动刷新 +3. 预览模式下表单是可交互的,但不会实际提交数据 +4. 如果表单定义数据加载失败,会在控制台输出错误信息 +