922 lines
30 KiB
TypeScript
922 lines
30 KiB
TypeScript
import React, {useEffect, useRef, useState} from 'react';
|
||
import {useNavigate, useParams} from 'react-router-dom';
|
||
import {Button, Card, Layout, message, Space, Spin, Drawer, Form, Dropdown} from 'antd';
|
||
import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons';
|
||
import {getDefinition, updateDefinition} from '../../service';
|
||
import {WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types';
|
||
import {Graph, Node, Cell, Edge, Shape} from '@antv/x6';
|
||
import '@antv/x6-react-shape';
|
||
import {Selection} from '@antv/x6-plugin-selection';
|
||
import {History} from '@antv/x6-plugin-history';
|
||
import {Clipboard} from '@antv/x6-plugin-clipboard';
|
||
import {Transform} from '@antv/x6-plugin-transform';
|
||
import {Keyboard} from '@antv/x6-plugin-keyboard';
|
||
import {Snapline} from '@antv/x6-plugin-snapline';
|
||
import {MiniMap} from '@antv/x6-plugin-minimap';
|
||
import './index.module.less';
|
||
import NodePanel from './components/NodePanel';
|
||
import NodeConfig from './components/NodeConfig';
|
||
import Toolbar from './components/Toolbar';
|
||
import {NodeType, getNodeTypes} from './service';
|
||
import {DeleteOutlined, CopyOutlined, SettingOutlined, ClearOutlined, FullscreenOutlined} from '@ant-design/icons';
|
||
import EdgeConfig from './components/EdgeConfig';
|
||
import { validateFlow, hasCycle } from './validate';
|
||
import { generateNodeStyle, generatePorts, calculateNodePosition, calculateCanvasPosition } from './utils/nodeUtils';
|
||
import { Position } from './types';
|
||
import { NODE_CONFIG } from './configs/nodeConfig';
|
||
import { isWorkflowError } from './utils/errors';
|
||
|
||
const {Sider, Content} = Layout;
|
||
|
||
interface NodeData {
|
||
type: string;
|
||
name?: string;
|
||
description?: string;
|
||
config: {
|
||
executor?: string;
|
||
retryTimes?: number;
|
||
retryInterval?: number;
|
||
script?: string;
|
||
timeout?: number;
|
||
workingDirectory?: string;
|
||
environment?: string;
|
||
successExitCode?: string;
|
||
[key: string]: any;
|
||
};
|
||
}
|
||
|
||
const FlowDesigner: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const {id} = useParams<{ id: string }>();
|
||
const [loading, setLoading] = useState(false);
|
||
const [detail, setDetail] = useState<WorkflowDefinition>();
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const graphRef = useRef<Graph>();
|
||
const [graph, setGraph] = useState<Graph>();
|
||
const draggedNodeRef = useRef<NodeType>();
|
||
const [configVisible, setConfigVisible] = useState(false);
|
||
const [currentNode, setCurrentNode] = useState<Node>();
|
||
const [currentNodeType, setCurrentNodeType] = useState<NodeType>();
|
||
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
||
const [form] = Form.useForm();
|
||
const [currentEdge, setCurrentEdge] = useState<Edge>();
|
||
const [edgeConfigVisible, setEdgeConfigVisible] = useState(false);
|
||
const [edgeForm] = Form.useForm();
|
||
|
||
// 右键菜单状态
|
||
const [contextMenu, setContextMenu] = useState<{
|
||
x: number;
|
||
y: number;
|
||
visible: boolean;
|
||
type: 'node' | 'edge' | 'canvas';
|
||
cell?: Cell;
|
||
}>({
|
||
x: 0,
|
||
y: 0,
|
||
visible: false,
|
||
type: 'canvas',
|
||
});
|
||
|
||
// 右键菜单项
|
||
const menuItems = {
|
||
node: [
|
||
{
|
||
key: 'delete',
|
||
label: '删除节点',
|
||
icon: <DeleteOutlined />,
|
||
onClick: () => {
|
||
if (contextMenu.cell) {
|
||
contextMenu.cell.remove();
|
||
}
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
{
|
||
key: 'copy',
|
||
label: '复制节点',
|
||
icon: <CopyOutlined />,
|
||
onClick: () => {
|
||
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
||
const pos = contextMenu.cell.position();
|
||
const newCell = contextMenu.cell.clone();
|
||
newCell.position(pos.x + 20, pos.y + 20);
|
||
graph?.addCell(newCell);
|
||
}
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
{
|
||
key: 'config',
|
||
label: '配置节点',
|
||
icon: <SettingOutlined />,
|
||
onClick: () => {
|
||
if (contextMenu.cell && contextMenu.cell.isNode()) {
|
||
setCurrentNode(contextMenu.cell);
|
||
const data = contextMenu.cell.getData() as NodeData;
|
||
const nodeType = nodeTypes.find(type => type.code === data.type);
|
||
if (nodeType) {
|
||
setCurrentNodeType(nodeType);
|
||
const formValues = {
|
||
name: data.name || nodeType.name,
|
||
description: data.description,
|
||
...data.config
|
||
};
|
||
form.setFieldsValue(formValues);
|
||
}
|
||
setConfigVisible(true);
|
||
}
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
],
|
||
edge: [
|
||
{
|
||
key: 'config',
|
||
label: '配置连线',
|
||
icon: <SettingOutlined />,
|
||
onClick: () => {
|
||
if (contextMenu.cell && contextMenu.cell.isEdge()) {
|
||
handleEdgeConfig(contextMenu.cell);
|
||
}
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
{
|
||
key: 'delete',
|
||
label: '删除连线',
|
||
icon: <DeleteOutlined />,
|
||
onClick: () => {
|
||
if (contextMenu.cell) {
|
||
contextMenu.cell.remove();
|
||
}
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
],
|
||
canvas: [
|
||
{
|
||
key: 'clear',
|
||
label: '清空画布',
|
||
icon: <ClearOutlined />,
|
||
onClick: () => {
|
||
graph?.clearCells();
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
{
|
||
key: 'fit',
|
||
label: '适应画布',
|
||
icon: <FullscreenOutlined />,
|
||
onClick: () => {
|
||
graph?.zoomToFit({ padding: 20 });
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
},
|
||
},
|
||
],
|
||
};
|
||
|
||
// 获取所有节点类型
|
||
const fetchNodeTypes = async () => {
|
||
try {
|
||
const types = await getNodeTypes({enabled: true});
|
||
setNodeTypes(types);
|
||
return types;
|
||
} catch (error) {
|
||
console.error('获取节点类型失败:', error);
|
||
message.error('获取节点类型失败');
|
||
return [];
|
||
}
|
||
};
|
||
|
||
// 首次加载获取节点类型
|
||
useEffect(() => {
|
||
fetchNodeTypes();
|
||
}, []);
|
||
|
||
// 初始化图形
|
||
const initGraph = () => {
|
||
if (!containerRef.current) return;
|
||
|
||
const graph = new Graph({
|
||
container: containerRef.current,
|
||
grid: {
|
||
visible: true,
|
||
type: 'mesh',
|
||
size: 10,
|
||
args: {
|
||
color: '#e5e5e5',
|
||
thickness: 1,
|
||
},
|
||
},
|
||
mousewheel: {
|
||
enabled: true,
|
||
modifiers: ['ctrl', 'meta'],
|
||
minScale: 0.5,
|
||
maxScale: 2,
|
||
},
|
||
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;
|
||
}
|
||
return true;
|
||
},
|
||
},
|
||
defaultEdge: {
|
||
attrs: {
|
||
line: {
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1,
|
||
targetMarker: {
|
||
name: 'classic',
|
||
size: 8,
|
||
},
|
||
},
|
||
},
|
||
router: {
|
||
name: 'manhattan',
|
||
args: {
|
||
padding: 1,
|
||
},
|
||
},
|
||
connector: {
|
||
name: 'rounded',
|
||
args: {
|
||
radius: 8,
|
||
},
|
||
},
|
||
labels: [
|
||
{
|
||
attrs: {
|
||
label: {
|
||
text: '',
|
||
fill: '#333',
|
||
fontSize: 12,
|
||
},
|
||
rect: {
|
||
fill: '#fff',
|
||
stroke: '#5F95FF',
|
||
strokeWidth: 1,
|
||
rx: 3,
|
||
ry: 3,
|
||
refWidth: 1,
|
||
refHeight: 1,
|
||
refX: 0,
|
||
refY: 0,
|
||
},
|
||
},
|
||
position: {
|
||
distance: 0.5,
|
||
offset: {
|
||
x: 0,
|
||
y: -10,
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
highlighting: {
|
||
magnetAvailable: {
|
||
name: 'stroke',
|
||
args: {
|
||
padding: 4,
|
||
attrs: {
|
||
strokeWidth: 4,
|
||
stroke: '#52c41a',
|
||
},
|
||
},
|
||
},
|
||
magnetAdsorbed: {
|
||
name: 'stroke',
|
||
args: {
|
||
padding: 4,
|
||
attrs: {
|
||
strokeWidth: 4,
|
||
stroke: '#1890ff',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
keyboard: {
|
||
enabled: true,
|
||
},
|
||
clipboard: {
|
||
enabled: true,
|
||
},
|
||
history: {
|
||
enabled: true,
|
||
},
|
||
snapline: {
|
||
enabled: true,
|
||
},
|
||
translating: {
|
||
restrict: true,
|
||
},
|
||
background: {
|
||
color: '#ffffff', // 画布背景色
|
||
},
|
||
});
|
||
|
||
// 启用必要的功能
|
||
graph.use(
|
||
new Selection({
|
||
enabled: true,
|
||
multiple: true,
|
||
rubberband: true,
|
||
rubberEdge: true,
|
||
movable: true,
|
||
showNodeSelectionBox: true,
|
||
showEdgeSelectionBox: true,
|
||
selectCellOnMoved: false,
|
||
selectEdgeOnMoved: false,
|
||
selectNodeOnMoved: false,
|
||
className: 'node-selected',
|
||
strict: true,
|
||
})
|
||
);
|
||
|
||
graph.use(
|
||
new History({
|
||
enabled: true,
|
||
beforeAddCommand: (event: string, args: any) => {
|
||
if (event === 'cell:change:*') {
|
||
return true;
|
||
}
|
||
return true;
|
||
},
|
||
})
|
||
);
|
||
|
||
graph.use(
|
||
new Clipboard({
|
||
enabled: true,
|
||
})
|
||
);
|
||
|
||
graph.use(
|
||
new Transform({
|
||
resizing: {
|
||
enabled: true,
|
||
minWidth: 1,
|
||
minHeight: 1,
|
||
orthogonal: true,
|
||
restricted: true,
|
||
},
|
||
rotating: {
|
||
enabled: true,
|
||
grid: 15,
|
||
},
|
||
})
|
||
);
|
||
|
||
graph.use(
|
||
new Keyboard({
|
||
enabled: true,
|
||
})
|
||
);
|
||
|
||
graph.use(
|
||
new Snapline({
|
||
enabled: true,
|
||
})
|
||
);
|
||
|
||
// 启用小地图
|
||
graph.use(
|
||
new MiniMap({
|
||
container: document.getElementById('workflow-minimap'),
|
||
width: 200,
|
||
height: 150,
|
||
padding: 10,
|
||
scalable: false,
|
||
minScale: 0.5,
|
||
maxScale: 2,
|
||
graphOptions: {
|
||
async: true,
|
||
// 简化节点渲染
|
||
grid: false,
|
||
background: {
|
||
color: '#f5f5f5',
|
||
},
|
||
},
|
||
})
|
||
);
|
||
|
||
// 绑定右键菜单事件
|
||
graph.on('cell:contextmenu', ({ cell, e }) => {
|
||
e.preventDefault();
|
||
setContextMenu({
|
||
x: e.clientX,
|
||
y: e.clientY,
|
||
visible: true,
|
||
type: cell.isNode() ? 'node' : 'edge',
|
||
cell,
|
||
});
|
||
});
|
||
|
||
graph.on('blank:contextmenu', ({ e }) => {
|
||
e.preventDefault();
|
||
setContextMenu({
|
||
x: e.clientX,
|
||
y: e.clientY,
|
||
visible: true,
|
||
type: 'canvas',
|
||
});
|
||
});
|
||
|
||
// 点击画布时隐藏右键菜单
|
||
graph.on('blank:click', () => {
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
});
|
||
|
||
// 点击节点时隐藏右键菜单
|
||
graph.on('cell:click', () => {
|
||
setContextMenu(prev => ({ ...prev, visible: false }));
|
||
});
|
||
|
||
graphRef.current = graph;
|
||
setGraph(graph);
|
||
|
||
// 加载流程图数据
|
||
if (detail) {
|
||
loadGraphData(graph, detail);
|
||
}
|
||
|
||
// 监听画布拖拽事件
|
||
containerRef.current.addEventListener('dragover', handleDragOver);
|
||
containerRef.current.addEventListener('drop', handleDrop);
|
||
|
||
// 监听节点点击事件
|
||
graph.on('node:click', ({node}: { node: Node }) => {
|
||
setCurrentNode(node);
|
||
const data = node.getData() as NodeData;
|
||
console.log('Node clicked, data:', data);
|
||
|
||
if (data) {
|
||
// 获取节点类型
|
||
const nodeType = nodeTypes.find(type => type.code === data.type);
|
||
if (nodeType) {
|
||
setCurrentNodeType(nodeType);
|
||
|
||
// 合并节点基本配置和执器配置
|
||
const formValues = {
|
||
name: data.name || nodeType.name,
|
||
description: data.description,
|
||
...data.config // 直接展开所有配置
|
||
};
|
||
|
||
console.log('Setting form values:', formValues);
|
||
form.setFieldsValue(formValues);
|
||
setConfigVisible(true);
|
||
} else {
|
||
message.error('未找到对应的节点类型');
|
||
}
|
||
}
|
||
});
|
||
|
||
// 监听选择状态变化
|
||
graph.on('selection:changed', () => {
|
||
// 强制更新工具栏状态
|
||
setGraph(graph);
|
||
});
|
||
};
|
||
|
||
// 处理拖拽移动
|
||
const handleDragOver = (e: DragEvent) => {
|
||
e.preventDefault();
|
||
e.dataTransfer!.dropEffect = 'copy';
|
||
};
|
||
|
||
// 处理拖拽放置
|
||
const handleDrop = (e: DragEvent) => {
|
||
e.preventDefault();
|
||
const nodeType = draggedNodeRef.current;
|
||
if (!nodeType || !graphRef.current || !containerRef.current) {
|
||
message.error('无效的节点类型或画布未初始化');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const rect = containerRef.current.getBoundingClientRect();
|
||
const matrix = graphRef.current.matrix();
|
||
|
||
// 使用新的工具函数计算画布位置
|
||
const dropPosition = calculateCanvasPosition(
|
||
e.clientX,
|
||
e.clientY,
|
||
rect,
|
||
{
|
||
scale: matrix.a,
|
||
offsetX: matrix.e,
|
||
offsetY: matrix.f,
|
||
}
|
||
);
|
||
|
||
// 获取节点配置
|
||
const position = calculateNodePosition(nodeType.code, dropPosition);
|
||
const nodeStyle = generateNodeStyle(nodeType.code);
|
||
const ports = generatePorts(nodeType.code);
|
||
const config = NODE_CONFIG[nodeType.code];
|
||
|
||
if (!config) {
|
||
throw new Error(`未找到节点类型 ${nodeType.code} 的配置`);
|
||
}
|
||
|
||
// 创建节点
|
||
const node = graphRef.current.addNode({
|
||
...position,
|
||
...nodeStyle,
|
||
ports,
|
||
data: {
|
||
type: nodeType.code,
|
||
name: config.label, // 使用配置中的中文名称
|
||
config: {} as any,
|
||
},
|
||
});
|
||
|
||
// 选中新创建的节点并打开配置
|
||
graphRef.current.cleanSelection();
|
||
graphRef.current.select(node);
|
||
setCurrentNode(node);
|
||
setCurrentNodeType(nodeType);
|
||
form.setFieldsValue({ name: config.label }); // 使用配置中的中文名称
|
||
setConfigVisible(true);
|
||
|
||
message.success('节点创建成功');
|
||
} catch (error) {
|
||
console.error('Error creating node:', error);
|
||
|
||
if (isWorkflowError(error)) {
|
||
message.error(`创建节点失败:${error.message}`);
|
||
} else {
|
||
message.error('创建节点失败:未知错误');
|
||
}
|
||
|
||
// 清理状态
|
||
setCurrentNode(undefined);
|
||
setCurrentNodeType(undefined);
|
||
setConfigVisible(false);
|
||
}
|
||
};
|
||
|
||
// 验证Shell节点配置
|
||
function validateShellConfig(config: any): boolean {
|
||
if (!config.script?.trim()) {
|
||
throw new Error('脚本内容不能为空');
|
||
}
|
||
if (config.timeout !== undefined && config.timeout <= 0) {
|
||
throw new Error('超时时间必须大于0');
|
||
}
|
||
if (config.retryTimes !== undefined && config.retryTimes < 0) {
|
||
throw new Error('重试次数不能为负数');
|
||
}
|
||
if (config.retryInterval !== undefined && config.retryInterval < 0) {
|
||
throw new Error('重试间不能为负数');
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 处理配置保存
|
||
const handleConfigSave = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
if (currentNode) {
|
||
const data = currentNode.getData() as NodeData;
|
||
const {name, description, ...config} = values;
|
||
|
||
// 如果是Shell节点,验证配置
|
||
if (data.type === 'SHELL' && config.executor === 'SHELL') {
|
||
validateShellConfig(config);
|
||
}
|
||
|
||
// 更新节点数据
|
||
currentNode.setData({
|
||
...data,
|
||
name,
|
||
description,
|
||
config,
|
||
});
|
||
|
||
// 更新节点标签
|
||
currentNode.setAttrByPath('label/text', name);
|
||
|
||
message.success('配置保存成功');
|
||
setConfigVisible(false);
|
||
}
|
||
} catch (error) {
|
||
// 表单验证失败或Shell配置验证失败
|
||
message.error((error as Error).message);
|
||
}
|
||
};
|
||
|
||
// 获取详情
|
||
const fetchDetail = async () => {
|
||
if (!id) return;
|
||
setLoading(true);
|
||
try {
|
||
const response = await getDefinition(parseInt(id));
|
||
if (response) {
|
||
setDetail(response);
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 处理保存
|
||
const handleSave = async () => {
|
||
if (!id || !detail || !graphRef.current || detail.status !== WorkflowStatus.DRAFT) return;
|
||
|
||
try {
|
||
// 验证流程
|
||
const result = validateFlow(graphRef.current);
|
||
const hasCycleResult = hasCycle(graphRef.current);
|
||
|
||
if (hasCycleResult) {
|
||
message.error('流程图中存在循环,请检查');
|
||
return;
|
||
}
|
||
|
||
if (!result.valid) {
|
||
message.error(result.errors.join('\n'));
|
||
return;
|
||
}
|
||
|
||
const graphData = graphRef.current.toJSON();
|
||
|
||
// 收集节点配置数据
|
||
const nodes = graphRef.current.getNodes().map(node => {
|
||
const data = node.getData() as NodeData;
|
||
return {
|
||
id: node.id,
|
||
type: data.type,
|
||
name: data.name || '',
|
||
description: data.description,
|
||
config: data.config || {}
|
||
};
|
||
});
|
||
|
||
// 收集连线配置数据
|
||
const transitions = graphRef.current.getEdges().map(edge => {
|
||
const data = edge.getData() || {};
|
||
return {
|
||
from: edge.getSourceCellId(),
|
||
to: edge.getTargetCellId(),
|
||
condition: data.condition || '',
|
||
description: data.description || '',
|
||
priority: data.priority || 0
|
||
};
|
||
});
|
||
|
||
// 构建更新数据
|
||
const data = {
|
||
...detail,
|
||
graphDefinition: JSON.stringify(graphData),
|
||
nodeConfig: JSON.stringify({nodes}),
|
||
transitionConfig: JSON.stringify({transitions})
|
||
};
|
||
|
||
await updateDefinition(parseInt(id), data);
|
||
message.success('保存成功');
|
||
} catch (error) {
|
||
message.error('保存失败');
|
||
console.error('保存失败:', error);
|
||
}
|
||
};
|
||
|
||
// 处理返回
|
||
const handleBack = () => {
|
||
navigate('/workflow/definition');
|
||
};
|
||
|
||
// 首次加载
|
||
useEffect(() => {
|
||
fetchDetail();
|
||
}, [id]);
|
||
|
||
// 初始化图形
|
||
useEffect(() => {
|
||
if (detail && containerRef.current) {
|
||
initGraph();
|
||
}
|
||
// 清理事件监听
|
||
return () => {
|
||
if (containerRef.current) {
|
||
containerRef.current.removeEventListener('dragover', handleDragOver);
|
||
containerRef.current.removeEventListener('drop', handleDrop);
|
||
}
|
||
if (graphRef.current) {
|
||
graphRef.current.dispose();
|
||
}
|
||
};
|
||
}, [detail, containerRef.current]);
|
||
|
||
useEffect(() => {
|
||
if (!containerRef.current) return;
|
||
|
||
const resizeObserver = new ResizeObserver(() => {
|
||
if (graphRef.current) {
|
||
const container = containerRef.current;
|
||
if (container) {
|
||
const { width, height } = container.getBoundingClientRect();
|
||
graphRef.current.resize(width, height);
|
||
}
|
||
}
|
||
});
|
||
|
||
resizeObserver.observe(containerRef.current);
|
||
|
||
return () => {
|
||
resizeObserver.disconnect();
|
||
};
|
||
}, []);
|
||
|
||
// 处理节点拖拽开始
|
||
const handleNodeDragStart = (nodeType: NodeType) => {
|
||
draggedNodeRef.current = nodeType;
|
||
};
|
||
|
||
// 加载流程图数据
|
||
const loadGraphData = (graph: Graph, detail: WorkflowDefinition) => {
|
||
try {
|
||
// 加载图数据
|
||
const graphData = JSON.parse(detail.graphDefinition);
|
||
graph.fromJSON(graphData);
|
||
|
||
// 加载节点配置数据
|
||
if (detail.nodeConfig) {
|
||
const nodeConfigData = JSON.parse(detail.nodeConfig);
|
||
// 更新节点数据和显示
|
||
graph.getNodes().forEach(node => {
|
||
const nodeConfig = nodeConfigData.nodes.find((n: any) => n.id === node.id);
|
||
if (nodeConfig) {
|
||
console.log('Node config found:', nodeConfig.config);
|
||
const nodeData = {
|
||
type: nodeConfig.type,
|
||
name: nodeConfig.name,
|
||
description: nodeConfig.description,
|
||
config: nodeConfig.config
|
||
};
|
||
console.log('Setting node data:', nodeData);
|
||
node.setData(nodeData);
|
||
node.setAttrByPath('label/text', nodeConfig.name);
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
message.error('加载流程图数据失败');
|
||
console.error('加载流程图数据失败:', error);
|
||
}
|
||
};
|
||
|
||
// 添加连线配置处理函数
|
||
const handleEdgeConfig = (edge: Edge) => {
|
||
setCurrentEdge(edge);
|
||
const data = edge.getData() || {};
|
||
edgeForm.setFieldsValue(data);
|
||
setEdgeConfigVisible(true);
|
||
};
|
||
|
||
// 添加连线配置保存函数
|
||
const handleEdgeConfigSubmit = async () => {
|
||
if (!currentEdge) return;
|
||
|
||
try {
|
||
const values = await edgeForm.validateFields();
|
||
currentEdge.setData(values);
|
||
|
||
// 更新连线标签
|
||
if (values.description) {
|
||
currentEdge.setLabelAt(0, values.description);
|
||
}
|
||
|
||
setEdgeConfigVisible(false);
|
||
} catch (error) {
|
||
// 表单验证失败
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{textAlign: 'center', padding: 100}}>
|
||
<Spin size="large"/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Card
|
||
title={
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||
<Space>
|
||
<Button type="primary" icon={<SaveOutlined/>} onClick={handleSave}>保存</Button>
|
||
<Button icon={<ArrowLeftOutlined/>} onClick={() => navigate(-1)}>返回</Button>
|
||
</Space>
|
||
</div>
|
||
}
|
||
className="workflow-designer"
|
||
>
|
||
<Layout>
|
||
<Sider width={250} className="workflow-designer-sider">
|
||
<NodePanel onNodeDragStart={handleNodeDragStart}/>
|
||
</Sider>
|
||
<Layout>
|
||
<Toolbar graph={graph}/>
|
||
<Content className="workflow-designer-content">
|
||
<div ref={containerRef} className="workflow-designer-graph"/>
|
||
<div id="workflow-minimap" className="workflow-designer-minimap"/>
|
||
<Dropdown
|
||
menu={{
|
||
items: menuItems[contextMenu.type],
|
||
onClick: ({ key, domEvent }) => {
|
||
domEvent.stopPropagation();
|
||
},
|
||
}}
|
||
open={contextMenu.visible}
|
||
trigger={['contextMenu']}
|
||
>
|
||
<div
|
||
style={{
|
||
position: 'fixed',
|
||
left: contextMenu.x,
|
||
top: contextMenu.y,
|
||
width: 1,
|
||
height: 1,
|
||
}}
|
||
/>
|
||
</Dropdown>
|
||
</Content>
|
||
</Layout>
|
||
</Layout>
|
||
|
||
<Drawer
|
||
title="节点配置"
|
||
width={400}
|
||
open={configVisible}
|
||
onClose={() => setConfigVisible(false)}
|
||
extra={
|
||
<Space>
|
||
<Button onClick={() => setConfigVisible(false)}>取消</Button>
|
||
<Button type="primary" onClick={handleConfigSave}>
|
||
确定
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
{currentNodeType && (
|
||
<NodeConfig nodeType={currentNodeType} form={form}/>
|
||
)}
|
||
</Drawer>
|
||
|
||
<Drawer
|
||
title="连线配置"
|
||
placement="right"
|
||
width={400}
|
||
onClose={() => setEdgeConfigVisible(false)}
|
||
open={edgeConfigVisible}
|
||
extra={
|
||
<Space>
|
||
<Button onClick={() => setEdgeConfigVisible(false)}>取消</Button>
|
||
<Button type="primary" onClick={handleEdgeConfigSubmit}>
|
||
确定
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
{currentEdge && (
|
||
<EdgeConfig
|
||
edge={currentEdge}
|
||
form={edgeForm}
|
||
onValuesChange={(changedValues, allValues) => {
|
||
if (changedValues.description) {
|
||
currentEdge.setLabelAt(0, changedValues.description);
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
</Drawer>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default FlowDesigner;
|