增加审批组件

This commit is contained in:
dengqichen 2025-10-24 15:57:41 +08:00
parent ecf3a1ba74
commit d0c1c6e516
16 changed files with 1277 additions and 49 deletions

View File

@ -46,8 +46,12 @@ import PropertyPanel from './components/PropertyPanel';
import FormPreview, { type FormPreviewRef } from './components/FormPreview';
import { COMPONENT_LIST } from './config';
import type { FieldConfig, FormConfig, FormSchema } from './types';
import type { ComponentMeta } from './config';
import './styles.css';
// 🔌 组件列表 Context用于插件式扩展
export const ComponentsContext = React.createContext<ComponentMeta[]>(COMPONENT_LIST);
export interface FormDesignerProps {
value?: FormSchema; // 受控模式:表单 Schema
onChange?: (schema: FormSchema) => void; // 受控模式Schema 变化回调
@ -55,6 +59,7 @@ export interface FormDesignerProps {
readonly?: boolean; // 只读模式
showToolbar?: boolean; // 是否显示工具栏
extraActions?: React.ReactNode; // 额外的操作按钮
extraComponents?: ComponentMeta[]; // 🆕 扩展字段组件(如工作流字段)
}
const FormDesigner: React.FC<FormDesignerProps> = ({
@ -63,7 +68,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
onSave,
readonly = false,
showToolbar = true,
extraActions
extraActions,
extraComponents = [],
}) => {
// 表单预览 ref
const formPreviewRef = useRef<FormPreviewRef>(null);
@ -149,6 +155,9 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
// 选中的字段
const [selectedFieldId, setSelectedFieldId] = useState<string>();
// 待选中的字段ID用于确保字段添加后再选中
const [pendingSelectId, setPendingSelectId] = useState<string | null>(null);
// 剪贴板(用于复制粘贴)
const [clipboard, setClipboard] = useState<FieldConfig | null>(null);
@ -170,6 +179,11 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
})
);
// 🔌 合并核心组件和扩展组件
const allComponents = useMemo(() => {
return [...COMPONENT_LIST, ...extraComponents];
}, [extraComponents]);
// 自定义碰撞检测策略:基于边界判断,区分"插入"和"拖入"
const customCollisionDetection = useCallback((args: any) => {
const pointerCollisions = pointerWithin(args);
@ -280,7 +294,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
// 从组件面板拖拽新组件到栅格列
if (activeData?.isNew && overData?.gridId && overData?.colIndex !== undefined) {
const componentType = active.id.toString().replace('new-', '');
const component = COMPONENT_LIST.find((c) => c.type === componentType);
const component = allComponents.find((c) => c.type === componentType);
if (component) {
const newField: FieldConfig = {
@ -312,7 +326,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
return updateGrid(prev);
});
setSelectedFieldId(newField.id);
// 🔄 使用 pendingSelectId 确保字段添加后再选中
setPendingSelectId(newField.id);
message.success(`已添加${component.label}到栅格列`);
}
return;
@ -321,7 +336,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
// 从组件面板拖拽新组件到画布或字段
if (activeData?.isNew && overData?.accept === 'field') {
const componentType = active.id.toString().replace('new-', '');
const component = COMPONENT_LIST.find((c) => c.type === componentType);
const component = allComponents.find((c) => c.type === componentType);
if (component) {
const newField: FieldConfig = {
@ -351,7 +366,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
return newFields;
});
setSelectedFieldId(newField.id);
// 🔄 使用 pendingSelectId 确保字段添加后再选中
setPendingSelectId(newField.id);
message.success(`已添加${component.label}`);
}
}
@ -432,7 +448,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
const newField = deepCopyField(clipboard);
setFields(prev => [...prev, newField]);
setSelectedFieldId(newField.id);
// 🔄 使用 pendingSelectId 确保字段添加后再选中
setPendingSelectId(newField.id);
message.success('已粘贴字段');
}, [clipboard, deepCopyField, readonly, setFields]);
@ -457,7 +474,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
return field;
}));
setSelectedFieldId(newField.id);
// 🔄 使用 pendingSelectId 确保字段添加后再选中
setPendingSelectId(newField.id);
message.success('已粘贴字段到栅格');
}, [clipboard, deepCopyField, readonly, setFields]);
@ -652,6 +670,20 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
const selectedField = findFieldById(fields, selectedFieldId) || null;
// 🔄 当有待选中的字段时,等待该字段被添加到 fields 后再选中
useEffect(() => {
if (pendingSelectId) {
const field = findFieldById(fields, pendingSelectId);
if (field) {
console.log('✅ 字段已添加,现在选中:', pendingSelectId, field.type);
setSelectedFieldId(pendingSelectId);
setPendingSelectId(null); // 清除待选中状态
} else {
console.log('⏳ 等待字段添加到 fields:', pendingSelectId);
}
}
}, [fields, pendingSelectId]);
// 获取所有字段扁平化包括grid中的嵌套字段
const getAllFields = useCallback((fieldList: FieldConfig[]): FieldConfig[] => {
const result: FieldConfig[] = [];
@ -719,6 +751,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
}, [selectedFieldId, clipboard, readonly, handleCopyField, handlePasteToCanvas, handleDeleteField]);
return (
<ComponentsContext.Provider value={allComponents}>
<div className="form-designer">
{showToolbar && (
<div className="form-designer-header">
@ -768,7 +801,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
<div className="form-designer-body">
{/* 左侧组件面板 */}
<div style={{ width: 280 }}>
<ComponentPanel />
<ComponentPanel extraComponents={extraComponents} />
</div>
{/* 中间设计画布 */}
@ -809,7 +842,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
cursor: 'grabbing',
}}>
{COMPONENT_LIST.find(c => `new-${c.type}` === activeId)?.label || '组件'}
{allComponents.find(c => `new-${c.type}` === activeId)?.label || '组件'}
</div>
) : null}
</DragOverlay>
@ -846,6 +879,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
<FormPreview ref={formPreviewRef} fields={fields} formConfig={formConfig} />
</Modal>
</div>
</ComponentsContext.Provider>
);
};

