333 lines
12 KiB
TypeScript
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;
|