Skip to content

Commit 8912e51

Browse files
authored
Merge pull request #1720 from alibaba/xflow-version-m12
Xflow version m12
2 parents dcc004d + 1fa33e0 commit 8912e51

File tree

9 files changed

+235
-53
lines changed

9 files changed

+235
-53
lines changed

docs/xflow/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ group:
8080
| hideAutoLayout | 是否隐藏整理画布功能 | `boolean` | false |
8181
| hideFullscreen | 是否隐藏全屏功能 | `boolean` | false |
8282
| hideInteractionMode | 是否隐藏指针和手形工具切换功能 | `boolean` | false |
83+
| onAutoLayoutCompleted | 整理画布完成后的回调函数,接收整理后的节点数组作为参数,支持异步函数 | `(nodes: node[]) => void \| Promise<void>` | - |
8384

8485

8586

docs/xflow/demo/nodeSetting/fullDemo/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import XFlow from '@xrenders/xflow';
33
import { settings } from './settings';
44
import CustomSvg from './CustomSvg';
55
import CustomImg from './CustomImg';
6+
import { message } from 'antd';
67

78

89
const initialValues = {
@@ -94,6 +95,15 @@ const Demo = () => {
9495
nodeSelector={{
9596
showSearch: true,
9697
}}
98+
globalConfig={{
99+
controls: {
100+
onAutoLayoutCompleted: async (nodes) => {
101+
console.log('整理画布完成,节点数量:', nodes.length);
102+
console.log('整理后的节点数据:', nodes);
103+
message.success(`画布已整理完成,共 ${nodes.length} 个节点`);
104+
}
105+
}
106+
}}
97107
/>
98108
</div>
99109
);

packages/x-flow/src/XFlow.tsx

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ const XFlow: FC<FlowProps> = memo(props => {
210210
);
211211

212212
const { eventEmitter } = useEventEmitterContextContext();
213-
eventEmitter?.useSubscription((v: any) => {
213+
eventEmitter?.useSubscription(async (v: any) => {
214214
// 整理画布
215215
if (v.type === 'auto-layout-nodes') {
216216
const newNodes: any = autoLayoutNodes(
@@ -219,6 +219,12 @@ const XFlow: FC<FlowProps> = memo(props => {
219219
layout
220220
);
221221
setNodes(newNodes, false);
222+
223+
// 整理画布完成后执行回调
224+
const onAutoLayoutCompleted = globalConfig?.controls?.onAutoLayoutCompleted;
225+
if (onAutoLayoutCompleted) {
226+
await onAutoLayoutCompleted(newNodes);
227+
}
222228
}
223229

224230
if (v.type === 'deleteNode') {
@@ -244,18 +250,32 @@ const XFlow: FC<FlowProps> = memo(props => {
244250
setCandidateNode(newNode);
245251
};
246252

247-
// edge 移入/移出效果
248-
const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => {
249-
const newEdges = produce(edges, draft => {
250-
const currEdge: any = draft.find(e => e.id === edge.id);
251-
currEdge.style = {
252-
...edge.style,
253-
stroke: color,
254-
};
255-
currEdge.markerEnd = {
256-
...edge?.markerEnd,
257-
color,
258-
};
253+
254+
const hoveredEdgeIdRef = useRef<string | null>(null);// edge 移入/移出效果
255+
256+
const getUpdateEdgeConfig = useMemoizedFn((edgeId: string, color: string, shouldCheckColor = false, allowedColors?: string[]) => {
257+
const currentEdges = storeApi.getState().edges;
258+
const currEdge = currentEdges.find(e => e.id === edgeId);
259+
260+
// 如果需要检查颜色,只有在允许的颜色范围内才更新
261+
if (shouldCheckColor && allowedColors && currEdge?.style?.stroke) {
262+
if (!allowedColors.includes(currEdge.style.stroke)) {
263+
return; // 如果是自定义颜色,不更新
264+
}
265+
}
266+
267+
const newEdges = produce(currentEdges, draft => {
268+
const draftEdge: any = draft.find(e => e.id === edgeId);
269+
if (draftEdge) {
270+
draftEdge.style = {
271+
...draftEdge.style,
272+
stroke: color,
273+
};
274+
draftEdge.markerEnd = {
275+
...draftEdge.markerEnd,
276+
color,
277+
};
278+
}
259279
});
260280
setEdges(newEdges);
261281
});
@@ -340,6 +360,40 @@ const XFlow: FC<FlowProps> = memo(props => {
340360
const strokeWidth = globalConfig?.edge?.strokeWidth ?? 1.5;
341361
const panelonClose = globalConfig?.nodePanel?.onClose;
342362

363+
const handleClosePanel = useMemoizedFn(async () => {
364+
// 面板关闭校验表单
365+
const result = await nodeEditorRef?.current?.validateForm();
366+
if (!result) {
367+
return;
368+
}
369+
setOpenPanel(false);
370+
workflowContainerRef.current?.focus();
371+
372+
// 如果日志面板关闭
373+
if (!isTruthy(activeNode?._status) || !openLogPanel) {
374+
setActiveNode(null);
375+
}
376+
if (isFunction(panelonClose)) {
377+
panelonClose(activeNode?.id);
378+
}
379+
});
380+
381+
const handleCloseLogPanel = useMemoizedFn(() => {
382+
setOpenLogPanel(false);
383+
!openPanel && setActiveNode(null);
384+
workflowContainerRef.current?.focus();
385+
});
386+
387+
// 点击空白处关闭抽屉
388+
const handlePaneClick = useMemoizedFn(() => {
389+
if (openPanel && activeNode) {
390+
handleClosePanel();
391+
}
392+
if (openLogPanel && activeNode) {
393+
handleCloseLogPanel();
394+
}
395+
});
396+
343397
return (
344398
<div
345399
id="xflow-container"
@@ -357,6 +411,7 @@ const XFlow: FC<FlowProps> = memo(props => {
357411
panOnScroll={panOnScroll} // 禁用滚动平移
358412
preventScrolling={preventScrolling} // 允许页面滚动
359413
connectionLineComponent={connectionLineComponent}
414+
connectionRadius={100}
360415
defaultEdgeOptions={{
361416
type: 'buttonedge',
362417
style: {
@@ -414,14 +469,26 @@ const XFlow: FC<FlowProps> = memo(props => {
414469
});
415470
}}
416471
onEdgeMouseEnter={(_, edge: any) => {
417-
if (!edge.style.stroke || edge.style.stroke === '#c9c9c9') {
418-
getUpdateEdgeConfig(edge, '#2970ff');
472+
// 如果之前有 hover 的 edge,先重置它的颜色(只重置我们设置过的颜色)
473+
if (hoveredEdgeIdRef.current && hoveredEdgeIdRef.current !== edge.id) {
474+
getUpdateEdgeConfig(hoveredEdgeIdRef.current, '#c9c9c9', true, ['#2970ff', '#c9c9c9']);
475+
}
476+
hoveredEdgeIdRef.current = edge.id;
477+
// 设置当前 edge 为高亮色(只有当没有自定义颜色时才更新)
478+
const currentEdges = storeApi.getState().edges;
479+
const currentEdge = currentEdges.find(e => e.id === edge.id);
480+
const currentStroke = currentEdge?.style?.stroke;
481+
// 只有当没有设置颜色或是默认灰色时才设置为高亮色
482+
if (!currentStroke || currentStroke === '#c9c9c9') {
483+
getUpdateEdgeConfig(edge.id, '#2970ff');
419484
}
420485
}}
421486
onEdgeMouseLeave={(_, edge) => {
422-
if (['#2970ff', '#c9c9c9'].includes(edge.style.stroke)) {
423-
getUpdateEdgeConfig(edge, '#c9c9c9');
487+
if (hoveredEdgeIdRef.current === edge.id) {
488+
// 重置当前 edge 的颜色(只重置我们设置过的颜色)
489+
hoveredEdgeIdRef.current = null;
424490
}
491+
getUpdateEdgeConfig(edge.id, '#c9c9c9', true, ['#2970ff', '#c9c9c9']);
425492
}}
426493
onNodesDelete={() => {
427494
setActiveNode(null);
@@ -433,6 +500,7 @@ const XFlow: FC<FlowProps> = memo(props => {
433500
onEdgeClick={(event, edge) => {
434501
onEdgeClick && onEdgeClick(event, edge);
435502
}}
503+
onPaneClick={handlePaneClick}
436504
>
437505
<CandidateNode />
438506
<Operator addNode={handleAddNode} xflowRef={workflowContainerRef} />
@@ -446,23 +514,7 @@ const XFlow: FC<FlowProps> = memo(props => {
446514
<PanelContainer
447515
id={activeNode?.id}
448516
nodeType={activeNode?._nodeType}
449-
onClose={async () => {
450-
// 面板关闭校验表单
451-
const result = await nodeEditorRef?.current?.validateForm();
452-
if (!result) {
453-
return;
454-
}
455-
setOpenPanel(false);
456-
workflowContainerRef.current?.focus();
457-
458-
// 如果日志面板关闭
459-
if (!isTruthy(activeNode?._status) || !openLogPanel) {
460-
setActiveNode(null);
461-
}
462-
if (isFunction(panelonClose)) {
463-
panelonClose(activeNode?.id);
464-
}
465-
}}
517+
onClose={handleClosePanel}
466518
node={activeNode}
467519
data={activeNode?.values}
468520
openLogPanel={openLogPanel}
@@ -476,11 +528,7 @@ const XFlow: FC<FlowProps> = memo(props => {
476528
<PanelStatusLogContainer
477529
id={activeNode?.id}
478530
nodeType={activeNode?._nodeType}
479-
onClose={() => {
480-
setOpenLogPanel(false);
481-
!openPanel && setActiveNode(null);
482-
workflowContainerRef.current?.focus();
483-
}}
531+
onClose={handleCloseLogPanel}
484532
data={activeNode?.values}
485533
>
486534
{NodeLogWrap}

packages/x-flow/src/components/CustomEdge/index.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,35 @@ export default memo((edge: any) => {
5656
const { addNodes } = useFlow();
5757

5858
const handleAddNode = (data: any) => {
59-
const { screenToFlowPosition } = reactflow;
60-
const { x, y } = screenToFlowPosition({
61-
x: mousePosition.pageX,
62-
y: mousePosition.pageY,
63-
});
59+
const { getNode, screenToFlowPosition } = reactflow;
60+
const sourceNode = getNode(source);
61+
const targetNode = getNode(target);
62+
63+
// 节点默认尺寸
64+
const defaultNodeWidth = 204;
65+
const defaultNodeHeight = 45;
66+
67+
let x, y;
68+
69+
// 如果源节点和目标节点都存在,将新节点放在边的中点位置
70+
if (sourceNode && targetNode) {
71+
const sourceX = sourceNode.position.x + (sourceNode.width || defaultNodeWidth) / 2;
72+
const sourceY = sourceNode.position.y + (sourceNode.height || defaultNodeHeight) / 2;
73+
const targetX = targetNode.position.x + (targetNode.width || defaultNodeWidth) / 2;
74+
const targetY = targetNode.position.y + (targetNode.height || defaultNodeHeight) / 2;
75+
76+
// 计算中点位置
77+
x = (sourceX + targetX) / 2 - defaultNodeWidth / 2;
78+
y = (sourceY + targetY) / 2 - defaultNodeHeight / 2;
79+
} else {
80+
// 如果节点不存在,使用鼠标位置作为后备方案
81+
const fallbackPos = screenToFlowPosition({
82+
x: mousePosition.pageX,
83+
y: mousePosition.pageY,
84+
});
85+
x = fallbackPos.x;
86+
y = fallbackPos.y;
87+
}
6488

6589
const targetId = uuid();
6690
const title = settingMap[data?._nodeType]?.title || data?._nodeType;

packages/x-flow/src/components/CustomNode/index.less

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
border-radius: 14px;
44
position: relative;
55
background: #ffffff;
6+
67
.react-flow__edge-path,
78
.react-flow__connection-path {
89
stroke: #d0d5dc;
@@ -22,6 +23,7 @@
2223
transition: all .5s;
2324
}
2425
&:hover{
26+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
2527
.xflow-node-actions-container{
2628
opacity: 1;
2729
}

packages/x-flow/src/components/CustomNode/index.tsx

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,90 @@ export default memo((props: any) => {
9090

9191
// 增加节点并进行联系
9292
const handleAddNode = (data: any, sourceHandle?: string) => {
93-
const { screenToFlowPosition } = reactflow;
94-
const { x, y } = screenToFlowPosition({
95-
x: mousePosition.pageX + 100,
96-
y: mousePosition.pageY + 100,
97-
});
93+
const { getNode } = reactflow;
94+
const currentNode = getNode(id);
95+
if (!currentNode) return;
96+
97+
// 节点默认尺寸
98+
const defaultNodeWidth = 204;
99+
const defaultNodeHeight = 45;
100+
const nodeSpacing = 50; // 节点之间的最小间距
101+
102+
// 获取当前节点的位置和尺寸
103+
const currentNodeX = currentNode.position.x;
104+
const currentNodeY = currentNode.position.y;
105+
const currentNodeWidth = currentNode.width || defaultNodeWidth;
106+
const currentNodeHeight = currentNode.height || defaultNodeHeight;
107+
108+
// 根据布局方向计算新节点的初始位置
109+
// LR布局:新节点在右侧,从上到下堆叠(垂直方向堆叠)
110+
// TB布局:新节点在下方,从左到右堆叠(水平方向堆叠)
111+
const isTBLayout = layout === 'TB';
112+
113+
// 新节点的尺寸(假设与当前节点相同,实际可能需要根据节点类型调整)
114+
const newNodeWidth = defaultNodeWidth;
115+
const newNodeHeight = defaultNodeHeight;
116+
117+
// 检测是否有其他节点遮挡
118+
const checkCollision = (x: number, y: number, width: number, height: number) => {
119+
return nodes.some((node: any) => {
120+
if (node.id === id) return false; // 排除当前节点
121+
const nodeX = node.position?.x || 0;
122+
const nodeY = node.position?.y || 0;
123+
const nodeWidth = node.width || defaultNodeWidth;
124+
const nodeHeight = node.height || defaultNodeHeight;
125+
126+
// 检查是否重叠
127+
return !(
128+
x + width < nodeX ||
129+
x > nodeX + nodeWidth ||
130+
y + height < nodeY ||
131+
y > nodeY + nodeHeight
132+
);
133+
});
134+
};
135+
136+
// 计算新节点的初始位置
137+
let newX: number;
138+
let newY: number;
139+
140+
if (isTBLayout) {
141+
// TB布局:新节点在下方,从左到右堆叠
142+
// 初始位置:x 从当前节点左侧开始,y 在当前节点下方
143+
newX = currentNodeX;
144+
newY = currentNodeY + currentNodeHeight + nodeSpacing;
145+
} else {
146+
// LR布局:新节点在右侧,从上到下堆叠
147+
// 初始位置:x 在当前节点右侧,y 从当前节点顶部开始
148+
newX = currentNodeX + currentNodeWidth + nodeSpacing;
149+
newY = currentNodeY;
150+
}
151+
152+
// 如果有遮挡,根据布局方向移动找到空位置
153+
const maxAttempts = 50; // 最多尝试50次,确保能找到空位置
154+
let attempts = 0;
155+
while (checkCollision(newX, newY, newNodeWidth, newNodeHeight) && attempts < maxAttempts) {
156+
if (isTBLayout) {
157+
// TB布局:往右堆叠(水平方向移动)
158+
newX += newNodeWidth + nodeSpacing;
159+
} else {
160+
// LR布局:往下堆叠(垂直方向移动)
161+
newY += newNodeHeight + nodeSpacing;
162+
}
163+
attempts++;
164+
}
165+
166+
// 如果尝试次数过多,使用原始逻辑(基于鼠标位置)
167+
if (attempts >= maxAttempts) {
168+
const { screenToFlowPosition } = reactflow;
169+
const fallbackPos = screenToFlowPosition({
170+
x: mousePosition.pageX + 100,
171+
y: mousePosition.pageY + 100,
172+
});
173+
newX = fallbackPos.x;
174+
newY = fallbackPos.y;
175+
}
176+
98177
const targetId = uuid();
99178
const title = settingMap[data?._nodeType]?.title || data?._nodeType;
100179
const newNodes = {
@@ -104,7 +183,7 @@ export default memo((props: any) => {
104183
title: `${title}_${uuid4()}`,
105184
...data,
106185
},
107-
position: { x, y },
186+
position: { x: newX, y: newY },
108187
};
109188
const newEdges = {
110189
id: uuid(),

0 commit comments

Comments
 (0)