import React, { useState, useCallback, useRef, useEffect } from 'react'; import { ReactFlow, applyNodeChanges, applyEdgeChanges, addEdge, reconnectEdge, Background, Controls, Node, Edge, ReactFlowProvider, useReactFlow, EdgeTypes, SelectionMode, useStoreApi, Panel, ConnectionLineType } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { Button, Modal } from '@arco-design/web-react'; import { nodeTypeMap, nodeTypes, registerNodeType } from './node'; import SideBar from './sideBar/sideBar'; import { convertFlowData } from '@/utils/convertFlowData'; import { exampleFlowData } from '@/pages/flowEditor/test/exampleFlowData'; import LocalNode from '@/pages/flowEditor/node/localNode/LocalNode'; 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 { defaultNodeTypes } from '@/pages/flowEditor/node/types/defaultType'; import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData'; import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines'; const edgeTypes: EdgeTypes = { custom: CustomEdge }; const FlowEditorWithProvider: React.FC = () => { return (
e.preventDefault()}> {/**/}
); }; const FlowEditor: React.FC = () => { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const reactFlowInstance = useReactFlow(); const reactFlowWrapper = useRef(null); const [menu, setMenu] = useState<{ id: string; type: 'node' | 'edge' | 'pane'; top: number; left: number; position?: { x: number; y: number }; } | null>(null); const store = useStoreApi(); // 添加编辑弹窗相关状态 const [editingNode, setEditingNode] = useState(null); const [isEditModalOpen, setIsEditModalOpen] = useState(false); // 添加节点选择弹窗状态 const [edgeForNodeAdd, setEdgeForNodeAdd] = useState(null); const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null); const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines(); // 获取handle类型 (api或data) const getHandleType = (handleId: string, nodeParams: any) => { // 检查是否为api类型的handle const apiOuts = nodeParams.apiOuts || []; const apiIns = nodeParams.apiIns || []; if (apiOuts.some((api: any) => api.name === handleId) || apiIns.some((api: any) => api.name === handleId)) { return 'api'; } // 检查是否为data类型的handle const dataOuts = nodeParams.dataOuts || []; const dataIns = nodeParams.dataIns || []; if (dataOuts.some((data: any) => data.name === handleId) || dataIns.some((data: any) => data.name === handleId)) { return 'data'; } // 默认为data类型 return 'data'; }; // 验证数据类型是否匹配 const validateDataType = (sourceNode: defaultNodeTypes, targetNode: defaultNodeTypes, sourceHandleId: string, targetHandleId: string) => { const sourceParams = sourceNode.data?.parameters || {}; const targetParams = targetNode.data?.parameters || {}; // 获取源节点的输出参数 let sourceDataType = ''; const sourceApiOuts = sourceParams.apiOuts || []; const sourceDataOuts = sourceParams.dataOuts || []; // 查找源handle的数据类型 const sourceApi = sourceApiOuts.find((api: any) => api.name === sourceHandleId); const sourceData = sourceDataOuts.find((data: any) => data.name === sourceHandleId); if (sourceApi) { sourceDataType = sourceApi.dataType || ''; } else if (sourceData) { sourceDataType = sourceData.dataType || ''; } // 获取目标节点的输入参数 let targetDataType = ''; const targetApiIns = targetParams.apiIns || []; const targetDataIns = targetParams.dataIns || []; // 查找目标handle的数据类型 const targetApi = targetApiIns.find((api: any) => api.name === targetHandleId); const targetData = targetDataIns.find((data: any) => data.name === targetHandleId); if (targetApi) { targetDataType = targetApi.dataType || ''; } else if (targetData) { targetDataType = targetData.dataType || ''; } // 如果任一数据类型为空,则允许连接 if (!sourceDataType || !targetDataType) { return true; } // 比较数据类型是否匹配 return sourceDataType === targetDataType; }; const onNodesChange = useCallback( (changes: any) => { const newNodes = applyNodeChanges(changes, nodes); setNodes(newNodes); // 如果需要在节点变化时执行某些操作,可以在这里添加 onPaneClick(); }, [nodes] ); const onEdgesChange = useCallback( (changes: any) => { const newEdges = applyEdgeChanges(changes, edges); setEdges(newEdges); // 如果需要在边变化时执行某些操作,可以在这里添加 onPaneClick(); }, [edges] ); const onConnect = useCallback( (params: any) => { // 获取源节点和目标节点 const sourceNode = nodes.find(node => node.id === params.source); const targetNode = nodes.find(node => node.id === params.target); // 如果找不到节点,不创建连接 if (!sourceNode || !targetNode) { return; } // 获取源节点和目标节点的参数信息 const sourceParams = sourceNode.data?.parameters || {}; const targetParams = targetNode.data?.parameters || {}; // 获取源handle和目标handle的类型 (api或data) const sourceHandleType = getHandleType(params.sourceHandle, sourceParams); const targetHandleType = getHandleType(params.targetHandle, targetParams); // 验证连接类型是否匹配 (api只能连api, data只能连data) if (sourceHandleType !== targetHandleType) { console.warn('连接类型不匹配: ', sourceHandleType, targetHandleType); return; } // 验证数据类型是否匹配 if (!validateDataType(sourceNode, targetNode, params.sourceHandle, params.targetHandle)) { console.warn('数据类型不匹配'); return; } // TODO 目前是临时定义,存在事件数据的时候才展示 // params.data = { // displayData: { // label: '测试', // value: '数据展示' // } // }; // 如果验证通过,创建连接 setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot)); }, [nodes] ); // 边重新连接处理 const onReconnect = useCallback( (oldEdge: Edge, newConnection: any) => { // 获取源节点和目标节点 const sourceNode = nodes.find(node => node.id === newConnection.source); const targetNode = nodes.find(node => node.id === newConnection.target); // 如果找不到节点,不创建连接 if (!sourceNode || !targetNode) { return; } // 获取源节点和目标节点的参数信息 const sourceParams = sourceNode.data?.parameters || {}; const targetParams = targetNode.data?.parameters || {}; // 获取源handle和目标handle的类型 (api或data) const sourceHandleType = getHandleType(newConnection.sourceHandle, sourceParams); const targetHandleType = getHandleType(newConnection.targetHandle, targetParams); // 验证连接类型是否匹配 (api只能连api, data只能连data) if (sourceHandleType !== targetHandleType) { console.warn('连接类型不匹配: ', sourceHandleType, targetHandleType); return; } // 验证数据类型是否匹配 if (!validateDataType(sourceNode, targetNode, newConnection.sourceHandle, newConnection.targetHandle)) { console.warn('数据类型不匹配'); return; } // 如果验证通过,重新连接 setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); }, [nodes] ); const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // 侧边栏节点实例 const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); if (!reactFlowInstance) return; const callBack = event.dataTransfer.getData('application/reactflow'); const nodeData = JSON.parse(callBack); if (typeof nodeData.nodeType === 'undefined' || !nodeData.nodeType) { return; } const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }); const newNode = { id: `${nodeData.nodeType}-${Date.now()}`, type: nodeData.nodeType, position, data: { ...nodeData.data, title: nodeData.nodeName, type: nodeData.nodeType } }; // 将未定义的节点动态追加进nodeTypes const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key)); // 目前默认添加的都是系统组件/本地组件 if (!nodeMap.includes(nodeData.nodeType)) registerNodeType(nodeData.nodeType, LocalNode, nodeData.nodeName); setNodes((nds) => nds.concat(newNode)); }, [reactFlowInstance] ); const onNodeDrag = useCallback( (_: any, node: Node) => { // 获取对齐线 getGuidelines(node, nodes); }, [nodes, getGuidelines] ); // 节点拖拽结束处理 const onNodeDragStop = useCallback(() => { // 清除对齐线 clearGuidelines(); }, [clearGuidelines]); useEffect(() => { const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData); // 为所有边添加类型 const initialEdges: Edge[] = convertedEdges.map(edge => ({ ...edge, type: 'custom' })); setNodes(convertedNodes); setEdges(initialEdges); }, []); // 监听边的变化,处理添加节点的触发 useEffect(() => { const edgeToAddNode = edges.find(edge => edge.data?.addNodeTrigger); const pane = reactFlowWrapper.current?.getBoundingClientRect(); if (edgeToAddNode) { edgeToAddNode.data.y = (edgeToAddNode.data.clientY as number) - pane.top; edgeToAddNode.data.x = (edgeToAddNode.data.clientX as number) - pane.left; setEdgeForNodeAdd(edgeToAddNode); // 清除触发标志 setEdges(eds => eds.map(edge => { if (edge.id === edgeToAddNode.id) { const { addNodeTrigger, ...restData } = edge.data || {}; return { ...edge, data: restData }; } return edge; })); } }, [edges]); // 节点右键菜单处理 const onNodeContextMenu = useCallback( (event: React.MouseEvent, node: Node) => { event.preventDefault(); const pane = reactFlowWrapper.current?.getBoundingClientRect(); if (!pane) return; setMenu({ id: node.id, type: 'node', top: event.clientY - pane.top, left: event.clientX - pane.left }); }, [setMenu] ); // 节点双击处理 const onNodeDoubleClick = useCallback( (event: React.MouseEvent, node: Node) => { // 不可编辑的类型 if (['AND', 'OR', 'JSON2STR', 'STR2JSON'].includes(node.type)) return; setEditingNode(node); setIsEditModalOpen(true); }, [] ); // 边右键菜单处理 const onEdgeContextMenu = useCallback( (event: React.MouseEvent, edge: Edge) => { event.preventDefault(); const pane = reactFlowWrapper.current?.getBoundingClientRect(); if (!pane) return; setMenu({ id: edge.id, type: 'edge', top: event.clientY - pane.top, left: event.clientX - pane.left }); }, [setMenu] ); // 画布右键菜单处理 const onPaneContextMenu = useCallback( (event: React.MouseEvent) => { event.preventDefault(); const pane = reactFlowWrapper.current?.getBoundingClientRect(); if (!pane || !reactFlowInstance) return; // 计算在画布中的位置 const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY }); setMenu({ id: 'pane-context-menu', type: 'pane', top: event.clientY - pane.top, left: event.clientX - pane.left, position }); }, [reactFlowInstance] ); // 点击画布其他区域关闭菜单 const onPaneClick = useCallback(() => { setMenu(null); // 关闭添加节点菜单 setEdgeForNodeAdd(null); setPositionForNodeAdd(null); }, [setMenu]); // 关闭编辑弹窗 const closeEditModal = useCallback(() => { setIsEditModalOpen(false); setEditingNode(null); }, []); // 保存节点编辑 const saveNodeEdit = useCallback((updatedData: any) => { console.log('updatedData:', updatedData); const updatedNodes = nodes.map((node) => { if (node.id === editingNode?.id) { return { ...node, data: { ...node.data, ...updatedData } }; } return node; }); setNodes(updatedNodes); closeEditModal(); // TODO 如果需要在节点编辑后立即保存到服务器,可以调用保存函数 // saveFlowDataToServer(); }, [nodes, editingNode, closeEditModal]); // 删除节点 const deleteNode = useCallback((node: Node) => { setNodes((nds) => nds.filter((n) => n.id !== node.id)); setEdges((eds) => eds.filter((e) => e.source !== node.id && e.target !== node.id)); setMenu(null); }, []); // 删除边 const deleteEdge = useCallback((edge: Edge) => { setEdges((eds) => eds.filter((e) => e.id !== edge.id)); setMenu(null); }, []); // 编辑节点 const editNode = useCallback((node: Node) => { setMenu(null); setEditingNode(node); setIsEditModalOpen(true); }, []); // 编辑边 const editEdge = useCallback((edge: Edge) => { // 这里可以实现边编辑逻辑 console.log('编辑边:', edge); setMenu(null); }, []); // 复制节点 const copyNode = useCallback((node: Node) => { // 这里可以实现节点复制逻辑 console.log('复制节点:', node); setMenu(null); }, []); // 在边上添加节点的具体实现 const addNodeOnEdge = useCallback((nodeType: string) => { if (!edgeForNodeAdd || !reactFlowInstance) return; // 查找节点定义 const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType); if (!nodeDefinition) return; // 获取源节点和目标节点 const sourceNode = nodes.find(n => n.id === edgeForNodeAdd.source); const targetNode = nodes.find(n => n.id === edgeForNodeAdd.target); if (!sourceNode || !targetNode) return; // 计算中点位置 const position = { x: (sourceNode.position.x + targetNode.position.x) / 2, y: (sourceNode.position.y + targetNode.position.y) / 2 }; // 创建新节点 const newNode = { id: `${nodeType}-${Date.now()}`, type: nodeType, position, data: { ...nodeDefinition.data, title: nodeDefinition.nodeName, type: nodeType } }; // 将未定义的节点动态追加进nodeTypes const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key)); if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeDefinition.nodeName); // 添加新节点 setNodes((nds) => [...nds, newNode]); // 删除旧边 setEdges((eds) => eds.filter(e => e.id !== edgeForNodeAdd.id)); // 创建新边: source -> new node, new node -> target setEdges((eds) => [ ...eds, { id: `e${edgeForNodeAdd.source}-${newNode.id}`, source: edgeForNodeAdd.source, target: newNode.id, type: 'custom' }, { id: `e${newNode.id}-${edgeForNodeAdd.target}`, source: newNode.id, target: edgeForNodeAdd.target, type: 'custom' } ]); // 关闭菜单 setEdgeForNodeAdd(null); setPositionForNodeAdd(null); }, [edgeForNodeAdd, nodes, reactFlowInstance]); // 在画布上添加节点 const addNodeOnPane = useCallback((nodeType: string, position: { x: number; y: number }) => { setMenu(null); if (!reactFlowInstance) return; // 查找节点定义 const nodeDefinition = localNodeData.find(n => n.nodeType === nodeType); if (!nodeDefinition) return; // 创建新节点 const newNode = { id: `${nodeType}-${Date.now()}`, type: nodeType, position, data: { ...nodeDefinition.data, title: nodeDefinition.nodeName, type: nodeType } }; // 将未定义的节点动态追加进nodeTypes const nodeMap = Array.from(Object.values(nodeTypeMap).map(key => key)); // 目前默认添加的都是系统组件/本地组件 if (!nodeMap.includes(nodeType)) registerNodeType(nodeType, LocalNode, nodeDefinition.nodeName); setNodes((nds) => [...nds, newNode]); }, [reactFlowInstance]); // 处理添加节点的统一方法 const handleAddNode = useCallback((nodeType: string) => { // 如果是通过边添加节点 if (edgeForNodeAdd) { addNodeOnEdge(nodeType); } // 如果是通过画布添加节点 else if (positionForNodeAdd) { addNodeOnPane(nodeType, positionForNodeAdd); } // 清除状态 setEdgeForNodeAdd(null); setPositionForNodeAdd(null); }, [edgeForNodeAdd, positionForNodeAdd, addNodeOnEdge, addNodeOnPane]); // 保存所有节点和边数据到服务器 const saveFlowDataToServer = useCallback(async () => { try { // 准备要发送到服务器的数据 const flowData = { nodes: nodes, edges: edges }; // TODO 接口对接后修改后续的更新操作 console.log('flowData:', flowData); return; // 发送到服务器的示例代码(需要根据您的实际API进行调整) const response = await fetch('/api/flow/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(flowData) }); if (response.ok) { console.log('Flow data saved successfully'); // 可以添加成功提示 } else { console.error('Failed to save flow data'); // 可以添加失败提示 } } catch (error) { console.error('Error saving flow data:', error); // 可以添加错误提示 } }, [nodes, edges]); return (
e.preventDefault()}> { 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 selectionKeyCode={['Meta', 'Control']} selectionMode={SelectionMode.Partial} panOnDrag={[0, 1, 2]} // 支持多点触控平移 zoomOnScroll={true} zoomOnPinch={true} panOnScrollSpeed={0.5} > {/**/} {/*节点右键上下文*/} {menu && menu.type === 'node' && (
n.id === menu.id)!} onDelete={deleteNode} onEdit={editNode} onCopy={copyNode} />
)} {/*边右键上下文*/} {menu && menu.type === 'edge' && (
e.id === menu.id)!} onDelete={deleteEdge} onEdit={editEdge} onAddNode={(edge) => { setEdgeForNodeAdd(edge); setMenu(null); // 关闭上下文菜单 }} />
)} {/*画布右键上下文*/} {menu && menu.type === 'pane' && (
{ addNodeOnPane(nodeType, position); setMenu(null); // 关闭上下文菜单 }} />
)} {/*节点双击/节点编辑上下文*/} {/*统一的添加节点菜单*/} {(edgeForNodeAdd || positionForNodeAdd) && (
{ handleAddNode(nodeType); // 关闭菜单 setEdgeForNodeAdd(null); setPositionForNodeAdd(null); }} position={positionForNodeAdd || undefined} edgeId={edgeForNodeAdd?.id} />
)}
); }; export default FlowEditorWithProvider;