fix: 连线位置偏移修复 - 改用 offsetLeft/offsetTop 替代 getBoundingClientRect
根因: getBoundingClientRect 返回屏幕坐标(受 scale 影响), 初始定位过程中 scale 从 1→0.45 变化时,连线在中间状态计算, 导致坐标与最终布局不匹配。 修复: 使用 offsetLeft/offsetTop 遍历 offsetParent 链获取本地坐标, 完全不受 CSS transform/scale 影响,消除 scale 依赖。
This commit is contained in:
@@ -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 确保浏览器完成布局后再计算
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user