From ca749aa7af438e51789c4768db1b4a2a5b6c0eaa Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 12 Dec 2024 18:21:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/Definition/Design/constants.ts | 155 ++++++++++++ .../Workflow/Definition/Design/index.tsx | 224 ++++++++---------- 2 files changed, 249 insertions(+), 130 deletions(-) create mode 100644 frontend/src/pages/Workflow/Definition/Design/constants.ts diff --git a/frontend/src/pages/Workflow/Definition/Design/constants.ts b/frontend/src/pages/Workflow/Definition/Design/constants.ts new file mode 100644 index 00000000..481ab497 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Design/constants.ts @@ -0,0 +1,155 @@ +// 节点端口配置 +export const PORT_GROUPS = ['top', 'right', 'bottom', 'left'] as const; + +// 默认样式配置 +export const DEFAULT_STYLES = { + node: { + stroke: '#5F95FF', + strokeWidth: 2, + fill: '#FFF', + }, + label: { + fontSize: 12, + fill: '#000000', + }, + port: { + r: 4, + magnet: true, + stroke: '#5F95FF', + strokeWidth: 1, + fill: '#fff', + style: { + visibility: 'hidden', + }, + }, + edge: { + stroke: '#1890ff', + strokeWidth: 2, + targetMarker: { + name: 'classic', + size: 8, + }, + }, +} as const; + +// 默认节点大小 +export const NODE_SIZES = { + circle: { + width: 60, + height: 60, + }, + rectangle: { + width: 100, + height: 50, + }, + diamond: { + width: 60, + height: 60, + }, +} as const; + +// 生成端口组配置 +export const generatePortGroups = () => { + const groups: Record = {}; + + PORT_GROUPS.forEach(position => { + groups[position] = { + position, + attrs: { + circle: { ...DEFAULT_STYLES.port }, + }, + }; + }); + + return groups; +}; + +// 生成端口项配置 +export const generatePortItems = () => + PORT_GROUPS.map(group => ({ group })); + +// 网格配置 +export const GRID_CONFIG = { + size: 10, + visible: true, + type: 'dot', + args: { + color: '#a0a0a0', + thickness: 1, + }, +} as const; + +// 连接配置 +export const CONNECTING_CONFIG = { + router: 'manhattan', + connector: { + name: 'rounded', + args: { + radius: 8, + }, + }, + anchor: 'center', + connectionPoint: 'anchor', + allowBlank: false, + snap: true, +} as const; + +// 高亮配置 +export const HIGHLIGHTING_CONFIG = { + magnetAvailable: { + name: 'stroke', + args: { + padding: 4, + attrs: { + strokeWidth: 4, + stroke: '#52c41a', + }, + }, + }, +} as const; + +// 节点注册配置 +export const NODE_REGISTRY_CONFIG = { + circle: { + inherit: 'circle', + width: NODE_SIZES.circle.width, + height: NODE_SIZES.circle.height, + attrs: { + body: DEFAULT_STYLES.node, + label: DEFAULT_STYLES.label, + }, + ports: { + groups: generatePortGroups(), + items: generatePortItems(), + }, + }, + rectangle: { + inherit: 'rect', + width: NODE_SIZES.rectangle.width, + height: NODE_SIZES.rectangle.height, + attrs: { + body: DEFAULT_STYLES.node, + label: DEFAULT_STYLES.label, + }, + ports: { + groups: generatePortGroups(), + items: generatePortItems(), + }, + }, + diamond: { + inherit: 'polygon', + width: NODE_SIZES.diamond.width, + height: NODE_SIZES.diamond.height, + attrs: { + body: { + ...DEFAULT_STYLES.node, + refPoints: '0,10 10,0 20,10 10,20', + }, + label: DEFAULT_STYLES.label, + }, + ports: { + groups: generatePortGroups(), + items: generatePortItems(), + }, + }, +} as const; diff --git a/frontend/src/pages/Workflow/Definition/Design/index.tsx b/frontend/src/pages/Workflow/Definition/Design/index.tsx index 86d2b187..ecb90257 100644 --- a/frontend/src/pages/Workflow/Definition/Design/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Design/index.tsx @@ -2,10 +2,17 @@ import React, { useEffect, useState, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Button, Space, Card, Row, Col } from 'antd'; import { ArrowLeftOutlined, SaveOutlined, PlayCircleOutlined } from '@ant-design/icons'; -import { Graph } from '@antv/x6'; +import { Graph, Shape } from '@antv/x6'; import { getDefinitionDetail } from '../service'; import NodePanel from './components/NodePanel'; import { NodeDefinition } from './types'; +import { + NODE_REGISTRY_CONFIG, + GRID_CONFIG, + CONNECTING_CONFIG, + HIGHLIGHTING_CONFIG, + DEFAULT_STYLES, generatePortGroups, generatePortItems, +} from './constants'; const WorkflowDesign: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -22,52 +29,47 @@ const WorkflowDesign: React.FC = () => { useEffect(() => { if (graphContainerRef.current) { + // 注册自定义节点 + Object.entries(NODE_REGISTRY_CONFIG).forEach(([name, config]) => { + Graph.registerNode(name, config, true); + }); + const graph = new Graph({ container: graphContainerRef.current, - grid: { - size: 10, - visible: true, - type: 'dot', - args: { - color: '#a0a0a0', - thickness: 1, - }, - }, + grid: GRID_CONFIG, connecting: { - router: 'manhattan', - connector: { - name: 'rounded', - args: { - radius: 8, - }, + ...CONNECTING_CONFIG, + validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) { + if (!sourceMagnet || !targetMagnet) { + return false; + } + if (sourceView === targetView) { + return false; + } + return true; + }, + validateMagnet({ magnet }) { + const portGroup = magnet?.getAttribute('port-group'); + return !!portGroup; }, - anchor: 'center', - connectionPoint: 'anchor', - allowBlank: false, - snap: true, createEdge() { return this.createEdge({ attrs: { - line: { - stroke: '#1890ff', - strokeWidth: 2, - targetMarker: { - name: 'classic', - size: 8, - }, - }, + line: DEFAULT_STYLES.edge, }, + zIndex: -1, }); }, }, highlighting: { - magnetAvailable: { + ...HIGHLIGHTING_CONFIG, + magnetAdsorbed: { name: 'stroke', args: { - padding: 4, attrs: { + fill: '#fff', + stroke: '#31d0c6', strokeWidth: 4, - stroke: '#52c41a', }, }, }, @@ -82,6 +84,21 @@ const WorkflowDesign: React.FC = () => { }, }); + // 显示/隐藏连接桩 + graph.on('node:mouseenter', ({ node }) => { + const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`); + ports.forEach((port) => { + port.setAttribute('style', 'visibility: visible'); + }); + }); + + 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'); + }); + }); + setGraph(graph); return () => { @@ -93,115 +110,60 @@ const WorkflowDesign: React.FC = () => { const loadDefinitionDetail = async () => { try { const response = await getDefinitionDetail(id); - if (response.success) { - setTitle(`工作流设计 - ${response.data.name}`); - } + setTitle(`工作流设计 - ${response.name}`); } catch (error) { console.error('Failed to load workflow definition:', error); } }; const handleNodeDragStart = (node: NodeDefinition, e: React.DragEvent) => { - if (graph) { - const { clientX, clientY } = e; - const point = graph.clientToLocal({ x: clientX, y: clientY }); - - const nodeConfig = { - shape: node.graphConfig.uiSchema.shape, - width: node.graphConfig.uiSchema.size.width, - height: node.graphConfig.uiSchema.size.height, - attrs: { - body: { - fill: node.graphConfig.uiSchema.style.fill, - stroke: node.graphConfig.uiSchema.style.stroke, - strokeWidth: node.graphConfig.uiSchema.style.strokeWidth, - }, - label: { - text: node.name, - fill: '#000000', - fontSize: 12, - }, - }, - ports: { - groups: { - top: { - position: 'top', - attrs: { - circle: { - r: 4, - magnet: true, - stroke: '#5F95FF', - strokeWidth: 1, - fill: '#fff', - style: { - visibility: 'hidden', - }, - }, - }, - }, - right: { - position: 'right', - attrs: { - circle: { - r: 4, - magnet: true, - stroke: '#5F95FF', - strokeWidth: 1, - fill: '#fff', - style: { - visibility: 'hidden', - }, - }, - }, - }, - bottom: { - position: 'bottom', - attrs: { - circle: { - r: 4, - magnet: true, - stroke: '#5F95FF', - strokeWidth: 1, - fill: '#fff', - style: { - visibility: 'hidden', - }, - }, - }, - }, - left: { - position: 'left', - attrs: { - circle: { - r: 4, - magnet: true, - stroke: '#5F95FF', - strokeWidth: 1, - fill: '#fff', - style: { - visibility: 'hidden', - }, - }, - }, - }, - }, - items: [ - { group: 'top' }, - { group: 'right' }, - { group: 'bottom' }, - { group: 'left' }, - ], - }, - }; + e.dataTransfer.setData('node', JSON.stringify(node)); + }; - graph.addNode({ - ...nodeConfig, - x: point.x, - y: point.y, - }); + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + if (graph) { + const nodeData = e.dataTransfer.getData('node'); + if (nodeData) { + const node = JSON.parse(nodeData); + const { clientX, clientY } = e; + const point = graph.clientToLocal({ x: clientX, y: clientY }); + + const nodeConfig = { + shape: node.graphConfig.uiSchema.shape, + width: node.graphConfig.uiSchema.size.width, + height: node.graphConfig.uiSchema.size.height, + attrs: { + body: { + fill: node.graphConfig.uiSchema.style.fill, + stroke: node.graphConfig.uiSchema.style.stroke, + strokeWidth: node.graphConfig.uiSchema.style.strokeWidth, + }, + label: { + text: node.name, + fill: '#000000', + fontSize: 12, + }, + }, + ports: { + groups: generatePortGroups(), + items: generatePortItems(), + }, + }; + + graph.addNode({ + ...nodeConfig, + x: point.x, + y: point.y, + }); + } } }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + return (
{ width: '100%', height: '100%', }} + onDrop={handleDrop} + onDragOver={handleDragOver} />