0.6.0.1
This commit is contained in:
@@ -11,12 +11,14 @@ import { useFamily } from "@/context/family-context"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
|
||||
export type RelationType = "child" | "spouse" | "father" | "mother"
|
||||
|
||||
interface AddRelationDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
member: FamilyMember
|
||||
relationType: "child" | "spouse"
|
||||
onCreateNew: (prefillName: string, relationType: "child" | "spouse") => void
|
||||
relationType: RelationType
|
||||
onCreateNew: (prefillName: string, relationType: RelationType) => void
|
||||
}
|
||||
|
||||
interface CandidateMeta {
|
||||
@@ -73,11 +75,21 @@ export function AddRelationDialog({
|
||||
// 非男女成员,允许填补任一缺失的父母
|
||||
if (candidate.fatherId && candidate.motherId) return false
|
||||
}
|
||||
} else {
|
||||
} else if (relationType === "spouse") {
|
||||
if (candidate.generation !== member.generation) return false
|
||||
if (member.spouseIds?.includes(candidate.id)) return false
|
||||
if (candidate.spouseIds?.includes(member.id)) return false
|
||||
if (candidate.gender === member.gender) return false
|
||||
} else if (relationType === "father") {
|
||||
// 父亲必须是男性,世代比当前成员小1
|
||||
if (candidate.gender !== "MALE") return false
|
||||
if (candidate.generation !== (member.generation - 1)) return false
|
||||
if (member.fatherId === candidate.id) return false
|
||||
} else if (relationType === "mother") {
|
||||
// 母亲必须是女性,世代比当前成员小1
|
||||
if (candidate.gender !== "FEMALE") return false
|
||||
if (candidate.generation !== (member.generation - 1)) return false
|
||||
if (member.motherId === candidate.id) return false
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -113,7 +125,7 @@ export function AddRelationDialog({
|
||||
title: "关联成功",
|
||||
description: `已将 ${target.fullName} 标记为 ${member.fullName} 的子女。`
|
||||
})
|
||||
} else {
|
||||
} else if (relationType === "spouse") {
|
||||
const existing = new Set(member.spouseIds || [])
|
||||
existing.add(target.id)
|
||||
await updateMember(member.id, { spouseIds: Array.from(existing) })
|
||||
@@ -121,6 +133,18 @@ export function AddRelationDialog({
|
||||
title: "关联成功",
|
||||
description: `${member.fullName} 与 ${target.fullName} 已建立配偶关系。`
|
||||
})
|
||||
} else if (relationType === "father") {
|
||||
await updateMember(member.id, { fatherId: target.id })
|
||||
toast({
|
||||
title: "关联成功",
|
||||
description: `已将 ${target.fullName} 设为 ${member.fullName} 的父亲。`
|
||||
})
|
||||
} else if (relationType === "mother") {
|
||||
await updateMember(member.id, { motherId: target.id })
|
||||
toast({
|
||||
title: "关联成功",
|
||||
description: `已将 ${target.fullName} 设为 ${member.fullName} 的母亲。`
|
||||
})
|
||||
}
|
||||
onOpenChange(false)
|
||||
} catch (error) {
|
||||
@@ -144,7 +168,7 @@ export function AddRelationDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加{relationType === "child" ? "子女" : "配偶"}</DialogTitle>
|
||||
<DialogTitle>添加{relationType === "child" ? "子女" : relationType === "spouse" ? "配偶" : relationType === "father" ? "父亲" : "母亲"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
先输入姓名,系统会检索已有成员。若没有匹配,可直接新建。
|
||||
</DialogDescription>
|
||||
|
||||
@@ -191,11 +191,13 @@ const D3OrgChartFlowComponent = forwardRef<D3OrgChartRef, D3OrgChartFlowProps>(
|
||||
.filter(Boolean) || []
|
||||
|
||||
// 确定父节点ID - 只有根节点的parentId为null
|
||||
// 重要:parentId 必须指向已经在 visited 集合中的节点,否则 d3-org-chart 会报 missing 错误
|
||||
let parentId: string | null = null
|
||||
if (currentId !== rootId) {
|
||||
// 根据姓氏判断主线,优先选择同姓的父母
|
||||
const father = member.fatherId ? members[member.fatherId] : null
|
||||
const mother = member.motherId ? members[member.motherId] : null
|
||||
// 同时确保父母已经被访问过(在 visited 中)
|
||||
const father = member.fatherId && visited.has(member.fatherId) ? members[member.fatherId] : null
|
||||
const mother = member.motherId && visited.has(member.motherId) ? members[member.motherId] : null
|
||||
|
||||
// 判断父母中谁是家族主线(同姓)
|
||||
const isFatherMainLine = father && member.surname === father.surname
|
||||
|
||||
@@ -28,6 +28,10 @@ interface FamilyNodeProps {
|
||||
selectedMembers?: string[]
|
||||
canEdit?: boolean
|
||||
onAddRelation?: (type: "child" | "spouse") => void
|
||||
// 折叠功能
|
||||
hasChildren?: boolean
|
||||
isCollapsed?: boolean
|
||||
onToggleCollapse?: () => void
|
||||
}
|
||||
|
||||
// 格式化日期显示(只显示年份)
|
||||
@@ -64,7 +68,10 @@ function PersonCard({
|
||||
relationMode,
|
||||
isSelected,
|
||||
canEdit = true,
|
||||
onAddRelation
|
||||
onAddRelation,
|
||||
hasChildren = false,
|
||||
isCollapsed = false,
|
||||
onToggleCollapse
|
||||
}: {
|
||||
member: FamilyMember
|
||||
isRoot?: boolean
|
||||
@@ -75,6 +82,9 @@ function PersonCard({
|
||||
isSelected?: boolean
|
||||
canEdit?: boolean
|
||||
onAddRelation?: (type: "child" | "spouse") => void
|
||||
hasChildren?: boolean
|
||||
isCollapsed?: boolean
|
||||
onToggleCollapse?: () => void
|
||||
}) {
|
||||
const { avatarUrl: avatarBlobUrl } = useAvatarCache(member?.avatarUrl)
|
||||
const router = useRouter()
|
||||
@@ -322,6 +332,35 @@ function PersonCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 折叠/展开按钮 */}
|
||||
{hasChildren && onToggleCollapse && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggleCollapse()
|
||||
}}
|
||||
className={cn(
|
||||
"absolute -bottom-3 left-1/2 -translate-x-1/2 z-20",
|
||||
"w-6 h-6 rounded-full flex items-center justify-center",
|
||||
"bg-white border-2 shadow-md transition-all hover:scale-110",
|
||||
isCollapsed
|
||||
? "border-amber-500 text-amber-600"
|
||||
: "border-stone-400 text-stone-500"
|
||||
)}
|
||||
title={isCollapsed ? "展开子孙" : "折叠子孙"}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="18 15 12 9 6 15"></polyline>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -410,18 +449,59 @@ function PersonCard({
|
||||
)
|
||||
}
|
||||
|
||||
export function FamilyNode({ member, spouse, spouses, isRoot, onSelect, isHighlighted, relationMode, isSelected, selectedMembers = [], canEdit = true, onAddRelation }: FamilyNodeProps) {
|
||||
export function FamilyNode({
|
||||
member,
|
||||
spouse,
|
||||
spouses,
|
||||
isRoot,
|
||||
onSelect,
|
||||
isHighlighted,
|
||||
relationMode,
|
||||
isSelected,
|
||||
selectedMembers = [],
|
||||
canEdit = true,
|
||||
onAddRelation,
|
||||
hasChildren = false,
|
||||
isCollapsed = false,
|
||||
onToggleCollapse
|
||||
}: FamilyNodeProps) {
|
||||
// 优先使用 spouses 数组,如果没有则使用单个 spouse
|
||||
const spouseList = spouses || (spouse ? [spouse] : [])
|
||||
|
||||
if (spouseList.length === 0) {
|
||||
return <PersonCard member={member} isRoot={isRoot} onSelect={onSelect} isHighlighted={isHighlighted} relationMode={relationMode} isSelected={isSelected} canEdit={canEdit} onAddRelation={onAddRelation} />
|
||||
return (
|
||||
<PersonCard
|
||||
member={member}
|
||||
isRoot={isRoot}
|
||||
onSelect={onSelect}
|
||||
isHighlighted={isHighlighted}
|
||||
relationMode={relationMode}
|
||||
isSelected={isSelected}
|
||||
canEdit={canEdit}
|
||||
onAddRelation={onAddRelation}
|
||||
hasChildren={hasChildren}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 有配偶时,并排显示所有配偶
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<PersonCard member={member} isRoot={isRoot} onSelect={onSelect} isHighlighted={isHighlighted} relationMode={relationMode} isSelected={isSelected} canEdit={canEdit} onAddRelation={onAddRelation} />
|
||||
<PersonCard
|
||||
member={member}
|
||||
isRoot={isRoot}
|
||||
onSelect={onSelect}
|
||||
isHighlighted={isHighlighted}
|
||||
relationMode={relationMode}
|
||||
isSelected={isSelected}
|
||||
canEdit={canEdit}
|
||||
onAddRelation={onAddRelation}
|
||||
hasChildren={hasChildren}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
/>
|
||||
{spouseList.map((spouseMember, index) => (
|
||||
<div key={spouseMember.id} className="flex items-center gap-2">
|
||||
<Heart className="h-4 w-4 text-red-400 flex-shrink-0" />
|
||||
|
||||
+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