This commit is contained in:
dengqichen 2025-10-21 10:17:32 +08:00
parent 3fadca9ffa
commit c08c99f422
22 changed files with 1033 additions and 901 deletions

View File

@ -113,6 +113,9 @@ importers:
'@types/recharts':
specifier: ^1.8.29
version: 1.8.29
'@xyflow/react':
specifier: ^12.8.6
version: 12.9.0(@types/react@18.3.18)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ajv:
specifier: ^8.17.1
version: 8.17.1
@ -1790,6 +1793,9 @@ packages:
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
@ -1805,6 +1811,9 @@ packages:
'@types/d3-scale@4.0.8':
resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
'@types/d3-shape@1.3.12':
resolution: {integrity: sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==}
@ -1817,6 +1826,12 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
'@types/dagre@0.7.52':
resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==}
@ -1990,6 +2005,15 @@ packages:
'@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
'@xyflow/react@12.9.0':
resolution: {integrity: sha512-bt37E8Wf2HQ7hHQaMSnOw4UEWQqWlNwzfgF9tjix5Fu9Pn/ph3wbexSS/wbWnTkv0vhgMVyphQLfFWIuCe59hQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@xyflow/system@0.0.71':
resolution: {integrity: sha512-O2xIK84Uv1hH8qzeY94SKsj0R1n2jXHLsX6RZnM4x1Uc4oWiVbXDFucnkbFwhnQm3IIdAxkbgd2rEDp5oTRhhQ==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -2151,6 +2175,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
@ -2262,6 +2289,10 @@ packages:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
@ -2297,6 +2328,10 @@ packages:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
@ -2313,6 +2348,16 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dagre@0.8.5:
resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==}
@ -5617,6 +5662,10 @@ snapshots:
'@types/d3-color@3.1.3': {}
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-ease@3.0.2': {}
'@types/d3-interpolate@3.0.4':
@ -5631,6 +5680,8 @@ snapshots:
dependencies:
'@types/d3-time': 3.0.4
'@types/d3-selection@3.0.11': {}
'@types/d3-shape@1.3.12':
dependencies:
'@types/d3-path': 1.0.11
@ -5643,6 +5694,15 @@ snapshots:
'@types/d3-timer@3.0.2': {}
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-zoom@3.0.8':
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/dagre@0.7.52': {}
'@types/eslint-scope@3.7.7':
@ -5879,6 +5939,29 @@ snapshots:
'@xtuc/long@4.2.2': {}
'@xyflow/react@12.9.0(@types/react@18.3.18)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@xyflow/system': 0.0.71
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.5(@types/react@18.3.18)(immer@10.1.1)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.71':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
acorn: 8.14.0
@ -6104,6 +6187,8 @@ snapshots:
dependencies:
clsx: 2.1.1
classcat@5.0.5: {}
classnames@2.5.1: {}
clsx@2.1.1: {}
@ -6214,6 +6299,11 @@ snapshots:
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-force-3d@3.0.5:
@ -6250,6 +6340,8 @@ snapshots:
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-selection@3.0.0: {}
d3-shape@3.2.0:
dependencies:
d3-path: 3.1.0
@ -6264,6 +6356,23 @@ snapshots:
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dagre@0.8.5:
dependencies:
graphlib: 2.1.8

View File

@ -1,102 +0,0 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import type { FlowNodeData } from '../../types';
const ServiceTaskNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
return (
<div
style={{
padding: '12px 16px',
borderRadius: '8px',
border: `2px solid ${selected ? '#3b82f6' : '#f59e0b'}`,
background: '#fffbeb',
minWidth: '120px',
minHeight: '60px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
}}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Left}
style={{
background: '#f59e0b',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
{/* 节点内容 */}
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px'
}}>
{/* 图标 */}
<div style={{
fontSize: '16px',
color: '#f59e0b',
}}>
</div>
{/* 标签 */}
<div style={{
fontSize: '12px',
color: '#374151',
fontWeight: '500',
textAlign: 'center',
lineHeight: '1.2'
}}>
{nodeData.label || '服务任务'}
</div>
</div>
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Right}
style={{
background: '#f59e0b',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
{/* 配置指示器 */}
{nodeData.configs && Object.keys(nodeData.configs).length > 0 && (
<div
style={{
position: 'absolute',
top: '-6px',
right: '-6px',
width: '12px',
height: '12px',
borderRadius: '50%',
background: '#10b981',
border: '2px solid white',
fontSize: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white'
}}
>
</div>
)}
</div>
);
};
export default memo(ServiceTaskNode);

View File

