deploy-ease-platform/frontend/src/pages/Workflow/Design/index.tsx.backup
dengqichen 2c66fed29a 1
2025-10-20 14:54:19 +08:00

1541 lines
56 KiB
Plaintext
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, message, Modal, Tooltip, Dropdown} from 'antd';
import {
ArrowLeftOutlined,
SaveOutlined,
CopyOutlined,
DeleteOutlined,
UndoOutlined,
RedoOutlined,
ScissorOutlined,
SnippetsOutlined,
SelectOutlined,
ZoomInOutlined,
ZoomOutOutlined,
} from '@ant-design/icons';
import {Graph, Cell, Edge} 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 {validateWorkflow} from './utils/validator';
import {addNodeToGraph} from './utils/nodeUtils';
import './index.less';
import type {NodeDefinitionResponse} from "@/workflow/nodes/nodeService";
import ExpressionModal from './components/ExpressionModal';
import {EdgeCondition} from './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 [scale, setScale] = useState(1);
const [expressionModalVisible, setExpressionModalVisible] = useState(false);
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
// 初始化图形
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 sourceNodeType = sourceCell.getProp('nodeType');
const targetNodeType = targetCell.getProp('nodeType');
// 如果源节点或目标节点是网关类型,允许多条连线
if (sourceNodeType === 'GATEWAY_NODE' || targetNodeType === 'GATEWAY_NODE') {
return true;
}
// 对于其他类型的节点,检查是否已存在连接
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,
global: false,
},
panning: {
enabled: true,
eventTypes: ['rightMouseDown'], // 右键按下时启用画布拖拽
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.2,
maxScale: 2,
},
edgeMovable: true, // 允许边移动
edgeLabelMovable: true, // 允许边标签移动
vertexAddable: true, // 允许添加顶点
vertexMovable: true, // 允许顶点移动
vertexDeletable: true, // 允许删除顶点
});
// 注册插件
const history = new History({
enabled: true,
beforeAddCommand(event: any, args: any) {
return true;
},
// History 插件事件处理已移除,因为不再需要强制更新
afterExecuteCommand: () => {},
afterUndo: () => {},
afterRedo: () => {},
});
// 初始化Selection插件
const selection = new Selection({
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: false, // 禁用节点选择框
showEdgeSelectionBox: false, // 禁用边选择框
selectNodeOnMoved: false,
selectEdgeOnMoved: false,
multipleSelectionModifiers: ['ctrl', 'meta'],
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();
// 强制更新组件状态已移除
});
graph.on('cell:removed', ({cell}) => {
const canUndo = history.canUndo();
const canRedo = history.canRedo();
console.log('Cell removed:', {
cell,
canUndo,
canRedo,
stackSize: history.stackSize,
});
// 强制更新组件状态已移除
});
graph.on('cell:changed', ({cell, options}) => {
const canUndo = history.canUndo();
const canRedo = history.canRedo();
// 强制更新组件状态已移除
});
// 监听主画布变化,同步更新小地图
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);
// 在 registerEventHandlers 中添加画布的快捷键处理
// 添加画布的快捷键处理
graphContainerRef.current?.addEventListener('keydown', (e: KeyboardEvent) => {
// 只有当画布或其子元素被聚焦时才处理快捷键
if (!graphContainerRef.current?.contains(document.activeElement)) {
return;
}
// Ctrl+A 或 Command+A (Mac)
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
e.preventDefault(); // 阻止浏览器默认的全选行为
if (!graph) return;
const cells = graph.getCells();
if (cells.length > 0) {
graph.resetSelection();
graph.select(cells);
// 为选中的元素添加高亮样式
cells.forEach(cell => {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#1890ff');
cell.setAttrByPath('body/strokeWidth', 3);
cell.setAttrByPath('body/strokeDasharray', '5 5');
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#1890ff');
cell.setAttrByPath('line/strokeWidth', 3);
cell.setAttrByPath('line/strokeDasharray', '5 5');
}
});
}
}
});
// 确保画布可以接收键盘事件
graphContainerRef.current?.setAttribute('tabindex', '0');
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: 2
};
};
// 恢复节点的原始样式
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('nodeType');
// 从节点定义列表中找到对应的定义
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeType);
if (nodeDefinition) {
// 获取已保存的配置
const savedConfig = node.getProp('workflowDefinitionNode');
// 合并节点定义和已保存的配置
const mergedNodeDefinition = {
...nodeDefinition, // 基础定义
...savedConfig, // 已保存的配置(从端加载的)
...node.getProp('graph') || {} // 当前会话中的修改(如果有)
};
setSelectedNode(node);
setSelectedNodeDefinition(mergedNodeDefinition);
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.nodeType === cell.getProp('nodeType')));
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: edge, view, e}) => {
e.preventDefault();
graph.cleanSelection();
graph.select(edge);
const sourceNode = graph.getCellById(edge.getSourceCellId());
const isFromGateway = sourceNode.getProp('nodeType') === 'GATEWAY_NODE';
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 = [
// 只有网关节点的出线才显示编辑条件选项
...(isFromGateway ? [{
key: 'edit',
label: '编辑条件',
onClick: () => {
closeMenu();
setSelectedEdge(edge);
setExpressionModalVisible(true);
}
}] : []),
{
key: 'delete',
label: '删除',
danger: true,
onClick: () => {
closeMenu();
Modal.confirm({
title: '确认删除',
content: '确定要删除该连接线吗?',
onOk: () => {
edge.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: 2,
},
});
});
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');
});
});
// 边选中时添加编辑工具
graph.on('edge:selected', ({edge}) => {
edge.addTools([
{
name: 'vertices', // 顶点工具
args: {
padding: 4,
attrs: {
fill: '#fff',
stroke: '#5F95FF',
strokeWidth: 2,
}
}
},
{
name: 'segments', // 线段工具
args: {
attrs: {
fill: '#fff',
stroke: '#5F95FF',
strokeWidth: 2,
}
}
}
]);
});
// 边取消选中时移除工具
graph.on('edge:unselected', ({edge}) => {
edge.removeTools();
});
// 在 registerEventHandlers 函数中更新选择状态的视觉反馈
graph.on('selection:changed', ({selected, removed}) => {
console.log(selected, removed)
// 处理新选中的元素
selected.forEach(cell => {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#1890ff');
cell.setAttrByPath('body/strokeWidth', 2);
cell.setAttrByPath('body/strokeDasharray', '5 5');
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#1890ff');
cell.setAttrByPath('line/strokeWidth', 2);
cell.setAttrByPath('line/strokeDasharray', '5 5');
}
});
// 处理取消选中的元素
removed.forEach(cell => {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#5F95FF');
cell.setAttrByPath('body/strokeWidth', 2);
cell.setAttrByPath('body/strokeDasharray', null);
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#5F95FF');
cell.setAttrByPath('line/strokeWidth', 2);
cell.setAttrByPath('line/strokeDasharray', null);
}
});
});
};
// 处理复制操作
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;
}
// 确保节点定义已加载
if (!nodeDefinitions || nodeDefinitions.length === 0) {
console.error('节点定义未加载,无法还原工作流');
return;
}
// 清空画布
graphInstance.clearCells();
const nodeMap = new Map();
// 创建节点
response.graph?.nodes?.forEach((existingNode: any) => {
console.log('正在还原节点:', existingNode.nodeType, existingNode);
const node = addNodeToGraph(false, graphInstance, existingNode, nodeDefinitions);
if (!node) {
console.error('节点创建失败:', existingNode);
return;
}
// 只设置 graph 属性
node.setProp('graph', {
uiVariables: existingNode.uiVariables,
panelVariables: existingNode.panelVariables,
localVariables: existingNode.localVariables
});
nodeMap.set(existingNode.id, node);
console.log('节点创建成功ID:', node.id, '映射 ID:', existingNode.id);
});
console.log('所有节点创建完成nodeMap:', nodeMap);
console.log('准备创建边:', response.graph?.edges);
// 创建边
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;
}
const newEdge = graphInstance.addEdge({
source: {
cell: sourceNode.id,
port: sourcePort,
},
target: {
cell: targetNode.id,
port: targetPort,
},
vertices: edge?.vertices || [], // 恢复顶点信息
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 2,
targetMarker: {
name: 'classic',
size: 7,
},
},
},
labels: [{
attrs: {
label: {
text: edge.config?.condition?.expression || edge.name || ''
}
}
}]
});
// 设置边的条件属性
if (edge.config?.condition) {
newEdge.setProp('condition', edge.config.condition);
}
}
});
// 传入节点数据,只在没有位置信息时应用自动布局
// 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});
addNodeToGraph(true, graph, node, nodeDefinitions, point);
} catch (error) {
console.error('创建节点失败:', error);
message.error('创建节点失败');
}
};
// 处理拖拽过程中
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
// 处理节点配置更新
const handleNodeConfigUpdate = (updatedNodeDefinition: any) => {
if (!selectedNode) return;
// 设置节点的 graph 属性,将所有数据统一放在 graph 下
selectedNode.setProp('graph', updatedNodeDefinition);
// 更新节点显示名称(如果存在)
if (updatedNodeDefinition.panelVariables?.name) {
selectedNode.attr('label/text', updatedNodeDefinition.panelVariables.name);
}
setConfigModalVisible(false);
message.success('节点配置已更新');
};
// 首先添加合并 schema 的工具函数
const mergeLocalVariablesSchemas = (schemas: any[]) => {
// 初始化合并后的 schema
const mergedSchema = {
type: 'object',
properties: {},
required: []
};
schemas.forEach(schema => {
if (!schema) return;
// 合并 properties
if (schema.properties) {
Object.entries(schema.properties).forEach(([key, property]) => {
// 如果属性已存在,检查是否完全相同
if (mergedSchema.properties[key]) {
if (JSON.stringify(mergedSchema.properties[key]) !== JSON.stringify(property)) {
console.warn(`属性 ${key} 在不同节点中定义不一致,使用第一个定义`);
}
} else {
mergedSchema.properties[key] = property;
}
});
}
// 合并 required 字段
if (schema.required) {
schema.required.forEach((field: string) => {
if (!mergedSchema.required.includes(field)) {
mergedSchema.required.push(field);
}
});
}
});
return mergedSchema;
};
// 处理保存工作流
const handleSaveWorkflow = async () => {
if (!graph || !definitionData) return;
console.log(definitionData)
try {
// 校验流程图
const validationResult = validateWorkflow(graph);
if (!validationResult.valid) {
message.error(validationResult.message);
return;
}
// 获取所有节点和边的数据
const nodes = graph.getNodes().map(node => {
const nodeType = node.getProp('nodeType');
const graphData = node.getProp('graph') || {};
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === nodeType);
const position = node.getPosition();
const {
uiVariables,
panelVariables,
localVariables,
...rest
} = graphData;
return {
id: node.id,
nodeCode: nodeType,
nodeType: nodeType,
nodeName: node.attr('label/text'),
uiVariables: {
...uiVariables,
position: position
},
panelVariables,
localVariables: nodeDefinition?.localVariablesSchema || {}
};
});
const edges = graph.getEdges().map(edge => {
const condition = edge.getProp('condition');
const vertices = edge.getVertices(); // 获取边的顶点信息
return {
id: edge.id,
from: edge.getSourceCellId(),
to: edge.getTargetCellId(),
name: edge.getLabels()?.[0]?.attrs?.label?.text || '',
config: {
type: 'sequence',
condition: condition || undefined
},
vertices: vertices // 保存顶点信息
};
});
// 收集并合并所有节点的 localVariablesSchema
const allLocalSchemas = nodes
.map(node => {
const nodeDefinition = nodeDefinitions.find(def => def.nodeType === node.nodeType);
return nodeDefinition?.localVariablesSchema;
})
.filter(schema => schema); // 过滤掉空值
const mergedLocalSchema = mergeLocalVariablesSchemas(allLocalSchemas);
// 构建保存数据
const saveData = {
...definitionData,
graph: {
nodes,
edges
},
localVariablesSchema: mergedLocalSchema
};
// 调用保存接口
await saveDefinition(saveData);
message.success('保存成功');
} catch (error) {
console.error('保存流程失败:', error);
message.error('保存流程失败');
}
};
// 处理条件更新
const handleConditionUpdate = (condition: EdgeCondition) => {
if (!selectedEdge) return;
// 更新边的属性
selectedEdge.setProp('condition', condition);
// 更新边的标签显示
const labelText = condition.type === 'EXPRESSION'
? condition.expression
: '默认路径';
selectedEdge.setLabels([{
attrs: {
label: {
text: labelText,
fill: '#333',
fontSize: 12
},
rect: {
fill: '#fff',
stroke: '#ccc',
rx: 3,
ry: 3,
padding: 5
}
},
position: {
distance: 0.5,
offset: 0
}
}]);
setExpressionModalVisible(false);
setSelectedEdge(null);
};
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={(e) => {
e.stopPropagation(); // 阻止事件冒泡
if (!graph) return;
const cells = graph.getCells();
if (cells.length === 0) {
message.info('当前没有可选择的元素');
return;
}
graph.resetSelection();
graph.select(cells);
// 为选中的元素添加高亮样式
cells.forEach(cell => {
if (cell.isNode()) {
cell.setAttrByPath('body/stroke', '#1890ff');
cell.setAttrByPath('body/strokeWidth', 3);
cell.setAttrByPath('body/strokeDasharray', '5 5');
} else if (cell.isEdge()) {
cell.setAttrByPath('line/stroke', '#1890ff');
cell.setAttrByPath('line/strokeWidth', 3);
cell.setAttrByPath('line/strokeDasharray', '5 5');
}
});
}}
/>
</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}
/>
)}
{selectedEdge && (
<ExpressionModal
visible={expressionModalVisible}
edge={selectedEdge}
onOk={handleConditionUpdate}
onCancel={() => {
setExpressionModalVisible(false);
setSelectedEdge(null);
}}
/>
)}
</div>
);
};
export default WorkflowDesign;