import { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Form, Input, message, Spin } from 'antd'; import { ReactFlow, Controls, Background, addEdge, useNodesState, useEdgesState, type Connection, type Node, type Edge, BackgroundVariant, MarkerType, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { type CreateProcessDefinitionRequest, type NodeDef, type EdgeDef, getProcessDefinition, } from '../../api/workflowDefinitions'; const NODE_TYPES_MAP: Record = { StartEvent: { label: '开始', color: '#52c41a' }, EndEvent: { label: '结束', color: '#ff4d4f' }, UserTask: { label: '用户任务', color: '#1890ff' }, ServiceTask: { label: '服务任务', color: '#722ed1' }, ExclusiveGateway: { label: '排他网关', color: '#fa8c16' }, ParallelGateway: { label: '并行网关', color: '#13c2c2' }, }; const PALETTE_ITEMS = Object.entries(NODE_TYPES_MAP).map(([type, info]) => ({ type, label: info.label, color: info.color, })); function createFlowNode(type: string, label: string, position: { x: number; y: number }, id?: string): Node { return { id: id || `node_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, type: 'default', position, data: { label: `${label}`, nodeType: type, name: label }, style: { background: NODE_TYPES_MAP[type]?.color || '#f0f0f0', color: '#fff', padding: '8px 16px', borderRadius: type.includes('Gateway') ? 0 : type === 'StartEvent' || type === 'EndEvent' ? 50 : 6, fontSize: 13, fontWeight: 500, border: '2px solid rgba(255,255,255,0.3)', width: type.includes('Gateway') ? 80 : 140, textAlign: 'center' as const, }, }; } interface ProcessDesignerProps { definitionId: string | null; onSave: (req: CreateProcessDefinitionRequest, id?: string) => void; } export default function ProcessDesigner({ definitionId, onSave }: ProcessDesignerProps) { const [form] = Form.useForm(); const [selectedNode, setSelectedNode] = useState(null); const [loading, setLoading] = useState(false); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const isEditing = definitionId !== null; // 加载流程定义(编辑模式)或初始化默认节点(新建模式) useEffect(() => { if (!definitionId) { const startNode = createFlowNode('StartEvent', '开始', { x: 250, y: 50 }); const userNode = createFlowNode('UserTask', '审批', { x: 250, y: 200 }); const endNode = createFlowNode('EndEvent', '结束', { x: 250, y: 400 }); setNodes([startNode, userNode, endNode]); setEdges([ { id: 'e_start_approve', source: startNode.id, target: userNode.id, markerEnd: { type: MarkerType.ArrowClosed } }, { id: 'e_approve_end', source: userNode.id, target: endNode.id, markerEnd: { type: MarkerType.ArrowClosed } }, ]); return; } setLoading(true); getProcessDefinition(definitionId) .then((def) => { form.setFieldsValue({ name: def.name, key: def.key, category: def.category, description: def.description, }); const flowNodes = def.nodes.map((n, i) => createFlowNode(n.type, n.name, n.position || { x: 200, y: i * 120 + 50 }, n.id) ); const flowEdges: Edge[] = def.edges.map((e) => ({ id: e.id, source: e.source, target: e.target, markerEnd: { type: MarkerType.ArrowClosed }, label: e.label || e.condition, })); setNodes(flowNodes); setEdges(flowEdges); }) .catch(() => message.error('加载流程定义失败')) .finally(() => setLoading(false)); }, [definitionId]); // eslint-disable-line react-hooks/exhaustive-deps const onConnect = useCallback( (connection: Connection) => { setEdges((eds) => addEdge( { ...connection, markerEnd: { type: MarkerType.ArrowClosed } }, eds, ), ); }, [setEdges], ); const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => { setSelectedNode(node); }, []); const handleAddNode = (type: string) => { const info = NODE_TYPES_MAP[type]; if (!info) return; const newNode = createFlowNode(type, info.label, { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300, }); setNodes((nds) => [...nds, newNode]); }; const handleDeleteNode = () => { if (!selectedNode) return; setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id)); setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id), ); setSelectedNode(null); }; const handleUpdateNodeName = (name: string) => { if (!selectedNode) return; setNodes((nds) => nds.map((n) => n.id === selectedNode.id ? { ...n, data: { ...n.data, label: name, name } } : n, ), ); setSelectedNode((prev) => (prev ? { ...prev, data: { ...prev.data, label: name, name } } : null)); }; const handleSave = () => { form.validateFields().then((values) => { const flowNodes: NodeDef[] = nodes.map((n) => ({ id: n.id, type: (n.data.nodeType as NodeDef['type']) || 'UserTask', name: String(n.data.name || n.data.label || ''), position: { x: Math.round(n.position.x), y: Math.round(n.position.y) }, })); const flowEdges: EdgeDef[] = edges.map((e) => ({ id: e.id, source: e.source, target: e.target, label: e.label ? String(e.label) : undefined, })); onSave( { ...values, nodes: flowNodes, edges: flowEdges }, definitionId || undefined, ); }).catch(() => { message.error('请填写必要字段'); }); }; const defaultEdgeOptions = useMemo( () => ({ markerEnd: { type: MarkerType.ArrowClosed }, }), [], ); if (loading) { return
; } return (
{/* 左侧工具面板 */}

添加节点

{PALETTE_ITEMS.map((item) => ( ))} {selectedNode && (

节点属性

handleUpdateNodeName(e.target.value)} placeholder="节点名称" style={{ marginBottom: 8 }} />
)}
{/* 中间画布 */}
{/* 右侧表单 */}
); }