0.8.2.0
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"buildStage": "compile",
|
||||
"buildStage": "static-generation",
|
||||
"buildOptions": {
|
||||
"useBuildWorker": "true"
|
||||
}
|
||||
|
||||
+35
-6
@@ -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
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"lastValidatedTimestamp": 1766316281425,
|
||||
"lastValidatedTimestamp": 1766317151581,
|
||||
"projects": {},
|
||||
"pnpmfiles": [],
|
||||
"settings": {
|
||||
|
||||
Reference in New Issue
Block a user