增加工具栏提示。

This commit is contained in:
dengqichen 2024-12-12 18:21:15 +08:00
parent 93d47eab40
commit ca749aa7af
2 changed files with 249 additions and 130 deletions

View File

@ -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<string, any> = {};
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;

View File

@ -2,10 +2,17 @@ import React, { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Button, Space, Card, Row, Col } from 'antd'; import { Button, Space, Card, Row, Col } from 'antd';
import { ArrowLeftOutlined, SaveOutlined, PlayCircleOutlined } from '@ant-design/icons'; import { ArrowLeftOutlined, SaveOutlined, PlayCircleOutlined } from '@ant-design/icons';
import { Graph } from '@antv/x6'; import { Graph, Shape } from '@antv/x6';
import { getDefinitionDetail } from '../service'; import { getDefinitionDetail } from '../service';
import NodePanel from './components/NodePanel'; import NodePanel from './components/NodePanel';
import { NodeDefinition } from './types'; 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 WorkflowDesign: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@ -22,52 +29,47 @@ const WorkflowDesign: React.FC = () => {
useEffect(() => { useEffect(() => {
if (graphContainerRef.current) { if (graphContainerRef.current) {
// 注册自定义节点
Object.entries(NODE_REGISTRY_CONFIG).forEach(([name, config]) => {
Graph.registerNode(name, config, true);
});
const graph = new Graph({ const graph = new Graph({
container: graphContainerRef.current, container: graphContainerRef.current,
grid: { grid: GRID_CONFIG,
size: 10,
visible: true,
type: 'dot',
args: {
color: '#a0a0a0',
thickness: 1,
},
},
connecting: { connecting: {
router: 'manhattan', ...CONNECTING_CONFIG,
connector: { validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) {
name: 'rounded', if (!sourceMagnet || !targetMagnet) {
args: { return false;
radius: 8, }
}, 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() { createEdge() {
return this.createEdge({ return this.createEdge({
attrs: { attrs: {
line: { line: DEFAULT_STYLES.edge,
stroke: '#1890ff',
strokeWidth: 2,
targetMarker: {
name: 'classic',
size: 8,
},
},
}, },
zIndex: -1,
}); });
}, },
}, },
highlighting: { highlighting: {
magnetAvailable: { ...HIGHLIGHTING_CONFIG,
magnetAdsorbed: {
name: 'stroke', name: 'stroke',
args: { args: {
padding: 4,
attrs: { attrs: {
fill: '#fff',
stroke: '#31d0c6',
strokeWidth: 4, 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); setGraph(graph);
return () => { return () => {
@ -93,115 +110,60 @@ const WorkflowDesign: React.FC = () => {
const loadDefinitionDetail = async () => { const loadDefinitionDetail = async () => {
try { try {
const response = await getDefinitionDetail(id); const response = await getDefinitionDetail(id);
if (response.success) { setTitle(`工作流设计 - ${response.name}`);
setTitle(`工作流设计 - ${response.data.name}`);
}
} catch (error) { } catch (error) {
console.error('Failed to load workflow definition:', error); console.error('Failed to load workflow definition:', error);
} }
}; };
const handleNodeDragStart = (node: NodeDefinition, e: React.DragEvent) => { const handleNodeDragStart = (node: NodeDefinition, e: React.DragEvent) => {
e.dataTransfer.setData('node', JSON.stringify(node));
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (graph) { if (graph) {
const { clientX, clientY } = e; const nodeData = e.dataTransfer.getData('node');
const point = graph.clientToLocal({ x: clientX, y: clientY }); if (nodeData) {
const node = JSON.parse(nodeData);
const { clientX, clientY } = e;
const point = graph.clientToLocal({ x: clientX, y: clientY });
const nodeConfig = { const nodeConfig = {
shape: node.graphConfig.uiSchema.shape, shape: node.graphConfig.uiSchema.shape,
width: node.graphConfig.uiSchema.size.width, width: node.graphConfig.uiSchema.size.width,
height: node.graphConfig.uiSchema.size.height, height: node.graphConfig.uiSchema.size.height,
attrs: { attrs: {
body: { body: {
fill: node.graphConfig.uiSchema.style.fill, fill: node.graphConfig.uiSchema.style.fill,
stroke: node.graphConfig.uiSchema.style.stroke, stroke: node.graphConfig.uiSchema.style.stroke,
strokeWidth: node.graphConfig.uiSchema.style.strokeWidth, 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: { label: {
position: 'right', text: node.name,
attrs: { fill: '#000000',
circle: { fontSize: 12,
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: [ ports: {
{ group: 'top' }, groups: generatePortGroups(),
{ group: 'right' }, items: generatePortItems(),
{ group: 'bottom' }, },
{ group: 'left' }, };
],
},
};
graph.addNode({ graph.addNode({
...nodeConfig, ...nodeConfig,
x: point.x, x: point.x,
y: point.y, y: point.y,
}); });
}
} }
}; };
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
return ( return (
<div style={{ padding: '24px' }}> <div style={{ padding: '24px' }}>
<Card <Card
@ -253,6 +215,8 @@ const WorkflowDesign: React.FC = () => {
width: '100%', width: '100%',
height: '100%', height: '100%',
}} }}
onDrop={handleDrop}
onDragOver={handleDragOver}
/> />
</Card> </Card>
</Col> </Col>