From c08c99f4228b062ee0c156986de80db199cdd308 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Tue, 21 Oct 2025 10:17:32 +0800 Subject: [PATCH] 1 --- frontend/pnpm-lock.yaml | 109 +++++++++ .../CustomNodes/ServiceTaskNode.tsx | 102 --------- .../components/CustomNodes/UserTaskNode.tsx | 102 --------- .../Design/components/CustomNodes/index.ts | 5 - .../Design/components/EdgeConfigModal.tsx | 149 +++++++++++++ .../Design/components/FlowCanvas.tsx | 15 +- .../Workflow2/Design/components/NodePanel.tsx | 2 +- .../Workflow2/Design/hooks/useWorkflowLoad.ts | 2 +- .../Workflow2/Design/hooks/useWorkflowSave.ts | 11 +- frontend/src/pages/Workflow2/Design/index.tsx | 56 ++++- .../CustomNodes => nodes}/EndEventNode.tsx | 64 +++++- .../Design/nodes/ServiceTaskNode.tsx | 177 +++++++++++++++ .../CustomNodes => nodes}/StartEventNode.tsx | 57 ++++- .../Workflow2/Design/nodes/UserTaskNode.tsx | 158 +++++++++++++ .../Design/nodes/definitions/DeployNode.ts | 189 ---------------- .../Design/nodes/definitions/EndEventNode.ts | 56 ----- .../nodes/definitions/ServiceTaskNode.ts | 180 --------------- .../nodes/definitions/StartEventNode.ts | 56 ----- .../Design/nodes/definitions/UserTaskNode.ts | 155 ------------- .../Design/nodes/definitions/index.ts | 29 --- .../src/pages/Workflow2/Design/nodes/index.ts | 53 +++++ .../pages/Workflow2/Design/utils/validator.ts | 207 ++++++++++++++++++ 22 files changed, 1033 insertions(+), 901 deletions(-) delete mode 100644 frontend/src/pages/Workflow2/Design/components/CustomNodes/ServiceTaskNode.tsx delete mode 100644 frontend/src/pages/Workflow2/Design/components/CustomNodes/UserTaskNode.tsx delete mode 100644 frontend/src/pages/Workflow2/Design/components/CustomNodes/index.ts create mode 100644 frontend/src/pages/Workflow2/Design/components/EdgeConfigModal.tsx rename frontend/src/pages/Workflow2/Design/{components/CustomNodes => nodes}/EndEventNode.tsx (56%) create mode 100644 frontend/src/pages/Workflow2/Design/nodes/ServiceTaskNode.tsx rename frontend/src/pages/Workflow2/Design/{components/CustomNodes => nodes}/StartEventNode.tsx (59%) create mode 100644 frontend/src/pages/Workflow2/Design/nodes/UserTaskNode.tsx delete mode 100644 frontend/src/pages/Workflow2/Design/nodes/definitions/DeployNode.ts delete mode 100644 frontend/src/pages/Workflow2/Design/nodes/definitions/EndEventNode.ts delete mode 100644 frontend/src/pages/Workflow2/Design/nodes/definitions/ServiceTaskNode.ts delete mode 100644 frontend/src/pages/Workflow2/Design/nodes/definitions/StartEventNode.ts delete mode 100644 frontend/src/pages/Workflow2/Design/nodes/definitions/UserTaskNode.ts delete mode 100644 frontend/src/pages/Workflow2/Design/nodes/definitions/index.ts create mode 100644 frontend/src/pages/Workflow2/Design/nodes/index.ts create mode 100644 frontend/src/pages/Workflow2/Design/utils/validator.ts diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a187c9c9..0299364d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -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 diff --git a/frontend/src/pages/Workflow2/Design/components/CustomNodes/ServiceTaskNode.tsx b/frontend/src/pages/Workflow2/Design/components/CustomNodes/ServiceTaskNode.tsx deleted file mode 100644 index 8d524ad3..00000000 --- a/frontend/src/pages/Workflow2/Design/components/CustomNodes/ServiceTaskNode.tsx +++ /dev/null @@ -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 = ({ data, selected }) => { - const nodeData = data as FlowNodeData; - return ( -
- {/* 输入连接点 */} - - - {/* 节点内容 */} -
- {/* 图标 */} -
- ⚙️ -
- - {/* 标签 */} -
- {nodeData.label || '服务任务'} -
-
- - {/* 输出连接点 */} - - - {/* 配置指示器 */} - {nodeData.configs && Object.keys(nodeData.configs).length > 0 && ( -
- ✓ -
- )} -
- ); -}; - -export default memo(ServiceTaskNode); diff --git a/frontend/src/pages/Workflow2/Design/components/CustomNodes/UserTaskNode.tsx b/frontend/src/pages/Workflow2/Design/components/CustomNodes/UserTaskNode.tsx deleted file mode 100644 index 7ede6ded..00000000 --- a/frontend/src/pages/Workflow2/Design/components/CustomNodes/UserTaskNode.tsx +++ /dev/null @@ -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 = ({ data, selected }) => { - const nodeData = data as FlowNodeData; - return ( -
- {/* 输入连接点 */} - - - {/* 节点内容 */} -
- {/* 图标 */} -
- 👤 -
- - {/* 标签 */} -
- {nodeData.label || '用户任务'} -
-
- - {/* 输出连接点 */} - - - {/* 配置指示器 */} - {nodeData.configs && Object.keys(nodeData.configs).length > 0 && ( -
- ✓ -
- )} -
- ); -}; - -export default memo(UserTaskNode); diff --git a/frontend/src/pages/Workflow2/Design/components/CustomNodes/index.ts b/frontend/src/pages/Workflow2/Design/components/CustomNodes/index.ts deleted file mode 100644 index aaeef7bf..00000000 --- a/frontend/src/pages/Workflow2/Design/components/CustomNodes/index.ts +++ /dev/null @@ -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'; diff --git a/frontend/src/pages/Workflow2/Design/components/EdgeConfigModal.tsx b/frontend/src/pages/Workflow2/Design/components/EdgeConfigModal.tsx new file mode 100644 index 00000000..ecd3c982 --- /dev/null +++ b/frontend/src/pages/Workflow2/Design/components/EdgeConfigModal.tsx @@ -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 = ({ + 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 ( + +
+ + + 表达式 + 默认路径 + + + + prevValues.type !== currentValues.type} + > + {({ getFieldValue }) => { + const type = getFieldValue('type'); + return type === 'EXPRESSION' ? ( + + + + ) : ( +
+ 默认路径:当没有其他条件分支满足时,将执行此路径 +
+ ); + }} +
+ + + + +
+
+ ); +}; + +export default EdgeConfigModal; + diff --git a/frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx b/frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx index 83458257..9bac3560 100644 --- a/frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx +++ b/frontend/src/pages/Workflow2/Design/components/FlowCanvas.tsx @@ -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[]; diff --git a/frontend/src/pages/Workflow2/Design/components/NodePanel.tsx b/frontend/src/pages/Workflow2/Design/components/NodePanel.tsx index c3ba6a2e..24126562 100644 --- a/frontend/src/pages/Workflow2/Design/components/NodePanel.tsx +++ b/frontend/src/pages/Workflow2/Design/components/NodePanel.tsx @@ -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'; // 图标映射函数 diff --git a/frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts b/frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts index 752c4e92..7faa3531 100644 --- a/frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts +++ b/frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts @@ -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[]; diff --git a/frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts b/frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts index 04572f9e..74dafc19 100644 --- a/frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts +++ b/frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts @@ -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 => { + // 保存前验证工作流 + 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: [] // 暂时为空数组 })) diff --git a/frontend/src/pages/Workflow2/Design/index.tsx b/frontend/src/pages/Workflow2/Design/index.tsx index 07789dc7..bab09389 100644 --- a/frontend/src/pages/Workflow2/Design/index.tsx +++ b/frontend/src/pages/Workflow2/Design/index.tsx @@ -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(null); + // 边配置模态框状态 + const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false); + const [configEdge, setConfigEdge] = useState(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 (
{ onCancel={handleCloseConfigModal} onOk={handleNodeConfigUpdate} /> + + {/* 边条件配置弹窗 */} +
); }; diff --git a/frontend/src/pages/Workflow2/Design/components/CustomNodes/EndEventNode.tsx b/frontend/src/pages/Workflow2/Design/nodes/EndEventNode.tsx similarity index 56% rename from frontend/src/pages/Workflow2/Design/components/CustomNodes/EndEventNode.tsx rename to frontend/src/pages/Workflow2/Design/nodes/EndEventNode.tsx index a8810ef1..f81196f2 100644 --- a/frontend/src/pages/Workflow2/Design/components/CustomNodes/EndEventNode.tsx +++ b/frontend/src/pages/Workflow2/Design/nodes/EndEventNode.tsx @@ -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 = ({ data, selected }) => { const nodeData = data as FlowNodeData; + return (
= ({ data, selected }) => { > {/* 图标 */}
⏹️
@@ -70,3 +123,4 @@ const EndEventNode: React.FC = ({ data, selected }) => { }; export default memo(EndEventNode); + diff --git a/frontend/src/pages/Workflow2/Design/nodes/ServiceTaskNode.tsx b/frontend/src/pages/Workflow2/Design/nodes/ServiceTaskNode.tsx new file mode 100644 index 00000000..9f1dc1c0 --- /dev/null +++ b/frontend/src/pages/Workflow2/Design/nodes/ServiceTaskNode.tsx @@ -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 = ({ data, selected }) => { + const nodeData = data as FlowNodeData; + + return ( +
+ {/* 输入连接点 */} + + + {/* 图标和标签 */} +
+ ⚙️ + + {nodeData.label || '服务任务'} + +
+ + {/* 输出连接点 */} + +
+ ); +}; + +export default memo(ServiceTaskNode); + diff --git a/frontend/src/pages/Workflow2/Design/components/CustomNodes/StartEventNode.tsx b/frontend/src/pages/Workflow2/Design/nodes/StartEventNode.tsx similarity index 59% rename from frontend/src/pages/Workflow2/Design/components/CustomNodes/StartEventNode.tsx rename to frontend/src/pages/Workflow2/Design/nodes/StartEventNode.tsx index 98b93c60..d2204012 100644 --- a/frontend/src/pages/Workflow2/Design/components/CustomNodes/StartEventNode.tsx +++ b/frontend/src/pages/Workflow2/Design/nodes/StartEventNode.tsx @@ -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 = ({ data, selected }) => { const nodeData = data as FlowNodeData; + return (
= ({ data, selected }) => { }; export default memo(StartEventNode); + diff --git a/frontend/src/pages/Workflow2/Design/nodes/UserTaskNode.tsx b/frontend/src/pages/Workflow2/Design/nodes/UserTaskNode.tsx new file mode 100644 index 00000000..8bdf9112 --- /dev/null +++ b/frontend/src/pages/Workflow2/Design/nodes/UserTaskNode.tsx @@ -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 = ({ data, selected }) => { + const nodeData = data as FlowNodeData; + + return ( +
+ {/* 输入连接点 */} + + + {/* 图标和标签 */} +
+ 👤 + + {nodeData.label || '用户任务'} + +
+ + {/* 输出连接点 */} + +
+ ); +}; + +export default memo(UserTaskNode); + diff --git a/frontend/src/pages/Workflow2/Design/nodes/definitions/DeployNode.ts b/frontend/src/pages/Workflow2/Design/nodes/definitions/DeployNode.ts deleted file mode 100644 index 514175ca..00000000 --- a/frontend/src/pages/Workflow2/Design/nodes/definitions/DeployNode.ts +++ /dev/null @@ -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"] - } -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/definitions/EndEventNode.ts b/frontend/src/pages/Workflow2/Design/nodes/definitions/EndEventNode.ts deleted file mode 100644 index 03e6aff0..00000000 --- a/frontend/src/pages/Workflow2/Design/nodes/definitions/EndEventNode.ts +++ /dev/null @@ -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"] - } -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/definitions/ServiceTaskNode.ts b/frontend/src/pages/Workflow2/Design/nodes/definitions/ServiceTaskNode.ts deleted file mode 100644 index 5f7ea096..00000000 --- a/frontend/src/pages/Workflow2/Design/nodes/definitions/ServiceTaskNode.ts +++ /dev/null @@ -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"] - } -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/definitions/StartEventNode.ts b/frontend/src/pages/Workflow2/Design/nodes/definitions/StartEventNode.ts deleted file mode 100644 index 8663d07e..00000000 --- a/frontend/src/pages/Workflow2/Design/nodes/definitions/StartEventNode.ts +++ /dev/null @@ -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"] - } -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/definitions/UserTaskNode.ts b/frontend/src/pages/Workflow2/Design/nodes/definitions/UserTaskNode.ts deleted file mode 100644 index dbb529fb..00000000 --- a/frontend/src/pages/Workflow2/Design/nodes/definitions/UserTaskNode.ts +++ /dev/null @@ -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"] - } -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/definitions/index.ts b/frontend/src/pages/Workflow2/Design/nodes/definitions/index.ts deleted file mode 100644 index dee7f987..00000000 --- a/frontend/src/pages/Workflow2/Design/nodes/definitions/index.ts +++ /dev/null @@ -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 -}; diff --git a/frontend/src/pages/Workflow2/Design/nodes/index.ts b/frontend/src/pages/Workflow2/Design/nodes/index.ts new file mode 100644 index 00000000..2653f481 --- /dev/null +++ b/frontend/src/pages/Workflow2/Design/nodes/index.ts @@ -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'; + diff --git a/frontend/src/pages/Workflow2/Design/utils/validator.ts b/frontend/src/pages/Workflow2/Design/utils/validator.ts new file mode 100644 index 00000000..c69ad562 --- /dev/null +++ b/frontend/src/pages/Workflow2/Design/utils/validator.ts @@ -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(); + + 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 }; +}; +