@ -1,102 +0,0 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import type { FlowNodeData } from '../../types';
const UserTaskNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
return (
<div
style={{
padding: '12px 16px',
borderRadius: '8px',
border: `2px solid ${selected ? '#3b82f6' : '#6366f1'}`,
background: '#f8faff',
minWidth: '120px',
minHeight: '60px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
}}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Left}
style={{
background: '#6366f1',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
{/* 节点内容 */}
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px'
}}>
{/* 图标 */}
<div style={{
fontSize: '16px',
color: '#6366f1',
}}>
👤
</div>
{/* 标签 */}
<div style={{
fontSize: '12px',
color: '#374151',
fontWeight: '500',
textAlign: 'center',
lineHeight: '1.2'
}}>
{nodeData.label || '用户任务'}
</div>
</div>
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Right}
style={{
background: '#6366f1',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
{/* 配置指示器 */}
{nodeData.configs && Object.keys(nodeData.configs).length > 0 && (
<div
style={{
position: 'absolute',
top: '-6px',
right: '-6px',
width: '12px',
height: '12px',
borderRadius: '50%',
background: '#10b981',
border: '2px solid white',
fontSize: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white'
}}
>
</div>
)}
</div>
);
};
export default memo(UserTaskNode);

View File

@ -1,5 +0,0 @@
// 统一导出所有自定义节点组件
export { default as StartEventNode } from './StartEventNode';
export { default as EndEventNode } from './EndEventNode';
export { default as UserTaskNode } from './UserTaskNode';
export { default as ServiceTaskNode } from './ServiceTaskNode';

View File

@ -0,0 +1,149 @@
import React, { useEffect } from 'react';
import { Modal, Form, Input, InputNumber, Radio, message } from 'antd';
import type { FlowEdge } from '../types';
interface EdgeConfigModalProps {
visible: boolean;
edge: FlowEdge | null;
onOk: (edgeId: string, condition: EdgeCondition) => void;
onCancel: () => void;
}
export interface EdgeCondition {
type: 'EXPRESSION' | 'DEFAULT';
expression?: string;
priority: number;
}
/**
*
* Workflow/Design/ExpressionModal
*/
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
visible,
edge,
onOk,
onCancel
}) => {
const [form] = Form.useForm();
// 当edge变化时更新表单初始值
useEffect(() => {
if (visible && edge) {
const condition = edge.data?.condition;
form.setFieldsValue({
type: condition?.type || 'EXPRESSION',
expression: condition?.expression || '',
priority: condition?.priority || 10
});
}
}, [visible, edge, form]);
const handleOk = async () => {
if (!edge) return;
try {
const values = await form.validateFields();
// 验证表达式
if (values.type === 'EXPRESSION') {
if (!values.expression || values.expression.trim() === '') {
message.error('请输入条件表达式');
return;
}
// 检查是否包含变量引用 ${...}
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
if (!hasVariable) {
message.warning('表达式建议包含变量引用,格式:${变量名}');
}
}
onOk(edge.id, values);
} catch (error) {
console.error('表单验证失败:', error);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="配置边条件"
open={visible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
okText="确定"
cancelText="取消"
>
<Form
form={form}
layout="vertical"
initialValues={{
type: 'EXPRESSION',
expression: '',
priority: 10
}}
>
<Form.Item
name="type"
label="条件类型"
rules={[{ required: true, message: '请选择条件类型' }]}
>
<Radio.Group>
<Radio value="EXPRESSION"></Radio>
<Radio value="DEFAULT"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
>
{({ getFieldValue }) => {
const type = getFieldValue('type');
return type === 'EXPRESSION' ? (
<Form.Item
name="expression"
label="条件表达式"
rules={[{ required: true, message: '请输入条件表达式' }]}
extra="支持使用 ${变量名} 引用流程变量,例如:${amount} > 1000"
>
<Input.TextArea
placeholder="请输入条件表达式,如:${amount} > 1000"
rows={4}
autoComplete="off"
/>
</Form.Item>
) : (
<div className="text-sm text-gray-500 mb-4">
</div>
);
}}
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true, message: '请设置优先级' }]}
extra="数字越小优先级越高1-999"
>
<InputNumber
min={1}
max={999}
className="w-full"
placeholder="请输入优先级"
/>
</Form.Item>
</Form>
</Modal>
);
};
export default EdgeConfigModal;

View File

