deploy-ease-platform/frontend/src/pages/Workflow/Definition/Design/index.tsx
dengqichen 0893ea614a 1
2024-12-13 10:38:59 +08:00

386 lines
14 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 { DagreLayout } from '@antv/layout';
import { getDefinitionDetail, saveDefinition } from '../service';
import NodePanel from './components/NodePanel';
import NodeConfigDrawer from './components/NodeConfigModal';
import { NodeDefinition } from './types';
import { validateWorkflow } from './utils/validator';
import {
GRID_CONFIG,
CONNECTING_CONFIG,
HIGHLIGHTING_CONFIG,
DEFAULT_STYLES, generatePortGroups, generatePortItems,
} 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);
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',
},
});
// 显示/隐藏连接桩
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 nodeDefinition = node.getProp('nodeDefinition');
setSelectedNode(node);
setSelectedNodeDefinition(nodeDefinition);
setConfigModalVisible(true);
});
setGraph(graph);
// 在 graph 初始化完成后加载数据
if (id) {
loadDefinitionDetail(graph, id);
}
return () => {
graph.dispose();
};
}
}, [graphContainerRef, id]);
const loadDefinitionDetail = async (graphInstance: Graph, definitionId: number) => {
try {
const response = await getDefinitionDetail(definitionId);
console.log('加载到的工作流数据:', response);
setTitle(`工作流设计 - ${response.name}`);
setDefinitionData(response);
// 清空画布
graphInstance.clearCells();
const nodeMap = new Map();
// 动态注册节点类型
response.graph.nodes.forEach((nodeData: any) => {
// 根据节点类型动态注册
const nodeConfig = {
inherit: 'rect', // 使用基础的矩形节点作为默认继承
width: nodeData.graph.size.width,
height: nodeData.graph.size.height,
attrs: {
body: nodeData.graph.style,
},
ports: nodeData.graph.ports
};
// 根据形状选择不同的基础节点类型
if (nodeData.graph.shape === 'circle') {
nodeConfig.inherit = 'circle';
} else if (nodeData.graph.shape === 'polygon') {
nodeConfig.inherit = 'polygon';
}
// 注册节点类型
Graph.registerNode(nodeData.code, nodeConfig, true);
// 创建节点
const node = graphInstance.addNode({
id: nodeData.id,
shape: nodeData.code,
position: nodeData.graph.position || { x: 100, y: 100 },
attrs: {
body: nodeData.graph.style,
label: {
text: nodeData.name,
fill: '#000000',
fontSize: 12,
}
},
nodeDefinition: 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 || '',
},
},
},
],
});
}
});
// 自动布局
const layout = new DagreLayout({
type: 'dagre',
rankdir: 'LR',
align: 'UL',
ranksep: 80,
nodesep: 50,
});
const cells = graphInstance.getCells();
layout.layout(cells);
graphInstance.resetCells(cells);
graphInstance.centerContent();
} 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) {
const nodeData = e.dataTransfer.getData('node');
if (nodeData) {
const node = JSON.parse(nodeData);
const { clientX, clientY } = e;
const point = graph.clientToLocal({ x: clientX, y: clientY });
const nodeConfig = {
shape: node.graphConfig.uiSchema.shape,
width: node.graphConfig.uiSchema.size.width,
height: node.graphConfig.uiSchema.size.height,
attrs: {
body: {
fill: node.graphConfig.uiSchema.style.fill,
stroke: node.graphConfig.uiSchema.style.stroke,
strokeWidth: node.graphConfig.uiSchema.style.strokeWidth,
},
label: {
text: node.name,
fill: '#000000',
fontSize: 12,
},
},
ports: {
groups: generatePortGroups(),
items: generatePortItems(),
},
};
// 创建节点时设置完整的属性
graph.addNode({
...nodeConfig,
x: point.x,
y: point.y,
// 保存完整的节点信息
type: node.type,
code: node.code,
graph: {
shape: node.graphConfig.uiSchema.shape,
size: node.graphConfig.uiSchema.size,
style: node.graphConfig.uiSchema.style,
ports: node.graphConfig.uiSchema.ports
},
// 保存节点定义,用于后续编辑
nodeDefinition: node,
});
}
}
};
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 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;