增加审批组件

This commit is contained in:
dengqichen 2025-10-25 01:05:18 +08:00
parent ef3e34b85d
commit f8583ba3ef
13 changed files with 1049 additions and 562 deletions

View File

@ -31,13 +31,11 @@
* ``` * ```
*/ */
import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; import { useImperativeHandle, forwardRef } from 'react';
import { Form, message } from 'antd'; import { Form, message } from 'antd';
import type { Rule } from 'antd/es/form'; import type { FieldConfig, FormConfig } from './types';
import dayjs from 'dayjs'; import { useFormCore } from './hooks/useFormCore';
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from './types'; import FormFieldsRenderer from './components/FormFieldsRenderer';
import FieldRenderer from './components/FieldRenderer';
import GridFieldPreview from './components/GridFieldPreview';
import './styles.css'; import './styles.css';
/** /**
@ -62,12 +60,9 @@ export interface FormPreviewRef {
const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, formConfig }, ref) => { const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, formConfig }, ref) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, any>>({});
const [fieldStates, setFieldStates] = useState<Record<string, { // 使用核心 hook 管理表单状态
visible?: boolean; const { formData, setFormData, fieldStates, handleValuesChange } = useFormCore({ fields, form });
disabled?: boolean;
required?: boolean;
}>>({});
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@ -92,247 +87,6 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
reset: handleReset, 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) { if (fields.length === 0) {
return ( return (
@ -356,12 +110,17 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign} labelAlign={formConfig.labelAlign === 'top' ? undefined : formConfig.labelAlign}
labelCol={labelColSpan ? { span: labelColSpan } : undefined} labelCol={labelColSpan ? { span: labelColSpan } : undefined}
wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined} wrapperCol={wrapperColSpan ? { span: wrapperColSpan } : undefined}
onValuesChange={handleValuesChange}
> >
{formConfig.title && ( {formConfig.title && (
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3> <h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
)} )}
{renderFields(fields)} <FormFieldsRenderer
fields={fields}
formConfig={formConfig}
fieldStates={fieldStates}
/>
</Form> </Form>
{Object.keys(formData).length > 0 && ( {Object.keys(formData).length > 0 && (

View File

@ -7,40 +7,50 @@
* - * -
* - API * - API
* - * -
* - * - beforeSubmit/afterSubmit/onError
* - * -
* -
* *
* @example * @example
* ```tsx * ```tsx
* // 使用示例 * // 方式1: 使用内置按钮(简单场景)
* import { FormRenderer } from '@/components/FormDesigner';
*
* const MyComponent = () => {
* const formSchema = await getFormDefinitionById(id);
*
* return (
* <FormRenderer * <FormRenderer
* fields={formSchema.fields} * schema={schema}
* formConfig={formSchema.formConfig} * onSubmit={handleSubmit}
* onSubmit={async (values) => { * showSubmit={true}
* await api.submitForm(values); * showCancel={true}
* }} * submitText="保存"
* cancelText="取消"
* /> * />
* ); *
* }; * // 方式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 React, { useImperativeHandle, forwardRef } from 'react';
import { Form, message } from 'antd'; import { Form, message, Button } from 'antd';
import type { Rule } from 'antd/es/form'; import type { FieldConfig, FormConfig, FormSchema } from './types';
import dayjs from 'dayjs';
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule, FormSchema } from './types';
import type { ComponentMeta } from './config'; import type { ComponentMeta } from './config';
import { COMPONENT_LIST } from './config'; import { COMPONENT_LIST } from './config';
import { ComponentsContext } from './Designer'; import { ComponentsContext } from './Designer';
import FieldRenderer from './components/FieldRenderer'; import { useFormCore } from './hooks/useFormCore';
import GridFieldPreview from './components/GridFieldPreview'; import FormFieldsRenderer from './components/FormFieldsRenderer';
import './styles.css'; import './styles.css';
/** /**
@ -116,12 +126,9 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
}; };
const [form] = Form.useForm(); const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, any>>({});
const [fieldStates, setFieldStates] = useState<Record<string, { // 使用核心 hook 管理表单状态
visible?: boolean; const { setFormData, fieldStates, handleValuesChange: coreHandleValuesChange } = useFormCore({ fields, form });
disabled?: boolean;
required?: boolean;
}>>({});
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@ -204,9 +211,9 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
// 监听表单值变化 // 监听表单值变化
const handleValuesChange = (_: any, allValues: Record<string, any>) => { const handleValuesChange = (_: any, allValues: Record<string, any>) => {
setFormData(allValues); coreHandleValuesChange(_, allValues); // 调用核心 hook 的处理
if (props.onChange) { if (props.onChange) {
props.onChange(allValues); props.onChange(allValues); // 通知外部
} }
}; };
@ -216,218 +223,6 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
reset: handleReset, 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) { if (fields.length === 0) {
return ( return (
<div style={{ padding: 40, textAlign: 'center', color: '#8c8c8c' }}> <div style={{ padding: 40, textAlign: 'center', color: '#8c8c8c' }}>
@ -457,7 +252,11 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
<h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3> <h3 style={{ marginBottom: 24 }}>{formConfig.title}</h3>
)} )}
{renderFields(fields)} <FormFieldsRenderer
fields={fields}
formConfig={formConfig}
fieldStates={fieldStates}
/>
{(showSubmit || showCancel) && ( {(showSubmit || showCancel) && (
<Form.Item <Form.Item
@ -466,38 +265,21 @@ const FormRenderer = forwardRef<FormRendererRef, FormRendererProps>((props, ref)
> >
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
{showSubmit && ( {showSubmit && (
<button <Button
type="button" type="primary"
htmlType="button"
onClick={handleSubmit} onClick={handleSubmit}
style={{
padding: '4px 15px',
fontSize: '14px',
borderRadius: '6px',
border: 'none',
background: '#1677ff',
color: 'white',
cursor: 'pointer',
}}
> >
{submitText} {submitText}
</button> </Button>
)} )}
{showCancel && ( {showCancel && (
<button <Button
type="button" htmlType="button"
onClick={onCancel || handleReset} 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} {cancelText}
</button> </Button>
)} )}
</div> </div>
</Form.Item> </Form.Item>

View File

@ -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;

View File

@ -11,15 +11,11 @@ import FieldRenderer from './FieldRenderer';
interface GridFieldPreviewProps { interface GridFieldPreviewProps {
field: FieldConfig; field: FieldConfig;
formData?: Record<string, any>;
onFieldChange?: (name: string, value: any) => void;
formConfig?: FormConfig; formConfig?: FormConfig;
} }
const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
field, field,
formData = {},
onFieldChange,
formConfig formConfig
}) => { }) => {
const columns = field.columns || 2; const columns = field.columns || 2;
@ -91,15 +87,9 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
// 根据表单配置决定布局方式 // 根据表单配置决定布局方式
const isVertical = formConfig?.labelAlign === 'top'; const isVertical = formConfig?.labelAlign === 'top';
// 合并基础验证规则和自定义验证规则和FormPreview保持一致 // 合并基础验证规则和自定义验证规则
const customRules = convertValidationRules(childField.validationRules); 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'); const hasRequiredRule = childField.validationRules?.some(rule => rule.type === 'required');
@ -114,13 +104,8 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
const allRules = [...baseRules, ...customRules]; const allRules = [...baseRules, ...customRules];
// 🐛 调试:打印最终规则
if (allRules.length > 0) {
console.log(`🎯 [GridFieldPreview] 字段 "${childField.label}" 最终验证规则:`, allRules);
}
// 普通表单字段用 Form.Item 包裹 // 普通表单字段用 Form.Item 包裹
// 栅格内字段:根据对齐方式使用不同的布局 // Form.Item 会自动管理 value 和 onChange不需要手动传递
return ( return (
<Form.Item <Form.Item
key={childField.id} key={childField.id}
@ -133,8 +118,6 @@ const GridFieldPreview: React.FC<GridFieldPreviewProps> = ({
> >
<FieldRenderer <FieldRenderer
field={childField} field={childField}
value={formData[childField.name]}
onChange={(value) => onFieldChange?.(childField.name, value)}
isPreview={true} isPreview={true}
/> />
</Form.Item> </Form.Item>

View 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,
};
};

View File

@ -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;
};

View 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;
};

View File

@ -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];
};

View File

@ -6,10 +6,11 @@
* - FormRenderer: 表单渲染器 * - FormRenderer: 表单渲染器
*/ */
import React, { useState } from 'react'; import React, { useState, useRef } from 'react';
import { message, Tabs, Card, Button, Modal, Space, Divider, Alert } from 'antd'; 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 { WORKFLOW_COMPONENTS } from '@/components/FormDesigner/extensions/workflow';
import { EditOutlined } from '@ant-design/icons';
const FormDesignerExamplesPage: React.FC = () => { const FormDesignerExamplesPage: React.FC = () => {
// ==================== 示例 1: 普通表单设计器 ==================== // ==================== 示例 1: 普通表单设计器 ====================
@ -41,7 +42,22 @@ const FormDesignerExamplesPage: React.FC = () => {
const handleStaticFormSubmit = async (data: Record<string, any>) => { const handleStaticFormSubmit = async (data: Record<string, any>) => {
console.log('📤 静态表单提交数据:', data); 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('表单提交成功!请查看控制台'); message.success('表单提交成功!请查看控制台');
return response; // 返回响应给 afterSubmit 钩子
}; };
// ==================== 示例 3: 工作流表单(弹窗) ==================== // ==================== 示例 3: 工作流表单(弹窗) ====================
@ -90,8 +106,22 @@ const FormDesignerExamplesPage: React.FC = () => {
const handleWorkflowSubmit = async (data: Record<string, any>) => { const handleWorkflowSubmit = async (data: Record<string, any>) => {
console.log('🚀 工作流启动参数:', data); 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('工作流已启动!'); message.success('工作流已启动!');
setWorkflowModalVisible(false); setWorkflowModalVisible(false);
return response;
}; };
// ==================== 示例 4: 只读模式 ==================== // ==================== 示例 4: 只读模式 ====================
@ -205,7 +235,21 @@ const FormDesignerExamplesPage: React.FC = () => {
const handleComplexFormSubmit = async (data: Record<string, any>) => { const handleComplexFormSubmit = async (data: Record<string, any>) => {
console.log('📋 复杂表单提交数据:', data); 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('表单提交成功!'); message.success('表单提交成功!');
return response;
}; };
// ==================== 示例 6: 带生命周期钩子的表单 ==================== // ==================== 示例 6: 带生命周期钩子的表单 ====================
@ -300,6 +344,95 @@ const FormDesignerExamplesPage: React.FC = () => {
message.error(`操作失败: ${error.message || '未知错误'}`); 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 配置 ==================== // ==================== Tab 配置 ====================
const tabItems = [ const tabItems = [
{ {
@ -608,6 +741,151 @@ const FormDesignerExamplesPage: React.FC = () => {
</Card> </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&lt;FormRendererRef&gt;</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 ( return (

View File

@ -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;

View File

@ -13,13 +13,16 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service'; 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 type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import EditModal from './components/EditModal'; import EditModal from './components/EditModal';
import DeleteDialog from './components/DeleteDialog'; import DeleteDialog from './components/DeleteDialog';
import DeployDialog from './components/DeployDialog'; import DeployDialog from './components/DeployDialog';
import CategoryManageDialog from './components/CategoryManageDialog'; 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 [deployDialogOpen, setDeployDialogOpen] = useState(false);
const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null); const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null);
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); 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>({ const [query, setQuery] = useState<WorkflowDefinitionQuery>({
pageNum: DEFAULT_CURRENT - 1, pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE, pageSize: DEFAULT_PAGE_SIZE,
@ -163,14 +169,36 @@ const WorkflowDefinitionList: React.FC = () => {
} }
}; };
// 启动 // 启动工作流
const handleStart = async (record: WorkflowDefinition) => { const handleStart = async (record: WorkflowDefinition) => {
// 检查是否有关联的启动表单
if (record.formDefinitionId) {
// 加载表单定义数据
try { try {
await startWorkflowInstance(record.key, record.categoryId); 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({ toast({
title: '启动成功', title: '启动成功',
description: `工作流 "${record.name}" 已启动`, description: `工作流 "${record.name}" 已启动`,
}); });
loadData(); // 刷新列表
} catch (error) { } catch (error) {
console.error('启动失败:', error); console.error('启动失败:', error);
toast({ toast({
@ -179,6 +207,38 @@ const WorkflowDefinitionList: React.FC = () => {
variant: 'destructive' variant: 'destructive'
}); });
} }
}
};
// 处理启动工作流提交(带表单数据)
const handleStartWorkflowSubmit = async (workflow: WorkflowDefinition, formData: Record<string, any>) => {
try {
console.log('🚀 启动工作流,携带表单数据:', formData);
// 调用启动API传递表单数据作为流程变量
const result = await startWorkflowInstance({
processKey: workflow.key, // 流程定义key
variables: formData, // 表单数据作为流程变量
businessKey: undefined // 自动生成 businessKey
});
console.log('✅ 工作流实例已创建:', result);
toast({
title: '启动成功',
description: `工作流 "${workflow.name}" 已启动`,
});
loadData(); // 刷新列表
} catch (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>
<Select <Select
value={query.status || undefined} 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"> <SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="全部状态" /> <SelectValue placeholder="全部状态" />
@ -495,6 +555,19 @@ const WorkflowDefinitionList: React.FC = () => {
onOpenChange={setCategoryDialogOpen} onOpenChange={setCategoryDialogOpen}
onSuccess={loadCategories} onSuccess={loadCategories}
/> />
{/* 启动工作流弹窗 */}
<StartWorkflowModal
open={startModalVisible}
onClose={() => {
setStartModalVisible(false);
setStartRecord(null);
setFormDefinition(null);
}}
workflowDefinition={startRecord}
formDefinition={formDefinition}
onSubmit={handleStartWorkflowSubmit}
/>
</div> </div>
); );
}; };

View File

@ -5,7 +5,9 @@ import {
WorkflowDefinitionRequest, WorkflowDefinitionRequest,
WorkflowCategoryResponse, WorkflowCategoryResponse,
WorkflowCategoryQuery, WorkflowCategoryQuery,
WorkflowCategoryRequest WorkflowCategoryRequest,
StartWorkflowInstanceRequest,
StartWorkflowInstanceResponse
} from './types'; } from './types';
import {Page} from '@/types/base'; import {Page} from '@/types/base';
@ -92,13 +94,16 @@ export const deleteWorkflowCategory = (id: number) =>
/** /**
* *
* @param processKey key * @param data
* @param categoryId ID * @returns Promise<StartWorkflowInstanceResponse>
* @returns Promise<void>
*/ */
export const startWorkflowInstance = (processKey: string, categoryId?: number) => export const startWorkflowInstance = (data: StartWorkflowInstanceRequest) => {
request.post<void>(`${INSTANCE_URL}/start`, { // 自动生成 businessKey如果未提供
processKey, const requestData: StartWorkflowInstanceRequest = {
businessKey: `workflow_${Date.now()}`, processKey: data.processKey,
categoryId businessKey: data.businessKey || `workflow_${data.processKey}_${Date.now()}`,
}); variables: data.variables || {}
};
return request.post<StartWorkflowInstanceResponse>(`${INSTANCE_URL}/start`, requestData);
};

View File

@ -126,3 +126,37 @@ export interface WorkflowCategoryRequest {
supportedTriggers?: string[]; supportedTriggers?: string[];
enabled?: boolean; 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;
}