386 lines
14 KiB
TypeScript
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;
|