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) return checkHidden(nodeId)
}, [getMember, collapsedNodes]) }, [getMember, collapsedNodes])
// 计算所有连线位置(使用 requestAnimationFrame 批量计算,避免 layout thrashing // 计算所有连线位置(使用 offsetLeft/offsetTop 获取本地坐标,不受 scale 影响
const rafIdRef = useRef<number | null>(null) const rafIdRef = useRef<number | null>(null)
const calculateConnections = useCallback(() => { const calculateConnections = useCallback(() => {
if (rafIdRef.current !== null) { if (rafIdRef.current !== null) {
@@ -158,9 +158,22 @@ export function TreeLayout({
} }
rafIdRef.current = requestAnimationFrame(() => { rafIdRef.current = requestAnimationFrame(() => {
rafIdRef.current = null 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 newConnections: Connection[] = []
const containerRect = containerRef.current?.getBoundingClientRect()
if (!containerRect || nodeRefs.current.size === 0) return
nodeRefs.current.forEach((nodeElement, nodeId) => { nodeRefs.current.forEach((nodeElement, nodeId) => {
const member = getMember(nodeId) const member = getMember(nodeId)
@@ -168,22 +181,22 @@ export function TreeLayout({
if (collapsedNodes.has(nodeId)) return if (collapsedNodes.has(nodeId)) return
if (isNodeHiddenByCollapse(nodeId)) return if (isNodeHiddenByCollapse(nodeId)) return
if (!document.body.contains(nodeElement)) return if (!document.body.contains(nodeElement)) return
const parentRect = nodeElement.getBoundingClientRect() const parentRect = getLocalRect(nodeElement)
if (parentRect.width === 0 || parentRect.height === 0) return if (parentRect.width === 0 || parentRect.height === 0) return
const parentCenterX = (parentRect.left + parentRect.width / 2 - containerRect.left) / scale const parentCenterX = parentRect.left + parentRect.width / 2
const parentBottomY = (parentRect.bottom - containerRect.top) / scale const parentBottomY = parentRect.top + parentRect.height
const childPositions = member.childrenIds const childPositions = member.childrenIds
.map(childId => { .map(childId => {
const childElement = nodeRefs.current.get(childId) const childElement = nodeRefs.current.get(childId)
if (!childElement || !document.body.contains(childElement)) return null 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 if (childRect.width === 0 || childRect.height === 0) return null
return { return {
id: childId, id: childId,
centerX: (childRect.left + childRect.width / 2 - containerRect.left) / scale, centerX: childRect.left + childRect.width / 2,
topY: (childRect.top - containerRect.top) / scale topY: childRect.top
} }
}) })
.filter(Boolean) as Array<{ id: string; centerX: number; topY: number }> .filter(Boolean) as Array<{ id: string; centerX: number; topY: number }>
@@ -218,9 +231,10 @@ export function TreeLayout({
setConnections(newConnections) setConnections(newConnections)
}) })
}, [getMember, collapsedNodes, isNodeHiddenByCollapse, scale]) }, [getMember, collapsedNodes, isNodeHiddenByCollapse])
// 使用 useLayoutEffect 在 DOM 更新后立即计算连线(替代 7 个 setTimeout // 使用 useLayoutEffect 在 DOM 更新后立即计算连线(替代 7 个 setTimeout
// offsetLeft/offsetTop 不受 scale 影响,无需依赖 scale
useLayoutEffect(() => { useLayoutEffect(() => {
calculateConnections() calculateConnections()
// 双 rAF 确保浏览器完成布局后再计算 // 双 rAF 确保浏览器完成布局后再计算
+1 -1
View File
File diff suppressed because one or more lines are too long