Files
chinese-family-tree-2/app/tree/page.tsx
T
freedakgmail e9b1641509 0.6.0.2
2025-12-10 10:19:29 +08:00

796 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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, Loader2, Maximize2, FileText } 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"
import { exportFamilyHTML } from "@/lib/export-pdf"
// 动态导入 D3 图表组件(减少初始加载体积)
const D3OrgChartFlow = dynamic(
() => import("@/components/tree/d3-org-chart-flow").then(mod => ({ default: mod.D3OrgChartFlow })),
{
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="ml-2 text-muted-foreground">...</span>
</div>
)
}
)
// 导入类型
import type { D3OrgChartRef } from "@/components/tree/d3-org-chart-flow"
type ViewMode = "traditional" | "d3-org-chart"
export default function TreePage() {
const { treeData, currentTree, refreshData } = useFamily()
const { calculateRelationship } = useRelationship()
const { data: session, status } = useSession()
const router = useRouter()
const searchParams = useSearchParams()
const [viewMode, setViewMode] = useState<ViewMode>("traditional")
// 关系查询模式
const [relationMode, setRelationMode] = useState(false)
const [selectedMembers, setSelectedMembers] = useState<string[]>([])
const [relationResult, setRelationResult] = useState<any>(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])
// 页面加载时刷新数据
useEffect(() => {
if (currentTree?.id) {
refreshData()
}
}, [currentTree?.id, refreshData])
// 从 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 [expandAll, setExpandAll] = useState(false)
const [hasInitialPositioned, setHasInitialPositioned] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const d3ChartRef = useRef<D3OrgChartRef>(null)
// 传统族谱初始定位到始祖并自适应缩放
useEffect(() => {
if (viewMode !== 'traditional' || hasInitialPositioned || !treeData.rootId) return
// 找到始祖成员:优先使用标记为 isFounder 的成员,否则使用 rootId 对应的成员
const founderMember = Object.values(treeData.members).find(m => m.isFounder === true)
|| treeData.members[treeData.rootId]
if (!founderMember) return
// 延迟执行,等待 DOM 渲染完成
const timer = setTimeout(() => {
if (!containerRef.current) return
// 查找树内容容器(包含 TreeLayout 的 div
const treeContent = containerRef.current.querySelector('div[style*="transform"]') as HTMLElement
if (!treeContent) return
// 查找始祖节点的 DOM 元素
const founderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement
if (!founderNode) return
const containerRect = containerRef.current.getBoundingClientRect()
const treeRect = treeContent.getBoundingClientRect()
// 计算树的实际尺寸(当前缩放为1)
const treeWidth = treeRect.width
const treeHeight = treeRect.height
// 计算适合容器的缩放比例,留出一些边距
const padding = 40
const scaleX = (containerRect.width - padding * 2) / treeWidth
const scaleY = (containerRect.height - padding * 2) / treeHeight
const fitScale = Math.min(scaleX, scaleY, 1) // 最大不超过1
const newScale = Math.max(fitScale, 0.2) // 最小0.2
// 先设置缩放
setScale(newScale)
// 延迟计算位置(等待缩放生效)
setTimeout(() => {
if (!containerRef.current) return
const updatedFounderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement
if (!updatedFounderNode) return
const updatedContainerRect = containerRef.current.getBoundingClientRect()
const updatedNodeRect = updatedFounderNode.getBoundingClientRect()
// 计算始祖节点相对于容器中心的偏移
const containerCenterX = updatedContainerRect.width / 2
const nodeCenterX = updatedNodeRect.left - updatedContainerRect.left + updatedNodeRect.width / 2
// 计算需要的 x 偏移量使始祖居中
const offsetX = containerCenterX - nodeCenterX
setPosition({ x: offsetX, y: 0 })
setHasInitialPositioned(true)
}, 100)
}, 500)
return () => clearTimeout(timer)
}, [viewMode, treeData.rootId, treeData.members, hasInitialPositioned])
// 切换视图模式时重置初始定位状态和缩放位置
useEffect(() => {
setHasInitialPositioned(false)
setScale(1)
setPosition({ x: 0, y: 0 })
}, [viewMode])
// 传统族谱自适应视图函数
const handleTraditionalFitView = useCallback(() => {
if (!containerRef.current || !treeData.rootId) return
// 找到始祖成员
const founderMember = Object.values(treeData.members).find(m => m.isFounder === true)
|| treeData.members[treeData.rootId]
if (!founderMember) return
// 查找树内容容器
const treeContent = containerRef.current.querySelector('div[style*="transform"]') as HTMLElement
if (!treeContent) return
// 查找始祖节点
const founderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement
if (!founderNode) return
const containerRect = containerRef.current.getBoundingClientRect()
const treeRect = treeContent.getBoundingClientRect()
// 计算树的实际尺寸(需要除以当前缩放)
const treeWidth = treeRect.width / scale
const treeHeight = treeRect.height / scale
// 计算适合容器的缩放比例
const padding = 40
const scaleX = (containerRect.width - padding * 2) / treeWidth
const scaleY = (containerRect.height - padding * 2) / treeHeight
const fitScale = Math.min(scaleX, scaleY, 1)
const newScale = Math.max(fitScale, 0.2)
// 先重置位置到0,再设置缩放
setPosition({ x: 0, y: 0 })
setScale(newScale)
// 延迟计算位置(等待缩放和位置重置生效)
setTimeout(() => {
if (!containerRef.current) return
const updatedFounderNode = containerRef.current.querySelector(`[data-member-id="${founderMember.id}"]`) as HTMLElement
if (!updatedFounderNode) return
const updatedContainerRect = containerRef.current.getBoundingClientRect()
const updatedNodeRect = updatedFounderNode.getBoundingClientRect()
// 计算始祖节点居中的偏移
// 容器中心点
const containerCenterX = updatedContainerRect.width / 2
// 始祖节点中心点相对于容器的位置
const nodeCenterX = updatedNodeRect.left - updatedContainerRect.left + updatedNodeRect.width / 2
// 需要的偏移量
const offsetX = containerCenterX - nodeCenterX
setPosition({ x: offsetX, y: 0 })
}, 150)
}, [treeData.rootId, treeData.members, scale])
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, current selected:', selectedMembersRef.current)
// 直接操作 DOM 来显示选中标记
// D3 图表的节点在 SVG 的 foreignObject 中,需要特殊查询
// 注意:配偶没有独立的节点,需要在配偶区域显示选中标记
const updateSelectionMark = (memberId: string, selected: boolean) => {
// 首先尝试查找主成员节点
let nodeElement = document.querySelector(`[data-member-id="${memberId}"]`) as HTMLElement
let isSpouse = false
// 如果找不到,可能是配偶,尝试查找配偶区域
if (!nodeElement) {
const spouseArea = document.querySelector(`[data-spouse-id="${memberId}"]`) as HTMLElement
if (spouseArea) {
nodeElement = spouseArea
isSpouse = true
}
}
if (!nodeElement) {
console.log('Node element not found for:', memberId)
return
}
console.log('Updating selection mark for:', memberId, 'isSpouse:', isSpouse, 'selected:', selected)
if (selected) {
// 只添加打钩标记,不改变边框和背景
if (!nodeElement.querySelector('.selection-mark')) {
const checkMark = document.createElement('div')
checkMark.className = 'selection-mark'
if (isSpouse) {
// 配偶区域打钩位置(右边)
checkMark.style.cssText = 'position: absolute; top: -6px; right: -6px; z-index: 20;'
nodeElement.style.position = 'relative'
} else {
// 主成员打钩位置(左边)
checkMark.style.cssText = 'position: absolute; top: 6px; left: 6px; z-index: 20;'
}
checkMark.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#2563eb" stroke="white" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="9 12 11 14 15 10" fill="none" stroke="white" stroke-width="2.5"/></svg>`
nodeElement.appendChild(checkMark)
}
} else {
// 移除选中标记
const mark = nodeElement.querySelector('.selection-mark')
if (mark) mark.remove()
}
}
// 直接处理选择逻辑
if (selectedMembersRef.current.length === 0) {
// 选择第一个成员
updateSelectionMark(id, true)
setSelectedMembers([id])
} else if (selectedMembersRef.current.length === 1) {
const [firstId] = selectedMembersRef.current
if (firstId === id) {
// 点击同一个成员,取消选择
updateSelectionMark(id, false)
setSelectedMembers([])
return
}
// 选择第二个成员,显示选中标记
updateSelectionMark(id, true)
setSelectedMembers([firstId, id])
const result = calculateRelationship(firstId, id)
const firstMember = treeData.members[firstId]
const secondMember = treeData.members[id]
setRelationResult({
from: firstMember,
to: secondMember,
relationship: result
})
setShowRelationDialog(true)
// 延迟清除选中状态
setTimeout(() => {
// 清除所有选中标记
document.querySelectorAll('.selection-mark').forEach(mark => mark.remove())
setSelectedMembers([])
setRelationMode(false)
}, 300)
}
} 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)
}
}
// 导出 PDF(按世代导出成员信息)
const handleExportPDF = useCallback(async () => {
if (!treeData.members || Object.keys(treeData.members).length === 0) {
alert('没有成员数据可导出')
return
}
try {
const members = Object.values(treeData.members)
const getMemberName = (id: string) => treeData.members[id]?.fullName || ''
const treeName = currentTree?.name || '家族'
// 获取当前页面的基础 URL
const baseUrl = window.location.origin
await exportFamilyHTML(members, getMemberName, treeName, baseUrl)
} catch (error) {
console.error('导出PDF失败:', error)
alert('导出失败,请重试')
}
}, [treeData.members, currentTree?.name])
// 未登录时不渲染内容(已在 useEffect 中重定向)
if (status === 'loading' || !session) {
return null
}
// 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">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 text-primary/80">
<Network className="h-16 w-16" strokeWidth={1} />
</div>
<p className="text-base text-foreground font-light tracking-wide"></p>
</div>
</div>
</div>
)
}
return (
<div className="h-screen flex flex-col bg-muted/30 overflow-hidden">
<SiteHeader />
{/* 视图切换和关系查询按钮 */}
<div className="absolute top-20 right-4 z-30 flex gap-2">
{/* 关系查询按钮 */}
<Button
variant={relationMode ? "default" : "outline"}
className="gap-2 shadow-lg"
onClick={toggleRelationMode}
>
<Users className="h-4 w-4" />
{relationMode ? (
selectedMembers.length === 0 ? '选择第一个成员' : '选择第二个成员'
) : '关系查询'}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2 shadow-lg">
<LayoutGrid className="h-4 w-4" />
{viewMode === 'traditional' ? '传统族谱' : 'D3族谱'}
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={() => {
const params = new URLSearchParams(searchParams.toString())
params.set('view', 'traditional')
router.push(`/tree?${params.toString()}`)
}}
className={viewMode === 'traditional' ? 'bg-accent' : ''}
>
<div className="flex items-center justify-between w-full">
<span></span>
{viewMode === 'traditional' && <span className="text-primary"></span>}
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const params = new URLSearchParams(searchParams.toString())
params.set('view', 'd3-org-chart')
router.push(`/tree?${params.toString()}`)
}}
className={viewMode === 'd3-org-chart' ? 'bg-accent' : ''}
>
<div className="flex items-center justify-between w-full">
<span>D3族谱</span>
{viewMode === 'd3-org-chart' && <span className="text-primary"></span>}
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{viewMode === 'traditional' ? (
<div className="flex-1 relative min-h-0">
{/* Toolbar */}
<div className="absolute top-4 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={handleTraditionalFitView} title="自适应视图">
<Move className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => setExpandAll(true)} title="完全展开">
<Maximize2 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>
<Button variant="outline" size="icon" onClick={handleExportPDF} title="导出族谱PDF">
<FileText className="h-4 w-4" />
</Button>
</div>
{/* Tree Canvas */}
<div
ref={containerRef}
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}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
>
<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}
relationMode={relationMode}
selectedMembers={selectedMembers}
onMemberClick={handleMemberClick}
expandAll={expandAll}
onExpandAllChange={setExpandAll}
scale={scale}
/>
</div>
</div>
</div>
) : (
<div className="flex-1 relative min-h-0">
{/* 统一的控制按钮 */}
<div className="absolute top-4 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={() => d3ChartRef.current?.expandAll()} title="完全展开">
<Maximize2 className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.collapseAll()} title="折叠全部">
<ZoomOut className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.fitView()} title="适应视图">
<Move className="h-4 w-4" />
</Button>
<div className="h-px bg-border my-1" />
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.exportPNG()} title="导出PNG">
<Download className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleExportPDF} title="导出族谱PDF">
<FileText className="h-4 w-4" />
</Button>
</div>
<div className="w-full h-full">
<D3OrgChartFlow
ref={d3ChartRef}
members={treeData.members}
rootId={treeData.rootId}
relationMode={relationMode}
selectedMembers={selectedMembers}
onMemberClick={handleD3MemberClick}
/>
</div>
</div>
)}
{/* 关系查询结果对话框 */}
<Dialog open={showRelationDialog} onOpenChange={handleDialogClose}>
<DialogContent className="max-w-2xl" aria-describedby="relation-description">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{relationResult && (
<div className="space-y-4">
{/* 成员信息 */}
<div className="flex items-center justify-center gap-4 p-6 bg-muted rounded-lg">
<div className="text-center">
<div className="text-2xl font-bold font-serif mb-1">
<MemberNameWithStatus
name={relationResult.from.fullName}
isDead={!!relationResult.from.deathDate}
/>
</div>
<div className="text-sm text-muted-foreground">
{relationResult.from.generation}
</div>
</div>
<div className="flex flex-col items-center gap-2">
<Badge variant="default" className="text-lg px-4 py-2">
{relationResult.relationship.term}
</Badge>
</div>
<div className="text-center">
<div className="text-2xl font-bold font-serif mb-1">
<MemberNameWithStatus
name={relationResult.to.fullName}
isDead={!!relationResult.to.deathDate}
/>
</div>
<div className="text-sm text-muted-foreground">
{relationResult.to.generation}
</div>
</div>
</div>
{/* 关系说明 */}
<div id="relation-description" className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div className="text-sm font-medium mb-1"></div>
<div className="text-muted-foreground">
<strong><MemberNameWithStatus name={relationResult.from.fullName} isDead={!!relationResult.from.deathDate} /></strong> <strong><MemberNameWithStatus name={relationResult.to.fullName} isDead={!!relationResult.to.deathDate} /></strong> <strong className="text-primary">{relationResult.relationship.term}</strong>
</div>
</div>
{/* 关系路径 */}
{relationResult.relationship.path && (
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm font-medium mb-2"></div>
<RelationshipPathDisplay
path={relationResult.relationship.path}
className="text-xs"
/>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}