0.6.0.1
This commit is contained in:
+159
-25
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user