388 lines
16 KiB
TypeScript
388 lines
16 KiB
TypeScript
"use client"
|
||
|
||
import Link from "next/link"
|
||
|
||
import { useParams, useRouter } from "next/navigation"
|
||
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 } 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"
|
||
|
||
export default function MemberProfilePage() {
|
||
const { id } = useParams()
|
||
const { getMember, updateMember, deleteMember, treeData } = useFamily()
|
||
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")
|
||
}
|
||
}
|
||
|
||
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">
|
||
<Button variant="outline" size="icon" onClick={() => window.print()}>
|
||
<Printer className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon">
|
||
<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>
|
||
</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-muted-foreground italic">暂无生平记录...</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>
|
||
)
|
||
}
|