deploy-ease-platform/frontend/src/pages/Workflow/Definition/Design/index.tsx
2024-12-13 13:36:28 +08:00

333 lines
12 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button, Space, Card, Row, Col, message } from 'antd';
import { ArrowLeftOutlined, SaveOutlined, PlayCircleOutlined } from '@ant-design/icons';
import { Graph, Cell } from '@antv/x6';
import { getDefinitionDetail, saveDefinition } from '../service';
import { getNodeDefinitionList } from './service';
import NodePanel from './components/NodePanel';
import NodeConfigDrawer from './components/NodeConfigModal';
import { NodeDefinition } from './types';
import { validateWorkflow } from './utils/validator';
import { addNodeToGraph } from './utils/nodeUtils';
import { applyAutoLayout } from './utils/layoutUtils';
import {
GRID_CONFIG,
CONNECTING_CONFIG,
HIGHLIGHTING_CONFIG,
DEFAULT_STYLES,
} from './constants';
const WorkflowDesign: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [title, setTitle] = useState<string>('工作流设计');
const graphContainerRef = useRef<HTMLDivElement>(null);
const [graph, setGraph] = useState<Graph | null>(null);
const [selectedNode, setSelectedNode] = useState<Cell | null>(null);
const [selectedNodeDefinition, setSelectedNodeDefinition] = useState<NodeDefinition | null>(null);
const [configModalVisible, setConfigModalVisible] = useState(false);
const [definitionData, setDefinitionData] = useState<any>(null);
const [nodeDefinitions, setNodeDefinitions] = useState<NodeDefinition[]>([]);
// 加载节点定义列表
useEffect(() => {
const loadNodeDefinitions = async () => {
try {
const data = await getNodeDefinitionList();
setNodeDefinitions(data);
} catch (error) {
console.error('加载节点定义失败:', error);
message.error('加载节点定义失败');
}
};
loadNodeDefinitions();
}, []);
useEffect(() => {
if (graphContainerRef.current) {
const graph = new Graph({
container: graphContainerRef.current,
grid: GRID_CONFIG,
connecting: CONNECTING_CONFIG,
highlighting: HIGHLIGHTING_CONFIG,
snapline: true,
history: true,
clipboard: true,
selecting: true,
keyboard: true,
background: {
color: '#f5f5f5',
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
factor: 1.1,
maxScale: 1.5,
minScale: 0.5,
},
panning: {
enabled: true,
eventTypes: ['rightMouseDown'],
},
preventDefaultContextMenu: true,
});
// 显示/隐藏连接桩
graph.on('node:mouseenter', ({ node }) => {
const ports = document.querySelectorAll(`[data-cell-id="${node.id}"] .x6-port-body`);
ports.forEach((port) => {
port.setAttribute('style', 'visibility: visible');
});
});
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');
});
});
// 节点双击事件
graph.on('node:dblclick', ({ node }) => {
const nodeType = node.getProp('type');
// 从节点定义列表中找到对应的定义
const definition = nodeDefinitions.find(def => def.type === nodeType);
if (definition) {
setSelectedNode(node);
setSelectedNodeDefinition(definition);
setConfigModalVisible(true);
}
});
setGraph(graph);
// 在 graph 初始化完成后加载数据
if (id) {
loadDefinitionDetail(graph, id);
}
return () => {
graph.dispose();
};
}
}, [graphContainerRef, id, nodeDefinitions]);
const loadDefinitionDetail = async (graphInstance: Graph, definitionId: number) => {
try {
const response = await getDefinitionDetail(definitionId);
setTitle(`工作流设计 - ${response.name}`);
setDefinitionData(response);
// 清空画布
graphInstance.clearCells();
const nodeMap = new Map();
// 创建节点
response.graph.nodes.forEach((nodeData: any) => {
const node = addNodeToGraph(graphInstance, nodeData);
nodeMap.set(nodeData.id, node);
});
// 创建边
response.graph.edges.forEach((edge: any) => {
const sourceNode = nodeMap.get(edge.from);
const targetNode = nodeMap.get(edge.to);
if (sourceNode && targetNode) {
graphInstance.addEdge({
source: { cell: sourceNode.id },
target: { cell: targetNode.id },
attrs: { line: DEFAULT_STYLES.edge },
labels: [{
attrs: { label: { text: edge.name || '' } }
}]
});
}
});
// 应用自动布局
applyAutoLayout(graphInstance);
} catch (error) {
console.error('加载工作流定义失败:', error);
message.error('加载工作流定义失败');
}
};
const handleNodeDragStart = (node: NodeDefinition, 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(graph, node, point);
} catch (error) {
console.error('创建节点失败:', error);
message.error('创建节点失败');
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleNodeConfigUpdate = (values: any) => {
if (!selectedNode) return;
// 更新节点配置
selectedNode.setProp('config', values);
// 更新节点显示名称
if (values.name) {
selectedNode.attr('label/text', values.name);
}
setConfigModalVisible(false);
message.success('节点配置已更新');
};
const handleSaveWorkflow = async () => {
if (!graph || !definitionData) return;
try {
// 校验流程图
const validationResult = validateWorkflow(graph);
if (!validationResult.valid) {
message.error(validationResult.message);
return;
}
// 获取所有节点和边的数据
const nodes = graph.getNodes().map(node => {
const nodeDefinition = node.getProp('nodeDefinition');
const nodeType = node.getProp('type');
return {
id: node.id,
code: nodeType,
type: nodeType,
name: node.attr('label/text'),
graph: {
shape: nodeDefinition?.graphConfig.uiSchema.shape,
size: {
width: node.size().width,
height: node.size().height
},
style: nodeDefinition?.graphConfig.uiSchema.style,
ports: nodeDefinition?.graphConfig.uiSchema.ports
},
config: node.getProp('config') || {}
};
});
const edges = graph.getEdges().map(edge => ({
id: edge.id,
from: edge.getSourceCellId(),
to: edge.getTargetCellId(),
name: edge.getLabels()?.[0]?.attrs?.label?.text || '',
config: {
type: 'sequence'
}
}));
// 构建保存数据
const saveData = {
...definitionData,
graph: {
nodes,
edges
}
};
// 调用保存接口
await saveDefinition(saveData);
message.success('流程保存成功');
} catch (error) {
console.error('保存流程失败:', error);
message.error('保存流程失败');
}
};
return (
<div style={{ padding: '24px' }}>
<Card
title={title}
extra={
<Space>
<Button
icon={<SaveOutlined />}
type="primary"
onClick={handleSaveWorkflow}
>
</Button>
<Button
icon={<PlayCircleOutlined />}
onClick={() => console.log('Deploy workflow')}
>
</Button>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/workflow/definition')}
>
</Button>
</Space>
}
>
<Row gutter={16}>
<Col span={6}>
<NodePanel
nodeDefinitions={nodeDefinitions}
onNodeDragStart={handleNodeDragStart}
/>
</Col>
<Col span={18}>
<Card
size="small"
styles={{
body: {
padding: 0,
height: 'calc(100vh - 250px)',
background: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '4px',
}
}}
>
<div
ref={graphContainerRef}
style={{
width: '100%',
height: '100%',
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
/>
</Card>
</Col>
</Row>
</Card>
<NodeConfigDrawer
visible={configModalVisible}
node={selectedNode}
nodeDefinition={selectedNodeDefinition}
onOk={handleNodeConfigUpdate}
onCancel={() => setConfigModalVisible(false)}
/>
</div>
);
};
export default WorkflowDesign;