From 380516fe90631b31cbca8f06f7d124a9a65c9cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Thu, 25 Dec 2025 11:55:28 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix=EF=BC=9A=E8=8A=82=E7=82=B9hover?= =?UTF-8?q?=E5=87=BA=E7=8E=B0=E9=98=B4=E5=BD=B1=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/components/CustomNode/index.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less index 32aa903ab..074c128df 100644 --- a/packages/x-flow/src/components/CustomNode/index.less +++ b/packages/x-flow/src/components/CustomNode/index.less @@ -3,6 +3,7 @@ border-radius: 14px; position: relative; background: #ffffff; + .react-flow__edge-path, .react-flow__connection-path { stroke: #d0d5dc; @@ -22,6 +23,7 @@ transition: all .5s; } &:hover{ + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); .xflow-node-actions-container{ opacity: 1; } From 14bcbc88bb8f32e7399b00fac183d1d23732ae68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Thu, 25 Dec 2025 19:16:05 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix:=E8=8A=82=E7=82=B9=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E6=89=A9=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/XFlow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 3f479303f..017621301 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -357,6 +357,7 @@ const XFlow: FC = memo(props => { panOnScroll={panOnScroll} // 禁用滚动平移 preventScrolling={preventScrolling} // 允许页面滚动 connectionLineComponent={connectionLineComponent} + connectionRadius={100} defaultEdgeOptions={{ type: 'buttonedge', style: { From c8347d1a1274d5116d5b304190ebeb2c9c527a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Thu, 25 Dec 2025 19:34:52 +0800 Subject: [PATCH 3/8] =?UTF-8?q?feat:=E6=95=B4=E7=90=86=E7=94=BB=E5=B8=83?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=90=8E=E9=80=8F=E5=87=BA=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=87=BD=E6=95=B0onAutoLayoutCompleted,=E8=AE=A9=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=8F=AF=E4=BB=A5=E8=87=AA=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/api.md | 1 + docs/xflow/demo/nodeSetting/fullDemo/index.tsx | 10 ++++++++++ packages/x-flow/src/XFlow.tsx | 8 +++++++- packages/x-flow/src/types.ts | 1 + 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/xflow/api.md b/docs/xflow/api.md index 664189381..735252cfb 100644 --- a/docs/xflow/api.md +++ b/docs/xflow/api.md @@ -80,6 +80,7 @@ group: | hideAutoLayout | 是否隐藏整理画布功能 | `boolean` | false | | hideFullscreen | 是否隐藏全屏功能 | `boolean` | false | | hideInteractionMode | 是否隐藏指针和手形工具切换功能 | `boolean` | false | +| onAutoLayoutCompleted | 整理画布完成后的回调函数,接收整理后的节点数组作为参数,支持异步函数 | `(nodes: node[]) => void \| Promise` | - | diff --git a/docs/xflow/demo/nodeSetting/fullDemo/index.tsx b/docs/xflow/demo/nodeSetting/fullDemo/index.tsx index a34a9b2a4..fb5d5c812 100644 --- a/docs/xflow/demo/nodeSetting/fullDemo/index.tsx +++ b/docs/xflow/demo/nodeSetting/fullDemo/index.tsx @@ -3,6 +3,7 @@ import XFlow from '@xrenders/xflow'; import { settings } from './settings'; import CustomSvg from './CustomSvg'; import CustomImg from './CustomImg'; +import { message } from 'antd'; const initialValues = { @@ -94,6 +95,15 @@ const Demo = () => { nodeSelector={{ showSearch: true, }} + globalConfig={{ + controls: { + onAutoLayoutCompleted: async (nodes) => { + console.log('整理画布完成,节点数量:', nodes.length); + console.log('整理后的节点数据:', nodes); + message.success(`画布已整理完成,共 ${nodes.length} 个节点`); + } + } + }} /> ); diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 017621301..0c4547177 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -210,7 +210,7 @@ const XFlow: FC = memo(props => { ); const { eventEmitter } = useEventEmitterContextContext(); - eventEmitter?.useSubscription((v: any) => { + eventEmitter?.useSubscription(async (v: any) => { // 整理画布 if (v.type === 'auto-layout-nodes') { const newNodes: any = autoLayoutNodes( @@ -219,6 +219,12 @@ const XFlow: FC = memo(props => { layout ); setNodes(newNodes, false); + + // 整理画布完成后执行回调 + const onAutoLayoutCompleted = globalConfig?.controls?.onAutoLayoutCompleted; + if (onAutoLayoutCompleted) { + await onAutoLayoutCompleted(newNodes); + } } if (v.type === 'deleteNode') { diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index 22c4d62aa..0c56bc173 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -138,6 +138,7 @@ export interface TControl{ hideAutoLayout?: boolean hideFullscreen?: boolean hideInteractionMode?: boolean + onAutoLayoutCompleted?: (nodes: node[]) => void | Promise; // 整理画布完成后的回调函数 } export interface THandle{ From c4b2d883c8639593c343973472727950d18e7959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Thu, 25 Dec 2025 19:53:22 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix:=E7=82=B9=E7=A9=BA=E7=99=BD=E5=A4=84?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E6=8A=BD=E5=B1=89=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/XFlow.tsx | 59 ++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 0c4547177..2286126e4 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -346,6 +346,40 @@ const XFlow: FC = memo(props => { const strokeWidth = globalConfig?.edge?.strokeWidth ?? 1.5; const panelonClose = globalConfig?.nodePanel?.onClose; + const handleClosePanel = useMemoizedFn(async () => { + // 面板关闭校验表单 + const result = await nodeEditorRef?.current?.validateForm(); + if (!result) { + return; + } + setOpenPanel(false); + workflowContainerRef.current?.focus(); + + // 如果日志面板关闭 + if (!isTruthy(activeNode?._status) || !openLogPanel) { + setActiveNode(null); + } + if (isFunction(panelonClose)) { + panelonClose(activeNode?.id); + } + }); + + const handleCloseLogPanel = useMemoizedFn(() => { + setOpenLogPanel(false); + !openPanel && setActiveNode(null); + workflowContainerRef.current?.focus(); + }); + + // 点击空白处关闭抽屉 + const handlePaneClick = useMemoizedFn(() => { + if (openPanel && activeNode) { + handleClosePanel(); + } + if (openLogPanel && activeNode) { + handleCloseLogPanel(); + } + }); + return (
= memo(props => { onEdgeClick={(event, edge) => { onEdgeClick && onEdgeClick(event, edge); }} + onPaneClick={handlePaneClick} > @@ -453,23 +488,7 @@ const XFlow: FC = memo(props => { { - // 面板关闭校验表单 - const result = await nodeEditorRef?.current?.validateForm(); - if (!result) { - return; - } - setOpenPanel(false); - workflowContainerRef.current?.focus(); - - // 如果日志面板关闭 - if (!isTruthy(activeNode?._status) || !openLogPanel) { - setActiveNode(null); - } - if (isFunction(panelonClose)) { - panelonClose(activeNode?.id); - } - }} + onClose={handleClosePanel} node={activeNode} data={activeNode?.values} openLogPanel={openLogPanel} @@ -483,11 +502,7 @@ const XFlow: FC = memo(props => { { - setOpenLogPanel(false); - !openPanel && setActiveNode(null); - workflowContainerRef.current?.focus(); - }} + onClose={handleCloseLogPanel} data={activeNode?.values} > {NodeLogWrap} From 417551132579e8f9a44bdbc9797788da199154da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Fri, 26 Dec 2025 10:31:50 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=E5=A4=9A=E6=9D=A1=E7=BA=BF=E7=A6=BB?= =?UTF-8?q?=E5=BE=97=E8=BF=91=E7=9A=84=E6=97=B6=E5=80=99=EF=BC=8C=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E5=88=87=E6=8D=A2hover=E7=9A=84=E6=97=B6=E5=80=99?= =?UTF-8?q?=E7=BA=BF=E7=9A=84=E9=A2=9C=E8=89=B2=E6=B2=A1=E6=9C=89=E6=B6=88?= =?UTF-8?q?=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/XFlow.tsx | 58 +++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 2286126e4..f828a2348 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -250,18 +250,32 @@ const XFlow: FC = memo(props => { setCandidateNode(newNode); }; - // edge 移入/移出效果 - const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => { - const newEdges = produce(edges, draft => { - const currEdge: any = draft.find(e => e.id === edge.id); - currEdge.style = { - ...edge.style, - stroke: color, - }; - currEdge.markerEnd = { - ...edge?.markerEnd, - color, - }; + + const hoveredEdgeIdRef = useRef(null);// edge 移入/移出效果 + + const getUpdateEdgeConfig = useMemoizedFn((edgeId: string, color: string, shouldCheckColor = false, allowedColors?: string[]) => { + const currentEdges = storeApi.getState().edges; + const currEdge = currentEdges.find(e => e.id === edgeId); + + // 如果需要检查颜色,只有在允许的颜色范围内才更新 + if (shouldCheckColor && allowedColors && currEdge?.style?.stroke) { + if (!allowedColors.includes(currEdge.style.stroke)) { + return; // 如果是自定义颜色,不更新 + } + } + + const newEdges = produce(currentEdges, draft => { + const draftEdge: any = draft.find(e => e.id === edgeId); + if (draftEdge) { + draftEdge.style = { + ...draftEdge.style, + stroke: color, + }; + draftEdge.markerEnd = { + ...draftEdge.markerEnd, + color, + }; + } }); setEdges(newEdges); }); @@ -455,14 +469,26 @@ const XFlow: FC = memo(props => { }); }} onEdgeMouseEnter={(_, edge: any) => { - if (!edge.style.stroke || edge.style.stroke === '#c9c9c9') { - getUpdateEdgeConfig(edge, '#2970ff'); + // 如果之前有 hover 的 edge,先重置它的颜色(只重置我们设置过的颜色) + if (hoveredEdgeIdRef.current && hoveredEdgeIdRef.current !== edge.id) { + getUpdateEdgeConfig(hoveredEdgeIdRef.current, '#c9c9c9', true, ['#2970ff', '#c9c9c9']); + } + hoveredEdgeIdRef.current = edge.id; + // 设置当前 edge 为高亮色(只有当没有自定义颜色时才更新) + const currentEdges = storeApi.getState().edges; + const currentEdge = currentEdges.find(e => e.id === edge.id); + const currentStroke = currentEdge?.style?.stroke; + // 只有当没有设置颜色或是默认灰色时才设置为高亮色 + if (!currentStroke || currentStroke === '#c9c9c9') { + getUpdateEdgeConfig(edge.id, '#2970ff'); } }} onEdgeMouseLeave={(_, edge) => { - if (['#2970ff', '#c9c9c9'].includes(edge.style.stroke)) { - getUpdateEdgeConfig(edge, '#c9c9c9'); + if (hoveredEdgeIdRef.current === edge.id) { + // 重置当前 edge 的颜色(只重置我们设置过的颜色) + hoveredEdgeIdRef.current = null; } + getUpdateEdgeConfig(edge.id, '#c9c9c9', true, ['#2970ff', '#c9c9c9']); }} onNodesDelete={() => { setActiveNode(null); From f9c13fb4a31de648aaa1c79774aa940bd04597aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Fri, 26 Dec 2025 11:37:08 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix:=E5=8E=BB=E9=99=A4=E5=85=A8=E9=80=89?= =?UTF-8?q?=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/index.less | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/x-flow/src/index.less b/packages/x-flow/src/index.less index 540913a0b..4d3592257 100644 --- a/packages/x-flow/src/index.less +++ b/packages/x-flow/src/index.less @@ -3,8 +3,25 @@ width: 100%; background: #F0F2F7; position: relative; - .react-flow__attribution { display: none; } -} \ No newline at end of file + + // 移除框选时画布边框的蓝色 + .react-flow__selection { + border: none !important; + outline: none !important; + } + + // 移除框选矩形区域的边框 + // .react-flow__nodesselection-rect { + // border: none !important; + // outline: none !important; + // } + + // 移除焦点时的蓝色边框 + &:focus { + outline: none !important; + border: none !important; + } +} From 7e7ddfe135cdb48dea5a903703ecf22a1397ecfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Fri, 26 Dec 2025 14:06:14 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=E5=8E=BB=E9=99=A4=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/nodes/node-common/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-flow/src/nodes/node-common/index.tsx b/packages/x-flow/src/nodes/node-common/index.tsx index de1fcd99a..384dc64fe 100644 --- a/packages/x-flow/src/nodes/node-common/index.tsx +++ b/packages/x-flow/src/nodes/node-common/index.tsx @@ -18,7 +18,7 @@ export default memo((props: any) => { return ( Date: Fri, 26 Dec 2025 14:35:12 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix=EF=BC=9A=E8=8A=82=E7=82=B9=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=B8=80=E4=B8=AA=E8=8A=82=E7=82=B9=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=8A=82=E7=82=B9=E7=9A=84=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CustomEdge/index.tsx | 34 ++++++- .../src/components/CustomNode/index.tsx | 91 +++++++++++++++++-- .../x-flow/src/nodes/node-common/index.tsx | 2 +- 3 files changed, 115 insertions(+), 12 deletions(-) diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index a63b93fe7..bb4f205ed 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -56,11 +56,35 @@ export default memo((edge: any) => { const { addNodes } = useFlow(); const handleAddNode = (data: any) => { - const { screenToFlowPosition } = reactflow; - const { x, y } = screenToFlowPosition({ - x: mousePosition.pageX, - y: mousePosition.pageY, - }); + const { getNode, screenToFlowPosition } = reactflow; + const sourceNode = getNode(source); + const targetNode = getNode(target); + + // 节点默认尺寸 + const defaultNodeWidth = 204; + const defaultNodeHeight = 45; + + let x, y; + + // 如果源节点和目标节点都存在,将新节点放在边的中点位置 + if (sourceNode && targetNode) { + const sourceX = sourceNode.position.x + (sourceNode.width || defaultNodeWidth) / 2; + const sourceY = sourceNode.position.y + (sourceNode.height || defaultNodeHeight) / 2; + const targetX = targetNode.position.x + (targetNode.width || defaultNodeWidth) / 2; + const targetY = targetNode.position.y + (targetNode.height || defaultNodeHeight) / 2; + + // 计算中点位置 + x = (sourceX + targetX) / 2 - defaultNodeWidth / 2; + y = (sourceY + targetY) / 2 - defaultNodeHeight / 2; + } else { + // 如果节点不存在,使用鼠标位置作为后备方案 + const fallbackPos = screenToFlowPosition({ + x: mousePosition.pageX, + y: mousePosition.pageY, + }); + x = fallbackPos.x; + y = fallbackPos.y; + } const targetId = uuid(); const title = settingMap[data?._nodeType]?.title || data?._nodeType; diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 9b3651145..48bf6ee79 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -90,11 +90,90 @@ export default memo((props: any) => { // 增加节点并进行联系 const handleAddNode = (data: any, sourceHandle?: string) => { - const { screenToFlowPosition } = reactflow; - const { x, y } = screenToFlowPosition({ - x: mousePosition.pageX + 100, - y: mousePosition.pageY + 100, - }); + const { getNode } = reactflow; + const currentNode = getNode(id); + if (!currentNode) return; + + // 节点默认尺寸 + const defaultNodeWidth = 204; + const defaultNodeHeight = 45; + const nodeSpacing = 50; // 节点之间的最小间距 + + // 获取当前节点的位置和尺寸 + const currentNodeX = currentNode.position.x; + const currentNodeY = currentNode.position.y; + const currentNodeWidth = currentNode.width || defaultNodeWidth; + const currentNodeHeight = currentNode.height || defaultNodeHeight; + + // 根据布局方向计算新节点的初始位置 + // LR布局:新节点在右侧,从上到下堆叠(垂直方向堆叠) + // TB布局:新节点在下方,从左到右堆叠(水平方向堆叠) + const isTBLayout = layout === 'TB'; + + // 新节点的尺寸(假设与当前节点相同,实际可能需要根据节点类型调整) + const newNodeWidth = defaultNodeWidth; + const newNodeHeight = defaultNodeHeight; + + // 检测是否有其他节点遮挡 + const checkCollision = (x: number, y: number, width: number, height: number) => { + return nodes.some((node: any) => { + if (node.id === id) return false; // 排除当前节点 + const nodeX = node.position?.x || 0; + const nodeY = node.position?.y || 0; + const nodeWidth = node.width || defaultNodeWidth; + const nodeHeight = node.height || defaultNodeHeight; + + // 检查是否重叠 + return !( + x + width < nodeX || + x > nodeX + nodeWidth || + y + height < nodeY || + y > nodeY + nodeHeight + ); + }); + }; + + // 计算新节点的初始位置 + let newX: number; + let newY: number; + + if (isTBLayout) { + // TB布局:新节点在下方,从左到右堆叠 + // 初始位置:x 从当前节点左侧开始,y 在当前节点下方 + newX = currentNodeX; + newY = currentNodeY + currentNodeHeight + nodeSpacing; + } else { + // LR布局:新节点在右侧,从上到下堆叠 + // 初始位置:x 在当前节点右侧,y 从当前节点顶部开始 + newX = currentNodeX + currentNodeWidth + nodeSpacing; + newY = currentNodeY; + } + + // 如果有遮挡,根据布局方向移动找到空位置 + const maxAttempts = 50; // 最多尝试50次,确保能找到空位置 + let attempts = 0; + while (checkCollision(newX, newY, newNodeWidth, newNodeHeight) && attempts < maxAttempts) { + if (isTBLayout) { + // TB布局:往右堆叠(水平方向移动) + newX += newNodeWidth + nodeSpacing; + } else { + // LR布局:往下堆叠(垂直方向移动) + newY += newNodeHeight + nodeSpacing; + } + attempts++; + } + + // 如果尝试次数过多,使用原始逻辑(基于鼠标位置) + if (attempts >= maxAttempts) { + const { screenToFlowPosition } = reactflow; + const fallbackPos = screenToFlowPosition({ + x: mousePosition.pageX + 100, + y: mousePosition.pageY + 100, + }); + newX = fallbackPos.x; + newY = fallbackPos.y; + } + const targetId = uuid(); const title = settingMap[data?._nodeType]?.title || data?._nodeType; const newNodes = { @@ -104,7 +183,7 @@ export default memo((props: any) => { title: `${title}_${uuid4()}`, ...data, }, - position: { x, y }, + position: { x: newX, y: newY }, }; const newEdges = { id: uuid(), diff --git a/packages/x-flow/src/nodes/node-common/index.tsx b/packages/x-flow/src/nodes/node-common/index.tsx index 384dc64fe..b9e8c2d14 100644 --- a/packages/x-flow/src/nodes/node-common/index.tsx +++ b/packages/x-flow/src/nodes/node-common/index.tsx @@ -18,7 +18,7 @@ export default memo((props: any) => { return (