1
This commit is contained in:
parent
3fadca9ffa
commit
c08c99f422
@ -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
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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';
|
||||
@ -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;
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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';
|
||||
|
||||
// 图标映射函数
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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: [] // 暂时为空数组
|
||||
}))
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
177
frontend/src/pages/Workflow2/Design/nodes/ServiceTaskNode.tsx
Normal file
177
frontend/src/pages/Workflow2/Design/nodes/ServiceTaskNode.tsx
Normal 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);
|
||||
|
||||
@ -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);
|
||||
|
||||
158
frontend/src/pages/Workflow2/Design/nodes/UserTaskNode.tsx
Normal file
158
frontend/src/pages/Workflow2/Design/nodes/UserTaskNode.tsx
Normal 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);
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
53
frontend/src/pages/Workflow2/Design/nodes/index.ts
Normal file
53
frontend/src/pages/Workflow2/Design/nodes/index.ts
Normal 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';
|
||||
|
||||
207
frontend/src/pages/Workflow2/Design/utils/validator.ts
Normal file
207
frontend/src/pages/Workflow2/Design/utils/validator.ts
Normal 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 };
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user