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('工作流设计'); const graphContainerRef = useRef(null); const [graph, setGraph] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const [selectedNodeDefinition, setSelectedNodeDefinition] = useState(null); const [configModalVisible, setConfigModalVisible] = useState(false); const [definitionData, setDefinitionData] = useState(null); const [nodeDefinitions, setNodeDefinitions] = useState([]); // 加载节点定义列表 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 (
} >
setConfigModalVisible(false)} />
); }; export default WorkflowDesign;