From f8583ba3efe743572616c252eaeddfe32d0b0600 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 25 Oct 2025 01:05:18 +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 --- .../src/components/FormDesigner/Preview.tsx | 267 +-------------- .../src/components/FormDesigner/Renderer.tsx | 324 +++--------------- .../components/FormFieldsRenderer.tsx | 96 ++++++ .../components/GridFieldPreview.tsx | 21 +- .../FormDesigner/hooks/useFormCore.ts | 86 +++++ .../FormDesigner/utils/defaultValueHelper.ts | 41 +++ .../FormDesigner/utils/linkageHelper.ts | 126 +++++++ .../FormDesigner/utils/validationHelper.ts | 86 +++++ frontend/src/pages/FormDesigner/index.tsx | 282 ++++++++++++++- .../components/StartWorkflowModal.tsx | 138 ++++++++ .../src/pages/Workflow/Definition/index.tsx | 85 ++++- .../src/pages/Workflow/Definition/service.ts | 25 +- .../src/pages/Workflow/Definition/types.ts | 34 ++ 13 files changed, 1049 insertions(+), 562 deletions(-) create mode 100644 frontend/src/components/FormDesigner/components/FormFieldsRenderer.tsx create mode 100644 frontend/src/components/FormDesigner/hooks/useFormCore.ts create mode 100644 frontend/src/components/FormDesigner/utils/defaultValueHelper.ts create mode 100644 frontend/src/components/FormDesigner/utils/linkageHelper.ts create mode 100644 frontend/src/components/FormDesigner/utils/validationHelper.ts create mode 100644 frontend/src/pages/Workflow/Definition/components/StartWorkflowModal.tsx diff --git a/frontend/src/components/FormDesigner/Preview.tsx b/frontend/src/components/FormDesigner/Preview.tsx index 5959f14b..1e9a4f68 100644 --- a/frontend/src/components/FormDesigner/Preview.tsx +++ b/frontend/src/components/FormDesigner/Preview.tsx @@ -31,13 +31,11 @@ * ``` */ -import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; +import { useImperativeHandle, forwardRef } from 'react'; import { Form, message } from 'antd'; -import type { Rule } from 'antd/es/form'; -import dayjs from 'dayjs'; -import type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from './types'; -import FieldRenderer from './components/FieldRenderer'; -import GridFieldPreview from './components/GridFieldPreview'; +import type { FieldConfig, FormConfig } from './types'; +import { useFormCore } from './hooks/useFormCore'; +import FormFieldsRenderer from './components/FormFieldsRenderer'; import './styles.css'; /** @@ -62,12 +60,9 @@ export interface FormPreviewRef { const FormPreview = forwardRef(({ fields, formConfig }, ref) => { const [form] = Form.useForm(); - const [formData, setFormData] = useState>({}); - const [fieldStates, setFieldStates] = useState>({}); + + // 使用核心 hook 管理表单状态 + const { formData, setFormData, fieldStates, handleValuesChange } = useFormCore({ fields, form }); const handleSubmit = async () => { try { @@ -92,247 +87,6 @@ const FormPreview = forwardRef(({ fields, form reset: handleReset, })); - // 将验证规则转换为Ant Design的Rule数组 - const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => { - if (!validationRules || validationRules.length === 0) return []; - - return validationRules.map(rule => { - 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) { - antdRule.pattern = new RegExp(rule.value); - } - break; - case 'min': - antdRule.type = 'number'; - antdRule.min = rule.value; - break; - case 'max': - antdRule.type = 'number'; - antdRule.max = rule.value; - break; - case 'minLength': - antdRule.min = rule.value; - break; - case 'maxLength': - antdRule.max = rule.value; - break; - case 'phone': - antdRule.pattern = /^1[3-9]\d{9}$/; - break; - case 'idCard': - antdRule.pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/; - break; - } - - return antdRule; - }); - }; - - // 评估联动条件 - const evaluateCondition = (condition: any, formValues: Record): boolean => { - const fieldValue = formValues[condition.field]; - const compareValue = condition.value; - - switch (condition.operator) { - case '==': - return fieldValue == compareValue; - case '!=': - return fieldValue != compareValue; - case '>': - return Number(fieldValue) > Number(compareValue); - case '<': - return Number(fieldValue) < Number(compareValue); - case '>=': - return Number(fieldValue) >= Number(compareValue); - case '<=': - return Number(fieldValue) <= Number(compareValue); - case 'includes': - return Array.isArray(fieldValue) ? fieldValue.includes(compareValue) : String(fieldValue).includes(compareValue); - case 'notIncludes': - return Array.isArray(fieldValue) ? !fieldValue.includes(compareValue) : !String(fieldValue).includes(compareValue); - default: - return false; - } - }; - - // 处理联动规则 - useEffect(() => { - const flattenFields = (fieldList: FieldConfig[]): FieldConfig[] => { - const result: FieldConfig[] = []; - fieldList.forEach(field => { - if (field.type !== 'divider' && field.type !== 'text') { - result.push(field); - } - if (field.type === 'grid' && field.children) { - field.children.forEach(columnFields => { - result.push(...flattenFields(columnFields)); - }); - } - }); - return result; - }; - - const allFields = flattenFields(fields); - const newFieldStates: Record = {}; - - allFields.forEach(field => { - if (field.linkageRules && field.linkageRules.length > 0) { - field.linkageRules.forEach((rule: LinkageRule) => { - // 检查所有条件是否都满足 - const allConditionsMet = rule.conditions.every(condition => - evaluateCondition(condition, formData) - ); - - if (allConditionsMet) { - if (!newFieldStates[field.name]) { - newFieldStates[field.name] = {}; - } - - switch (rule.type) { - case 'visible': - newFieldStates[field.name].visible = rule.action; - break; - case 'disabled': - newFieldStates[field.name].disabled = rule.action; - break; - case 'required': - newFieldStates[field.name].required = rule.action; - break; - case 'value': - // 值联动需要直接设置表单值 - form.setFieldValue(field.name, rule.action); - break; - } - } - }); - } - }); - - setFieldStates(newFieldStates); - }, [formData, fields, form]); - - // 设置默认值 - useEffect(() => { - const defaultValues: Record = {}; - - 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 { - defaultValues[field.name] = field.defaultValue; - } - } - if (field.type === 'grid' && field.children) { - field.children.forEach(columnFields => { - collectDefaultValues(columnFields); - }); - } - }); - }; - - collectDefaultValues(fields); - form.setFieldsValue(defaultValues); - setFormData(prev => ({ ...prev, ...defaultValues })); - }, [fields, form]); - - // 递归渲染字段(包括栅格布局内的字段) - const renderFields = (fieldList: FieldConfig[]): React.ReactNode => { - return fieldList.map((field) => { - // 布局组件:文本、分割线 - if (field.type === 'text' || field.type === 'divider') { - return ( -
- -
- ); - } - - // 栅格布局:使用专门的预览组件,自动处理内部字段的 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 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({ - required: true, - message: `请输入${field.label}`, - }); - } - - const allRules = [...baseRules, ...customRules]; - - // 🐛 调试:打印最终规则 - if (allRules.length > 0) { - console.log(`🎯 字段 "${field.label}" 最终验证规则:`, allRules); - } - - // 普通表单组件使用 Form.Item 包裹 - return ( - - setFormData(prev => ({ ...prev, [field.name]: value }))} - isPreview={true} - /> - - ); - }); - }; if (fields.length === 0) { return ( @@ -356,12 +110,17 @@ const FormPreview = forwardRef(({ fields, form 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)} + {Object.keys(formData).length > 0 && ( diff --git a/frontend/src/components/FormDesigner/Renderer.tsx b/frontend/src/components/FormDesigner/Renderer.tsx index 8388969f..d54b8b36 100644 --- a/frontend/src/components/FormDesigner/Renderer.tsx +++ b/frontend/src/components/FormDesigner/Renderer.tsx @@ -7,40 +7,50 @@ * - ✅ 支持验证规则和联动规则 * - ✅ 支持动态数据源(API、预定义) * - ✅ 非受控组件,内部管理状态 - * - ✅ 支持生命周期钩子(后续补充) + * - ✅ 支持生命周期钩子(beforeSubmit/afterSubmit/onError) * - ✅ 用于生产环境的真实表单提交 + * - ✅ 支持内置按钮和外部按钮两种模式 * * @example * ```tsx - * // 使用示例 - * import { FormRenderer } from '@/components/FormDesigner'; + * // 方式1: 使用内置按钮(简单场景) + * * - * const MyComponent = () => { - * const formSchema = await getFormDefinitionById(id); - * - * return ( - * { - * await api.submitForm(values); - * }} - * /> - * ); - * }; + * // 方式2: 使用外部按钮(复杂场景,如 Modal) + * const formRef = useRef(null); + * + * + * + * + * + * } + * > + * + * * ``` */ -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, FormConfig, ValidationRule, LinkageRule, FormSchema } from './types'; +import React, { useImperativeHandle, forwardRef } from 'react'; +import { Form, message, Button } from 'antd'; +import type { FieldConfig, FormConfig, FormSchema } from './types'; import type { ComponentMeta } from './config'; import { COMPONENT_LIST } from './config'; import { ComponentsContext } from './Designer'; -import FieldRenderer from './components/FieldRenderer'; -import GridFieldPreview from './components/GridFieldPreview'; +import { useFormCore } from './hooks/useFormCore'; +import FormFieldsRenderer from './components/FormFieldsRenderer'; import './styles.css'; /** @@ -116,12 +126,9 @@ const FormRenderer = forwardRef((props, ref) }; const [form] = Form.useForm(); - const [formData, setFormData] = useState>({}); - const [fieldStates, setFieldStates] = useState>({}); + + // 使用核心 hook 管理表单状态 + const { setFormData, fieldStates, handleValuesChange: coreHandleValuesChange } = useFormCore({ fields, form }); const handleSubmit = async () => { try { @@ -204,9 +211,9 @@ const FormRenderer = forwardRef((props, ref) // 监听表单值变化 const handleValuesChange = (_: any, allValues: Record) => { - setFormData(allValues); + coreHandleValuesChange(_, allValues); // 调用核心 hook 的处理 if (props.onChange) { - props.onChange(allValues); + props.onChange(allValues); // 通知外部 } }; @@ -216,218 +223,6 @@ const FormRenderer = forwardRef((props, ref) reset: handleReset, })); - // 将验证规则转换为Ant Design的Rule数组 - const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => { - if (!validationRules || validationRules.length === 0) return []; - - return validationRules.map(rule => { - const antdRule: Rule = { - type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined, - message: rule.message || `请输入正确的${rule.type}`, - }; - - switch (rule.type) { - case 'required': - antdRule.required = true; - break; - case 'pattern': - if (rule.value) { - antdRule.pattern = new RegExp(rule.value); - } - break; - case 'min': - antdRule.type = 'number'; - antdRule.min = rule.value; - break; - case 'max': - antdRule.type = 'number'; - antdRule.max = rule.value; - break; - case 'minLength': - antdRule.min = rule.value; - break; - case 'maxLength': - antdRule.max = rule.value; - break; - case 'phone': - antdRule.pattern = /^1[3-9]\d{9}$/; - break; - case 'idCard': - antdRule.pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/; - break; - } - - return antdRule; - }); - }; - - // 评估联动条件 - const evaluateCondition = (condition: any, formValues: Record): boolean => { - const fieldValue = formValues[condition.field]; - const compareValue = condition.value; - - switch (condition.operator) { - case '==': - return fieldValue == compareValue; - case '!=': - return fieldValue != compareValue; - case '>': - return Number(fieldValue) > Number(compareValue); - case '<': - return Number(fieldValue) < Number(compareValue); - case '>=': - return Number(fieldValue) >= Number(compareValue); - case '<=': - return Number(fieldValue) <= Number(compareValue); - case 'includes': - return Array.isArray(fieldValue) ? fieldValue.includes(compareValue) : String(fieldValue).includes(compareValue); - case 'notIncludes': - return Array.isArray(fieldValue) ? !fieldValue.includes(compareValue) : !String(fieldValue).includes(compareValue); - default: - return false; - } - }; - - // 处理联动规则 - useEffect(() => { - const flattenFields = (fieldList: FieldConfig[]): FieldConfig[] => { - const result: FieldConfig[] = []; - fieldList.forEach(field => { - if (field.type !== 'divider' && field.type !== 'text') { - result.push(field); - } - if (field.type === 'grid' && field.children) { - field.children.forEach(columnFields => { - result.push(...flattenFields(columnFields)); - }); - } - }); - return result; - }; - - const allFields = flattenFields(fields); - const newFieldStates: Record = {}; - - allFields.forEach(field => { - if (field.linkageRules && field.linkageRules.length > 0) { - field.linkageRules.forEach((rule: LinkageRule) => { - const allConditionsMet = rule.conditions.every(condition => - evaluateCondition(condition, formData) - ); - - if (allConditionsMet) { - if (!newFieldStates[field.name]) { - newFieldStates[field.name] = {}; - } - - switch (rule.type) { - case 'visible': - newFieldStates[field.name].visible = rule.action; - break; - case 'disabled': - newFieldStates[field.name].disabled = rule.action; - break; - case 'required': - newFieldStates[field.name].required = rule.action; - break; - case 'value': - form.setFieldValue(field.name, rule.action); - break; - } - } - }); - } - }); - - setFieldStates(newFieldStates); - }, [formData, fields, form]); - - // 设置默认值 - useEffect(() => { - const defaultValues: Record = {}; - - const collectDefaultValues = (fieldList: FieldConfig[]) => { - fieldList.forEach(field => { - if (field.defaultValue !== undefined && field.name) { - if (field.type === 'date' && typeof field.defaultValue === 'string') { - defaultValues[field.name] = dayjs(field.defaultValue); - } else { - defaultValues[field.name] = field.defaultValue; - } - } - if (field.type === 'grid' && field.children) { - field.children.forEach(columnFields => { - collectDefaultValues(columnFields); - }); - } - }); - }; - - collectDefaultValues(fields); - form.setFieldsValue(defaultValues); - setFormData(prev => ({ ...prev, ...defaultValues })); - }, [fields, form]); - - // 递归渲染字段 - const renderFields = (fieldList: FieldConfig[]): React.ReactNode => { - return fieldList.map((field) => { - if (field.type === 'text' || field.type === 'divider') { - return ( -
- -
- ); - } - - if (field.type === 'grid') { - return ( - - ); - } - - const fieldState = fieldStates[field.name] || {}; - 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); - const hasRequiredRule = field.validationRules?.some(rule => rule.type === 'required'); - - const baseRules: Rule[] = []; - if (isRequired && !hasRequiredRule) { - baseRules.push({ - required: true, - message: `请输入${field.label}`, - }); - } - - const allRules = [...baseRules, ...customRules]; - - return ( - - - - ); - }); - }; - if (fields.length === 0) { return (
@@ -453,11 +248,15 @@ const FormRenderer = forwardRef((props, ref) wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined} onValuesChange={handleValuesChange} > - {formConfig.title && ( -

{formConfig.title}

- )} + {formConfig.title && ( +

{formConfig.title}

+ )} - {renderFields(fields)} + {(showSubmit || showCancel) && ( ((props, ref) >
{showSubmit && ( - + )} {showCancel && ( - + )}
diff --git a/frontend/src/components/FormDesigner/components/FormFieldsRenderer.tsx b/frontend/src/components/FormDesigner/components/FormFieldsRenderer.tsx new file mode 100644 index 00000000..59bfffe0 --- /dev/null +++ b/frontend/src/components/FormDesigner/components/FormFieldsRenderer.tsx @@ -0,0 +1,96 @@ +/** + * 表单字段渲染器 + * 递归渲染表单字段,处理普通字段、栅格布局、布局组件 + */ + +import React from 'react'; +import { Form } from 'antd'; +import type { FieldConfig, FormConfig } from '../types'; +import type { FieldState } from '../utils/linkageHelper'; +import { mergeValidationRules } from '../utils/validationHelper'; +import FieldRenderer from './FieldRenderer'; +import GridFieldPreview from './GridFieldPreview'; + +export interface FormFieldsRendererProps { + /** 字段列表 */ + fields: FieldConfig[]; + /** 表单配置 */ + formConfig: FormConfig; + /** 字段状态(来自联动规则) */ + fieldStates: Record; +} + +/** + * 表单字段渲染器组件 + * + * 功能: + * - 递归渲染所有字段 + * - 处理布局组件(文本、分割线) + * - 处理栅格布局 + * - 应用联动规则的字段状态 + * - 应用验证规则 + */ +const FormFieldsRenderer: React.FC = ({ + fields, + formConfig, + fieldStates, +}) => { + const renderFields = (fieldList: FieldConfig[]): React.ReactNode => { + return fieldList.map((field) => { + // 布局组件:文本、分割线 + if (field.type === 'text' || field.type === 'divider') { + return ( +
+ +
+ ); + } + + // 栅格布局:使用专门的预览组件 + if (field.type === 'grid') { + return ( + + ); + } + + // 获取字段状态(联动规则影响) + const fieldState = fieldStates[field.name] || {}; + 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 allRules = mergeValidationRules(field, isRequired); + + // 普通表单组件使用 Form.Item 包裹 + return ( + + + + ); + }); + }; + + return <>{renderFields(fields)}; +}; + +export default FormFieldsRenderer; + diff --git a/frontend/src/components/FormDesigner/components/GridFieldPreview.tsx b/frontend/src/components/FormDesigner/components/GridFieldPreview.tsx index 83cee183..1480588a 100644 --- a/frontend/src/components/FormDesigner/components/GridFieldPreview.tsx +++ b/frontend/src/components/FormDesigner/components/GridFieldPreview.tsx @@ -11,15 +11,11 @@ import FieldRenderer from './FieldRenderer'; interface GridFieldPreviewProps { field: FieldConfig; - formData?: Record; - onFieldChange?: (name: string, value: any) => void; formConfig?: FormConfig; } const GridFieldPreview: React.FC = ({ field, - formData = {}, - onFieldChange, formConfig }) => { const columns = field.columns || 2; @@ -91,15 +87,9 @@ const GridFieldPreview: React.FC = ({ // 根据表单配置决定布局方式 const isVertical = formConfig?.labelAlign === 'top'; - // 合并基础验证规则和自定义验证规则(和FormPreview保持一致) + // 合并基础验证规则和自定义验证规则 const customRules = convertValidationRules(childField.validationRules); - // 🐛 调试:打印验证规则 - if (childField.validationRules && childField.validationRules.length > 0) { - console.log(`📋 [GridFieldPreview] 字段 "${childField.label}" 的验证规则:`, childField.validationRules); - console.log(`✅ [GridFieldPreview] 转换后的规则:`, customRules); - } - // 检查自定义验证规则中是否已经包含必填验证 const hasRequiredRule = childField.validationRules?.some(rule => rule.type === 'required'); @@ -114,13 +104,8 @@ const GridFieldPreview: React.FC = ({ const allRules = [...baseRules, ...customRules]; - // 🐛 调试:打印最终规则 - if (allRules.length > 0) { - console.log(`🎯 [GridFieldPreview] 字段 "${childField.label}" 最终验证规则:`, allRules); - } - // 普通表单字段用 Form.Item 包裹 - // 栅格内字段:根据对齐方式使用不同的布局 + // Form.Item 会自动管理 value 和 onChange,不需要手动传递 return ( = ({ > onFieldChange?.(childField.name, value)} isPreview={true} /> diff --git a/frontend/src/components/FormDesigner/hooks/useFormCore.ts b/frontend/src/components/FormDesigner/hooks/useFormCore.ts new file mode 100644 index 00000000..f5c8c3d2 --- /dev/null +++ b/frontend/src/components/FormDesigner/hooks/useFormCore.ts @@ -0,0 +1,86 @@ +/** + * 表单核心逻辑 Hook + * 集成验证规则、联动规则、默认值等核心功能 + */ + +import { useState, useEffect } from 'react'; +import type { FormInstance } from 'antd'; +import type { FieldConfig } from '../types'; +import { calculateLinkageStates, evaluateCondition, flattenFields, type FieldState } from '../utils/linkageHelper'; +import { collectDefaultValues } from '../utils/defaultValueHelper'; + +export interface UseFormCoreOptions { + /** 字段列表 */ + fields: FieldConfig[]; + /** Ant Design Form 实例 */ + form: FormInstance; +} + +export interface UseFormCoreReturn { + /** 表单数据 */ + formData: Record; + /** 设置表单数据 */ + setFormData: React.Dispatch>>; + /** 字段状态(联动规则影响) */ + fieldStates: Record; + /** 值变化处理函数 */ + handleValuesChange: (_: any, allValues: Record) => void; +} + +/** + * 表单核心逻辑 Hook + * + * 功能: + * - 管理表单数据状态 + * - 处理联动规则 + * - 设置默认值 + * - 处理值联动(自动设置字段值) + */ +export const useFormCore = ({ fields, form }: UseFormCoreOptions): UseFormCoreReturn => { + const [formData, setFormData] = useState>({}); + const [fieldStates, setFieldStates] = useState>({}); + + // 处理表单值变化 + const handleValuesChange = (_: any, allValues: Record) => { + setFormData(allValues); + }; + + // 处理联动规则(包括值联动) + useEffect(() => { + // 1. 计算字段状态(visible、disabled、required) + const newFieldStates = calculateLinkageStates(fields, formData); + setFieldStates(newFieldStates); + + // 2. 处理值联动(需要直接操作 form) + const allFields = flattenFields(fields); + allFields.forEach(field => { + if (field.linkageRules && field.linkageRules.length > 0) { + field.linkageRules.forEach(rule => { + if (rule.type === 'value') { + const allConditionsMet = rule.conditions.every(condition => + evaluateCondition(condition, formData) + ); + if (allConditionsMet) { + form.setFieldValue(field.name, rule.action); + } + } + }); + } + }); + }, [formData, fields, form]); + + // 设置默认值 + useEffect(() => { + const defaultValues = collectDefaultValues(fields); + form.setFieldsValue(defaultValues); + setFormData(prev => ({ ...prev, ...defaultValues })); + }, [fields, form]); + + return { + formData, + setFormData, + fieldStates, + handleValuesChange, + }; +}; + diff --git a/frontend/src/components/FormDesigner/utils/defaultValueHelper.ts b/frontend/src/components/FormDesigner/utils/defaultValueHelper.ts new file mode 100644 index 00000000..c36c910e --- /dev/null +++ b/frontend/src/components/FormDesigner/utils/defaultValueHelper.ts @@ -0,0 +1,41 @@ +/** + * 表单默认值处理工具 + * 处理字段的默认值,包括日期类型的特殊处理 + */ + +import dayjs from 'dayjs'; +import type { FieldConfig } from '../types'; + +/** + * 收集字段的默认值 + * @param fieldList 字段列表 + * @returns 默认值对象 + */ +export const collectDefaultValues = (fieldList: FieldConfig[]): Record => { + const defaultValues: Record = {}; + + const collect = (fields: FieldConfig[]) => { + fields.forEach(field => { + // 处理有默认值的字段 + if (field.defaultValue !== undefined && field.name) { + // 日期字段需要转换为 dayjs 对象 + if (field.type === 'date' && typeof field.defaultValue === 'string') { + defaultValues[field.name] = dayjs(field.defaultValue); + } else { + defaultValues[field.name] = field.defaultValue; + } + } + + // 递归处理栅格布局内的字段 + if (field.type === 'grid' && field.children) { + field.children.forEach(columnFields => { + collect(columnFields); + }); + } + }); + }; + + collect(fieldList); + return defaultValues; +}; + diff --git a/frontend/src/components/FormDesigner/utils/linkageHelper.ts b/frontend/src/components/FormDesigner/utils/linkageHelper.ts new file mode 100644 index 00000000..4d55bebc --- /dev/null +++ b/frontend/src/components/FormDesigner/utils/linkageHelper.ts @@ -0,0 +1,126 @@ +/** + * 表单联动规则工具 + * 处理字段之间的联动逻辑 + */ + +import type { FieldConfig, LinkageCondition } from '../types'; + +/** + * 评估联动条件 + * @param condition 联动条件 + * @param formValues 表单值 + * @returns 条件是否满足 + */ +export const evaluateCondition = ( + condition: LinkageCondition, + formValues: Record +): boolean => { + const fieldValue = formValues[condition.field]; + const compareValue = condition.value; + + switch (condition.operator) { + case '==': + return fieldValue == compareValue; + case '!=': + return fieldValue != compareValue; + case '>': + return Number(fieldValue) > Number(compareValue); + case '<': + return Number(fieldValue) < Number(compareValue); + case '>=': + return Number(fieldValue) >= Number(compareValue); + case '<=': + return Number(fieldValue) <= Number(compareValue); + case 'includes': + return Array.isArray(fieldValue) + ? fieldValue.includes(compareValue) + : String(fieldValue).includes(compareValue); + case 'notIncludes': + return Array.isArray(fieldValue) + ? !fieldValue.includes(compareValue) + : !String(fieldValue).includes(compareValue); + default: + return false; + } +}; + +/** + * 扁平化字段列表(包括栅格内的字段) + * @param fieldList 字段列表 + * @returns 扁平化后的字段数组 + */ +export const flattenFields = (fieldList: FieldConfig[]): FieldConfig[] => { + const result: FieldConfig[] = []; + + fieldList.forEach(field => { + // 排除布局组件 + if (field.type !== 'divider' && field.type !== 'text') { + result.push(field); + } + + // 递归处理栅格布局内的字段 + if (field.type === 'grid' && field.children) { + field.children.forEach(columnFields => { + result.push(...flattenFields(columnFields)); + }); + } + }); + + return result; +}; + +/** + * 字段状态接口 + */ +export interface FieldState { + visible?: boolean; + disabled?: boolean; + required?: boolean; +} + +/** + * 计算字段联动状态 + * @param fields 字段列表 + * @param formData 表单数据 + * @returns 字段状态映射 + */ +export const calculateLinkageStates = ( + fields: FieldConfig[], + formData: Record +): Record => { + const allFields = flattenFields(fields); + const fieldStates: Record = {}; + + allFields.forEach(field => { + if (field.linkageRules && field.linkageRules.length > 0) { + field.linkageRules.forEach(rule => { + // 检查所有条件是否都满足 + const allConditionsMet = rule.conditions.every(condition => + evaluateCondition(condition, formData) + ); + + if (allConditionsMet) { + if (!fieldStates[field.name]) { + fieldStates[field.name] = {}; + } + + switch (rule.type) { + case 'visible': + fieldStates[field.name].visible = rule.action; + break; + case 'disabled': + fieldStates[field.name].disabled = rule.action; + break; + case 'required': + fieldStates[field.name].required = rule.action; + break; + // 'value' 类型需要在调用方处理,因为需要访问 form 实例 + } + } + }); + } + }); + + return fieldStates; +}; + diff --git a/frontend/src/components/FormDesigner/utils/validationHelper.ts b/frontend/src/components/FormDesigner/utils/validationHelper.ts new file mode 100644 index 00000000..879cb677 --- /dev/null +++ b/frontend/src/components/FormDesigner/utils/validationHelper.ts @@ -0,0 +1,86 @@ +/** + * 表单验证规则转换工具 + * 将自定义的 ValidationRule 转换为 Ant Design 的 Rule 数组 + */ + +import type { Rule } from 'antd/es/form'; +import type { ValidationRule } from '../types'; + +/** + * 将验证规则转换为 Ant Design 的 Rule 数组 + * @param validationRules 自定义验证规则 + * @returns Ant Design Rule 数组 + */ +export const convertValidationRules = (validationRules?: ValidationRule[]): Rule[] => { + if (!validationRules || validationRules.length === 0) return []; + + return validationRules.map(rule => { + const antdRule: Rule = { + type: rule.type === 'email' ? 'email' : rule.type === 'url' ? 'url' : undefined, + message: rule.message || `请输入正确的${rule.type}`, + }; + + switch (rule.type) { + case 'required': + antdRule.required = true; + break; + case 'pattern': + if (rule.value) { + antdRule.pattern = new RegExp(rule.value); + } + break; + case 'min': + antdRule.type = 'number'; + antdRule.min = rule.value; + break; + case 'max': + antdRule.type = 'number'; + antdRule.max = rule.value; + break; + case 'minLength': + antdRule.min = rule.value; + break; + case 'maxLength': + antdRule.max = rule.value; + break; + case 'phone': + antdRule.pattern = /^1[3-9]\d{9}$/; + break; + case 'idCard': + antdRule.pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/; + break; + } + + return antdRule; + }); +}; + +/** + * 合并字段的验证规则 + * @param field 字段配置 + * @param isRequired 是否必填(来自联动规则) + * @returns 合并后的验证规则 + */ +export const mergeValidationRules = ( + field: { validationRules?: ValidationRule[]; required?: boolean; label?: string }, + isRequired?: boolean +): Rule[] => { + const customRules = convertValidationRules(field.validationRules); + + // 检查自定义验证规则中是否已经包含必填验证 + const hasRequiredRule = field.validationRules?.some(rule => rule.type === 'required'); + + // 基础规则:只有在没有自定义必填验证时,才使用字段属性中的"是否必填" + const baseRules: Rule[] = []; + const finalRequired = isRequired !== undefined ? isRequired : field.required; + + if (finalRequired && !hasRequiredRule) { + baseRules.push({ + required: true, + message: `请输入${field.label}`, + }); + } + + return [...baseRules, ...customRules]; +}; + diff --git a/frontend/src/pages/FormDesigner/index.tsx b/frontend/src/pages/FormDesigner/index.tsx index 5e1e8f08..857e67fa 100644 --- a/frontend/src/pages/FormDesigner/index.tsx +++ b/frontend/src/pages/FormDesigner/index.tsx @@ -6,10 +6,11 @@ * - FormRenderer: 表单渲染器(运行时) */ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { message, Tabs, Card, Button, Modal, Space, Divider, Alert } from 'antd'; -import { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner'; +import { FormDesigner, FormRenderer, type FormSchema, type FormRendererRef } from '@/components/FormDesigner'; import { WORKFLOW_COMPONENTS } from '@/components/FormDesigner/extensions/workflow'; +import { EditOutlined } from '@ant-design/icons'; const FormDesignerExamplesPage: React.FC = () => { // ==================== 示例 1: 普通表单设计器 ==================== @@ -41,7 +42,22 @@ const FormDesignerExamplesPage: React.FC = () => { const handleStaticFormSubmit = async (data: Record) => { console.log('📤 静态表单提交数据:', data); + + // 模拟 API 调用 + await new Promise(resolve => setTimeout(resolve, 500)); + + // 返回模拟的服务器响应 + const response = { + success: true, + id: Math.floor(Math.random() * 10000), + timestamp: new Date().toISOString(), + data: data, + }; + + console.log('✅ 服务器响应:', response); message.success('表单提交成功!请查看控制台'); + + return response; // 返回响应给 afterSubmit 钩子 }; // ==================== 示例 3: 工作流表单(弹窗) ==================== @@ -90,8 +106,22 @@ const FormDesignerExamplesPage: React.FC = () => { const handleWorkflowSubmit = async (data: Record) => { console.log('🚀 工作流启动参数:', data); + + // 模拟工作流启动 API + await new Promise(resolve => setTimeout(resolve, 800)); + + const response = { + success: true, + workflowId: `WF-${Date.now()}`, + status: 'RUNNING', + data: data, + }; + + console.log('✅ 工作流启动响应:', response); message.success('工作流已启动!'); setWorkflowModalVisible(false); + + return response; }; // ==================== 示例 4: 只读模式 ==================== @@ -205,7 +235,21 @@ const FormDesignerExamplesPage: React.FC = () => { const handleComplexFormSubmit = async (data: Record) => { console.log('📋 复杂表单提交数据:', data); + + // 模拟 API 调用 + await new Promise(resolve => setTimeout(resolve, 600)); + + const response = { + success: true, + id: Math.floor(Math.random() * 10000), + message: '数据已成功保存', + data: data, + }; + + console.log('✅ 服务器响应:', response); message.success('表单提交成功!'); + + return response; }; // ==================== 示例 6: 带生命周期钩子的表单 ==================== @@ -300,6 +344,95 @@ const FormDesignerExamplesPage: React.FC = () => { message.error(`操作失败: ${error.message || '未知错误'}`); }; + // ==================== 示例 7: Modal 中使用 FormRenderer(外部按钮) ==================== + const [modalFormVisible, setModalFormVisible] = useState(false); + const [modalFormLoading, setModalFormLoading] = useState(false); + const modalFormRef = useRef(null); + + const modalFormSchema: FormSchema = { + version: '1.0', + formConfig: { + labelAlign: 'right', + size: 'middle', + formWidth: 500 + }, + fields: [ + { + id: 'modal_field_1', + type: 'input', + label: '姓名', + name: 'name', + placeholder: '请输入姓名', + required: true + }, + { + id: 'modal_field_2', + type: 'input', + label: '手机号', + name: 'phone', + placeholder: '请输入手机号', + required: true, + validationRules: [ + { type: 'phone', message: '请输入正确的手机号' } + ] + }, + { + id: 'modal_field_3', + type: 'select', + label: '部门', + name: 'department', + placeholder: '请选择部门', + dataSourceType: 'predefined', + predefinedDataSource: { + sourceType: 'DEPARTMENTS' + }, + options: [] + }, + { + id: 'modal_field_4', + type: 'textarea', + label: '备注', + name: 'remark', + placeholder: '请输入备注', + rows: 3 + }, + ], + }; + + const handleModalFormSubmit = async (data: Record) => { + console.log('📤 Modal 表单提交数据:', data); + + // 模拟 API 调用 + setModalFormLoading(true); + await new Promise(resolve => setTimeout(resolve, 1500)); + + const response = { + success: true, + id: Math.floor(Math.random() * 10000), + message: '员工信息已保存', + data: data, + }; + + console.log('✅ 服务器响应:', response); + message.success('员工信息保存成功!'); + + setModalFormVisible(false); + setModalFormLoading(false); + + return response; + }; + + const handleModalFormCancel = () => { + Modal.confirm({ + title: '确认取消', + content: '表单数据将不会被保存,确定要取消吗?', + onOk: () => { + modalFormRef.current?.reset(); + setModalFormVisible(false); + }, + }); + }; + // ==================== Tab 配置 ==================== const tabItems = [ { @@ -608,6 +741,151 @@ const FormDesignerExamplesPage: React.FC = () => { ), }, + { + key: '7', + label: '🪟 示例 7: Modal 弹窗(外部按钮)', + children: ( + +

Modal 弹窗中使用 FormRenderer(外部按钮 + useRef)

+

+ 演示如何在 Modal 中使用 FormRenderer,通过 useRef 控制表单提交,实现自定义按钮布局和 loading 状态。 +

+ +

✅ 使用 useRef<FormRendererRef> 引用表单实例

+

✅ 在 Modal footer 中放置自定义按钮

+

✅ 通过 formRef.current?.submit() 触发提交

+

✅ 通过 formRef.current?.reset() 重置表单

+

✅ 支持 loading 状态、图标、确认对话框等

+
+ } + type="info" + showIcon + style={{ marginBottom: 16 }} + /> +
+ 核心代码:
+ {`// 1. 创建 ref +const formRef = useRef(null); +const [loading, setLoading] = useState(false); + +// 2. 提交处理 +const handleSubmit = async () => { + setLoading(true); + try { + await formRef.current?.submit(); + } finally { + setLoading(false); + } +}; + +// 3. 在 Modal 中使用 + + + + + } +> + +`} +
+ + + + + + modalFormRef.current?.reset()} + > + 重置 + , + , + , + ]} + destroyOnClose + > + + + + +
  • Modal 弹窗表单
  • +
  • Drawer 抽屉表单
  • +
  • 需要自定义按钮样式和位置
  • +
  • 需要 loading 状态或图标
  • +
  • 多步骤表单(上一步/下一步)
  • + + } + type="success" + showIcon + style={{ marginTop: 16 }} + /> + + ), + }, ]; return ( diff --git a/frontend/src/pages/Workflow/Definition/components/StartWorkflowModal.tsx b/frontend/src/pages/Workflow/Definition/components/StartWorkflowModal.tsx new file mode 100644 index 00000000..ff049ff2 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/components/StartWorkflowModal.tsx @@ -0,0 +1,138 @@ +/** + * 启动工作流弹窗组件 + * 在工作流定义列表页使用,用于启动工作流实例 + */ + +import React, { useRef, useState } from 'react'; +import { Modal } from 'antd'; +import { Button } from '@/components/ui/button'; +import { FormRenderer, type FormRendererRef } from '@/components/FormDesigner'; +import type { FormDefinitionResponse } from '@/pages/Form/Definition/types'; +import type { WorkflowDefinition } from '../types'; + +interface StartWorkflowModalProps { + /** 是否显示弹窗 */ + open: boolean; + /** 关闭弹窗回调 */ + onClose: () => void; + /** 工作流定义 */ + workflowDefinition: WorkflowDefinition | null; + /** 表单定义数据 */ + formDefinition: FormDefinitionResponse | null; + /** 提交回调 */ + onSubmit: (workflowDefinition: WorkflowDefinition, formData: Record) => Promise; +} + +const StartWorkflowModal: React.FC = ({ + open, + onClose, + workflowDefinition, + formDefinition, + onSubmit, +}) => { + const formRef = useRef(null); + const [loading, setLoading] = useState(false); + + // 处理提交 + const handleSubmit = async (formData: Record) => { + if (!workflowDefinition) { + return; + } + + console.log('🚀 启动工作流:', { + workflowId: workflowDefinition.id, + workflowName: workflowDefinition.name, + workflowKey: workflowDefinition.key, + formData: formData, + }); + + await onSubmit(workflowDefinition, formData); + onClose(); + + return { + success: true, + workflowDefinitionId: workflowDefinition.id, + data: formData, + }; + }; + + // 处理取消 + const handleCancel = () => { + Modal.confirm({ + title: '确认取消', + content: '工作流将不会启动,确定要取消吗?', + onOk: () => { + formRef.current?.reset(); + onClose(); + }, + }); + }; + + // 处理表单提交触发 + const handleTriggerSubmit = async () => { + setLoading(true); + try { + await formRef.current?.submit(); + } catch (error) { + console.error('启动工作流失败:', error); + } finally { + setLoading(false); + } + }; + + if (!workflowDefinition || !formDefinition) { + return null; + } + + const { schema } = formDefinition; + + return ( + formRef.current?.reset()}> + 重置 + , + , + , + ]} + destroyOnClose + > + + + {/* 表单元信息 */} +
    +
    工作流标识:{workflowDefinition.key}
    +
    工作流版本:v{workflowDefinition.flowVersion}
    +
    表单标识:{formDefinition.key}
    +
    表单版本:v{formDefinition.formVersion}
    +
    +
    + ); +}; + +export default StartWorkflowModal; + diff --git a/frontend/src/pages/Workflow/Definition/index.tsx b/frontend/src/pages/Workflow/Definition/index.tsx index d44ff439..dd3ddb90 100644 --- a/frontend/src/pages/Workflow/Definition/index.tsx +++ b/frontend/src/pages/Workflow/Definition/index.tsx @@ -13,13 +13,16 @@ import { } from 'lucide-react'; import { useToast } from '@/components/ui/use-toast'; import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service'; -import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse } from './types'; +import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse, WorkflowDefinitionStatus } from './types'; import type { Page } from '@/types/base'; import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; import EditModal from './components/EditModal'; import DeleteDialog from './components/DeleteDialog'; import DeployDialog from './components/DeployDialog'; import CategoryManageDialog from './components/CategoryManageDialog'; +import StartWorkflowModal from './components/StartWorkflowModal'; +import { getDefinitionById as getFormDefinitionById } from '@/pages/Form/Definition/service'; +import type { FormDefinitionResponse } from '@/pages/Form/Definition/types'; /** * 工作流定义列表页 @@ -37,6 +40,9 @@ const WorkflowDefinitionList: React.FC = () => { const [deployDialogOpen, setDeployDialogOpen] = useState(false); const [deployRecord, setDeployRecord] = useState(null); const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); + const [startModalVisible, setStartModalVisible] = useState(false); + const [startRecord, setStartRecord] = useState(null); + const [formDefinition, setFormDefinition] = useState(null); const [query, setQuery] = useState({ pageNum: DEFAULT_CURRENT - 1, pageSize: DEFAULT_PAGE_SIZE, @@ -163,21 +169,75 @@ const WorkflowDefinitionList: React.FC = () => { } }; - // 启动 + // 启动工作流 const handleStart = async (record: WorkflowDefinition) => { + // 检查是否有关联的启动表单 + if (record.formDefinitionId) { + // 加载表单定义数据 + try { + const formDef = await getFormDefinitionById(record.formDefinitionId); + setFormDefinition(formDef); + setStartRecord(record); + setStartModalVisible(true); + } catch (error) { + console.error('加载启动表单失败:', error); + toast({ + title: '加载失败', + description: '无法加载启动表单,请稍后重试', + variant: 'destructive' + }); + } + } else { + // 没有启动表单,直接启动(不传递表单数据) + try { + const result = await startWorkflowInstance({ + processKey: record.key + }); + console.log('🚀 工作流启动成功:', result); + toast({ + title: '启动成功', + description: `工作流 "${record.name}" 已启动`, + }); + loadData(); // 刷新列表 + } catch (error) { + console.error('启动失败:', error); + toast({ + title: '启动失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + } + }; + + // 处理启动工作流提交(带表单数据) + const handleStartWorkflowSubmit = async (workflow: WorkflowDefinition, formData: Record) => { try { - await startWorkflowInstance(record.key, record.categoryId); + console.log('🚀 启动工作流,携带表单数据:', formData); + + // 调用启动API,传递表单数据作为流程变量 + const result = await startWorkflowInstance({ + processKey: workflow.key, // 流程定义key + variables: formData, // 表单数据作为流程变量 + businessKey: undefined // 自动生成 businessKey + }); + + console.log('✅ 工作流实例已创建:', result); + toast({ title: '启动成功', - description: `工作流 "${record.name}" 已启动`, + description: `工作流 "${workflow.name}" 已启动`, }); + + loadData(); // 刷新列表 } catch (error) { - console.error('启动失败:', error); + console.error('❌ 启动失败:', error); toast({ title: '启动失败', description: error instanceof Error ? error.message : '未知错误', variant: 'destructive' }); + throw error; // 重新抛出让弹窗知道失败了 } }; @@ -298,7 +358,7 @@ const WorkflowDefinitionList: React.FC = () => {