"use client" import type React from "react" import type { FamilyMember } from "@/types/family" import { FamilyNode } from "./family-node" import { TreeNode } from "./tree-node" import { AddRelationDialog, type RelationType } from "./add-relation-dialog" import { useFamily } from "@/context/family-context" import { useEffect, useRef, useState, useCallback, useMemo, memo } from "react" import { useRouter } from "next/navigation" import { permissions } from "@/lib/permissions" interface TreeLayoutProps { rootId: string onSelectMember?: (member: FamilyMember) => void relationMode?: boolean selectedMembers?: string[] onMemberClick?: (memberId: string) => void expandAll?: boolean onExpandAllChange?: (expanded: boolean) => void scale?: number } interface Connection { from: { x: number; y: number } to: { x: number; y: number } type: 'vertical' | 'horizontal' } export function TreeLayout({ rootId, onSelectMember, relationMode = false, selectedMembers = [], onMemberClick, expandAll = false, onExpandAllChange, scale = 1 }: TreeLayoutProps) { const { getMember, treeData, highlightedMemberId, currentTree } = useFamily() // 检查用户是否有创建权限 const canEdit = permissions.canCreate(currentTree?.currentUserRole) const root = getMember(rootId) const router = useRouter() const containerRef = useRef(null) const [connections, setConnections] = useState([]) const nodeRefs = useRef>(new Map()) const [relationDialog, setRelationDialog] = useState<{ member: FamilyMember; type: "child" | "spouse" } | null>(null) // 节点间距配置 const HORIZONTAL_GAP = 16 // 1rem = 16px const VERTICAL_GAP = 48 // 3rem = 48px const CONNECTOR_HEIGHT = 48 // 连接线垂直高度 // 折叠状态管理:存储被折叠的节点ID const [collapsedNodes, setCollapsedNodes] = useState>(() => new Set()) // 切换节点折叠状态 const toggleCollapse = useCallback((memberId: string) => { setCollapsedNodes(prev => { const newSet = new Set(prev) if (newSet.has(memberId)) { newSet.delete(memberId) } else { newSet.add(memberId) } return newSet }) }, []) // 响应 expandAll 变化,展开所有节点 useEffect(() => { if (expandAll) { setCollapsedNodes(new Set()) // 通知父组件已展开 onExpandAllChange?.(false) } }, [expandAll, onExpandAllChange]) // 处理节点 ref 注册 const handleNodeRef = useCallback((memberId: string, el: HTMLDivElement | null) => { if (el) { nodeRefs.current.set(memberId, el) } else { nodeRefs.current.delete(memberId) } }, []) // 处理添加关系 const handleAddRelation = useCallback((member: FamilyMember, type: "child" | "spouse") => { setRelationDialog({ member, type }) }, []) // 处理节点点击 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 isNodeHiddenByCollapse = useCallback((nodeId: string): boolean => { const checkHidden = (id: string): boolean => { const member = getMember(id) if (!member) return true // 检查父节点是否被折叠 const parentId = member.fatherId || member.motherId if (parentId) { if (collapsedNodes.has(parentId)) return true // 递归检查祖先 return checkHidden(parentId) } return false } return checkHidden(nodeId) }, [getMember, collapsedNodes]) // 计算所有连线位置 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 // 如果该节点被折叠,不绘制到子节点的连线 if (collapsedNodes.has(nodeId)) return // 如果该节点被祖先折叠隐藏,跳过 if (isNodeHiddenByCollapse(nodeId)) return // 检查元素是否仍在 DOM 中且可见 if (!document.body.contains(nodeElement)) return const parentRect = nodeElement.getBoundingClientRect() // 如果元素不可见(宽高为0),跳过 if (parentRect.width === 0 || parentRect.height === 0) return // 考虑缩放因素:getBoundingClientRect 返回的是缩放后的尺寸 // 需要除以 scale 来获取实际的逻辑位置 const parentCenterX = (parentRect.left + parentRect.width / 2 - containerRect.left) / scale const parentBottomY = (parentRect.bottom - containerRect.top) / scale // 获取所有子节点的位置 const childPositions = member.childrenIds .map(childId => { const childElement = nodeRefs.current.get(childId) if (!childElement) return null // 检查子元素是否仍在 DOM 中 if (!document.body.contains(childElement)) return null const childRect = childElement.getBoundingClientRect() // 如果子元素不可见,跳过 if (childRect.width === 0 || childRect.height === 0) return null return { id: childId, centerX: (childRect.left + childRect.width / 2 - containerRect.left) / scale, topY: (childRect.top - containerRect.top) / scale } }) .filter(Boolean) as Array<{ id: string; centerX: number; topY: number }> if (childPositions.length === 0) return // 父节点到水平线的垂直连线 const verticalLineY = parentBottomY + CONNECTOR_HEIGHT / 2 newConnections.push({ from: { x: parentCenterX, y: parentBottomY }, to: { x: parentCenterX, y: verticalLineY }, type: 'vertical' }) // 如果有多个子节点,绘制水平连线 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, collapsedNodes, isNodeHiddenByCollapse, scale]) // 折叠状态变化时重新计算连线 useEffect(() => { // 多次延迟计算,等待 DOM 完全更新 const timers: NodeJS.Timeout[] = [] timers.push(setTimeout(calculateConnections, 50)) timers.push(setTimeout(calculateConnections, 150)) timers.push(setTimeout(calculateConnections, 300)) return () => timers.forEach(timer => clearTimeout(timer)) }, [collapsedNodes, calculateConnections]) // 缩放变化时重新计算连线 useEffect(() => { calculateConnections() }, [scale, calculateConnections]) 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]) // 预计算排序后的子节点映射,避免每次渲染时重复计算 const sortedChildrenMap = useMemo(() => { const map = new Map() Object.values(treeData.members).forEach(member => { if (member.childrenIds && member.childrenIds.length > 0) { const sorted = [...member.childrenIds].sort((aId, bId) => { const a = treeData.members[aId] const b = treeData.members[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 || "") }) map.set(member.id, sorted) } }) return map }, [treeData.members]) if (!root) return null // 姓名分解函数:将全名分解为姓和名 const parseName = (fullName: string): { surname: string; givenName: string } => { const name = fullName.trim() if (!name) return { surname: '', givenName: '' } // 常见复姓列表 const compoundSurnames = [ '欧阳', '太史', '端木', '上官', '司马', '东方', '独孤', '南宫', '万俟', '闻人', '夏侯', '诸葛', '尉迟', '公羊', '赫连', '澹台', '皇甫', '宗政', '濮阳', '公冶', '太叔', '申屠', '公孙', '慕容', '仲孙', '钟离', '长孙', '宇文', '司徒', '鲜于', '司空', '闾丘', '子车', '亓官', '司寇', '巫马', '公西', '颛孙', '壤驷', '公良', '漆雕', '乐正', '宰父', '谷梁', '拓跋', '夹谷', '轩辕', '令狐', '段干', '百里', '呼延', '东郭', '南门', '羊舌', '微生', '公户', '公玉', '公仪', '梁丘', '公仲', '公上', '公门', '公山', '公坚', '左丘', '公伯', '西门', '公祖', '第五', '公乘', '贯丘', '公皙', '南荣', '东里', '东宫', '仲长', '子书', '子桑', '即墨', '达奚', '褚师' ] // 检查是否是复姓 for (const compound of compoundSurnames) { if (name.startsWith(compound) && name.length > compound.length) { return { surname: compound, givenName: name.slice(compound.length) } } } // 单姓处理 if (name.length >= 2) { return { surname: name.charAt(0), givenName: name.slice(1) } } // 只有一个字,作为姓 return { surname: name, givenName: '' } } // 递归渲染树节点 - 使用 memo 优化 const handleCreateNewRelation = (prefillName: string, relationType: RelationType) => { if (!relationDialog) return const params = new URLSearchParams() // 分解姓名 const { surname, givenName } = parseName(prefillName) if (surname) params.set("surname", surname) if (givenName) params.set("givenName", givenName) if (relationType === "child") { params.set("generation", String(relationDialog.member.generation + 1)) if (relationDialog.member.gender === "MALE") { params.set("fatherId", relationDialog.member.id) if (relationDialog.member.spouseIds?.length) { params.set("motherId", relationDialog.member.spouseIds[0]) } } else if (relationDialog.member.gender === "FEMALE") { params.set("motherId", relationDialog.member.id) if (relationDialog.member.spouseIds?.length) { params.set("fatherId", relationDialog.member.spouseIds[0]) } } } else if (relationType === "spouse") { params.set("generation", String(relationDialog.member.generation)) params.set("spouseId", relationDialog.member.id) params.set("gender", relationDialog.member.gender === "MALE" ? "FEMALE" : "MALE") // 如果当前成员有子女,将他们也作为新配偶的子女 if (relationDialog.member.childrenIds && relationDialog.member.childrenIds.length > 0) { relationDialog.member.childrenIds.forEach(id => { params.append('childrenIds', id) }) } } else if (relationType === "father") { // 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代) params.set("generation", String(relationDialog.member.generation === 1 ? 1 : relationDialog.member.generation - 1)) params.set("childId", relationDialog.member.id) params.set("gender", "MALE") // 如果已有母亲,设为配偶 if (relationDialog.member.motherId) { params.set("spouseId", relationDialog.member.motherId) } } else if (relationType === "mother") { // 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代) params.set("generation", String(relationDialog.member.generation === 1 ? 1 : relationDialog.member.generation - 1)) params.set("childId", relationDialog.member.id) params.set("gender", "FEMALE") // 如果已有父亲,设为配偶 if (relationDialog.member.fatherId) { params.set("spouseId", relationDialog.member.fatherId) } } const treeParams = new URLSearchParams(window.location.search) treeParams.forEach((value, key) => { params.set(key, value) }) window.location.href = `/members/new?${params.toString()}` } // 准备根节点数据 const rootSpouses = root.spouseIds && root.spouseIds.length > 0 ? root.spouseIds.map(id => getMember(id)).filter((s): s is FamilyMember => s !== undefined) : [] const rootHasChildren = root.childrenIds && root.childrenIds.length > 0 const rootChildren = rootHasChildren ? (sortedChildrenMap.get(root.id) || []) .map(id => getMember(id)) .filter((c): c is FamilyMember => c !== undefined) : [] return (
{/* 连线层 - 使用 div 绘制 */}
{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 (
) })}
{/* 树节点层 */}
{relationDialog && ( { if (!open) setRelationDialog(null) }} member={relationDialog.member} relationType={relationDialog.type} onCreateNew={handleCreateNewRelation} /> )}
) }