正常启动
This commit is contained in:
parent
1207d730db
commit
e8c9a4c539
@ -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;
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user