fix: 连线位置偏移修复 - 改用 offsetLeft/offsetTop 替代 getBoundingClientRect

根因: getBoundingClientRect 返回屏幕坐标(受 scale 影响),
初始定位过程中 scale 从 1→0.45 变化时,连线在中间状态计算,
导致坐标与最终布局不匹配。

修复: 使用 offsetLeft/offsetTop 遍历 offsetParent 链获取本地坐标,
完全不受 CSS transform/scale 影响,消除 scale 依赖。
This commit is contained in:
selfrelease
2026-07-16 16:03:40 +08:00
parent ce06674cba
commit b4536c52ef
2 changed files with 25 additions and 11 deletions
+24 -10
View File
@@ -150,7 +150,7 @@ export function TreeLayout({
return checkHidden(nodeId)
}, [getMember, collapsedNodes])
// 计算所有连线位置(使用 requestAnimationFrame 批量计算,避免 layout thrashing
// 计算所有连线位置(使用 offsetLeft/offsetTop 获取本地坐标,不受 scale 影响
const rafIdRef = useRef<number | null>(null)
const calculateConnections = useCallback(() => {
if (rafIdRef.current !== null) {
@@ -158,9 +158,22 @@ export function TreeLayout({
}
rafIdRef.current = requestAnimationFrame(() => {
rafIdRef.current = null
const container = containerRef.current
if (!container || nodeRefs.current.size === 0) return
// 辅助函数:获取元素相对于容器的本地坐标(不受 scale/transform 影响)
const getLocalRect = (el: HTMLElement) => {
let top = 0, left = 0
let node = el
while (node && node !== container) {
top += node.offsetTop
left += node.offsetLeft
node = node.offsetParent as HTMLElement
}
return { top, left, width: el.offsetWidth, height: el.offsetHeight }
}
const newConnections: Connection[] = []
const containerRect = containerRef.current?.getBoundingClientRect()
if (!containerRect || nodeRefs.current.size === 0) return
nodeRefs.current.forEach((nodeElement, nodeId) => {
const member = getMember(nodeId)
@@ -168,22 +181,22 @@ export function TreeLayout({
if (collapsedNodes.has(nodeId)) return
if (isNodeHiddenByCollapse(nodeId)) return
if (!document.body.contains(nodeElement)) return
const parentRect = nodeElement.getBoundingClientRect()
const parentRect = getLocalRect(nodeElement)
if (parentRect.width === 0 || parentRect.height === 0) return
const parentCenterX = (parentRect.left + parentRect.width / 2 - containerRect.left) / scale
const parentBottomY = (parentRect.bottom - containerRect.top) / scale
const parentCenterX = parentRect.left + parentRect.width / 2
const parentBottomY = parentRect.top + parentRect.height
const childPositions = member.childrenIds
.map(childId => {
const childElement = nodeRefs.current.get(childId)
if (!childElement || !document.body.contains(childElement)) return null
const childRect = childElement.getBoundingClientRect()
const childRect = getLocalRect(childElement)
if (childRect.width === 0 || childRect.height === 0) return null
return {
id: childId,
centerX: (childRect.left + childRect.width / 2 - containerRect.left) / scale,
topY: (childRect.top - containerRect.top) / scale
centerX: childRect.left + childRect.width / 2,
topY: childRect.top
}
})
.filter(Boolean) as Array<{ id: string; centerX: number; topY: number }>
@@ -218,9 +231,10 @@ export function TreeLayout({
setConnections(newConnections)
})
}, [getMember, collapsedNodes, isNodeHiddenByCollapse, scale])
}, [getMember, collapsedNodes, isNodeHiddenByCollapse])
// 使用 useLayoutEffect 在 DOM 更新后立即计算连线(替代 7 个 setTimeout
// offsetLeft/offsetTop 不受 scale 影响,无需依赖 scale
useLayoutEffect(() => {
calculateConnections()
// 双 rAF 确保浏览器完成布局后再计算
+1 -1
View File
File diff suppressed because one or more lines are too long