249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
"use client"
|
|
import type { FamilyMember } from "@/types/family"
|
|
import { FamilyNode } from "./family-node"
|
|
import { useFamily } from "@/context/family-context"
|
|
import { useEffect, useRef, useState, useCallback } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
|
|
interface TreeLayoutProps {
|
|
rootId: string
|
|
onSelectMember?: (member: FamilyMember) => void
|
|
relationMode?: boolean
|
|
selectedMembers?: string[]
|
|
onMemberClick?: (memberId: string) => void
|
|
}
|
|
|
|
interface Connection {
|
|
from: { x: number; y: number }
|
|
to: { x: number; y: number }
|
|
type: 'vertical' | 'horizontal'
|
|
}
|
|
|
|
export function TreeLayout({
|
|
rootId,
|
|
onSelectMember,
|
|
relationMode = false,
|
|
selectedMembers = [],
|
|
onMemberClick
|
|
}: TreeLayoutProps) {
|
|
const { getMember, treeData, highlightedMemberId } = useFamily()
|
|
const root = getMember(rootId)
|
|
const router = useRouter()
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const [connections, setConnections] = useState<Connection[]>([])
|
|
const nodeRefs = useRef<Map<string, HTMLDivElement>>(new Map())
|
|
|
|
// 节点间距配置
|
|
const HORIZONTAL_GAP = 32 // 2rem = 32px
|
|
const VERTICAL_GAP = 48 // 3rem = 48px
|
|
const CONNECTOR_HEIGHT = 48 // 连接线垂直高度
|
|
|
|
// 处理节点点击
|
|
const handleNodeClick = useCallback((member: FamilyMember) => {
|
|
if (relationMode && onMemberClick) {
|
|
onMemberClick(member.id)
|
|
} else {
|
|
const params = new URLSearchParams(window.location.search)
|
|
router.push(`/members/${member.id}?${params.toString()}`)
|
|
}
|
|
}, [relationMode, onMemberClick, router])
|
|
|
|
// 计算所有连线位置
|
|
const calculateConnections = useCallback(() => {
|
|
const newConnections: Connection[] = []
|
|
const containerRect = containerRef.current?.getBoundingClientRect()
|
|
if (!containerRect) return
|
|
|
|
// 检查是否有足够的节点已注册
|
|
if (nodeRefs.current.size === 0) return
|
|
|
|
nodeRefs.current.forEach((nodeElement, nodeId) => {
|
|
const member = getMember(nodeId)
|
|
if (!member || !member.childrenIds || member.childrenIds.length === 0) return
|
|
|
|
const parentRect = nodeElement.getBoundingClientRect()
|
|
const parentCenterX = parentRect.left + parentRect.width / 2 - containerRect.left
|
|
const parentBottomY = parentRect.bottom - containerRect.top
|
|
|
|
// 父节点到水平线的垂直连线
|
|
const verticalLineY = parentBottomY + CONNECTOR_HEIGHT / 2
|
|
newConnections.push({
|
|
from: { x: parentCenterX, y: parentBottomY },
|
|
to: { x: parentCenterX, y: verticalLineY },
|
|
type: 'vertical'
|
|
})
|
|
|
|
// 获取所有子节点的位置
|
|
const childPositions = member.childrenIds
|
|
.map(childId => {
|
|
const childElement = nodeRefs.current.get(childId)
|
|
if (!childElement) return null
|
|
const childRect = childElement.getBoundingClientRect()
|
|
return {
|
|
id: childId,
|
|
centerX: childRect.left + childRect.width / 2 - containerRect.left,
|
|
topY: childRect.top - containerRect.top
|
|
}
|
|
})
|
|
.filter(Boolean) as Array<{ id: string; centerX: number; topY: number }>
|
|
|
|
if (childPositions.length === 0) return
|
|
|
|
// 如果有多个子节点,绘制水平连线
|
|
if (childPositions.length > 1) {
|
|
const leftmostX = Math.min(...childPositions.map(p => p.centerX))
|
|
const rightmostX = Math.max(...childPositions.map(p => p.centerX))
|
|
|
|
newConnections.push({
|
|
from: { x: leftmostX, y: verticalLineY },
|
|
to: { x: rightmostX, y: verticalLineY },
|
|
type: 'horizontal'
|
|
})
|
|
}
|
|
|
|
// 每个子节点的垂直连线
|
|
childPositions.forEach(child => {
|
|
newConnections.push({
|
|
from: { x: child.centerX, y: verticalLineY },
|
|
to: { x: child.centerX, y: child.topY },
|
|
type: 'vertical'
|
|
})
|
|
})
|
|
})
|
|
|
|
setConnections(newConnections)
|
|
}, [getMember])
|
|
|
|
useEffect(() => {
|
|
// 多次尝试计算,确保 DOM 完全渲染
|
|
const timers: NodeJS.Timeout[] = []
|
|
timers.push(setTimeout(calculateConnections, 50))
|
|
timers.push(setTimeout(calculateConnections, 150))
|
|
timers.push(setTimeout(calculateConnections, 300))
|
|
timers.push(setTimeout(calculateConnections, 500))
|
|
|
|
// 监听窗口大小变化
|
|
window.addEventListener('resize', calculateConnections)
|
|
|
|
return () => {
|
|
timers.forEach(timer => clearTimeout(timer))
|
|
window.removeEventListener('resize', calculateConnections)
|
|
}
|
|
}, [calculateConnections, treeData])
|
|
|
|
if (!root) return null
|
|
|
|
// 递归渲染树节点
|
|
const TreeNode = ({ memberId, isRoot = false }: { memberId: string; isRoot?: boolean }) => {
|
|
const member = getMember(memberId)
|
|
if (!member) return null
|
|
|
|
const hasChildren = member.childrenIds && member.childrenIds.length > 0
|
|
|
|
// 获取所有配偶
|
|
const spouses = member.spouseIds && member.spouseIds.length > 0
|
|
? member.spouseIds.map(id => getMember(id)).filter(Boolean) as FamilyMember[]
|
|
: []
|
|
|
|
return (
|
|
<div className="flex flex-col items-center">
|
|
{/* 节点本身 */}
|
|
<div
|
|
ref={(el) => {
|
|
if (el) {
|
|
nodeRefs.current.set(memberId, el)
|
|
} else {
|
|
nodeRefs.current.delete(memberId)
|
|
}
|
|
}}
|
|
className="relative z-10"
|
|
>
|
|
<FamilyNode
|
|
member={member}
|
|
spouses={spouses}
|
|
onSelect={handleNodeClick}
|
|
isRoot={isRoot}
|
|
isHighlighted={member.id === highlightedMemberId}
|
|
relationMode={relationMode}
|
|
isSelected={selectedMembers.includes(member.id)}
|
|
/>
|
|
</div>
|
|
|
|
{/* 子节点 */}
|
|
{hasChildren && (
|
|
<div
|
|
className="flex items-start relative"
|
|
style={{
|
|
marginTop: `${VERTICAL_GAP}px`,
|
|
gap: `${HORIZONTAL_GAP}px`
|
|
}}
|
|
>
|
|
{(() => {
|
|
// 对子节点进行排序:始祖排最左边,其他按出生日期排序
|
|
const sortedChildren = [...member.childrenIds].sort((aId, bId) => {
|
|
const a = getMember(aId)
|
|
const b = getMember(bId)
|
|
if (!a || !b) return 0
|
|
|
|
// 始祖永远排最左边
|
|
const aIsFounder = a.tags?.includes('始祖') || a.generation === 1
|
|
const bIsFounder = b.tags?.includes('始祖') || b.generation === 1
|
|
if (aIsFounder && !bIsFounder) return -1
|
|
if (!aIsFounder && bIsFounder) return 1
|
|
|
|
// 其他按出生日期排序
|
|
return (a.birthDate || "").localeCompare(b.birthDate || "")
|
|
})
|
|
|
|
return sortedChildren.map((childId) => (
|
|
<TreeNode key={childId} memberId={childId} />
|
|
))
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="relative">
|
|
<div ref={containerRef} className="p-12 min-w-fit relative" style={{ minHeight: '100%' }}>
|
|
{/* 连线层 - 使用 div 绘制 */}
|
|
<div className="absolute inset-0 pointer-events-none" style={{ zIndex: 1 }}>
|
|
{connections.map((conn, index) => {
|
|
const isHorizontal = conn.type === 'horizontal'
|
|
// 使用 1px 但通过 transform scale 缩小来实现细线
|
|
const width = isHorizontal ? Math.abs(conn.to.x - conn.from.x) : 1
|
|
const height = isHorizontal ? 1 : Math.abs(conn.to.y - conn.from.y)
|
|
const left = Math.min(conn.from.x, conn.to.x)
|
|
const top = Math.min(conn.from.y, conn.to.y)
|
|
|
|
// 跳过无效的连线(宽度或高度为0)
|
|
if (width <= 0 || height <= 0) return null
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="absolute bg-black"
|
|
style={{
|
|
left: `${left}px`,
|
|
top: `${top}px`,
|
|
width: `${width}px`,
|
|
height: `${height}px`,
|
|
transform: isHorizontal ? 'scaleY(0.5)' : 'scaleX(0.5)',
|
|
transformOrigin: 'top left',
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* 树节点层 */}
|
|
<div className="relative" style={{ zIndex: 10 }}>
|
|
<TreeNode memberId={rootId} isRoot={true} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|