@ -15,20 +15,7 @@ import {
import '@xyflow/react/dist/style.css';
import type { FlowNode, FlowEdge } from '../types';
import {
StartEventNode,
EndEventNode,
UserTaskNode,
ServiceTaskNode
} from './CustomNodes';
// 注册自定义节点类型
const nodeTypes = {
START_EVENT: StartEventNode,
END_EVENT: EndEventNode,
USER_TASK: UserTaskNode,
SERVICE_TASK: ServiceTaskNode,
};
import { nodeTypes } from '../nodes';
interface FlowCanvasProps {
initialNodes?: FlowNode[];

View File

@ -1,6 +1,6 @@
import React from 'react';
import { NodeCategory } from '../types';
import { NODE_DEFINITIONS } from '../nodes/definitions';
import { NODE_DEFINITIONS } from '../nodes';
import type { WorkflowNodeDefinition } from '../nodes/types';
// 图标映射函数

View File

@ -3,7 +3,7 @@ import { message } from 'antd';
import * as definitionService from '../../Definition/service';
import type { FlowNode, FlowEdge } from '../types';
import type { WorkflowDefinition } from '../../Definition/types';
import { NODE_DEFINITIONS } from '../nodes/definitions';
import { NODE_DEFINITIONS } from '../nodes';
interface LoadedWorkflowData {
nodes: FlowNode[];

View File

@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
import { message } from 'antd';
import * as definitionService from '../../Definition/service';
import type { FlowNode, FlowEdge } from '../types';
import { validateWorkflow } from '../utils/validator';
interface WorkflowSaveData {
nodes: FlowNode[];
@ -19,6 +20,13 @@ export const useWorkflowSave = () => {
// 保存工作流数据
const saveWorkflow = useCallback(async (data: WorkflowSaveData): Promise<boolean> => {
// 保存前验证工作流
const validationResult = validateWorkflow(data.nodes, data.edges);
if (!validationResult.valid) {
message.error(validationResult.message || '工作流验证失败');
return false;
}
setSaving(true);
try {
@ -43,7 +51,8 @@ export const useWorkflowSave = () => {
to: edge.target, // 后端使用to字段
name: edge.data?.label || "", // 边的名称
config: {
type: "sequence" // 固定为sequence类型
type: "sequence", // 固定为sequence类型
condition: edge.data?.condition // 保存边条件
},
vertices: [] // 暂时为空数组
}))

View File

@ -7,6 +7,7 @@ import WorkflowToolbar from './components/WorkflowToolbar';
import NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas';
import NodeConfigModal from './components/NodeConfigModal';
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types';
@ -42,6 +43,10 @@ const WorkflowDesignInner: React.FC = () => {
const [configModalVisible, setConfigModalVisible] = useState(false);
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
// 边配置模态框状态
const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false);
const [configEdge, setConfigEdge] = useState<FlowEdge | null>(null);
// 保存和加载hooks
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
@ -280,11 +285,12 @@ const WorkflowDesignInner: React.FC = () => {
}
}, []);
// 处理边双击 - 暂时只记录日志
// 处理边双击 - 打开条件配置弹窗
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
if (event.detail === 2) {
console.log('双击边:', edge);
message.info(`双击了连接: ${edge.data?.label || '连接'},配置功能待实现`);
console.log('双击边,打开配置:', edge);
setConfigEdge(edge);
setEdgeConfigModalVisible(true);
}
}, []);
@ -306,12 +312,46 @@ const WorkflowDesignInner: React.FC = () => {
markUnsaved(); // 标记有未保存更改
}, [setNodes, markUnsaved]);
// 关闭配置模态框
// 关闭节点配置模态框
const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false);
setConfigNode(null);
}, []);
// 处理边条件更新
const handleEdgeConditionUpdate = useCallback((edgeId: string, condition: EdgeCondition) => {
setEdges((edges) =>
edges.map((edge) => {
if (edge.id === edgeId) {
// 根据条件类型生成显示文本
const label = condition.type === 'EXPRESSION'
? condition.expression
: '默认路径';
return {
...edge,
data: {
...edge.data,
condition,
},
label,
};
}
return edge;
})
);
markUnsaved(); // 标记有未保存更改
message.success('边条件配置已更新');
setEdgeConfigModalVisible(false);
setConfigEdge(null);
}, [setEdges, markUnsaved]);
// 关闭边配置模态框
const handleCloseEdgeConfigModal = useCallback(() => {
setEdgeConfigModalVisible(false);
setConfigEdge(null);
}, []);
return (
<div
className="workflow-design-container"
@ -366,6 +406,14 @@ const WorkflowDesignInner: React.FC = () => {
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>
{/* 边条件配置弹窗 */}
<EdgeConfigModal
visible={edgeConfigModalVisible}
edge={configEdge}
onOk={handleEdgeConditionUpdate}
onCancel={handleCloseEdgeConfigModal}
/>
</div>
);
};

View File

