正常启动

This commit is contained in:
戚辰先生 2024-12-05 13:59:20 +08:00
parent 1207d730db
commit e8c9a4c539
2 changed files with 195 additions and 512 deletions

View File

@ -1,86 +1,32 @@
.container { .flowDesigner {
width: 100%; display: flex;
height: 600px; gap: 16px;
background-color: #fff; height: 100%;
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
} }
.dndNode { .nodeTypes {
width: 100%; width: 200px;
height: 100%; flex-shrink: 0;
display: flex; }
align-items: center;
justify-content: center; .nodeType {
background-color: #fff; margin: 8px 0;
border: 1px solid #1890ff; padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
cursor: move; cursor: move;
user-select: none; user-select: none;
transition: all 0.3s;
} }
.dndNode:hover { .nodeType:hover {
background-color: #e6f7ff; opacity: 0.8;
border-color: #1890ff; transform: translateY(-1px);
} }
:global(.lf-node) { .canvas {
display: flex; flex: 1;
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;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
} border-radius: 4px;
: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) {
background: #f5f5f5; background: #f5f5f5;
border-color: #d9d9d9; min-height: 600px;
} }

View File

@ -1,33 +1,21 @@
import React, { useEffect, useRef, useState } from 'react'; 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 dagre from 'dagre';
import { getNodeTypes } from '@/pages/Workflow/service'; import { getNodeTypes } from '@/pages/Workflow/service';
import type { NodeTypeDTO } from '@/pages/Workflow/types'; import type { NodeTypeDTO } from '@/pages/Workflow/types';
import styles from './index.module.css'; import styles from './index.module.css';
import { Card } from 'antd';
// 节点类型映射 // 节点类型映射
const NODE_TYPE_MAP: Record<string, string> = { const NODE_TYPE_MAP: Record<string, string> = {
'TASK': 'SHELL' // 将 TASK 类型映射到 SHELL 类型 'TASK': 'SHELL', // 任务节点映射到 SHELL
}; 'START': 'START',
'END': 'END'
// 特殊节点类型
const SPECIAL_NODE_TYPES = {
START: 'START',
END: 'END'
}; };
// 记录已注册的节点类型 // 记录已注册的节点类型
const registeredNodes = new Set<string>(); const registeredNodes = new Set<string>();
// 布局配置
const LAYOUT_CONFIG = {
PADDING: 20,
NODE_MIN_DISTANCE: 100,
RANK_SEPARATION: 100,
NODE_SEPARATION: 80,
EDGE_SPACING: 20,
};
interface FlowDesignerProps { interface FlowDesignerProps {
value?: string; value?: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
@ -45,302 +33,75 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
const graphRef = useRef<Graph>(); const graphRef = useRef<Graph>();
const [nodeTypes, setNodeTypes] = useState<NodeTypeDTO[]>([]); const [nodeTypes, setNodeTypes] = useState<NodeTypeDTO[]>([]);
// 使用 dagre 布局算法 // 注册节点类型
const layout = (graph: Graph, force: boolean = false) => { const registerNodeTypes = (types: NodeTypeDTO[]) => {
const nodes = graph.getNodes(); types.forEach(nodeType => {
const edges = graph.getEdges(); // 如果节点类型已经注册,则跳过
if (registeredNodes.has(nodeType.code)) {
if (nodes.length === 0) return;
// 如果不是强制布局,且所有节点都有位置信息,则不进行自动布局
if (!force && nodes.every(node => {
const position = node.position();
return position.x !== undefined && position.y !== undefined;
})) {
return; return;
} }
const g = new dagre.graphlib.Graph(); // 根据节点类型注册不同的节点
g.setGraph({ Graph.registerNode(nodeType.code, {
rankdir: 'LR', inherit: nodeType.category === 'GATEWAY' ? 'polygon' :
nodesep: LAYOUT_CONFIG.NODE_SEPARATION, nodeType.category === 'BASIC' ? 'circle' : 'rect',
ranksep: LAYOUT_CONFIG.RANK_SEPARATION, width: nodeType.category === 'BASIC' ? 60 : 120,
edgesep: LAYOUT_CONFIG.EDGE_SPACING, height: nodeType.category === 'BASIC' ? 60 : 60,
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: { 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: { body: {
fill: nodeType.color || '#fff', fill: nodeType.color || '#fff',
stroke: '#1890ff', stroke: nodeType.color || '#1890ff',
strokeWidth: 2, strokeWidth: 2,
...(nodeType.category === 'GATEWAY' ? {
refPoints: '0,10 10,0 20,10 10,20',
} : nodeType.category === 'TASK' ? {
rx: 4,
ry: 4,
} : {}),
}, },
label: { label: {
text: nodeType.name, text: nodeType.name,
fill: '#000', fill: nodeType.category === 'BASIC' ? '#fff' : '#000',
fontSize: 12, fontSize: 12,
fontFamily: 'Arial, helvetica, sans-serif', fontWeight: nodeType.category === 'BASIC' ? 'bold' : 'normal',
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';
}
// 注册节点
Graph.registerNode(nodeType.code, {
inherit: shape,
width,
height,
attrs,
ports: { ports: {
groups: { groups: {
left: { in: {
position: {
name: 'left',
args: {
dx: -4,
},
},
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#1890ff',
strokeWidth: 2,
fill: '#fff',
},
},
label: {
position: 'left', position: 'left',
},
},
right: {
position: {
name: 'right',
args: {
dx: 4,
},
},
attrs: { attrs: {
circle: { circle: {
r: 4, r: 4,
magnet: true, magnet: true,
stroke: '#1890ff', stroke: nodeType.color || '#1890ff',
strokeWidth: 2, strokeWidth: 2,
fill: '#fff', fill: '#fff',
}, },
}, },
label: { },
out: {
position: 'right', position: 'right',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: nodeType.color || '#1890ff',
strokeWidth: 2,
fill: '#fff',
}, },
}, },
}, },
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); registeredNodes.add(nodeType.code);
@ -359,19 +120,20 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
}; };
loadNodeTypes(); loadNodeTypes();
// 清理注册的节点类型
return () => { return () => {
registeredNodes.clear(); registeredNodes.clear();
}; };
}, []); }, []);
// 初始化流程设计器 // 初始化画布
useEffect(() => { useEffect(() => {
if (!containerRef.current || !nodeTypes.length) { if (!containerRef.current || !nodeTypes.length) return;
return;
}
// 注册节点类型
registerNodeTypes(nodeTypes); registerNodeTypes(nodeTypes);
// 创建画布
const graph = new Graph({ const graph = new Graph({
container: containerRef.current, container: containerRef.current,
width: 800, width: 800,
@ -393,29 +155,15 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
], ],
}, },
connecting: { connecting: {
router: { router: 'manhattan',
name: 'manhattan',
args: {
padding: LAYOUT_CONFIG.PADDING,
startDirections: ['right'],
endDirections: ['left'],
},
},
connector: { connector: {
name: 'rounded', name: 'rounded',
args: { args: { radius: 8 },
radius: 8,
},
}, },
anchor: 'center', anchor: 'center',
connectionPoint: 'anchor', connectionPoint: 'anchor',
allowBlank: false, allowBlank: false,
allowLoop: true, snap: { radius: 20 },
allowNode: false,
allowEdge: false,
snap: {
radius: 20,
},
createEdge() { createEdge() {
return new Shape.Edge({ return new Shape.Edge({
attrs: { attrs: {
@ -429,29 +177,24 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
}, },
}, },
}, },
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 }) { validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) {
if (!targetMagnet || !sourceMagnet) return false; if (!sourceMagnet || !targetMagnet) return false;
if (sourceMagnet.getAttribute('port-group') !== 'right' ||
targetMagnet.getAttribute('port-group') !== 'left') return false; // 开始节点只能有出口连线
if (targetView === sourceView && targetMagnet === sourceMagnet) return false; if (sourceView?.cell.shape === 'START' && sourceMagnet.getAttribute('port-group') !== 'out') return false;
return true; 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: { highlighting: {
@ -466,106 +209,85 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
}, },
}, },
}, },
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
factor: 1.1,
maxScale: 1.5,
minScale: 0.5,
},
interacting: { interacting: {
nodeMovable: (view) => { nodeMovable: !readOnly,
const node = view.cell;
// 开始和结束节点不允许移动
if (node.shape === SPECIAL_NODE_TYPES.START || node.shape === SPECIAL_NODE_TYPES.END) {
return false;
}
return !readOnly;
},
edgeMovable: !readOnly, edgeMovable: !readOnly,
edgeLabelMovable: !readOnly,
vertexMovable: !readOnly,
vertexAddable: !readOnly,
vertexDeletable: !readOnly,
magnetConnectable: !readOnly, magnetConnectable: !readOnly,
}, },
scaling: {
min: 0.5,
max: 1.5,
},
background: { background: {
color: '#f5f5f5', color: '#f5f5f5',
}, },
preventDefaultBlankAction: true,
preventDefaultContextMenu: true,
clickThreshold: 10,
magnetThreshold: 10,
translating: {
restrict: true,
},
}); });
// 加载流程数据
if (value) { if (value) {
try { try {
const graphData = JSON.parse(value); const graphData = JSON.parse(value);
const cells: Cell[] = [];
// 批量添加节点和边 // 添加节点
const model = { const nodesMap = new Map<string, Cell>();
nodes: graphData.nodes.map((node: any) => { if (graphData.nodes) {
const isSpecialNode = node.type === 'START' || node.type === 'END'; graphData.nodes.forEach((node: any) => {
return { // 将 TASK 类型映射到 SHELL
id: node.id, const nodeShape = NODE_TYPE_MAP[node.type] || node.type;
shape: isSpecialNode ? node.type : (NODE_TYPE_MAP[node.type] || node.type), const cell = graph.createNode({
x: node.position?.x, id: node.nodeId,
y: node.position?.y, shape: nodeShape,
label: isSpecialNode ? (node.type === 'START' ? '开始' : '结束') : (node.data?.name || '未命名节点'), x: node.x || 100,
y: node.y || 100,
label: node.name || '未命名节点',
data: { data: {
nodeType: isSpecialNode ? node.type : (NODE_TYPE_MAP[node.type] || node.type), ...node,
...node.data, config: node.config ? JSON.parse(node.config) : {},
config: node.data?.config || {} },
});
cells.push(cell);
nodesMap.set(node.nodeId, cell);
});
} }
};
}),
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,
},
},
}))
};
graph.fromJSON(model); // 从 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);
layout(graph, true); graph.centerContent();
} catch (error) { } catch (error) {
console.error('加载流程数据失败:', error); console.error('加载流程数据失败:', error);
} }
} }
// 监听事件
if (!readOnly) { if (!readOnly) {
let updateTimer: number | null = null; let updateTimer: number | null = null;
@ -576,40 +298,33 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
updateTimer = window.setTimeout(() => { updateTimer = window.setTimeout(() => {
const nodes = graph.getNodes().map(node => ({ const nodes = graph.getNodes().map(node => ({
id: node.id, nodeId: node.id,
type: node.data?.nodeType || node.shape,
position: node.position(),
data: {
name: node.attr('label/text'), name: node.attr('label/text'),
description: node.data?.description, type: node.data?.type || node.shape,
config: node.data?.config || {} x: node.position().x,
} y: node.position().y,
config: JSON.stringify(node.data?.config || {}),
})); }));
const edges = graph.getEdges().map(edge => ({ const edges = graph.getEdges().map(edge => ({
id: edge.id, from: edge.getSourceCellId(),
source: edge.getSourceCellId(), to: edge.getTargetCellId(),
target: edge.getTargetCellId(),
type: 'default',
data: {
condition: edge.data?.condition || null
}
})); }));
onChange?.(JSON.stringify({ nodes, edges })); const data = {
nodes,
transitionConfig: JSON.stringify({
transitions: edges
})
};
onChange?.(JSON.stringify(data));
}, 300); }, 300);
}; };
// 监听节点变化
graph.on('node:moved', updateGraph); graph.on('node:moved', updateGraph);
graph.on('cell:changed', updateGraph); graph.on('edge:connected', updateGraph);
graph.on('edge:connected', () => { graph.on('cell:removed', updateGraph);
// 只在新增连线时应用布局且仅当节点数量大于1时
if (graph.getNodes().length > 1) {
layout(graph, false);
}
updateGraph();
});
} }
graphRef.current = graph; graphRef.current = graph;
@ -620,7 +335,29 @@ const FlowDesigner: React.FC<FlowDesignerProps> = ({
}, [nodeTypes, value, onChange, readOnly]); }, [nodeTypes, value, onChange, readOnly]);
return ( return (
<div ref={containerRef} className={styles.container} /> <div className={styles.flowDesigner}>
<div className={styles.nodeTypes}>
<Card title="节点类型" size="small" bordered={false}>
{nodeTypes.map(nodeType => (
<div
key={nodeType.code}
className={styles.nodeType}
style={{
backgroundColor: nodeType.color,
color: nodeType.category === 'BASIC' ? '#fff' : '#000'
}}
draggable
onDragStart={(e) => {
e.dataTransfer.setData('nodeType', JSON.stringify(nodeType));
}}
>
{nodeType.name}
</div>
))}
</Card>
</div>
<div className={styles.canvas} ref={containerRef} />
</div>
); );
}; };