Files
chinese-family-tree-2/app/members/[id]/page.tsx
T
freedakgmail 3b0d8255f6 0.0.8.1
2025-11-23 19:38:51 +08:00

461 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
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 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 { treeData, currentTree, getMember, updateMember, deleteMember } = useFamily()
const role = currentTree?.currentUserRole
const { showConfirm } = useDialog()
const router = useRouter()
const [isEditing, setIsEditing] = useState(false)
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | undefined>(undefined)
const member = getMember(id as string)
const allMembers = Object.values(treeData.members)
// Load avatar blob if exists
useEffect(() => {
if (member?.avatarImageId) {
db.images.get(member.avatarImageId).then((image) => {
if (image) {
const url = URL.createObjectURL(image.blob)
setAvatarBlobUrl(url)
return () => URL.revokeObjectURL(url)
}
})
} else {
setAvatarBlobUrl(undefined)
}
}, [member?.avatarImageId])
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>
</div>
</div>
)
}
const handleSave = (updatedMember: FamilyMember) => {
updateMember(updatedMember.id, updatedMember)
setIsEditing(false)
}
const handleDelete = async () => {
const confirmed = await showConfirm("确定要删除这位成员吗?这将同时删除其关联关系。", "删除成员", "destructive")
if (confirmed) {
deleteMember(member.id)
router.push("/members")
}
}
const handleQuickAddChild = () => {
// 跳转到新增成员页面,并预填父母信息
const params = new URLSearchParams()
if (member.gender === 'MALE') {
params.set('fatherId', member.id)
} else {
params.set('motherId', member.id)
}
params.set('generation', (member.generation + 1).toString())
router.push(`/members/new?${params.toString()}`)
}
const handleQuickAddSpouse = () => {
// 跳转到新增成员页面,并预填配偶信息
const params = new URLSearchParams()
params.set('spouseId', member.id)
params.set('generation', member.generation.toString())
// 配偶性别与当前成员相反
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
router.push(`/members/new?${params.toString()}`)
}
const handleQuickAddFather = () => {
// 跳转到新增成员页面,并预填父亲信息
const params = new URLSearchParams()
params.set('childId', member.id)
params.set('generation', (member.generation - 1).toString())
params.set('gender', 'MALE')
// 如果已有母亲,将母亲设置为配偶
if (member.motherId) {
params.set('spouseId', member.motherId)
}
router.push(`/members/new?${params.toString()}`)
}
const handleQuickAddMother = () => {
// 跳转到新增成员页面,并预填母亲信息
const params = new URLSearchParams()
params.set('childId', member.id)
params.set('generation', (member.generation - 1).toString())
params.set('gender', 'FEMALE')
// 如果已有父亲,将父亲设置为配偶
if (member.fatherId) {
params.set('spouseId', member.fatherId)
}
router.push(`/members/new?${params.toString()}`)
}
if (isEditing) {
return (
<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)}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold font-serif">: {member.fullName}</h1>
</div>
<MemberForm
initialData={member}
existingMembers={allMembers}
onSubmit={handleSave}
onCancel={() => setIsEditing(false)}
/>
</div>
</div>
)
}
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6">
{/* 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" />
) : (
<CircleUser className="h-20 w-20 text-blue-400" />
)
)}
</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>
</div>
<div className="space-y-1 text-muted-foreground">
{member.courtesyName && (
<p>
<span className="text-foreground">{member.courtesyName}</span>
</p>
)}
{member.artName && (
<p>
<span className="text-foreground">{member.artName}</span>
</p>
)}
{member.ancestralHome && <p>{member.ancestralHome}</p>}
</div>
{member.tags && member.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-3">
{member.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 bg-primary/10 text-primary text-xs font-medium rounded-md border border-primary/20"
>
{tag}
</span>
))}
</div>
)}
</div>
</div>
<div className="flex gap-2 flex-wrap">
{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>
</>
)}
<Button variant="outline" size="icon" onClick={() => window.print()} title="打印">
<Printer className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" title="分享">
<Share2 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>
{/* Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main 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-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">
{member.bio ? (
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
) : (
<p className="text-sm text-foreground font-light tracking-wide text-center py-8"></p>
)}
</div>
</section>
{/* Photo Gallery */}
{member.photoIds && member.photoIds.length > 0 && (
<section>
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
<Images className="h-6 w-6 text-primary" />
</h2>
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
<PhotoGallery
photoIds={member.photoIds}
onChange={(photoIds) => updateMember(member.id, { photoIds })}
readonly={false}
/>
</div>
</section>
)}
{/* Family Stories */}
{member.stories && member.stories.length > 0 && (
<section>
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
<BookOpen className="h-6 w-6 text-primary" />
</h2>
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
<StoryManager
stories={member.stories}
onChange={(stories) => updateMember(member.id, { stories })}
readonly={false}
/>
</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="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>
</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>
)
}