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": {
|
"buildOptions": {
|
||||||
"useBuildWorker": "true"
|
"useBuildWorker": "true"
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-6
@@ -295,11 +295,17 @@ export default function TreePage() {
|
|||||||
scaleRef.current = scale
|
scaleRef.current = scale
|
||||||
}, [scale])
|
}, [scale])
|
||||||
|
|
||||||
// 使用原生事件监听器处理触摸和滚轮事件(避免 passive 事件问题)
|
// 滚轮和触摸事件处理函数 - 使用 useCallback 确保稳定引用
|
||||||
useEffect(() => {
|
const bindWheelEvents = useCallback((container: HTMLDivElement | null) => {
|
||||||
const container = containerRef.current
|
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
|
// 清理之前的事件监听器
|
||||||
|
const cleanup = (container as any).__wheelCleanup
|
||||||
|
if (cleanup) {
|
||||||
|
cleanup()
|
||||||
|
delete (container as any).__wheelCleanup
|
||||||
|
}
|
||||||
|
|
||||||
// 鼠标滚轮缩放
|
// 鼠标滚轮缩放
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -359,13 +365,36 @@ export default function TreePage() {
|
|||||||
container.addEventListener('touchmove', handleTouchMove, { passive: false })
|
container.addEventListener('touchmove', handleTouchMove, { passive: false })
|
||||||
container.addEventListener('touchend', handleTouchEnd, { passive: true })
|
container.addEventListener('touchend', handleTouchEnd, { passive: true })
|
||||||
|
|
||||||
return () => {
|
// 保存清理函数
|
||||||
|
;(container as any).__wheelCleanup = () => {
|
||||||
container.removeEventListener('wheel', handleWheel)
|
container.removeEventListener('wheel', handleWheel)
|
||||||
container.removeEventListener('touchstart', handleTouchStart)
|
container.removeEventListener('touchstart', handleTouchStart)
|
||||||
container.removeEventListener('touchmove', handleTouchMove)
|
container.removeEventListener('touchmove', handleTouchMove)
|
||||||
container.removeEventListener('touchend', handleTouchEnd)
|
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) => {
|
const handleMemberClick = (memberId: string) => {
|
||||||
@@ -780,7 +809,7 @@ export default function TreePage() {
|
|||||||
|
|
||||||
{/* Tree Canvas */}
|
{/* Tree Canvas */}
|
||||||
<div
|
<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"
|
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}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|||||||
+75
-35
@@ -31,6 +31,7 @@ export function Minimap({
|
|||||||
const minimapScaleRef = useRef(1)
|
const minimapScaleRef = useRef(1)
|
||||||
const treeOffsetRef = useRef({ x: 0, y: 0 })
|
const treeOffsetRef = useRef({ x: 0, y: 0 })
|
||||||
const treeBoundsRef = useRef({ minX: 0, minY: 0, maxX: 0, maxY: 0 })
|
const treeBoundsRef = useRef({ minX: 0, minY: 0, maxX: 0, maxY: 0 })
|
||||||
|
const lastMousePosRef = useRef({ x: 0, y: 0 })
|
||||||
|
|
||||||
// 更新缩略图
|
// 更新缩略图
|
||||||
const updateMinimap = useCallback(() => {
|
const updateMinimap = useCallback(() => {
|
||||||
@@ -59,7 +60,6 @@ export function Minimap({
|
|||||||
// 获取所有节点
|
// 获取所有节点
|
||||||
const nodes = container.querySelectorAll('[data-member-id]')
|
const nodes = container.querySelectorAll('[data-member-id]')
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
// 没有节点时显示提示
|
|
||||||
ctx.fillStyle = '#9ca3af'
|
ctx.fillStyle = '#9ca3af'
|
||||||
ctx.font = '12px sans-serif'
|
ctx.font = '12px sans-serif'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
@@ -69,30 +69,36 @@ export function Minimap({
|
|||||||
|
|
||||||
const containerRect = container.getBoundingClientRect()
|
const containerRect = container.getBoundingClientRect()
|
||||||
|
|
||||||
// 计算所有节点的边界
|
// 计算所有节点在原始坐标系中的边界
|
||||||
|
// 屏幕坐标 = 原始坐标 * scale + position
|
||||||
|
// 原始坐标 = (屏幕坐标 - position) / scale
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
||||||
const nodePositions: { x: number; y: number; w: number; h: number }[] = []
|
const nodePositions: { x: number; y: number; w: number; h: number }[] = []
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
const nodeRect = (node as HTMLElement).getBoundingClientRect()
|
const nodeRect = (node as HTMLElement).getBoundingClientRect()
|
||||||
// 计算相对于容器的位置
|
// 计算相对于容器的屏幕位置
|
||||||
const relativeX = nodeRect.left - containerRect.left
|
const screenX = nodeRect.left - containerRect.left
|
||||||
const relativeY = nodeRect.top - containerRect.top
|
const screenY = nodeRect.top - containerRect.top
|
||||||
const nodeWidth = nodeRect.width
|
|
||||||
const nodeHeight = nodeRect.height
|
|
||||||
|
|
||||||
minX = Math.min(minX, relativeX)
|
// 转换为原始坐标系(不受 position 和 scale 影响)
|
||||||
minY = Math.min(minY, relativeY)
|
const originalX = (screenX - position.x) / scale
|
||||||
maxX = Math.max(maxX, relativeX + nodeWidth)
|
const originalY = (screenY - position.y) / scale
|
||||||
maxY = Math.max(maxY, relativeY + nodeHeight)
|
const originalW = nodeRect.width / scale
|
||||||
|
const originalH = nodeRect.height / scale
|
||||||
|
|
||||||
nodePositions.push({ x: relativeX, y: relativeY, w: nodeWidth, h: 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: originalX, y: originalY, w: originalW, h: originalH })
|
||||||
})
|
})
|
||||||
|
|
||||||
if (minX === Infinity) return
|
if (minX === Infinity) return
|
||||||
|
|
||||||
// 添加边距
|
// 添加边距
|
||||||
const padding = 50
|
const padding = 30
|
||||||
minX -= padding
|
minX -= padding
|
||||||
minY -= padding
|
minY -= padding
|
||||||
maxX += padding
|
maxX += padding
|
||||||
@@ -111,21 +117,30 @@ export function Minimap({
|
|||||||
minimapScaleRef.current = minimapScale
|
minimapScaleRef.current = minimapScale
|
||||||
treeOffsetRef.current = { x: minX, y: minY }
|
treeOffsetRef.current = { x: minX, y: minY }
|
||||||
|
|
||||||
// 绘制节点
|
// 绘制节点(使用原始坐标)
|
||||||
ctx.fillStyle = '#3b82f6'
|
ctx.fillStyle = '#3b82f6'
|
||||||
nodePositions.forEach(({ x, y, w, h }) => {
|
nodePositions.forEach(({ x, y, w, h }) => {
|
||||||
const drawX = (x - minX) * minimapScale + 5
|
const drawX = (x - minX) * minimapScale + 5
|
||||||
const drawY = (y - minY) * minimapScale + 5
|
const drawY = (y - minY) * minimapScale + 5
|
||||||
const drawW = Math.max(w * minimapScale, 3)
|
const drawW = Math.max(w * minimapScale, 4)
|
||||||
const drawH = Math.max(h * minimapScale, 2)
|
const drawH = Math.max(h * minimapScale, 3)
|
||||||
ctx.fillRect(drawX, drawY, drawW, drawH)
|
ctx.beginPath()
|
||||||
|
ctx.roundRect(drawX, drawY, drawW, drawH, 2)
|
||||||
|
ctx.fill()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 绘制视口框(当前可见区域)
|
// 绘制视口框(当前可见区域)
|
||||||
const viewportX = (0 - minX) * minimapScale + 5
|
// 视口在原始坐标系中的位置
|
||||||
const viewportY = (0 - minY) * minimapScale + 5
|
const viewportOriginalX = -position.x / scale
|
||||||
const viewportW = containerRect.width * minimapScale
|
const viewportOriginalY = -position.y / scale
|
||||||
const viewportH = containerRect.height * minimapScale
|
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.strokeStyle = '#ef4444'
|
||||||
ctx.lineWidth = 2
|
ctx.lineWidth = 2
|
||||||
@@ -138,16 +153,16 @@ export function Minimap({
|
|||||||
// D3族谱模式 - 查找 SVG(在整个文档中查找)
|
// D3族谱模式 - 查找 SVG(在整个文档中查找)
|
||||||
// D3 图表的 SVG 在 chartRef 容器内
|
// D3 图表的 SVG 在 chartRef 容器内
|
||||||
const allSvgs = document.querySelectorAll('svg')
|
const allSvgs = document.querySelectorAll('svg')
|
||||||
let svg: SVGSVGElement | null = null
|
let foundSvg: SVGSVGElement | null = null
|
||||||
|
|
||||||
// 找到包含 foreignObject 的 SVG(这是 d3-org-chart 的特征)
|
// 找到包含 foreignObject 的 SVG(这是 d3-org-chart 的特征)
|
||||||
allSvgs.forEach((s) => {
|
allSvgs.forEach((s) => {
|
||||||
if (s.querySelector('foreignObject') && s.querySelector('g')) {
|
if (s.querySelector('foreignObject') && s.querySelector('g')) {
|
||||||
svg = s as SVGSVGElement
|
foundSvg = s as SVGSVGElement
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!svg) {
|
if (!foundSvg) {
|
||||||
ctx.fillStyle = '#9ca3af'
|
ctx.fillStyle = '#9ca3af'
|
||||||
ctx.font = '12px sans-serif'
|
ctx.font = '12px sans-serif'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
@@ -155,6 +170,7 @@ export function Minimap({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const svg = foundSvg as SVGSVGElement
|
||||||
const gElement = svg.querySelector('g')
|
const gElement = svg.querySelector('g')
|
||||||
if (!gElement) return
|
if (!gElement) return
|
||||||
|
|
||||||
@@ -281,7 +297,7 @@ export function Minimap({
|
|||||||
}
|
}
|
||||||
}, [isExpanded, updateMinimap])
|
}, [isExpanded, updateMinimap])
|
||||||
|
|
||||||
// 处理缩略图点击
|
// 处理缩略图点击(用于单击定位)
|
||||||
const handleMinimapClick = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
const handleMinimapClick = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (!minimapRef.current) return
|
if (!minimapRef.current) return
|
||||||
|
|
||||||
@@ -297,13 +313,17 @@ export function Minimap({
|
|||||||
const minimapScale = minimapScaleRef.current
|
const minimapScale = minimapScaleRef.current
|
||||||
const treeOffset = treeOffsetRef.current
|
const treeOffset = treeOffsetRef.current
|
||||||
|
|
||||||
// 计算点击位置对应的树坐标
|
// 计算点击位置对应的原始树坐标
|
||||||
const targetX = (clickX - 10) / minimapScale + treeOffset.x
|
// minimap 坐标 = (原始坐标 - offset) * minimapScale + 5
|
||||||
const targetY = (clickY - 10) / minimapScale + treeOffset.y
|
// 原始坐标 = (minimap 坐标 - 5) / minimapScale + offset
|
||||||
|
const targetOriginalX = (clickX - 5) / minimapScale + treeOffset.x
|
||||||
|
const targetOriginalY = (clickY - 5) / minimapScale + treeOffset.y
|
||||||
|
|
||||||
// 计算新的位置(使点击位置居中)
|
// 计算新的 position(使点击位置居中)
|
||||||
const newX = -(targetX * scale - containerRect.width / 2)
|
// 屏幕中心 = 原始坐标 * scale + position
|
||||||
const newY = -(targetY * scale - containerRect.height / 2)
|
// position = 屏幕中心 - 原始坐标 * scale
|
||||||
|
const newX = containerRect.width / 2 - targetOriginalX * scale
|
||||||
|
const newY = containerRect.height / 2 - targetOriginalY * scale
|
||||||
|
|
||||||
onPositionChange({ x: newX, y: newY })
|
onPositionChange({ x: newX, y: newY })
|
||||||
|
|
||||||
@@ -313,17 +333,37 @@ export function Minimap({
|
|||||||
}
|
}
|
||||||
}, [viewMode, containerRef, d3ChartRef, scale, onPositionChange])
|
}, [viewMode, containerRef, d3ChartRef, scale, onPositionChange])
|
||||||
|
|
||||||
// 处理拖拽
|
// 处理拖拽开始
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
|
lastMousePosRef.current = { x: e.clientX, y: e.clientY }
|
||||||
|
// 点击时也定位
|
||||||
handleMinimapClick(e)
|
handleMinimapClick(e)
|
||||||
}, [handleMinimapClick])
|
}, [handleMinimapClick])
|
||||||
|
|
||||||
|
// 处理拖拽移动(使用增量移动,更平滑)
|
||||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (isDragging) {
|
if (!isDragging) return
|
||||||
handleMinimapClick(e)
|
|
||||||
|
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(() => {
|
const handleMouseUp = useCallback(() => {
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
|
|||||||
@@ -389,12 +389,13 @@ export function TreeLayout({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="absolute bg-amber-700/70 dark:bg-amber-500/60"
|
className="absolute"
|
||||||
style={{
|
style={{
|
||||||
left: `${left}px`,
|
left: `${left}px`,
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
backgroundColor: 'rgba(180, 83, 9, 0.7)', // amber-700 with 70% opacity
|
||||||
transform: isHorizontal ? 'scaleY(0.5)' : 'scaleX(0.5)',
|
transform: isHorizontal ? 'scaleY(0.5)' : 'scaleX(0.5)',
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top left',
|
||||||
}}
|
}}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lastValidatedTimestamp": 1766316281425,
|
"lastValidatedTimestamp": 1766317151581,
|
||||||
"projects": {},
|
"projects": {},
|
||||||
"pnpmfiles": [],
|
"pnpmfiles": [],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
Reference in New Issue
Block a user