"use client" import type React from "react" import dynamic from "next/dynamic" import { SiteHeader } from "@/components/site-header" import { TreeLayout } from "@/components/tree/tree-layout" import { useFamily } from "@/context/family-context" import { useRelationship } from "@/hooks/use-relationship" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { ZoomIn, ZoomOut, Move, Download, LayoutGrid, ChevronDown, Users, Network, User, Loader2 } from "lucide-react" import { useState, useRef, useEffect, useCallback } from "react" import { useRouter, useSearchParams } from "next/navigation" import { MemberNameWithStatus } from "@/components/member-name-with-status" import { RelationshipPathDisplay } from "@/components/relationship-path-display" import { useSession } from "next-auth/react" import { EmptyStateBadge } from "@/components/empty-state-badge" // 动态导入 D3 图表组件(减少初始加载体积) const D3OrgChartFlow = dynamic( () => import("@/components/tree/d3-org-chart-flow").then(mod => ({ default: mod.D3OrgChartFlow })), { ssr: false, loading: () => (
加载图表组件...
) } ) // 导入类型 import type { D3OrgChartRef } from "@/components/tree/d3-org-chart-flow" type ViewMode = "traditional" | "d3-org-chart" export default function TreePage() { const { treeData } = useFamily() const { calculateRelationship } = useRelationship() const { data: session, status } = useSession() const router = useRouter() const searchParams = useSearchParams() const [viewMode, setViewMode] = useState("traditional") // 关系查询模式 const [relationMode, setRelationMode] = useState(false) const [selectedMembers, setSelectedMembers] = useState([]) const [relationResult, setRelationResult] = useState(null) const [showRelationDialog, setShowRelationDialog] = useState(false) // 使用ref存储最新的relationMode值,避免回调重新创建 const relationModeRef = useRef(relationMode) const selectedMembersRef = useRef(selectedMembers) useEffect(() => { relationModeRef.current = relationMode selectedMembersRef.current = selectedMembers }, [relationMode, selectedMembers]) // 未登录时重定向到登录页 useEffect(() => { if (status === 'unauthenticated') { router.push('/auth/signin') } }, [status, router]) // 从 URL 参数读取视图模式 useEffect(() => { const view = searchParams.get('view') if (view === 'd3-org-chart' || view === 'traditional') { setViewMode(view) } }, [searchParams]) 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(null) const d3ChartRef = useRef(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 handleWheel = (e: React.WheelEvent) => { e.preventDefault() const delta = e.deltaY > 0 ? -0.1 : 0.1 setScale((s) => Math.min(Math.max(s + delta, 0.1), 2)) } // 处理成员选择(关系查询模式) const handleMemberClick = (memberId: string) => { if (selectedMembers.length === 0) { // 选择第一个成员 setSelectedMembers([memberId]) } else if (selectedMembers.length === 1) { // 选择第二个成员,计算关系 const [firstId] = selectedMembers if (firstId === memberId) { // 点击同一个人,取消选择 setSelectedMembers([]) return } const result = calculateRelationship(firstId, memberId) const firstMember = treeData.members[firstId] const secondMember = treeData.members[memberId] setRelationResult({ from: firstMember, to: secondMember, relationship: result }) setShowRelationDialog(true) setSelectedMembers([]) setRelationMode(false) } } // 切换关系查询模式 const toggleRelationMode = () => { setRelationMode(!relationMode) setSelectedMembers([]) setRelationResult(null) } // 处理对话框关闭 const handleDialogClose = (open: boolean) => { setShowRelationDialog(open) if (!open && relationResult) { // 清除选中样式 const clearNodeStyle = (nodeId: string) => { const chartContainer = document.querySelector('.w-full.h-full.bg-background.rounded-lg.border') const svg = chartContainer?.querySelector('svg') if (!svg) return const foreignObjects = svg.querySelectorAll('foreignObject') foreignObjects.forEach((fo: any) => { const nodeData = (fo.parentNode as any)?.__data__?.data if (nodeData?.id === nodeId) { const nameDiv = fo.querySelector('div[style*="font-size: 13px"]') as HTMLElement if (nameDiv) { // 移除勾选图标 const checkIcon = nameDiv.querySelector('svg') if (checkIcon) { checkIcon.remove() } } } }) } clearNodeStyle(relationResult.from.id) clearNodeStyle(relationResult.to.id) } } // 使用useCallback创建稳定的回调,通过ref访问最新的relationMode const handleD3MemberClick = useCallback((id: string) => { console.log('D3 onMemberClick triggered:', id, 'relationMode:', relationModeRef.current) if (relationModeRef.current) { console.log('Calling handleMemberClick') // 更新节点样式的辅助函数 const updateNodeStyle = (nodeId: string, isSelected: boolean) => { // 从DOM中查找D3图表容器 const chartContainer = document.querySelector('.w-full.h-full.bg-background.rounded-lg.border') const svg = chartContainer?.querySelector('svg') if (!svg) return const foreignObjects = svg.querySelectorAll('foreignObject') foreignObjects.forEach((fo: any) => { const nodeData = (fo.parentNode as any)?.__data__?.data if (nodeData?.id === nodeId) { const nameDiv = fo.querySelector('div[style*="font-size: 13px"]') as HTMLElement if (nameDiv) { if (isSelected) { // 添加勾选图标 if (!nameDiv.querySelector('svg')) { const iconHTML = `` nameDiv.style.display = 'flex' nameDiv.style.alignItems = 'center' nameDiv.insertAdjacentHTML('beforeend', iconHTML) } } else { // 移除勾选图标 const checkIcon = nameDiv.querySelector('svg') if (checkIcon) { checkIcon.remove() } } } } }) } // 直接处理选择逻辑 if (selectedMembersRef.current.length === 0) { updateNodeStyle(id, true) setSelectedMembers([id]) } else if (selectedMembersRef.current.length === 1) { const [firstId] = selectedMembersRef.current if (firstId === id) { updateNodeStyle(id, false) setSelectedMembers([]) return } updateNodeStyle(id, true) const result = calculateRelationship(firstId, id) const firstMember = treeData.members[firstId] const secondMember = treeData.members[id] setRelationResult({ from: firstMember, to: secondMember, relationship: result }) setShowRelationDialog(true) setSelectedMembers([]) setRelationMode(false) // 选中样式会在对话框关闭时清除 } } else { console.log('Navigating to member detail') const params = new URLSearchParams(window.location.search) router.push(`/members/${id}?${params.toString()}`) } }, [router, calculateRelationship, treeData.members]) 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) } } // 未登录时不渲染内容(已在 useEffect 中重定向) if (status === 'loading' || !session) { return null } // Handle root not found if (!treeData.rootId) { return (

谱系待续

) } return (
{/* 视图切换和关系查询按钮 */}
{/* 关系查询按钮 */} { const params = new URLSearchParams(searchParams.toString()) params.set('view', 'traditional') router.push(`/tree?${params.toString()}`) }} className={viewMode === 'traditional' ? 'bg-accent' : ''} >
传统家谱视图 {viewMode === 'traditional' && }
{ const params = new URLSearchParams(searchParams.toString()) params.set('view', 'd3-org-chart') router.push(`/tree?${params.toString()}`) }} className={viewMode === 'd3-org-chart' ? 'bg-accent' : ''} >
D3 家谱图 {viewMode === 'd3-org-chart' && }
{viewMode === 'traditional' ? (
{/* Toolbar */}
{/* Tree Canvas */}
) : (
{/* 统一的控制按钮 */}
)} {/* 关系查询结果对话框 */} 关系查询结果 {relationResult && (
{/* 成员信息 */}
第{relationResult.from.generation}世
{relationResult.relationship.term}
第{relationResult.to.generation}世
{/* 关系说明 */}
关系说明:
{relationResult.relationship.term}
{/* 关系路径 */} {relationResult.relationship.path && (
关系路径:
)}
)}
) }