增加审批组件
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 FormPreview, { type FormPreviewRef } from './components/FormPreview';
|
||||||
import { COMPONENT_LIST } from './config';
|
import { COMPONENT_LIST } from './config';
|
||||||
import type { FieldConfig, FormConfig, FormSchema } from './types';
|
import type { FieldConfig, FormConfig, FormSchema } from './types';
|
||||||
|
import type { ComponentMeta } from './config';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
|
// 🔌 组件列表 Context(用于插件式扩展)
|
||||||
|
export const ComponentsContext = React.createContext<ComponentMeta[]>(COMPONENT_LIST);
|
||||||
|
|
||||||
export interface FormDesignerProps {
|
export interface FormDesignerProps {
|
||||||
value?: FormSchema; // 受控模式:表单 Schema
|
value?: FormSchema; // 受控模式:表单 Schema
|
||||||
onChange?: (schema: FormSchema) => void; // 受控模式:Schema 变化回调
|
onChange?: (schema: FormSchema) => void; // 受控模式:Schema 变化回调
|
||||||
@ -55,6 +59,7 @@ export interface FormDesignerProps {
|
|||||||
readonly?: boolean; // 只读模式
|
readonly?: boolean; // 只读模式
|
||||||
showToolbar?: boolean; // 是否显示工具栏
|
showToolbar?: boolean; // 是否显示工具栏
|
||||||
extraActions?: React.ReactNode; // 额外的操作按钮
|
extraActions?: React.ReactNode; // 额外的操作按钮
|
||||||
|
extraComponents?: ComponentMeta[]; // 🆕 扩展字段组件(如工作流字段)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormDesigner: React.FC<FormDesignerProps> = ({
|
const FormDesigner: React.FC<FormDesignerProps> = ({
|
||||||
@ -63,7 +68,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
onSave,
|
onSave,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
showToolbar = true,
|
showToolbar = true,
|
||||||
extraActions
|
extraActions,
|
||||||
|
extraComponents = [],
|
||||||
}) => {
|
}) => {
|
||||||
// 表单预览 ref
|
// 表单预览 ref
|
||||||
const formPreviewRef = useRef<FormPreviewRef>(null);
|
const formPreviewRef = useRef<FormPreviewRef>(null);
|
||||||
@ -149,6 +155,9 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
// 选中的字段
|
// 选中的字段
|
||||||
const [selectedFieldId, setSelectedFieldId] = useState<string>();
|
const [selectedFieldId, setSelectedFieldId] = useState<string>();
|
||||||
|
|
||||||
|
// 待选中的字段ID(用于确保字段添加后再选中)
|
||||||
|
const [pendingSelectId, setPendingSelectId] = useState<string | null>(null);
|
||||||
|
|
||||||
// 剪贴板(用于复制粘贴)
|
// 剪贴板(用于复制粘贴)
|
||||||
const [clipboard, setClipboard] = useState<FieldConfig | 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 customCollisionDetection = useCallback((args: any) => {
|
||||||
const pointerCollisions = pointerWithin(args);
|
const pointerCollisions = pointerWithin(args);
|
||||||
@ -280,7 +294,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
// 从组件面板拖拽新组件到栅格列
|
// 从组件面板拖拽新组件到栅格列
|
||||||
if (activeData?.isNew && overData?.gridId && overData?.colIndex !== undefined) {
|
if (activeData?.isNew && overData?.gridId && overData?.colIndex !== undefined) {
|
||||||
const componentType = active.id.toString().replace('new-', '');
|
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) {
|
if (component) {
|
||||||
const newField: FieldConfig = {
|
const newField: FieldConfig = {
|
||||||
@ -312,7 +326,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
return updateGrid(prev);
|
return updateGrid(prev);
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedFieldId(newField.id);
|
// 🔄 使用 pendingSelectId 确保字段添加后再选中
|
||||||
|
setPendingSelectId(newField.id);
|
||||||
message.success(`已添加${component.label}到栅格列`);
|
message.success(`已添加${component.label}到栅格列`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -321,7 +336,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
// 从组件面板拖拽新组件到画布或字段
|
// 从组件面板拖拽新组件到画布或字段
|
||||||
if (activeData?.isNew && overData?.accept === 'field') {
|
if (activeData?.isNew && overData?.accept === 'field') {
|
||||||
const componentType = active.id.toString().replace('new-', '');
|
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) {
|
if (component) {
|
||||||
const newField: FieldConfig = {
|
const newField: FieldConfig = {
|
||||||
@ -351,7 +366,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
return newFields;
|
return newFields;
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedFieldId(newField.id);
|
// 🔄 使用 pendingSelectId 确保字段添加后再选中
|
||||||
|
setPendingSelectId(newField.id);
|
||||||
message.success(`已添加${component.label}`);
|
message.success(`已添加${component.label}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -432,7 +448,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
|
|
||||||
const newField = deepCopyField(clipboard);
|
const newField = deepCopyField(clipboard);
|
||||||
setFields(prev => [...prev, newField]);
|
setFields(prev => [...prev, newField]);
|
||||||
setSelectedFieldId(newField.id);
|
// 🔄 使用 pendingSelectId 确保字段添加后再选中
|
||||||
|
setPendingSelectId(newField.id);
|
||||||
message.success('已粘贴字段');
|
message.success('已粘贴字段');
|
||||||
}, [clipboard, deepCopyField, readonly, setFields]);
|
}, [clipboard, deepCopyField, readonly, setFields]);
|
||||||
|
|
||||||
@ -457,7 +474,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
return field;
|
return field;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setSelectedFieldId(newField.id);
|
// 🔄 使用 pendingSelectId 确保字段添加后再选中
|
||||||
|
setPendingSelectId(newField.id);
|
||||||
message.success('已粘贴字段到栅格');
|
message.success('已粘贴字段到栅格');
|
||||||
}, [clipboard, deepCopyField, readonly, setFields]);
|
}, [clipboard, deepCopyField, readonly, setFields]);
|
||||||
|
|
||||||
@ -652,6 +670,20 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
|
|
||||||
const selectedField = findFieldById(fields, selectedFieldId) || null;
|
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中的嵌套字段)
|
// 获取所有字段(扁平化,包括grid中的嵌套字段)
|
||||||
const getAllFields = useCallback((fieldList: FieldConfig[]): FieldConfig[] => {
|
const getAllFields = useCallback((fieldList: FieldConfig[]): FieldConfig[] => {
|
||||||
const result: FieldConfig[] = [];
|
const result: FieldConfig[] = [];
|
||||||
@ -719,7 +751,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
}, [selectedFieldId, clipboard, readonly, handleCopyField, handlePasteToCanvas, handleDeleteField]);
|
}, [selectedFieldId, clipboard, readonly, handleCopyField, handlePasteToCanvas, handleDeleteField]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-designer">
|
<ComponentsContext.Provider value={allComponents}>
|
||||||
|
<div className="form-designer">
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<div className="form-designer-header">
|
<div className="form-designer-header">
|
||||||
<div className="form-designer-title">表单设计</div>
|
<div className="form-designer-title">表单设计</div>
|
||||||
@ -768,7 +801,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
<div className="form-designer-body">
|
<div className="form-designer-body">
|
||||||
{/* 左侧组件面板 */}
|
{/* 左侧组件面板 */}
|
||||||
<div style={{ width: 280 }}>
|
<div style={{ width: 280 }}>
|
||||||
<ComponentPanel />
|
<ComponentPanel extraComponents={extraComponents} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 中间设计画布 */}
|
{/* 中间设计画布 */}
|
||||||
@ -809,7 +842,7 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||||
cursor: 'grabbing',
|
cursor: 'grabbing',
|
||||||
}}>
|
}}>
|
||||||
{COMPONENT_LIST.find(c => `new-${c.type}` === activeId)?.label || '组件'}
|
{allComponents.find(c => `new-${c.type}` === activeId)?.label || '组件'}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
@ -845,7 +878,8 @@ const FormDesigner: React.FC<FormDesignerProps> = ({
|
|||||||
>
|
>
|
||||||
<FormPreview ref={formPreviewRef} fields={fields} formConfig={formConfig} />
|
<FormPreview ref={formPreviewRef} fields={fields} formConfig={formConfig} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
</ComponentsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,21 @@
|
|||||||
* 根据 Schema 渲染可交互的表单
|
* 根据 Schema 渲染可交互的表单
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Form, Button, message } from 'antd';
|
import { Form, Button, message } from 'antd';
|
||||||
import type { Rule } from 'antd/es/form';
|
import type { Rule } from 'antd/es/form';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { FieldConfig, FormSchema, ValidationRule, LinkageRule } from './types';
|
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 FieldRenderer from './components/FieldRenderer';
|
||||||
import GridFieldPreview from './components/GridFieldPreview';
|
import GridFieldPreview from './components/GridFieldPreview';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
export interface FormRendererProps {
|
export interface FormRendererProps {
|
||||||
schema: FormSchema; // 表单 Schema
|
schema: FormSchema; // 表单 Schema
|
||||||
|
extraComponents?: ComponentMeta[]; // 🆕 扩展组件(如工作流字段)
|
||||||
value?: Record<string, any>; // 表单值(受控)
|
value?: Record<string, any>; // 表单值(受控)
|
||||||
onChange?: (values: Record<string, any>) => void; // 值变化回调
|
onChange?: (values: Record<string, any>) => void; // 值变化回调
|
||||||
onSubmit?: (values: Record<string, any>) => void | Promise<any>; // 提交回调
|
onSubmit?: (values: Record<string, any>) => void | Promise<any>; // 提交回调
|
||||||
@ -30,6 +35,7 @@ export interface FormRendererProps {
|
|||||||
|
|
||||||
const FormRenderer: React.FC<FormRendererProps> = ({
|
const FormRenderer: React.FC<FormRendererProps> = ({
|
||||||
schema,
|
schema,
|
||||||
|
extraComponents = [],
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -45,6 +51,11 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { fields, formConfig } = schema;
|
const { fields, formConfig } = schema;
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 🔌 合并核心组件和扩展组件
|
||||||
|
const allComponents = useMemo(() => {
|
||||||
|
return [...COMPONENT_LIST, ...extraComponents];
|
||||||
|
}, [extraComponents]);
|
||||||
const [formData, setFormData] = useState<Record<string, any>>(value || {});
|
const [formData, setFormData] = useState<Record<string, any>>(value || {});
|
||||||
const [fieldStates, setFieldStates] = useState<Record<string, {
|
const [fieldStates, setFieldStates] = useState<Record<string, {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
@ -272,7 +283,12 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||||
fieldList.forEach(field => {
|
fieldList.forEach(field => {
|
||||||
if (field.defaultValue !== undefined && field.name) {
|
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) {
|
if (field.type === 'grid' && field.children) {
|
||||||
field.children.forEach(columnFields => {
|
field.children.forEach(columnFields => {
|
||||||
@ -384,8 +400,9 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
|
const wrapperColSpan = formConfig.labelAlign === 'left' || formConfig.labelAlign === 'right' ? 18 : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px 40px' }}>
|
<ComponentsContext.Provider value={allComponents}>
|
||||||
<Form
|
<div style={{ padding: '24px 40px' }}>
|
||||||
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout={formLayout}
|
layout={formLayout}
|
||||||
size={formConfig.size}
|
size={formConfig.size}
|
||||||
@ -428,7 +445,8 @@ const FormRenderer: React.FC<FormRendererProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</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 ComponentPanel: React.FC<ComponentPanelProps> = ({ extraComponents = [] }) => {
|
||||||
const componentsByCategory = getComponentsByCategory();
|
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 = ['布局字段', '基础字段', '高级字段'];
|
const categoryOrder = ['布局字段', '基础字段', '高级字段'];
|
||||||
@ -64,7 +82,7 @@ const ComponentPanel: React.FC = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
items={categoryOrder
|
items={categoryOrder
|
||||||
.map((category) => {
|
.map((category) => {
|
||||||
const components = componentsByCategory[category];
|
const components = allComponents[category];
|
||||||
if (!components || components.length === 0) return null;
|
if (!components || components.length === 0) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
* 根据字段类型渲染对应的表单组件
|
* 根据字段类型渲染对应的表单组件
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { ComponentsContext } from '../Designer';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@ -69,6 +70,20 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
const cascadeOptions = useCascaderOptions(field);
|
const cascadeOptions = useCascaderOptions(field);
|
||||||
const loadData = useCascaderLoadData(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') {
|
if (field.type === 'divider') {
|
||||||
return <Divider>{field.label}</Divider>;
|
return <Divider>{field.label}</Divider>;
|
||||||
@ -253,6 +268,8 @@ const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|||||||
</Upload>
|
</Upload>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// approver-selector 等扩展字段已通过插件机制处理,不在此硬编码
|
||||||
|
|
||||||
case 'cascader':
|
case 'cascader':
|
||||||
return (
|
return (
|
||||||
<Cascader
|
<Cascader
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||||
import { Form, message } from 'antd';
|
import { Form, message } from 'antd';
|
||||||
import type { Rule } from 'antd/es/form';
|
import type { Rule } from 'antd/es/form';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from '../types';
|
import type { FieldConfig, FormConfig, ValidationRule, LinkageRule } from '../types';
|
||||||
import FieldRenderer from './FieldRenderer';
|
import FieldRenderer from './FieldRenderer';
|
||||||
import GridFieldPreview from './GridFieldPreview';
|
import GridFieldPreview from './GridFieldPreview';
|
||||||
@ -191,7 +192,12 @@ const FormPreview = forwardRef<FormPreviewRef, FormPreviewProps>(({ fields, form
|
|||||||
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
const collectDefaultValues = (fieldList: FieldConfig[]) => {
|
||||||
fieldList.forEach(field => {
|
fieldList.forEach(field => {
|
||||||
if (field.defaultValue !== undefined && field.name) {
|
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) {
|
if (field.type === 'grid' && field.children) {
|
||||||
field.children.forEach(columnFields => {
|
field.children.forEach(columnFields => {
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
* 配置选中字段的属性
|
* 配置选中字段的属性
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { ComponentsContext } from '../Designer';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@ -44,6 +45,9 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
fieldNames,
|
fieldNames,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 🔌 获取所有组件(包括扩展组件)- 必须在顶层调用 useContext
|
||||||
|
const allComponents = useContext(ComponentsContext);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selectedField) {
|
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 hasOptions = ['select', 'radio', 'checkbox'].includes(selectedField.type);
|
||||||
const isCascader = selectedField.type === 'cascader';
|
const isCascader = selectedField.type === 'cascader';
|
||||||
|
|
||||||
@ -432,6 +465,35 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
</Form.Item>
|
</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 && (
|
{hasOptions && (
|
||||||
<div>
|
<div>
|
||||||
<Divider style={{ margin: '16px 0' }} />
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
* 表单设计器组件配置
|
* 表单设计器组件配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
FormOutlined,
|
FormOutlined,
|
||||||
FontSizeOutlined,
|
FontSizeOutlined,
|
||||||
@ -23,13 +24,33 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { FieldType, FieldConfig } from './types';
|
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 {
|
export interface ComponentMeta {
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
label: string;
|
label: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
category: '基础字段' | '高级字段' | '布局字段';
|
category: '基础字段' | '高级字段' | '布局字段';
|
||||||
defaultConfig: Partial<FieldConfig>;
|
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数据和预定义数据源
|
* 支持静态数据、动态API数据和预定义数据源
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import type { FieldConfig, FieldOption } from '../types';
|
import type { FieldConfig, FieldOption } from '../types';
|
||||||
import request from '../../../utils/request';
|
import request from '../../../utils/request';
|
||||||
import { loadDataSource, DataSourceType as WorkflowDataSourceType } from '@/domain/dataSource';
|
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[] => {
|
export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
|
||||||
const [options, setOptions] = useState<FieldOption[]>([]);
|
const [options, setOptions] = useState<FieldOption[]>([]);
|
||||||
|
// 🔥 使用 useRef 存储加载状态,避免触发 useEffect
|
||||||
|
const loadingStateRef = useRef<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
|
const lastSourceKeyRef = useRef<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
// 静态数据源
|
// 静态数据源
|
||||||
if (field.dataSourceType === 'static' || !field.dataSourceType) {
|
if (field.dataSourceType === 'static' || !field.dataSourceType) {
|
||||||
setOptions(field.options || []);
|
setOptions(field.options || []);
|
||||||
|
loadingStateRef.current = 'success';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,19 +52,38 @@ export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
|
|||||||
if (!sourceType) {
|
if (!sourceType) {
|
||||||
console.warn('⚠️ 预定义数据源配置不完整,缺少 sourceType');
|
console.warn('⚠️ 预定义数据源配置不完整,缺少 sourceType');
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
loadingStateRef.current = 'error';
|
||||||
return;
|
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 {
|
try {
|
||||||
// 调用 Workflow 的数据源加载器
|
// 调用 Workflow 的数据源加载器
|
||||||
const dataSourceOptions = await loadDataSource(sourceType as WorkflowDataSourceType);
|
const dataSourceOptions = await loadDataSource(sourceType as WorkflowDataSourceType);
|
||||||
|
|
||||||
// dataSourceLoader 已经转换为 { label, value } 格式
|
// dataSourceLoader 已经转换为 { label, value } 格式
|
||||||
setOptions(dataSourceOptions);
|
setOptions(dataSourceOptions);
|
||||||
|
loadingStateRef.current = 'success';
|
||||||
console.log('✅ 预定义数据源加载成功', { sourceType, count: dataSourceOptions.length });
|
console.log('✅ 预定义数据源加载成功', { sourceType, count: dataSourceOptions.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 加载预定义数据源失败', { sourceType, error });
|
console.error('❌ 加载预定义数据源失败', { sourceType, error });
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
loadingStateRef.current = 'error'; // 🔥 标记为错误状态,不再重试
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -71,20 +94,28 @@ export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
|
|||||||
|
|
||||||
// 验证必填字段(空字符串也视为缺失)
|
// 验证必填字段(空字符串也视为缺失)
|
||||||
if (!url || !url.trim() || !labelField || !labelField.trim() || !valueField || !valueField.trim()) {
|
if (!url || !url.trim() || !labelField || !labelField.trim() || !valueField || !valueField.trim()) {
|
||||||
console.warn('⚠️ API 数据源配置不完整,缺少必填字段', {
|
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' : '✓',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
loadingStateRef.current = 'error';
|
||||||
return;
|
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 {
|
try {
|
||||||
let response: any;
|
let response: any;
|
||||||
|
|
||||||
@ -105,25 +136,31 @@ export const useFieldOptions = (field: FieldConfig): FieldOption[] => {
|
|||||||
value: item[valueField],
|
value: item[valueField],
|
||||||
}));
|
}));
|
||||||
setOptions(transformedOptions);
|
setOptions(transformedOptions);
|
||||||
|
loadingStateRef.current = 'success';
|
||||||
console.log('✅ API 数据加载成功', { url, count: transformedOptions.length });
|
console.log('✅ API 数据加载成功', { url, count: transformedOptions.length });
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ API 返回的数据格式不正确,期望数组', {
|
console.error('❌ API 返回的数据格式不正确,期望数组');
|
||||||
url,
|
|
||||||
dataPath,
|
|
||||||
actualData: dataArray,
|
|
||||||
fullResponse: response,
|
|
||||||
});
|
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
loadingStateRef.current = 'error';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 加载 API 数据失败', { url, error });
|
console.error('❌ 加载 API 数据失败', { url, error });
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
loadingStateRef.current = 'error'; // 🔥 标记为错误状态,不再重试
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadOptions();
|
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;
|
return options;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,3 +35,6 @@ export type {
|
|||||||
export { COMPONENT_LIST, getComponentsByCategory } from './config';
|
export { COMPONENT_LIST, getComponentsByCategory } from './config';
|
||||||
export type { ComponentMeta } from './config';
|
export type { ComponentMeta } from './config';
|
||||||
|
|
||||||
|
// 导出扩展字段(可选,按需使用)
|
||||||
|
export { WORKFLOW_COMPONENTS, ApproverSelector } from './extensions/workflow';
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,8 @@ export type FieldType =
|
|||||||
| 'cascader' // 级联选择
|
| 'cascader' // 级联选择
|
||||||
| 'text' // 纯文本
|
| 'text' // 纯文本
|
||||||
| 'grid' // 栅格布局
|
| 'grid' // 栅格布局
|
||||||
| 'divider'; // 分割线
|
| 'divider' // 分割线
|
||||||
|
| 'approver-selector'; // 审批人选择器(工作流扩展)
|
||||||
|
|
||||||
// 选项类型(用于 select, radio, checkbox)
|
// 选项类型(用于 select, radio, checkbox)
|
||||||
export interface FieldOption {
|
export interface FieldOption {
|
||||||
@ -139,6 +140,25 @@ export interface FieldConfig {
|
|||||||
columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2],总和不超过24)
|
columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2],总和不超过24)
|
||||||
gutter?: number; // 栅格间距(用于 grid 组件)
|
gutter?: number; // 栅格间距(用于 grid 组件)
|
||||||
children?: FieldConfig[][]; // 子字段(用于容器组件,二维数组,每个子数组代表一列)
|
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 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 { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner';
|
||||||
|
import { WORKFLOW_COMPONENTS } from '@/components/FormDesigner/extensions/workflow';
|
||||||
|
|
||||||
const FormDesignerExamplesPage: React.FC = () => {
|
const FormDesignerExamplesPage: React.FC = () => {
|
||||||
// ==================== 示例 1: 表单设计器 ====================
|
// ==================== 示例 1: 普通表单设计器 ====================
|
||||||
const [designerSchema, setDesignerSchema] = useState<FormSchema>();
|
const [designerSchema, setDesignerSchema] = useState<FormSchema>();
|
||||||
|
|
||||||
const handleDesignerSave = async (schema: FormSchema) => {
|
const handleDesignerSave = async (schema: FormSchema) => {
|
||||||
@ -22,6 +23,19 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
// await formSchemaService.save(schema);
|
// 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: 表单渲染器(静态) ====================
|
// ==================== 示例 2: 表单渲染器(静态) ====================
|
||||||
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
|
const [previewSchema, setPreviewSchema] = useState<FormSchema>();
|
||||||
const [staticFormData, setStaticFormData] = useState<Record<string, any>>({});
|
const [staticFormData, setStaticFormData] = useState<Record<string, any>>({});
|
||||||
@ -200,13 +214,20 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: '📝 示例 1: 表单设计器',
|
label: '📝 示例 1: 普通表单设计器',
|
||||||
children: (
|
children: (
|
||||||
<Card>
|
<Card>
|
||||||
<h2>表单设计器(FormDesigner)</h2>
|
<h2>普通表单设计器(FormDesigner)</h2>
|
||||||
<p style={{ marginBottom: 16, color: '#666' }}>
|
<p style={{ marginBottom: 16, color: '#666' }}>
|
||||||
可视化拖拽设计表单,支持导入/导出 JSON Schema,支持字段属性配置、验证规则、联动规则等。
|
可视化拖拽设计表单,支持导入/导出 JSON Schema,支持字段属性配置、验证规则、联动规则等。
|
||||||
</p>
|
</p>
|
||||||
|
<Alert
|
||||||
|
message="核心组件列表"
|
||||||
|
description="此设计器仅包含核心表单组件(输入框、下拉框、日期等),不包含工作流扩展字段。"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<FormDesigner
|
<FormDesigner
|
||||||
value={designerSchema}
|
value={designerSchema}
|
||||||
@ -217,11 +238,59 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '1.5',
|
||||||
label: '🎨 示例 2: 静态表单渲染',
|
label: '🔄 示例 1.5: 工作流表单设计器',
|
||||||
children: (
|
children: (
|
||||||
<Card>
|
<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' }}>
|
<p style={{ marginBottom: 16, color: '#666' }}>
|
||||||
根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。
|
根据设计器保存的 Schema 渲染表单,支持数据收集、验证和提交。
|
||||||
</p>
|
</p>
|
||||||
@ -242,6 +311,41 @@ const FormDesignerExamplesPage: React.FC = () => {
|
|||||||
</Card>
|
</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',
|
key: '3',
|
||||||
label: '🚀 示例 3: 工作流表单',
|
label: '🚀 示例 3: 工作流表单',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user