From 54235f0f8e1a37168d3162b3b64671d6f7fa6930 Mon Sep 17 00:00:00 2001 From: ZLY Date: Fri, 29 Aug 2025 15:29:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(flowEditor):=20=E6=B7=BB=E5=8A=A0=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=92=8C=E8=BE=B9=E7=9A=84=E5=8F=B3=E9=94=AE=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增节点右键菜单和边右键菜单组件 - 实现节点和边的删除、编辑、复制等功能 - 优化画布点击事件,关闭未选中的菜单- 调整菜单显示位置,确保在画布区域内显示 --- .../flowEditor/components/edgeContextMenu.tsx | 36 +++++ .../flowEditor/components/nodeContextMenu.tsx | 51 +++++++ src/pages/flowEditor/index.tsx | 128 +++++++++++++++++- 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/pages/flowEditor/components/edgeContextMenu.tsx create mode 100644 src/pages/flowEditor/components/nodeContextMenu.tsx diff --git a/src/pages/flowEditor/components/edgeContextMenu.tsx b/src/pages/flowEditor/components/edgeContextMenu.tsx new file mode 100644 index 0000000..399a0bd --- /dev/null +++ b/src/pages/flowEditor/components/edgeContextMenu.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Menu } from '@arco-design/web-react'; +import { Edge } from '@xyflow/react'; + +interface EdgeContextMenuProps { + edge: Edge; + onDelete?: (edge: Edge) => void; + onEdit?: (edge: Edge) => void; +} + +const EdgeContextMenu: React.FC = ({ + edge, + onDelete, + onEdit +}) => { + const handleDelete = () => { + onDelete && onDelete(edge); + }; + + const handleEdit = () => { + onEdit && onEdit(edge); + }; + + return ( + + + 编辑连接 + + + 删除连接 + + + ); +}; + +export default EdgeContextMenu; \ No newline at end of file diff --git a/src/pages/flowEditor/components/nodeContextMenu.tsx b/src/pages/flowEditor/components/nodeContextMenu.tsx new file mode 100644 index 0000000..a2ce794 --- /dev/null +++ b/src/pages/flowEditor/components/nodeContextMenu.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Menu, Dropdown } from '@arco-design/web-react'; +import { Node } from '@xyflow/react'; + +interface NodeContextMenuProps { + node: Node; + onRename?: (node: Node) => void; + onDelete?: (node: Node) => void; + onCopy?: (node: Node) => void; + onEdit?: (node: Node) => void; +} + +const NodeContextMenu: React.FC = ({ + node, + onDelete, + onCopy, + onEdit + }) => { + const handleDelete = () => { + onDelete && onDelete(node); + }; + + const handleCopy = () => { + onCopy && onCopy(node); + }; + + const handleEdit = () => { + onEdit && onEdit(node); + }; + + return ( + + + 编辑节点 + + + {(!['start', 'end'].includes(node.type)) && ( + <> + + 复制节点 + + + 删除节点 + + + )} + + ); +}; + +export default NodeContextMenu; \ No newline at end of file diff --git a/src/pages/flowEditor/index.tsx b/src/pages/flowEditor/index.tsx index c3d0af3..817ed14 100644 --- a/src/pages/flowEditor/index.tsx +++ b/src/pages/flowEditor/index.tsx @@ -11,7 +11,8 @@ import { ReactFlowProvider, useReactFlow, EdgeTypes, - SelectionMode + SelectionMode, + useStoreApi } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { nodeTypeMap, nodeTypes, registerNodeType } from './node'; @@ -20,6 +21,8 @@ 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 NodeContextMenu from './components/nodeContextMenu'; +import EdgeContextMenu from './components/edgeContextMenu'; const edgeTypes: EdgeTypes = { custom: CustomEdge @@ -41,6 +44,13 @@ const FlowEditor: React.FC = () => { const [edges, setEdges] = useState([]); const reactFlowInstance = useReactFlow(); const reactFlowWrapper = useRef(null); + const [menu, setMenu] = useState<{ + id: string; + type: 'node' | 'edge'; + top: number; + left: number; + } | null>(null); + const store = useStoreApi(); const onNodesChange = useCallback( (changes: any) => setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot)), @@ -50,6 +60,7 @@ const FlowEditor: React.FC = () => { (changes: any) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)), [] ); + const onConnect = useCallback( (params: any) => setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot)), [] @@ -105,8 +116,81 @@ const FlowEditor: React.FC = () => { setEdges(initialEdges); }, []); + // 节点右键菜单处理 + 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 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 onPaneClick = useCallback(() => setMenu(null), [setMenu]); + + // 删除节点 + 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) => { + // 这里可以实现节点编辑逻辑 + console.log('编辑节点:', node); + setMenu(null); + }, []); + + // 编辑边 + const editEdge = useCallback((edge: Edge) => { + // 这里可以实现边编辑逻辑 + console.log('编辑边:', edge); + setMenu(null); + }, []); + + // 复制节点 + const copyNode = useCallback((node: Node) => { + // 这里可以实现节点复制逻辑 + console.log('复制节点:', node); + setMenu(null); + }, []); + return ( -
+
{ onConnect={onConnect} onDrop={onDrop} onDragOver={onDragOver} + onNodeContextMenu={onNodeContextMenu} + onEdgeContextMenu={onEdgeContextMenu} + onPaneClick={onPaneClick} + onPaneContextMenu={onPaneClick} fitView selectionKeyCode={['Meta', 'Control']} selectionMode={SelectionMode.Partial} @@ -131,8 +219,44 @@ const FlowEditor: React.FC = () => { {/*
从左侧拖拽节点到画布中
*/} {/**/}
+ + {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} + /> +
+ )}
); + }; export default FlowEditorWithProvider; \ No newline at end of file