From 0ec831fc36775efe2c5a9a40a44574eda0b664d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=9A=E8=BE=B0=E5=85=88=E7=94=9F?= Date: Fri, 6 Dec 2024 22:19:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=BF=81=E7=A7=BB=E5=87=BA?= =?UTF-8?q?=E5=8E=BB=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 7 - .../Workflow/Definition/Designer/index.tsx | 383 ++---------------- .../Definition/Designer/utils/graphUtils.ts | 302 ++++++++++++++ 3 files changed, 343 insertions(+), 349 deletions(-) create mode 100644 frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0dd9252e..989783cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -232,7 +232,6 @@ "version": "2.1.6", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-clipboard/-/x6-plugin-clipboard-2.1.6.tgz", "integrity": "sha512-roZPLnZx6PK8MBvee0QMo90fz/TXeF0WNe4EGin2NBq5M1I5XTWrYvA6N2XVIiWAAI67gjQeEE8TpkL7f8QdqA==", - "license": "MIT", "peerDependencies": { "@antv/x6": "^2.x" } @@ -250,7 +249,6 @@ "version": "2.2.4", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-history/-/x6-plugin-history-2.2.4.tgz", "integrity": "sha512-9gHHvEW4Fla+1hxUV49zNgJyIMoV9CjVM52MrFgAJcvyRn1Kvxz4MfxiKlG+DEZUs+/zvfjl9pS6gJOd8laRkg==", - "license": "MIT", "peerDependencies": { "@antv/x6": "^2.x" } @@ -259,7 +257,6 @@ "version": "2.2.3", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-keyboard/-/x6-plugin-keyboard-2.2.3.tgz", "integrity": "sha512-pnCIC+mDyKKfkcDyLePfGxKVIqXBcldTgannITkHC1kc0IafRS1GMvzpvuDGrM5haRYd6Nwz8kjkJyHkJE4GPA==", - "license": "MIT", "dependencies": { "mousetrap": "^1.6.5" }, @@ -271,7 +268,6 @@ "version": "2.0.7", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-minimap/-/x6-plugin-minimap-2.0.7.tgz", "integrity": "sha512-8zzESCx0jguFPCOKCA0gPFb6JmRgq81CXXtgJYe34XhySdZ6PB23I5Po062lNsEuTjTjnzli4lju8vvU+jzlGw==", - "license": "MIT", "peerDependencies": { "@antv/x6": "^2.x" } @@ -280,7 +276,6 @@ "version": "2.2.2", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-selection/-/x6-plugin-selection-2.2.2.tgz", "integrity": "sha512-s2gtR9Onlhr7HOHqyqg0d+4sG76JCcQEbvrZZ64XmSChlvieIPlC3YtH4dg1KMNhYIuBmBmpSum6S0eVTEiPQw==", - "license": "MIT", "peerDependencies": { "@antv/x6": "^2.x" } @@ -289,7 +284,6 @@ "version": "2.1.7", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-snapline/-/x6-plugin-snapline-2.1.7.tgz", "integrity": "sha512-AsysoCb9vES0U2USNhEpYuO/W8I0aYfkhlbee5Kt4NYiMfQfZKQyqW/YjDVaS2pm38C1NKu1LdPVk/BBr4CasA==", - "license": "MIT", "peerDependencies": { "@antv/x6": "^2.x" } @@ -298,7 +292,6 @@ "version": "2.1.8", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-transform/-/x6-plugin-transform-2.1.8.tgz", "integrity": "sha512-GvJuiJ4BKp0H7+qx3R1I+Vzbw5gXp9+oByXo/WyVxE3urOC7LC5sqnaDfIjyYMN6ROLPYPZraLSeSyYBgMgcDw==", - "license": "MIT", "peerDependencies": { "@antv/x6": "^2.x" } diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index 15852ee8..ddbfbe37 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -6,13 +6,6 @@ import {getDefinition, updateDefinition} from '../../service'; import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types'; 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'; -import {Clipboard} from '@antv/x6-plugin-clipboard'; -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 './index.module.less'; import NodePanel from './components/NodePanel'; import NodeConfig from './components/NodeConfig'; @@ -22,9 +15,9 @@ import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, Fullscreen import EdgeConfig from './components/EdgeConfig'; import { validateFlow, hasCycle } from './validate'; import { generateNodeStyle, generatePorts, calculateNodePosition, calculateCanvasPosition } from './utils/nodeUtils'; -import { Position } from './types'; import { NODE_CONFIG } from './configs/nodeConfig'; import { isWorkflowError } from './utils/errors'; +import { initGraph } from './utils/graphUtils'; const {Sider, Content} = Layout; @@ -194,312 +187,55 @@ const FlowDesigner: React.FC = () => { }, []); // 初始化图形 - const initGraph = () => { - if (!containerRef.current) return; + useEffect(() => { + if (detail && containerRef.current) { + const graph = initGraph({ + container: containerRef.current, + miniMapContainer: document.getElementById('workflow-minimap')!, + onContextMenu: (params) => setContextMenu(params), + onNodeClick: (node) => { + setCurrentNode(node); + const data = node.getData() as NodeData; + console.log('Node clicked, data:', data); - const graph = new Graph({ - container: containerRef.current, - grid: { - visible: true, - type: 'mesh', - size: 10, - args: { - color: '#e5e5e5', - thickness: 1, - }, - }, - mousewheel: { - enabled: true, - modifiers: ['ctrl', 'meta'], - minScale: 0.5, - maxScale: 2, - }, - 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 (data) { + const nodeType = nodeTypes.find(type => type.code === data.type); + if (nodeType) { + setCurrentNodeType(nodeType); + const formValues = { + name: data.name || nodeType.name, + description: data.description, + ...data.config + }; + console.log('Setting form values:', formValues); + form.setFieldsValue(formValues); + setConfigVisible(true); + } else { + message.error('未找到对应的节点类型'); + } } - if (!sourceMagnet || !targetMagnet) { - 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', - args: { - padding: 4, - attrs: { - strokeWidth: 4, - stroke: '#52c41a', - }, - }, - }, - magnetAdsorbed: { - name: 'stroke', - args: { - padding: 4, - attrs: { - strokeWidth: 4, - stroke: '#1890ff', - }, - }, - }, - }, - keyboard: { - enabled: true, - }, - clipboard: { - enabled: true, - }, - history: { - enabled: true, - }, - snapline: { - enabled: true, - }, - translating: { - restrict: true, - }, - background: { - color: '#ffffff', // 画布背景色 - }, - }); - - // 启用必要的功能 - graph.use( - new Selection({ - enabled: true, - multiple: true, - rubberband: true, - rubberEdge: true, - movable: true, - showNodeSelectionBox: true, - showEdgeSelectionBox: true, - selectCellOnMoved: false, - selectEdgeOnMoved: false, - selectNodeOnMoved: false, - className: 'node-selected', - strict: true, - }) - ); - - graph.use( - new History({ - enabled: true, - beforeAddCommand: (event: string, args: any) => { - if (event === 'cell:change:*') { - return true; - } - return true; - }, - }) - ); - - graph.use( - new Clipboard({ - enabled: true, - }) - ); - - graph.use( - new Transform({ - resizing: { - enabled: true, - minWidth: 1, - minHeight: 1, - orthogonal: true, - restricted: true, - }, - rotating: { - enabled: true, - grid: 15, - }, - }) - ); - - graph.use( - new Keyboard({ - enabled: true, - }) - ); - - graph.use( - new Snapline({ - enabled: true, - }) - ); - - // 启用小地图 - graph.use( - new MiniMap({ - container: document.getElementById('workflow-minimap'), - width: 200, - height: 150, - padding: 10, - scalable: false, - minScale: 0.5, - maxScale: 2, - graphOptions: { - async: true, - // 简化节点渲染 - grid: false, - background: { - color: '#f5f5f5', - }, - }, - }) - ); - - // 绑定右键菜单事件 - graph.on('cell:contextmenu', ({ cell, e }) => { - e.preventDefault(); - setContextMenu({ - x: e.clientX, - y: e.clientY, - visible: true, - type: cell.isNode() ? 'node' : 'edge', - cell, + onGraphChange: (g) => setGraph(g), + onDragOver: handleDragOver, + onDrop: handleDrop, + flowDetail: detail }); - }); - graph.on('blank:contextmenu', ({ e }) => { - e.preventDefault(); - setContextMenu({ - x: e.clientX, - y: e.clientY, - visible: true, - type: 'canvas', - }); - }); + graphRef.current = graph; + setGraph(graph); - // 点击画布时隐藏右键菜单 - graph.on('blank:click', () => { - setContextMenu(prev => ({ ...prev, visible: false })); - }); - - // 点击节点时隐藏右键菜单 - graph.on('cell:click', () => { - setContextMenu(prev => ({ ...prev, visible: false })); - }); - - graphRef.current = graph; - setGraph(graph); - - // 加载流程图数据 - if (detail) { - loadGraphData(graph, detail); + // 加载流程图数据 + if (detail) { + loadGraphData(graph, detail); + } } - // 监听画布拖拽事件 - containerRef.current.addEventListener('dragover', handleDragOver); - containerRef.current.addEventListener('drop', handleDrop); - - // 监听节点点击事件 - graph.on('node:click', ({node}: { node: Node }) => { - setCurrentNode(node); - const data = node.getData() as NodeData; - console.log('Node clicked, data:', data); - - if (data) { - // 获取节点类型 - const nodeType = nodeTypes.find(type => type.code === data.type); - if (nodeType) { - setCurrentNodeType(nodeType); - - // 合并节点基本配置和执器配置 - const formValues = { - name: data.name || nodeType.name, - description: data.description, - ...data.config // 直接展开所有配置 - }; - - console.log('Setting form values:', formValues); - form.setFieldsValue(formValues); - setConfigVisible(true); - } else { - message.error('未找到对应的节点类型'); - } + return () => { + if (graphRef.current) { + graphRef.current.dispose(); } - }); - - // 监听选择状态变化 - graph.on('selection:changed', () => { - // 强制更新工具栏状态 - setGraph(graph); - }); - }; + }; + }, [detail]); // 处理拖拽移动 const handleDragOver = (e: DragEvent) => { @@ -714,43 +450,6 @@ const FlowDesigner: React.FC = () => { fetchDetail(); }, [id]); - // 初始化图形 - useEffect(() => { - if (detail && containerRef.current) { - initGraph(); - } - // 清理事件监听 - return () => { - if (containerRef.current) { - containerRef.current.removeEventListener('dragover', handleDragOver); - containerRef.current.removeEventListener('drop', handleDrop); - } - if (graphRef.current) { - graphRef.current.dispose(); - } - }; - }, [detail, containerRef.current]); - - useEffect(() => { - if (!containerRef.current) return; - - const resizeObserver = new ResizeObserver(() => { - if (graphRef.current) { - const container = containerRef.current; - if (container) { - const { width, height } = container.getBoundingClientRect(); - graphRef.current.resize(width, height); - } - } - }); - - resizeObserver.observe(containerRef.current); - - return () => { - resizeObserver.disconnect(); - }; - }, []); - // 处理节点拖拽开始 const handleNodeDragStart = (nodeType: NodeType) => { draggedNodeRef.current = nodeType; diff --git a/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts b/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts new file mode 100644 index 00000000..62e3358d --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/utils/graphUtils.ts @@ -0,0 +1,302 @@ +import { Graph } from '@antv/x6'; +import { Selection } from '@antv/x6-plugin-selection'; +import { Keyboard } from '@antv/x6-plugin-keyboard'; +import { Clipboard } from '@antv/x6-plugin-clipboard'; +import { History } from '@antv/x6-plugin-history'; +import { Transform } from '@antv/x6-plugin-transform'; +import { Snapline } from '@antv/x6-plugin-snapline'; +import { MiniMap } from '@antv/x6-plugin-minimap'; +import { NodeData, NodeType, WorkflowDefinition } from '../types'; +import { Node } from '@antv/x6'; + +export const initGraph = ({ + container, + miniMapContainer, + onContextMenu, + onNodeClick, + onGraphChange, + onDragOver, + onDrop, + flowDetail +}: { + container: HTMLDivElement; + miniMapContainer: HTMLElement; + onContextMenu: (params: any) => void; + onNodeClick: (node: Node) => void; + onGraphChange: (graph: Graph) => void; + onDragOver: (e: DragEvent) => void; + onDrop: (e: DragEvent) => void; + flowDetail?: WorkflowDefinition; +}) => { + const graph = new Graph({ + container, + grid: { + visible: true, + type: 'mesh', + size: 10, + args: { + color: '#e5e5e5', + thickness: 1, + }, + }, + mousewheel: { + enabled: true, + modifiers: ['ctrl', 'meta'], + minScale: 0.5, + maxScale: 2, + }, + 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; + } + 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', + args: { + padding: 4, + attrs: { + strokeWidth: 4, + stroke: '#52c41a', + }, + }, + }, + magnetAdsorbed: { + name: 'stroke', + args: { + padding: 4, + attrs: { + strokeWidth: 4, + stroke: '#1890ff', + }, + }, + }, + }, + keyboard: { + enabled: true, + }, + clipboard: { + enabled: true, + }, + history: { + enabled: true, + }, + snapline: { + enabled: true, + }, + translating: { + restrict: true, + }, + background: { + color: '#ffffff', // 画布背景色 + }, + }); + + // 启用必要的功能 + graph.use( + new Selection({ + enabled: true, + multiple: true, + rubberband: true, + rubberEdge: true, + movable: true, + showNodeSelectionBox: true, + showEdgeSelectionBox: true, + selectCellOnMoved: false, + selectEdgeOnMoved: false, + selectNodeOnMoved: false, + className: 'node-selected', + strict: true, + }) + ); + + graph.use( + new History({ + enabled: true, + beforeAddCommand: (event: string, args: any) => { + if (event === 'cell:change:*') { + return true; + } + return true; + }, + }) + ); + + graph.use( + new Clipboard({ + enabled: true, + }) + ); + + graph.use( + new Transform({ + resizing: { + enabled: true, + minWidth: 1, + minHeight: 1, + orthogonal: true, + restricted: true, + }, + rotating: { + enabled: true, + grid: 15, + }, + }) + ); + + graph.use( + new Keyboard({ + enabled: true, + }) + ); + + graph.use( + new Snapline({ + enabled: true, + }) + ); + + // 启用小地图 + graph.use( + new MiniMap({ + container: miniMapContainer, + width: 200, + height: 150, + padding: 10, + scalable: false, + minScale: 0.5, + maxScale: 2, + graphOptions: { + async: true, + grid: false, + background: { + color: '#f5f5f5', + }, + }, + }) + ); + + // 绑定事件 + graph.on('cell:contextmenu', ({ cell, e }) => { + e.preventDefault(); + onContextMenu({ + x: e.clientX, + y: e.clientY, + visible: true, + type: cell.isNode() ? 'node' : 'edge', + cell, + }); + }); + + graph.on('blank:contextmenu', ({ e }) => { + e.preventDefault(); + onContextMenu({ + x: e.clientX, + y: e.clientY, + visible: true, + type: 'canvas', + }); + }); + + // 点击画布时隐藏右键菜单 + graph.on('blank:click', () => { + onContextMenu({ visible: false, x: 0, y: 0, type: 'canvas' }); + }); + + // 点击节点时隐藏右键菜单 + graph.on('cell:click', () => { + onContextMenu({ visible: false, x: 0, y: 0, type: 'canvas' }); + }); + + // 监听节点点击事件 + graph.on('node:click', ({ node }) => { + onNodeClick(node); + }); + + // 监听选择状态变化 + graph.on('selection:changed', () => { + onGraphChange(graph); + }); + + // 监听画布拖拽事件 + container.addEventListener('dragover', onDragOver); + container.addEventListener('drop', onDrop); + + return graph; +};