import React, { useEffect, useMemo } from 'react'; import { ReactFlow, Background, Panel, SelectionMode, ConnectionLineType, Node, Edge, OnNodesChange, OnEdgesChange, OnConnect, OnReconnect } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import CustomEdge from './components/customEdge'; import CustomConnectionLine from './components/customConnectionLine'; import NodeContextMenu from './components/nodeContextMenu'; import EdgeContextMenu from './components/edgeContextMenu'; import PaneContextMenu from './components/paneContextMenu'; import NodeEditModal from './components/nodeEditModal'; import AddNodeMenu from './components/addNodeMenu'; import ActionBar from './components/actionBar'; import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines'; import { useHistory } from './components/historyContext'; import { NodeTypes } from '@xyflow/react'; import { useSelector } from 'react-redux'; import HandlerBar from '@/pages/flowEditor/components/handlerBar'; import PublishFlowModal from '@/pages/flowEditor/components/publishFlowModal'; const edgeTypes = { custom: CustomEdge }; interface FlowEditorMainProps { nodes: Node[]; edges: Edge[]; nodeTypes: NodeTypes; setNodes: React.Dispatch>; setEdges: React.Dispatch>; useDefault: boolean; readOnly?: boolean; // 新增:只读模式 reactFlowInstance: any; reactFlowWrapper: React.RefObject; menu: any; setMenu: React.Dispatch>; editingNode: Node | null; setEditingNode: React.Dispatch>; isEditModalOpen: boolean; setIsEditModalOpen: React.Dispatch>; isDelete: boolean; setIsDelete: React.Dispatch>; edgeForNodeAdd: Edge | null; setEdgeForNodeAdd: React.Dispatch>; positionForNodeAdd: { x: number, y: number } | null; setPositionForNodeAdd: React.Dispatch>; isRunning: boolean; initialData: any; canvasDataMap: any; // Callbacks onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: OnConnect; onReconnect: OnReconnect; onDragOver: (event: React.DragEvent) => void; onDrop: (event: React.DragEvent) => void; onNodeDrag: (event: React.MouseEvent, node: Node) => void; onNodeDragStop: () => void; onNodeContextMenu: (event: React.MouseEvent, node: Node) => void; onNodeDoubleClick: (event: React.MouseEvent, node: Node) => void; onEdgeContextMenu: (event: React.MouseEvent, edge: Edge) => void; onPaneContextMenu: (event: React.MouseEvent) => void; onPaneClick: () => void; closeEditModal: () => void; saveNodeEdit: (updatedData: any) => void; deleteNode: (node: Node) => void; deleteEdge: (edge: Edge) => void; editNode: (node: Node) => void; editEdge: (edge: Edge) => void; copyNode: (node: Node) => void; pasteNode: (position: { x: number; y: number }) => void; addNodeOnEdge: (nodeType: string, node: any) => void; addNodeOnPane: (nodeType: string, position: { x: number, y: number }, node?: any) => void; handleAddNode: (nodeType: string, node: any) => void; saveFlowDataToServer: () => void; handleRun: (running: boolean) => void; handlePause: (isPaused: boolean) => void; handleReRun: () => void; } const FlowEditorMain: React.FC = (props) => { const { nodes, edges, nodeTypes, setNodes, setEdges, useDefault, readOnly = false, // 解构 readOnly,默认为 false reactFlowInstance, reactFlowWrapper, menu, setMenu, editingNode, setEditingNode, isEditModalOpen, setIsEditModalOpen, isDelete, setIsDelete, edgeForNodeAdd, setEdgeForNodeAdd, positionForNodeAdd, setPositionForNodeAdd, isRunning, initialData, canvasDataMap, onNodesChange, onEdgesChange, onConnect, onReconnect, onDragOver, onDrop, onNodeDrag, onNodeDragStop, onNodeContextMenu, onNodeDoubleClick, onEdgeContextMenu, onPaneContextMenu, onPaneClick, closeEditModal, saveNodeEdit, deleteNode, deleteEdge, editNode, editEdge, copyNode, pasteNode, addNodeOnEdge, addNodeOnPane, handleAddNode, saveFlowDataToServer, handleRun, handlePause, handleReRun } = props; const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines(); const { undo, redo, canUndo, canRedo } = useHistory(); const reactFlowId = useMemo(() => new Date().getTime().toString(), []); // 用于存储隐藏的节点ID const [hiddenNodes, setHiddenNodes] = React.useState>(new Set()); // 流程发布弹窗状态 const [publishModalVisible, setPublishModalVisible] = React.useState(false); // 监听自定义事件以隐藏/显示节点 useEffect(() => { const handleToggleNodeVisibility = (event: CustomEvent) => { const { appId, isVisible } = event.detail; if (isVisible) { // 显示节点 - 从隐藏节点集合中移除 setHiddenNodes(prev => { const newSet = new Set(prev); newSet.delete(appId); return newSet; }); } else { // 隐藏节点 - 添加到隐藏节点集合 setHiddenNodes(prev => new Set(prev).add(appId)); } }; document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener); return () => { document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener); }; }, []); // 从Redux store中获取当前应用的运行状态 const { appRuntimeData, currentAppData } = useSelector((state: any) => state.ideContainer); // 辅助函数:获取当前应用/子流程的唯一标识符 const getCurrentAppKey = () => { if (!currentAppData) return null; // 如果是子流程(key包含'sub'),使用key作为标识符 if (currentAppData.key && currentAppData.key.includes('sub')) { return currentAppData.key; } // 否则使用id return currentAppData.id; }; const currentAppKey = getCurrentAppKey(); const currentAppIsRunning = currentAppKey && appRuntimeData[currentAppKey] ? appRuntimeData[currentAppKey].isRunning : false; // 监听自定义事件以隐藏/显示节点 useEffect(() => { const handleToggleNodeVisibility = (event: CustomEvent) => { const { appId, isVisible } = event.detail; if (isVisible) { // 显示节点 - 从隐藏节点集合中移除 setHiddenNodes(prev => { const newSet = new Set(prev); newSet.delete(appId); return newSet; }); } else { // 隐藏节点 - 添加到隐藏节点集合 setHiddenNodes(prev => new Set(prev).add(appId)); } }; document.addEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener); return () => { document.removeEventListener('toggleNodeVisibility', handleToggleNodeVisibility as EventListener); }; }, []); // 监听键盘事件实现快捷键 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // 如果当前正在运行或不是默认模式,不处理复制粘贴快捷键 const canEdit = !currentAppIsRunning && useDefault; // 兼容 Mac (metaKey) 和 Windows/Linux (ctrlKey) const isModifierKey = e.ctrlKey || e.metaKey; // 检查事件目标是否在可编辑元素中(input、textarea、contenteditable、CodeMirror等) const target = e.target as HTMLElement; const isEditableElement = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target?.getAttribute('contenteditable') === 'true' || target?.closest('[contenteditable="true"]') !== null || target?.closest('.cm-content') !== null || // CodeMirror 编辑器 target?.closest('.cm-editor') !== null || target?.closest('.arco-input') !== null || // Arco Design 输入框 target?.closest('.arco-textarea') !== null; // 检查是否有打开的弹出层(Modal、Drawer、Popover等),如果有则不拦截快捷键 const hasOpenPopup = document.querySelector('.arco-modal-wrapper') !== null || document.querySelector('.arco-drawer-wrapper') !== null || document.querySelector('.arco-popover-popup') !== null; // 如果在可编辑元素中或有打开的弹出层,不拦截复制粘贴快捷键,让浏览器处理 if (isEditableElement || hasOpenPopup) { return; } // Ctrl/Cmd+Z 撤销 if (isModifierKey && e.key === 'z' && !e.shiftKey && canUndo) { e.preventDefault(); undo(); } // Ctrl/Cmd+Shift+Z 重做 if (isModifierKey && e.shiftKey && e.key === 'Z' && canRedo) { e.preventDefault(); redo(); } // Ctrl/Cmd+Y 重做 if (isModifierKey && e.key === 'y' && canRedo) { e.preventDefault(); redo(); } // Ctrl/Cmd+C 复制选中的节点 if (isModifierKey && e.key === 'c' && canEdit) { // 获取当前选中的节点 const selectedNode = nodes.find(node => node.selected); if (selectedNode) { // 不允许复制开始和结束节点 if (selectedNode.type === 'start' || selectedNode.type === 'end') { return; } e.preventDefault(); copyNode(selectedNode); } } // Ctrl/Cmd+V 粘贴节点 if (isModifierKey && e.key === 'v' && canEdit) { const copiedNodeStr = localStorage.getItem('copiedNode'); if (copiedNodeStr) { e.preventDefault(); // 获取当前视口中心位置 if (reactFlowInstance) { const viewport = reactFlowInstance.getViewport(); const { x, y, zoom } = viewport; // 获取画布容器的尺寸 const wrapper = reactFlowWrapper.current; if (wrapper) { const rect = wrapper.getBoundingClientRect(); // 计算视口中心点在流程图坐标系中的位置 const centerX = (-x + rect.width / 2) / zoom; const centerY = (-y + rect.height / 2) / zoom; pasteNode({ x: centerX, y: centerY }); } } } } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [undo, redo, canUndo, canRedo, currentAppIsRunning, useDefault, nodes, copyNode, pasteNode, reactFlowInstance]); // 处理流程发布 const handlePublish = () => { // 先保存流程数据 saveFlowDataToServer(); // 打开发布弹窗 setPublishModalVisible(true); }; // 发布成功后的回调 const handlePublishSuccess = () => { setPublishModalVisible(false); // 可以在这里添加其他逻辑,比如刷新流程列表等 }; // 监听节点和边的变化以拍摄快照 useEffect(() => { // 获取 HistoryProvider 中的 takeSnapshot 方法 const event = new CustomEvent('takeSnapshot', { detail: { nodes: [...nodes], edges: [...edges] } }); document.dispatchEvent(event); }, [nodes, edges]); return (
e.preventDefault()} // 添加点击事件处理器,用于关闭添加节点菜单 onClick={() => { if (edgeForNodeAdd || positionForNodeAdd) { setEdgeForNodeAdd(null); setPositionForNodeAdd(null); } }}> { // 检查节点是否应该被隐藏 const isHidden = hiddenNodes.has(node.id); // 应用透明度样式 const style = isHidden ? { opacity: 0.3 } : {}; return { ...node, draggable: readOnly ? false : !currentAppIsRunning, // readOnly 模式下禁止拖拽 style: { ...node.style, ...style } }; })} edges={edges.map(edge => { // 检查边连接的节点是否被隐藏 const isSourceHidden = hiddenNodes.has(edge.source); const isTargetHidden = hiddenNodes.has(edge.target); // 如果源节点或目标节点被隐藏,则边也应用透明度 const style = (isSourceHidden || isTargetHidden) ? { opacity: 0.3 } : {}; // 更新边的数据,确保选择框也应用透明度 return { ...edge, style: { ...edge.style, ...style }, data: { ...edge.data, ...(isSourceHidden || isTargetHidden ? { hidden: true, opacity: 0.3 } : { opacity: 1 }) } }; })} nodeTypes={nodeTypes} edgeTypes={edgeTypes} snapToGrid={true} snapGrid={[2, 2]} proOptions={{ hideAttribution: true }} // 隐藏水印 nodesConnectable={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用节点连接 nodesDraggable={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用节点拖拽 elementsSelectable={!readOnly} // readOnly 模式下禁用元素选择 connectOnClick={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用点击连接 disableKeyboardA11y={readOnly || currentAppIsRunning} // readOnly 或运行时禁用键盘交互 edgesFocusable={!readOnly} // readOnly 模式下边不可聚焦 nodesFocusable={!readOnly} // readOnly 模式下节点不可聚焦 edgesReconnectable={readOnly ? false : !currentAppIsRunning} // readOnly 或运行时禁用边重连 onBeforeDelete={async ({ nodes, edges }) => { // readOnly 模式下禁止删除 if (readOnly) { return false; } // 在应用编排模式下(useDefault为false)只允许删除边,不允许删除节点 if (!useDefault && nodes.length > 0) { console.warn('在应用编排模式下不允许删除节点'); return false; // 阻止删除节点操作 } // 检查是否有开始或结束节点 const hasStartOrEndNode = nodes.some(node => node.type === 'start' || node.type === 'end'); if (hasStartOrEndNode) { console.warn('开始和结束节点不允许删除'); return false; // 阻止删除操作 } // 检查是否有循环节点(这里只是检查,实际删除逻辑在onNodesDelete中处理) const loopNodes = nodes.filter(node => node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END' ); // 允许删除操作继续进行 return !currentAppIsRunning; // 在运行时禁止删除任何元素 }} onNodesDelete={(deleted) => { // 在应用编排模式下(useDefault为false)不允许删除节点 if (!useDefault) { console.warn('在应用编排模式下不允许删除节点'); return; } // 如果在运行时,禁止删除 if (currentAppIsRunning) { return; } // 检查是否有循环节点 const loopNodes = deleted.filter(node => node.data?.type === 'LOOP_START' || node.data?.type === 'LOOP_END' ); if (loopNodes.length > 0) { // 处理循环节点删除 let nodesToRemove = [...deleted]; // 为每个循环节点找到其配对节点 loopNodes.forEach(loopNode => { const component = loopNode.data?.component as { customDef?: string } | undefined; if (loopNode.data?.type === 'LOOP_START' && component?.customDef) { try { const customDef = JSON.parse(component.customDef); const relatedNodeId = customDef.loopEndNodeId; // 添加关联的结束节点到删除列表 const relatedNode = nodes.find(n => n.id === relatedNodeId); if (relatedNode) { nodesToRemove.push(relatedNode); } } catch (e) { console.error('解析循环开始节点数据失败:', e); } } else if (loopNode.data?.type === 'LOOP_END' && component?.customDef) { try { const customDef = JSON.parse(component.customDef); const relatedNodeId = customDef.loopStartNodeId; // 添加关联的开始节点到删除列表 const relatedNode = nodes.find(n => n.id === relatedNodeId); if (relatedNode) { nodesToRemove.push(relatedNode); } } catch (e) { console.error('解析循环结束节点数据失败:', e); } } }); // 去重 nodesToRemove = nodesToRemove.filter((node, index, self) => index === self.findIndex(n => n.id === node.id) ); // 删除所有相关节点和边 setNodes((nds) => nds.filter((n) => !nodesToRemove.find((d) => d.id === n.id))); // 删除与这些节点相关的所有边 const nodeIdsToRemove = nodesToRemove.map(node => node.id); setEdges((eds) => eds.filter((e) => !nodeIdsToRemove.includes(e.source) && !nodeIdsToRemove.includes(e.target) )); } else { // 普通节点删除 setNodes((nds) => nds.filter((n) => !deleted.find((d) => d.id === n.id))); } setIsEditModalOpen(false); }} onNodesChange={readOnly ? undefined : (!currentAppIsRunning ? onNodesChange : undefined)} // readOnly 或运行时禁用节点变更 onEdgesChange={readOnly ? undefined : (!currentAppIsRunning ? onEdgesChange : undefined)} // readOnly 或运行时禁用边变更 onConnect={readOnly ? undefined : (!currentAppIsRunning ? onConnect : undefined)} // readOnly 或运行时禁用连接 onReconnect={readOnly ? undefined : (!currentAppIsRunning ? onReconnect : undefined)} // readOnly 或运行时禁用重新连接 onDragOver={readOnly ? undefined : (!currentAppIsRunning ? onDragOver : undefined)} // readOnly 或运行时禁用拖拽 onDrop={readOnly ? undefined : (!currentAppIsRunning ? onDrop : undefined)} // readOnly 或运行时禁用放置 onNodeDrag={readOnly ? undefined : (!currentAppIsRunning ? onNodeDrag : undefined)} // readOnly 或运行时禁用节点拖拽 connectionLineType={ConnectionLineType.SmoothStep} connectionLineComponent={CustomConnectionLine} onNodeDragStop={readOnly ? undefined : (!currentAppIsRunning ? onNodeDragStop : undefined)} // readOnly 或运行时禁用节点拖拽停止 onNodeContextMenu={readOnly ? undefined : ((useDefault || currentAppIsRunning) ? onNodeContextMenu : undefined)} // readOnly 或应用编排模式下禁用节点上下文菜单 onNodeDoubleClick={readOnly ? undefined : (!currentAppIsRunning ? onNodeDoubleClick : undefined)} // readOnly 或运行时禁用节点双击 onEdgeContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onEdgeContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用边上下文菜单 onPaneClick={(useDefault || currentAppIsRunning) ? onPaneClick : undefined} // 运行时禁用面板点击 onPaneContextMenu={readOnly ? undefined : ((!currentAppIsRunning && useDefault) ? onPaneContextMenu : undefined)} // readOnly 或运行时或应用编排模式下禁用面板上下文菜单 onEdgeMouseEnter={(_event, edge) => { setEdges((eds) => eds.map(e => { if (e.id === edge.id) { return { ...e, data: { ...e.data, hovered: true } }; } return e; })); }} onEdgeMouseLeave={(_event, edge) => { setEdges((eds) => eds.map(e => { if (e.id === edge.id) { return { ...e, data: { ...e.data, hovered: false } }; } return e; })); }} fitView selectionOnDrag={!currentAppIsRunning} // 运行时禁用拖拽选择 selectionMode={!currentAppIsRunning ? SelectionMode.Partial : undefined} // 运行时禁用选择模式 > {!readOnly && ( )} {useDefault && !readOnly && } {/*节点右键上下文 - 在默认模式或运行状态下显示,运行时隐藏编辑复制删除功能*/} {(useDefault || currentAppIsRunning) && menu && menu.type === 'node' && (
n.id === menu.id)!} onDelete={deleteNode} onEdit={editNode} onCopy={copyNode} onCloseMenu={setMenu} onCloseOpenModal={setIsEditModalOpen} isRunning={currentAppIsRunning} />
)} {/*边右键上下文 - 在非运行时显示,应用编排模式下也允许*/} {!currentAppIsRunning && menu && menu.type === 'edge' && (
e.id === menu.id)!} onDelete={deleteEdge} onEdit={editEdge} onAddNode={(edge) => { setEdgeForNodeAdd(edge); setIsEditModalOpen(false); setMenu(null); // 关闭上下文菜单 }} />
)} {/*画布右键上下文 - 仅在默认模式且非运行时显示*/} {!currentAppIsRunning && useDefault && menu && menu.type === 'pane' && (
{ addNodeOnPane(nodeType, position, node); setIsEditModalOpen(false); setMenu(null); // 关闭上下文菜单 }} />
)} {/*节点点击/节点编辑上下文*/} {/*统一的添加节点菜单 - 仅在默认模式且非运行时显示*/} {!currentAppIsRunning && useDefault && (edgeForNodeAdd || positionForNodeAdd) && (
e.stopPropagation()} > { handleAddNode(nodeType, node); // 关闭菜单 setEdgeForNodeAdd(null); setPositionForNodeAdd(null); }} position={positionForNodeAdd || undefined} edgeId={edgeForNodeAdd?.id} />
)} {/*流程发布弹窗*/} setPublishModalVisible(false)} onSuccess={handlePublishSuccess} appId={initialData?.id} flowId={initialData?.flowId} currentFlowName={initialData?.name} />
); }; export default FlowEditorMain;