Files
chinese-family-tree-2/app/members/page.tsx
T
freedakgmail 5c8c70097c 0.8.0.0
2025-12-21 17:32:33 +08:00

938 lines
36 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState, useMemo, useEffect, useCallback, memo } from "react"
import { SiteHeader } from "@/components/site-header"
import { useFamily } from "@/context/family-context"
import { useRouter } from "next/navigation"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { ChineseCard, ChineseCardContent } from "@/components/ui/chinese-card"
import { Search, Plus, Filter, X, Users, User, UserPlus, Baby } from "lucide-react"
import Link from "next/link"
import { AvatarDisplay } from "@/components/ui/avatar-display"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Slider } from "@/components/ui/slider"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import type { FamilyMember } from "@/types/family"
import { permissions } from "@/lib/permissions"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { useSession } from "next-auth/react"
import { EmptyStateBadge } from "@/components/empty-state-badge"
import { AddRelationDialog, type RelationType } from "@/components/tree/add-relation-dialog"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import { GenerationWarningDialog } from "@/components/tree/generation-warning-dialog"
import { isFirstGenerationMember, shouldShowGenerationWarning } from "@/lib/generation-utils"
// 占位符类型
interface PlaceholderMember {
id: string
isPlaceholder: true
}
type MemberOrPlaceholder = FamilyMember | PlaceholderMember
// 提取配偶添加逻辑为独立函数(移除占位符,让卡片自然流动排列)
function addSpousesToFamilyUnit(
member: FamilyMember,
spouses: FamilyMember[],
processedSpouses: Set<string>
): MemberOrPlaceholder[] {
const result: MemberOrPlaceholder[] = [member]
// 添加配偶(紧跟在主成员后面)
if (member.spouseIds && member.spouseIds.length > 0) {
member.spouseIds.forEach(spouseId => {
const spouse = spouses.find(m => m.id === spouseId)
if (spouse && !processedSpouses.has(spouseId)) {
result.push(spouse)
processedSpouses.add(spouseId)
}
})
}
return result
}
// 判断是否是占位符
function isPlaceholder(member: MemberOrPlaceholder): member is PlaceholderMember {
return 'isPlaceholder' in member && member.isPlaceholder === true
}
// 成员卡片组件 - 使用 memo 优化
const MemberCard = memo(function MemberCard({
member,
allMembers,
onLocateInTree,
currentTreeId,
canEdit,
onOpenAddDialog,
}: {
member: FamilyMember
allMembers: FamilyMember[]
onLocateInTree: (id: string) => void
currentTreeId?: string
canEdit: boolean
onOpenAddDialog: (member: FamilyMember, type: RelationType) => void
}) {
// 代数重新计算警告对话框状态
const [showWarningDialog, setShowWarningDialog] = useState(false)
const [pendingRelationType, setPendingRelationType] = useState<'father' | 'mother' | null>(null)
// 判断是否是始祖(使用 rootMemberId 而不是 isFounder 字段)
const isFounder = member.id === currentTreeId
// 判断是否是配偶
const isSpouse = !member.fatherId && !member.motherId &&
member.spouseIds && member.spouseIds.length > 0 &&
!isFounder
// 判断是否是孤立成员(没有父母、没有配偶、不是始祖)
const isOrphan = !member.fatherId && !member.motherId &&
(!member.spouseIds || member.spouseIds.length === 0) &&
!isFounder
// 获取配偶信息
const spouse = isSpouse && member.spouseIds?.[0]
? allMembers.find(m => m.id === member.spouseIds[0])
: null
const spouseName = spouse ? spouse.fullName : null
// 获取父母信息
const father = member.fatherId ? allMembers.find(m => m.id === member.fatherId) : null
const mother = member.motherId ? allMembers.find(m => m.id === member.motherId) : null
// 判断是否已故
const isDead = !!member.deathDate
// 根据在世/已故状态设置卡片样式
// 优先级:始祖 > 孤立 > 已故 > 在世
const cardStyle = isFounder
? '!border-2 !border-amber-400 !bg-amber-50/30 dark:!bg-amber-900/20 shadow-md' // 始祖:金色边框(优先级最高)
: isOrphan
? '!border-2 !border-orange-400 !bg-orange-50/30 dark:!bg-orange-900/20' // 孤立:橙色边框提醒
: isDead
? '!border-2 !border-gray-400 dark:!border-gray-600 !bg-gray-50/50 dark:!bg-gray-900/30' // 已故:灰色深边框
: '!border-2 !border-amber-300/70 !bg-card dark:!border-amber-600/50' // 在世:浅金色边框
// 判断可以添加的关系
const canAddFather = !member.fatherId
const canAddMother = !member.motherId
const canAddSpouse = true // 配偶总是可以添加
const canAddChild = true // 子女总是可以添加
// 构建新增成员的 URL
const buildAddUrl = (type: 'father' | 'mother' | 'spouse' | 'child') => {
const params = new URLSearchParams()
if (currentTreeId) {
params.set('treeId', currentTreeId)
}
switch (type) {
case 'father':
params.set('childId', member.id)
params.set('gender', 'MALE')
// 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代)
params.set('generation', member.generation === 1 ? '1' : (member.generation - 1).toString())
// 如果已有母亲,设为配偶
if (member.motherId) {
params.set('spouseId', member.motherId)
}
break
case 'mother':
params.set('childId', member.id)
params.set('gender', 'FEMALE')
// 如果是第1代成员,新父母的世代也设为1(后端会重新计算所有成员世代)
params.set('generation', member.generation === 1 ? '1' : (member.generation - 1).toString())
// 如果已有父亲,设为配偶
if (member.fatherId) {
params.set('spouseId', member.fatherId)
}
break
case 'spouse':
params.set('spouseId', member.id)
params.set('generation', member.generation.toString())
// 配偶性别与当前成员相反
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
// 如果当前成员有子女,将他们也作为新配偶的子女
if (member.childrenIds && member.childrenIds.length > 0) {
member.childrenIds.forEach(id => {
params.append('childrenIds', id)
})
}
break
case 'child':
// 根据当前成员性别设置父/母
if (member.gender === 'MALE') {
params.set('fatherId', member.id)
// 如果父亲有配偶,设置配偶为母亲
if (member.spouseIds && member.spouseIds.length > 0) {
params.set('motherId', member.spouseIds[0])
}
} else {
params.set('motherId', member.id)
// 如果母亲有配偶,设置配偶为父亲
if (member.spouseIds && member.spouseIds.length > 0) {
params.set('fatherId', member.spouseIds[0])
}
}
params.set('generation', (member.generation + 1).toString())
break
}
return `/members/new?${params.toString()}`
}
const handleAddMember = (type: RelationType) => {
// 检查是否需要显示代数重新计算警告
if ((type === 'father' || type === 'mother') && shouldShowGenerationWarning(member, type)) {
setPendingRelationType(type)
setShowWarningDialog(true)
return
}
onOpenAddDialog(member, type)
}
// 确认添加父母后的处理
const handleConfirmAddParent = () => {
if (pendingRelationType) {
onOpenAddDialog(member, pendingRelationType)
setPendingRelationType(null)
}
}
// 卡片内容
const cardContent = (
<div className="relative group">
<Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}>
<ChineseCard variant="default" className={`hover:shadow-lg transition-all cursor-pointer h-full rounded-xl ${cardStyle}`}>
<ChineseCardContent className="px-4 py-3 flex items-center gap-5">
<div className="relative flex-shrink-0">
<AvatarDisplay
imageUrl={member.avatarUrl}
fallbackText={member.surname}
gender={member.gender}
className="h-20 w-20 border-2 border-border"
/>
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center">
<div className="flex items-baseline justify-between gap-1.5">
<h3 className="text-lg font-bold font-serif truncate flex items-center gap-1">
{isFounder && <span className="text-amber-600">👑</span>}
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
dotPosition="top-left"
/>
{member.courtesyName && (
<span className="text-xs font-normal text-muted-foreground ml-1">
{member.courtesyName}
</span>
)}
</h3>
{/* 父母信息 - 放在姓名右侧 */}
{(father || mother) && (
<div className="flex items-center gap-1 text-xs text-muted-foreground flex-shrink-0">
<span className="text-muted-foreground/70">:</span>
{father && (
<span
className="hover:text-primary hover:underline cursor-pointer"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.location.href = `/members/${father.id}${window.location.search}`
}}
>
<MemberNameWithStatus
name={father.fullName}
isDead={!!father.deathDate}
dotPosition="top-left"
/>
</span>
)}
{father && mother && <span className="text-muted-foreground/50">·</span>}
{mother && (
<span
className="hover:text-primary hover:underline cursor-pointer"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.location.href = `/members/${mother.id}${window.location.search}`
}}
>
<MemberNameWithStatus
name={mother.fullName}
isDead={!!mother.deathDate}
dotPosition="top-left"
/>
</span>
)}
</div>
)}
</div>
{isFounder ? (
<p className="text-xs text-amber-700 font-semibold mt-0.5 truncate">
</p>
) : isOrphan ? (
<p className="text-xs text-orange-600 font-medium mt-0.5 truncate flex items-center gap-1">
<span></span>
<span> - </span>
</p>
) : isSpouse && spouseName ? (
<p className="text-xs text-purple-700 mt-0.5 truncate">
{spouseName}
</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{member.generationName ? `字辈: ${member.generationName}` : "未录入字辈"}
</p>
)}
{/* 籍贯和生卒年并排 */}
<div className="flex items-start justify-between gap-2 mt-0.5">
<p className="text-xs text-muted-foreground truncate flex-1">{member.ancestralHome || "籍贯未知"}</p>
{/* 生卒年和年龄 */}
{(member.birthDate || member.deathDate) && (
<div className="text-xs text-muted-foreground text-right space-y-0.5 flex-shrink-0">
<div className="flex items-center gap-1 justify-end">
<span>
{member.birthDate && new Date(member.birthDate).getFullYear()}
{member.deathDate && ` - ${new Date(member.deathDate).getFullYear()}`}
{!member.deathDate && member.birthDate && ' - '}
</span>
{member.isLunarDate && (
<span className="text-[10px] text-amber-600 bg-amber-50 px-1 py-0.5 rounded border border-amber-200">
</span>
)}
</div>
{(() => {
if (!member.birthDate) return null
const birthYear = new Date(member.birthDate).getFullYear()
const deathYear = member.deathDate ? new Date(member.deathDate).getFullYear() : new Date().getFullYear()
const age = deathYear - birthYear
return (
<div className="text-[10px] text-muted-foreground">
{member.deathDate ? `享年${age}` : `${age}`}
</div>
)
})()}
</div>
)}
</div>
{member.tags && member.tags.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
{member.tags.map((tag) => (
<span
key={tag}
className={tag === '始祖'
? "text-xs bg-amber-100 text-amber-800 px-2 py-0.5 rounded border border-amber-300 font-semibold"
: "text-xs bg-primary/10 text-primary px-2 py-0.5 rounded border border-primary/20"}
>
{tag === '始祖' ? '👑 始祖' : tag}
</span>
))}
</div>
)}
</div>
</ChineseCardContent>
</ChineseCard>
</Link>
<Button
size="sm"
variant="ghost"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6 p-0"
onClick={() => onLocateInTree(member.id)}
title="在族谱图中定位"
>
<Search className="h-3 w-3" />
</Button>
</div>
)
// 如果没有编辑权限,直接返回卡片内容
if (!canEdit) {
return cardContent
}
// 有编辑权限时,包裹右键菜单
return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>
{cardContent}
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuLabel className="text-xs text-muted-foreground">
{member.fullName}
</ContextMenuLabel>
<ContextMenuSeparator />
{canAddFather && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('father')
}}
>
<UserPlus className="h-4 w-4 text-blue-500" />
</ContextMenuItem>
)}
{canAddMother && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('mother')
}}
>
<UserPlus className="h-4 w-4 text-pink-500" />
</ContextMenuItem>
)}
{(canAddFather || canAddMother) && (canAddSpouse || canAddChild) && (
<ContextMenuSeparator />
)}
{canAddSpouse && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('spouse')
}}
>
<Users className="h-4 w-4 text-red-500" />
</ContextMenuItem>
)}
{canAddChild && (
<ContextMenuItem
className="gap-2 cursor-pointer"
onPointerDown={(e) => {
e.preventDefault()
handleAddMember('child')
}}
>
<Baby className="h-4 w-4 text-green-500" />
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
{/* 代数重新计算警告对话框 */}
{pendingRelationType && (
<GenerationWarningDialog
open={showWarningDialog}
onOpenChange={setShowWarningDialog}
memberName={member.fullName}
relationType={pendingRelationType}
onConfirm={handleConfirmAddParent}
/>
)}
</>
)
})
export default function MembersPage() {
const { treeData, currentTree, searchMembers, setHighlightedMemberId, refreshData } = useFamily()
const { data: session, status } = useSession()
const router = useRouter()
const [query, setQuery] = useState("")
const [showFilters, setShowFilters] = useState(false)
// 添加成员对话框状态
const [relationDialog, setRelationDialog] = useState<{
member: FamilyMember
type: RelationType
} | null>(null)
// 打开添加成员对话框
const handleOpenAddDialog = useCallback((member: FamilyMember, type: RelationType) => {
setRelationDialog({ member, type })
}, [])
// 姓名分解函数
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: '' }
}
// 创建新成员的处理函数
const handleCreateNewRelation = useCallback((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)
}
}
if (currentTree?.id) {
params.set("treeId", currentTree.id)
}
setRelationDialog(null)
router.push(`/members/new?${params.toString()}`)
}, [relationDialog, currentTree?.id, router])
// 未登录时重定向到登录页
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin')
}
}, [status, router])
// 页面加载时刷新数据
useEffect(() => {
if (currentTree?.id) {
refreshData()
}
}, [currentTree?.id, refreshData])
// 从 URL 参数读取搜索关键词
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const searchParam = params.get('search')
if (searchParam) {
setQuery(searchParam)
}
}, [])
// Filter States
const [selectedSurname, setSelectedSurname] = useState<string>("all")
const [selectedHome, setSelectedHome] = useState<string>("all")
const [statusFilter, setStatusFilter] = useState<string>("all")
const [generationRange, setGenerationRange] = useState<[number, number]>([1, 100])
// Derived Data
const allMembers = useMemo(() => Object.values(treeData.members), [treeData])
const { uniqueSurnames, uniqueHomes, minGen, maxGen } = useMemo(() => {
const surnames = new Set<string>()
const homes = new Set<string>()
let min = 1000
let max = 0
allMembers.forEach((m) => {
if (m.surname) surnames.add(m.surname)
if (m.ancestralHome) homes.add(m.ancestralHome)
if (m.generation) {
if (m.generation < min) min = m.generation
if (m.generation > max) max = m.generation
}
})
return {
uniqueSurnames: Array.from(surnames).sort(),
uniqueHomes: Array.from(homes).sort(),
minGen: min === 1000 ? 1 : min,
maxGen: max === 0 ? 100 : max,
}
}, [allMembers])
// Initialize slider when data loads - 修复:使用 useEffect 而不是 useMemo
useEffect(() => {
if (minGen !== 1000 && maxGen !== 0) {
// 只在初始加载时设置一次
setGenerationRange([minGen, maxGen])
}
}, [minGen, maxGen])
const filteredMembers = useMemo(() => {
let result = allMembers
// 1. Search Query
if (query) {
const lowerQuery = query.toLowerCase()
result = result.filter(
(m) =>
m.fullName.toLowerCase().includes(lowerQuery) ||
m.courtesyName?.toLowerCase().includes(lowerQuery) ||
m.artName?.toLowerCase().includes(lowerQuery),
)
}
// 2. Surname Filter
if (selectedSurname !== "all") {
result = result.filter((m) => m.surname === selectedSurname)
}
// 3. Home Filter
if (selectedHome !== "all") {
result = result.filter((m) => m.ancestralHome === selectedHome)
}
// 4. Status Filter
if (statusFilter !== "all") {
if (statusFilter === "living") {
result = result.filter((m) => !m.deathDate)
} else if (statusFilter === "deceased") {
result = result.filter((m) => !!m.deathDate)
}
}
// 5. Generation Filter
result = result.filter((m) => m.generation >= generationRange[0] && m.generation <= generationRange[1])
// Sort
return result.sort((a, b) => a.generation - b.generation || (a.birthDate || "").localeCompare(b.birthDate || ""))
}, [allMembers, query, selectedSurname, selectedHome, statusFilter, generationRange])
// 按世代分组并排序 - 优化后的版本
const membersByGeneration = useMemo(() => {
const groups = new Map<number, FamilyMember[]>()
// 第一步:按世代分组
filteredMembers.forEach(member => {
const gen = member.generation
if (!groups.has(gen)) {
groups.set(gen, [])
}
groups.get(gen)!.push(member)
})
// 第二步:转换为数组并排序
return Array.from(groups.entries())
.sort((a, b) => a[0] - b[0])
.map(([generation, members]) => {
// 区分家族成员和配偶
// 配偶定义:没有父母但有配偶的成员(嫁入/娶入的成员)
// 其他所有成员都应该显示,包括孤立成员
const familyMembers = members.filter(m => {
// 使用 treeData.rootId 判断始祖,而不是 isFounder 字段
const isFounder = m.id === treeData.rootId
const hasFamilyParents = m.fatherId || m.motherId
const hasSpouse = m.spouseIds && m.spouseIds.length > 0
// 始祖或有父母的成员:显示
if (hasFamilyParents || isFounder) return true
// 第一代无配偶成员:显示
if (generation === 1 && !hasSpouse) return true
// 孤立成员(无父母、无配偶、非始祖):也要显示,方便用户关联
if (!hasFamilyParents && !hasSpouse && !isFounder) return true
// 配偶(无父母但有配偶):不在这里显示,会跟随主成员显示
return false
})
// 对家族成员按年龄排序
const sortedFamilyMembers = familyMembers.sort((a, b) => {
// 使用 treeData.rootId 判断始祖
const aIsFounder = a.id === treeData.rootId
const bIsFounder = b.id === treeData.rootId
if (aIsFounder && !bIsFounder) return -1
if (!aIsFounder && bIsFounder) return 1
const aDate = a.birthDate || "9999-12-31"
const bDate = b.birthDate || "9999-12-31"
return aDate.localeCompare(bDate)
})
// 添加配偶(紧跟在主成员后面,不使用占位符)
const result: MemberOrPlaceholder[] = []
const processedSpouses = new Set<string>()
sortedFamilyMembers.forEach(member => {
const familyUnit = addSpousesToFamilyUnit(member, members, processedSpouses)
result.push(...familyUnit)
})
// 计算实际成员数(不包括占位符)
const actualMemberCount = result.filter(m => !isPlaceholder(m)).length
return {
generation,
members: result,
actualCount: actualMemberCount
}
})
}, [filteredMembers, treeData.rootId])
const clearFilters = () => {
setSelectedSurname("all")
setSelectedHome("all")
setStatusFilter("all")
setGenerationRange([1, 100]) // Reset to wide range
setQuery("")
}
const activeFilterCount =
(selectedSurname !== "all" ? 1 : 0) +
(selectedHome !== "all" ? 1 : 0) +
(statusFilter !== "all" ? 1 : 0) +
(generationRange[0] > 1 || generationRange[1] < 100 ? 1 : 0)
// 未登录时不渲染内容(已在 useEffect 中重定向)
if (status === 'loading' || !session) {
return null
}
return (
<div className="h-screen bg-background flex flex-col font-sans overflow-hidden">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6 flex-1 flex flex-col overflow-hidden">
<div className="flex flex-col gap-6 mb-8 flex-shrink-0">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<h1 className="text-3xl font-serif font-bold"></h1>
<div className="flex items-center gap-2 w-full md:w-auto">
<div className="relative flex-1 md:w-80">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索姓名、字号..."
className="pl-9"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<Button
variant={showFilters || activeFilterCount > 0 ? "secondary" : "outline"}
size="icon"
onClick={() => setShowFilters(!showFilters)}
className="relative"
>
<Filter className="h-4 w-4" />
{activeFilterCount > 0 && (
<span className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-primary border-2 border-background" />
)}
</Button>
{permissions.canCreate(currentTree?.currentUserRole) && (
<Link href={`/members/new${typeof window !== 'undefined' ? window.location.search : ''}`}>
<Button className="gap-2 bg-primary text-primary-foreground">
<Plus className="h-4 w-4" />
</Button>
</Link>
)}
</div>
</div>
{/* Filter Bar */}
{showFilters && (
<div className="bg-muted/30 p-4 rounded-lg border border-border space-y-4 animate-in slide-in-from-top-2">
<div className="flex items-center justify-between">
<h3 className="font-medium text-sm text-muted-foreground"></h3>
<Button variant="ghost" size="sm" onClick={clearFilters} className="h-8 text-xs">
<X className="mr-1 h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select value={selectedSurname} onValueChange={setSelectedSurname}>
<SelectTrigger>
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{uniqueSurnames.map((s) => (
<SelectItem key={s} value={s}>
{s}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select value={selectedHome} onValueChange={setSelectedHome}>
<SelectTrigger>
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{uniqueHomes.map((h) => (
<SelectItem key={h} value={h}>
{h}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-xs"></Label>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="living"></SelectItem>
<SelectItem value="deceased"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label className="text-xs"></Label>
<span className="text-xs text-muted-foreground">
{generationRange[0]} - {generationRange[1]}
</span>
</div>
<Slider
defaultValue={[1, 100]}
value={generationRange}
min={1}
max={100} // Can be dynamic based on maxGen
step={1}
onValueChange={(val) => setGenerationRange(val as [number, number])}
className="py-4"
/>
</div>
</div>
</div>
)}
</div>
{filteredMembers.length === 0 ? (
<div className="flex-1 flex items-center justify-center -mt-20">
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 text-primary/80">
<Search className="h-16 w-16" strokeWidth={1} />
</div>
<p className="text-base text-foreground font-light tracking-wide"></p>
</div>
</div>
) : (
<div className="space-y-8 overflow-y-auto flex-1 pb-8">
{membersByGeneration.map(({ generation, members, actualCount }) => (
<div key={generation}>
<div className="flex items-center gap-3 mb-4">
<h2 className="text-xl font-serif font-bold text-foreground"> {generation} </h2>
<span className="text-sm text-muted-foreground"> {actualCount} </span>
<div className="flex-1 h-px bg-border"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{members.map((member) => {
// 检查是否是占位符
if (isPlaceholder(member)) {
return <div key={member.id} className="invisible"></div>
}
// 使用优化后的 MemberCard 组件
return (
<MemberCard
key={member.id}
member={member}
allMembers={allMembers}
onLocateInTree={(id) => {
setHighlightedMemberId(id)
const params = new URLSearchParams(window.location.search)
router.push(`/tree?${params.toString()}`)
}}
currentTreeId={treeData.rootId || undefined}
canEdit={permissions.canCreate(currentTree?.currentUserRole)}
onOpenAddDialog={handleOpenAddDialog}
/>
)
})}
</div>
</div>
))}
</div>
)}
</main>
{/* 添加成员对话框 */}
{relationDialog && (
<AddRelationDialog
open={!!relationDialog}
onOpenChange={(open) => {
if (!open) setRelationDialog(null)
}}
member={relationDialog.member}
relationType={relationDialog.type}
onCreateNew={handleCreateNewRelation}
/>
)}
</div>
)
}