From dde9083fd5237c782ccc17bb5d67fce80134c36a Mon Sep 17 00:00:00 2001 From: ZLY Date: Wed, 17 Dec 2025 15:42:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(flowEditor):=20=E5=AE=9E=E7=8E=B0=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 pasteNode 方法支持在指定位置粘贴节点 - 在 FlowEditorMain 中添加对 Ctrl/Cmd+C 和 Ctrl/Cmd+V 快捷键的支持 - 限制仅允许复制非开始和结束类型的节点 - 粘贴时自动计算视口中心位置作为新节点坐标 - 更新依赖数组以确保键盘事件监听器正确响应状态变化 --- src/pages/flowEditor/FlowEditorMain.tsx | 55 +++++++++++++++++++++---- src/pages/flowEditor/index.tsx | 1 + 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/pages/flowEditor/FlowEditorMain.tsx b/src/pages/flowEditor/FlowEditorMain.tsx index d05e10e..d44a0f1 100644 --- a/src/pages/flowEditor/FlowEditorMain.tsx +++ b/src/pages/flowEditor/FlowEditorMain.tsx @@ -78,6 +78,7 @@ interface FlowEditorMainProps { 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; @@ -130,6 +131,7 @@ const FlowEditorMain: React.FC = (props) => { editNode, editEdge, copyNode, + pasteNode, addNodeOnEdge, addNodeOnPane, handleAddNode, @@ -224,28 +226,67 @@ const FlowEditorMain: React.FC = (props) => { // 监听键盘事件实现快捷键 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - // Ctrl+Z 撤销 - if (e.ctrlKey && e.key === 'z' && !e.shiftKey && canUndo) { + // 如果当前正在运行或不是默认模式,不处理复制粘贴快捷键 + const canEdit = !currentAppIsRunning && useDefault; + // 兼容 Mac (metaKey) 和 Windows/Linux (ctrlKey) + const isModifierKey = e.ctrlKey || e.metaKey; + + // Ctrl/Cmd+Z 撤销 + if (isModifierKey && e.key === 'z' && !e.shiftKey && canUndo) { e.preventDefault(); undo(); } - // Ctrl+Shift+Z 重做 - if (e.ctrlKey && e.shiftKey && e.key === 'Z' && canRedo) { + // Ctrl/Cmd+Shift+Z 重做 + if (isModifierKey && e.shiftKey && e.key === 'Z' && canRedo) { e.preventDefault(); redo(); } - // Ctrl+Y 重做 - if (e.ctrlKey && e.key === 'y' && canRedo) { + // 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]); + }, [undo, redo, canUndo, canRedo, currentAppIsRunning, useDefault, nodes, copyNode, pasteNode, reactFlowInstance]); // 处理流程发布 const handlePublish = () => { diff --git a/src/pages/flowEditor/index.tsx b/src/pages/flowEditor/index.tsx index 5aed36b..98506b9 100644 --- a/src/pages/flowEditor/index.tsx +++ b/src/pages/flowEditor/index.tsx @@ -341,6 +341,7 @@ const FlowEditor: React.FC<{ initialData?: any, useDefault?: boolean }> = ({ ini editNode={editNode} editEdge={editEdge} copyNode={copyNode} + pasteNode={pasteNode} // Node operations addNodeOnEdge={addNodeOnEdge}