diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/EdgeConfig/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/components/EdgeConfig/index.tsx new file mode 100644 index 00000000..37e1a8f1 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/components/EdgeConfig/index.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Form, Input, InputNumber } from 'antd'; +import { Edge } from '@antv/x6'; + +interface EdgeConfigProps { + edge: Edge; + form: any; + onValuesChange?: (changedValues: any, allValues: any) => void; +} + +interface EdgeData { + condition?: string; + description?: string; + priority?: number; +} + +const EdgeConfig: React.FC = ({ edge, form, onValuesChange }) => { + return ( +
+ + + + + + + + + + + +
+ ); +}; + +export default EdgeConfig; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index bde5c2d4..263f9c5a 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -4,7 +4,7 @@ import {Button, Card, Layout, message, Space, Spin, Drawer, Form, Dropdown} from import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons'; import {getDefinition, updateDefinition} from '../../service'; import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types'; -import {Graph, Node, Cell} from '@antv/x6'; +import {Graph, Node, Cell, Edge, Shape} from '@antv/x6'; import '@antv/x6-react-shape'; import {Selection} from '@antv/x6-plugin-selection'; import {History} from '@antv/x6-plugin-history'; @@ -13,14 +13,13 @@ import {Transform} from '@antv/x6-plugin-transform'; import {Keyboard} from '@antv/x6-plugin-keyboard'; import {Snapline} from '@antv/x6-plugin-snapline'; import {MiniMap} from '@antv/x6-plugin-minimap'; -import {Menu} from '@antv/x6-plugin-menu'; import './index.module.less'; import NodePanel from './components/NodePanel'; import NodeConfig from './components/NodeConfig'; import Toolbar from './components/Toolbar'; import {NodeType, getNodeTypes} from './service'; import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons'; -import { validateFlow, hasCycle } from './validate'; +import EdgeConfig from './components/EdgeConfig'; const {Sider, Content} = Layout; @@ -55,6 +54,9 @@ const FlowDesigner: React.FC = () => { const [currentNodeType, setCurrentNodeType] = useState(); const [nodeTypes, setNodeTypes] = useState([]); const [form] = Form.useForm(); + const [currentEdge, setCurrentEdge] = useState(); + const [edgeConfigVisible, setEdgeConfigVisible] = useState(false); + const [edgeForm] = Form.useForm(); // 右键菜单状态 const [contextMenu, setContextMenu] = useState<{ @@ -100,7 +102,7 @@ const FlowDesigner: React.FC = () => { }, { key: 'config', - label: '���置节点', + label: '配置节点', icon: , onClick: () => { if (contextMenu.cell && contextMenu.cell.isNode()) { @@ -123,6 +125,17 @@ const FlowDesigner: React.FC = () => { }, ], edge: [ + { + key: 'config', + label: '配置连线', + icon: , + onClick: () => { + if (contextMenu.cell && contextMenu.cell.isEdge()) { + handleEdgeConfig(contextMenu.cell); + } + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, { key: 'delete', label: '删除连线', @@ -179,17 +192,15 @@ const FlowDesigner: React.FC = () => { const initGraph = () => { if (!containerRef.current) return; - // 创建画布 const graph = new Graph({ container: containerRef.current, - autoResize: true, // 启用自动调整大小 grid: { visible: true, type: 'mesh', size: 10, args: { - color: '#e5e5e5', // 网格线颜色 - thickness: 1, // 网格线宽度 + color: '#e5e5e5', + thickness: 1, }, }, mousewheel: { @@ -199,11 +210,11 @@ const FlowDesigner: React.FC = () => { maxScale: 2, }, connecting: { - snap: true, // 连线时自动吸附 - allowBlank: false, // 禁止连接到空白位置 - allowLoop: false, // 禁止自环 - allowNode: false, // 禁止直接连接到节点(必须连接到连接桩) - allowEdge: false, // 禁止边连接到边 + snap: true, + allowBlank: false, + allowLoop: false, + allowNode: false, + allowEdge: false, connector: { name: 'rounded', args: { @@ -211,21 +222,74 @@ const FlowDesigner: React.FC = () => { }, }, router: { - name: 'manhattan', // 使用曼哈顿路由 + name: 'manhattan', args: { padding: 1, }, }, validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) { if (sourceCell === targetCell) { - return false; // 禁止自环 + return false; } if (!sourceMagnet || !targetMagnet) { - return false; // 必须使用连接桩 + return false; } return true; }, }, + defaultEdge: { + attrs: { + line: { + stroke: '#5F95FF', + strokeWidth: 1, + targetMarker: { + name: 'classic', + size: 8, + }, + }, + }, + router: { + name: 'manhattan', + args: { + padding: 1, + }, + }, + connector: { + name: 'rounded', + args: { + radius: 8, + }, + }, + labels: [ + { + attrs: { + label: { + text: '', + fill: '#333', + fontSize: 12, + }, + rect: { + fill: '#fff', + stroke: '#5F95FF', + strokeWidth: 1, + rx: 3, + ry: 3, + refWidth: 1, + refHeight: 1, + refX: 0, + refY: 0, + }, + }, + position: { + distance: 0.5, + offset: { + x: 0, + y: -10, + }, + }, + }, + ], + }, highlighting: { magnetAvailable: { name: 'stroke', @@ -248,7 +312,9 @@ const FlowDesigner: React.FC = () => { }, }, }, - keyboard: true, + keyboard: { + enabled: true, + }, clipboard: { enabled: true, }, @@ -309,7 +375,7 @@ const FlowDesigner: React.FC = () => { minWidth: 1, minHeight: 1, orthogonal: true, - restrict: true, + restricted: true, }, rotating: { enabled: true, @@ -407,7 +473,7 @@ const FlowDesigner: React.FC = () => { if (nodeType) { setCurrentNodeType(nodeType); - // 合并节点基本配置和执行器配置 + // 合并节点基本配置和执器配置 const formValues = { name: data.name || nodeType.name, description: data.description, @@ -558,7 +624,7 @@ const FlowDesigner: React.FC = () => { throw new Error('重试次数不能为负数'); } if (config.retryInterval !== undefined && config.retryInterval < 0) { - throw new Error('重试间隔不能为负数'); + throw new Error('重试间不能为负数'); } return true; } @@ -596,7 +662,7 @@ const FlowDesigner: React.FC = () => { } }; - // 获取��情 + // 获取详情 const fetchDetail = async () => { if (!id) return; setLoading(true); @@ -614,34 +680,10 @@ const FlowDesigner: React.FC = () => { const handleSave = async () => { if (!id || !detail || !graphRef.current || detail.status !== WorkflowStatus.DRAFT) return; - // 先进行流程验证 - const result = validateFlow(graphRef.current); - const hasCycleResult = hasCycle(graphRef.current); - - if (hasCycleResult) { - result.errors.push('流程图中存在循环依赖'); - result.valid = false; - } - - if (!result.valid) { - message.error( -
-
流程验证失败,无法保存:
-
    - {result.errors.map((error, index) => ( -
  • {error}
  • - ))} -
