diff --git a/frontend/src/pages/Workflow/Definition/Design/index.tsx b/frontend/src/pages/Workflow/Definition/Design/index.tsx index d54a0027..06926bfb 100644 --- a/frontend/src/pages/Workflow/Definition/Design/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Design/index.tsx @@ -18,12 +18,16 @@ import { } from '@ant-design/icons'; import {Graph, Cell} from '@antv/x6'; import '@antv/x6-plugin-snapline'; -import '@antv/x6-plugin-clipboard'; +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'; @@ -79,9 +83,73 @@ const WorkflowDesign: React.FC = () => { const graph = new Graph({ container: graphContainerRef.current, - grid: GRID_CONFIG, - connecting: CONNECTING_CONFIG, - highlighting: HIGHLIGHTING_CONFIG, + 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 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, }, @@ -90,23 +158,24 @@ const WorkflowDesign: React.FC = () => { multiple: true, rubberband: true, movable: true, - showNodeSelectionBox: true, - strict: true, - selectCellOnMoved: true, - selectNodeOnMoved: true, - selectEdgeOnMoved: true, - multipleSelectionModifiers: ['ctrl', 'meta'], - showEdgeSelectionBox: true, - showAnchorSelectionBox: true, - pointerEvents: 'auto' + showNodeSelectionBox: false, // 禁用节点选择框 + showEdgeSelectionBox: false, // 禁用边选择框 + selectNodeOnMoved: false, + selectEdgeOnMoved: false, }, snapline: true, keyboard: { enabled: true, }, + panning: { + enabled: true, + eventTypes: ['rightMouseDown'], // 右键按下时启用画布拖拽 + }, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], + minScale: 0.2, + maxScale: 2, }, }); @@ -137,14 +206,13 @@ const WorkflowDesign: React.FC = () => { multiple: true, rubberband: true, movable: true, - showNodeSelectionBox: true, - strict: true, - selectCellOnMoved: true, - selectNodeOnMoved: true, - selectEdgeOnMoved: true, + showNodeSelectionBox: false, // 禁用节点选择框 + showEdgeSelectionBox: false, // 禁用边选择框 + selectNodeOnMoved: false, + selectEdgeOnMoved: false, multipleSelectionModifiers: ['ctrl', 'meta'], - showEdgeSelectionBox: true, - showAnchorSelectionBox: true, + showEdgeSelectionBox: false, + showAnchorSelectionBox: false, pointerEvents: 'auto' }); console.log('Initializing Selection plugin:', selection); @@ -188,6 +256,12 @@ const WorkflowDesign: React.FC = () => { })); graph.use(new Clipboard()); graph.use(history); + graph.use( + new Transform({ + resizing: false, + rotating: false, + }) + ); // 扩展 graph 对象,添加 history 属性 (graph as any).history = history; @@ -243,6 +317,58 @@ const WorkflowDesign: React.FC = () => { } }); + // 处理连线重新连接 + 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); return graph; @@ -378,7 +504,7 @@ const WorkflowDesign: React.FC = () => { // 显示连接桩 const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`); ports.forEach((port) => { - port.setAttribute('style', 'visibility: visible'); + port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;'); }); // 显示悬停样式 @@ -590,6 +716,160 @@ const WorkflowDesign: React.FC = () => { }; 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: 3, + }, + }); + }); + + 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'); + }); + }); }; // 处理复制操作 @@ -674,9 +954,60 @@ const WorkflowDesign: React.FC = () => { const targetNode = nodeMap.get(edge.to); if (sourceNode && targetNode) { graphInstance.addEdge({ - source: {cell: sourceNode.id}, - target: {cell: targetNode.id}, - attrs: {line: DEFAULT_STYLES.edge}, + source: { + cell: sourceNode.id, + port: edge.fromPort || sourceNode.getPorts()[0].id, // 使用指定的端口或默认第一个端口 + }, + target: { + cell: targetNode.id, + port: edge.toPort || targetNode.getPorts()[0].id, // 使用指定的端口或默认第一个端口 + }, + attrs: { + line: { + stroke: '#5F95FF', + strokeWidth: 2, + targetMarker: { + name: 'classic', + size: 7, + }, + }, + }, + tools: [ + { + 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', + }, + }, + }, + ], + connector: { + name: 'rounded', + args: { + radius: 8 + } + }, + router: { + name: 'manhattan', + args: { + padding: 1 + } + }, labels: [{ attrs: {label: {text: edge.name || ''}} }]