88import '@xyflow/react/dist/style.css' ;
99import { useEventListener , useMemoizedFn } from 'ahooks' ;
1010import { produce , setAutoFreeze } from 'immer' ;
11- import { debounce , isFunction } from 'lodash' ;
11+ import { isFunction , isString } from 'lodash' ;
1212import type { FC } from 'react' ;
1313import React , {
1414 memo ,
@@ -87,7 +87,8 @@ const XFlow: FC<FlowProps> = memo(props => {
8787 ) ;
8888 const { record } = useTemporalStore ( ) ;
8989 const [ activeNode , setActiveNode ] = useState < any > ( null ) ;
90- const { settingMap, globalConfig, readOnly, logPanel } = useContext ( ConfigContext ) ;
90+ const { settingMap, globalConfig, readOnly, logPanel } =
91+ useContext ( ConfigContext ) ;
9192 const [ openPanel , setOpenPanel ] = useState < boolean > ( true ) ;
9293 const [ openLogPanel , setOpenLogPanel ] = useState < boolean > ( true ) ;
9394 const {
@@ -101,8 +102,6 @@ const XFlow: FC<FlowProps> = memo(props => {
101102 const nodeEditorRef = useRef ( null ) ;
102103 const { copyNode, pasteNodeSimple } = useFlow ( ) ;
103104 const { undo, redo } = useTemporalStore ( ) ;
104- const isNodeCopyingRef = useRef ( false ) ; // 是否正在进行节点复制
105- const lastClickedNodeRef = useRef ( false ) ; // 最后一次点击是否是节点
106105
107106 useEffect ( ( ) => {
108107 zoomTo ( 0.8 ) ;
@@ -116,7 +115,7 @@ const XFlow: FC<FlowProps> = memo(props => {
116115 } ;
117116 } , [ ] ) ;
118117
119- useEventListener ( 'keydown' , e => {
118+ const handleKeyDown = useMemoizedFn ( ( e : KeyboardEvent ) => {
120119 if ( ( e . key === 'd' || e . key === 'D' ) && ( e . ctrlKey || e . metaKey ) )
121120 e . preventDefault ( ) ;
122121 if ( ( e . key === 'z' || e . key === 'Z' ) && ( e . ctrlKey || e . metaKey ) ) {
@@ -130,74 +129,53 @@ const XFlow: FC<FlowProps> = memo(props => {
130129 if ( ( e . key === 's' || e . key === 'S' ) && ( e . ctrlKey || e . metaKey ) )
131130 e . preventDefault ( ) ;
132131 if ( ( e . key === 'c' || e . key === 'C' ) && ( e . ctrlKey || e . metaKey ) ) {
133- const selectedNode = nodes ?. find ( node => node . selected ) ;
134- // 获取当前选中的文本(非节点的内容)
135- const selectedText = window . getSelection ( ) ?. toString ( ) ;
136- // 如果最后点击的是节点 且 有节点被选中 则 复制节点
137- if ( selectedNode && lastClickedNodeRef . current ) {
138- // 复制节点
139- isNodeCopyingRef . current = true ; // 标记为节点复制
140- copyNode ( selectedNode . id ) ;
141- e . preventDefault ( ) ;
132+ const latestNodes = storeApi . getState ( ) . nodes ;
133+ let isNodeCopyEvent = false ;
134+ if ( e . target instanceof HTMLElement ) {
135+ const target = e . target as HTMLElement ;
136+ if (
137+ ( isString ( target . tagName ) &&
138+ target . tagName . toLowerCase ( ) === 'body' ) ||
139+ ( target . tagName . toLowerCase ( ) === 'div' &&
140+ target . classList &&
141+ isFunction ( target . classList . contains ) &&
142+ ( target . classList . contains ( 'ant-drawer' ) ||
143+ target . classList . contains ( 'react-flow__node' ) ||
144+ target . id === 'xflow-container' ) )
145+ ) {
146+ isNodeCopyEvent = true ;
147+ }
148+ }
149+ const selectedNode = latestNodes ?. find ( node => node . selected ) ;
142150
143- // 清空系统剪贴板,确保粘贴时使用节点而非之前的文本
144- try {
145- navigator . clipboard . writeText ( '' ) . catch ( ( ) => { } ) ;
146- } catch ( err ) { }
147- } else if ( selectedText ) {
148- // 复制文本
149- // 清除之前的节点复制状态
150- const { copyNodes, copyTimeoutId } = storeApi . getState ( ) ;
151- if ( copyNodes ?. length > 0 ) {
152- if ( copyTimeoutId ) {
153- clearTimeout ( copyTimeoutId ) ;
151+ if ( isNodeCopyEvent && selectedNode ?. id ) {
152+ const nodeType = selectedNode ?. data ?. _nodeType ;
153+ if ( isString ( nodeType ) && nodeType ) {
154+ const nodeConfig = settingMap [ nodeType ] ;
155+ if ( nodeConfig ?. disabledShortcutCopy ) {
156+ message . warning (
157+ `${ selectedNode . data ?. title || selectedNode . id } 节点不允许复制`
158+ ) ;
159+ return ;
154160 }
155- storeApi . setState ( {
156- copyNodes : [ ] ,
157- copyTimeoutId : null ,
158- isAddingNode : false ,
159- } ) ;
160161 }
162+ // 复制节点
163+ e . preventDefault ( ) ;
164+ copyNode ( selectedNode . id ) ;
161165 }
162166 } else if ( ( e . key === 'v' || e . key === 'V' ) && ( e . ctrlKey || e . metaKey ) ) {
163167 const { copyNodes } = storeApi . getState ( ) ;
164- // 只有在有节点复制状态时才拦截粘贴操作
165168 if ( copyNodes ?. length > 0 ) {
166- pasteNodeSimple ( ) ;
167169 e . preventDefault ( ) ;
170+ pasteNodeSimple ( ) ;
168171 }
169- } else if ( copyNodes . length > 0 ) {
170- // 只在有复制节点时才检查其他操作
171- const { copyTimeoutId } = storeApi . getState ( ) ;
172- if ( copyTimeoutId ) {
173- clearTimeout ( copyTimeoutId ) ;
174- storeApi . setState ( {
175- copyTimeoutId : null ,
176- isAddingNode : false ,
177- } ) ;
178- }
179- } else if ( e . key === 'Escape' ) {
180- setOpenPanel ( false )
172+ } else if ( e . key === 'Escape' ) {
173+ setOpenPanel ( false ) ;
174+ workflowContainerRef . current ?. focus ( ) ;
181175 }
182176 } ) ;
183-
184- // 添加 copy 事件监听,获取实际复制的内容
185- useEventListener ( 'copy' , ( e : ClipboardEvent ) => {
186- if ( ! isNodeCopyingRef . current ) {
187- // 清除节点复制状态,因为用户复制了其他内容
188- const { copyNodes, copyTimeoutId } = storeApi . getState ( ) ;
189- if ( copyNodes ?. length > 0 ) {
190- if ( copyTimeoutId ) {
191- clearTimeout ( copyTimeoutId ) ;
192- }
193- storeApi . setState ( {
194- copyNodes : [ ] ,
195- copyTimeoutId : null ,
196- isAddingNode : false ,
197- } ) ;
198- }
199- }
200- isNodeCopyingRef . current = false ;
177+ useEventListener ( 'keydown' , handleKeyDown , {
178+ target : workflowContainerRef ,
201179 } ) ;
202180
203181 useEventListener (
@@ -221,28 +199,6 @@ const XFlow: FC<FlowProps> = memo(props => {
221199 }
222200 ) ;
223201
224- // 当点击非节点区域时重置标记
225- useEventListener ( 'mousedown' , ( e : MouseEvent ) => {
226- const target = e . target as HTMLElement ;
227- const isClickingNode =
228- target . closest ( '.xflow-node-container' ) ||
229- target . closest ( '.candidate-node' ) ;
230- // 如果点击的不是节点,重置标记
231- if ( ! isClickingNode ) {
232- lastClickedNodeRef . current = false ;
233-
234- // 清除复制状态
235- const { copyTimeoutId, copyNodes } = storeApi . getState ( ) ;
236- if ( copyTimeoutId && copyNodes ?. length > 0 ) {
237- clearTimeout ( copyTimeoutId ) ;
238- storeApi . setState ( {
239- copyTimeoutId : null ,
240- isAddingNode : false ,
241- } ) ;
242- }
243- }
244- } ) ;
245-
246202 const { eventEmitter } = useEventEmitterContextContext ( ) ;
247203 eventEmitter ?. useSubscription ( ( v : any ) => {
248204 // 整理画布
@@ -278,28 +234,6 @@ const XFlow: FC<FlowProps> = memo(props => {
278234 setCandidateNode ( newNode ) ;
279235 } ;
280236
281- // 插入节点
282- // const handleInsertNode = () => {
283- // const newNode = {
284- // id: uuid(),
285- // data: { label: 'new node' },
286- // position: {
287- // x: 0,
288- // y: 0,
289- // },
290- // };
291- // addNodes(newNode);
292- // addEdges({
293- // id: uuid(),
294- // source: '2',
295- // target: newNode.id,
296- // });
297- // const targetEdge = edges.find(edge => edge.source === '2');
298- // updateEdge(targetEdge?.id as string, {
299- // source: newNode.id,
300- // });
301- // };
302-
303237 // edge 移入/移出效果
304238 const getUpdateEdgeConfig = useMemoizedFn ( ( edge : any , color : string ) => {
305239 const newEdges = produce ( edges , draft => {
@@ -323,7 +257,6 @@ const XFlow: FC<FlowProps> = memo(props => {
323257 const { _nodeType, _status, ...restData } = data || { } ;
324258 const nodeSetting = settingMap [ _nodeType ] || { } ;
325259 const showPanel = nodeSetting ?. nodePanel ?. showPanel ?? true ;
326-
327260 return (
328261 < CustomNode
329262 { ...rest }
@@ -333,9 +266,6 @@ const XFlow: FC<FlowProps> = memo(props => {
333266 layout = { layout }
334267 status = { _status }
335268 onClick = { async e => {
336- // 记录用户点击了节点
337- lastClickedNodeRef . current = true ;
338-
339269 if ( nodeEditorRef ?. current ?. validateForm ) {
340270 const result = await nodeEditorRef ?. current ?. validateForm ( ) ;
341271 if ( ! result ) {
@@ -356,7 +286,7 @@ const XFlow: FC<FlowProps> = memo(props => {
356286 }
357287 setOpenLogPanel ( true ) ;
358288 } }
359- onDelete = { ( ) => {
289+ onDelete = { ( ) => {
360290 // 删除节点并关闭弹窗
361291 setActiveNode ( null ) ;
362292 } }
@@ -400,7 +330,14 @@ const XFlow: FC<FlowProps> = memo(props => {
400330 const panelonClose = globalConfig ?. nodePanel ?. onClose ;
401331
402332 return (
403- < div id = "xflow-container" ref = { workflowContainerRef } >
333+ < div
334+ id = "xflow-container"
335+ ref = { workflowContainerRef }
336+ tabIndex = { 0 }
337+ onMouseDown = { ( ) => {
338+ workflowContainerRef . current ?. focus ( ) ;
339+ } }
340+ >
404341 < ReactFlow
405342 panOnDrag = { panOnDrag }
406343 nodeTypes = { nodeTypes }
@@ -424,7 +361,7 @@ const XFlow: FC<FlowProps> = memo(props => {
424361 } ,
425362 deletable : deletable , //默认连线属性受此项控制
426363 } }
427- onBeforeDelete = { async ( elements ) => {
364+ onBeforeDelete = { async elements => {
428365 if ( readOnly ) {
429366 return false ;
430367 }
@@ -436,10 +373,14 @@ const XFlow: FC<FlowProps> = memo(props => {
436373 : false ;
437374 } ) ;
438375 if ( blockedNodes ?. length > 0 ) {
439- message . warning ( `${ blockedNodes . map ( n => n . data ?. title || n . id ) . join ( ', ' ) } 节点不允许删除!` ) ;
376+ message . warning (
377+ `${ blockedNodes
378+ . map ( n => n . data ?. title || n . id )
379+ . join ( ', ' ) } 节点不允许删除!`
380+ ) ;
440381 return false ;
441382 }
442- return true
383+ return true ;
443384 } }
444385 onConnect = { onConnect }
445386 onNodesChange = { changes => {
@@ -465,24 +406,24 @@ const XFlow: FC<FlowProps> = memo(props => {
465406 } ) ;
466407 } }
467408 onEdgeMouseEnter = { ( _ , edge : any ) => {
468- if ( ! edge . style . stroke || edge . style . stroke === '#c9c9c9' ) {
409+ if ( ! edge . style . stroke || edge . style . stroke === '#c9c9c9' ) {
469410 getUpdateEdgeConfig ( edge , '#2970ff' ) ;
470411 }
471412 } }
472413 onEdgeMouseLeave = { ( _ , edge ) => {
473- if ( [ '#2970ff' , " #c9c9c9" ] . includes ( edge . style . stroke ) ) {
414+ if ( [ '#2970ff' , ' #c9c9c9' ] . includes ( edge . style . stroke ) ) {
474415 getUpdateEdgeConfig ( edge , '#c9c9c9' ) ;
475416 }
476417 } }
477418 onNodesDelete = { ( ) => {
478- setActiveNode ( null ) ;
419+ setActiveNode ( null ) ;
479420 } }
480421 onNodeClick = { ( event , node ) => {
481422 onNodeClick && onNodeClick ( event , node ) ;
482423 } }
483424 deleteKeyCode = { globalConfig ?. deleteKeyCode }
484- onEdgeClick = { ( event , edge ) => {
485- onEdgeClick && onEdgeClick ( event , edge )
425+ onEdgeClick = { ( event , edge ) => {
426+ onEdgeClick && onEdgeClick ( event , edge ) ;
486427 } }
487428 >
488429 < CandidateNode />
@@ -504,6 +445,7 @@ const XFlow: FC<FlowProps> = memo(props => {
504445 return ;
505446 }
506447 setOpenPanel ( false ) ;
448+ workflowContainerRef . current ?. focus ( ) ;
507449
508450 // 如果日志面板关闭
509451 if ( ! isTruthy ( activeNode ?. _status ) || ! openLogPanel ) {
@@ -520,19 +462,22 @@ const XFlow: FC<FlowProps> = memo(props => {
520462 { NodeEditorWrap }
521463 </ PanelContainer >
522464 ) }
523- { isTruthy ( activeNode ?. _status ) && openLogPanel && Boolean ( logPanel ?. enable ?? true ) && (
524- < PanelStatusLogContainer
525- id = { activeNode ?. id }
526- nodeType = { activeNode ?. _nodeType }
527- onClose = { ( ) => {
528- setOpenLogPanel ( false ) ;
529- ! openPanel && setActiveNode ( null ) ;
530- } }
531- data = { activeNode ?. values }
532- >
533- { NodeLogWrap }
534- </ PanelStatusLogContainer >
535- ) }
465+ { isTruthy ( activeNode ?. _status ) &&
466+ openLogPanel &&
467+ Boolean ( logPanel ?. enable ?? true ) && (
468+ < PanelStatusLogContainer
469+ id = { activeNode ?. id }
470+ nodeType = { activeNode ?. _nodeType }
471+ onClose = { ( ) => {
472+ setOpenLogPanel ( false ) ;
473+ ! openPanel && setActiveNode ( null ) ;
474+ workflowContainerRef . current ?. focus ( ) ;
475+ } }
476+ data = { activeNode ?. values }
477+ >
478+ { NodeLogWrap }
479+ </ PanelStatusLogContainer >
480+ ) }
536481 </ ReactFlow >
537482 </ div >
538483 ) ;
0 commit comments