From d7ad2a57301f66ef987f6093ada9dbf20af083f6 Mon Sep 17 00:00:00 2001 From: ZLY Date: Tue, 16 Sep 2025 16:07:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(flowEditor):=20=E6=B7=BB=E5=8A=A0=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=AF=B9=E9=BD=90=E8=BE=85=E5=8A=A9=E7=BA=BF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useAlignmentGuidelines 钩子用于处理对齐逻辑 - 在 ReactFlow 组件中集成对齐辅助线 - 实现节点拖动时动态显示对齐线 - 拖动停止后清除对齐线 --- src/hooks/useAlignmentGuidelines.tsx | 125 +++++++++++++++++++++++++++ src/pages/flowEditor/index.tsx | 24 ++++- 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useAlignmentGuidelines.tsx diff --git a/src/hooks/useAlignmentGuidelines.tsx b/src/hooks/useAlignmentGuidelines.tsx new file mode 100644 index 0000000..0fbd29f --- /dev/null +++ b/src/hooks/useAlignmentGuidelines.tsx @@ -0,0 +1,125 @@ +import React, { useState, useCallback } from 'react'; +import { Node, useReactFlow } from '@xyflow/react'; + +interface Guideline { + type: 'left' | 'right' | 'center' | 'top' | 'middle' | 'bottom'; + x?: number; + y?: number; + from: string; + to: string; +} + +interface AlignmentGuidelines { + verticalLines: Array; + horizontalLines: Array; +} + +const TOLERANCE = 2; + +export const useAlignmentGuidelines = () => { + const [guidelines, setGuidelines] = useState({ + verticalLines: [], + horizontalLines: [] + }); + + const reactFlowInstance = useReactFlow(); + + const clearGuidelines = useCallback(() => { + setGuidelines({ verticalLines: [], horizontalLines: [] }); + }, []); + + const getGuidelines = useCallback((draggingNode: Node, allNodes: Node[]) => { + const { position, width = 100, height = 50 } = draggingNode; + const { x: dx, y: dy } = position; + const dWidth = width; + const dHeight = height; + + const verticalLines: Array = []; + const horizontalLines: Array = []; + + allNodes.forEach(node => { + if (node.id === draggingNode.id) return; + + const { position: pos, measured } = node; + const { x, y } = pos; + const width2 = measured.width | 0; + const height2 = measured.height | 0; + + // 垂直对齐 + if (Math.abs(dx - x) < TOLERANCE) { + verticalLines.push({ type: 'left', x: x - width2 - 1, from: draggingNode.id, to: node.id }); + } + + // 水平对齐 + if (Math.abs(dy - y) < TOLERANCE) { + horizontalLines.push({ type: 'top', y: y - height2 / 2 - 3, from: draggingNode.id, to: node.id }); + } + + }); + + setGuidelines({ verticalLines, horizontalLines }); + return { verticalLines, horizontalLines }; + }, []); + + const AlignmentGuides = () => { + // 将节点坐标转换为屏幕坐标 + const screenVerticalLines = guidelines.verticalLines.map(line => ({ + ...line, + x: reactFlowInstance.flowToScreenPosition({ x: line.x, y: 0 }).x + })); + + const screenHorizontalLines = guidelines.horizontalLines.map(line => ({ + ...line, + y: reactFlowInstance.flowToScreenPosition({ x: 0, y: line.y }).y + })); + + return ( + <> + {/* 垂直辅助线 */} + {screenVerticalLines.map((line, i) => ( +
+ ))} + + {/* 水平辅助线 */} + {screenHorizontalLines.map((line, i) => ( +
+ ))} + + ); + }; + + return { + guidelines, + getGuidelines, + clearGuidelines, + AlignmentGuides + }; +}; + +export type { AlignmentGuidelines, Guideline }; \ No newline at end of file diff --git a/src/pages/flowEditor/index.tsx b/src/pages/flowEditor/index.tsx index b676356..db8f46e 100644 --- a/src/pages/flowEditor/index.tsx +++ b/src/pages/flowEditor/index.tsx @@ -28,9 +28,10 @@ import NodeContextMenu from './components/nodeContextMenu'; import EdgeContextMenu from './components/edgeContextMenu'; import PaneContextMenu from './components/paneContextMenu'; import NodeEditModal from './components/nodeEditModal'; -import AddNodeMenu from './components/addNodeMenu'; // 添加导入 +import AddNodeMenu from './components/addNodeMenu'; import { defaultNodeTypes } from '@/pages/flowEditor/node/types/defaultType'; import { localNodeData } from '@/pages/flowEditor/sideBar/config/localNodeData'; +import { useAlignmentGuidelines } from '@/hooks/useAlignmentGuidelines'; const edgeTypes: EdgeTypes = { custom: CustomEdge @@ -69,6 +70,8 @@ const FlowEditor: React.FC = () => { const [edgeForNodeAdd, setEdgeForNodeAdd] = useState(null); const [positionForNodeAdd, setPositionForNodeAdd] = useState<{ x: number, y: number } | null>(null); + const { getGuidelines, clearGuidelines, AlignmentGuides } = useAlignmentGuidelines(); + // 获取handle类型 (api或data) const getHandleType = (handleId: string, nodeParams: any) => { // 检查是否为api类型的handle @@ -274,6 +277,20 @@ const FlowEditor: React.FC = () => { [reactFlowInstance] ); + const onNodeDrag = useCallback( + (_: any, node: Node) => { + // 获取对齐线 + getGuidelines(node, nodes); + }, + [nodes, getGuidelines] + ); + + // 节点拖拽结束处理 + const onNodeDragStop = useCallback(() => { + // 清除对齐线 + clearGuidelines(); + }, [clearGuidelines]); + useEffect(() => { const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData); // 为所有边添加类型 @@ -601,12 +618,16 @@ const FlowEditor: React.FC = () => { edges={edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} + snapToGrid={true} + snapGrid={[2, 2]} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onReconnect={onReconnect} onDrop={onDrop} onDragOver={onDragOver} + onNodeDrag={onNodeDrag} + onNodeDragStop={onNodeDragStop} onNodeContextMenu={onNodeContextMenu} onEdgeContextMenu={onEdgeContextMenu} onNodeDoubleClick={onNodeDoubleClick} @@ -642,6 +663,7 @@ const FlowEditor: React.FC = () => {
从左侧拖拽节点到画布中
+ {/*节点右键上下文*/}