View File

@ -3,16 +3,21 @@
* Schema
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { Form, Button, message } from 'antd';
import type { Rule } from 'antd/es/form';
import dayjs from 'dayjs';
import type { FieldConfig, FormSchema, ValidationRule, LinkageRule } 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 './styles.css';
export interface FormRendererProps {
schema: FormSchema; // 表单 Schema
extraComponents?: ComponentMeta[]; // 🆕 扩展组件(如工作流字段)
value?: Record<string, any>; // 表单值(受控)
onChange?: (values: Record<string, any>) => void; // 值变化回调
onSubmit?: (values: Record<string, any>) => void | Promise<any>; // 提交回调
@ -30,6 +35,7 @@ export interface FormRendererProps {
const FormRenderer: React.FC<FormRendererProps> = ({
schema,
extraComponents = [],
value,
onChange,
onSubmit,
@ -45,6 +51,11 @@ const FormRenderer: React.FC<FormRendererProps> = ({
}) => {
const { fields, formConfig } = schema;
const [form] = Form.useForm();
// 🔌 合并核心组件和扩展组件
const allComponents = useMemo(() => {
return [...COMPONENT_LIST, ...extraComponents];
}, [extraComponents]);
const [formData, setFormData] = useState<Record<string, any>>(value || {});
const [fieldStates, setFieldStates] = useState<Record<string, {
visible?: boolean;
@ -272,8 +283,13 @@ const FormRenderer: React.FC<FormRendererProps> = ({
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);
@ -384,6 +400,7 @@ const FormRenderer: React.FC<FormRendererProps> = ({
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
return (
<ComponentsContext.Provider value={allComponents}>
<div style={{ padding: '24px 40px' }}>
<Form
form={form}
@ -429,6 +446,7 @@ const FormRenderer: React.FC<FormRendererProps> = ({
)}
</Form>
</div>
</ComponentsContext.Provider>
);
};

View File

@ -44,9 +44,27 @@ const DraggableComponent: React.FC<{ component: ComponentMeta }> = ({ component
);
};
interface ComponentPanelProps {
extraComponents?: ComponentMeta[]; // 额外的组件(如工作流扩展字段)
}
// 组件面板
const ComponentPanel: React.FC = () => {
const componentsByCategory = getComponentsByCategory();
const ComponentPanel: React.FC<ComponentPanelProps> = ({ extraComponents = [] }) => {
const coreComponents = getComponentsByCategory();
// 合并核心组件和扩展组件
const allComponents = React.useMemo(() => {
const merged = { ...coreComponents };
extraComponents.forEach(comp => {
if (!merged[comp.category]) {
merged[comp.category] = [];
}
merged[comp.category].push(comp);
});
return merged;
}, [coreComponents, extraComponents]);
// 定义分类显示顺序
const categoryOrder = ['布局字段', '基础字段', '高级字段'];
@ -64,7 +82,7 @@ const ComponentPanel: React.FC = () => {
bordered={false}
items={categoryOrder
.map((category) => {
const components = componentsByCategory[category];
const components = allComponents[category];
if (!components || components.length === 0) return null;
return {

View File

@ -3,7 +3,8 @@
*
*/
import React from 'react';
import React, { useContext } from 'react';
import { ComponentsContext } from '../Designer';
import {
Form,
Input,
@ -69,6 +70,20 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
const cascadeOptions = useCascaderOptions(field);
const loadData = useCascaderLoadData(field);
// 🔌 检查是否有自定义渲染组件(插件式扩展)
const allComponents = useContext(ComponentsContext);
const componentMeta = allComponents.find(c => c.type === field.type);
if (componentMeta?.FieldRendererComponent) {
const CustomRenderer = componentMeta.FieldRendererComponent;
console.log('✅ FieldRenderer: 使用自定义渲染组件', {
fieldType: field.type,
isPreview,
hasRenderer: !!componentMeta.FieldRendererComponent
});
return <CustomRenderer field={field} value={value} onChange={onChange} isPreview={isPreview} />;
}
// 布局组件特殊处理
if (field.type === 'divider') {
return <Divider>{field.label}</Divider>;
@ -253,6 +268,8 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
</Upload>
);
// approver-selector 等扩展字段已通过插件机制处理,不在此硬编码
case 'cascader':
return (
<Cascader

View File

@ -6,6 +6,7 @@
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 } from '../types';
import FieldRenderer from './FieldRenderer';
import GridFieldPreview from './GridFieldPreview';
@ -191,8 +192,13 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
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);

View File

@ -3,7 +3,8 @@
*
*/
import React from 'react';
import React, { useContext } from 'react';
import { ComponentsContext } from '../Designer';
import {
Form,
Input,
@ -45,6 +46,9 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
}) => {
const [form] = Form.useForm();
// 🔌 获取所有组件(包括扩展组件)- 必须在顶层调用 useContext
const allComponents = useContext(ComponentsContext);
React.useEffect(() => {
if (selectedField) {
// 确保嵌套对象被正确设置到表单
@ -180,6 +184,35 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
);
}
// 🔌 检查是否有自定义配置组件(插件式扩展)
const componentMeta = allComponents.find(c => c.type === selectedField.type);
console.log('🔍 PropertyPanel Debug:', {
selectedFieldType: selectedField.type,
allComponentsCount: allComponents.length,
allComponentTypes: allComponents.map(c => c.type),
componentMeta: componentMeta ? {
type: componentMeta.type,
label: componentMeta.label,
hasPropertyConfig: !!componentMeta.PropertyConfigComponent
} : null
});
if (componentMeta?.PropertyConfigComponent) {
const CustomConfig = componentMeta.PropertyConfigComponent;
console.log('✅ 使用自定义配置组件:', componentMeta.type);
return (
<div style={{ padding: 16 }}>
<CustomConfig
field={selectedField}
onChange={onFieldChange}
/>
</div>
);
}
console.log('⚠️ 未找到自定义配置组件,使用默认配置');
const hasOptions = ['select', 'radio', 'checkbox'].includes(selectedField.type);
const isCascader = selectedField.type === 'cascader';
@ -432,6 +465,35 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
</Form.Item>
)}
{/* 🎯 审批人选择器专属配置 */}
{selectedField.type === 'approver-selector' && (
<>
<Form.Item label="选择模式" name="selectionMode">
<Radio.Group>
<Radio.Button value="user"></Radio.Button>
<Radio.Button value="role"></Radio.Button>
<Radio.Button value="department"></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="审批模式" name="approvalMode">
<Radio.Group>
<Radio.Button value="single"></Radio.Button>
<Radio.Button value="countersign"></Radio.Button>
<Radio.Button value="or-sign"></Radio.Button>
</Radio.Group>
</Form.Item>
<div style={{ padding: '8px 12px', background: '#f0f5ff', borderRadius: 4, marginBottom: 16 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
<br />
<br />
</Text>
</div>
</>
)}
{hasOptions && (
<div>
<Divider style={{ margin: '16px 0' }} />

View File

@ -2,6 +2,7 @@
*
*/
import React from 'react';
import {
FormOutlined,
FontSizeOutlined,
@ -23,13 +24,33 @@ import {
} from '@ant-design/icons';
import type { FieldType, FieldConfig } from './types';
// 组件元数据
// 🔌 属性配置组件 Props
export interface PropertyConfigProps {
field: FieldConfig;
onChange: (field: FieldConfig) => void;
}
// 🔌 字段渲染组件 Props
export interface FieldRendererProps {
field: FieldConfig;
value?: any;
onChange?: (value: any) => void;
isPreview?: boolean;
}
// 🔌 组件元数据(支持插件式扩展)
export interface ComponentMeta {
type: FieldType;
label: string;
icon: any;
category: '基础字段' | '高级字段' | '布局字段';
defaultConfig: Partial<FieldConfig>;
// 🆕 插件式扩展:自定义属性配置组件
PropertyConfigComponent?: React.FC<PropertyConfigProps>;
// 🆕 插件式扩展:自定义字段渲染组件
FieldRendererComponent?: React.FC<FieldRendererProps>;
}
// 组件列表配置

View File

@ -0,0 +1,281 @@
# 表单设计器扩展字段
本目录包含表单设计器的扩展字段,采用松耦合的架构设计,不影响核心组件功能。
## 📁 目录结构
```
extensions/
└── workflow/ # 工作流扩展字段
├── index.ts # 导出文件
├── config.ts # 字段配置
├── ApproverSelector.tsx # 审批人选择器组件
└── README.md # 使用文档(本文件)
```
---
## 🎯 工作流扩展字段
### 1. 审批人选择器 (`approver-selector`)
与审批节点功能保持一致的审批人选择组件。
#### 功能特性
- ✅ **审批模式**:单人审批、会签(所有人同意)、或签(任一人同意)
- ✅ **选择模式**:按用户、按角色、按部门
- ✅ **数据源集成**:使用项目统一的 DataSourceType (USERS/ROLES/DEPARTMENTS)
- ✅ **实时数据加载**:通过 `useFieldOptions` Hook 获取数据
- ✅ **多选支持**:会签和或签模式自动启用多选
- ✅ **视觉提示**:清晰的模式说明和选择提示
---
## 🚀 快速开始
### 基础使用
```tsx
import { FormDesigner } from '@/components/FormDesigner';
import { WORKFLOW_COMPONENTS } from '@/components/FormDesigner/extensions/workflow';
function MyPage() {
const [schema, setSchema] = useState<FormSchema>();
return (
<FormDesigner
value={schema}
onChange={setSchema}
// 🔑 关键:注入工作流扩展字段
extraComponents={WORKFLOW_COMPONENTS}
/>
);
}
```
### 完整示例
查看 `/src/pages/FormDesigner/WorkflowExample.tsx` 获取完整的使用示例。
---
## 📊 数据结构
### FormSchema 中的审批人字段
```typescript
{
id: 'field_xxx',
type: 'approver-selector',
label: '审批人',
name: 'approver',
required: true,
selectionMode: 'user', // 'user' | 'role' | 'department'
approvalMode: 'single', // 'single' | 'countersign' | 'or-sign'
placeholder: '请选择审批人'
}
```
### 提交的表单数据
**单人审批**
```json
{
"approver": "user_1"
}
```
**会签/或签**
```json
{
"approver": ["user_1", "user_2", "user_3"]
}
```
---
## 🔧 开发指南
### 添加新的扩展字段
1. **创建组件文件** (`MyExtensionField.tsx`)
```typescript
import React from 'react';
import type { FieldConfig } from '../../types';
interface MyExtensionFieldProps {
field: FieldConfig;
value?: any;
onChange?: (value: any) => void;
disabled?: boolean;
isPreview?: boolean;
}
export const MyExtensionField: React.FC<MyExtensionFieldProps> = ({
field,
value,
onChange,
disabled,
isPreview,
}) => {
return (
<div>
{/* 你的组件实现 */}
</div>
);
};
```
2. **更新配置** (`config.ts`)
```typescript
export const WORKFLOW_COMPONENTS: ComponentMeta[] = [
// ... existing
{
type: 'my-extension-field',
label: '我的扩展字段',
icon: MyIcon,
category: '高级字段',
defaultConfig: {
id: '',
type: 'my-extension-field',
label: '我的扩展字段',
name: 'myField',
// ... 其他配置
},
},
];
```
3. **更新类型定义** (`types.ts`)
```typescript
export type FieldType =
| 'input'
// ... existing types
| 'my-extension-field';
```
4. **更新 FieldRenderer** (`FieldRenderer.tsx`)
```typescript
case 'my-extension-field':
const { MyExtensionField } = require('../extensions/workflow/MyExtensionField');
return (
<MyExtensionField
field={field}
value={value}
onChange={onChange}
disabled={field.disabled}
isPreview={isPreview}
/>
);
```
5. **(可选)添加属性配置** (`PropertyPanel.tsx`)
```typescript
{selectedField.type === 'my-extension-field' && (
<Form.Item label="自定义属性" name="customProp">
<Input />
</Form.Item>
)}
```
---
## 🎨 设计原则
### 1. 松耦合
- 扩展字段独立于核心组件
- 通过 `extraComponents` 参数注入
- 不影响现有功能
### 2. 按需加载
- 使用动态 `require()` 加载扩展组件
- 只在使用时加载代码
- 减少打包体积
### 3. 数据源统一
- 使用项目统一的 DataSourceType
- 复用现有的数据加载逻辑
- 保持数据格式一致
### 4. 类型安全
- 完整的 TypeScript 类型定义
- 扩展字段类型纳入 FieldType
- IDE 自动补全支持
---
## 🧪 测试
### 访问测试页面
1. 启动开发服务器
```bash
npm run dev
```
2. 访问工作流表单示例
```
http://localhost:3000/form-designer/workflow-example
```
### 测试步骤
1. **拖拽审批人字段**到设计画布
2. **配置字段属性**
- 选择模式(用户/角色/部门)
- 审批模式(单人/会签/或签)
3. **预览表单**:测试审批人选择功能
4. **查看数据**:检查提交的数据格式
---
## 📝 API 参考
### ApproverSelector Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| field | FieldConfig | - | 字段配置对象 |
| value | string \| string[] | - | 当前选中值 |
| onChange | (value) => void | - | 值变化回调 |
| disabled | boolean | false | 是否禁用 |
| isPreview | boolean | false | 是否为预览模式 |
### 字段配置属性
| 属性 | 类型 | 可选值 | 说明 |
|------|------|--------|------|
| selectionMode | string | 'user' \| 'role' \| 'department' | 选择模式 |
| approvalMode | string | 'single' \| 'countersign' \| 'or-sign' | 审批模式 |
---
## 🤝 贡献
欢迎贡献新的扩展字段!请遵循以下步骤:
1. Fork 项目
2. 在 `extensions/` 下创建你的扩展目录
3. 实现组件和配置
4. 添加测试和文档
5. 提交 Pull Request
---
## 📚 相关文档
- [表单设计器主文档](../README.md)
- [类型定义](../types.ts)
- [组件配置](../config.ts)
- [工作流示例](/src/pages/FormDesigner/WorkflowExample.tsx)
---
Made with ❤️ for Deploy Ease Platform

View File

@ -0,0 +1,377 @@
/**
* -
*
*/
import React, { useEffect } from 'react';
import {
Form,
Input,
InputNumber,
Radio,
Switch,
Select,
Divider,
Typography,
} from 'antd';
import type { PropertyConfigProps } from '../../config';
import { useFieldOptions } from '../../hooks/useFieldOptions';
import { DataSourceType } from '@/domain/dataSource';
const { Text } = Typography;
const { TextArea } = Input;
/**
*
* 93
*/
export const ApproverPropertyConfig: React.FC<PropertyConfigProps> = ({
field,
onChange,
}) => {
const [form] = Form.useForm();
// 🔄 按需加载数据源(只加载当前选中的审批人类型对应的数据源)
const currentApproverType = field.approverType || 'USER';
const { options: userOptions } = useFieldOptions({
dataSourceType: currentApproverType === 'USER' ? 'predefined' : 'static', // 只在选中时加载
predefinedDataSource: { sourceType: DataSourceType.USERS },
options: [] // 未选中时使用空数组
});
const { options: roleOptions } = useFieldOptions({
dataSourceType: currentApproverType === 'ROLE' ? 'predefined' : 'static',
predefinedDataSource: { sourceType: DataSourceType.ROLES },
options: []
});
const { options: departmentOptions } = useFieldOptions({
dataSourceType: currentApproverType === 'DEPARTMENT' ? 'predefined' : 'static',
predefinedDataSource: { sourceType: DataSourceType.DEPARTMENTS },
options: []
});
// 同步 field 到表单
useEffect(() => {
form.setFieldsValue(field);
}, [field, form]);
// 监听表单变化,合并到 field 并回调
const handleValuesChange = (changedValues: any, allValues: any) => {
onChange({
...field,
...allValues,
});
};
return (
<Form
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
initialValues={field}
>
{/* ========== 审批配置 ========== */}
<div style={{
fontSize: 14,
fontWeight: 500,
color: 'rgba(0, 0, 0, 0.88)',
marginBottom: 16,
marginTop: 8,
paddingBottom: 8,
borderBottom: '1px solid rgba(5, 5, 5, 0.06)'
}}>
</div>
<Form.Item
label="审批模式"
name="approvalMode"
tooltip="定义审批人数量和审批方式"
>
<Radio.Group style={{ display: 'flex', width: '100%' }}>
<Radio.Button value="SINGLE" style={{ flex: 1, textAlign: 'center' }}></Radio.Button>
<Radio.Button value="ALL" style={{ flex: 1, textAlign: 'center' }}></Radio.Button>
<Radio.Button value="ANY" style={{ flex: 1, textAlign: 'center' }}></Radio.Button>
</Radio.Group>
</Form.Item>
<div style={{
padding: '8px 12px',
background: '#f0f5ff',
borderRadius: 4,
marginBottom: 16
}}>
<Text type="secondary" style={{ fontSize: 12 }}>
<strong></strong><br />
<strong></strong><br />
<strong></strong>
</Text>
</div>
<Form.Item
label="审批人类型"
name="approverType"
tooltip="指定审批人的方式"
>
<Radio.Group style={{ display: 'flex', width: '100%' }}>
<Radio.Button
value="USER"
style={{ flex: 1, fontSize: 11, padding: '0 4px', lineHeight: '28px', height: 28, textAlign: 'center' }}
>
</Radio.Button>
<Radio.Button
value="ROLE"
style={{ flex: 1, fontSize: 11, padding: '0 4px', lineHeight: '28px', height: 28, textAlign: 'center' }}
>
</Radio.Button>
<Radio.Button
value="DEPARTMENT"
style={{ flex: 1, fontSize: 11, padding: '0 4px', lineHeight: '28px', height: 28, textAlign: 'center' }}
>
</Radio.Button>
<Radio.Button
value="VARIABLE"
style={{ flex: 1, fontSize: 11, padding: '0 4px', lineHeight: '28px', height: 28, textAlign: 'center' }}
>
</Radio.Button>
</Radio.Group>
</Form.Item>
{/* 🔄 根据审批人类型和审批模式显示不同的选择器 */}
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
prevValues.approverType !== currentValues.approverType ||
prevValues.approvalMode !== currentValues.approvalMode
}>
{({ getFieldValue }) => {
const approverType = getFieldValue('approverType');
const approvalMode = getFieldValue('approvalMode');
const isMultiple = approvalMode === 'ALL' || approvalMode === 'ANY'; // 会签和或签允许多选
// 指定用户
if (approverType === 'USER') {
return (
<Form.Item
label="审批人列表"
name="approvers"
tooltip={
approvalMode === 'SINGLE'
? '选择一个审批人'
: approvalMode === 'ALL'
? '选中的所有用户都必须审批'
: '任一用户审批即可'
}
rules={[{ required: true, message: '请选择审批人' }]}
>
<Select
mode={isMultiple ? 'multiple' : undefined}
placeholder={isMultiple ? '请选择审批人(可多选)' : '请选择审批人'}
options={userOptions}
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
);
}
// 指定角色
if (approverType === 'ROLE') {
return (
<Form.Item
label="审批角色"
name="approverRoles"
tooltip={
approvalMode === 'SINGLE'
? '选择一个角色'
: approvalMode === 'ALL'
? '拥有选中角色的所有用户都必须审批'
: '任一拥有该角色的用户审批即可'
}
rules={[{ required: true, message: '请选择审批角色' }]}
>
<Select
mode={isMultiple ? 'multiple' : undefined}
placeholder={isMultiple ? '请选择角色(可多选)' : '请选择角色'}
options={roleOptions}
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
);
}
// 指定部门
if (approverType === 'DEPARTMENT') {
return (
<Form.Item
label="审批部门"
name="approverDepartments"
tooltip={
approvalMode === 'SINGLE'
? '选择一个部门'
: approvalMode === 'ALL'
? '选中部门的所有成员都必须审批'
: '任一部门成员审批即可'
}
rules={[{ required: true, message: '请选择审批部门' }]}
>
<Select
mode={isMultiple ? 'multiple' : undefined}
placeholder={isMultiple ? '请选择部门(可多选)' : '请选择部门'}
options={departmentOptions}
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
);
}
// 变量指定
if (approverType === 'VARIABLE') {
return (
<Form.Item
label="审批人变量"
name="approverVariable"
tooltip="使用流程变量指定审批人(格式:${变量名}"
rules={[
{ required: true, message: '请输入审批人变量' },
{ pattern: /^\$\{.+\}$/, message: '格式错误,应为 ${变量名}' }
]}
>
<Input placeholder="例如:${initiator}" />
</Form.Item>
);
}
return null;
}}
</Form.Item>
{/* ========== 审批界面 ========== */}
<div style={{
fontSize: 14,
fontWeight: 500,
color: 'rgba(0, 0, 0, 0.88)',
marginBottom: 16,
marginTop: 24,
paddingBottom: 8,
borderBottom: '1px solid rgba(5, 5, 5, 0.06)'
}}>
</div>
<Form.Item
label="审批标题"
name="approvalTitle"
tooltip="显示在审批弹窗的标题"
rules={[{ max: 50, message: '标题最长50个字符' }]}
>
<Input placeholder="请输入审批标题" maxLength={50} />
</Form.Item>
<Form.Item
label="审批说明"
name="approvalContent"
tooltip="显示在审批弹窗中的说明文字"
>
<TextArea
rows={3}
placeholder="请输入审批说明,将显示在审批弹窗中"
maxLength={200}
showCount
/>
</Form.Item>
{/* ========== 高级设置 ========== */}
<div style={{
fontSize: 14,
fontWeight: 500,
color: 'rgba(0, 0, 0, 0.88)',
marginBottom: 16,
marginTop: 24,
paddingBottom: 8,
borderBottom: '1px solid rgba(5, 5, 5, 0.06)'
}}>
</div>
<Form.Item
label="超时时间"
name="timeoutDuration"
tooltip="审批超时时间0表示不限制"
extra="单位小时0表示不限制"
>
<InputNumber
min={0}
max={720}
style={{ width: '100%' }}
addonAfter="小时"
placeholder="0"
/>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
prevValues.timeoutDuration !== currentValues.timeoutDuration
}>
{({ getFieldValue }) => {
const timeoutDuration = getFieldValue('timeoutDuration');
return timeoutDuration && timeoutDuration > 0 ? (
<Form.Item
label="超时处理"
name="timeoutAction"
tooltip="审批超时后的处理方式"
>
<Select placeholder="请选择超时处理方式">
<Select.Option value="NONE"></Select.Option>
<Select.Option value="AUTO_APPROVE"></Select.Option>
<Select.Option value="AUTO_REJECT"></Select.Option>
<Select.Option value="NOTIFY"></Select.Option>
</Select>
</Form.Item>
) : null;
}}
</Form.Item>
<Form.Item
label="允许转交"
name="allowDelegate"
valuePropName="checked"
tooltip="审批人是否可以将任务转交给他人"
>
<Switch />
</Form.Item>
<Form.Item
label="允许加签"
name="allowAddSign"
valuePropName="checked"
tooltip="审批人是否可以加签其他人一起审批"
>
<Switch />
</Form.Item>
<Form.Item
label="必须填写意见"
name="requireComment"
valuePropName="checked"
tooltip="审批时是否必须填写审批意见"
>
<Switch />
</Form.Item>
</Form>
);
};
export default ApproverPropertyConfig;

View File

@ -0,0 +1,175 @@
/**
*
*
*
* -
* - + + /
* -
*/
import React, { useMemo } from 'react';
import { Space, Typography, Tag, Alert, Input, Button } from 'antd';
import { CheckCircleOutlined, UsergroupAddOutlined } from '@ant-design/icons';
import type { FieldConfig } from '../../types';
const { Text } = Typography;
const { TextArea } = Input;
interface ApproverSelectorProps {
field: FieldConfig;
value?: string | string[];
onChange?: (value: string | string[]) => void;
disabled?: boolean;
isPreview?: boolean;
}
export const ApproverSelector: React.FC<ApproverSelectorProps> = ({
field,
value,
onChange,
disabled = false,
isPreview = false,
}) => {
// 审批配置
const {
approvalMode = 'SINGLE',
approverType = 'USER',
approvalTitle = '审批节点',
approvalContent,
} = field;
// 获取审批模式的文本
const modeText = useMemo(() => {
switch (approvalMode) {
case 'SINGLE':
return '单人审批';
case 'ALL':
return '会签(所有人同意)';
case 'ANY':
return '或签(任一人同意)';
default:
return '单人审批';
}
}, [approvalMode]);
// 获取审批人类型的文本
const approverText = useMemo(() => {
switch (approverType) {
case 'USER':
return '指定用户';
case 'ROLE':
return '指定角色';
case 'DEPARTMENT':
return '指定部门';
case 'VARIABLE':
return '变量指定';
default:
return '指定用户';
}
}, [approverType]);
// ==================== 1⃣ 预览模式:模拟审批界面 ====================
if (isPreview) {
return (
<div style={{
border: '1px solid #d9d9d9',
borderRadius: 8,
padding: 20,
backgroundColor: '#fafafa'
}}>
{/* 审批标题 */}
<div style={{ marginBottom: 16 }}>
<Text strong style={{ fontSize: 16, color: '#262626' }}>
<CheckCircleOutlined style={{ marginRight: 8, color: '#52c41a' }} />
{approvalTitle || '待审批'}
</Text>
</div>
{/* 审批说明 */}
{approvalContent && (
<div style={{ marginBottom: 16, padding: 12, backgroundColor: '#e6f7ff', borderRadius: 4 }}>
<Text type="secondary" style={{ fontSize: 14 }}>
{approvalContent}
</Text>
</div>
)}
{/* 审批方式提示 */}
<div style={{ marginBottom: 16 }}>
<Space size={4}>
<Tag color="blue">{modeText}</Tag>
<Tag color="green">{approverText}</Tag>
</Space>
</div>
{/* 审批意见输入框(预览状态) */}
<div style={{ marginBottom: 16 }}>
<Text type="secondary" style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>
{field.requireComment && <Text type="danger">*</Text>}
</Text>
<TextArea
placeholder="请填写审批意见..."
rows={4}
disabled
style={{ backgroundColor: '#fff' }}
/>
</div>
{/* 审批按钮(预览状态) */}
<Space>
<Button type="primary" disabled>
</Button>
<Button danger disabled>
</Button>
{field.allowDelegate && (
<Button disabled>
</Button>
)}
{field.allowAddSign && (
<Button disabled>
</Button>
)}
</Space>
{/* 预览提示 */}
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
💡
</div>
</div>
);
}
// ==================== 2⃣ 设计模式:简洁占位提示 ====================
return (
<Alert
icon={<CheckCircleOutlined />}
message={
<Space>
<UsergroupAddOutlined />
<span style={{ fontWeight: 500 }}>{approvalTitle || '审批节点'}</span>
</Space>
}
description={
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
{modeText} · {approverText}
{approvalContent && ` · ${approvalContent.substring(0, 30)}${approvalContent.length > 30 ? '...' : ''}`}
</div>
}
type="info"
showIcon
style={{
width: '100%',
cursor: 'pointer',
backgroundColor: '#f0f9ff',
border: '1px solid #91caff'
}}
/>
);
};
export default ApproverSelector;

View File

@ -0,0 +1,47 @@
/**
*
*/
import type { ComponentMeta } from '../../config';
import { UserOutlined } from '@ant-design/icons';
import { ApproverPropertyConfig } from './ApproverPropertyConfig';
import { ApproverSelector } from './ApproverSelector';
/**
*
* extraComponents FormDesigner
*/
export const WORKFLOW_COMPONENTS: ComponentMeta[] = [
{
type: 'approver-selector',
label: '审批人',
icon: UserOutlined,
category: '高级字段',
// 🔌 插件式扩展:自定义属性配置组件
PropertyConfigComponent: ApproverPropertyConfig,
// 🔌 插件式扩展:自定义字段渲染组件
FieldRendererComponent: ApproverSelector,
// 默认配置
defaultConfig: {
// id 和 type 由 Designer 自动生成,不在这里定义
// label 和 name 也由 Designer 生成,这里只定义审批相关的配置
required: true,
// 审批配置
approvalMode: 'SINGLE', // SINGLE | ALL | ANY
approverType: 'USER', // USER | ROLE | DEPARTMENT | VARIABLE
// 审批界面
approvalTitle: '请审批',
approvalContent: '',
// 高级设置
timeoutDuration: 0, // 超时时间小时0表示不限制
timeoutAction: 'NONE', // NONE | AUTO_APPROVE | AUTO_REJECT | NOTIFY
allowDelegate: false, // 允许转交
allowAddSign: false, // 允许加签
requireComment: false, // 必须填写意见
},
},
];

View File

@ -0,0 +1,8 @@
/**
*
*/
export { ApproverSelector } from './ApproverSelector';
export { ApproverPropertyConfig } from './ApproverPropertyConfig';
export { WORKFLOW_COMPONENTS } from './config';

View File

@ -3,7 +3,7 @@
* API数据和预定义数据源
*/
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import type { FieldConfig, FieldOption } from '../types';
import request from '../../../utils/request';
import { loadDataSource, DataSourceType as WorkflowDataSourceType } from '@/domain/dataSource';
@ -32,12 +32,16 @@ const getValueByPath = (obj: any, path?: string): any => {
export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
const [options, setOptions] = useState<FieldOption[]>([]);
// 🔥 使用 useRef 存储加载状态,避免触发 useEffect
const loadingStateRef = useRef<'idle' | 'loading' | 'success' | 'error'>('idle');
const lastSourceKeyRef = useRef<string>('');
useEffect(() => {
const loadOptions = async () => {
// 静态数据源
if (field.dataSourceType === 'static' || !field.dataSourceType) {
setOptions(field.options || []);
loadingStateRef.current = 'success';
return;
}
@ -48,19 +52,38 @@ export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
if (!sourceType) {
console.warn('⚠️ 预定义数据源配置不完整,缺少 sourceType');
setOptions([]);
loadingStateRef.current = 'error';
return;
}
// 🔥 生成数据源唯一标识
const sourceKey = `predefined_${sourceType}`;
// 🔥 如果已经加载过这个数据源,不重复加载
if (sourceKey === lastSourceKeyRef.current && (loadingStateRef.current === 'success' || loadingStateRef.current === 'error')) {
return;
}
// 🔥 如果当前正在加载,不重复发起请求
if (loadingStateRef.current === 'loading') {
return;
}
loadingStateRef.current = 'loading';
lastSourceKeyRef.current = sourceKey;
try {
// 调用 Workflow 的数据源加载器
const dataSourceOptions = await loadDataSource(sourceType as WorkflowDataSourceType);
// dataSourceLoader 已经转换为 { label, value } 格式
setOptions(dataSourceOptions);
loadingStateRef.current = 'success';
console.log('✅ 预定义数据源加载成功', { sourceType, count: dataSourceOptions.length });
} catch (error) {
console.error('❌ 加载预定义数据源失败', { sourceType, error });
setOptions([]);
loadingStateRef.current = 'error'; // 🔥 标记为错误状态,不再重试
}
return;
}
@ -71,20 +94,28 @@ export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
// 验证必填字段(空字符串也视为缺失)
if (!url || !url.trim() || !labelField || !labelField.trim() || !valueField || !valueField.trim()) {
console.warn('⚠️ API 数据源配置不完整,缺少必填字段', {
fieldId: field.id,
fieldName: field.name,
配置信息: field.apiDataSource,
: {
url: !url || !url.trim() ? '缺少 URL' : '✓',
labelField: !labelField || !labelField.trim() ? '缺少 labelField' : '✓',
valueField: !valueField || !valueField.trim() ? '缺少 valueField' : '✓',
},
});
console.warn('⚠️ API 数据源配置不完整,缺少必填字段');
setOptions([]);
loadingStateRef.current = 'error';
return;
}
// 🔥 生成 API 数据源唯一标识
const sourceKey = `api_${url}_${method}_${labelField}_${valueField}`;
// 🔥 如果已经加载过这个数据源,不重复加载
if (sourceKey === lastSourceKeyRef.current && (loadingStateRef.current === 'success' || loadingStateRef.current === 'error')) {
return;
}
// 🔥 如果当前正在加载,不重复发起请求
if (loadingStateRef.current === 'loading') {
return;
}
loadingStateRef.current = 'loading';
lastSourceKeyRef.current = sourceKey;
try {
let response: any;
@ -105,25 +136,31 @@ export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
value: item[valueField],
}));
setOptions(transformedOptions);
loadingStateRef.current = 'success';
console.log('✅ API 数据加载成功', { url, count: transformedOptions.length });
} else {
console.error('❌ API 返回的数据格式不正确,期望数组', {
url,
dataPath,
actualData: dataArray,
fullResponse: response,
});
console.error('❌ API 返回的数据格式不正确,期望数组');
setOptions([]);
loadingStateRef.current = 'error';
}
} catch (error) {
console.error('❌ 加载 API 数据失败', { url, error });
setOptions([]);
loadingStateRef.current = 'error'; // 🔥 标记为错误状态,不再重试
}
}
};
loadOptions();
}, [field.dataSourceType, field.options, field.apiDataSource, field.predefinedDataSource]);
}, [
field.dataSourceType,
field.options,
field.predefinedDataSource?.sourceType, // 🔥 只依赖具体的值,而不是整个对象
field.apiDataSource?.url,
field.apiDataSource?.method,
field.apiDataSource?.labelField,
field.apiDataSource?.valueField,
]);
return options;
};

View File

@ -35,3 +35,6 @@ export type {
export { COMPONENT_LIST, getComponentsByCategory } from './config';
export type { ComponentMeta } from './config';
// 导出扩展字段(可选,按需使用)
export { WORKFLOW_COMPONENTS, ApproverSelector } from './extensions/workflow';

View File

@ -20,7 +20,8 @@ export type FieldType =
| 'cascader' // 级联选择
| 'text' // 纯文本
| 'grid' // 栅格布局
| 'divider'; // 分割线
| 'divider' // 分割线
| 'approver-selector'; // 审批人选择器(工作流扩展)
// 选项类型(用于 select, radio, checkbox
export interface FieldOption {
@ -139,6 +140,25 @@ export interface FieldConfig {
columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2]总和不超过24
gutter?: number; // 栅格间距(用于 grid 组件)
children?: FieldConfig[][]; // 子字段(用于容器组件,二维数组,每个子数组代表一列)
// ========== 审批人选择器专用属性(工作流扩展) ==========
// 审批配置
approvalMode?: 'SINGLE' | 'ALL' | 'ANY'; // 审批模式SINGLE=单人审批ALL=会签所有人同意ANY=或签(任一人同意)
approverType?: 'USER' | 'ROLE' | 'DEPARTMENT' | 'VARIABLE'; // 审批人类型USER=指定用户ROLE=指定角色DEPARTMENT=指定部门VARIABLE=变量指定
// 审批人选择
approvers?: string[]; // 审批人列表用户username- 当 approverType='USER' 时使用
approverRoles?: string[]; // 审批角色列表角色code- 当 approverType='ROLE' 时使用
approverDepartments?: string[]; // 审批部门列表部门code- 当 approverType='DEPARTMENT' 时使用
approverVariable?: string; // 审批人变量(如 ${initiator}- 当 approverType='VARIABLE' 时使用
// 审批界面
approvalTitle?: string; // 审批标题(显示在审批弹窗中)
approvalContent?: string; // 审批说明(显示在审批弹窗中)
// 高级设置
timeoutDuration?: number; // 超时时间小时0表示不限制
timeoutAction?: 'NONE' | 'AUTO_APPROVE' | 'AUTO_REJECT' | 'NOTIFY'; // 超时处理NONE=不处理AUTO_APPROVE=自动同意AUTO_REJECT=自动拒绝NOTIFY=通知管理员
allowDelegate?: boolean; // 允许转交
allowAddSign?: boolean; // 允许加签
requireComment?: boolean; // 必须填写审批意见
}
// 表单配置

View File

@ -7,11 +7,12 @@
*/
import React, { useState } from 'react';
import { message, Tabs, Card, Button, Modal, Space, Divider } from 'antd';
import { message, Tabs, Card, Button, Modal, Space, Divider, Alert } from 'antd';
import { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner';
import { WORKFLOW_COMPONENTS } from '@/components/FormDesigner/extensions/workflow';
const FormDesignerExamplesPage: React.FC = () => {
// ==================== 示例 1: 表单设计器 ====================
// ==================== 示例 1: 普通表单设计器 ====================
const [designerSchema, setDesignerSchema] = useState<FormSchema>();
const handleDesignerSave = async (schema: FormSchema) => {
@ -22,6 +23,19 @@ const FormDesignerExamplesPage: React.FC = () => {
// await formSchemaService.save(schema);
};
// ==================== 示例 1.5: 工作流表单设计器(带审批字段) ====================
const [workflowDesignerSchema, setWorkflowDesignerSchema] = useState<FormSchema>();
const handleWorkflowDesignerSave = async (schema: FormSchema) => {
console.log('🔄 保存的工作流表单 Schema:', JSON.stringify(schema, null, 2));
message.success('工作流表单设计已保存!请查看控制台');
setWorkflowPreviewSchema(schema);
// 实际项目中这里会调用后端 API 保存
// await workflowFormService.save(schema);
};
const [workflowPreviewSchema, setWorkflowPreviewSchema] = useState<FormSchema>();
// ==================== 示例 2: 表单渲染器(静态) ====================
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
const [staticFormData, setStaticFormData] = useState<Record<string, any>>({});
@ -200,13 +214,20 @@ const FormDesignerExamplesPage: React.FC = () => {
const tabItems = [
{
key: '1',
label: '📝 示例 1: 表单设计器',
label: '📝 示例 1: 普通表单设计器',
children: (
<Card>
<h2>FormDesigner</h2>
<h2>FormDesigner</h2>
<p style={{ marginBottom: 16, color: '#666' }}>
/ JSON Schema
</p>
<Alert
message="核心组件列表"
description="此设计器仅包含核心表单组件(输入框、下拉框、日期等),不包含工作流扩展字段。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Divider />
<FormDesigner
value={designerSchema}
@ -217,11 +238,59 @@ const FormDesignerExamplesPage: React.FC = () => {
),
},
{
key: '2',
label: '🎨 示例 2: 静态表单渲染',
key: '1.5',
label: '🔄 示例 1.5: 工作流表单设计器',
children: (
<Card>
<h2></h2>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
<code>extraComponents</code>
</p>
<Alert
message="🔌 插件式架构展示"
description={
<div>
<p> + </p>
<p> "高级字段""审批人"</p>
<p> 9</p>
<p> </p>
</div>
}
type="success"
showIcon
style={{ marginBottom: 16 }}
/>
<div style={{
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
marginBottom: 16,
fontFamily: 'monospace',
fontSize: 12
}}>
<strong>使:</strong><br />
{`<FormDesigner`}<br />
{` value={schema}`}<br />
{` onChange={setSchema}`}<br />
{` extraComponents={WORKFLOW_COMPONENTS}`}<br />
{`/>`}
</div>
<Divider />
<FormDesigner
value={workflowDesignerSchema}
onChange={setWorkflowDesignerSchema}
onSave={handleWorkflowDesignerSave}
extraComponents={WORKFLOW_COMPONENTS}
/>
</Card>
),
},
{
key: '2',
label: '🎨 示例 2: 普通表单渲染',
children: (
<Card>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
Schema
</p>
@ -242,6 +311,41 @@ const FormDesignerExamplesPage: React.FC = () => {
</Card>
),
},
{
key: '2.5',
label: '🔄 示例 2.5: 工作流表单渲染',
children: (
<Card>
<h2></h2>
<p style={{ marginBottom: 16, color: '#666' }}>
<code>extraComponents</code>
</p>
<Alert
message="重要提示"
description="使用 FormRenderer 渲染包含扩展字段的表单时,必须传递 extraComponents={WORKFLOW_COMPONENTS},否则扩展字段无法正确渲染。"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<Divider />
{workflowPreviewSchema ? (
<FormRenderer
schema={workflowPreviewSchema}
extraComponents={WORKFLOW_COMPONENTS}
onSubmit={async (data) => {
console.log('🔄 工作流表单提交数据:', data);
message.success('工作流表单提交成功!');
}}
showCancel
/>
) : (
<div style={{ padding: 40, textAlign: 'center', color: '#999' }}>
"示例 1.5"
</div>
)}
</Card>
),
},
{
key: '3',
label: '🚀 示例 3: 工作流表单',