1541 lines
56 KiB
Plaintext
1541 lines
56 KiB
Plaintext
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;
|