This commit is contained in:
freedakgmail
2025-12-09 18:52:00 +08:00
parent debcce71bf
commit 09809c0e49
156 changed files with 1150 additions and 1330 deletions
+159 -25
View File
@@ -1,7 +1,8 @@
"use client"
import type React from "react"
import type { FamilyMember } from "@/types/family"
import { FamilyNode } from "./family-node"
import { AddRelationDialog } from "./add-relation-dialog"
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"
@@ -38,7 +39,66 @@ export function TreeLayout({
const [connections, setConnections] = useState<Connection[]>([])
const nodeRefs = useRef<Map<string, HTMLDivElement>>(new Map())
const [relationDialog, setRelationDialog] = useState<{ member: FamilyMember; type: "child" | "spouse" } | null>(null)
// 重新计算连线的函数(使用 ref 避免依赖问题)
const recalculateConnections = useCallback(() => {
const containerRect = containerRef.current?.getBoundingClientRect()
if (!containerRect || nodeRefs.current.size === 0) return
const newConnections: Connection[] = []
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 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
const verticalLineY = parentBottomY + 24
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])
// 节点间距配置
const HORIZONTAL_GAP = 32 // 2rem = 32px
const VERTICAL_GAP = 48 // 3rem = 48px
@@ -71,14 +131,6 @@ export function TreeLayout({
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 => {
@@ -95,6 +147,14 @@ export function TreeLayout({
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))
@@ -137,6 +197,7 @@ export function TreeLayout({
}
}, [calculateConnections, treeData])
// 预计算排序后的子节点映射,避免每次渲染时重复计算
const sortedChildrenMap = useMemo(() => {
const map = new Map<string, string[]>()
@@ -162,13 +223,57 @@ export function TreeLayout({
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: "child" | "spouse") => {
const handleCreateNewRelation = (prefillName: string, relationType: RelationType) => {
if (!relationDialog) return
const params = new URLSearchParams()
params.set("generation", relationType === "child" ? String(relationDialog.member.generation + 1) : String(relationDialog.member.generation))
params.set("prefillName", prefillName)
// 分解姓名
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) {
@@ -180,10 +285,34 @@ export function TreeLayout({
params.set("fatherId", relationDialog.member.spouseIds[0])
}
}
} else {
} 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") {
params.set("generation", String(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") {
params.set("generation", String(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)
@@ -191,22 +320,26 @@ export function TreeLayout({
window.location.href = `/members/new?${params.toString()}`
}
const TreeNode = memo(function TreeNode({ memberId, isRoot = false }: { memberId: string; isRoot?: boolean }) {
// 递归渲染树节点
function renderTreeNode(memberId: string, isRoot: boolean = false): React.ReactNode {
const member = getMember(memberId)
if (!member) return null
const hasChildren = member.childrenIds && member.childrenIds.length > 0
// 计算实际要显示的子节点(过滤掉不存在的成员)
const visibleChildren = hasChildren
? (sortedChildrenMap.get(memberId) || []).filter(childId => getMember(childId) !== undefined)
: []
const hasVisibleChildren = visibleChildren.length > 0
// 获取所有配偶
const spouses = member.spouseIds && member.spouseIds.length > 0
? member.spouseIds.map(id => getMember(id)).filter(Boolean) as FamilyMember[]
: []
// 使用预计算的排序结果
const sortedChildren = sortedChildrenMap.get(memberId) || []
return (
<div className="flex flex-col items-center">
<div key={memberId} className="flex flex-col items-center">
{/* 节点本身 */}
<div
ref={(el) => {
@@ -229,11 +362,14 @@ export function TreeLayout({
selectedMembers={selectedMembers}
canEdit={canEdit}
onAddRelation={(type) => setRelationDialog({ member, type })}
hasChildren={false}
isCollapsed={false}
onToggleCollapse={undefined}
/>
</div>
{/* 子节点 */}
{hasChildren && (
{hasVisibleChildren && (
<div
className="flex items-start relative"
style={{
@@ -241,14 +377,12 @@ export function TreeLayout({
gap: `${HORIZONTAL_GAP}px`
}}
>
{sortedChildren.map((childId) => (
<TreeNode key={childId} memberId={childId} />
))}
{visibleChildren.map((childId) => renderTreeNode(childId, false))}
</div>
)}
</div>
)
})
}
return (
<div className="relative">
@@ -284,8 +418,8 @@ export function TreeLayout({
</div>
{/* 树节点层 */}
<div className="relative" style={{ zIndex: 10 }}>
<TreeNode memberId={rootId} isRoot={true} />
<div className="relative" style={{ zIndex: 10 }} key="tree-root">
{renderTreeNode(rootId, true)}
</div>
</div>