deploy-ease-platform/frontend/src/pages/Workflow/Definition/Design/index.tsx
dengqichen e76af9b8f5 1
2024-12-20 14:04:55 +08:00

1317 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;