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
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"buildStage": "compile",
"buildStage": "static-generation",
"buildOptions": {
"useBuildWorker": "true"
}
+35 -6
View File
@@ -295,11 +295,17 @@ export default function TreePage() {
scaleRef.current = scale
}, [scale])
// 使用原生事件监听器处理触摸和滚轮事件(避免 passive 事件问题)
useEffect(() => {
const container = containerRef.current
// 滚轮和触摸事件处理函数 - 使用 useCallback 确保稳定引用
const bindWheelEvents = useCallback((container: HTMLDivElement | null) => {
if (!container) return
// 清理之前的事件监听器
const cleanup = (container as any).__wheelCleanup
if (cleanup) {
cleanup()
delete (container as any).__wheelCleanup
}
// 鼠标滚轮缩放
const handleWheel = (e: WheelEvent) => {
e.preventDefault()
@@ -359,13 +365,36 @@ export default function TreePage() {
container.addEventListener('touchmove', handleTouchMove, { passive: false })
container.addEventListener('touchend', handleTouchEnd, { passive: true })
return () => {
// 保存清理函数
;(container as any).__wheelCleanup = () => {
container.removeEventListener('wheel', handleWheel)
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchmove', handleTouchMove)
container.removeEventListener('touchend', handleTouchEnd)
}
}, [viewMode])
}, [])
// 使用 ref callback 在 DOM 挂载时绑定事件
const containerRefCallback = useCallback((node: HTMLDivElement | null) => {
// 更新 ref
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
// 绑定事件
if (node && viewMode === 'traditional') {
bindWheelEvents(node)
}
}, [viewMode, bindWheelEvents])
// 清理事件监听器
useEffect(() => {
return () => {
const container = containerRef.current
if (container && (container as any).__wheelCleanup) {
(container as any).__wheelCleanup()
delete (container as any).__wheelCleanup
}
}
}, [])
// 处理成员选择(关系查询模式)
const handleMemberClick = (memberId: string) => {
@@ -780,7 +809,7 @@ export default function TreePage() {
{/* Tree Canvas */}
<div
ref={containerRef}
ref={containerRefCallback}
className="w-full h-full overflow-hidden cursor-move relative touch-none bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')] bg-repeat select-none"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
+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',
}}
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"lastValidatedTimestamp": 1766316281425,
"lastValidatedTimestamp": 1766317151581,
"projects": {},
"pnpmfiles": [],
"settings": {