@ -1,15 +1,69 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import type { FlowNodeData } from '../../types';
import { BaseNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/**
*
*/
export const EndEventNodeDefinition: BaseNodeDefinition = {
nodeCode: "END_EVENT",
nodeName: "结束",
nodeType: NodeType.END_EVENT,
category: NodeCategory.EVENT,
description: "工作流的结束节点",
uiConfig: {
size: { width: 80, height: 50 },
style: {
fill: '#ff4d4f',
stroke: '#cf1322',
strokeWidth: 2,
icon: 'stop-circle',
iconColor: '#fff'
}
},
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "结束"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "END_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的结束节点"
}
},
required: ["nodeName", "nodeCode"]
}
};
/**
*
*/
const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
return (
<div
style={{
padding: '8px',
borderRadius: '50%',
border: `3px solid ${selected ? '#3b82f6' : '#ef4444'}`,
border: `2px solid ${selected ? '#3b82f6' : '#ef4444'}`,
background: '#fef2f2',
minWidth: '60px',
minHeight: '60px',
@ -24,12 +78,11 @@ const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
>
{/* 图标 */}
<div style={{
fontSize: '18px',
fontSize: '20px',
color: '#ef4444',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold'
justifyContent: 'center'
}}>
</div>
@ -70,3 +123,4 @@ const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
};
export default memo(EndEventNode);

View File

@ -0,0 +1,177 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/**
*
*/
export const ServiceTaskNodeDefinition: ConfigurableNodeDefinition = {
nodeCode: "SERVICE_TASK",
nodeName: "服务任务",
nodeType: NodeType.SERVICE_TASK,
category: NodeCategory.TASK,
description: "自动执行的服务任务节点",
uiConfig: {
size: { width: 120, height: 80 },
style: {
fill: '#722ed1',
stroke: '#531dab',
strokeWidth: 2,
icon: 'api',
iconColor: '#fff'
}
},
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和服务配置",
properties: {
nodeName: {
type: "string",
title: "节点名称",
default: "服务任务"
},
nodeCode: {
type: "string",
title: "节点编码",
default: "SERVICE_TASK"
},
description: {
type: "string",
title: "节点描述",
default: "自动执行的服务任务节点"
},
serviceUrl: {
type: "string",
title: "服务地址",
description: "调用的服务URL",
format: "uri"
},
method: {
type: "string",
title: "请求方法",
enum: ["GET", "POST", "PUT", "DELETE"],
default: "POST"
},
timeout: {
type: "number",
title: "超时时间(秒)",
default: 30,
minimum: 1,
maximum: 300
},
retryCount: {
type: "number",
title: "重试次数",
default: 0,
minimum: 0,
maximum: 5
}
},
required: ["nodeName", "nodeCode", "serviceUrl"]
},
inputMappingSchema: {
type: "object",
title: "输入映射",
properties: {
requestBody: {
type: "object",
title: "请求体"
},
headers: {
type: "object",
title: "请求头"
}
}
},
outputMappingSchema: {
type: "object",
title: "输出映射",
properties: {
responseData: {
type: "object",
title: "响应数据"
},
statusCode: {
type: "number",
title: "状态码"
}
}
}
};
/**
*
*/
const ServiceTaskNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
return (
<div
style={{
padding: '12px 16px',
borderRadius: '8px',
border: `2px solid ${selected ? '#3b82f6' : '#722ed1'}`,
background: 'white',
minWidth: '120px',
minHeight: '60px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
}}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Left}
style={{
background: '#722ed1',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
{/* 图标和标签 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{ fontSize: '18px' }}></span>
<span style={{
fontSize: '14px',
color: '#722ed1',
fontWeight: '500'
}}>
{nodeData.label || '服务任务'}
</span>
</div>
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Right}
style={{
background: '#722ed1',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
</div>
);
};
export default memo(ServiceTaskNode);

View File

@ -1,9 +1,63 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import type { FlowNodeData } from '../../types';
import { BaseNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/**
*
*/
export const StartEventNodeDefinition: BaseNodeDefinition = {
nodeCode: "START_EVENT",
nodeName: "开始",
nodeType: NodeType.START_EVENT,
category: NodeCategory.EVENT,
description: "工作流的起始节点",
uiConfig: {
size: { width: 80, height: 50 },
style: {
fill: '#52c41a',
stroke: '#389e08',
strokeWidth: 2,
icon: 'play-circle',
iconColor: '#fff'
}
},
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "开始"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "START_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的起始节点"
}
},
required: ["nodeName", "nodeCode"]
}
};
/**
*
*/
const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
return (
<div
style={{
@ -69,3 +123,4 @@ const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
};
export default memo(StartEventNode);

View File

@ -0,0 +1,158 @@
import React, { memo } from 'react';
import { Handle, Position, NodeProps } from '@xyflow/react';
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from './types';
import type { FlowNodeData } from '../types';
/**
*
*/
export const UserTaskNodeDefinition: ConfigurableNodeDefinition = {
nodeCode: "USER_TASK",
nodeName: "用户任务",
nodeType: NodeType.USER_TASK,
category: NodeCategory.TASK,
description: "需要用户手动处理的任务节点",
uiConfig: {
size: { width: 120, height: 80 },
style: {
fill: '#1890ff',
stroke: '#096dd9',
strokeWidth: 2,
icon: 'user',
iconColor: '#fff'
}
},
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和任务配置",
properties: {
nodeName: {
type: "string",
title: "节点名称",
default: "用户任务"
},
nodeCode: {
type: "string",
title: "节点编码",
default: "USER_TASK"
},
description: {
type: "string",
title: "节点描述",
default: "需要用户手动处理的任务节点"
},
assignee: {
type: "string",
title: "处理人",
description: "指定任务处理人"
},
candidateGroups: {
type: "string",
title: "候选组",
description: "候选用户组,多个用逗号分隔"
},
dueDate: {
type: "string",
title: "截止日期",
format: "date-time"
}
},
required: ["nodeName", "nodeCode"]
},
inputMappingSchema: {
type: "object",
title: "输入映射",
properties: {
taskData: {
type: "object",
title: "任务数据"
}
}
},
outputMappingSchema: {
type: "object",
title: "输出映射",
properties: {
result: {
type: "string",
title: "处理结果"
}
}
}
};
/**
*
*/
const UserTaskNode: React.FC<NodeProps> = ({ data, selected }) => {
const nodeData = data as FlowNodeData;
return (
<div
style={{
padding: '12px 16px',
borderRadius: '8px',
border: `2px solid ${selected ? '#3b82f6' : '#1890ff'}`,
background: 'white',
minWidth: '120px',
minHeight: '60px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
}}
>
{/* 输入连接点 */}
<Handle
type="target"
position={Position.Left}
style={{
background: '#1890ff',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
{/* 图标和标签 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{ fontSize: '18px' }}>👤</span>
<span style={{
fontSize: '14px',
color: '#1890ff',
fontWeight: '500'
}}>
{nodeData.label || '用户任务'}
</span>
</div>
{/* 输出连接点 */}
<Handle
type="source"
position={Position.Right}
style={{
background: '#1890ff',
border: '2px solid white',
width: '10px',
height: '10px',
}}
/>
</div>
);
};
export default memo(UserTaskNode);

View File

@ -1,189 +0,0 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const DeployNode: ConfigurableNodeDefinition = {
nodeCode: "DEPLOY_NODE",
nodeName: "构建任务",
nodeType: NodeType.DEPLOY_NODE,
category: NodeCategory.TASK,
description: "执行应用构建和部署任务",
// UI 配置
uiConfig: {
size: {
width: 120,
height: 60
},
style: {
fill: '#1890ff',
stroke: '#0050b3',
strokeWidth: 2,
icon: 'build',
iconColor: '#fff'
}
},
// 基本配置Schema - 设计时用于生成表单保存时转为key/value包含基本信息+节点配置)
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和构建任务的配置参数",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "构建任务"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "DEPLOY_NODE"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "执行应用构建和部署任务"
},
// 节点配置
buildCommand: {
type: "string",
title: "构建命令",
description: "执行构建的命令",
default: "npm run build"
},
timeout: {
type: "number",
title: "超时时间(秒)",
description: "构建超时时间",
default: 300,
minimum: 30,
maximum: 3600
},
retryCount: {
type: "number",
title: "重试次数",
description: "构建失败时的重试次数",
default: 2,
minimum: 0,
maximum: 5
},
environment: {
type: "string",
title: "运行环境",
description: "构建运行的环境",
enum: ["development", "staging", "production"],
default: "production"
},
dockerImage: {
type: "string",
title: "Docker镜像",
description: "构建使用的Docker镜像",
default: "node:18-alpine"
},
workingDirectory: {
type: "string",
title: "工作目录",
description: "构建的工作目录",
default: "/app"
}
},
required: ["nodeName", "nodeCode", "buildCommand", "timeout"]
},
// 输入映射Schema - 定义从上游节点接收的数据
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
sourceCodePath: {
type: "string",
title: "源代码路径",
description: "源代码在存储中的路径",
default: "${upstream.outputPath}"
},
buildArgs: {
type: "array",
title: "构建参数",
description: "额外的构建参数列表",
items: {
type: "string"
},
default: []
},
envVariables: {
type: "object",
title: "环境变量",
description: "构建时的环境变量",
properties: {},
additionalProperties: true
},
dependencies: {
type: "string",
title: "依赖文件",
description: "依赖配置文件路径",
default: "${upstream.dependenciesFile}"
}
},
required: ["sourceCodePath"]
},
// 输出映射Schema - 定义传递给下游节点的数据
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
buildArtifactPath: {
type: "string",
title: "构建产物路径",
description: "构建完成后的产物存储路径",
default: "/artifacts/${buildId}"
},
buildLog: {
type: "string",
title: "构建日志",
description: "构建过程的日志文件路径",
default: "/logs/build-${buildId}.log"
},
buildStatus: {
type: "string",
title: "构建状态",
description: "构建完成状态",
enum: ["SUCCESS", "FAILED", "TIMEOUT"],
default: "SUCCESS"
},
buildTime: {
type: "number",
title: "构建耗时",
description: "构建耗时(秒)",
default: 0
},
dockerImageTag: {
type: "string",
title: "Docker镜像标签",
description: "构建生成的Docker镜像标签",
default: "${imageRegistry}/${projectName}:${buildId}"
},
metadata: {
type: "object",
title: "构建元数据",
description: "构建过程中的元数据信息",
properties: {
buildId: { type: "string" },
buildTime: { type: "string" },
gitCommit: { type: "string" },
gitBranch: { type: "string" }
}
}
},
required: ["buildArtifactPath", "buildStatus"]
}
};

