Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/xflow/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ group:
| hideAutoLayout | 是否隐藏整理画布功能 | `boolean` | false |
| hideFullscreen | 是否隐藏全屏功能 | `boolean` | false |
| hideInteractionMode | 是否隐藏指针和手形工具切换功能 | `boolean` | false |
| onAutoLayoutCompleted | 整理画布完成后的回调函数,接收整理后的节点数组作为参数,支持异步函数 | `(nodes: node[]) => void \| Promise<void>` | - |



Expand Down
10 changes: 10 additions & 0 deletions docs/xflow/demo/nodeSetting/fullDemo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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} 个节点`);
}
}
}}
/>
</div>
);
Expand Down
126 changes: 87 additions & 39 deletions packages/x-flow/src/XFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const XFlow: FC<FlowProps> = memo(props => {
);

const { eventEmitter } = useEventEmitterContextContext();
eventEmitter?.useSubscription((v: any) => {
eventEmitter?.useSubscription(async (v: any) => {
// 整理画布
if (v.type === 'auto-layout-nodes') {
const newNodes: any = autoLayoutNodes(
Expand All @@ -219,6 +219,12 @@ const XFlow: FC<FlowProps> = memo(props => {
layout
);
setNodes(newNodes, false);

// 整理画布完成后执行回调
const onAutoLayoutCompleted = globalConfig?.controls?.onAutoLayoutCompleted;
if (onAutoLayoutCompleted) {
await onAutoLayoutCompleted(newNodes);
}
}

if (v.type === 'deleteNode') {
Expand All @@ -244,18 +250,32 @@ const XFlow: FC<FlowProps> = 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<string | null>(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);
});
Expand Down Expand Up @@ -340,6 +360,40 @@ const XFlow: FC<FlowProps> = 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 (
<div
id="xflow-container"
Expand All @@ -357,6 +411,7 @@ const XFlow: FC<FlowProps> = memo(props => {
panOnScroll={panOnScroll} // 禁用滚动平移
preventScrolling={preventScrolling} // 允许页面滚动
connectionLineComponent={connectionLineComponent}
connectionRadius={100}
defaultEdgeOptions={{
type: 'buttonedge',
style: {
Expand Down Expand Up @@ -414,14 +469,26 @@ const XFlow: FC<FlowProps> = 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);
Expand All @@ -433,6 +500,7 @@ const XFlow: FC<FlowProps> = memo(props => {
onEdgeClick={(event, edge) => {
onEdgeClick && onEdgeClick(event, edge);
}}
onPaneClick={handlePaneClick}
>
<CandidateNode />
<Operator addNode={handleAddNode} xflowRef={workflowContainerRef} />
Expand All @@ -446,23 +514,7 @@ const XFlow: FC<FlowProps> = memo(props => {
<PanelContainer
id={activeNode?.id}
nodeType={activeNode?._nodeType}
onClose={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);
}
}}
onClose={handleClosePanel}
node={activeNode}
data={activeNode?.values}
openLogPanel={openLogPanel}
Expand All @@ -476,11 +528,7 @@ const XFlow: FC<FlowProps> = memo(props => {
<PanelStatusLogContainer
id={activeNode?.id}
nodeType={activeNode?._nodeType}
onClose={() => {
setOpenLogPanel(false);
!openPanel && setActiveNode(null);
workflowContainerRef.current?.focus();
}}
onClose={handleCloseLogPanel}
data={activeNode?.values}
>
{NodeLogWrap}
Expand Down
34 changes: 29 additions & 5 deletions packages/x-flow/src/components/CustomEdge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/x-flow/src/components/CustomNode/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
border-radius: 14px;
position: relative;
background: #ffffff;

.react-flow__edge-path,
.react-flow__connection-path {
stroke: #d0d5dc;
Expand All @@ -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;
}
Expand Down
91 changes: 85 additions & 6 deletions packages/x-flow/src/components/CustomNode/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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(),
Expand Down
Loading
Loading