216 lines
7.2 KiB
TypeScript
216 lines
7.2 KiB
TypeScript
"use client"
|
||
|
||
import type React from "react"
|
||
|
||
import { SiteHeader } from "@/components/site-header"
|
||
import { TreeLayout } from "@/components/tree/tree-layout"
|
||
import { useFamily } from "@/context/family-context"
|
||
import { Button } from "@/components/ui/button"
|
||
import { ZoomIn, ZoomOut, Move, Filter, Download } from "lucide-react"
|
||
import { useState, useRef } from "react"
|
||
|
||
export default function TreePage() {
|
||
const { treeData } = useFamily()
|
||
const [scale, setScale] = useState(1)
|
||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||
const [isDragging, setIsDragging] = useState(false)
|
||
const [startPos, setStartPos] = useState({ x: 0, y: 0 })
|
||
const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 })
|
||
const containerRef = useRef<HTMLDivElement>(null)
|
||
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
// 如果点击的是节点卡片,不启动拖拽
|
||
const target = e.target as HTMLElement
|
||
if (target.closest('.family-node-card')) {
|
||
return
|
||
}
|
||
|
||
setIsDragging(true)
|
||
setStartPos({ x: e.clientX - position.x, y: e.clientY - position.y })
|
||
setDragStartPos({ x: e.clientX, y: e.clientY })
|
||
}
|
||
|
||
const handleMouseMove = (e: React.MouseEvent) => {
|
||
if (!isDragging) return
|
||
|
||
// 只有移动超过5px才算拖拽,避免误触
|
||
const deltaX = Math.abs(e.clientX - dragStartPos.x)
|
||
const deltaY = Math.abs(e.clientY - dragStartPos.y)
|
||
if (deltaX < 5 && deltaY < 5) return
|
||
|
||
setPosition({
|
||
x: e.clientX - startPos.x,
|
||
y: e.clientY - startPos.y,
|
||
})
|
||
}
|
||
|
||
const handleMouseUp = () => {
|
||
setIsDragging(false)
|
||
}
|
||
|
||
const handleExportImage = async () => {
|
||
if (!containerRef.current) return
|
||
|
||
try {
|
||
// 临时重置位置和缩放以获取完整视图
|
||
const originalPosition = { ...position }
|
||
const originalScale = scale
|
||
setPosition({ x: 0, y: 0 })
|
||
setScale(1)
|
||
|
||
// 等待 DOM 更新
|
||
await new Promise(resolve => setTimeout(resolve, 800))
|
||
|
||
// 找到实际的族谱容器(包含 TreeLayout 的 div)
|
||
const treeContainer = containerRef.current.querySelector('div[style*="transform"]') as HTMLElement
|
||
if (!treeContainer) {
|
||
throw new Error('找不到族谱容器')
|
||
}
|
||
|
||
// 查找所有连接线(使用更宽泛的选择器)
|
||
const allDivs = treeContainer.querySelectorAll('div')
|
||
const lineElements: HTMLElement[] = []
|
||
|
||
allDivs.forEach(div => {
|
||
const el = div as HTMLElement
|
||
const classes = String(el.className || '')
|
||
// 查找包含 bg-foreground 的元素(连接线)
|
||
if (classes.includes('bg-foreground')) {
|
||
lineElements.push(el)
|
||
}
|
||
})
|
||
|
||
// 克隆容器用于导出
|
||
const clonedContainer = treeContainer.cloneNode(true) as HTMLElement
|
||
|
||
// 在克隆的容器中找到对应的连接线并设置样式
|
||
const clonedDivs = clonedContainer.querySelectorAll('div')
|
||
let lineIndex = 0
|
||
clonedDivs.forEach(div => {
|
||
const el = div as HTMLElement
|
||
const classes = String(el.className || '')
|
||
if (classes.includes('bg-foreground') && lineElements[lineIndex]) {
|
||
const original = lineElements[lineIndex]
|
||
const computed = window.getComputedStyle(original)
|
||
|
||
// 直接设置内联样式
|
||
el.style.backgroundColor = computed.backgroundColor
|
||
el.style.width = computed.width
|
||
el.style.height = computed.height
|
||
el.style.position = 'absolute'
|
||
el.style.left = computed.left
|
||
el.style.top = computed.top
|
||
|
||
lineIndex++
|
||
}
|
||
})
|
||
|
||
// 移除卡片边框
|
||
clonedContainer.querySelectorAll('*').forEach((child) => {
|
||
const el = child as HTMLElement
|
||
const classes = String(el.className || '')
|
||
|
||
// 跳过连接线和连接线容器
|
||
if (classes.includes('bg-foreground') || classes.includes('pointer-events-none')) {
|
||
return
|
||
}
|
||
|
||
// 移除其他元素的边框
|
||
el.style.border = 'none'
|
||
el.style.outline = 'none'
|
||
})
|
||
|
||
// 临时添加到 DOM
|
||
clonedContainer.style.position = 'fixed'
|
||
clonedContainer.style.left = '-9999px'
|
||
clonedContainer.style.top = '0'
|
||
document.body.appendChild(clonedContainer)
|
||
|
||
// 等待渲染
|
||
await new Promise(resolve => setTimeout(resolve, 200))
|
||
|
||
// 动态导入 dom-to-image-more(仅在客户端)
|
||
const domtoimage = await import('dom-to-image-more')
|
||
|
||
// 使用 toBlob 方法
|
||
const blob = await domtoimage.default.toBlob(clonedContainer, {
|
||
quality: 0.95,
|
||
bgcolor: '#ffffff'
|
||
})
|
||
|
||
// 清理克隆的容器
|
||
document.body.removeChild(clonedContainer)
|
||
|
||
// 恢复原始位置和缩放
|
||
setPosition(originalPosition)
|
||
setScale(originalScale)
|
||
|
||
// 创建下载链接
|
||
const url = URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.download = `family-tree-${new Date().toISOString().split('T')[0]}.png`
|
||
link.href = url
|
||
link.click()
|
||
|
||
// 清理
|
||
setTimeout(() => URL.revokeObjectURL(url), 100)
|
||
} catch (error) {
|
||
console.error('导出图片失败:', error)
|
||
}
|
||
}
|
||
|
||
// Handle root not found
|
||
if (!treeData.rootId) {
|
||
return (
|
||
<div className="min-h-screen bg-background flex flex-col">
|
||
<SiteHeader />
|
||
<div className="flex-1 flex items-center justify-center text-muted-foreground">No family tree data found.</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="h-screen flex flex-col bg-muted/30 overflow-hidden">
|
||
<SiteHeader />
|
||
|
||
{/* Toolbar */}
|
||
<div className="absolute top-20 left-4 z-20 flex flex-col gap-2 bg-background/80 backdrop-blur p-2 rounded-lg border shadow-sm">
|
||
<Button variant="outline" size="icon" onClick={() => setScale((s) => Math.min(s + 0.1, 2))}>
|
||
<ZoomIn className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon" onClick={() => setScale((s) => Math.max(s - 0.1, 0.1))}>
|
||
<ZoomOut className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon" onClick={() => setPosition({ x: 0, y: 0 })}>
|
||
<Move className="h-4 w-4" />
|
||
</Button>
|
||
<div className="h-px bg-border my-1" />
|
||
<Button variant="outline" size="icon" onClick={handleExportImage} title="导出为图片">
|
||
<Download className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Tree Canvas */}
|
||
<div
|
||
ref={containerRef}
|
||
className="flex-1 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}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseLeave={handleMouseUp}
|
||
>
|
||
<div
|
||
style={{
|
||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||
transformOrigin: "top center",
|
||
transition: isDragging ? "none" : "transform 0.1s ease-out",
|
||
}}
|
||
className="absolute min-w-full min-h-full flex justify-center pt-20 pb-20"
|
||
>
|
||
<TreeLayout rootId={treeData.rootId} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|