import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { message } from 'antd'; import { ReactFlowProvider, useReactFlow } from '@xyflow/react'; import { Loader2 } from 'lucide-react'; import WorkflowToolbar from './components/WorkflowToolbar'; import NodePanel from './components/NodePanel'; import FlowCanvas from './components/FlowCanvas'; import NodeConfigModal from './components/NodeConfigModal'; import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal'; import FormPreviewModal from './components/FormPreviewModal'; import KeyboardShortcutsPanel from './components/KeyboardShortcutsPanel'; import type { FlowNode, FlowEdge, FlowNodeData } from './types'; import type { WorkflowNodeDefinition } from './nodes/types'; import { isConfigurableNode } from './nodes/types'; import { useWorkflowSave } from './hooks/useWorkflowSave'; import { useWorkflowLoad } from './hooks/useWorkflowLoad'; import { useHistory } from './hooks/useHistory'; import { generateNodeId, generateEdgeId } from './utils/idGenerator'; import { getDefinitionById as getFormDefinitionById } from '@/pages/Form/Definition/List/service'; import type { FormDefinitionResponse } from '@/pages/Form/Definition/List/types'; // 样式 import '@xyflow/react/dist/style.css'; import './index.less'; const WorkflowDesignInner: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { getNodes, setNodes, getEdges, setEdges, screenToFlowPosition, fitView, zoomIn, zoomOut, getZoom } = useReactFlow(); const [workflowTitle, setWorkflowTitle] = useState('新建工作流'); const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例 const [loading, setLoading] = useState(false); // 加载状态 const [lastSaveTime, setLastSaveTime] = useState(null); // 最后保存时间 const reactFlowWrapper = useRef(null); const autoSaveTimerRef = useRef(null); // 当前工作流ID const currentWorkflowId = id ? parseInt(id) : undefined; // 节点配置模态框状态 const [configModalVisible, setConfigModalVisible] = useState(false); const [configNode, setConfigNode] = useState(null); // 边配置模态框状态 const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false); const [configEdge, setConfigEdge] = useState(null); // 表单定义数据和预览弹窗状态 const [formDefinition, setFormDefinition] = useState(null); const [formPreviewVisible, setFormPreviewVisible] = useState(false); // 快捷键面板状态 const [shortcutsPanelOpen, setShortcutsPanelOpen] = useState(false); // 提取表单字段(用于变量引用) const formFields = useMemo(() => { if (!formDefinition?.schema?.fields) return []; const extractFields = (fields: any[]): any[] => { const result: any[] = []; for (const field of fields) { // 跳过布局组件 if (['text', 'divider'].includes(field.type)) { continue; } // 栅格布局:递归提取子字段 if (field.type === 'grid' && field.children) { for (const column of field.children) { if (Array.isArray(column)) { result.push(...extractFields(column)); } } continue; } // 普通字段 result.push({ name: field.name, label: field.label || field.name, type: field.type, description: field.description, }); } return result; }; return extractFields(formDefinition.schema.fields); }, [formDefinition]); // 保存和加载hooks const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave(); const { workflowDefinition, loadWorkflow } = useWorkflowLoad(); // 历史记录管理 const history = useHistory(); // 剪贴板(用于复制粘贴) const clipboard = useRef<{ nodes: FlowNode[]; edges: FlowEdge[] } | null>(null); // 加载工作流数据 useEffect(() => { const loadData = async () => { if (currentWorkflowId) { setLoading(true); try { const data = await loadWorkflow(currentWorkflowId); if (data) { setNodes(data.nodes); setEdges(data.edges); setWorkflowTitle(data.definition?.name || '未命名工作流'); // 如果工作流关联了表单,加载表单定义数据 if (data.definition?.formDefinitionId) { try { const formDef = await getFormDefinitionById(data.definition.formDefinitionId); setFormDefinition(formDef); console.log('表单定义数据已加载:', formDef); } catch (error) { console.error('加载表单定义失败:', error); } } else { setFormDefinition(null); } } } finally { setLoading(false); } } }; loadData(); }, [currentWorkflowId, loadWorkflow, setNodes, setEdges]); // 初始化缩放比例 useEffect(() => { setCurrentZoom(getZoom()); }, [getZoom]); // 自动适应视图 useEffect(() => { // 延迟执行fitView以确保节点已渲染 const timer = setTimeout(() => { fitView({ padding: 0.1, duration: 800, minZoom: 1.0, // 最小缩放100% maxZoom: 1.0 // 最大缩放100%,确保默认100% }); // 更新zoom显示 setTimeout(() => setCurrentZoom(getZoom()), 850); }, 100); return () => clearTimeout(timer); }, [fitView, getZoom]); // 初始化空白画布 const initialNodes: FlowNode[] = []; const initialEdges: FlowEdge[] = []; // 工具栏事件处理 const handleSave = useCallback(async (isAutoSave = false) => { const nodes = getNodes() as FlowNode[]; const edges = getEdges() as FlowEdge[]; const success = await saveWorkflow({ nodes, edges, workflowId: currentWorkflowId, name: workflowTitle, description: workflowDefinition?.description || '', definitionData: workflowDefinition // 传递原始定义数据 }); if (success) { setLastSaveTime(new Date()); if (!isAutoSave) { console.log('保存工作流成功:', { nodes, edges }); } } }, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]); // 自动保存 useEffect(() => { if (!currentWorkflowId || !hasUnsavedChanges) return; // 清除之前的定时器 if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); } // 30秒后自动保存 autoSaveTimerRef.current = setTimeout(() => { handleSave(true); message.success('已自动保存', 1); }, 30000); return () => { if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); } }; }, [currentWorkflowId, hasUnsavedChanges, handleSave]); const handleBack = useCallback(() => { navigate(-1); // 返回上一页,避免硬编码路径 }, [navigate]); // 预览表单 const handlePreviewForm = useCallback(() => { if (!formDefinition) { message.warning('当前工作流未关联启动表单'); return; } setFormPreviewVisible(true); }, [formDefinition]); // 撤销操作 const handleUndo = useCallback(() => { const state = history.undo(); if (state) { history.pauseRecording(); setNodes(state.nodes); setEdges(state.edges); setTimeout(() => history.resumeRecording(), 100); message.success('已撤销'); } else { message.info('没有可撤销的操作'); } }, [history, setNodes, setEdges]); // 重做操作 const handleRedo = useCallback(() => { const state = history.redo(); if (state) { history.pauseRecording(); setNodes(state.nodes); setEdges(state.edges); setTimeout(() => history.resumeRecording(), 100); message.success('已重做'); } else { message.info('没有可重做的操作'); } }, [history, setNodes, setEdges]); // 复制选中的节点和边 const handleCopy = useCallback(() => { const selectedNodes = getNodes().filter(node => node.selected); const selectedEdges = getEdges().filter(edge => { // 只复制两端都被选中的边 return selectedNodes.some(n => n.id === edge.source) && selectedNodes.some(n => n.id === edge.target); }); if (selectedNodes.length > 0) { clipboard.current = { nodes: JSON.parse(JSON.stringify(selectedNodes)), edges: JSON.parse(JSON.stringify(selectedEdges)) }; message.success(`已复制 ${selectedNodes.length} 个节点`); } else { message.warning('请先选择要复制的节点'); } }, [getNodes, getEdges]); // 粘贴节点 const handlePaste = useCallback(() => { if (!clipboard.current || clipboard.current.nodes.length === 0) { message.info('剪贴板为空'); return; } const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current; const offset = 50; // 粘贴偏移量 const idMap = new Map(); // 创建新节点(带偏移) const newNodes = copiedNodes.map(node => { const newId = generateNodeId(); // ✅ 使用标准ID生成函数: sid_xxxxxxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx idMap.set(node.id, newId); return { ...node, id: newId, position: { x: node.position.x + offset, y: node.position.y + offset }, selected: true }; }); // 创建新边(更新source和target为新ID) const newEdges = copiedEdges.map(edge => { const newSource = idMap.get(edge.source); const newTarget = idMap.get(edge.target); if (!newSource || !newTarget) return null; return { ...edge, id: generateEdgeId(), // ✅ 使用标准ID生成函数: eid_xxxxxxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx source: newSource, target: newTarget, selected: true }; }).filter(edge => edge !== null) as FlowEdge[]; // 取消其他元素的选中状态 setNodes(nodes => [ ...nodes.map(n => ({ ...n, selected: false })), ...newNodes ]); setEdges(edges => [ ...edges.map(e => ({ ...e, selected: false })), ...newEdges ]); message.success(`已粘贴 ${newNodes.length} 个节点`); }, [setNodes, setEdges]); const handleDelete = useCallback(() => { const selectedNodes = getNodes().filter(node => node.selected); const selectedEdges = getEdges().filter(edge => edge.selected); if (selectedNodes.length > 0 || selectedEdges.length > 0) { setNodes(nodes => nodes.filter(node => !node.selected)); setEdges(edges => edges.filter(edge => !edge.selected)); message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`); markUnsaved(); } else { message.warning('请先选择要删除的元素'); } }, [getNodes, getEdges, setNodes, setEdges, markUnsaved]); const handleSelectAll = useCallback(() => { setNodes(nodes => nodes.map(node => ({ ...node, selected: true }))); setEdges(edges => edges.map(edge => ({ ...edge, selected: true }))); message.info('已全选所有元素'); }, [setNodes, setEdges]); const handleFitView = useCallback(() => { fitView({ padding: 0.2, duration: 800 }); // 延迟更新zoom值以获取最新的缩放比例 setTimeout(() => setCurrentZoom(getZoom()), 850); }, [fitView, getZoom]); const handleZoomIn = useCallback(() => { zoomIn({ duration: 300 }); // 延迟更新zoom值以获取最新的缩放比例 setTimeout(() => setCurrentZoom(getZoom()), 350); }, [zoomIn, getZoom]); const handleZoomOut = useCallback(() => { zoomOut({ duration: 300 }); // 延迟更新zoom值以获取最新的缩放比例 setTimeout(() => setCurrentZoom(getZoom()), 350); }, [zoomOut, getZoom]); // 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法 const handleDrop = useCallback((event: React.DragEvent) => { event.preventDefault(); const dragData = event.dataTransfer.getData('application/reactflow'); if (!dragData) return; try { const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData); // 根据React Flow官方文档,screenToFlowPosition会自动处理所有边界计算 // 不需要手动减去容器边界! const position = screenToFlowPosition({ x: event.clientX, y: event.clientY, }); const newNode: FlowNode = { id: generateNodeId(), // ✅ 生成格式: sid_xxxxxxxx_xxxx_xxxx_xxxx_xxxxxxxxxxxx type: nodeType, position, data: { label: nodeDefinition.nodeName, nodeType: nodeDefinition.nodeType, category: nodeDefinition.category, icon: nodeDefinition.renderConfig.icon.content, color: nodeDefinition.renderConfig.theme.primary, // 保存原始节点定义引用,用于配置 nodeDefinition, // 初始化配置数据(nodeCode 使用预定义的常量值) configs: { nodeName: nodeDefinition.nodeName, nodeCode: nodeDefinition.nodeCode, // 使用预定义的 CODE(如 "END_EVENT") description: nodeDefinition.description }, inputMapping: {}, outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : [] // ✅ 从节点定义中获取输出能力 } }; setNodes(nodes => [...nodes, newNode]); message.success(`已添加 ${nodeDefinition.nodeName} 节点`); } catch (error) { console.error('解析拖拽数据失败:', error); message.error('添加节点失败'); } }, [screenToFlowPosition, setNodes]); // 处理节点双击 - 打开配置面板 const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => { // 只处理双击事件 if (event.detail === 2) { console.log('双击节点,打开配置:', node); setConfigNode(node); setConfigModalVisible(true); } }, []); // 处理边双击 - 打开条件配置弹窗 const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => { if (event.detail === 2) { console.log('双击边,打开配置:', edge); // 检查源节点是否为并行网关 const sourceNode = getNodes().find(n => n.id === edge.source); const gatewayType = (sourceNode?.data?.inputMapping as any)?.gatewayType || (sourceNode?.data?.configs as any)?.gatewayType; const isParallelGateway = sourceNode?.data?.nodeType === 'GATEWAY_NODE' && gatewayType === 'parallelGateway'; // 并行网关的边无需配置,直接提示 if (isParallelGateway) { message.info('并行网关的所有分支自动执行,无需配置条件'); return; } setConfigEdge(edge); setEdgeConfigModalVisible(true); } }, [getNodes]); // 处理节点配置更新 const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial) => { setNodes((nodes) => nodes.map((node) => node.id === nodeId ? { ...node, data: { ...node.data, ...updatedData, }, } : node ) ); markUnsaved(); // 标记有未保存更改 }, [setNodes, markUnsaved]); // 关闭节点配置模态框 const handleCloseConfigModal = useCallback(() => { setConfigModalVisible(false); setConfigNode(null); }, []); // 处理边条件更新 const handleEdgeConditionUpdate = useCallback((edgeId: string, condition: EdgeCondition) => { setEdges((edges) => edges.map((edge) => { if (edge.id === edgeId) { // 根据条件类型生成显示文本 const label = condition.type === 'EXPRESSION' ? condition.expression : '默认路径'; return { ...edge, data: { ...edge.data, condition, }, label, }; } return edge; }) ); markUnsaved(); // 标记有未保存更改 message.success('边条件配置已更新'); setEdgeConfigModalVisible(false); setConfigEdge(null); }, [setEdges, markUnsaved]); // 关闭边配置模态框 const handleCloseEdgeConfigModal = useCallback(() => { setEdgeConfigModalVisible(false); setConfigEdge(null); }, []); // 监听视图变化(缩放、平移等) const handleViewportChange = useCallback(() => { const zoom = getZoom(); setCurrentZoom(zoom); }, [getZoom]); // 监听节点和边的变化,记录到历史 useEffect(() => { const nodes = getNodes() as FlowNode[]; const edges = getEdges() as FlowEdge[]; // 只在有节点或边时记录(避免空状态) if (nodes.length > 0 || edges.length > 0) { history.record(nodes, edges); } }, [getNodes, getEdges, history]); // 键盘快捷键支持 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 检查焦点是否在输入框、文本域或可编辑元素内 const target = e.target as HTMLElement; const tagName = target.tagName?.toUpperCase(); const isInputElement = tagName === 'INPUT' || tagName === 'TEXTAREA' || target.isContentEditable || target.getAttribute('contenteditable') === 'true'; // 检查是否在 shadcn/ui Sheet、Dialog 或其他模态框内 const isInSheet = target.closest('[role="dialog"]') !== null; const isInModal = target.closest('[role="alertdialog"]') !== null; const isInPopover = target.closest('[role="menu"]') !== null || target.closest('[role="listbox"]') !== null || target.closest('[data-radix-popper-content-wrapper]') !== null; // 在模态框或弹窗内,或者在输入元素中时,允许原生行为 const shouldSkipShortcut = isInputElement || isInSheet || isInModal || isInPopover; const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const ctrlKey = isMac ? e.metaKey : e.ctrlKey; // Ctrl+Z / Cmd+Z - 撤销(仅在画布区域) if (ctrlKey && e.key === 'z' && !e.shiftKey) { if (!shouldSkipShortcut) { e.preventDefault(); handleUndo(); } } // Ctrl+Shift+Z / Cmd+Shift+Z - 重做(仅在画布区域) else if (ctrlKey && e.key === 'z' && e.shiftKey) { if (!shouldSkipShortcut) { e.preventDefault(); handleRedo(); } } // Ctrl+C / Cmd+C - 复制节点(仅在画布区域) else if (ctrlKey && e.key === 'c') { if (!shouldSkipShortcut) { e.preventDefault(); handleCopy(); } } // Ctrl+V / Cmd+V - 粘贴节点(仅在画布区域) else if (ctrlKey && e.key === 'v') { if (!shouldSkipShortcut) { e.preventDefault(); handlePaste(); } } // Ctrl+A / Cmd+A - 全选节点(仅在画布区域) else if (ctrlKey && e.key === 'a') { if (!shouldSkipShortcut) { e.preventDefault(); handleSelectAll(); } } // Delete / Backspace - 删除节点(仅在画布区域) else if (e.key === 'Delete' || e.key === 'Backspace') { if (!shouldSkipShortcut) { e.preventDefault(); handleDelete(); } } // ? - 显示快捷键面板 else if (e.key === '?' && !shouldSkipShortcut) { e.preventDefault(); setShortcutsPanelOpen(true); } // Ctrl+S / Cmd+S - 保存 else if (ctrlKey && e.key === 's') { e.preventDefault(); handleSave(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete, handleSave]); return (
{/* 加载状态 */} {loading && (

加载工作流中...

)} {/* 工具栏 */} handleSave(false)} onBack={handleBack} onPreviewForm={handlePreviewForm} hasFormDefinition={!!formDefinition} onUndo={handleUndo} onRedo={handleRedo} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onFitView={handleFitView} canUndo={history.canUndo} canRedo={history.canRedo} zoom={currentZoom} /> {/* 主要内容区域 */}
{/* 节点面板 */} {/* 画布区域 */}
{/* 节点配置弹窗 */} {/* 边条件配置弹窗 */} {/* 表单预览弹窗 */} setFormPreviewVisible(false)} formDefinition={formDefinition} /> {/* 快捷键面板 */} setShortcutsPanelOpen(false)} /> {/* 最后保存时间提示 */} {lastSaveTime && (
最后保存: {lastSaveTime.toLocaleTimeString()}
)}
); }; const WorkflowDesign: React.FC = () => { return ( ); }; export default WorkflowDesign;