0.0.8.5
This commit is contained in:
+426
-199
@@ -7,29 +7,59 @@ 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 { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus, Camera } from "lucide-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"
|
||||
import { MemberNameWithStatus } from "@/components/member-name-with-status"
|
||||
import { MemberVersionHistory } from "@/components/members/member-version-history"
|
||||
|
||||
export default function MemberProfilePage() {
|
||||
const { id } = useParams()
|
||||
const { treeData, currentTree, getMember, updateMember, deleteMember } = useFamily()
|
||||
const { treeData, currentTree, getMember, updateMember, deleteMember, isLoading } = useFamily()
|
||||
const role = currentTree?.currentUserRole
|
||||
const { showConfirm } = useDialog()
|
||||
const router = useRouter()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | undefined>(undefined)
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
|
||||
const member = getMember(id as string)
|
||||
const allMembers = Object.values(treeData.members)
|
||||
|
||||
// 始祖专用边框样式
|
||||
const founderBorderClass = member?.isFounder ? "border-2 border-amber-400 shadow-md" : "border border-border"
|
||||
|
||||
// 计算年龄或享年
|
||||
const calculateAge = () => {
|
||||
if (!member?.birthDate) return null
|
||||
|
||||
const birthYear = new Date(member.birthDate).getFullYear()
|
||||
|
||||
if (member.deathDate) {
|
||||
// 已故,计算享年
|
||||
const deathYear = new Date(member.deathDate).getFullYear()
|
||||
return { age: deathYear - birthYear, type: 'deceased' }
|
||||
} else {
|
||||
// 在世,计算年龄
|
||||
const currentYear = new Date().getFullYear()
|
||||
return { age: currentYear - birthYear, type: 'living' }
|
||||
}
|
||||
}
|
||||
|
||||
const ageInfo = calculateAge()
|
||||
|
||||
// Load avatar blob if exists
|
||||
// Load avatar
|
||||
useEffect(() => {
|
||||
if (member?.avatarImageId) {
|
||||
// 优先使用 avatarUrl(服务器存储)
|
||||
if (member?.avatarUrl) {
|
||||
setAvatarBlobUrl(member.avatarUrl)
|
||||
}
|
||||
// 兼容旧的 IndexedDB 存储
|
||||
else if (member?.avatarImageId) {
|
||||
db.images.get(member.avatarImageId).then((image) => {
|
||||
if (image) {
|
||||
const url = URL.createObjectURL(image.blob)
|
||||
@@ -40,15 +70,28 @@ export default function MemberProfilePage() {
|
||||
} else {
|
||||
setAvatarBlobUrl(undefined)
|
||||
}
|
||||
}, [member?.avatarImageId])
|
||||
}, [member?.avatarUrl, member?.avatarImageId])
|
||||
|
||||
// 显示加载状态
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 成员未找到
|
||||
if (!member) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground">Member not found</p>
|
||||
<Button onClick={() => router.push("/members")}>Back to List</Button>
|
||||
<p className="text-muted-foreground">未找到该成员</p>
|
||||
<Button onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}>返回列表</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -63,13 +106,14 @@ export default function MemberProfilePage() {
|
||||
const confirmed = await showConfirm("确定要删除这位成员吗?这将同时删除其关联关系。", "删除成员", "destructive")
|
||||
if (confirmed) {
|
||||
deleteMember(member.id)
|
||||
router.push("/members")
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
router.push(`/members?${params.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAddChild = () => {
|
||||
// 跳转到新增成员页面,并预填父母信息
|
||||
const params = new URLSearchParams()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (member.gender === 'MALE') {
|
||||
params.set('fatherId', member.id)
|
||||
} else {
|
||||
@@ -81,7 +125,7 @@ export default function MemberProfilePage() {
|
||||
|
||||
const handleQuickAddSpouse = () => {
|
||||
// 跳转到新增成员页面,并预填配偶信息
|
||||
const params = new URLSearchParams()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('spouseId', member.id)
|
||||
params.set('generation', member.generation.toString())
|
||||
// 配偶性别与当前成员相反
|
||||
@@ -91,7 +135,7 @@ export default function MemberProfilePage() {
|
||||
|
||||
const handleQuickAddFather = () => {
|
||||
// 跳转到新增成员页面,并预填父亲信息
|
||||
const params = new URLSearchParams()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('childId', member.id)
|
||||
params.set('generation', (member.generation - 1).toString())
|
||||
params.set('gender', 'MALE')
|
||||
@@ -102,9 +146,47 @@ export default function MemberProfilePage() {
|
||||
router.push(`/members/new?${params.toString()}`)
|
||||
}
|
||||
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
setIsUploadingAvatar(true)
|
||||
|
||||
// 上传到服务器
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || '上传失败')
|
||||
}
|
||||
|
||||
const { url } = await response.json()
|
||||
|
||||
// 更新成员头像 URL
|
||||
await updateMember(member.id, {
|
||||
avatarUrl: url,
|
||||
})
|
||||
|
||||
// 显示新头像
|
||||
setAvatarBlobUrl(url)
|
||||
} catch (error) {
|
||||
console.error('上传头像失败:', error)
|
||||
alert('上传头像失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
} finally {
|
||||
setIsUploadingAvatar(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAddMother = () => {
|
||||
// 跳转到新增成员页面,并预填母亲信息
|
||||
const params = new URLSearchParams()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('childId', member.id)
|
||||
params.set('generation', (member.generation - 1).toString())
|
||||
params.set('gender', 'FEMALE')
|
||||
@@ -120,17 +202,47 @@ export default function MemberProfilePage() {
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Button variant="ghost" onClick={() => setIsEditing(false)}>
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" onClick={() => setIsEditing(false)} className="mb-4">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> 返回
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold font-serif">编辑成员: {member.fullName}</h1>
|
||||
|
||||
{member.isFounder && (
|
||||
<div className="mb-6 p-6 bg-gradient-to-br from-amber-50 via-orange-50 to-amber-50 border-2 border-amber-400 rounded-xl shadow-md">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
|
||||
<span className="text-3xl">👑</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
|
||||
编辑家族始祖
|
||||
</h2>
|
||||
<p className="text-base text-amber-800 leading-relaxed font-serif">
|
||||
此为家族树之<span className="font-bold text-amber-900">始祖</span>,
|
||||
<span className="font-bold text-amber-900">第一世</span>传人。
|
||||
<br />
|
||||
请谨慎修改始祖信息,这将影响整个家族树的根基。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-3xl font-serif font-bold mb-2">
|
||||
{member.isFounder ? '编辑家族始祖' : `编辑成员: ${member.fullName}`}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{member.isFounder
|
||||
? '修改始祖信息将影响整个家族树'
|
||||
: '更新成员信息'}
|
||||
</p>
|
||||
</div>
|
||||
<MemberForm
|
||||
initialData={member}
|
||||
existingMembers={allMembers}
|
||||
onSubmit={handleSave}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
isFounder={member.isFounder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,32 +253,92 @@ export default function MemberProfilePage() {
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<main className="container mx-auto py-8 px-4 md:px-6">
|
||||
{/* 始祖特殊提示框 */}
|
||||
{member.isFounder && (
|
||||
<div className="mb-6 p-6 bg-gradient-to-br from-amber-50 via-orange-50 to-amber-50 border-2 border-amber-400 rounded-xl shadow-md">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
|
||||
<span className="text-3xl">👑</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
|
||||
家族始祖
|
||||
</h2>
|
||||
<p className="text-base text-amber-800 leading-relaxed font-serif">
|
||||
此为家族树之<span className="font-bold text-amber-900">始祖</span>,
|
||||
<span className="font-bold text-amber-900">第一世</span>传人。
|
||||
<br />
|
||||
血脉由此而始,世系因此而传。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start gap-6 mb-8 border-b border-border pb-8">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="h-32 w-32 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
|
||||
{avatarBlobUrl ? (
|
||||
<img
|
||||
src={avatarBlobUrl}
|
||||
alt={member.fullName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
member.gender === 'FEMALE' ? (
|
||||
<CircleUserRound className="h-20 w-20 text-pink-400" />
|
||||
<div className="relative group">
|
||||
<div className="h-32 w-32 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
|
||||
{avatarBlobUrl ? (
|
||||
<img
|
||||
src={avatarBlobUrl}
|
||||
alt={member.fullName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<CircleUser className="h-20 w-20 text-blue-400" />
|
||||
)
|
||||
member.gender === 'FEMALE' ? (
|
||||
<CircleUserRound className="h-20 w-20 text-pink-400" />
|
||||
) : (
|
||||
<CircleUser className="h-20 w-20 text-blue-400" />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{permissions.canUpdate(role) && (
|
||||
<label className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer rounded-lg">
|
||||
<Camera className="h-8 w-8 text-white" />
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleAvatarUpload}
|
||||
disabled={isUploadingAvatar}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
{isUploadingAvatar && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-lg">
|
||||
<div className="text-white text-xs">上传中...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-4xl font-bold font-serif text-foreground">{member.fullName}</h1>
|
||||
<span className="px-2 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full border border-primary/20">
|
||||
{member.generation}世
|
||||
</span>
|
||||
<h1 className="text-4xl font-bold font-serif text-foreground flex items-center gap-2">
|
||||
{member.isFounder && <span className="text-amber-600 text-3xl">👑</span>}
|
||||
<MemberNameWithStatus
|
||||
name={member.fullName}
|
||||
isDead={!!member.deathDate}
|
||||
/>
|
||||
</h1>
|
||||
{member.isFounder ? (
|
||||
<span className="px-3 py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-sm font-semibold rounded-full border-2 border-amber-300 shadow-sm">
|
||||
始祖 · {member.generation}世
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full border border-primary/20">
|
||||
{member.generation}世
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1 text-muted-foreground">
|
||||
{member.isFounder && (
|
||||
<p className="text-amber-700 font-semibold text-sm flex items-center gap-1">
|
||||
<span>家族始祖</span>
|
||||
<span className="text-amber-600">•</span>
|
||||
<span>家族树的根基</span>
|
||||
</p>
|
||||
)}
|
||||
{member.courtesyName && (
|
||||
<p>
|
||||
字:<span className="text-foreground">{member.courtesyName}</span>
|
||||
@@ -197,12 +369,12 @@ export default function MemberProfilePage() {
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{permissions.canCreate(role) && (
|
||||
<>
|
||||
{!member.fatherId && (
|
||||
{!member.isFounder && !member.fatherId && (
|
||||
<Button variant="outline" onClick={handleQuickAddFather} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加父亲
|
||||
</Button>
|
||||
)}
|
||||
{!member.motherId && (
|
||||
{!member.isFounder && !member.motherId && (
|
||||
<Button variant="outline" onClick={handleQuickAddMother} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加母亲
|
||||
</Button>
|
||||
@@ -236,14 +408,222 @@ export default function MemberProfilePage() {
|
||||
|
||||
{/* Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Biography */}
|
||||
{/* Left Column - Details and Biography */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* 家族关系 */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-secondary rounded-full"></span>
|
||||
家族关系
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
父亲
|
||||
</h3>
|
||||
{member.fatherId ? (
|
||||
<Link
|
||||
href={`/members/${member.fatherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(member.fatherId)?.fullName || member.fatherId}
|
||||
isDead={!!getMember(member.fatherId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
) : member.spouseFatherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseFatherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
母亲
|
||||
</h3>
|
||||
{member.motherId ? (
|
||||
<Link
|
||||
href={`/members/${member.motherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(member.motherId)?.fullName || member.motherId}
|
||||
isDead={!!getMember(member.motherId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
) : member.spouseMotherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseMotherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
配偶
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.spouseIds?.length > 0 ? (
|
||||
member.spouseIds.map((spouseId) => (
|
||||
<Link
|
||||
href={`/members/${spouseId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
key={spouseId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(spouseId)?.fullName || spouseId}
|
||||
isDead={!!getMember(spouseId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
子女
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.childrenIds?.length > 0 ? (
|
||||
member.childrenIds.map((childId) => (
|
||||
<Link
|
||||
href={`/members/${childId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
key={childId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(childId)?.fullName || childId}
|
||||
isDead={!!getMember(childId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-secondary rounded-full"></span>
|
||||
详细信息
|
||||
</h2>
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}>
|
||||
<dl className="grid grid-cols-2 gap-x-8 gap-y-4 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">出生日期</dt>
|
||||
<dd className="font-medium flex items-center gap-2">
|
||||
{member.birthDate || "-"}
|
||||
{member.birthDate && member.isLunarDate && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
{ageInfo && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">{ageInfo.type === 'deceased' ? '享年' : '年龄'}</dt>
|
||||
<dd className="font-medium text-primary">{ageInfo.age} 岁</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.deathDate && (
|
||||
<>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">逝世日期</dt>
|
||||
<dd className="font-medium flex items-center gap-2">
|
||||
{member.deathDate}
|
||||
{member.isLunarDate && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">谥号</dt>
|
||||
<dd className="font-medium">{member.posthumousName || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">墓地</dt>
|
||||
<dd className="font-medium">{member.burialPlace || "-"}</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{member.generationName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">字辈</dt>
|
||||
<dd className="font-medium">{member.generationName}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 联系方式 */}
|
||||
{(member.phone || member.telephone || member.email || member.address) && (
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-secondary rounded-full"></span>
|
||||
联系方式
|
||||
</h2>
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}>
|
||||
<dl className="space-y-4 text-sm">
|
||||
{member.phone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">手机号</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`tel:${member.phone}`} className="hover:text-primary">
|
||||
{member.phone}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.telephone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">固定电话</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`tel:${member.telephone}`} className="hover:text-primary">
|
||||
{member.telephone}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.email && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">邮箱</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`mailto:${member.email}`} className="hover:text-primary">
|
||||
{member.email}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.address && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">现居地址</dt>
|
||||
<dd className="font-medium">{member.address}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-primary rounded-full"></span>
|
||||
生平事迹
|
||||
</h2>
|
||||
<div className="prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg border border-border shadow-sm">
|
||||
<div className={`prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm`}>
|
||||
{member.bio ? (
|
||||
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
|
||||
) : (
|
||||
@@ -259,7 +639,7 @@ export default function MemberProfilePage() {
|
||||
<Images className="h-6 w-6 text-primary" />
|
||||
照片墙
|
||||
</h2>
|
||||
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
|
||||
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm`}>
|
||||
<PhotoGallery
|
||||
photoIds={member.photoIds}
|
||||
onChange={(photoIds) => updateMember(member.id, { photoIds })}
|
||||
@@ -276,7 +656,7 @@ export default function MemberProfilePage() {
|
||||
<BookOpen className="h-6 w-6 text-primary" />
|
||||
家族故事
|
||||
</h2>
|
||||
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
|
||||
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm`}>
|
||||
<StoryManager
|
||||
stories={member.stories}
|
||||
onChange={(stories) => updateMember(member.id, { stories })}
|
||||
@@ -285,174 +665,21 @@ export default function MemberProfilePage() {
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Version History */}
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-secondary rounded-full"></span>
|
||||
家族关系
|
||||
版本历史
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
父亲 (Father)
|
||||
</h3>
|
||||
{member.fatherId ? (
|
||||
<Link
|
||||
href={`/members/${member.fatherId}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
{getMember(member.fatherId)?.fullName || member.fatherId}
|
||||
</Link>
|
||||
) : member.spouseFatherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseFatherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
母亲 (Mother)
|
||||
</h3>
|
||||
{member.motherId ? (
|
||||
<Link
|
||||
href={`/members/${member.motherId}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
{getMember(member.motherId)?.fullName || member.motherId}
|
||||
</Link>
|
||||
) : member.spouseMotherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseMotherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
配偶 (Spouse)
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.spouseIds?.length > 0 ? (
|
||||
member.spouseIds.map((spouseId) => (
|
||||
<Link
|
||||
href={`/members/${spouseId}`}
|
||||
key={spouseId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
{getMember(spouseId)?.fullName || spouseId}
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
子女 (Children)
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.childrenIds?.length > 0 ? (
|
||||
member.childrenIds.map((childId) => (
|
||||
<Link
|
||||
href={`/members/${childId}`}
|
||||
key={childId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
{getMember(childId)?.fullName || childId}
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MemberVersionHistory
|
||||
memberId={member.id}
|
||||
treeId={currentTree?.id || ''}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Info */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm">
|
||||
<h3 className="font-serif font-bold text-lg mb-4 border-b border-border pb-2">详细信息</h3>
|
||||
<dl className="space-y-4 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground">出生日期</dt>
|
||||
<dd className="font-medium">{member.birthDate || "-"}</dd>
|
||||
</div>
|
||||
{member.deathDate && (
|
||||
<>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">逝世日期</dt>
|
||||
<dd className="font-medium">{member.deathDate}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">谥号</dt>
|
||||
<dd className="font-medium">{member.posthumousName || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">墓地</dt>
|
||||
<dd className="font-medium">{member.burialPlace || "-"}</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{member.generationName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">字辈</dt>
|
||||
<dd className="font-medium">{member.generationName}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* 联系方式 */}
|
||||
{(member.phone || member.telephone || member.email || member.address) && (
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm">
|
||||
<h3 className="font-serif font-bold text-lg mb-4 border-b border-border pb-2">联系方式</h3>
|
||||
<dl className="space-y-4 text-sm">
|
||||
{member.phone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">手机号</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`tel:${member.phone}`} className="hover:text-primary">
|
||||
{member.phone}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.telephone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">固定电话</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`tel:${member.telephone}`} className="hover:text-primary">
|
||||
{member.telephone}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.email && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">邮箱</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`mailto:${member.email}`} className="hover:text-primary">
|
||||
{member.email}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.address && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">现居地址</dt>
|
||||
<dd className="font-medium">{member.address}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function NewMemberPage() {
|
||||
const { addMember, treeData, getMember } = useFamily()
|
||||
|
||||
const existingMembers = Object.values(treeData.members)
|
||||
const isFirstMember = existingMembers.length === 0
|
||||
|
||||
// 从URL参数获取预填信息
|
||||
const initialData = useMemo(() => {
|
||||
@@ -73,13 +74,23 @@ export default function NewMemberPage() {
|
||||
|
||||
const handleSave = async (newMember: FamilyMember) => {
|
||||
try {
|
||||
await addMember({
|
||||
// 如果是第一个成员,自动设置为始祖
|
||||
const memberData = isFirstMember ? {
|
||||
...newMember,
|
||||
generation: 1,
|
||||
isFounder: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} : {
|
||||
...newMember,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
// 跳转到成员列表页
|
||||
router.push('/members')
|
||||
}
|
||||
|
||||
await addMember(memberData)
|
||||
// 跳转到成员列表页,保留treeId参数
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
router.push(`/members?${params.toString()}`)
|
||||
} catch (error) {
|
||||
console.error('添加成员失败:', error)
|
||||
alert('添加成员失败: ' + (error as Error).message)
|
||||
@@ -94,12 +105,44 @@ export default function NewMemberPage() {
|
||||
<Button variant="ghost" onClick={() => router.back()} className="mb-4">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> 返回
|
||||
</Button>
|
||||
<h1 className="text-3xl font-serif font-bold mb-2">新增家族成员</h1>
|
||||
|
||||
{isFirstMember && (
|
||||
<div className="mb-6 p-6 bg-gradient-to-br from-amber-50 via-orange-50 to-amber-50 border-2 border-amber-400 rounded-xl shadow-md">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
|
||||
<span className="text-3xl">👑</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
|
||||
立家族之根基
|
||||
</h2>
|
||||
<p className="text-base text-amber-800 leading-relaxed font-serif">
|
||||
此为家族树之<span className="font-bold text-amber-900">始祖</span>,
|
||||
<span className="font-bold text-amber-900">第一世</span>传人。
|
||||
<br />
|
||||
血脉由此而始,世系因此而传。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h1 className="text-3xl font-serif font-bold mb-2">
|
||||
{isFirstMember ? '添加家族始祖' : '新增家族成员'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
填写成员信息,完善家族树
|
||||
{isFirstMember
|
||||
? '请认真填写始祖信息,这将是家族历史的起点'
|
||||
: '填写成员信息,完善家族树'}
|
||||
</p>
|
||||
</div>
|
||||
<MemberForm initialData={initialData} existingMembers={existingMembers} onSubmit={handleSave} onCancel={() => router.back()} />
|
||||
<MemberForm
|
||||
initialData={initialData}
|
||||
existingMembers={existingMembers}
|
||||
onSubmit={handleSave}
|
||||
onCancel={() => router.back()}
|
||||
isFounder={isFirstMember}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+42
-34
@@ -17,6 +17,7 @@ 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"
|
||||
|
||||
// 占位符类型
|
||||
interface PlaceholderMember {
|
||||
@@ -72,7 +73,7 @@ const MemberCard = memo(function MemberCard({
|
||||
onLocateInTree: (id: string) => void
|
||||
}) {
|
||||
// 判断是否是配偶
|
||||
const isFounder = member.tags?.includes('始祖')
|
||||
const isFounder = member.isFounder === true
|
||||
const isSpouse = !member.fatherId && !member.motherId &&
|
||||
member.spouseIds && member.spouseIds.length > 0 &&
|
||||
!isFounder
|
||||
@@ -85,7 +86,7 @@ const MemberCard = memo(function MemberCard({
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<Link href={`/members/${member.id}`}>
|
||||
<Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}>
|
||||
<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'
|
||||
@@ -94,24 +95,21 @@ const MemberCard = memo(function MemberCard({
|
||||
<CardContent className="p-3 flex items-center gap-5">
|
||||
<div className="relative flex-shrink-0">
|
||||
<AvatarDisplay
|
||||
imageId={member.avatarImageId}
|
||||
fallbackUrl={member.avatarUrl}
|
||||
imageUrl={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}
|
||||
<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}
|
||||
@@ -120,7 +118,7 @@ const MemberCard = memo(function MemberCard({
|
||||
</h3>
|
||||
</div>
|
||||
{isFounder ? (
|
||||
<p className="text-xs text-amber-700 font-medium mt-0.5 truncate">
|
||||
<p className="text-xs text-amber-700 font-semibold mt-0.5 truncate">
|
||||
家族始祖
|
||||
</p>
|
||||
) : isSpouse && spouseName ? (
|
||||
@@ -140,10 +138,17 @@ const MemberCard = memo(function MemberCard({
|
||||
{/* 生卒年和年龄 */}
|
||||
{(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 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
|
||||
@@ -165,9 +170,11 @@ const MemberCard = memo(function MemberCard({
|
||||
{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"
|
||||
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 === '始祖' ? '👑 始祖' : tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -303,7 +310,7 @@ export default function MembersPage() {
|
||||
.map(([generation, members]) => {
|
||||
// 区分家族成员和配偶
|
||||
const familyMembers = members.filter(m => {
|
||||
const isFounder = m.tags?.includes('始祖')
|
||||
const isFounder = m.isFounder === true
|
||||
const hasFamilyParents = m.fatherId || m.motherId
|
||||
|
||||
if (hasFamilyParents || isFounder) return true
|
||||
@@ -314,8 +321,8 @@ export default function MembersPage() {
|
||||
|
||||
// 对家族成员按年龄排序
|
||||
const sortedFamilyMembers = familyMembers.sort((a, b) => {
|
||||
const aIsFounder = a.tags?.includes('始祖')
|
||||
const bIsFounder = b.tags?.includes('始祖')
|
||||
const aIsFounder = a.isFounder === true
|
||||
const bIsFounder = b.isFounder === true
|
||||
if (aIsFounder && !bIsFounder) return -1
|
||||
if (!aIsFounder && bIsFounder) return 1
|
||||
|
||||
@@ -388,7 +395,7 @@ export default function MembersPage() {
|
||||
)}
|
||||
</Button>
|
||||
{permissions.canCreate(currentTree?.currentUserRole) && (
|
||||
<Link href="/members/new">
|
||||
<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" />
|
||||
新增成员
|
||||
@@ -409,13 +416,13 @@ export default function MembersPage() {
|
||||
</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">姓氏 (Surname)</Label>
|
||||
<Label className="text-xs">姓氏</Label>
|
||||
<Select value={selectedSurname} onValueChange={setSelectedSurname}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="全部" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部 (All)</SelectItem>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
{uniqueSurnames.map((s) => (
|
||||
<SelectItem key={s} value={s}>
|
||||
{s}
|
||||
@@ -426,13 +433,13 @@ export default function MembersPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">籍贯 (Ancestral Home)</Label>
|
||||
<Label className="text-xs">籍贯</Label>
|
||||
<Select value={selectedHome} onValueChange={setSelectedHome}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="全部" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部 (All)</SelectItem>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
{uniqueHomes.map((h) => (
|
||||
<SelectItem key={h} value={h}>
|
||||
{h}
|
||||
@@ -443,22 +450,22 @@ export default function MembersPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">状态 (Status)</Label>
|
||||
<Label className="text-xs">状态</Label>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="全部" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部 (All)</SelectItem>
|
||||
<SelectItem value="living">在世 (Living)</SelectItem>
|
||||
<SelectItem value="deceased">已故 (Deceased)</SelectItem>
|
||||
<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">世系范围 (Generation)</Label>
|
||||
<Label className="text-xs">世系范围</Label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{generationRange[0]} - {generationRange[1]} 世
|
||||
</span>
|
||||
@@ -511,7 +518,8 @@ export default function MembersPage() {
|
||||
allMembers={allMembers}
|
||||
onLocateInTree={(id) => {
|
||||
setHighlightedMemberId(id)
|
||||
router.push('/tree')
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
router.push(`/tree?${params.toString()}`)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user