-
- ); - return; - } - try { - // 获取图形数据 const graphData = graphRef.current.toJSON(); - // 收���节点配置数据 + // 收集节点配置数据 const nodes = graphRef.current.getNodes().map(node => { const data = node.getData() as NodeData; return { @@ -653,14 +695,26 @@ const FlowDesigner: React.FC = () => { }; }); + // 收集连线配置数据 + const transitions = graphRef.current.getEdges().map(edge => { + const data = edge.getData() || {}; + return { + from: edge.getSourceCellId(), + to: edge.getTargetCellId(), + condition: data.condition || '', + description: data.description || '', + priority: data.priority || 0 + }; + }); + // 构建更新数据 const data = { ...detail, graphDefinition: JSON.stringify(graphData), - nodeConfig: JSON.stringify({nodes}) + nodeConfig: JSON.stringify({nodes}), + transitionConfig: JSON.stringify({transitions}) }; - // 调用更新接口 await updateDefinition(parseInt(id), data); message.success('保存成功'); } catch (error) { @@ -724,7 +778,7 @@ const FlowDesigner: React.FC = () => { // 加载流程图数据 const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => { try { - // 加载图形数据 + // 加载图数据 const graphData = JSON.parse(detail.graphDefinition); graph.fromJSON(graphData); @@ -754,6 +808,33 @@ const FlowDesigner: React.FC = () => { } }; + // 添加连线配置处理函数 + const handleEdgeConfig = (edge: Edge) => { + setCurrentEdge(edge); + const data = edge.getData() || {}; + edgeForm.setFieldsValue(data); + setEdgeConfigVisible(true); + }; + + // 添加连线配置保存函数 + const handleEdgeConfigSubmit = async () => { + if (!currentEdge) return; + + try { + const values = await edgeForm.validateFields(); + currentEdge.setData(values); + + // 更新连线标签 + if (values.description) { + currentEdge.setLabelAt(0, values.description); + } + + setEdgeConfigVisible(false); + } catch (error) { + // 表单验证失败 + } + }; + if (loading) { return (
@@ -825,6 +906,34 @@ const FlowDesigner: React.FC = () => { )} + + setEdgeConfigVisible(false)} + open={edgeConfigVisible} + extra={ + + + + + } + > + {currentEdge && ( + { + if (changedValues.description) { + currentEdge.setLabelAt(0, changedValues.description); + } + }} + /> + )} + ); }; diff --git a/frontend/src/pages/Workflow/Definition/Designer/validate.ts b/frontend/src/pages/Workflow/Definition/Designer/validate.ts new file mode 100644 index 00000000..e91a7867 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/validate.ts @@ -0,0 +1,117 @@ +import { Graph } from '@antv/x6'; + +interface ValidationResult { + valid: boolean; + errors: string[]; +} + +export const validateFlow = (graph: Graph): ValidationResult => { + const result: ValidationResult = { + valid: true, + errors: [], + }; + + const nodes = graph.getNodes(); + const edges = graph.getEdges(); + + // 验证开始节点 + const startNodes = nodes.filter(node => node.getData()?.type === 'START'); + if (startNodes.length === 0) { + result.errors.push('流程必须包含一个开始节点'); + result.valid = false; + } else if (startNodes.length > 1) { + result.errors.push('流程只能包含一个开始节点'); + result.valid = false; + } + + // 验证结束节点 + const endNodes = nodes.filter(node => node.getData()?.type === 'END'); + if (endNodes.length === 0) { + result.errors.push('流程必须包含至少一个结束节点'); + result.valid = false; + } + + // 验证孤立节点 + nodes.forEach(node => { + const incomingEdges = graph.getIncomingEdges(node) || []; + const outgoingEdges = graph.getOutgoingEdges(node) || []; + const nodeData = node.getData(); + const nodeType = nodeData?.type; + const nodeName = nodeData?.name || '未命名'; + + if (nodeType === 'START' && incomingEdges.length > 0) { + result.errors.push('开始节点不能有入边'); + result.valid = false; + } + + if (nodeType === 'END' && outgoingEdges.length > 0) { + result.errors.push('结束节点不能有出边'); + result.valid = false; + } + + if (nodeType !== 'START' && nodeType !== 'END' && + (incomingEdges.length === 0 || outgoingEdges.length === 0)) { + result.errors.push(`节点 "${nodeName}" 未完全连接`); + result.valid = false; + } + }); + + // 验证网关配对 + const validateGatewayPairs = () => { + const gatewayNodes = nodes.filter(node => node.getData()?.type === 'GATEWAY'); + gatewayNodes.forEach(gateway => { + const outgoingEdges = graph.getOutgoingEdges(gateway) || []; + const incomingEdges = graph.getIncomingEdges(gateway) || []; + const gatewayData = gateway.getData(); + const gatewayName = gatewayData?.name || '未命名'; + + if (gatewayData?.config?.type === 'PARALLEL' || + gatewayData?.config?.type === 'INCLUSIVE') { + if (outgoingEdges.length < 2) { + result.errors.push(`并行/包容网关 "${gatewayName}" 必须有至少两个出口`); + result.valid = false; + } + } + }); + }; + validateGatewayPairs(); + + return result; +}; + +// 检查是否存在环路 +export const hasCycle = (graph: Graph): boolean => { + const visited = new Set(); + const recursionStack = new Set(); + + const dfs = (nodeId: string): boolean => { + visited.add(nodeId); + recursionStack.add(nodeId); + + const outgoingEdges = graph.getOutgoingEdges(nodeId); + if (outgoingEdges) { + for (const edge of outgoingEdges) { + const targetId = edge.getTargetCellId(); + if (!visited.has(targetId)) { + if (dfs(targetId)) { + return true; + } + } else if (recursionStack.has(targetId)) { + return true; + } + } + } + + recursionStack.delete(nodeId); + return false; + }; + + const nodes = graph.getNodes(); + for (const node of nodes) { + if (!visited.has(node.id) && dfs(node.id)) { + return true; + } + } + + return false; +}; \ No newline at end of file