From e8c9a4c539d52c6e3d798e087d7089bdcf759352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=9A=E8=BE=B0=E5=85=88=E7=94=9F?= Date: Thu, 5 Dec 2024 13:59:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E5=90=AF=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/FlowDesigner/index.module.css | 94 +-- .../Edit/components/FlowDesigner/index.tsx | 613 +++++------------- 2 files changed, 195 insertions(+), 512 deletions(-) diff --git a/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.module.css b/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.module.css index cc6c7eed..982426e5 100644 --- a/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.module.css +++ b/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.module.css @@ -1,86 +1,32 @@ -.container { - width: 100%; - height: 600px; - background-color: #fff; - border: 1px solid #e8e8e8; - border-radius: 4px; - overflow: hidden; +.flowDesigner { + display: flex; + gap: 16px; + height: 100%; } -.dndNode { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-color: #fff; - border: 1px solid #1890ff; +.nodeTypes { + width: 200px; + flex-shrink: 0; +} + +.nodeType { + margin: 8px 0; + padding: 8px 12px; border-radius: 4px; cursor: move; user-select: none; + transition: all 0.3s; } -.dndNode:hover { - background-color: #e6f7ff; - border-color: #1890ff; +.nodeType:hover { + opacity: 0.8; + transform: translateY(-1px); } -:global(.lf-node) { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; -} - -:global(.lf-node-text) { - font-size: 12px; - color: #333; - text-align: center; - word-break: break-all; -} - -:global(.lf-edge-text) { - font-size: 12px; - color: #666; - background: #fff; - padding: 2px 4px; - border-radius: 2px; +.canvas { + flex: 1; border: 1px solid #e8e8e8; -} - -:global(.lf-node-selected) { - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3); -} - -:global(.lf-edge-selected) { - stroke: #1890ff !important; - stroke-width: 2px !important; -} - -:global(.lf-anchor) { - stroke: #1890ff; - fill: #fff; - stroke-width: 1px; - cursor: crosshair; -} - -:global(.lf-anchor:hover) { - fill: #1890ff; -} - -:global(.lf-edge-label) { - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 2px; - padding: 2px 4px; - font-size: 12px; - color: #666; - user-select: none; - cursor: pointer; -} - -:global(.lf-edge-label:hover) { + border-radius: 4px; background: #f5f5f5; - border-color: #d9d9d9; + min-height: 600px; } \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.tsx b/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.tsx index bea322e0..11001e00 100644 --- a/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Edit/components/FlowDesigner/index.tsx @@ -1,33 +1,21 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Graph, Node, Edge, Shape } from '@antv/x6'; +import { Graph, Node, Edge, Shape, Cell } from '@antv/x6'; import dagre from 'dagre'; import { getNodeTypes } from '@/pages/Workflow/service'; import type { NodeTypeDTO } from '@/pages/Workflow/types'; import styles from './index.module.css'; +import { Card } from 'antd'; // 节点类型映射 const NODE_TYPE_MAP: Record = { - 'TASK': 'SHELL' // 将 TASK 类型映射到 SHELL 类型 -}; - -// 特殊节点类型 -const SPECIAL_NODE_TYPES = { - START: 'START', - END: 'END' + 'TASK': 'SHELL', // 任务节点映射到 SHELL + 'START': 'START', + 'END': 'END' }; // 记录已注册的节点类型 const registeredNodes = new Set(); -// 布局配置 -const LAYOUT_CONFIG = { - PADDING: 20, - NODE_MIN_DISTANCE: 100, - RANK_SEPARATION: 100, - NODE_SEPARATION: 80, - EDGE_SPACING: 20, -}; - interface FlowDesignerProps { value?: string; onChange?: (value: string) => void; @@ -45,302 +33,75 @@ const FlowDesigner: React.FC = ({ const graphRef = useRef(); const [nodeTypes, setNodeTypes] = useState([]); - // 使用 dagre 布局算法 - const layout = (graph: Graph, force: boolean = false) => { - const nodes = graph.getNodes(); - const edges = graph.getEdges(); - - if (nodes.length === 0) return; - - // 如果不是强制布局,且所有节点都有位置信息,则不进行自动布局 - if (!force && nodes.every(node => { - const position = node.position(); - return position.x !== undefined && position.y !== undefined; - })) { - return; - } - - const g = new dagre.graphlib.Graph(); - g.setGraph({ - rankdir: 'LR', - nodesep: LAYOUT_CONFIG.NODE_SEPARATION, - ranksep: LAYOUT_CONFIG.RANK_SEPARATION, - edgesep: LAYOUT_CONFIG.EDGE_SPACING, - marginx: LAYOUT_CONFIG.PADDING, - marginy: LAYOUT_CONFIG.PADDING, - }); - - // 设置默认节点大小 - g.setDefaultEdgeLabel(() => ({})); - - // 添加节点 - nodes.forEach((node) => { - g.setNode(node.id, { - width: node.getSize().width, - height: node.getSize().height, - }); - }); - - // 添加边 - edges.forEach((edge) => { - const source = edge.getSourceCellId(); - const target = edge.getTargetCellId(); - g.setEdge(source, target); - }); - - // 执行布局 - dagre.layout(g); - - // 批量更新节点位置 - const updates = nodes.map((node) => { - const nodeWithPosition = g.node(node.id); - if (nodeWithPosition) { - return { - node, - position: { - x: nodeWithPosition.x - nodeWithPosition.width / 2, - y: nodeWithPosition.y - nodeWithPosition.height / 2, - }, - }; - } - return null; - }).filter(Boolean); - - // 使用批量更新 - graph.batchUpdate(() => { - updates.forEach((update) => { - if (update) { - update.node.position(update.position.x, update.position.y); - } - }); - }); - - graph.centerContent(); - }; - // 注册节点类型 - const registerNodeTypes = (nodeTypes: NodeTypeDTO[]) => { - // 注册特殊节点类型 - if (!registeredNodes.has(SPECIAL_NODE_TYPES.START)) { - Graph.registerNode(SPECIAL_NODE_TYPES.START, { - inherit: 'circle', - width: 40, - height: 40, - attrs: { - body: { - fill: '#52c41a', - stroke: '#52c41a', - strokeWidth: 2, - }, - label: { - text: '开始', - fill: '#fff', - fontSize: 12, - fontFamily: 'Arial, helvetica, sans-serif', - }, - }, - ports: { - groups: { - right: { - position: { - name: 'right', - args: { - dx: 4, - }, - }, - attrs: { - circle: { - r: 4, - magnet: true, - stroke: '#52c41a', - strokeWidth: 2, - fill: '#fff', - }, - }, - }, - }, - items: [ - { - id: 'right', - group: 'right', - }, - ], - }, - }); - registeredNodes.add(SPECIAL_NODE_TYPES.START); - } - - if (!registeredNodes.has(SPECIAL_NODE_TYPES.END)) { - Graph.registerNode(SPECIAL_NODE_TYPES.END, { - inherit: 'circle', - width: 40, - height: 40, - attrs: { - body: { - fill: '#ff4d4f', - stroke: '#ff4d4f', - strokeWidth: 2, - }, - label: { - text: '结束', - fill: '#fff', - fontSize: 12, - fontFamily: 'Arial, helvetica, sans-serif', - }, - }, - ports: { - groups: { - left: { - position: { - name: 'left', - args: { - dx: -4, - }, - }, - attrs: { - circle: { - r: 4, - magnet: true, - stroke: '#ff4d4f', - strokeWidth: 2, - fill: '#fff', - }, - }, - }, - }, - items: [ - { - id: 'left', - group: 'left', - }, - ], - }, - }); - registeredNodes.add(SPECIAL_NODE_TYPES.END); - } - - // 注册其他节点类型 - nodeTypes.forEach(nodeType => { - if (registeredNodes.has(nodeType.code)) return; - - let shape: string; - let width = 120; - let height = 60; - let attrs: any = { - body: { - fill: nodeType.color || '#fff', - stroke: '#1890ff', - strokeWidth: 2, - }, - label: { - text: nodeType.name, - fill: '#000', - fontSize: 12, - fontFamily: 'Arial, helvetica, sans-serif', - textWrap: { - width: -10, - height: -10, - ellipsis: true, - }, - }, - }; - - switch (nodeType.category) { - case 'BASIC': - shape = 'circle'; - width = 60; - height = 60; - break; - case 'TASK': - shape = 'rect'; - attrs.body.rx = 4; - attrs.body.ry = 4; - break; - case 'GATEWAY': - shape = 'polygon'; - attrs.body.refPoints = '0,10 10,0 20,10 10,20'; - width = 80; - height = 80; - break; - case 'EVENT': - shape = 'circle'; - width = 60; - height = 60; - break; - default: - shape = 'rect'; + const registerNodeTypes = (types: NodeTypeDTO[]) => { + types.forEach(nodeType => { + // 如果节点类型已经注册,则跳过 + if (registeredNodes.has(nodeType.code)) { + return; } - // 注册节点 + // 根据节点类型注册不同的节点 Graph.registerNode(nodeType.code, { - inherit: shape, - width, - height, - attrs, + inherit: nodeType.category === 'GATEWAY' ? 'polygon' : + nodeType.category === 'BASIC' ? 'circle' : 'rect', + width: nodeType.category === 'BASIC' ? 60 : 120, + height: nodeType.category === 'BASIC' ? 60 : 60, + attrs: { + body: { + fill: nodeType.color || '#fff', + stroke: nodeType.color || '#1890ff', + strokeWidth: 2, + ...(nodeType.category === 'GATEWAY' ? { + refPoints: '0,10 10,0 20,10 10,20', + } : nodeType.category === 'TASK' ? { + rx: 4, + ry: 4, + } : {}), + }, + label: { + text: nodeType.name, + fill: nodeType.category === 'BASIC' ? '#fff' : '#000', + fontSize: 12, + fontWeight: nodeType.category === 'BASIC' ? 'bold' : 'normal', + }, + }, ports: { groups: { - left: { - position: { - name: 'left', - args: { - dx: -4, - }, - }, + in: { + position: 'left', attrs: { circle: { r: 4, magnet: true, - stroke: '#1890ff', + stroke: nodeType.color || '#1890ff', strokeWidth: 2, fill: '#fff', }, }, - label: { - position: 'left', - }, }, - right: { - position: { - name: 'right', - args: { - dx: 4, - }, - }, + out: { + position: 'right', attrs: { circle: { r: 4, magnet: true, - stroke: '#1890ff', + stroke: nodeType.color || '#1890ff', strokeWidth: 2, fill: '#fff', }, }, - label: { - position: 'right', - }, }, }, - items: [ - { - id: 'left', - group: 'left', - }, - { - id: 'right', - group: 'right', - }, + items: nodeType.code === 'START' ? [ + { id: 'out', group: 'out' } + ] : nodeType.code === 'END' ? [ + { id: 'in', group: 'in' } + ] : [ + { id: 'in', group: 'in' }, + { id: 'out', group: 'out' } ], }, - markup: [ - { - tagName: 'rect', - selector: 'body', - }, - { - tagName: 'text', - selector: 'label', - }, - ], }); registeredNodes.add(nodeType.code); @@ -359,19 +120,20 @@ const FlowDesigner: React.FC = ({ }; loadNodeTypes(); + // 清理注册的节点类型 return () => { registeredNodes.clear(); }; }, []); - // 初始化流程设计器 + // 初始化画布 useEffect(() => { - if (!containerRef.current || !nodeTypes.length) { - return; - } + if (!containerRef.current || !nodeTypes.length) return; + // 注册节点类型 registerNodeTypes(nodeTypes); + // 创建画布 const graph = new Graph({ container: containerRef.current, width: 800, @@ -393,29 +155,15 @@ const FlowDesigner: React.FC = ({ ], }, connecting: { - router: { - name: 'manhattan', - args: { - padding: LAYOUT_CONFIG.PADDING, - startDirections: ['right'], - endDirections: ['left'], - }, - }, + router: 'manhattan', connector: { name: 'rounded', - args: { - radius: 8, - }, + args: { radius: 8 }, }, anchor: 'center', connectionPoint: 'anchor', allowBlank: false, - allowLoop: true, - allowNode: false, - allowEdge: false, - snap: { - radius: 20, - }, + snap: { radius: 20 }, createEdge() { return new Shape.Edge({ attrs: { @@ -429,29 +177,24 @@ const FlowDesigner: React.FC = ({ }, }, }, - zIndex: -1, - router: { - name: 'manhattan', - args: { - padding: LAYOUT_CONFIG.PADDING, - startDirections: ['right'], - endDirections: ['left'], - }, - }, - connector: { - name: 'rounded', - args: { - radius: 8, - }, - }, }); }, - validateConnection({ targetMagnet, targetView, sourceView, sourceMagnet }) { - if (!targetMagnet || !sourceMagnet) return false; - if (sourceMagnet.getAttribute('port-group') !== 'right' || - targetMagnet.getAttribute('port-group') !== 'left') return false; - if (targetView === sourceView && targetMagnet === sourceMagnet) return false; - return true; + validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) { + if (!sourceMagnet || !targetMagnet) return false; + + // 开始节点只能有出口连线 + if (sourceView?.cell.shape === 'START' && sourceMagnet.getAttribute('port-group') !== 'out') return false; + if (targetView?.cell.shape === 'START') return false; + + // 结束节点只能有入口连线 + if (sourceView?.cell.shape === 'END') return false; + if (targetView?.cell.shape === 'END' && targetMagnet.getAttribute('port-group') !== 'in') return false; + + // 普通节点的连线规则 + if (sourceMagnet.getAttribute('port-group') !== 'out') return false; + if (targetMagnet.getAttribute('port-group') !== 'in') return false; + + return sourceView !== targetView; }, }, highlighting: { @@ -466,150 +209,122 @@ const FlowDesigner: React.FC = ({ }, }, }, - mousewheel: { - enabled: true, - modifiers: ['ctrl', 'meta'], - factor: 1.1, - maxScale: 1.5, - minScale: 0.5, - }, interacting: { - nodeMovable: (view) => { - const node = view.cell; - // 开始和结束节点不允许移动 - if (node.shape === SPECIAL_NODE_TYPES.START || node.shape === SPECIAL_NODE_TYPES.END) { - return false; - } - return !readOnly; - }, + nodeMovable: !readOnly, edgeMovable: !readOnly, - edgeLabelMovable: !readOnly, - vertexMovable: !readOnly, - vertexAddable: !readOnly, - vertexDeletable: !readOnly, magnetConnectable: !readOnly, }, - scaling: { - min: 0.5, - max: 1.5, - }, background: { color: '#f5f5f5', }, - preventDefaultBlankAction: true, - preventDefaultContextMenu: true, - clickThreshold: 10, - magnetThreshold: 10, - translating: { - restrict: true, - }, }); + // 加载流程数据 if (value) { try { const graphData = JSON.parse(value); - - // 批量添加节点和边 - const model = { - nodes: graphData.nodes.map((node: any) => { - const isSpecialNode = node.type === 'START' || node.type === 'END'; - return { - id: node.id, - shape: isSpecialNode ? node.type : (NODE_TYPE_MAP[node.type] || node.type), - x: node.position?.x, - y: node.position?.y, - label: isSpecialNode ? (node.type === 'START' ? '开始' : '结束') : (node.data?.name || '未命名节点'), - data: { - nodeType: isSpecialNode ? node.type : (NODE_TYPE_MAP[node.type] || node.type), - ...node.data, - config: node.data?.config || {} - } - }; - }), - edges: graphData.edges.map((edge: any) => ({ - id: edge.id, - source: { - cell: edge.source, - port: 'right', - }, - target: { - cell: edge.target, - port: 'left', - }, - label: edge.data?.condition || '', - data: { - condition: edge.data?.condition || null - }, - router: { - name: 'manhattan', - args: { - padding: LAYOUT_CONFIG.PADDING, - startDirections: ['right'], - endDirections: ['left'], - }, - }, - connector: { - name: 'rounded', - args: { - radius: 8, - }, - }, - })) - }; + const cells: Cell[] = []; - graph.fromJSON(model); - - // 只在初始加载时应用布局 - layout(graph, true); + // 添加节点 + const nodesMap = new Map(); + if (graphData.nodes) { + graphData.nodes.forEach((node: any) => { + // 将 TASK 类型映射到 SHELL + const nodeShape = NODE_TYPE_MAP[node.type] || node.type; + const cell = graph.createNode({ + id: node.nodeId, + shape: nodeShape, + x: node.x || 100, + y: node.y || 100, + label: node.name || '未命名节点', + data: { + ...node, + config: node.config ? JSON.parse(node.config) : {}, + }, + }); + cells.push(cell); + nodesMap.set(node.nodeId, cell); + }); + } + + // 从 transitionConfig 中获取边的信息 + if (graphData.transitionConfig) { + try { + const transitionConfig = JSON.parse(graphData.transitionConfig); + if (transitionConfig.transitions) { + transitionConfig.transitions.forEach((transition: any) => { + const sourceNode = nodesMap.get(transition.from); + const targetNode = nodesMap.get(transition.to); + if (sourceNode && targetNode) { + cells.push(graph.createEdge({ + source: { cell: sourceNode.id, port: 'out' }, + target: { cell: targetNode.id, port: 'in' }, + attrs: { + line: { + stroke: '#1890ff', + strokeWidth: 2, + targetMarker: { + name: 'block', + width: 12, + height: 8, + }, + }, + }, + data: transition, + })); + } + }); + } + } catch (error) { + console.error('解析 transitionConfig 失败:', error); + } + } + + graph.resetCells(cells); + graph.centerContent(); } catch (error) { console.error('加载流程数据失败:', error); } } + // 监听事件 if (!readOnly) { let updateTimer: number | null = null; - + const updateGraph = () => { if (updateTimer) { window.clearTimeout(updateTimer); } - + updateTimer = window.setTimeout(() => { const nodes = graph.getNodes().map(node => ({ - id: node.id, - type: node.data?.nodeType || node.shape, - position: node.position(), - data: { - name: node.attr('label/text'), - description: node.data?.description, - config: node.data?.config || {} - } + nodeId: node.id, + name: node.attr('label/text'), + type: node.data?.type || node.shape, + x: node.position().x, + y: node.position().y, + config: JSON.stringify(node.data?.config || {}), })); const edges = graph.getEdges().map(edge => ({ - id: edge.id, - source: edge.getSourceCellId(), - target: edge.getTargetCellId(), - type: 'default', - data: { - condition: edge.data?.condition || null - } + from: edge.getSourceCellId(), + to: edge.getTargetCellId(), })); - onChange?.(JSON.stringify({ nodes, edges })); + const data = { + nodes, + transitionConfig: JSON.stringify({ + transitions: edges + }) + }; + + onChange?.(JSON.stringify(data)); }, 300); }; - // 监听节点变化 graph.on('node:moved', updateGraph); - graph.on('cell:changed', updateGraph); - graph.on('edge:connected', () => { - // 只在新增连线时应用布局,且仅当节点数量大于1时 - if (graph.getNodes().length > 1) { - layout(graph, false); - } - updateGraph(); - }); + graph.on('edge:connected', updateGraph); + graph.on('cell:removed', updateGraph); } graphRef.current = graph; @@ -620,7 +335,29 @@ const FlowDesigner: React.FC = ({ }, [nodeTypes, value, onChange, readOnly]); return ( -
+
+
+ + {nodeTypes.map(nodeType => ( +
{ + e.dataTransfer.setData('nodeType', JSON.stringify(nodeType)); + }} + > + {nodeType.name} +
+ ))} +
+
+
+
); };