增加审批组件
This commit is contained in:
parent
ef3e34b85d
commit
f8583ba3ef
@ -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<FormPreviewRef, FormPreviewProps>(({ fields, formConfig }, ref) => {
|
||||
const [form] = Form.useForm();
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [fieldStates, setFieldStates] = useState<Record<string, {
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}>>({});
|
||||
|
||||
// 使用核心 hook 管理表单状态
|
||||
const { formData, setFormData, fieldStates, handleValuesChange } = useFormCore({ fields, form });
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@ -92,247 +87,6 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ 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<string, any>): 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<string, any> = {};
|
||||
|
||||
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<string, any> = {};
|
||||
|
||||
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 (
|
||||
<div key={field.id}>
|
||||
<FieldRenderer field={field} isPreview={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 栅格布局:使用专门的预览组件,自动处理内部字段的 Form.Item
|
||||
if (field.type === 'grid') {
|
||||
return (
|
||||
<GridFieldPreview
|
||||
key={field.id}
|
||||
field={field}
|
||||
formData={formData}
|
||||
onFieldChange={(name, value) => 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 (
|
||||
<Form.Item
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
colon={true}
|
||||
rules={allRules}
|
||||
>
|
||||
<FieldRenderer
|
||||
field={{ ...field, disabled: isDisabled }}
|
||||
value={formData[field.name]}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, [field.name]: value }))}
|
||||
isPreview={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (fields.length === 0) {
|
||||
return (
|
||||
@ -356,12 +110,17 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
|
||||
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
|
||||
labelCol={labelColSpan ? { span: labelColSpan } : undefined}
|
||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
{formConfig.title && (
|
||||
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
||||
)}
|
||||
|
||||
{renderFields(fields)}
|
||||
<FormFieldsRenderer
|
||||
fields={fields}
|
||||
formConfig={formConfig}
|
||||
fieldStates={fieldStates}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
{Object.keys(formData).length > 0 && (
|
||||
|
||||
@ -7,40 +7,50 @@
|
||||
* - ✅ 支持验证规则和联动规则
|
||||
* - ✅ 支持动态数据源(API、预定义)
|
||||
* - ✅ 非受控组件,内部管理状态
|
||||
* - ✅ 支持生命周期钩子(后续补充)
|
||||
* - ✅ 支持生命周期钩子(beforeSubmit/afterSubmit/onError)
|
||||
* - ✅ 用于生产环境的真实表单提交
|
||||
* - ✅ 支持内置按钮和外部按钮两种模式
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 使用示例
|
||||
* import { FormRenderer } from '@/components/FormDesigner';
|
||||
* // 方式1: 使用内置按钮(简单场景)
|
||||
* <FormRenderer
|
||||
* schema={schema}
|
||||
* onSubmit={handleSubmit}
|
||||
* showSubmit={true}
|
||||
* showCancel={true}
|
||||
* submitText="保存"
|
||||
* cancelText="取消"
|
||||
* />
|
||||
*
|
||||
* const MyComponent = () => {
|
||||
* const formSchema = await getFormDefinitionById(id);
|
||||
*
|
||||
* return (
|
||||
* <FormRenderer
|
||||
* fields={formSchema.fields}
|
||||
* formConfig={formSchema.formConfig}
|
||||
* onSubmit={async (values) => {
|
||||
* await api.submitForm(values);
|
||||
* }}
|
||||
* />
|
||||
* );
|
||||
* };
|
||||
* // 方式2: 使用外部按钮(复杂场景,如 Modal)
|
||||
* const formRef = useRef<FormRendererRef>(null);
|
||||
*
|
||||
* <Modal
|
||||
* footer={
|
||||
* <>
|
||||
* <Button onClick={() => formRef.current?.reset()}>重置</Button>
|
||||
* <Button type="primary" onClick={() => formRef.current?.submit()}>提交</Button>
|
||||
* </>
|
||||
* }
|
||||
* >
|
||||
* <FormRenderer
|
||||
* ref={formRef}
|
||||
* schema={schema}
|
||||
* onSubmit={handleSubmit}
|
||||
* />
|
||||
* </Modal>
|
||||
* ```
|
||||
*/
|
||||
|
||||
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<FormRendererRef, FormRendererProps>((props, ref)
|
||||
};
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [fieldStates, setFieldStates] = useState<Record<string, {
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}>>({});
|
||||
|
||||
// 使用核心 hook 管理表单状态
|
||||
const { setFormData, fieldStates, handleValuesChange: coreHandleValuesChange } = useFormCore({ fields, form });
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@ -204,9 +211,9 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
|
||||
|
||||
// 监听表单值变化
|
||||
const handleValuesChange = (_: any, allValues: Record<string, any>) => {
|
||||
setFormData(allValues);
|
||||
coreHandleValuesChange(_, allValues); // 调用核心 hook 的处理
|
||||
if (props.onChange) {
|
||||
props.onChange(allValues);
|
||||
props.onChange(allValues); // 通知外部
|
||||
}
|
||||
};
|
||||
|
||||
@ -216,218 +223,6 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((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<string, any>): 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<string, any> = {};
|
||||
|
||||
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<string, any> = {};
|
||||
|
||||
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 (
|
||||
<div key={field.id}>
|
||||
<FieldRenderer field={field} isPreview={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'grid') {
|
||||
return (
|
||||
<GridFieldPreview
|
||||
key={field.id}
|
||||
field={field}
|
||||
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);
|
||||
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 (
|
||||
<Form.Item
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
colon={true}
|
||||
rules={allRules}
|
||||
>
|
||||
<FieldRenderer
|
||||
field={{ ...field, disabled: isDisabled }}
|
||||
isPreview={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (fields.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center', color: '#8c8c8c' }}>
|
||||
@ -453,11 +248,15 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
|
||||
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
{formConfig.title && (
|
||||
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
||||
)}
|
||||
{formConfig.title && (
|
||||
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
|
||||
)}
|
||||
|
||||
{renderFields(fields)}
|
||||
<FormFieldsRenderer
|
||||
fields={fields}
|
||||
formConfig={formConfig}
|
||||
fieldStates={fieldStates}
|
||||
/>
|
||||
|
||||
{(showSubmit || showCancel) && (
|
||||
<Form.Item
|
||||
@ -466,38 +265,21 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{showSubmit && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
padding: '4px 15px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: '#1677ff',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{submitText}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{showCancel && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
htmlType="button"
|
||||
onClick={onCancel || handleReset}
|
||||
style={{
|
||||
padding: '4px 15px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #d9d9d9',
|
||||
background: 'white',
|
||||
color: 'rgba(0, 0, 0, 0.88)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
@ -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<string, FieldState>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单字段渲染器组件
|
||||
*
|
||||
* 功能:
|
||||
* - 递归渲染所有字段
|
||||
* - 处理布局组件(文本、分割线)
|
||||
* - 处理栅格布局
|
||||
* - 应用联动规则的字段状态
|
||||
* - 应用验证规则
|
||||
*/
|
||||
const FormFieldsRenderer: React.FC<FormFieldsRendererProps> = ({
|
||||
fields,
|
||||
formConfig,
|
||||
fieldStates,
|
||||
}) => {
|
||||
const renderFields = (fieldList: FieldConfig[]): React.ReactNode => {
|
||||
return fieldList.map((field) => {
|
||||
// 布局组件:文本、分割线
|
||||
if (field.type === 'text' || field.type === 'divider') {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
<FieldRenderer field={field} isPreview={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 栅格布局:使用专门的预览组件
|
||||
if (field.type === 'grid') {
|
||||
return (
|
||||
<GridFieldPreview
|
||||
key={field.id}
|
||||
field={field}
|
||||
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 allRules = mergeValidationRules(field, isRequired);
|
||||
|
||||
// 普通表单组件使用 Form.Item 包裹
|
||||
return (
|
||||
<Form.Item
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
colon={true}
|
||||
rules={allRules}
|
||||
>
|
||||
<FieldRenderer
|
||||
field={{ ...field, disabled: isDisabled }}
|
||||
isPreview={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return <>{renderFields(fields)}</>;
|
||||
};
|
||||
|
||||
export default FormFieldsRenderer;
|
||||
|
||||
@ -11,15 +11,11 @@ import FieldRenderer from './FieldRenderer';
|
||||
|
||||
interface GridFieldPreviewProps {
|
||||
field: FieldConfig;
|
||||
formData?: Record<string, any>;
|
||||
onFieldChange?: (name: string, value: any) => void;
|
||||
formConfig?: FormConfig;
|
||||
}
|
||||
|
||||
const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
||||
field,
|
||||
formData = {},
|
||||
onFieldChange,
|
||||
formConfig
|
||||
}) => {
|
||||
const columns = field.columns || 2;
|
||||
@ -91,15 +87,9 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
||||
// 根据表单配置决定布局方式
|
||||
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<GridFieldPreviewProps> = ({
|
||||
|
||||
const allRules = [...baseRules, ...customRules];
|
||||
|
||||
// 🐛 调试:打印最终规则
|
||||
if (allRules.length > 0) {
|
||||
console.log(`🎯 [GridFieldPreview] 字段 "${childField.label}" 最终验证规则:`, allRules);
|
||||
}
|
||||
|
||||
// 普通表单字段用 Form.Item 包裹
|
||||
// 栅格内字段:根据对齐方式使用不同的布局
|
||||
// Form.Item 会自动管理 value 和 onChange,不需要手动传递
|
||||
return (
|
||||
<Form.Item
|
||||
key={childField.id}
|
||||
@ -133,8 +118,6 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
|
||||
>
|
||||
<FieldRenderer
|
||||
field={childField}
|
||||
value={formData[childField.name]}
|
||||
onChange={(value) => onFieldChange?.(childField.name, value)}
|
||||
isPreview={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
86
frontend/src/components/FormDesigner/hooks/useFormCore.ts
Normal file
86
frontend/src/components/FormDesigner/hooks/useFormCore.ts
Normal file
@ -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<string, any>;
|
||||
/** 设置表单数据 */
|
||||
setFormData: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
/** 字段状态(联动规则影响) */
|
||||
fieldStates: Record<string, FieldState>;
|
||||
/** 值变化处理函数 */
|
||||
handleValuesChange: (_: any, allValues: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单核心逻辑 Hook
|
||||
*
|
||||
* 功能:
|
||||
* - 管理表单数据状态
|
||||
* - 处理联动规则
|
||||
* - 设置默认值
|
||||
* - 处理值联动(自动设置字段值)
|
||||
*/
|
||||
export const useFormCore = ({ fields, form }: UseFormCoreOptions): UseFormCoreReturn => {
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [fieldStates, setFieldStates] = useState<Record<string, FieldState>>({});
|
||||
|
||||
// 处理表单值变化
|
||||
const handleValuesChange = (_: any, allValues: Record<string, any>) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 表单默认值处理工具
|
||||
* 处理字段的默认值,包括日期类型的特殊处理
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import type { FieldConfig } from '../types';
|
||||
|
||||
/**
|
||||
* 收集字段的默认值
|
||||
* @param fieldList 字段列表
|
||||
* @returns 默认值对象
|
||||
*/
|
||||
export const collectDefaultValues = (fieldList: FieldConfig[]): Record<string, any> => {
|
||||
const defaultValues: Record<string, any> = {};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
126
frontend/src/components/FormDesigner/utils/linkageHelper.ts
Normal file
126
frontend/src/components/FormDesigner/utils/linkageHelper.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 表单联动规则工具
|
||||
* 处理字段之间的联动逻辑
|
||||
*/
|
||||
|
||||
import type { FieldConfig, LinkageCondition } from '../types';
|
||||
|
||||
/**
|
||||
* 评估联动条件
|
||||
* @param condition 联动条件
|
||||
* @param formValues 表单值
|
||||
* @returns 条件是否满足
|
||||
*/
|
||||
export const evaluateCondition = (
|
||||
condition: LinkageCondition,
|
||||
formValues: Record<string, any>
|
||||
): 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<string, any>
|
||||
): Record<string, FieldState> => {
|
||||
const allFields = flattenFields(fields);
|
||||
const fieldStates: Record<string, FieldState> = {};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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];
|
||||
};
|
||||
|
||||
@ -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<string, any>) => {
|
||||
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<string, any>) => {
|
||||
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<string, any>) => {
|
||||
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<FormRendererRef>(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<string, any>) => {
|
||||
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 = () => {
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
label: '🪟 示例 7: Modal 弹窗(外部按钮)',
|
||||
children: (
|
||||
<Card>
|
||||
<h2>Modal 弹窗中使用 FormRenderer(外部按钮 + useRef)</h2>
|
||||
<p style={{ marginBottom: 16, color: '#666' }}>
|
||||
演示如何在 Modal 中使用 FormRenderer,通过 useRef 控制表单提交,实现自定义按钮布局和 loading 状态。
|
||||
</p>
|
||||
<Alert
|
||||
message="💡 核心要点"
|
||||
description={
|
||||
<div>
|
||||
<p>✅ 使用 <code>useRef<FormRendererRef></code> 引用表单实例</p>
|
||||
<p>✅ 在 Modal footer 中放置自定义按钮</p>
|
||||
<p>✅ 通过 <code>formRef.current?.submit()</code> 触发提交</p>
|
||||
<p>✅ 通过 <code>formRef.current?.reset()</code> 重置表单</p>
|
||||
<p>✅ 支持 loading 状态、图标、确认对话框等</p>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<div style={{
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 4,
|
||||
marginBottom: 16,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}>
|
||||
<strong>核心代码:</strong><br />
|
||||
{`// 1. 创建 ref
|
||||
const formRef = useRef<FormRendererRef>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 2. 提交处理
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await formRef.current?.submit();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 在 Modal 中使用
|
||||
<Modal
|
||||
title="编辑信息"
|
||||
open={visible}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => formRef.current?.reset()}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormRenderer
|
||||
ref={formRef}
|
||||
schema={schema}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Modal>`}
|
||||
</div>
|
||||
<Divider />
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setModalFormVisible(true)}
|
||||
>
|
||||
打开 Modal 表单
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
title="编辑员工信息"
|
||||
open={modalFormVisible}
|
||||
onCancel={handleModalFormCancel}
|
||||
width={600}
|
||||
footer={[
|
||||
<Button
|
||||
key="reset"
|
||||
onClick={() => modalFormRef.current?.reset()}
|
||||
>
|
||||
重置
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={handleModalFormCancel}
|
||||
>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={modalFormLoading}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await modalFormRef.current?.submit();
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
>
|
||||
<FormRenderer
|
||||
ref={modalFormRef}
|
||||
schema={modalFormSchema}
|
||||
onSubmit={handleModalFormSubmit}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Alert
|
||||
message="✨ 适用场景"
|
||||
description={
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||
<li>Modal 弹窗表单</li>
|
||||
<li>Drawer 抽屉表单</li>
|
||||
<li>需要自定义按钮样式和位置</li>
|
||||
<li>需要 loading 状态或图标</li>
|
||||
<li>多步骤表单(上一步/下一步)</li>
|
||||
</ul>
|
||||
}
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@ -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<string, any>) => Promise<void>;
|
||||
}
|
||||
|
||||
const StartWorkflowModal: React.FC<StartWorkflowModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
workflowDefinition,
|
||||
formDefinition,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const formRef = useRef<FormRendererRef>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = async (formData: Record<string, any>) => {
|
||||
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 (
|
||||
<Modal
|
||||
title={`启动工作流:${workflowDefinition.name}`}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
width={schema.formConfig.formWidth || 600}
|
||||
footer={[
|
||||
<Button key="reset" variant="outline" onClick={() => formRef.current?.reset()}>
|
||||
重置
|
||||
</Button>,
|
||||
<Button key="cancel" variant="outline" onClick={handleCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
onClick={handleTriggerSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '启动中...' : '启动工作流'}
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
>
|
||||
<FormRenderer
|
||||
ref={formRef}
|
||||
fields={schema.fields}
|
||||
formConfig={schema.formConfig}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
{/* 表单元信息 */}
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
color: '#666'
|
||||
}}>
|
||||
<div><strong>工作流标识:</strong>{workflowDefinition.key}</div>
|
||||
<div><strong>工作流版本:</strong>v{workflowDefinition.flowVersion}</div>
|
||||
<div><strong>表单标识:</strong>{formDefinition.key}</div>
|
||||
<div><strong>表单版本:</strong>v{formDefinition.formVersion}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartWorkflowModal;
|
||||
|
||||
@ -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<WorkflowDefinition | null>(null);
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
|
||||
const [startModalVisible, setStartModalVisible] = useState(false);
|
||||
const [startRecord, setStartRecord] = useState<WorkflowDefinition | null>(null);
|
||||
const [formDefinition, setFormDefinition] = useState<FormDefinitionResponse | null>(null);
|
||||
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
|
||||
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<string, any>) => {
|
||||
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 = () => {
|
||||
</Select>
|
||||
<Select
|
||||
value={query.status || undefined}
|
||||
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value }))}
|
||||
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as WorkflowDefinitionStatus | undefined }))}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-9">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
@ -495,6 +555,19 @@ const WorkflowDefinitionList: React.FC = () => {
|
||||
onOpenChange={setCategoryDialogOpen}
|
||||
onSuccess={loadCategories}
|
||||
/>
|
||||
|
||||
{/* 启动工作流弹窗 */}
|
||||
<StartWorkflowModal
|
||||
open={startModalVisible}
|
||||
onClose={() => {
|
||||
setStartModalVisible(false);
|
||||
setStartRecord(null);
|
||||
setFormDefinition(null);
|
||||
}}
|
||||
workflowDefinition={startRecord}
|
||||
formDefinition={formDefinition}
|
||||
onSubmit={handleStartWorkflowSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,7 +5,9 @@ import {
|
||||
WorkflowDefinitionRequest,
|
||||
WorkflowCategoryResponse,
|
||||
WorkflowCategoryQuery,
|
||||
WorkflowCategoryRequest
|
||||
WorkflowCategoryRequest,
|
||||
StartWorkflowInstanceRequest,
|
||||
StartWorkflowInstanceResponse
|
||||
} from './types';
|
||||
import {Page} from '@/types/base';
|
||||
|
||||
@ -92,13 +94,16 @@ export const deleteWorkflowCategory = (id: number) =>
|
||||
|
||||
/**
|
||||
* 启动工作流实例
|
||||
* @param processKey 流程定义key
|
||||
* @param categoryId 分类ID
|
||||
* @returns Promise<void>
|
||||
* @param data 启动工作流请求参数
|
||||
* @returns Promise<StartWorkflowInstanceResponse> 返回工作流实例信息
|
||||
*/
|
||||
export const startWorkflowInstance = (processKey: string, categoryId?: number) =>
|
||||
request.post<void>(`${INSTANCE_URL}/start`, {
|
||||
processKey,
|
||||
businessKey: `workflow_${Date.now()}`,
|
||||
categoryId
|
||||
});
|
||||
export const startWorkflowInstance = (data: StartWorkflowInstanceRequest) => {
|
||||
// 自动生成 businessKey(如果未提供)
|
||||
const requestData: StartWorkflowInstanceRequest = {
|
||||
processKey: data.processKey,
|
||||
businessKey: data.businessKey || `workflow_${data.processKey}_${Date.now()}`,
|
||||
variables: data.variables || {}
|
||||
};
|
||||
|
||||
return request.post<StartWorkflowInstanceResponse>(`${INSTANCE_URL}/start`, requestData);
|
||||
};
|
||||
@ -126,3 +126,37 @@ export interface WorkflowCategoryRequest {
|
||||
supportedTriggers?: string[];
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动工作流实例请求
|
||||
*/
|
||||
export interface StartWorkflowInstanceRequest {
|
||||
/** 流程定义key */
|
||||
processKey: string;
|
||||
|
||||
/** 业务标识(可选,默认自动生成) */
|
||||
businessKey?: string;
|
||||
|
||||
/** 流程变量(表单数据) */
|
||||
variables?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动工作流实例响应
|
||||
*/
|
||||
export interface StartWorkflowInstanceResponse {
|
||||
/** 流程实例ID */
|
||||
instanceId: string;
|
||||
|
||||
/** 流程定义ID */
|
||||
processDefinitionId: string;
|
||||
|
||||
/** 业务标识 */
|
||||
businessKey: string;
|
||||
|
||||
/** 流程定义key */
|
||||
processKey: string;
|
||||
|
||||
/** 启动时间 */
|
||||
startTime?: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user