增加审批组件
This commit is contained in:
parent
ecf3a1ba74
commit
d0c1c6e516
@ -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,7 +751,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
||||
}, [selectedFieldId, clipboard, readonly, handleCopyField, handlePasteToCanvas, handleDeleteField]);
|
||||
|
||||
return (
|
||||
<div className="form-designer">
|
||||
<ComponentsContext.Provider value={allComponents}>
|
||||
<div className="form-designer">
|
||||
{showToolbar && (
|
||||
<div className="form-designer-header">
|
||||
<div className="form-designer-title">表单设计</div>
|
||||
@ -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>
|
||||
@ -845,7 +878,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
||||
>
|
||||
<FormPreview ref={formPreviewRef} fields={fields} formConfig={formConfig} />
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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,7 +283,12 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||
fieldList.forEach(field => {
|
||||
if (field.defaultValue !== undefined && field.name) {
|
||||
defaultValues[field.name] = field.defaultValue;
|
||||
// 🔧 日期字段需要转换为 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 => {
|
||||
@ -384,8 +400,9 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px 40px' }}>
|
||||
<Form
|
||||
<ComponentsContext.Provider value={allComponents}>
|
||||
<div style={{ padding: '24px 40px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout={formLayout}
|
||||
size={formConfig.size}
|
||||
@ -428,7 +445,8 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,7 +192,12 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
|
||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||
fieldList.forEach(field => {
|
||||
if (field.defaultValue !== undefined && field.name) {
|
||||
defaultValues[field.name] = field.defaultValue;
|
||||
// 🔧 日期字段需要转换为 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 => {
|
||||
|
||||
@ -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' }} />
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
// 组件列表配置
|
||||
|
||||
281
frontend/src/components/FormDesigner/extensions/README.md
Normal file
281
frontend/src/components/FormDesigner/extensions/README.md
Normal 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
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 审批人选择器属性配置组件
|
||||
* 包含9个配置项,分为3组:审批配置、审批界面、高级设置
|
||||
*/
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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, // 必须填写意见
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 工作流扩展字段导出
|
||||
*/
|
||||
|
||||
export { ApproverSelector } from './ApproverSelector';
|
||||
export { ApproverPropertyConfig } from './ApproverPropertyConfig';
|
||||
export { WORKFLOW_COMPONENTS } from './config';
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -35,3 +35,6 @@ export type {
|
||||
export { COMPONENT_LIST, getComponentsByCategory } from './config';
|
||||
export type { ComponentMeta } from './config';
|
||||
|
||||
// 导出扩展字段(可选,按需使用)
|
||||
export { WORKFLOW_COMPONENTS, ApproverSelector } from './extensions/workflow';
|
||||
|
||||
|
||||
@ -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; // 必须填写审批意见
|
||||
}
|
||||
|
||||
// 表单配置
|
||||
|
||||
@ -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: 工作流表单',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user