From eac40c3f6f5433305ed7cee852b63b4c5bae611c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Tue, 11 Nov 2025 20:25:19 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=E5=BF=AB=E6=8D=B7=E9=94=AE=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E7=B2=98=E8=B4=B4=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/XFlow.tsx | 187 +++++++++++----------------------- 1 file changed, 62 insertions(+), 125 deletions(-) diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index c6cf8984f..0df5994ec 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -8,7 +8,7 @@ import { import '@xyflow/react/dist/style.css'; import { useEventListener, useMemoizedFn } from 'ahooks'; import { produce, setAutoFreeze } from 'immer'; -import { debounce, isFunction } from 'lodash'; +import { isFunction, isString } from 'lodash'; import type { FC } from 'react'; import React, { memo, @@ -87,7 +87,8 @@ const XFlow: FC = memo(props => { ); const { record } = useTemporalStore(); const [activeNode, setActiveNode] = useState(null); - const { settingMap, globalConfig, readOnly, logPanel } = useContext(ConfigContext); + const { settingMap, globalConfig, readOnly, logPanel } = + useContext(ConfigContext); const [openPanel, setOpenPanel] = useState(true); const [openLogPanel, setOpenLogPanel] = useState(true); const { @@ -101,8 +102,6 @@ const XFlow: FC = memo(props => { const nodeEditorRef = useRef(null); const { copyNode, pasteNodeSimple } = useFlow(); const { undo, redo } = useTemporalStore(); - const isNodeCopyingRef = useRef(false); // 是否正在进行节点复制 - const lastClickedNodeRef = useRef(false); // 最后一次点击是否是节点 useEffect(() => { zoomTo(0.8); @@ -116,7 +115,7 @@ const XFlow: FC = memo(props => { }; }, []); - useEventListener('keydown', e => { + const handleKeyDown = useMemoizedFn((e: KeyboardEvent) => { if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) e.preventDefault(); if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) { @@ -130,74 +129,49 @@ const XFlow: FC = memo(props => { if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey)) e.preventDefault(); if ((e.key === 'c' || e.key === 'C') && (e.ctrlKey || e.metaKey)) { - const selectedNode = nodes?.find(node => node.selected); - // 获取当前选中的文本(非节点的内容) - const selectedText = window.getSelection()?.toString(); - // 如果最后点击的是节点 且 有节点被选中 则 复制节点 - if (selectedNode && lastClickedNodeRef.current) { + const latestNodes = storeApi.getState().nodes; + let isNodeCopyEvent = false; + if (e.target instanceof HTMLElement) { + const target = e.target as HTMLElement; + if ( + (isString(target.tagName) && + target.tagName.toLowerCase() === 'body') || + (target.tagName.toLowerCase() === 'div' && + target.classList && + isFunction(target.classList.contains) && + (target.classList.contains('ant-drawer') || + target.classList.contains('react-flow__node') || + target.id === 'xflow-container')) + ) { + isNodeCopyEvent = true; + } + } + const selectedNode = latestNodes?.find(node => node.selected); + const targetElement = + e.target instanceof HTMLElement ? e.target : undefined; + const targetTagName = targetElement?.tagName; + const targetClassList = targetElement?.classList; + + if (isNodeCopyEvent && selectedNode?.id) { // 复制节点 - isNodeCopyingRef.current = true; // 标记为节点复制 - copyNode(selectedNode.id); e.preventDefault(); - - // 清空系统剪贴板,确保粘贴时使用节点而非之前的文本 - try { - navigator.clipboard.writeText('').catch(() => {}); - } catch (err) {} - } else if (selectedText) { - // 复制文本 - // 清除之前的节点复制状态 - const { copyNodes, copyTimeoutId } = storeApi.getState(); - if (copyNodes?.length > 0) { - if (copyTimeoutId) { - clearTimeout(copyTimeoutId); - } - storeApi.setState({ - copyNodes: [], - copyTimeoutId: null, - isAddingNode: false, - }); - } + copyNode(selectedNode.id); } + + } else if ((e.key === 'v' || e.key === 'V') && (e.ctrlKey || e.metaKey)) { const { copyNodes } = storeApi.getState(); - // 只有在有节点复制状态时才拦截粘贴操作 if (copyNodes?.length > 0) { - pasteNodeSimple(); e.preventDefault(); + pasteNodeSimple(); } - } else if (copyNodes.length > 0) { - // 只在有复制节点时才检查其他操作 - const { copyTimeoutId } = storeApi.getState(); - if (copyTimeoutId) { - clearTimeout(copyTimeoutId); - storeApi.setState({ - copyTimeoutId: null, - isAddingNode: false, - }); - } - } else if (e.key === 'Escape'){ - setOpenPanel(false) + } else if (e.key === 'Escape') { + setOpenPanel(false); + workflowContainerRef.current?.focus(); } }); - - // 添加 copy 事件监听,获取实际复制的内容 - useEventListener('copy', (e: ClipboardEvent) => { - if (!isNodeCopyingRef.current) { - // 清除节点复制状态,因为用户复制了其他内容 - const { copyNodes, copyTimeoutId } = storeApi.getState(); - if (copyNodes?.length > 0) { - if (copyTimeoutId) { - clearTimeout(copyTimeoutId); - } - storeApi.setState({ - copyNodes: [], - copyTimeoutId: null, - isAddingNode: false, - }); - } - } - isNodeCopyingRef.current = false; + useEventListener('keydown', handleKeyDown, { + target: workflowContainerRef, }); useEventListener( @@ -221,28 +195,6 @@ const XFlow: FC = memo(props => { } ); - // 当点击非节点区域时重置标记 - useEventListener('mousedown', (e: MouseEvent) => { - const target = e.target as HTMLElement; - const isClickingNode = - target.closest('.xflow-node-container') || - target.closest('.candidate-node'); - // 如果点击的不是节点,重置标记 - if (!isClickingNode) { - lastClickedNodeRef.current = false; - - // 清除复制状态 - const { copyTimeoutId, copyNodes } = storeApi.getState(); - if (copyTimeoutId && copyNodes?.length > 0) { - clearTimeout(copyTimeoutId); - storeApi.setState({ - copyTimeoutId: null, - isAddingNode: false, - }); - } - } - }); - const { eventEmitter } = useEventEmitterContextContext(); eventEmitter?.useSubscription((v: any) => { // 整理画布 @@ -278,28 +230,6 @@ const XFlow: FC = memo(props => { setCandidateNode(newNode); }; - // 插入节点 - // const handleInsertNode = () => { - // const newNode = { - // id: uuid(), - // data: { label: 'new node' }, - // position: { - // x: 0, - // y: 0, - // }, - // }; - // addNodes(newNode); - // addEdges({ - // id: uuid(), - // source: '2', - // target: newNode.id, - // }); - // const targetEdge = edges.find(edge => edge.source === '2'); - // updateEdge(targetEdge?.id as string, { - // source: newNode.id, - // }); - // }; - // edge 移入/移出效果 const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => { const newEdges = produce(edges, draft => { @@ -323,7 +253,6 @@ const XFlow: FC = memo(props => { const { _nodeType, _status, ...restData } = data || {}; const nodeSetting = settingMap[_nodeType] || {}; const showPanel = nodeSetting?.nodePanel?.showPanel ?? true; - return ( = memo(props => { layout={layout} status={_status} onClick={async e => { - // 记录用户点击了节点 - lastClickedNodeRef.current = true; - if (nodeEditorRef?.current?.validateForm) { const result = await nodeEditorRef?.current?.validateForm(); if (!result) { @@ -400,7 +326,14 @@ const XFlow: FC = memo(props => { const panelonClose = globalConfig?.nodePanel?.onClose; return ( -
+
{ + workflowContainerRef.current?.focus(); + }} + > = memo(props => { return; } setOpenPanel(false); + workflowContainerRef.current?.focus(); // 如果日志面板关闭 if (!isTruthy(activeNode?._status) || !openLogPanel) { @@ -520,19 +454,22 @@ const XFlow: FC = memo(props => { {NodeEditorWrap} )} - {isTruthy(activeNode?._status) && openLogPanel && Boolean(logPanel?.enable ?? true) && ( - { - setOpenLogPanel(false); - !openPanel && setActiveNode(null); - }} - data={activeNode?.values} - > - {NodeLogWrap} - - )} + {isTruthy(activeNode?._status) && + openLogPanel && + Boolean(logPanel?.enable ?? true) && ( + { + setOpenLogPanel(false); + !openPanel && setActiveNode(null); + workflowContainerRef.current?.focus(); + }} + data={activeNode?.values} + > + {NodeLogWrap} + + )}
); From ba56453a99bc07c97aea8597a55ee0e57a2cb9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Tue, 11 Nov 2025 20:40:17 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E8=AF=A5=E8=8A=82=E7=82=B9=E7=9A=84=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E9=94=AE=E5=A4=8D=E5=88=B6=E5=8A=9F=E8=83=BD=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/api.md | 1 + docs/xflow/demo/quickStart/index.tsx | 1 + packages/x-flow/src/XFlow.tsx | 34 +++++++++++++++++++--------- packages/x-flow/src/types.ts | 1 + 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/docs/xflow/api.md b/docs/xflow/api.md index b8ce064a6..664189381 100644 --- a/docs/xflow/api.md +++ b/docs/xflow/api.md @@ -171,6 +171,7 @@ Handle 配置继承自 React Flow 的 Handle 配置,用于控制节点连接 | getSettingSchema | 动态获取节点的业务配置信息,返回值同settingSchema。同时设置`settingSchema`和`getSettingSchema`只生效`getSettingSchema` | `(nodeId: string, nodeType: string, nodeItem: TNodeItem, nodeData: any, form: ReturnType) => Promise` | | | renderHandle | 自定义渲染节点的handle,以实现某个节点自定义的出口数量.`sourceHandle`是原handle组件 | `(sourceHandle: SourceHandleType,sourceHandleProps:ComponentProps,nodeProps: {id: string;type: string;data: any;layout: 'LR';isConnectable: boolean;readOnly: boolean;}) => React.JSX.Element`| | | disabledShortcutDelete | 是否禁用该节点的快捷键删除功能 | `boolean` | false | +| disabledShortcutCopy | 是否禁用该节点的快捷键复制功能 | `boolean` | false | ## TNodeSelector diff --git a/docs/xflow/demo/quickStart/index.tsx b/docs/xflow/demo/quickStart/index.tsx index c39da786b..c155edffe 100644 --- a/docs/xflow/demo/quickStart/index.tsx +++ b/docs/xflow/demo/quickStart/index.tsx @@ -8,6 +8,7 @@ export default () => { type: 'Start', hidden: true, disabledShortcutDelete: true, + disabledShortcutCopy: true, targetHandleHidden: true, icon: { type: 'icon-start', diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 0df5994ec..c5f288b61 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -153,12 +153,20 @@ const XFlow: FC = memo(props => { const targetClassList = targetElement?.classList; if (isNodeCopyEvent && selectedNode?.id) { + const nodeType = selectedNode?.data?._nodeType; + if (isString(nodeType) && nodeType) { + const nodeConfig = settingMap[nodeType]; + if (nodeConfig?.disabledShortcutCopy) { + message.warning( + `${selectedNode.data?.title || selectedNode.id}节点不允许复制` + ); + return; + } + } // 复制节点 e.preventDefault(); copyNode(selectedNode.id); } - - } else if ((e.key === 'v' || e.key === 'V') && (e.ctrlKey || e.metaKey)) { const { copyNodes } = storeApi.getState(); if (copyNodes?.length > 0) { @@ -282,7 +290,7 @@ const XFlow: FC = memo(props => { } setOpenLogPanel(true); }} - onDelete={()=>{ + onDelete={() => { // 删除节点并关闭弹窗 setActiveNode(null); }} @@ -357,7 +365,7 @@ const XFlow: FC = memo(props => { }, deletable: deletable, //默认连线属性受此项控制 }} - onBeforeDelete={async (elements) => { + onBeforeDelete={async elements => { if (readOnly) { return false; } @@ -369,10 +377,14 @@ const XFlow: FC = memo(props => { : false; }); if (blockedNodes?.length > 0) { - message.warning(`${blockedNodes.map(n => n.data?.title || n.id).join(', ')}节点不允许删除!`); + message.warning( + `${blockedNodes + .map(n => n.data?.title || n.id) + .join(', ')}节点不允许删除!` + ); return false; } - return true + return true; }} onConnect={onConnect} onNodesChange={changes => { @@ -398,24 +410,24 @@ const XFlow: FC = memo(props => { }); }} onEdgeMouseEnter={(_, edge: any) => { - if(!edge.style.stroke || edge.style.stroke === '#c9c9c9'){ + if (!edge.style.stroke || edge.style.stroke === '#c9c9c9') { getUpdateEdgeConfig(edge, '#2970ff'); } }} onEdgeMouseLeave={(_, edge) => { - if(['#2970ff',"#c9c9c9"].includes(edge.style.stroke)){ + if (['#2970ff', '#c9c9c9'].includes(edge.style.stroke)) { getUpdateEdgeConfig(edge, '#c9c9c9'); } }} onNodesDelete={() => { - setActiveNode(null); + setActiveNode(null); }} onNodeClick={(event, node) => { onNodeClick && onNodeClick(event, node); }} deleteKeyCode={globalConfig?.deleteKeyCode} - onEdgeClick={(event,edge)=>{ - onEdgeClick && onEdgeClick(event,edge) + onEdgeClick={(event, edge) => { + onEdgeClick && onEdgeClick(event, edge); }} > diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index 60a91f2af..174d03e2b 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -58,6 +58,7 @@ export interface TNodeItem { showTestingBtn?: boolean; // 是否显示单点调试按钮 className?: string;// 自定义节点class disabledShortcutDelete?: boolean; // 是否禁用快捷键删除 + disabledShortcutCopy?: boolean; // 是否禁用快捷键复制 } export interface TNodeGroup { From b13e472640db1ba2ff22e954cad4f1719afd25a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Tue, 11 Nov 2025 20:42:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/XFlow.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index c5f288b61..62c620001 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -147,10 +147,6 @@ const XFlow: FC = memo(props => { } } const selectedNode = latestNodes?.find(node => node.selected); - const targetElement = - e.target instanceof HTMLElement ? e.target : undefined; - const targetTagName = targetElement?.tagName; - const targetClassList = targetElement?.classList; if (isNodeCopyEvent && selectedNode?.id) { const nodeType = selectedNode?.data?._nodeType;