0.0.8.0
This commit is contained in:
+36
-27
@@ -1,23 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { MemberForm } from "@/components/members/member-form"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import { db } from "@/lib/db"
|
||||
import { PhotoGallery } from "@/components/members/photo-gallery"
|
||||
import { StoryManager } from "@/components/members/story-manager"
|
||||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||||
import { permissions } from "@/lib/permissions"
|
||||
|
||||
export default function MemberProfilePage() {
|
||||
const { id } = useParams()
|
||||
const { getMember, updateMember, deleteMember, treeData } = useFamily()
|
||||
const { treeData, currentTree, getMember, updateMember, deleteMember } = useFamily()
|
||||
const role = currentTree?.currentUserRole
|
||||
const { showConfirm } = useDialog()
|
||||
const router = useRouter()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
@@ -194,34 +195,42 @@ export default function MemberProfilePage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{!member.fatherId && (
|
||||
<Button variant="outline" onClick={handleQuickAddFather} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加父亲
|
||||
</Button>
|
||||
{permissions.canCreate(role) && (
|
||||
<>
|
||||
{!member.fatherId && (
|
||||
<Button variant="outline" onClick={handleQuickAddFather} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加父亲
|
||||
</Button>
|
||||
)}
|
||||
{!member.motherId && (
|
||||
<Button variant="outline" onClick={handleQuickAddMother} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加母亲
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="default" onClick={handleQuickAddChild} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加子女
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleQuickAddSpouse} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加配偶
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!member.motherId && (
|
||||
<Button variant="outline" onClick={handleQuickAddMother} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加母亲
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="default" onClick={handleQuickAddChild} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加子女
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleQuickAddSpouse} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加配偶
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => window.print()}>
|
||||
<Button variant="outline" size="icon" onClick={() => window.print()} title="打印">
|
||||
<Printer className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<Button variant="outline" size="icon" title="分享">
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsEditing(true)} className="gap-2">
|
||||
<Edit className="h-4 w-4" /> 编辑
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={handleDelete}>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{permissions.canUpdate(role) && (
|
||||
<Button variant="outline" onClick={() => setIsEditing(true)} className="gap-2">
|
||||
<Edit className="h-4 w-4" /> 编辑
|
||||
</Button>
|
||||
)}
|
||||
{permissions.canDeleteMember(role) && (
|
||||
<Button variant="destructive" size="icon" onClick={handleDelete} title="删除">
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+216
-178
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo, useEffect } from "react"
|
||||
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"
|
||||
@@ -15,9 +15,182 @@ 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"
|
||||
|
||||
// 占位符类型
|
||||
interface PlaceholderMember {
|
||||
id: string
|
||||
isPlaceholder: true
|
||||
}
|
||||
|
||||
type MemberOrPlaceholder = FamilyMember | PlaceholderMember
|
||||
|
||||
// 提取占位符生成逻辑为独立函数
|
||||
function addPlaceholdersToFamilyUnit(
|
||||
member: FamilyMember,
|
||||
spouses: FamilyMember[],
|
||||
processedSpouses: Set<string>
|
||||
): MemberOrPlaceholder[] {
|
||||
const result: MemberOrPlaceholder[] = [member]
|
||||
let familyUnitSize = 1 // 主成员自己
|
||||
|
||||
// 添加配偶
|
||||
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)
|
||||
familyUnitSize++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算需要添加的占位符数量,确保填满4的倍数
|
||||
const placeholdersNeeded = (4 - (familyUnitSize % 4)) % 4
|
||||
for (let i = 0; i < placeholdersNeeded; i++) {
|
||||
result.push({ id: `placeholder-${member.id}-${i}`, isPlaceholder: true })
|
||||
}
|
||||
|
||||
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,
|
||||
}: {
|
||||
member: FamilyMember
|
||||
allMembers: FamilyMember[]
|
||||
onLocateInTree: (id: string) => void
|
||||
}) {
|
||||
// 判断是否是配偶
|
||||
const isFounder = member.tags?.includes('始祖')
|
||||
const isSpouse = !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
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<Link href={`/members/${member.id}`}>
|
||||
<Card className={`hover:border-primary transition-colors cursor-pointer h-full ${
|
||||
isSpouse
|
||||
? 'bg-pink-50/50 dark:bg-pink-950/20 border-pink-200/50 dark:border-pink-800/30'
|
||||
: 'bg-blue-50/50 dark:bg-blue-950/20 border-blue-200/50 dark:border-blue-800/30'
|
||||
}`}>
|
||||
<CardContent className="p-3 flex items-center gap-5">
|
||||
<div className="relative flex-shrink-0">
|
||||
<AvatarDisplay
|
||||
imageId={member.avatarImageId}
|
||||
fallbackUrl={member.avatarUrl}
|
||||
fallbackText={member.surname}
|
||||
gender={member.gender}
|
||||
className="h-20 w-20 border-2 border-border"
|
||||
/>
|
||||
{/* 在世/去世标识 */}
|
||||
{member.deathDate && (
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-gray-500 border-2 border-white" title="已故" />
|
||||
)}
|
||||
{!member.deathDate && (
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-green-500 border-2 border-white" title="在世" />
|
||||
)}
|
||||
</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">
|
||||
{member.fullName}
|
||||
{member.courtesyName && (
|
||||
<span className="text-xs font-normal text-muted-foreground ml-1">
|
||||
字 {member.courtesyName}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
{isFounder ? (
|
||||
<p className="text-xs text-amber-700 font-medium mt-0.5 truncate">
|
||||
家族始祖
|
||||
</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>
|
||||
{member.birthDate && new Date(member.birthDate).getFullYear()}
|
||||
{member.deathDate && ` - ${new Date(member.deathDate).getFullYear()}`}
|
||||
{!member.deathDate && member.birthDate && ' - '}
|
||||
</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="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
)
|
||||
})
|
||||
|
||||
export default function MembersPage() {
|
||||
const { treeData, searchMembers, setHighlightedMemberId } = useFamily()
|
||||
const { treeData, currentTree, searchMembers, setHighlightedMemberId } = useFamily()
|
||||
const router = useRouter()
|
||||
const [query, setQuery] = useState("")
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
@@ -63,12 +236,11 @@ export default function MembersPage() {
|
||||
}
|
||||
}, [allMembers])
|
||||
|
||||
// Initialize slider when data loads
|
||||
useMemo(() => {
|
||||
// Initialize slider when data loads - 修复:使用 useEffect 而不是 useMemo
|
||||
useEffect(() => {
|
||||
if (minGen !== 1000 && maxGen !== 0) {
|
||||
// Only update if range is default
|
||||
// This is a bit tricky, for now let's just rely on user adjustment or default wide range
|
||||
// Ideally we sync this once.
|
||||
// 只在初始加载时设置一次
|
||||
setGenerationRange([minGen, maxGen])
|
||||
}
|
||||
}, [minGen, maxGen])
|
||||
|
||||
@@ -112,10 +284,11 @@ export default function MembersPage() {
|
||||
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, typeof filteredMembers>()
|
||||
const groups = new Map<number, FamilyMember[]>()
|
||||
|
||||
// 第一步:按世代分组
|
||||
filteredMembers.forEach(member => {
|
||||
const gen = member.generation
|
||||
if (!groups.has(gen)) {
|
||||
@@ -124,78 +297,49 @@ export default function MembersPage() {
|
||||
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 => {
|
||||
// 家族成员:有父母关系 或 明确标记为始祖
|
||||
const isFounder = m.tags?.includes('始祖')
|
||||
const hasFamilyParents = m.fatherId || m.motherId
|
||||
|
||||
// 如果有父母关系,肯定是家族成员
|
||||
if (hasFamilyParents) return true
|
||||
|
||||
// 如果明确标记为始祖,是家族成员
|
||||
if (isFounder) return true
|
||||
|
||||
// 第1世且没有配偶的,可能是始祖(兼容没有标签的情况)
|
||||
if (hasFamilyParents || isFounder) return true
|
||||
if (generation === 1 && (!m.spouseIds || m.spouseIds.length === 0)) return true
|
||||
|
||||
// 其他情况(如第1世但有配偶且没有父母的)是配偶身份
|
||||
return false
|
||||
})
|
||||
|
||||
// 对家族成员按年龄排序(出生日期从早到晚 = 年龄从高到低)
|
||||
const sortedFamilyMembers = [...familyMembers].sort((a, b) => {
|
||||
// 1. 始祖永远排第一
|
||||
// 对家族成员按年龄排序
|
||||
const sortedFamilyMembers = familyMembers.sort((a, b) => {
|
||||
const aIsFounder = a.tags?.includes('始祖')
|
||||
const bIsFounder = b.tags?.includes('始祖')
|
||||
if (aIsFounder && !bIsFounder) return -1
|
||||
if (!aIsFounder && bIsFounder) return 1
|
||||
|
||||
// 2. 按出生日期排序(早出生的在前 = 年龄大的在前)
|
||||
const aDate = a.birthDate || "9999-12-31"
|
||||
const bDate = b.birthDate || "9999-12-31"
|
||||
return aDate.localeCompare(bDate)
|
||||
})
|
||||
|
||||
// 3. 将配偶插入到主成员后面
|
||||
const result: typeof members = []
|
||||
// 使用优化后的函数添加配偶和占位符
|
||||
const result: MemberOrPlaceholder[] = []
|
||||
const processedSpouses = new Set<string>()
|
||||
|
||||
sortedFamilyMembers.forEach(member => {
|
||||
result.push(member)
|
||||
|
||||
// 计算该家庭单元的成员数(主成员 + 配偶)
|
||||
let familyUnitSize = 1 // 主成员自己
|
||||
|
||||
// 如果有配偶,将配偶紧跟在后面
|
||||
if (member.spouseIds && member.spouseIds.length > 0) {
|
||||
member.spouseIds.forEach(spouseId => {
|
||||
const spouse = members.find(m => m.id === spouseId)
|
||||
if (spouse && !processedSpouses.has(spouseId)) {
|
||||
result.push(spouse)
|
||||
processedSpouses.add(spouseId)
|
||||
familyUnitSize++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算需要添加的占位符数量,确保填满4的倍数
|
||||
// 例如:1个主成员 + 1个配偶 = 2人,需要添加2个占位符
|
||||
// 例如:1个主成员 + 3个配偶 = 4人,不需要占位符
|
||||
// 例如:1个主成员 + 0个配偶 = 1人,需要添加3个占位符
|
||||
const placeholdersNeeded = (4 - (familyUnitSize % 4)) % 4
|
||||
for (let i = 0; i < placeholdersNeeded; i++) {
|
||||
result.push({ id: `placeholder-${member.id}-${i}`, isPlaceholder: true } as any)
|
||||
}
|
||||
const familyUnit = addPlaceholdersToFamilyUnit(member, members, processedSpouses)
|
||||
result.push(...familyUnit)
|
||||
})
|
||||
|
||||
// 计算实际成员数(不包括占位符)
|
||||
const actualMemberCount = result.filter(m => !isPlaceholder(m)).length
|
||||
|
||||
return {
|
||||
generation,
|
||||
members: result
|
||||
members: result,
|
||||
actualCount: actualMemberCount
|
||||
}
|
||||
})
|
||||
}, [filteredMembers])
|
||||
@@ -243,12 +387,14 @@ export default function MembersPage() {
|
||||
<span className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-primary border-2 border-background" />
|
||||
)}
|
||||
</Button>
|
||||
<Link href="/members/new">
|
||||
<Button className="gap-2 bg-primary text-primary-foreground">
|
||||
<Plus className="h-4 w-4" />
|
||||
新增成员
|
||||
</Button>
|
||||
</Link>
|
||||
{permissions.canCreate(currentTree?.currentUserRole) && (
|
||||
<Link href="/members/new">
|
||||
<Button className="gap-2 bg-primary text-primary-foreground">
|
||||
<Plus className="h-4 w-4" />
|
||||
新增成员
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -338,139 +484,31 @@ export default function MembersPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{membersByGeneration.map(({ generation, members }) => (
|
||||
{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">共 {members.length} 人</span>
|
||||
<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 ((member as any).isPlaceholder) {
|
||||
if (isPlaceholder(member)) {
|
||||
return <div key={member.id} className="invisible"></div>
|
||||
}
|
||||
|
||||
// 判断是否是配偶:
|
||||
// 1. 没有父母ID
|
||||
// 2. 有配偶ID
|
||||
// 3. 不是始祖(没有'始祖'标签)
|
||||
const isFounder = member.tags?.includes('始祖')
|
||||
const isSpouse = !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
|
||||
|
||||
// 使用优化后的 MemberCard 组件
|
||||
return (
|
||||
<div key={member.id} className="relative group">
|
||||
<Link href={`/members/${member.id}`}>
|
||||
<Card className={`hover:border-primary transition-colors cursor-pointer h-full ${
|
||||
isSpouse
|
||||
? 'bg-pink-50/50 dark:bg-pink-950/20 border-pink-200/50 dark:border-pink-800/30'
|
||||
: 'bg-blue-50/50 dark:bg-blue-950/20 border-blue-200/50 dark:border-blue-800/30'
|
||||
}`}>
|
||||
<CardContent className="p-3 flex items-center gap-5">
|
||||
<div className="relative flex-shrink-0">
|
||||
<AvatarDisplay
|
||||
imageId={member.avatarImageId}
|
||||
fallbackUrl={member.avatarUrl}
|
||||
fallbackText={member.surname}
|
||||
gender={member.gender}
|
||||
className="h-20 w-20 border-2 border-border"
|
||||
/>
|
||||
{/* 在世/去世标识 */}
|
||||
{member.deathDate && (
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-gray-500 border-2 border-white" title="已故" />
|
||||
)}
|
||||
{!member.deathDate && (
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-green-500 border-2 border-white" title="在世" />
|
||||
)}
|
||||
</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">
|
||||
{member.fullName}
|
||||
{member.courtesyName && (
|
||||
<span className="text-xs font-normal text-muted-foreground ml-1">
|
||||
字 {member.courtesyName}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
{isFounder ? (
|
||||
<p className="text-xs text-amber-700 font-medium mt-0.5 truncate">
|
||||
家族始祖
|
||||
</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>
|
||||
{member.birthDate && new Date(member.birthDate).getFullYear()}
|
||||
{member.deathDate && ` - ${new Date(member.deathDate).getFullYear()}`}
|
||||
{!member.deathDate && member.birthDate && ' - '}
|
||||
</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="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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={() => {
|
||||
setHighlightedMemberId(member.id)
|
||||
router.push('/tree')
|
||||
}}
|
||||
title="在族谱图中定位"
|
||||
>
|
||||
<Search className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<MemberCard
|
||||
key={member.id}
|
||||
member={member}
|
||||
allMembers={allMembers}
|
||||
onLocateInTree={(id) => {
|
||||
setHighlightedMemberId(id)
|
||||
router.push('/tree')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user