import React, {useEffect, useState, useRef} from 'react'; import {createRoot} from 'react-dom/client'; import {useParams, useNavigate} from 'react-router-dom'; import {Button, Space, message, Modal, Tooltip, Dropdown} from 'antd'; import { ArrowLeftOutlined, SaveOutlined, CopyOutlined, DeleteOutlined, UndoOutlined, RedoOutlined, ScissorOutlined, SnippetsOutlined, SelectOutlined, ZoomInOutlined, ZoomOutOutlined, } from '@ant-design/icons'; import {Graph, Cell, Edge} from '@antv/x6'; import '@antv/x6-plugin-snapline'; import '@antv/x6-plugin-selection'; import '@antv/x6-plugin-keyboard'; import '@antv/x6-plugin-history'; import '@antv/x6-plugin-clipboard'; import '@antv/x6-plugin-transform'; import {Selection} from '@antv/x6-plugin-selection'; import {MiniMap} from '@antv/x6-plugin-minimap'; import {Clipboard} from '@antv/x6-plugin-clipboard'; import {History} from '@antv/x6-plugin-history'; import {Transform} from '@antv/x6-plugin-transform'; import {getDefinitionDetail, saveDefinition} from '../service'; import {getNodeDefinitionList} from './service'; import NodePanel from './components/NodePanel'; import NodeConfigDrawer from './components/NodeConfigModal'; import {validateWorkflow} from './utils/validator'; import {addNodeToGraph} from './utils/nodeUtils'; import './index.less'; import type {NodeDefinitionResponse} from "@/workflow/nodes/nodeService"; import ExpressionModal from './components/ExpressionModal'; import {EdgeCondition} from './types'; const WorkflowDesign: React.FC = () => { const {id} = useParams<{ id: string }>(); const navigate = useNavigate(); const [title, setTitle] = useState('工作流设计'); const graphContainerRef = useRef(null); const minimapContainerRef = useRef(null); const [graph, setGraph] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const [selectedNodeDefinition, setSelectedNodeDefinition] = useState(null); const [configModalVisible, setConfigModalVisible] = useState(false); const [definitionData, setDefinitionData] = useState(null); const [nodeDefinitions, setNodeDefinitions] = useState([]); const [isNodeDefinitionsLoaded, setIsNodeDefinitionsLoaded] = useState(false); const [scale, setScale] = useState(1); const [expressionModalVisible, setExpressionModalVisible] = useState(false); const [selectedEdge, setSelectedEdge] = useState(null); // 初始化图形 const initGraph = () => { if (!graphContainerRef.current) return; // 获取主画布容器尺寸 const container = graphContainerRef.current; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // 计算主画布和小地图的尺寸比例 const MINIMAP_BASE_WIDTH = 200; const minimapWidth = MINIMAP_BASE_WIDTH; const minimapHeight = Math.round((MINIMAP_BASE_WIDTH * containerHeight) / containerWidth); // 计算缩放比例 const scale = minimapWidth / containerWidth; // 将尺寸信息保存到样式变量中 document.documentElement.style.setProperty('--minimap-width', `${minimapWidth}px`); document.documentElement.style.setProperty('--minimap-height', `${minimapHeight}px`); const graph = new Graph({ container: graphContainerRef.current, grid: { size: 10, visible: true, type: 'dot', args: { color: '#a0a0a0', thickness: 1, }, }, connecting: { snap: true, // 连线时自动吸附 allowBlank: false, // 禁止连线到空白位置 allowLoop: false, // 禁止自环 allowNode: false, // 禁止连接到节点(只允许连接到连接桩) allowEdge: false, // 禁止边连接到边 connector: { name: 'rounded', args: { radius: 8 } }, router: { name: 'manhattan', args: { padding: 1 } }, validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) { if (sourceCell === targetCell) { return false; // 禁止自连接 } if (!sourceMagnet || !targetMagnet) { return false; // 需要有效的连接点 } // 获取源节点和目标节点的类型 const sourceNodeType = sourceCell.getProp('nodeType'); const targetNodeType = targetCell.getProp('nodeType'); // 如果源节点或目标节点是网关类型,允许多条连线 if (sourceNodeType === 'GATEWAY_NODE' || targetNodeType === 'GATEWAY_NODE') { return true; } // 对于其他类型的节点,检查是否已存在连接 const edges = graph.getEdges(); const exists = edges.some(edge => { const source = edge.getSource(); const target = edge.getTarget(); return ( source.cell === sourceCell.id && target.cell === targetCell.id && source.port === sourceMagnet.getAttribute('port') && target.port === targetMagnet.getAttribute('port') ); }); return !exists; }, validateMagnet({magnet}) { return magnet.getAttribute('port-group') !== 'top'; }, validateEdge({edge}) { return true; } }, highlighting: { magnetAvailable: { name: 'stroke', args: { padding: 4, attrs: { strokeWidth: 4, stroke: '#52c41a', }, }, }, }, clipboard: { enabled: true, }, selecting: { enabled: true, multiple: true, rubberband: true, movable: true, showNodeSelectionBox: false, // 禁用节点选择框 showEdgeSelectionBox: false, // 禁用边选择框 selectNodeOnMoved: false, selectEdgeOnMoved: false, }, snapline: true, keyboard: { enabled: true, global: false, }, panning: { enabled: true, eventTypes: ['rightMouseDown'], // 右键按下时启用画布拖拽 }, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], minScale: 0.2, maxScale: 2, }, edgeMovable: true, // 允许边移动 edgeLabelMovable: true, // 允许边标签移动 vertexAddable: true, // 允许添加顶点 vertexMovable: true, // 允许顶点移动 vertexDeletable: true, // 允许删除顶点 }); // 注册插件 const history = new History({ enabled: true, beforeAddCommand(event: any, args: any) { return true; }, // History 插件事件处理已移除,因为不再需要强制更新 afterExecuteCommand: () => {}, afterUndo: () => {}, afterRedo: () => {}, }); // 初始化Selection插件 const selection = new Selection({ enabled: true, multiple: true, rubberband: true, movable: true, showNodeSelectionBox: false, // 禁用节点选择框 showEdgeSelectionBox: false, // 禁用边选择框 selectNodeOnMoved: false, selectEdgeOnMoved: false, multipleSelectionModifiers: ['ctrl', 'meta'], showAnchorSelectionBox: false, pointerEvents: 'auto' }); graph.use(selection); graph.use(new MiniMap({ container: minimapContainerRef.current!, width: minimapWidth, height: minimapHeight, padding: 5, scalable: false, minScale: scale * 0.8, // 稍微缩小一点以显示更多内容 maxScale: scale * 1.2, graphOptions: { connecting: { connector: 'rounded', connectionPoint: 'anchor', router: { name: 'manhattan', }, }, async: true, frozen: true, interacting: false, grid: false }, viewport: { padding: 0, fitToContent: false, // 禁用自动适配内容 initialPosition: { // 初始位置与主画布保持一致 x: 0, y: 0 }, initialScale: scale } })); graph.use(new Clipboard()); graph.use(history); graph.use( new Transform({ resizing: false, rotating: false, }) ); // 扩展 graph 对象,添加 history 属性 (graph as any).history = history; // 监听图形化 graph.on('cell:added', ({cell}) => { const canUndo = history.canUndo(); const canRedo = history.canRedo(); // 强制更新组件状态已移除 }); graph.on('cell:removed', ({cell}) => { const canUndo = history.canUndo(); const canRedo = history.canRedo(); console.log('Cell removed:', { cell, canUndo, canRedo, stackSize: history.stackSize, }); // 强制更新组件状态已移除 }); graph.on('cell:changed', ({cell, options}) => { const canUndo = history.canUndo(); const canRedo = history.canRedo(); // 强制更新组件状态已移除 }); // 监听主画布变化,同步更新小地图 graph.on('transform', () => { const minimap = graph.getPlugin('minimap') as MiniMap; if (minimap) { const mainViewport = graph.getView().getVisibleArea(); minimap.viewport.scale = scale; minimap.viewport.center(mainViewport.center); } }); // 处理连线重新连接 graph.on('edge:moved', ({edge, terminal, previous}) => { if (!edge || !terminal) return; const isSource = terminal.type === 'source'; const source = isSource ? terminal : edge.getSource(); const target = isSource ? edge.getTarget() : terminal; if (source && target) { // 移除旧的连线 edge.remove(); // 创建新的连线 graph.addEdge({ source: { cell: source.cell, port: source.port, }, target: { cell: target.cell, port: target.port, }, attrs: { line: { stroke: '#5F95FF', strokeWidth: 2, targetMarker: { name: 'classic', size: 7, }, }, }, }); } }); // 处理连线更改 graph.on('edge:change:source edge:change:target', ({edge, current, previous}) => { if (edge && current) { edge.setAttrs({ line: { stroke: '#5F95FF', strokeWidth: 2, targetMarker: { name: 'classic', size: 7, }, }, }); } }); registerEventHandlers(graph); // 在 registerEventHandlers 中添加画布的快捷键处理 // 添加画布的快捷键处理 graphContainerRef.current?.addEventListener('keydown', (e: KeyboardEvent) => { // 只有当画布或其子元素被聚焦时才处理快捷键 if (!graphContainerRef.current?.contains(document.activeElement)) { return; } // Ctrl+A 或 Command+A (Mac) if ((e.ctrlKey || e.metaKey) && e.key === 'a') { e.preventDefault(); // 阻止浏览器默认的全选行为 if (!graph) return; const cells = graph.getCells(); if (cells.length > 0) { graph.resetSelection(); graph.select(cells); // 为选中的元素添加高亮样式 cells.forEach(cell => { if (cell.isNode()) { cell.setAttrByPath('body/stroke', '#1890ff'); cell.setAttrByPath('body/strokeWidth', 3); cell.setAttrByPath('body/strokeDasharray', '5 5'); } else if (cell.isEdge()) { cell.setAttrByPath('line/stroke', '#1890ff'); cell.setAttrByPath('line/strokeWidth', 3); cell.setAttrByPath('line/strokeDasharray', '5 5'); } }); } } }); // 确保画布可以接收键盘事件 graphContainerRef.current?.setAttribute('tabindex', '0'); return graph; }; // 处理撤销操作 const handleUndo = () => { if (!graph) return; const history = (graph as any).history; if (!history) { console.error('History plugin not initialized'); return; } const beforeState = { canUndo: history.canUndo(), canRedo: history.canRedo(), stackSize: history.stackSize, }; if (history.canUndo()) { history.undo(); const afterState = { canUndo: history.canUndo(), canRedo: history.canRedo(), stackSize: history.stackSize, }; console.log('Undo operation:', { before: beforeState, after: afterState, }); message.success('已撤销'); } else { message.info('没有可撤销的操作'); } }; // 处理重做操作 const handleRedo = () => { if (!graph) return; const history = (graph as any).history; if (!history) { console.error('History plugin not initialized'); return; } const beforeState = { canUndo: history.canUndo(), canRedo: history.canRedo(), stackSize: history.stackSize, }; if (history.canRedo()) { history.redo(); const afterState = { canUndo: history.canUndo(), canRedo: history.canRedo(), stackSize: history.stackSize, }; console.log('Redo operation:', { before: beforeState, after: afterState, }); message.success('已重做'); } else { message.info('没有可重做的操作'); } }; // 处理缩放 const handleZoom = (delta: number) => { if (!graph) return; const currentScale = graph.scale(); const newScale = Math.max(0.2, Math.min(2, currentScale.sx + delta)); graph.scale(newScale, newScale); setScale(newScale); }; // 放大 const handleZoomIn = () => { handleZoom(0.1); }; // 缩小 const handleZoomOut = () => { handleZoom(-0.1); }; // 注册事件处理器 const registerEventHandlers = (graph: Graph) => { // 定义悬停样式 const hoverStyle = { strokeWidth: 2, stroke: '#52c41a' // 绿色 }; // 保存节点的原始样式 const saveNodeOriginalStyle = (node: any) => { const data = node.getData(); if (!data?.originalStyle) { const originalStyle = { stroke: node.getAttrByPath('body/stroke') || '#5F95FF', strokeWidth: node.getAttrByPath('body/strokeWidth') || 1 }; node.setData({ ...data, originalStyle }); } }; // 获取节点的原始样式 const getNodeOriginalStyle = (node: any) => { const data = node.getData(); return data?.originalStyle || { stroke: '#5F95FF', strokeWidth: 2 }; }; // 恢复节点的原始样式 const resetNodeStyle = (node: any) => { const originalStyle = getNodeOriginalStyle(node); node.setAttrByPath('body/stroke', originalStyle.stroke); node.setAttrByPath('body/strokeWidth', originalStyle.strokeWidth); }; // 节点悬停事件 graph.on('node:mouseenter', ({node}) => { // 保存原始样式 saveNodeOriginalStyle(node); // 显示连接桩 const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`); ports.forEach((port) => { port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;'); }); // 显示悬停样式 node.setAttrByPath('body/stroke', hoverStyle.stroke); node.setAttrByPath('body/strokeWidth', hoverStyle.strokeWidth); }); graph.on('node:mouseleave', ({node}) => { // 隐藏连接桩 const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`); ports.forEach((port) => { port.setAttribute('style', 'visibility: hidden'); }); // 恢复原始样式 resetNodeStyle(node); }); // 节点拖动开始时记录状态 graph.on('node:drag:start', ({node}) => { // 保存原始样式,以防还没保存过 saveNodeOriginalStyle(node); }); // 节点拖动结束后恢复样式 graph.on('node:moved', ({node}) => { resetNodeStyle(node); // 隐藏连接桩 const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`); ports.forEach((port) => { port.setAttribute('style', 'visibility: hidden'); }); }); // 节点点击事件 graph.on('node:click', ({node}) => { // 取当前选中的节点 const selectedNode = graph.getSelectedCells()[0]; // 如果有其他节点被选中,恢复其样式 if (selectedNode && selectedNode.isNode() && selectedNode.id !== node.id) { resetNodeStyle(selectedNode); } // 更新选中状态 graph.resetSelection(); graph.select(node); }); // 点击空白处事件 graph.on('blank:click', () => { // 获取当前选中的节点 const selectedNode = graph.getSelectedCells()[0]; // 如果有节点被选中,恢复其样式 if (selectedNode && selectedNode.isNode()) { resetNodeStyle(selectedNode); } // 清除选中状态 graph.resetSelection(); }); // 节点双击事件 graph.on('node:dblclick', ({node}) => { const nodeType = node.getProp('nodeType'); // 从节点定义列表中找到对应的定义 const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeType); if (nodeDefinition) { // 获取已保存的配置 const savedConfig = node.getProp('workflowDefinitionNode'); // 合并节点定义和已保存的配置 const mergedNodeDefinition = { ...nodeDefinition, // 基础定义 ...savedConfig, // 已保存的配置(从端加载的) ...node.getProp('graph') || {} // 当前会话中的修改(如果有) }; setSelectedNode(node); setSelectedNodeDefinition(mergedNodeDefinition); setConfigModalVisible(true); } }); // 添加右键菜单 graph.on('node:contextmenu', ({cell, view, e}) => { e.preventDefault(); graph.cleanSelection(); graph.select(cell); const dropdownContainer = document.createElement('div'); dropdownContainer.style.position = 'absolute'; dropdownContainer.style.left = `${e.clientX}px`; dropdownContainer.style.top = `${e.clientY}px`; document.body.appendChild(dropdownContainer); const root = createRoot(dropdownContainer); let isOpen = true; const closeMenu = () => { isOpen = false; root.render( { if (!open) { setTimeout(() => { root.unmount(); document.body.removeChild(dropdownContainer); }, 100); } }}>
); }; const items = [ { key: '1', label: '编辑', onClick: () => { closeMenu(); setSelectedNode(cell); setSelectedNodeDefinition(nodeDefinitions.find(def => def.nodeType === cell.getProp('nodeType'))); setConfigModalVisible(true); } }, { key: '2', label: '删除', onClick: () => { closeMenu(); Modal.confirm({ title: '确认删除', content: '确定要删除该节点吗?', onOk: () => { cell.remove(); } }); } } ]; root.render(
); const handleClickOutside = (e: MouseEvent) => { if (!dropdownContainer.contains(e.target as Node)) { closeMenu(); document.removeEventListener('click', handleClickOutside); } }; document.addEventListener('click', handleClickOutside); }); // 添加边的右键菜单 graph.on('edge:contextmenu', ({cell: edge, view, e}) => { e.preventDefault(); graph.cleanSelection(); graph.select(edge); const sourceNode = graph.getCellById(edge.getSourceCellId()); const isFromGateway = sourceNode.getProp('nodeType') === 'GATEWAY_NODE'; const dropdownContainer = document.createElement('div'); dropdownContainer.style.position = 'absolute'; dropdownContainer.style.left = `${e.clientX}px`; dropdownContainer.style.top = `${e.clientY}px`; document.body.appendChild(dropdownContainer); const root = createRoot(dropdownContainer); let isOpen = true; const closeMenu = () => { isOpen = false; root.render( { if (!open) { setTimeout(() => { root.unmount(); document.body.removeChild(dropdownContainer); }, 100); } }}>
); }; const items = [ // 只有网关节点的出线才显示编辑条件选项 ...(isFromGateway ? [{ key: 'edit', label: '编辑条件', onClick: () => { closeMenu(); setSelectedEdge(edge); setExpressionModalVisible(true); } }] : []), { key: 'delete', label: '删除', danger: true, onClick: () => { closeMenu(); Modal.confirm({ title: '确认删除', content: '确定要删除该连接线吗?', onOk: () => { edge.remove(); } }); } } ]; root.render(
); const handleClickOutside = (e: MouseEvent) => { if (!dropdownContainer.contains(e.target as Node)) { closeMenu(); document.removeEventListener('click', handleClickOutside); } }; document.addEventListener('click', handleClickOutside); }); // 禁用默认的右键菜单 graph.on('blank:contextmenu', ({e}) => { e.preventDefault(); }); // 禁用节点的右键菜单 graph.on('node:contextmenu', ({e}) => { e.preventDefault(); }); // 禁用边的右键菜单 graph.on('edge:contextmenu', ({e}) => { e.preventDefault(); }); // 连接桩显示/隐藏 graph.on('node:mouseenter', ({node}) => { // 保存原始样式 saveNodeOriginalStyle(node); // 显示连接桩 const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`); ports.forEach((port) => { port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;'); }); // 显示悬停样式 node.setAttrByPath('body/stroke', hoverStyle.stroke); node.setAttrByPath('body/strokeWidth', hoverStyle.strokeWidth); }); // 连线开始时的处理 graph.on('edge:connected', ({edge}) => { // 设置连线样式 edge.setAttrs({ line: { stroke: '#5F95FF', strokeWidth: 2, targetMarker: { name: 'classic', size: 7, }, }, }); }); // 连线悬停效果 graph.on('edge:mouseenter', ({edge}) => { edge.setAttrs({ line: { stroke: '#52c41a', strokeWidth: 2, }, }); }); graph.on('edge:mouseleave', ({edge}) => { edge.setAttrs({ line: { stroke: '#5F95FF', strokeWidth: 2, }, }); }); // 允许拖动连线中间的点和端点 graph.on('edge:selected', ({edge}) => { edge.addTools([ { name: 'source-arrowhead', args: { attrs: { fill: '#fff', stroke: '#5F95FF', strokeWidth: 1, d: 'M 0 -6 L -8 0 L 0 6 Z', }, }, }, { name: 'target-arrowhead', args: { attrs: { fill: '#fff', stroke: '#5F95FF', strokeWidth: 1, d: 'M 0 -6 L 8 0 L 0 6 Z', }, }, }, { name: 'vertices', args: { padding: 2, attrs: { fill: '#fff', stroke: '#5F95FF', strokeWidth: 1, r: 4 }, }, }, { name: 'segments', args: { padding: 2, attrs: { fill: '#fff', stroke: '#5F95FF', strokeWidth: 1, r: 4 }, }, }, ]); }); // 线工具移除 graph.on('edge:unselected', ({edge}) => { edge.removeTools(); }); // 连线移动到其他连接桩时的样式 graph.on('edge:connected', ({edge}) => { edge.setAttrs({ line: { stroke: '#5F95FF', strokeWidth: 2, targetMarker: { name: 'classic', size: 7, }, }, }); }); // 连线悬停在连接桩上时的样式 graph.on('edge:mouseenter', ({edge}) => { const ports = document.querySelectorAll('.x6-port-body'); ports.forEach((port) => { const portGroup = port.getAttribute('port-group'); if (portGroup !== 'top') { port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;'); } }); }); graph.on('edge:mouseleave', ({edge}) => { const ports = document.querySelectorAll('.x6-port-body'); ports.forEach((port) => { port.setAttribute('style', 'visibility: hidden'); }); }); // 边选中时添加编辑工具 graph.on('edge:selected', ({edge}) => { edge.addTools([ { name: 'vertices', // 顶点工具 args: { padding: 4, attrs: { fill: '#fff', stroke: '#5F95FF', strokeWidth: 2, } } }, { name: 'segments', // 线段工具 args: { attrs: { fill: '#fff', stroke: '#5F95FF', strokeWidth: 2, } } } ]); }); // 边取消选中时移除工具 graph.on('edge:unselected', ({edge}) => { edge.removeTools(); }); // 在 registerEventHandlers 函数中更新选择状态的视觉反馈 graph.on('selection:changed', ({selected, removed}) => { console.log(selected, removed) // 处理新选中的元素 selected.forEach(cell => { if (cell.isNode()) { cell.setAttrByPath('body/stroke', '#1890ff'); cell.setAttrByPath('body/strokeWidth', 2); cell.setAttrByPath('body/strokeDasharray', '5 5'); } else if (cell.isEdge()) { cell.setAttrByPath('line/stroke', '#1890ff'); cell.setAttrByPath('line/strokeWidth', 2); cell.setAttrByPath('line/strokeDasharray', '5 5'); } }); // 处理取消选中的元素 removed.forEach(cell => { if (cell.isNode()) { cell.setAttrByPath('body/stroke', '#5F95FF'); cell.setAttrByPath('body/strokeWidth', 2); cell.setAttrByPath('body/strokeDasharray', null); } else if (cell.isEdge()) { cell.setAttrByPath('line/stroke', '#5F95FF'); cell.setAttrByPath('line/strokeWidth', 2); cell.setAttrByPath('line/strokeDasharray', null); } }); }); }; // 处理复制操作 const handleCopy = () => { if (!graph) return; const cells = graph.getSelectedCells(); if (cells.length === 0) { message.info('请先选择要复制的节点'); return; } graph.copy(cells); message.success('已复制'); }; // 处理剪切操作 const handleCut = () => { if (!graph) return; const cells = graph.getSelectedCells(); if (cells.length === 0) { message.info('请先选择要剪切的节点'); return; } graph.cut(cells); message.success('已剪切'); }; // 处理粘贴操作 const handlePaste = () => { if (!graph) return; if (graph.isClipboardEmpty()) { message.info('剪贴板为空'); return; } const cells = graph.paste({offset: 32}); graph.cleanSelection(); graph.select(cells); message.success('已粘贴'); }; // 首先加载节点定义列表 useEffect(() => { const loadNodeDefinitions = async () => { try { const data = await getNodeDefinitionList(); setNodeDefinitions(data); setIsNodeDefinitionsLoaded(true); } catch (error) { console.error('加载节点定义失败:', error); message.error('加载节点定义失败'); } }; loadNodeDefinitions(); }, []); // 加载工作流定义详情 const loadDefinitionDetail = async (graphInstance: Graph, definitionId: string) => { try { const response = await getDefinitionDetail(Number(definitionId)); setTitle(`工作流设计 - ${response.name}`); setDefinitionData(response); if (!graphInstance) { console.error('Graph instance is not initialized'); return; } // 确保节点定义已加载 if (!nodeDefinitions || nodeDefinitions.length === 0) { console.error('节点定义未加载,无法还原工作流'); return; } // 清空画布 graphInstance.clearCells(); const nodeMap = new Map(); // 创建节点 response.graph?.nodes?.forEach((existingNode: any) => { console.log('正在还原节点:', existingNode.nodeType, existingNode); const node = addNodeToGraph(false, graphInstance, existingNode, nodeDefinitions); if (!node) { console.error('节点创建失败:', existingNode); return; } // 只设置 graph 属性 node.setProp('graph', { uiVariables: existingNode.uiVariables, panelVariables: existingNode.panelVariables, localVariables: existingNode.localVariables }); nodeMap.set(existingNode.id, node); console.log('节点创建成功,ID:', node.id, '映射 ID:', existingNode.id); }); console.log('所有节点创建完成,nodeMap:', nodeMap); console.log('准备创建边:', response.graph?.edges); // 创建边 response.graph?.edges?.forEach((edge: any) => { const sourceNode = nodeMap.get(edge.from); const targetNode = nodeMap.get(edge.to); if (sourceNode && targetNode) { // 根据节点类型获取正确的端口组 const getPortByGroup = (node: any, group: string) => { const ports = node.getPorts(); const port = ports.find((p: any) => p.group === group); return port?.id; }; // 获取源节点的输出端口(一定是out组) const sourcePort = getPortByGroup(sourceNode, 'out'); // 获取目标节点的输入端口(一定是in组) const targetPort = getPortByGroup(targetNode, 'in'); if (!sourcePort || !targetPort) { console.error('无法找到正确的端口:', edge); return; } const newEdge = graphInstance.addEdge({ source: { cell: sourceNode.id, port: sourcePort, }, target: { cell: targetNode.id, port: targetPort, }, vertices: edge?.vertices || [], // 恢复顶点信息 attrs: { line: { stroke: '#5F95FF', strokeWidth: 2, targetMarker: { name: 'classic', size: 7, }, }, }, labels: [{ attrs: { label: { text: edge.config?.condition?.expression || edge.name || '' } } }] }); // 设置边的条件属性 if (edge.config?.condition) { newEdge.setProp('condition', edge.config.condition); } } }); // 传入节点数据,只在没有位置信息时应用自动布局 // applyAutoLayout(graphInstance, response.graph?.nodes || []); } catch (error) { console.error('加载工作流定义失败:', error); message.error('加载工作流定义失败'); } }; // 初始化图形和加载数据 useEffect(() => { if (!graphContainerRef.current || !isNodeDefinitionsLoaded) { return; } const newGraph = initGraph(); if (newGraph) { setGraph(newGraph); // 在图形初始化完成后加载数据 if (id) { loadDefinitionDetail(newGraph, id); } } return () => { graph?.dispose(); }; }, [graphContainerRef, id, nodeDefinitions, isNodeDefinitionsLoaded]); // 处理节点拖拽开始 const handleNodeDragStart = (node: NodeDefinitionResponse, e: React.DragEvent) => { e.dataTransfer.setData('node', JSON.stringify(node)); }; // 处理节点拖拽结束 const handleDrop = (e: React.DragEvent) => { e.preventDefault(); if (!graph) return; try { const nodeData = e.dataTransfer.getData('node'); if (!nodeData) return; const node = JSON.parse(nodeData); const {clientX, clientY} = e; const point = graph.clientToLocal({x: clientX, y: clientY}); addNodeToGraph(true, graph, node, nodeDefinitions, point); } catch (error) { console.error('创建节点失败:', error); message.error('创建节点失败'); } }; // 处理拖拽过程中 const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); }; // 处理节点配置更新 const handleNodeConfigUpdate = (updatedNodeDefinition: any) => { if (!selectedNode) return; // 设置节点的 graph 属性,将所有数据统一放在 graph 下 selectedNode.setProp('graph', updatedNodeDefinition); // 更新节点显示名称(如果存在) if (updatedNodeDefinition.panelVariables?.name) { selectedNode.attr('label/text', updatedNodeDefinition.panelVariables.name); } setConfigModalVisible(false); message.success('节点配置已更新'); }; // 首先添加合并 schema 的工具函数 const mergeLocalVariablesSchemas = (schemas: any[]) => { // 初始化合并后的 schema const mergedSchema = { type: 'object', properties: {}, required: [] }; schemas.forEach(schema => { if (!schema) return; // 合并 properties if (schema.properties) { Object.entries(schema.properties).forEach(([key, property]) => { // 如果属性已存在,检查是否完全相同 if (mergedSchema.properties[key]) { if (JSON.stringify(mergedSchema.properties[key]) !== JSON.stringify(property)) { console.warn(`属性 ${key} 在不同节点中定义不一致,使用第一个定义`); } } else { mergedSchema.properties[key] = property; } }); } // 合并 required 字段 if (schema.required) { schema.required.forEach((field: string) => { if (!mergedSchema.required.includes(field)) { mergedSchema.required.push(field); } }); } }); return mergedSchema; }; // 处理保存工作流 const handleSaveWorkflow = async () => { if (!graph || !definitionData) return; console.log(definitionData) try { // 校验流程图 const validationResult = validateWorkflow(graph); if (!validationResult.valid) { message.error(validationResult.message); return; } // 获取所有节点和边的数据 const nodes = graph.getNodes().map(node => { const nodeType = node.getProp('nodeType'); const graphData = node.getProp('graph') || {}; const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeType); const position = node.getPosition(); const { uiVariables, panelVariables, localVariables, ...rest } = graphData; return { id: node.id, nodeCode: nodeType, nodeType: nodeType, nodeName: node.attr('label/text'), uiVariables: { ...uiVariables, position: position }, panelVariables, localVariables: nodeDefinition?.localVariablesSchema || {} }; }); const edges = graph.getEdges().map(edge => { const condition = edge.getProp('condition'); const vertices = edge.getVertices(); // 获取边的顶点信息 return { id: edge.id, from: edge.getSourceCellId(), to: edge.getTargetCellId(), name: edge.getLabels()?.[0]?.attrs?.label?.text || '', config: { type: 'sequence', condition: condition || undefined }, vertices: vertices // 保存顶点信息 }; }); // 收集并合并所有节点的 localVariablesSchema const allLocalSchemas = nodes .map(node => { const nodeDefinition = nodeDefinitions.find(def => def.nodeType === node.nodeType); return nodeDefinition?.localVariablesSchema; }) .filter(schema => schema); // 过滤掉空值 const mergedLocalSchema = mergeLocalVariablesSchemas(allLocalSchemas); // 构建保存数据 const saveData = { ...definitionData, graph: { nodes, edges }, localVariablesSchema: mergedLocalSchema }; // 调用保存接口 await saveDefinition(saveData); message.success('保存成功'); } catch (error) { console.error('保存流程失败:', error); message.error('保存流程失败'); } }; // 处理条件更新 const handleConditionUpdate = (condition: EdgeCondition) => { if (!selectedEdge) return; // 更新边的属性 selectedEdge.setProp('condition', condition); // 更新边的标签显示 const labelText = condition.type === 'EXPRESSION' ? condition.expression : '默认路径'; selectedEdge.setLabels([{ attrs: { label: { text: labelText, fill: '#333', fontSize: 12 }, rect: { fill: '#fff', stroke: '#ccc', rx: 3, ry: 3, padding: 5 } }, position: { distance: 0.5, offset: 0 } }]); setExpressionModalVisible(false); setSelectedEdge(null); }; return (
{configModalVisible && selectedNode && selectedNodeDefinition && ( setConfigModalVisible(false)} onOk={handleNodeConfigUpdate} /> )} {selectedEdge && ( { setExpressionModalVisible(false); setSelectedEdge(null); }} /> )}
); }; export default WorkflowDesign;