1317 lines
47 KiB
TypeScript
1317 lines
47 KiB
TypeScript
import React, {useEffect, useState, useRef} from 'react';
|
||
import {createRoot} from 'react-dom/client';
|
||
import {useParams, useNavigate} from 'react-router-dom';
|
||
import {Button, Space, Card, Row, Col, message, Modal, Collapse, Tooltip, Dropdown} from 'antd';
|
||
import {
|
||
ArrowLeftOutlined,
|
||
SaveOutlined,
|
||
PlayCircleOutlined,
|
||
CopyOutlined,
|
||
DeleteOutlined,
|
||
UndoOutlined,
|
||
RedoOutlined,
|
||
ScissorOutlined,
|
||
SnippetsOutlined,
|
||
SelectOutlined,
|
||
ZoomInOutlined,
|
||
ZoomOutOutlined,
|
||
} from '@ant-design/icons';
|
||
import {Graph, Cell} from '@antv/x6';
|
||
import '@antv/x6-plugin-snapline';
|
||
import '@antv/x6-plugin-selection';
|
||
import '@antv/x6-plugin-keyboard';
|
||
import '@antv/x6-plugin-history';
|
||
import '@antv/x6-plugin-clipboard';
|
||
import '@antv/x6-plugin-transform';
|
||
import {Selection} from '@antv/x6-plugin-selection';
|
||
import {MiniMap} from '@antv/x6-plugin-minimap';
|
||
import {Clipboard} from '@antv/x6-plugin-clipboard';
|
||
import {History} from '@antv/x6-plugin-history';
|
||
import {Transform} from '@antv/x6-plugin-transform';
|
||
import {getDefinitionDetail, saveDefinition} from '../service';
|
||
import {getNodeDefinitionList} from './service';
|
||
import NodePanel from './components/NodePanel';
|
||
import NodeConfigDrawer from './components/NodeConfigModal';
|
||
import {NodeDefinition} from './types';
|
||
import {validateWorkflow} from './utils/validator';
|
||
import {addNodeToGraph} from './utils/nodeUtils';
|
||
import {applyAutoLayout} from './utils/layoutUtils';
|
||
import {
|
||
GRID_CONFIG,
|
||
CONNECTING_CONFIG,
|
||
HIGHLIGHTING_CONFIG,
|
||
DEFAULT_STYLES,
|
||
} from './constants';
|
||
import './index.less';
|
||
import {NodeDefinitionResponse, NodeDesignDataResponse} from "@/pages/Workflow/NodeDesign/types";
|
||
|
||
const WorkflowDesign: React.FC = () => {
|
||
const {id} = useParams<{ id: string }>();
|
||
const navigate = useNavigate();
|
||
const [title, setTitle] = useState<string>('工作流设计');
|
||
const graphContainerRef = useRef<HTMLDivElement>(null);
|
||
const minimapContainerRef = useRef<HTMLDivElement>(null);
|
||
const [graph, setGraph] = useState<Graph | null>(null);
|
||
const [selectedNode, setSelectedNode] = useState<Cell | null>(null);
|
||
const [selectedNodeDefinition, setSelectedNodeDefinition] = useState<NodeDefinitionResponse | null>(null);
|
||
const [configModalVisible, setConfigModalVisible] = useState(false);
|
||
const [definitionData, setDefinitionData] = useState<any>(null);
|
||
const [nodeDefinitions, setNodeDefinitions] = useState<NodeDefinitionResponse[]>([]);
|
||
const [isNodeDefinitionsLoaded, setIsNodeDefinitionsLoaded] = useState(false);
|
||
const [forceUpdate, setForceUpdate] = useState(false);
|
||
const [scale, setScale] = useState(1);
|
||
|
||
// 初始化图形
|
||
const initGraph = () => {
|
||
if (!graphContainerRef.current) return;
|
||
|
||
// 获取主画布容器尺寸
|
||
const container = graphContainerRef.current;
|
||
const containerWidth = container.clientWidth;
|
||
const containerHeight = container.clientHeight;
|
||
|
||
// 计算主画布和小地图的尺寸比例
|
||
const MINIMAP_BASE_WIDTH = 200;
|
||
const minimapWidth = MINIMAP_BASE_WIDTH;
|
||
const minimapHeight = Math.round((MINIMAP_BASE_WIDTH * containerHeight) / containerWidth);
|
||
|
||
// 计算缩放比例
|
||
const scale = minimapWidth / containerWidth;
|
||
|
||
// 将尺寸信息保存到样式变量中
|
||
document.documentElement.style.setProperty('--minimap-width', `${minimapWidth}px`);
|
||
document.documentElement.style.setProperty('--minimap-height', `${minimapHeight}px`);
|
||
|
||
const graph = new Graph({
|
||
container: graphContainerRef.current,
|
||
grid: {
|
||
size: 10,
|
||
visible: true,
|
||
type: 'dot',
|
||
args: {
|
||
color: '#a0a0a0',
|
||
thickness: 1,
|
||
},
|
||
},
|
||
connecting: {
|
||
snap: true, // 连线时自动吸附
|
||
allowBlank: false, // 禁止连线到空白位置
|
||
allowLoop: false, // 禁止自环
|
||
allowNode: false, // 禁止连接到节点(只允许连接到连接桩)
|
||
allowEdge: false, // 禁止边连接到边
|
||
connector: {
|
||
name: 'rounded',
|
||
args: {
|
||
radius: 8
|
||
}
|
||
},
|
||
router: {
|
||
name: 'manhattan',
|
||
args: {
|
||
padding: 1
|
||
}
|
||
},
|
||
validateConnection({sourceCell, targetCell, sourceMagnet, targetMagnet}) {
|
||
if (sourceCell === targetCell) {
|
||
return false;
|
||
}
|
||
if (!sourceMagnet || !targetMagnet) {
|
||
return false;
|
||
}
|
||
// 检查是否已存在相同的连接
|
||
const edges = graph.getEdges();
|
||
const exists = edges.some(edge => {
|
||
const source = edge.getSource();
|
||
const target = edge.getTarget();
|
||
return (
|
||
source.cell === sourceCell.id &&
|
||
target.cell === targetCell.id &&
|
||
source.port === sourceMagnet.getAttribute('port') &&
|
||
target.port === targetMagnet.getAttribute('port')
|
||
);
|
||
});
|
||
return !exists;
|
||
},
|
||
validateMagnet({magnet}) {
|
||
return magnet.getAttribute('port-group') !== 'top';
|
||
},
|
||
validateEdge({edge}) {
|
||
return true;
|
||
}
|
||
},
|
||
highlighting: {
|
||
magnetAvailable: {
|
||
name: 'stroke',
|
||
args: {
|
||
padding: 4,
|
||
attrs: {
|
||
strokeWidth: 4,
|
||
stroke: '#52c41a',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
clipboard: {
|
||
enabled: true,
|
||
},
|
||
selecting: {
|
||
enabled: true,
|
||
multiple: true,
|
||
rubberband: true,
|
||
movable: true,
|
||
showNodeSelectionBox: false, // 禁用节点选择框
|
||
showEdgeSelectionBox: false, // 禁用边选择框
|
||
selectNodeOnMoved: false,
|
||
selectEdgeOnMoved: false,
|
||
},
|
||
snapline: true,
|
||
keyboard: {
|
||
enabled: true,
|
||
},
|
||
panning: {
|
||
enabled: true,
|
||
eventTypes: ['rightMouseDown'], // 右键按下时启用画布拖拽
|
||
},
|
||
mousewheel: {
|
||
enabled: true,
|
||
modifiers: ['ctrl', 'meta'],
|
||
minScale: 0.2,
|
||
maxScale: 2,
|
||
},
|
||
});
|
||
|
||
// 注册插件
|
||
const history = new History({
|
||
enabled: true,
|
||
beforeAddCommand(event: any, args: any) {
|
||
return true;
|
||
},
|
||
afterExecuteCommand: () => {
|
||
// 强制更新组件状态
|
||
setForceUpdate(prev => !prev);
|
||
},
|
||
afterUndo: () => {
|
||
// 强制更新组件状态
|
||
setForceUpdate(prev => !prev);
|
||
},
|
||
afterRedo: () => {
|
||
// 强制更新组件状态
|
||
setForceUpdate(prev => !prev);
|
||
},
|
||
});
|
||
|
||
// 初始化Selection插件
|
||
const selection = new Selection({
|
||
enabled: true,
|
||
multiple: true,
|
||
rubberband: true,
|
||
movable: true,
|
||
showNodeSelectionBox: false, // 禁用节点选择框
|
||
showEdgeSelectionBox: false, // 禁用边选择框
|
||
selectNodeOnMoved: false,
|
||
selectEdgeOnMoved: false,
|
||
multipleSelectionModifiers: ['ctrl', 'meta'],
|
||
showEdgeSelectionBox: false,
|
||
showAnchorSelectionBox: false,
|
||
pointerEvents: 'auto'
|
||
});
|
||
graph.use(selection);
|
||
graph.use(new MiniMap({
|
||
container: minimapContainerRef.current!,
|
||
width: minimapWidth,
|
||
height: minimapHeight,
|
||
padding: 5,
|
||
scalable: false,
|
||
minScale: scale * 0.8, // 稍微缩小一点以显示更多内容
|
||
maxScale: scale * 1.2,
|
||
graphOptions: {
|
||
connecting: {
|
||
connector: 'rounded',
|
||
connectionPoint: 'anchor',
|
||
router: {
|
||
name: 'manhattan',
|
||
},
|
||
},
|
||
async: true,
|
||
frozen: true,
|
||
interacting: false,
|
||
grid: false
|
||
},
|
||
viewport: {
|
||
padding: 0,
|
||
fitToContent: false, // 禁用自动适配内容
|
||
initialPosition: { // 初始位置与主画布保持一致
|
||
x: 0,
|
||
y: 0
|
||
},
|
||
initialScale: scale
|
||
}
|
||
}));
|
||
graph.use(new Clipboard());
|
||
graph.use(history);
|
||
graph.use(
|
||
new Transform({
|
||
resizing: false,
|
||
rotating: false,
|
||
})
|
||
);
|
||
|
||
// 扩展 graph 对象,添加 history 属性
|
||
(graph as any).history = history;
|
||
|
||
// 监听图形变化
|
||
graph.on('cell:added', ({cell}) => {
|
||
const canUndo = history.canUndo();
|
||
const canRedo = history.canRedo();
|
||
// 强制更新组件状态
|
||
setForceUpdate(prev => !prev);
|
||
});
|
||
|
||
graph.on('cell:removed', ({cell}) => {
|
||
const canUndo = history.canUndo();
|
||
const canRedo = history.canRedo();
|
||
console.log('Cell removed:', {
|
||
cell,
|
||
canUndo,
|
||
canRedo,
|
||
stackSize: history.stackSize,
|
||
});
|
||
// 强制更新组件状态
|
||
setForceUpdate(prev => !prev);
|
||
});
|
||
|
||
graph.on('cell:changed', ({cell, options}) => {
|
||
const canUndo = history.canUndo();
|
||
const canRedo = history.canRedo();
|
||
// 强制更新组件状态
|
||
setForceUpdate(prev => !prev);
|
||
});
|
||
|
||
// 监听主画布变化,同步更新小地图
|
||
graph.on('transform', () => {
|
||
const minimap = graph.getPlugin('minimap') as MiniMap;
|
||
if (minimap) {
|
||
const mainViewport = graph.getView().getVisibleArea();
|
||
minimap.viewport.scale = scale;
|
||
minimap.viewport.center(mainViewport.center);
|
||
}
|
||
});
|
||
|
||
// 处理连线重新连接
|
||
graph.on('edge:moved', ({edge, terminal, previous}) => {
|
||
if (!edge || !terminal) return;
|
||
|
||
const isSource = terminal.type === 'source';
|
||
const source = isSource ? terminal : edge.getSource();
|
||
const target = isSource ? edge.getTarget() : terminal;
|
||
|
||
if (source && target) {
|
||
// 移除旧的连线
|
||
edge.remove();
|
||
|
||
// 创建新的连线
|
||
graph.addEdge({
|
||
source: {
|
||
cell: source.cell,
|
||
port: source.port,
|
||
},
|
||
target: {
|
||
cell: target.cell,
|
||
port: target.port,
|
||
},
|
||
attrs: {
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 2,
|
||
targetMarker: {
|
||
name: 'classic',
|
||
size: 7,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
});
|
||
|
||
// 处理连线更改
|
||
graph.on('edge:change:source edge:change:target', ({edge, current, previous}) => {
|
||
if (edge && current) {
|
||
edge.setAttrs({
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 2,
|
||
targetMarker: {
|
||
name: 'classic',
|
||
size: 7,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
});
|
||
|
||
registerEventHandlers(graph);
|
||
|
||
return graph;
|
||
};
|
||
|
||
// 处理撤销操作
|
||
const handleUndo = () => {
|
||
if (!graph) return;
|
||
const history = (graph as any).history;
|
||
if (!history) {
|
||
console.error('History plugin not initialized');
|
||
return;
|
||
}
|
||
|
||
const beforeState = {
|
||
canUndo: history.canUndo(),
|
||
canRedo: history.canRedo(),
|
||
stackSize: history.stackSize,
|
||
};
|
||
|
||
if (history.canUndo()) {
|
||
history.undo();
|
||
const afterState = {
|
||
canUndo: history.canUndo(),
|
||
canRedo: history.canRedo(),
|
||
stackSize: history.stackSize,
|
||
};
|
||
console.log('Undo operation:', {
|
||
before: beforeState,
|
||
after: afterState,
|
||
});
|
||
message.success('已撤销');
|
||
} else {
|
||
message.info('没有可撤销的操作');
|
||
}
|
||
};
|
||
|
||
// 处理重做操作
|
||
const handleRedo = () => {
|
||
if (!graph) return;
|
||
const history = (graph as any).history;
|
||
if (!history) {
|
||
console.error('History plugin not initialized');
|
||
return;
|
||
}
|
||
|
||
const beforeState = {
|
||
canUndo: history.canUndo(),
|
||
canRedo: history.canRedo(),
|
||
stackSize: history.stackSize,
|
||
};
|
||
|
||
if (history.canRedo()) {
|
||
history.redo();
|
||
const afterState = {
|
||
canUndo: history.canUndo(),
|
||
canRedo: history.canRedo(),
|
||
stackSize: history.stackSize,
|
||
};
|
||
console.log('Redo operation:', {
|
||
before: beforeState,
|
||
after: afterState,
|
||
});
|
||
message.success('已重做');
|
||
} else {
|
||
message.info('没有可重做的操作');
|
||
}
|
||
};
|
||
|
||
// 处理缩放
|
||
const handleZoom = (delta: number) => {
|
||
if (!graph) return;
|
||
const currentScale = graph.scale();
|
||
const newScale = Math.max(0.2, Math.min(2, currentScale.sx + delta));
|
||
graph.scale(newScale, newScale);
|
||
setScale(newScale);
|
||
};
|
||
|
||
// 放大
|
||
const handleZoomIn = () => {
|
||
handleZoom(0.1);
|
||
};
|
||
|
||
// 缩小
|
||
const handleZoomOut = () => {
|
||
handleZoom(-0.1);
|
||
};
|
||
|
||
// 注册事件处理器
|
||
const registerEventHandlers = (graph: Graph) => {
|
||
// 定义悬停样式
|
||
const hoverStyle = {
|
||
strokeWidth: 2,
|
||
stroke: '#52c41a' // 绿色
|
||
};
|
||
|
||
// 保存节点的原始样式
|
||
const saveNodeOriginalStyle = (node: any) => {
|
||
const data = node.getData();
|
||
if (!data?.originalStyle) {
|
||
const originalStyle = {
|
||
stroke: node.getAttrByPath('body/stroke') || '#5F95FF',
|
||
strokeWidth: node.getAttrByPath('body/strokeWidth') || 1
|
||
};
|
||
node.setData({
|
||
...data,
|
||
originalStyle
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取节点的原始样式
|
||
const getNodeOriginalStyle = (node: any) => {
|
||
const data = node.getData();
|
||
return data?.originalStyle || {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1
|
||
};
|
||
};
|
||
|
||
// 恢复节点的原始样式
|
||
const resetNodeStyle = (node: any) => {
|
||
const originalStyle = getNodeOriginalStyle(node);
|
||
node.setAttrByPath('body/stroke', originalStyle.stroke);
|
||
node.setAttrByPath('body/strokeWidth', originalStyle.strokeWidth);
|
||
};
|
||
|
||
// 节点悬停事件
|
||
graph.on('node:mouseenter', ({node}) => {
|
||
// 保存原始样式
|
||
saveNodeOriginalStyle(node);
|
||
|
||
// 显示连接桩
|
||
const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`);
|
||
ports.forEach((port) => {
|
||
port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;');
|
||
});
|
||
|
||
// 显示悬停样式
|
||
node.setAttrByPath('body/stroke', hoverStyle.stroke);
|
||
node.setAttrByPath('body/strokeWidth', hoverStyle.strokeWidth);
|
||
});
|
||
|
||
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');
|
||
});
|
||
|
||
// 恢复原始样式
|
||
resetNodeStyle(node);
|
||
});
|
||
|
||
// 节点拖动开始时记录状态
|
||
graph.on('node:drag:start', ({node}) => {
|
||
// 保存原始样式,以防还没保存过
|
||
saveNodeOriginalStyle(node);
|
||
});
|
||
|
||
// 节点拖动结束后恢复样式
|
||
graph.on('node:moved', ({node}) => {
|
||
resetNodeStyle(node);
|
||
// 隐藏连接桩
|
||
const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`);
|
||
ports.forEach((port) => {
|
||
port.setAttribute('style', 'visibility: hidden');
|
||
});
|
||
});
|
||
|
||
// 节点点击事件
|
||
graph.on('node:click', ({node}) => {
|
||
// 获取当前选中的节点
|
||
const selectedNode = graph.getSelectedCells()[0];
|
||
|
||
// 如果有其他节点被选中,恢复其样式
|
||
if (selectedNode && selectedNode.isNode() && selectedNode.id !== node.id) {
|
||
resetNodeStyle(selectedNode);
|
||
}
|
||
|
||
// 更新选中状态
|
||
graph.resetSelection();
|
||
graph.select(node);
|
||
});
|
||
|
||
// 点击空白处事件
|
||
graph.on('blank:click', () => {
|
||
// 获取当前选中的节点
|
||
const selectedNode = graph.getSelectedCells()[0];
|
||
|
||
// 如果有节点被选中,恢复其样式
|
||
if (selectedNode && selectedNode.isNode()) {
|
||
resetNodeStyle(selectedNode);
|
||
}
|
||
|
||
// 清除选中状态
|
||
graph.resetSelection();
|
||
});
|
||
|
||
// 节点双击事件
|
||
graph.on('node:dblclick', ({node}) => {
|
||
const nodeType = node.getProp('type');
|
||
// 从节点定义列表中找到对应的定义
|
||
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeType);
|
||
if (nodeDefinition) {
|
||
setSelectedNode(node);
|
||
setSelectedNodeDefinition(nodeDefinition);
|
||
setConfigModalVisible(true);
|
||
}
|
||
});
|
||
|
||
// 添加右键菜单
|
||
graph.on('node:contextmenu', ({cell, view, e}) => {
|
||
e.preventDefault();
|
||
graph.cleanSelection();
|
||
graph.select(cell);
|
||
|
||
const dropdownContainer = document.createElement('div');
|
||
dropdownContainer.style.position = 'absolute';
|
||
dropdownContainer.style.left = `${e.clientX}px`;
|
||
dropdownContainer.style.top = `${e.clientY}px`;
|
||
document.body.appendChild(dropdownContainer);
|
||
|
||
const root = createRoot(dropdownContainer);
|
||
let isOpen = true;
|
||
|
||
const closeMenu = () => {
|
||
isOpen = false;
|
||
root.render(
|
||
<Dropdown menu={{items}} open={false} onOpenChange={(open) => {
|
||
if (!open) {
|
||
setTimeout(() => {
|
||
root.unmount();
|
||
document.body.removeChild(dropdownContainer);
|
||
}, 100);
|
||
}
|
||
}}>
|
||
<div/>
|
||
</Dropdown>
|
||
);
|
||
};
|
||
|
||
const items = [
|
||
{
|
||
key: '1',
|
||
label: '编辑',
|
||
onClick: () => {
|
||
closeMenu();
|
||
setSelectedNode(cell);
|
||
setSelectedNodeDefinition(nodeDefinitions.find(def => def.type === cell.getProp('type')));
|
||
setConfigModalVisible(true);
|
||
}
|
||
},
|
||
{
|
||
key: '2',
|
||
label: '删除',
|
||
onClick: () => {
|
||
closeMenu();
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: '确定要删除该节点吗?',
|
||
onOk: () => {
|
||
cell.remove();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
];
|
||
|
||
root.render(
|
||
<Dropdown menu={{items}} open={isOpen}>
|
||
<div/>
|
||
</Dropdown>
|
||
);
|
||
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
if (!dropdownContainer.contains(e.target as Node)) {
|
||
closeMenu();
|
||
document.removeEventListener('click', handleClickOutside);
|
||
}
|
||
};
|
||
document.addEventListener('click', handleClickOutside);
|
||
});
|
||
|
||
// 添加边的右键菜单
|
||
graph.on('edge:contextmenu', ({cell, view, e}) => {
|
||
e.preventDefault();
|
||
graph.cleanSelection();
|
||
graph.select(cell);
|
||
|
||
const dropdownContainer = document.createElement('div');
|
||
dropdownContainer.style.position = 'absolute';
|
||
dropdownContainer.style.left = `${e.clientX}px`;
|
||
dropdownContainer.style.top = `${e.clientY}px`;
|
||
document.body.appendChild(dropdownContainer);
|
||
|
||
const root = createRoot(dropdownContainer);
|
||
let isOpen = true;
|
||
|
||
const closeMenu = () => {
|
||
isOpen = false;
|
||
root.render(
|
||
<Dropdown menu={{items}} open={false} onOpenChange={(open) => {
|
||
if (!open) {
|
||
setTimeout(() => {
|
||
root.unmount();
|
||
document.body.removeChild(dropdownContainer);
|
||
}, 100);
|
||
}
|
||
}}>
|
||
<div/>
|
||
</Dropdown>
|
||
);
|
||
};
|
||
|
||
const items = [
|
||
{
|
||
key: '1',
|
||
label: '删除',
|
||
onClick: () => {
|
||
closeMenu();
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: '确定要删除该连接线吗?',
|
||
onOk: () => {
|
||
cell.remove();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
];
|
||
|
||
root.render(
|
||
<Dropdown menu={{items}} open={isOpen}>
|
||
<div/>
|
||
</Dropdown>
|
||
);
|
||
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
if (!dropdownContainer.contains(e.target as Node)) {
|
||
closeMenu();
|
||
document.removeEventListener('click', handleClickOutside);
|
||
}
|
||
};
|
||
document.addEventListener('click', handleClickOutside);
|
||
});
|
||
|
||
// 禁用默认的右键菜单
|
||
graph.on('blank:contextmenu', ({e}) => {
|
||
e.preventDefault();
|
||
});
|
||
|
||
// 禁用节点的右键菜单
|
||
graph.on('node:contextmenu', ({e}) => {
|
||
e.preventDefault();
|
||
});
|
||
|
||
// 禁用边的右键菜单
|
||
graph.on('edge:contextmenu', ({e}) => {
|
||
e.preventDefault();
|
||
});
|
||
|
||
// 连接桩显示/隐藏
|
||
graph.on('node:mouseenter', ({node}) => {
|
||
// 保存原始样式
|
||
saveNodeOriginalStyle(node);
|
||
|
||
// 显示连接桩
|
||
const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`);
|
||
ports.forEach((port) => {
|
||
port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;');
|
||
});
|
||
|
||
// 显示悬停样式
|
||
node.setAttrByPath('body/stroke', hoverStyle.stroke);
|
||
node.setAttrByPath('body/strokeWidth', hoverStyle.strokeWidth);
|
||
});
|
||
|
||
// 连线开始时的处理
|
||
graph.on('edge:connected', ({edge}) => {
|
||
// 设置连线样式
|
||
edge.setAttrs({
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 2,
|
||
targetMarker: {
|
||
name: 'classic',
|
||
size: 7,
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
// 连线悬停效果
|
||
graph.on('edge:mouseenter', ({edge}) => {
|
||
edge.setAttrs({
|
||
line: {
|
||
stroke: '#52c41a',
|
||
strokeWidth: 3,
|
||
},
|
||
});
|
||
});
|
||
|
||
graph.on('edge:mouseleave', ({edge}) => {
|
||
edge.setAttrs({
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 2,
|
||
},
|
||
});
|
||
});
|
||
|
||
// 允许拖动连线中间的点和端点
|
||
graph.on('edge:selected', ({edge}) => {
|
||
edge.addTools([
|
||
{
|
||
name: 'source-arrowhead',
|
||
args: {
|
||
attrs: {
|
||
fill: '#fff',
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1,
|
||
d: 'M 0 -6 L -8 0 L 0 6 Z',
|
||
},
|
||
},
|
||
},
|
||
{
|
||
name: 'target-arrowhead',
|
||
args: {
|
||
attrs: {
|
||
fill: '#fff',
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1,
|
||
d: 'M 0 -6 L 8 0 L 0 6 Z',
|
||
},
|
||
},
|
||
},
|
||
{
|
||
name: 'vertices',
|
||
args: {
|
||
padding: 2,
|
||
attrs: {
|
||
fill: '#fff',
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1,
|
||
r: 4
|
||
},
|
||
},
|
||
},
|
||
{
|
||
name: 'segments',
|
||
args: {
|
||
padding: 2,
|
||
attrs: {
|
||
fill: '#fff',
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1,
|
||
r: 4
|
||
},
|
||
},
|
||
},
|
||
]);
|
||
});
|
||
|
||
// 连线工具移除
|
||
graph.on('edge:unselected', ({edge}) => {
|
||
edge.removeTools();
|
||
});
|
||
|
||
// 连线移动到其他连接桩时的样式
|
||
graph.on('edge:connected', ({edge}) => {
|
||
edge.setAttrs({
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 2,
|
||
targetMarker: {
|
||
name: 'classic',
|
||
size: 7,
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
// 连线悬停在连接桩上时的样式
|
||
graph.on('edge:mouseenter', ({edge}) => {
|
||
const ports = document.querySelectorAll('.x6-port-body');
|
||
ports.forEach((port) => {
|
||
const portGroup = port.getAttribute('port-group');
|
||
if (portGroup !== 'top') {
|
||
port.setAttribute('style', 'visibility: visible; fill: #fff; stroke: #85ca6d;');
|
||
}
|
||
});
|
||
});
|
||
|
||
graph.on('edge:mouseleave', ({edge}) => {
|
||
const ports = document.querySelectorAll('.x6-port-body');
|
||
ports.forEach((port) => {
|
||
port.setAttribute('style', 'visibility: hidden');
|
||
});
|
||
});
|
||
};
|
||
|
||
// 处理复制操作
|
||
const handleCopy = () => {
|
||
if (!graph) return;
|
||
const cells = graph.getSelectedCells();
|
||
if (cells.length === 0) {
|
||
message.info('请先选择要复制的节点');
|
||
return;
|
||
}
|
||
graph.copy(cells);
|
||
message.success('已复制');
|
||
};
|
||
|
||
// 处理剪切操作
|
||
const handleCut = () => {
|
||
if (!graph) return;
|
||
const cells = graph.getSelectedCells();
|
||
if (cells.length === 0) {
|
||
message.info('请先选择要剪切的节点');
|
||
return;
|
||
}
|
||
graph.cut(cells);
|
||
message.success('已剪切');
|
||
};
|
||
|
||
// 处理粘贴操作
|
||
const handlePaste = () => {
|
||
if (!graph) return;
|
||
if (graph.isClipboardEmpty()) {
|
||
message.info('剪贴板为空');
|
||
return;
|
||
}
|
||
const cells = graph.paste({offset: 32});
|
||
graph.cleanSelection();
|
||
graph.select(cells);
|
||
message.success('已粘贴');
|
||
};
|
||
|
||
// 首先加载节点定义列表
|
||
useEffect(() => {
|
||
const loadNodeDefinitions = async () => {
|
||
try {
|
||
const data = await getNodeDefinitionList();
|
||
setNodeDefinitions(data);
|
||
setIsNodeDefinitionsLoaded(true);
|
||
} catch (error) {
|
||
console.error('加载节点定义失败:', error);
|
||
message.error('加载节点定义失败');
|
||
}
|
||
};
|
||
loadNodeDefinitions();
|
||
}, []);
|
||
|
||
// 加载工作流定义详情
|
||
const loadDefinitionDetail = async (graphInstance: Graph, definitionId: string) => {
|
||
try {
|
||
const response = await getDefinitionDetail(Number(definitionId));
|
||
setTitle(`工作流设计 - ${response.name}`);
|
||
setDefinitionData(response);
|
||
|
||
if (!graphInstance) {
|
||
console.error('Graph instance is not initialized');
|
||
return;
|
||
}
|
||
|
||
// 清空画布
|
||
graphInstance.clearCells();
|
||
const nodeMap = new Map();
|
||
|
||
// 创建节点
|
||
response.graph?.nodes?.forEach((workflowDefinitionNode: any) => {
|
||
const node = addNodeToGraph(false, graphInstance, workflowDefinitionNode, nodeDefinitions);
|
||
// 保存节点配置
|
||
node.setProp('workflowDefinitionNode', workflowDefinitionNode);
|
||
nodeMap.set(workflowDefinitionNode.id, node);
|
||
});
|
||
|
||
// 创建边
|
||
response.graph?.edges?.forEach((edge: any) => {
|
||
const sourceNode = nodeMap.get(edge.from);
|
||
const targetNode = nodeMap.get(edge.to);
|
||
if (sourceNode && targetNode) {
|
||
// 根据节点类型获取正确的端口组
|
||
const getPortByGroup = (node: any, group: string) => {
|
||
const ports = node.getPorts();
|
||
const port = ports.find((p: any) => p.group === group);
|
||
return port?.id;
|
||
};
|
||
|
||
// 获取源节点的输出端口(一定是out组)
|
||
const sourcePort = getPortByGroup(sourceNode, 'out');
|
||
|
||
// 获取目标节点的输入端口(一定是in组)
|
||
const targetPort = getPortByGroup(targetNode, 'in');
|
||
|
||
if (!sourcePort || !targetPort) {
|
||
console.error('无法找到正确的端口:', edge);
|
||
return;
|
||
}
|
||
|
||
graphInstance.addEdge({
|
||
source: {
|
||
cell: sourceNode.id,
|
||
port: sourcePort,
|
||
},
|
||
target: {
|
||
cell: targetNode.id,
|
||
port: targetPort,
|
||
},
|
||
attrs: {
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 2,
|
||
targetMarker: {
|
||
name: 'classic',
|
||
size: 7,
|
||
},
|
||
},
|
||
},
|
||
labels: [{
|
||
attrs: {label: {text: edge.name || ''}}
|
||
}]
|
||
});
|
||
}
|
||
});
|
||
|
||
// 传入节点数据,只在没有位置信息时应用自动布局
|
||
applyAutoLayout(graphInstance, response.graph?.nodes || []);
|
||
} catch (error) {
|
||
console.error('加载工作流定义失败:', error);
|
||
message.error('加载工作流定义失败');
|
||
}
|
||
};
|
||
|
||
// 初始化图形和加载数据
|
||
useEffect(() => {
|
||
if (!graphContainerRef.current || !isNodeDefinitionsLoaded) {
|
||
return;
|
||
}
|
||
|
||
const newGraph = initGraph();
|
||
if (newGraph) {
|
||
setGraph(newGraph);
|
||
|
||
// 在图形初始化完成后加载数据
|
||
if (id) {
|
||
loadDefinitionDetail(newGraph, id);
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
graph?.dispose();
|
||
};
|
||
}, [graphContainerRef, id, nodeDefinitions, isNodeDefinitionsLoaded]);
|
||
|
||
// 处理节点拖拽开始
|
||
const handleNodeDragStart = (node: NodeDefinitionResponse, e: React.DragEvent) => {
|
||
e.dataTransfer.setData('node', JSON.stringify(node));
|
||
};
|
||
|
||
// 处理节点拖拽结束
|
||
const handleDrop = (e: React.DragEvent) => {
|
||
e.preventDefault();
|
||
if (!graph) return;
|
||
|
||
try {
|
||
const nodeData = e.dataTransfer.getData('node');
|
||
if (!nodeData) return;
|
||
|
||
const node = JSON.parse(nodeData);
|
||
const {clientX, clientY} = e;
|
||
const point = graph.clientToLocal({x: clientX, y: clientY});
|
||
console.log("dasdasd", graph, node, nodeDefinitions, point);
|
||
addNodeToGraph(true, graph, node, nodeDefinitions, point);
|
||
} catch (error) {
|
||
console.error('创建节点失败:', error);
|
||
message.error('创建节点失败');
|
||
}
|
||
};
|
||
|
||
// 处理拖拽过程中
|
||
const handleDragOver = (e: React.DragEvent) => {
|
||
e.preventDefault();
|
||
};
|
||
|
||
// 处理节点配置更新
|
||
const handleNodeConfigUpdate = (values: any) => {
|
||
if (!selectedNode) return;
|
||
// 更新节点配置
|
||
console.log("// 更新节点配置", values);
|
||
selectedNode.setProp('config', values);
|
||
// 更新节点显示名称
|
||
if (values.name) {
|
||
selectedNode.attr('label/text', values.name);
|
||
}
|
||
|
||
setConfigModalVisible(false);
|
||
message.success('节点配置已更新');
|
||
};
|
||
|
||
// 处理保存工作流
|
||
const handleSaveWorkflow = async () => {
|
||
if (!graph || !definitionData) return;
|
||
|
||
try {
|
||
// 校验流程图
|
||
const validationResult = validateWorkflow(graph);
|
||
if (!validationResult.valid) {
|
||
message.error(validationResult.message);
|
||
return;
|
||
}
|
||
|
||
// 获取所有节点和边的数据
|
||
const nodes = graph.getNodes().map(node => {
|
||
const nodeDefinition = node.getProp('nodeDefinition');
|
||
const nodeType = node.getProp('type');
|
||
const position = node.getPosition();
|
||
|
||
return {
|
||
id: node.id,
|
||
code: nodeType,
|
||
type: nodeType,
|
||
name: node.attr('label/text'),
|
||
graph: {
|
||
// shape: nodeDefinition?.graphConfig.uiSchema.shape,
|
||
// size: {
|
||
// width: node.size().width,
|
||
// height: node.size().height
|
||
// },
|
||
// style: nodeDefinition?.graphConfig.uiSchema.style,
|
||
// ports: nodeDefinition?.graphConfig.uiSchema.ports,
|
||
// position: {
|
||
// x: position.x,
|
||
// y: position.y
|
||
// }
|
||
uiVariables: nodeDefinition.uiVariables
|
||
},
|
||
config: node.getProp('config') || {}
|
||
};
|
||
});
|
||
|
||
const edges = graph.getEdges().map(edge => ({
|
||
id: edge.id,
|
||
from: edge.getSourceCellId(),
|
||
to: edge.getTargetCellId(),
|
||
name: edge.getLabels()?.[0]?.attrs?.label?.text || '',
|
||
config: {
|
||
type: 'sequence'
|
||
}
|
||
}));
|
||
|
||
// 构建保存数据
|
||
const saveData = {
|
||
...definitionData,
|
||
graph: {
|
||
nodes,
|
||
edges
|
||
}
|
||
};
|
||
|
||
// 调用保存接口
|
||
await saveDefinition(saveData);
|
||
|
||
// 使用 Modal.confirm 显示操作选择
|
||
Modal.confirm({
|
||
title: '保存成功',
|
||
content: '流程设计已保存成功,请选择下一步操作',
|
||
okText: '继续设计',
|
||
cancelText: '返回列表',
|
||
onOk: () => {
|
||
// 重新加载设计数据
|
||
if (id) {
|
||
loadDefinitionDetail(graph, id);
|
||
}
|
||
},
|
||
onCancel: () => {
|
||
// 返回列表页
|
||
navigate('/workflow/definition');
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('保存流程失败:', error);
|
||
message.error('保存流程失败');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="workflow-design">
|
||
<div className="header">
|
||
<Space>
|
||
<Tooltip title="返回">
|
||
<Button
|
||
className="back-button"
|
||
icon={<ArrowLeftOutlined/>}
|
||
onClick={() => navigate('/workflow/definition')}
|
||
/>
|
||
</Tooltip>
|
||
<span>{title}</span>
|
||
</Space>
|
||
<div className="actions">
|
||
<Space>
|
||
<Space.Compact>
|
||
<Tooltip title="撤销">
|
||
<Button
|
||
icon={<UndoOutlined/>}
|
||
onClick={handleUndo}
|
||
disabled={!(graph as any)?.history?.canUndo()}
|
||
>
|
||
撤销
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title="重做">
|
||
<Button
|
||
icon={<RedoOutlined/>}
|
||
onClick={handleRedo}
|
||
disabled={!(graph as any)?.history?.canRedo()}
|
||
>
|
||
重做
|
||
</Button>
|
||
</Tooltip>
|
||
</Space.Compact>
|
||
<Space.Compact>
|
||
<Tooltip title="剪切">
|
||
<Button
|
||
icon={<ScissorOutlined/>}
|
||
onClick={handleCut}
|
||
/>
|
||
</Tooltip>
|
||
<Tooltip title="复制">
|
||
<Button
|
||
icon={<CopyOutlined/>}
|
||
onClick={handleCopy}
|
||
/>
|
||
</Tooltip>
|
||
<Tooltip title="粘贴">
|
||
<Button
|
||
icon={<SnippetsOutlined/>}
|
||
onClick={handlePaste}
|
||
/>
|
||
</Tooltip>
|
||
</Space.Compact>
|
||
<Space.Compact>
|
||
<Tooltip title="放大">
|
||
<Button
|
||
icon={<ZoomInOutlined/>}
|
||
onClick={handleZoomIn}
|
||
disabled={scale >= 2}
|
||
>
|
||
放大
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title="缩小">
|
||
<Button
|
||
icon={<ZoomOutOutlined/>}
|
||
onClick={handleZoomOut}
|
||
disabled={scale <= 0.2}
|
||
>
|
||
缩小
|
||
</Button>
|
||
</Tooltip>
|
||
</Space.Compact>
|
||
<Space.Compact>
|
||
<Tooltip title="全选">
|
||
<Button
|
||
icon={<SelectOutlined/>}
|
||
onClick={() => {
|
||
if (!graph) return;
|
||
|
||
// 添加小延时确保图形已完全初始化
|
||
setTimeout(() => {
|
||
const cells = graph.getCells();
|
||
console.log('All cells:', cells);
|
||
|
||
if (cells.length === 0) {
|
||
message.info('当前没有可选择的元素');
|
||
return;
|
||
}
|
||
|
||
// 打印当前选择状态
|
||
console.log('Current selection before reset:', graph.getSelectedCells());
|
||
graph.resetSelection();
|
||
console.log('Selection after reset:', graph.getSelectedCells());
|
||
|
||
// 尝试选择所有单元格
|
||
try {
|
||
graph.select(cells);
|
||
console.log('Selection after select all:', graph.getSelectedCells());
|
||
} catch (error) {
|
||
console.error('Error selecting cells:', error);
|
||
}
|
||
|
||
// 检查Selection插件是否正确初始化
|
||
const selection = graph.getPlugin('selection');
|
||
console.log('Selection plugin:', selection);
|
||
}, 100);
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
<Tooltip title="删除">
|
||
<Button
|
||
icon={<DeleteOutlined/>}
|
||
onClick={() => {
|
||
if (!graph) return;
|
||
const cells = graph.getSelectedCells();
|
||
if (cells.length === 0) {
|
||
message.info('请先选择要删除的元素');
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: '确定要删除选中的元素吗?',
|
||
onOk: () => {
|
||
graph.removeCells(cells);
|
||
}
|
||
});
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
</Space.Compact>
|
||
<Tooltip title="保存">
|
||
<Button
|
||
type="primary"
|
||
icon={<SaveOutlined/>}
|
||
onClick={handleSaveWorkflow}
|
||
>
|
||
保存
|
||
</Button>
|
||
</Tooltip>
|
||
</Space>
|
||
</div>
|
||
</div>
|
||
<div className="content">
|
||
<div className="sidebar">
|
||
<NodePanel
|
||
nodeDefinitions={nodeDefinitions}
|
||
onNodeDragStart={handleNodeDragStart}
|
||
/>
|
||
</div>
|
||
<div className="main-area">
|
||
<div className="workflow-container">
|
||
<div
|
||
ref={graphContainerRef}
|
||
className="workflow-canvas"
|
||
onDrop={handleDrop}
|
||
onDragOver={handleDragOver}
|
||
/>
|
||
<div ref={minimapContainerRef} className="minimap-container"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{configModalVisible && selectedNode && selectedNodeDefinition && (
|
||
<NodeConfigDrawer
|
||
visible={configModalVisible}
|
||
node={selectedNode}
|
||
nodeDefinition={selectedNodeDefinition}
|
||
onCancel={() => setConfigModalVisible(false)}
|
||
onOk={handleNodeConfigUpdate}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default WorkflowDesign;
|