Files
chinese-family-tree-2/app/members/[id]/page.tsx
T
freedakgmail b25f4bea02 0.0.9.0
2025-11-24 16:07:24 +08:00

685 lines
29 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, Camera } from "lucide-react"
import type { FamilyMember } from "@/types/family"
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"
import { useSession } from "next-auth/react"
export default function MemberProfilePage() {
const { id } = useParams()
const { treeData, currentTree, getMember, updateMember, deleteMember, isLoading } = useFamily()
const role = currentTree?.currentUserRole
const { showConfirm } = useDialog()
const router = useRouter()
const { data: session, status } = useSession()
const [isEditing, setIsEditing] = useState(false)
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | undefined>(undefined)
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
// 未登录时重定向到登录页
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin')
}
}, [status, router])
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
useEffect(() => {
if (member?.avatarUrl) {
setAvatarBlobUrl(member.avatarUrl)
} else {
setAvatarBlobUrl(undefined)
}
}, [member?.avatarUrl])
// 显示加载状态
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"></p>
<Button onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}></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)
const params = new URLSearchParams(window.location.search)
router.push(`/members?${params.toString()}`)
}
}
const handleQuickAddChild = () => {
// 跳转到新增成员页面,并预填父母信息
const params = new URLSearchParams(window.location.search)
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(window.location.search)
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(window.location.search)
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 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(window.location.search)
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">
<Button variant="ghost" onClick={() => setIsEditing(false)} className="mb-4">
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
{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)}
isFounder={member.isFounder}
/>
</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">
{/* 始祖特殊提示框 */}
{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="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"
/>
) : (
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 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>
</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.isFounder && !member.fatherId && (
<Button variant="outline" onClick={handleQuickAddFather} className="gap-2">
<UserPlus className="h-4 w-4" />
</Button>
)}
{!member.isFounder && !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">
{/* 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 ${founderBorderClass} 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 ${founderBorderClass} 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 ${founderBorderClass} shadow-sm`}>
<StoryManager
stories={member.stories}
onChange={(stories) => updateMember(member.id, { stories })}
readonly={false}
/>
</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>
<MemberVersionHistory
memberId={member.id}
treeId={currentTree?.id || ''}
/>
</section>
</div>
</div>
</main>
</div>
)
}