This commit is contained in:
freedakgmail
2025-12-21 19:59:57 +08:00
parent 3489a10807
commit 5983f13282
7 changed files with 116 additions and 46 deletions
+75 -35
View File
@@ -31,6 +31,7 @@ export function Minimap({
const minimapScaleRef = useRef(1)
const treeOffsetRef = useRef({ x: 0, y: 0 })
const treeBoundsRef = useRef({ minX: 0, minY: 0, maxX: 0, maxY: 0 })
const lastMousePosRef = useRef({ x: 0, y: 0 })
// 更新缩略图
const updateMinimap = useCallback(() => {
@@ -59,7 +60,6 @@ export function Minimap({
// 获取所有节点
const nodes = container.querySelectorAll('[data-member-id]')
if (nodes.length === 0) {
// 没有节点时显示提示
ctx.fillStyle = '#9ca3af'
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
@@ -69,30 +69,36 @@ export function Minimap({
const containerRect = container.getBoundingClientRect()
// 计算所有节点的边界
// 计算所有节点在原始坐标系中的边界
// 屏幕坐标 = 原始坐标 * scale + position
// 原始坐标 = (屏幕坐标 - position) / scale
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
const nodePositions: { x: number; y: number; w: number; h: number }[] = []
nodes.forEach((node) => {
const nodeRect = (node as HTMLElement).getBoundingClientRect()
// 计算相对于容器的位置
const relativeX = nodeRect.left - containerRect.left
const relativeY = nodeRect.top - containerRect.top
const nodeWidth = nodeRect.width
const nodeHeight = nodeRect.height
// 计算相对于容器的屏幕位置
const screenX = nodeRect.left - containerRect.left
const screenY = nodeRect.top - containerRect.top
// 转换为原始坐标系(不受 position 和 scale 影响)
const originalX = (screenX - position.x) / scale
const originalY = (screenY - position.y) / scale
const originalW = nodeRect.width / scale
const originalH = nodeRect.height / scale
minX = Math.min(minX, relativeX)
minY = Math.min(minY, relativeY)
maxX = Math.max(maxX, relativeX + nodeWidth)
maxY = Math.max(maxY, relativeY + nodeHeight)
minX = Math.min(minX, originalX)
minY = Math.min(minY, originalY)
maxX = Math.max(maxX, originalX + originalW)
maxY = Math.max(maxY, originalY + originalH)
nodePositions.push({ x: relativeX, y: relativeY, w: nodeWidth, h: nodeHeight })
nodePositions.push({ x: originalX, y: originalY, w: originalW, h: originalH })
})
if (minX === Infinity) return
// 添加边距
const padding = 50
const padding = 30
minX -= padding
minY -= padding
maxX += padding
@@ -111,21 +117,30 @@ export function Minimap({
minimapScaleRef.current = minimapScale
treeOffsetRef.current = { x: minX, y: minY }
// 绘制节点
// 绘制节点(使用原始坐标)
ctx.fillStyle = '#3b82f6'
nodePositions.forEach(({ x, y, w, h }) => {
const drawX = (x - minX) * minimapScale + 5
const drawY = (y - minY) * minimapScale + 5
const drawW = Math.max(w * minimapScale, 3)
const drawH = Math.max(h * minimapScale, 2)
ctx.fillRect(drawX, drawY, drawW, drawH)
const drawW = Math.max(w * minimapScale, 4)
const drawH = Math.max(h * minimapScale, 3)
ctx.beginPath()
ctx.roundRect(drawX, drawY, drawW, drawH, 2)
ctx.fill()
})
// 绘制视口框(当前可见区域)
const viewportX = (0 - minX) * minimapScale + 5
const viewportY = (0 - minY) * minimapScale + 5
const viewportW = containerRect.width * minimapScale
const viewportH = containerRect.height * minimapScale
// 视口在原始坐标系中的位置
const viewportOriginalX = -position.x / scale
const viewportOriginalY = -position.y / scale
const viewportOriginalW = containerRect.width / scale
const viewportOriginalH = containerRect.height / scale
// 转换到 minimap 坐标
const viewportX = (viewportOriginalX - minX) * minimapScale + 5
const viewportY = (viewportOriginalY - minY) * minimapScale + 5
const viewportW = viewportOriginalW * minimapScale
const viewportH = viewportOriginalH * minimapScale
ctx.strokeStyle = '#ef4444'
ctx.lineWidth = 2
@@ -138,16 +153,16 @@ export function Minimap({
// D3族谱模式 - 查找 SVG(在整个文档中查找)
// D3 图表的 SVG 在 chartRef 容器内
const allSvgs = document.querySelectorAll('svg')
let svg: SVGSVGElement | null = null
let foundSvg: SVGSVGElement | null = null
// 找到包含 foreignObject 的 SVG(这是 d3-org-chart 的特征)
allSvgs.forEach((s) => {
if (s.querySelector('foreignObject') && s.querySelector('g')) {
svg = s as SVGSVGElement
foundSvg = s as SVGSVGElement
}
})
if (!svg) {
if (!foundSvg) {
ctx.fillStyle = '#9ca3af'
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
@@ -155,6 +170,7 @@ export function Minimap({
return
}
const svg = foundSvg as SVGSVGElement
const gElement = svg.querySelector('g')
if (!gElement) return
@@ -281,7 +297,7 @@ export function Minimap({
}
}, [isExpanded, updateMinimap])
// 处理缩略图点击
// 处理缩略图点击(用于单击定位)
const handleMinimapClick = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
if (!minimapRef.current) return
@@ -297,13 +313,17 @@ export function Minimap({
const minimapScale = minimapScaleRef.current
const treeOffset = treeOffsetRef.current
// 计算点击位置对应的树坐标
const targetX = (clickX - 10) / minimapScale + treeOffset.x
const targetY = (clickY - 10) / minimapScale + treeOffset.y
// 计算点击位置对应的原始树坐标
// minimap 坐标 = (原始坐标 - offset) * minimapScale + 5
// 原始坐标 = (minimap 坐标 - 5) / minimapScale + offset
const targetOriginalX = (clickX - 5) / minimapScale + treeOffset.x
const targetOriginalY = (clickY - 5) / minimapScale + treeOffset.y
// 计算新的位置(使点击位置居中)
const newX = -(targetX * scale - containerRect.width / 2)
const newY = -(targetY * scale - containerRect.height / 2)
// 计算新的 position(使点击位置居中)
// 屏幕中心 = 原始坐标 * scale + position
// position = 屏幕中心 - 原始坐标 * scale
const newX = containerRect.width / 2 - targetOriginalX * scale
const newY = containerRect.height / 2 - targetOriginalY * scale
onPositionChange({ x: newX, y: newY })
@@ -313,17 +333,37 @@ export function Minimap({
}
}, [viewMode, containerRef, d3ChartRef, scale, onPositionChange])
// 处理拖拽
// 处理拖拽开始
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
setIsDragging(true)
lastMousePosRef.current = { x: e.clientX, y: e.clientY }
// 点击时也定位
handleMinimapClick(e)
}, [handleMinimapClick])
// 处理拖拽移动(使用增量移动,更平滑)
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
if (isDragging) {
handleMinimapClick(e)
if (!isDragging) return
if (viewMode === 'traditional' && containerRef?.current && onPositionChange) {
const minimapScale = minimapScaleRef.current
// 计算鼠标移动的增量
const deltaX = e.clientX - lastMousePosRef.current.x
const deltaY = e.clientY - lastMousePosRef.current.y
// 更新上次鼠标位置
lastMousePosRef.current = { x: e.clientX, y: e.clientY }
// 将 minimap 上的移动转换为主视图的移动(方向相反)
// minimap 上移动 1px = 主视图移动 (1 / minimapScale * scale) px
const moveRatio = scale / minimapScale
const newX = position.x - deltaX * moveRatio
const newY = position.y - deltaY * moveRatio
onPositionChange({ x: newX, y: newY })
}
}, [isDragging, handleMinimapClick])
}, [isDragging, viewMode, containerRef, scale, position, onPositionChange])
const handleMouseUp = useCallback(() => {
setIsDragging(false)
+2 -1
View File
@@ -389,12 +389,13 @@ export function TreeLayout({
return (
<div
key={index}
className="absolute bg-amber-700/70 dark:bg-amber-500/60"
className="absolute"
style={{
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
backgroundColor: 'rgba(180, 83, 9, 0.7)', // amber-700 with 70% opacity
transform: isHorizontal ? 'scaleY(0.5)' : 'scaleX(0.5)',
transformOrigin: 'top left',
}}