From 67b5deafae6a9a6a7034f5215fb6a369edb126fb Mon Sep 17 00:00:00 2001 From: zly Date: Tue, 7 Jul 2026 09:59:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=A1=86=E9=80=89?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E5=8F=B3=E9=94=AE=E5=A4=8D=E5=88=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=B0=81=E8=A3=85=E5=A4=8D=E5=88=B6=E7=B2=98?= =?UTF-8?q?=E8=B4=B4=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 10 +++ src/pages/flowEditor/FlowEditorMain.tsx | 77 +++++++++++++++++-- .../flowEditor/components/edgeContextMenu.tsx | 15 +++- .../flowEditor/components/nodeContextMenu.tsx | 4 +- .../flowEditor/components/paneContextMenu.tsx | 20 +++-- 5 files changed, 108 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4df3db..bde9c4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: remark-gfm: specifier: ^4.0.1 version: 4.0.1 + utif: + specifier: ^3.1.0 + version: 3.1.0 devDependencies: '@svgr/webpack': specifier: ^5.5.0 @@ -4990,6 +4993,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utif@3.1.0: + resolution: {integrity: sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -11549,6 +11555,10 @@ snapshots: dependencies: react: 17.0.2 + utif@3.1.0: + dependencies: + pako: 1.0.11 + util-deprecate@1.0.2: {} util.promisify@1.0.1: diff --git a/src/pages/flowEditor/FlowEditorMain.tsx b/src/pages/flowEditor/FlowEditorMain.tsx index a4c4367..2d45efa 100644 --- a/src/pages/flowEditor/FlowEditorMain.tsx +++ b/src/pages/flowEditor/FlowEditorMain.tsx @@ -163,6 +163,54 @@ const FlowEditorMain: React.FC = (props) => { // 流程发布弹窗状态 const [publishModalVisible, setPublishModalVisible] = React.useState(false); + const selectedCopyableNodes = useMemo( + () => + nodes.filter( + (node) => + node.selected && node.type !== 'start' && node.type !== 'end' + ), + [nodes] + ); + + const selectedEdgeCount = useMemo( + () => edges.filter((edge) => edge.selected).length, + [edges] + ); + + const hasMultiElementSelection = + selectedCopyableNodes.length > 1 || selectedEdgeCount > 0; + + const copySelectedElements = React.useCallback( + (fallbackNode?: Node) => { + const shouldCopySelection = + selectedCopyableNodes.length > 0 && + (!fallbackNode || fallbackNode.selected); + const nodeToCopy = + (shouldCopySelection ? selectedCopyableNodes[0] : undefined) || + (fallbackNode?.type !== 'start' && fallbackNode?.type !== 'end' + ? fallbackNode + : undefined); + + if (!nodeToCopy) { + return; + } + + copyNode(nodeToCopy); + setIsEditModalOpen(false); + setMenu(null); + }, + [copyNode, selectedCopyableNodes, setIsEditModalOpen, setMenu] + ); + + const contextNode = + menu?.type === 'node' ? nodes.find((node) => node.id === menu.id) : null; + const contextEdge = + menu?.type === 'edge' ? edges.find((edge) => edge.id === menu.id) : null; + const nodeContextCopiesSelection = + !!contextNode?.selected && hasMultiElementSelection; + const edgeContextCopiesSelection = + !!contextEdge?.selected && selectedCopyableNodes.length > 0; + // 监听自定义事件以隐藏/显示节点 useEffect(() => { const handleToggleNodeVisibility = (event: CustomEvent) => { @@ -255,12 +303,11 @@ const FlowEditorMain: React.FC = (props) => { // Ctrl/Cmd+C 复制选中的节点 if (isModifierKey && e.key === 'c' && canEdit) { // 获取当前选中的节点 - const selectedNode = nodes.find((node) => node.selected); + const selectedNode = nodes.find( + (node) => + node.selected && node.type !== 'start' && node.type !== 'end' + ); if (selectedNode) { - // 不允许复制开始和结束节点 - if (selectedNode.type === 'start' || selectedNode.type === 'end') { - return; - } e.preventDefault(); copyNode(selectedNode); } @@ -650,10 +697,11 @@ const FlowEditorMain: React.FC = (props) => { }} > n.id === menu.id)!} + node={contextNode!} onDelete={deleteNode} onEdit={editNode} - onCopy={copyNode} + onCopy={copySelectedElements} + copyLabel={nodeContextCopiesSelection ? '复制选中内容' : '复制节点'} onCloseMenu={setMenu} onCloseOpenModal={setIsEditModalOpen} isRunning={currentAppIsRunning} @@ -672,9 +720,13 @@ const FlowEditorMain: React.FC = (props) => { }} > e.id === menu.id)!} + edge={contextEdge!} onDelete={deleteEdge} onEdit={editEdge} + onCopy={ + edgeContextCopiesSelection ? () => copySelectedElements() : undefined + } + copyLabel="复制选中内容" onAddNode={(edge) => { setEdgeForNodeAdd(edge); setIsEditModalOpen(false); @@ -696,6 +748,15 @@ const FlowEditorMain: React.FC = (props) => { > 0 + ? () => copySelectedElements() + : undefined + } + onPasteNode={(position) => { + pasteNode(position); + setMenu(null); + }} onAddNode={( nodeType: string, position: { x: number; y: number }, diff --git a/src/pages/flowEditor/components/edgeContextMenu.tsx b/src/pages/flowEditor/components/edgeContextMenu.tsx index 267d6bc..f417243 100644 --- a/src/pages/flowEditor/components/edgeContextMenu.tsx +++ b/src/pages/flowEditor/components/edgeContextMenu.tsx @@ -6,6 +6,8 @@ interface EdgeContextMenuProps { edge: Edge; onDelete?: (edge: Edge) => void; onEdit?: (edge: Edge) => void; + onCopy?: () => void; + copyLabel?: string; onAddNode?: (edge: Edge) => void; // 添加在边上添加节点的功能 } @@ -13,6 +15,8 @@ const EdgeContextMenu: React.FC = ({ edge, onDelete, onEdit, + onCopy, + copyLabel = '复制选中内容', onAddNode }) => { const handleDelete = () => { @@ -23,6 +27,10 @@ const EdgeContextMenu: React.FC = ({ onEdit && onEdit(edge); }; + const handleCopy = () => { + onCopy && onCopy(); + }; + // 添加在边上添加节点的处理函数 const handleAddNode = () => { onAddNode && onAddNode(edge); @@ -36,6 +44,11 @@ const EdgeContextMenu: React.FC = ({ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)' }} > + {onCopy && ( + + {copyLabel} + + )} 添加节点 @@ -46,4 +59,4 @@ const EdgeContextMenu: React.FC = ({ ); }; -export default EdgeContextMenu; \ No newline at end of file +export default EdgeContextMenu; diff --git a/src/pages/flowEditor/components/nodeContextMenu.tsx b/src/pages/flowEditor/components/nodeContextMenu.tsx index 41283cd..4ec1417 100644 --- a/src/pages/flowEditor/components/nodeContextMenu.tsx +++ b/src/pages/flowEditor/components/nodeContextMenu.tsx @@ -9,6 +9,7 @@ interface NodeContextMenuProps { onRename?: (node: Node) => void; onDelete?: (node: Node) => void; onCopy?: (node: Node) => void; + copyLabel?: string; onEdit?: (node: Node) => void; onCloseMenu?: (data: React.Dispatch>) => void; onCloseOpenModal?: (boolean: boolean) => void; @@ -19,6 +20,7 @@ const NodeContextMenu: React.FC = ({ node, onDelete, onCopy, + copyLabel = '复制节点', onEdit, onCloseMenu, onCloseOpenModal, @@ -104,7 +106,7 @@ const NodeContextMenu: React.FC = ({ menuItems.push( - 复制节点 + {copyLabel} ); menuItems.push( diff --git a/src/pages/flowEditor/components/paneContextMenu.tsx b/src/pages/flowEditor/components/paneContextMenu.tsx index 629154d..0dc65fa 100644 --- a/src/pages/flowEditor/components/paneContextMenu.tsx +++ b/src/pages/flowEditor/components/paneContextMenu.tsx @@ -6,11 +6,15 @@ const SubMenu = Menu.SubMenu; interface PaneContextMenuProps { onAddNode: (nodeType: string, position: { x: number; y: number }, node: any) => void; + onCopySelection?: () => void; + onPasteNode?: (position: { x: number; y: number }) => void; position: { x: number; y: number }; } const PaneContextMenu: React.FC = ({ onAddNode, + onCopySelection, + onPasteNode, position }) => { // 包装onAddNode函数以适配AddNodeMenu组件的接口 @@ -20,13 +24,8 @@ const PaneContextMenu: React.FC = ({ // 处理粘贴节点 const handlePasteNode = useCallback(() => { - // 创建自定义粘贴事件 - const pasteEvent = new ClipboardEvent('paste', { - clipboardData: new DataTransfer() - }); - pasteEvent.clipboardData?.setData('text', 'paste-node'); - document.dispatchEvent(pasteEvent); - }, []); + onPasteNode && onPasteNode(position); + }, [onPasteNode, position]); // 检查是否有复制的节点数据 const hasCopiedNode = !!localStorage.getItem('copiedNode') || !!localStorage.getItem('copiedFlowData'); @@ -41,6 +40,11 @@ const PaneContextMenu: React.FC = ({ }} triggerProps={{ trigger: 'click' }} > + {onCopySelection && ( + + 复制选中内容 + + )} = ({ ); }; -export default PaneContextMenu; \ No newline at end of file +export default PaneContextMenu;