diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7a5aba08..87e8fc57 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@antv/x6-plugin-export": "^2.1.6", "@antv/x6-plugin-history": "^2.2.4", "@antv/x6-plugin-keyboard": "^2.2.3", + "@antv/x6-plugin-minimap": "^2.0.7", "@antv/x6-plugin-selection": "^2.2.2", "@antv/x6-plugin-snapline": "^2.1.7", "@antv/x6-plugin-transform": "^2.1.8", @@ -264,6 +265,15 @@ "@antv/x6": "^2.x" } }, + "node_modules/@antv/x6-plugin-minimap": { + "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" + } + }, "node_modules/@antv/x6-plugin-selection": { "version": "2.2.2", "resolved": "https://registry.npmmirror.com/@antv/x6-plugin-selection/-/x6-plugin-selection-2.2.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5e80c87f..0fe2cf1b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@antv/x6-plugin-export": "^2.1.6", "@antv/x6-plugin-history": "^2.2.4", "@antv/x6-plugin-keyboard": "^2.2.3", + "@antv/x6-plugin-minimap": "^2.0.7", "@antv/x6-plugin-selection": "^2.2.2", "@antv/x6-plugin-snapline": "^2.1.7", "@antv/x6-plugin-transform": "^2.1.8", diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.module.less b/frontend/src/pages/Workflow/Definition/Designer/index.module.less index ffe2f017..e0a510bd 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.module.less +++ b/frontend/src/pages/Workflow/Definition/Designer/index.module.less @@ -1,5 +1,7 @@ :global { .workflow-designer { + height: 100%; + &-container { height: calc(100vh - 170px); background: #fff; @@ -8,21 +10,43 @@ &-sider { background: #fff; - border-right: 1px solid #e8e8e8; + border-right: 1px solid #f0f0f0; } &-content { position: relative; - background: #fafafa; + height: 100%; padding: 16px; + background: #fafafa; } &-graph { - width: 100%; height: 100%; background: #fff; - border: 1px solid #e8e8e8; + border: 1px solid #f0f0f0; border-radius: 2px; } + + &-minimap { + position: absolute; + right: 24px; + bottom: 24px; + width: 200px; + height: 150px; + background-color: #fff; + border: 1px solid #f0f0f0; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } } -} \ No newline at end of file + + .ant-card-body { + height: calc(100% - 57px); + padding: 0; + } + + .ant-layout { + height: 100%; + background: #fff; + } +} \ 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 954cb6f3..5820f9e6 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useRef, useState} from 'react'; import {useNavigate, useParams} from 'react-router-dom'; -import {Button, Card, Layout, message, Space, Spin, Drawer, Form} from 'antd'; +import {Button, Card, Layout, message, Space, Spin, Drawer, Form, Dropdown} from 'antd'; import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons'; import {getDefinition, updateDefinition} from '../../service'; import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types'; @@ -12,11 +12,14 @@ 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 {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'; const {Sider, Content} = Layout; @@ -52,6 +55,107 @@ const FlowDesigner: React.FC = () => { const [nodeTypes, setNodeTypes] = useState([]); const [form] = Form.useForm(); + // 右键菜单状态 + const [contextMenu, setContextMenu] = useState<{ + x: number; + y: number; + visible: boolean; + type: 'node' | 'edge' | 'canvas'; + cell?: Cell; + }>({ + x: 0, + y: 0, + visible: false, + type: 'canvas', + }); + + // 右键菜单项 + const menuItems = { + node: [ + { + key: 'delete', + label: '删除节点', + icon: , + onClick: () => { + if (contextMenu.cell) { + contextMenu.cell.remove(); + } + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, + { + key: 'copy', + label: '复制节点', + icon: , + onClick: () => { + if (contextMenu.cell && contextMenu.cell.isNode()) { + const pos = contextMenu.cell.position(); + const newCell = contextMenu.cell.clone(); + newCell.position(pos.x + 20, pos.y + 20); + graph?.addCell(newCell); + } + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, + { + key: 'config', + label: '配置节点', + icon: , + onClick: () => { + if (contextMenu.cell && contextMenu.cell.isNode()) { + setCurrentNode(contextMenu.cell); + const data = contextMenu.cell.getData() as NodeData; + 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 + }; + form.setFieldsValue(formValues); + } + setConfigVisible(true); + } + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, + ], + edge: [ + { + key: 'delete', + label: '删除连线', + icon: , + onClick: () => { + if (contextMenu.cell) { + contextMenu.cell.remove(); + } + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, + ], + canvas: [ + { + key: 'clear', + label: '清空画布', + icon: , + onClick: () => { + graph?.clearCells(); + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, + { + key: 'fit', + label: '适应画布', + icon: , + onClick: () => { + graph?.zoomToFit({ padding: 20 }); + setContextMenu(prev => ({ ...prev, visible: false })); + }, + }, + ], + }; + // 获取所有节点类型 const fetchNodeTypes = async () => { try { @@ -80,50 +184,46 @@ const FlowDesigner: React.FC = () => { width: 800, height: 600, grid: { - size: 10, visible: true, - type: 'dot', + type: 'mesh', + size: 10, args: { - color: '#ccc', - thickness: 1, + color: '#e5e5e5', // 网格线颜色 + thickness: 1, // 网格线宽度 }, }, + mousewheel: { + enabled: true, + modifiers: ['ctrl', 'meta'], + minScale: 0.5, + maxScale: 2, + }, connecting: { - router: 'manhattan', + snap: true, // 连线时自动吸附 + allowBlank: false, // 禁止连接到空白位置 + allowLoop: false, // 禁止自环 + allowNode: false, // 禁止直接连接到节点(必须连接到连接桩) + allowEdge: false, // 禁止边连接到边 connector: { name: 'rounded', args: { radius: 8, }, }, - anchor: 'center', - connectionPoint: 'anchor', - allowBlank: false, - snap: { - radius: 20, + router: { + name: 'manhattan', // 使用曼哈顿路由 + args: { + padding: 1, + }, }, - createEdge() { - return this.createEdge({ - attrs: { - line: { - stroke: '#5F95FF', - strokeWidth: 1, - targetMarker: { - name: 'classic', - size: 8, - }, - }, - }, - router: { - name: 'manhattan', - }, - connector: { - name: 'rounded', - args: { - radius: 8, - }, - }, - }); + validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) { + if (sourceCell === targetCell) { + return false; // 禁止自环 + } + if (!sourceMagnet || !targetMagnet) { + return false; // 必须使用连接桩 + } + return true; }, }, highlighting: { @@ -137,22 +237,16 @@ const FlowDesigner: React.FC = () => { }, }, }, - }, - mousewheel: { - enabled: true, - modifiers: ['ctrl', 'meta'], - minScale: 0.5, - maxScale: 2, - }, - interacting: { - nodeMovable: true, - edgeMovable: true, - edgeLabelMovable: true, - arrowheadMovable: true, - vertexMovable: true, - vertexAddable: true, - vertexDeletable: true, - magnetConnectable: true, + magnetAdsorbed: { + name: 'stroke', + args: { + padding: 4, + attrs: { + strokeWidth: 4, + stroke: '#1890ff', + }, + }, + }, }, keyboard: { enabled: true, @@ -170,7 +264,7 @@ const FlowDesigner: React.FC = () => { restrict: true, }, background: { - color: '#F8F9FA', + color: '#ffffff', // 画布背景色 }, }); @@ -238,12 +332,58 @@ const FlowDesigner: React.FC = () => { }) ); - // 启用基础功能 - graph.enableSelection(); - graph.enableRubberband(); - graph.enableKeyboard(); - graph.enableClipboard(); - graph.enableHistory(); + // 启用小地图 + 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, + }); + }); + + graph.on('blank:contextmenu', ({ e }) => { + e.preventDefault(); + setContextMenu({ + x: e.clientX, + y: e.clientY, + visible: true, + type: 'canvas', + }); + }); + + // 点击画布时隐藏右键菜单 + graph.on('blank:click', () => { + setContextMenu(prev => ({ ...prev, visible: false })); + }); + + // 点击节点时隐藏右键菜单 + graph.on('cell:click', () => { + setContextMenu(prev => ({ ...prev, visible: false })); + }); graphRef.current = graph; setGraph(graph); @@ -583,31 +723,43 @@ const FlowDesigner: React.FC = () => { return ( - - + + } + className="workflow-designer" > - - + +
+
+ { + domEvent.stopPropagation(); + }, + }} + open={contextMenu.visible} + trigger={['contextMenu']} + > +
+