View File

@ -1,56 +0,0 @@
import {BaseNodeDefinition, NodeType, NodeCategory} from '../types';
/**
*
*
*/
export const EndEventNode: BaseNodeDefinition = {
nodeCode: "END_EVENT",
nodeName: "结束",
nodeType: NodeType.END_EVENT,
category: NodeCategory.EVENT,
description: "工作流的结束节点",
// UI 配置
uiConfig: {
size: {
width: 80,
height: 50
},
style: {
fill: '#ff4d4f',
stroke: '#cf1322',
strokeWidth: 2,
icon: 'stop-circle',
iconColor: '#fff'
}
},
// 基本配置Schema - 用于生成"基本配置"TAB包含基本信息
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "结束"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "END_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的结束节点"
}
},
required: ["nodeName", "nodeCode"]
}
};

View File

@ -1,180 +0,0 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const ServiceTaskNode: ConfigurableNodeDefinition = {
nodeCode: "SERVICE_TASK",
nodeName: "服务任务",
nodeType: NodeType.SERVICE_TASK,
category: NodeCategory.TASK,
description: "自动执行的服务任务",
// UI 配置
uiConfig: {
size: {
width: 120,
height: 60
},
style: {
fill: '#fa8c16',
stroke: '#d46b08',
strokeWidth: 2,
icon: 'api',
iconColor: '#fff'
}
},
// 基本配置Schema
configSchema: {
type: "object",
title: "基本配置",
description: "服务任务的基本配置信息",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "服务任务"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "SERVICE_TASK"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "自动执行的服务任务"
},
// 节点配置
serviceUrl: {
type: "string",
title: "服务URL",
description: "调用的服务接口地址",
default: "https://api.example.com/service"
},
httpMethod: {
type: "string",
title: "HTTP方法",
description: "HTTP请求方法",
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
default: "POST"
},
timeout: {
type: "number",
title: "超时时间(秒)",
description: "服务调用超时时间",
default: 30,
minimum: 1,
maximum: 300
},
retryCount: {
type: "number",
title: "重试次数",
description: "失败时的重试次数",
default: 3,
minimum: 0,
maximum: 10
},
headers: {
type: "object",
title: "请求头",
description: "HTTP请求头配置",
default: {
"Content-Type": "application/json"
}
},
async: {
type: "boolean",
title: "异步执行",
description: "是否异步执行服务调用",
default: false
}
},
required: ["nodeName", "nodeCode", "serviceUrl", "httpMethod"]
},
// 输入映射Schema
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
requestBody: {
type: "object",
title: "请求体",
description: "发送给服务的请求数据",
default: {}
},
queryParams: {
type: "object",
title: "查询参数",
description: "URL查询参数",
default: {}
},
pathParams: {
type: "object",
title: "路径参数",
description: "URL路径参数",
default: {}
},
authToken: {
type: "string",
title: "认证令牌",
description: "服务调用的认证令牌",
default: "${upstream.token}"
}
}
},
// 输出映射Schema
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
responseData: {
type: "object",
title: "响应数据",
description: "服务返回的响应数据",
default: {}
},
statusCode: {
type: "number",
title: "状态码",
description: "HTTP响应状态码",
default: 200
},
responseHeaders: {
type: "object",
title: "响应头",
description: "HTTP响应头信息",
default: {}
},
executionTime: {
type: "number",
title: "执行时间",
description: "服务调用耗时(毫秒)",
default: 0
},
success: {
type: "boolean",
title: "执行成功",
description: "服务调用是否成功",
default: true
},
errorMessage: {
type: "string",
title: "错误信息",
description: "失败时的错误信息",
default: ""
}
},
required: ["responseData", "statusCode", "success"]
}
};

View File

@ -1,56 +0,0 @@
import {BaseNodeDefinition, NodeType, NodeCategory} from '../types';
/**
*
*
*/
export const StartEventNode: BaseNodeDefinition = {
nodeCode: "START_EVENT",
nodeName: "开始",
nodeType: NodeType.START_EVENT,
category: NodeCategory.EVENT,
description: "工作流的起始节点",
// UI 配置
uiConfig: {
size: {
width: 80,
height: 50
},
style: {
fill: '#52c41a',
stroke: '#389e08',
strokeWidth: 2,
icon: 'play-circle',
iconColor: '#fff'
}
},
// 基本配置Schema - 用于生成"基本配置"TAB包含基本信息
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "开始"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "START_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的起始节点"
}
},
required: ["nodeName", "nodeCode"]
}
};

View File

@ -1,155 +0,0 @@
import { ConfigurableNodeDefinition, NodeType, NodeCategory } from '../types';
/**
*
*
*/
export const UserTaskNode: ConfigurableNodeDefinition = {
nodeCode: "USER_TASK",
nodeName: "用户任务",
nodeType: NodeType.USER_TASK,
category: NodeCategory.TASK,
description: "需要用户手动执行的任务",
// UI 配置
uiConfig: {
size: {
width: 120,
height: 60
},
style: {
fill: '#722ed1',
stroke: '#531dab',
strokeWidth: 2,
icon: 'user',
iconColor: '#fff'
}
},
// 基本配置Schema
configSchema: {
type: "object",
title: "基本配置",
description: "用户任务的基本配置信息",
properties: {
// 基本信息
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "用户任务"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "USER_TASK"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "需要用户手动执行的任务"
},
// 节点配置
assignee: {
type: "string",
title: "任务分配人",
description: "任务分配给的用户",
default: ""
},
candidateGroups: {
type: "array",
title: "候选组",
description: "可以执行此任务的用户组",
items: {
type: "string"
},
default: []
},
dueDate: {
type: "string",
title: "截止时间",
description: "任务的截止时间",
format: "date-time"
},
priority: {
type: "string",
title: "优先级",
description: "任务优先级",
enum: ["low", "normal", "high", "urgent"],
default: "normal"
},
formKey: {
type: "string",
title: "表单键",
description: "关联的表单标识",
default: ""
}
},
required: ["nodeName", "nodeCode"]
},
// 输入映射Schema
inputMappingSchema: {
type: "object",
title: "输入映射",
description: "从上游节点接收的数据映射配置",
properties: {
taskData: {
type: "object",
title: "任务数据",
description: "传递给用户任务的数据",
default: {}
},
assigneeExpression: {
type: "string",
title: "分配人表达式",
description: "动态计算分配人的表达式",
default: "${upstream.userId}"
},
formVariables: {
type: "object",
title: "表单变量",
description: "表单初始化变量",
default: {}
}
}
},
// 输出映射Schema
outputMappingSchema: {
type: "object",
title: "输出映射",
description: "传递给下游节点的数据映射配置",
properties: {
taskResult: {
type: "object",
title: "任务结果",
description: "用户任务的执行结果",
default: {}
},
completedBy: {
type: "string",
title: "完成人",
description: "实际完成任务的用户",
default: "${task.assignee}"
},
completedAt: {
type: "string",
title: "完成时间",
description: "任务完成的时间",
format: "date-time",
default: "${task.endTime}"
},
decision: {
type: "string",
title: "决策结果",
description: "用户的决策结果",
enum: ["approved", "rejected", "pending"],
default: "approved"
}
},
required: ["taskResult", "completedBy"]
}
};

View File

@ -1,29 +0,0 @@
import {WorkflowNodeDefinition} from '../types';
import {DeployNode} from './DeployNode';
import {StartEventNode} from './StartEventNode';
import {EndEventNode} from './EndEventNode';
import {UserTaskNode} from './UserTaskNode';
import {ServiceTaskNode} from './ServiceTaskNode';
/**
*
*/
export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
StartEventNode,
EndEventNode,
UserTaskNode,
ServiceTaskNode,
DeployNode,
// 在这里添加更多节点定义
];
/**
*
*/
export {
StartEventNode,
EndEventNode,
UserTaskNode,
ServiceTaskNode,
DeployNode
};

View File

@ -0,0 +1,53 @@
/**
*
*
* 1. xxxDefinition -
* 2. default export -
*/
import StartEventNode, { StartEventNodeDefinition } from './StartEventNode';
import EndEventNode, { EndEventNodeDefinition } from './EndEventNode';
import UserTaskNode, { UserTaskNodeDefinition } from './UserTaskNode';
import ServiceTaskNode, { ServiceTaskNodeDefinition } from './ServiceTaskNode';
import type { WorkflowNodeDefinition } from './types';
/**
* NodePanel 使
*/
export const NODE_DEFINITIONS: WorkflowNodeDefinition[] = [
StartEventNodeDefinition,
EndEventNodeDefinition,
UserTaskNodeDefinition,
ServiceTaskNodeDefinition,
];
/**
* React Flow FlowCanvas 使
*/
export const nodeTypes = {
START_EVENT: StartEventNode,
END_EVENT: EndEventNode,
USER_TASK: UserTaskNode,
SERVICE_TASK: ServiceTaskNode,
};
/**
*
*/
export {
// 组件
StartEventNode,
EndEventNode,
UserTaskNode,
ServiceTaskNode,
// 定义
StartEventNodeDefinition,
EndEventNodeDefinition,
UserTaskNodeDefinition,
ServiceTaskNodeDefinition,
};
// 导出类型
export * from './types';

View File

@ -0,0 +1,207 @@
import type { FlowNode, FlowEdge } from '../types';
import { NodeType } from '../types';
/**
*
*/
export interface ValidationResult {
valid: boolean;
message?: string;
}
/**
*
*/
const validateNotEmpty = (nodes: FlowNode[]): ValidationResult => {
if (nodes.length === 0) {
return {
valid: false,
message: '流程图中没有任何节点,请至少添加一个节点'
};
}
return { valid: true };
};
/**
*
*/
const validateRequiredNodes = (nodes: FlowNode[]): ValidationResult => {
// 检查开始节点
const hasStartNode = nodes.some(node => node.data.nodeType === NodeType.START_EVENT);
if (!hasStartNode) {
return {
valid: false,
message: '流程图中必须包含开始节点'
};
}
// 检查结束节点
const hasEndNode = nodes.some(node => node.data.nodeType === NodeType.END_EVENT);
if (!hasEndNode) {
return {
valid: false,
message: '流程图中必须包含结束节点'
};
}
return { valid: true };
};
/**
*
*
*/
const validateNodeConnections = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
if (nodes.length <= 1) {
return { valid: true }; // 单节点或空流程不需要验证连接
}
// 构建节点连接映射
const nodeConnections = new Map<string, { incoming: number; outgoing: number }>();
nodes.forEach(node => {
nodeConnections.set(node.id, { incoming: 0, outgoing: 0 });
});
edges.forEach(edge => {
const source = nodeConnections.get(edge.source);
const target = nodeConnections.get(edge.target);
if (source) source.outgoing++;
if (target) target.incoming++;
});
// 检查每个节点的连接情况
for (const [nodeId, connections] of nodeConnections.entries()) {
const node = nodes.find(n => n.id === nodeId);
if (!node) continue;
const nodeType = node.data.nodeType;
// 开始节点必须有出边
if (nodeType === NodeType.START_EVENT && connections.outgoing === 0) {
return {
valid: false,
message: `开始节点"${node.data.label}"没有连接到任何其他节点`
};
}
// 结束节点必须有入边
if (nodeType === NodeType.END_EVENT && connections.incoming === 0) {
return {
valid: false,
message: `结束节点"${node.data.label}"没有被任何节点连接`
};
}
// 中间节点必须既有入边又有出边
if (nodeType !== NodeType.START_EVENT && nodeType !== NodeType.END_EVENT) {
if (connections.incoming === 0) {
return {
valid: false,
message: `节点"${node.data.label}"没有输入连接,请确保它被其他节点连接`
};
}
if (connections.outgoing === 0) {
return {
valid: false,
message: `节点"${node.data.label}"没有输出连接,请确保它连接到其他节点`
};
}
}
}
return { valid: true };
};
/**
*
*
*/
const validateNodeConfigs = (nodes: FlowNode[]): ValidationResult => {
for (const node of nodes) {
// 检查节点是否有配置数据
if (!node.data.configs) {
return {
valid: false,
message: `节点"${node.data.label}"缺少配置信息,请配置该节点`
};
}
// 检查节点名称
if (!node.data.configs.nodeName || node.data.configs.nodeName.trim() === '') {
return {
valid: false,
message: `节点"${node.data.label}"的名称不能为空`
};
}
// 检查节点编码
if (!node.data.configs.nodeCode || node.data.configs.nodeCode.trim() === '') {
return {
valid: false,
message: `节点"${node.data.label}"的编码不能为空`
};
}
}
return { valid: true };
};
/**
*
*
*/
const validateEdgeConditions = (edges: FlowEdge[]): ValidationResult => {
for (const edge of edges) {
const condition = edge.data?.condition;
if (condition) {
// 如果是表达式类型,检查表达式是否为空
if (condition.type === 'EXPRESSION') {
if (!condition.expression || condition.expression.trim() === '') {
return {
valid: false,
message: `边"${edge.id}"配置了表达式条件,但表达式为空`
};
}
}
// 检查优先级是否有效
if (condition.priority < 1 || condition.priority > 999) {
return {
valid: false,
message: `边"${edge.id}"的优先级必须在1-999之间`
};
}
}
}
return { valid: true };
};
/**
*
*
*/
export const validateWorkflow = (nodes: FlowNode[], edges: FlowEdge[]): ValidationResult => {
// 定义验证规则链
const validators = [
() => validateNotEmpty(nodes),
() => validateRequiredNodes(nodes),
() => validateNodeConnections(nodes, edges),
() => validateNodeConfigs(nodes),
() => validateEdgeConditions(edges),
];
// 依次执行验证
for (const validator of validators) {
const result = validator();
if (!result.valid) {
return result;
}
}
return { valid: true };
};