0.3.0.2
This commit is contained in:
+430
-423
@@ -7,6 +7,7 @@ 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 { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||
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"
|
||||
@@ -253,458 +254,464 @@ 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">
|
||||
{/* 返回按钮 */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}
|
||||
className="gap-2 pl-0 hover:bg-transparent hover:text-primary text-2xl font-bold"
|
||||
>
|
||||
<ArrowLeft className="h-6 w-6" />
|
||||
返回成员列表
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs defaultValue="family" className="flex flex-col">
|
||||
{/* 固定头部区域 - top-16 对应 SiteHeader 的 h-16 高度 */}
|
||||
<div className="sticky top-16 z-30 bg-background pb-4 -mx-4 px-4 md:-mx-6 md:px-6 border-b border-border -mt-8 pt-8">
|
||||
{/* 返回按钮 */}
|
||||
<div className="py-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}
|
||||
className="gap-2 pl-0 hover:bg-transparent hover:text-primary text-2xl font-bold"
|
||||
>
|
||||
<ArrowLeft className="h-6 w-6" />
|
||||
返回成员列表
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 始祖特殊提示框 */}
|
||||
{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>
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="relative group">
|
||||
<div className="h-24 w-24 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-16 w-16 text-pink-400" />
|
||||
) : (
|
||||
<CircleUser className="h-16 w-16 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-6 w-6 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>
|
||||
<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="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl font-bold font-serif text-foreground flex items-center gap-2">
|
||||
{member.isFounder && <span className="text-amber-600 text-2xl">👑</span>}
|
||||
<MemberNameWithStatus
|
||||
name={member.fullName}
|
||||
isDead={!!member.deathDate}
|
||||
memberId={member.id}
|
||||
treeId={currentTree?.id}
|
||||
/>
|
||||
</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="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||
{member.courtesyName && <span>字:{member.courtesyName}</span>}
|
||||
{member.artName && <span>号:{member.artName}</span>}
|
||||
{member.generationName && <span>字辈:{member.generationName}</span>}
|
||||
{ageInfo && <span>{ageInfo.type === 'deceased' ? '享年' : '年龄'}:{ageInfo.age}岁</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{permissions.canCreate(role) && (
|
||||
<>
|
||||
{!member.isFounder && !member.fatherId && (
|
||||
<Button variant="outline" size="sm" onClick={handleQuickAddFather} className="gap-1">
|
||||
<UserPlus className="h-4 w-4" /> 添加父亲
|
||||
</Button>
|
||||
)}
|
||||
{!member.isFounder && !member.motherId && (
|
||||
<Button variant="outline" size="sm" onClick={handleQuickAddMother} className="gap-1">
|
||||
<UserPlus className="h-4 w-4" /> 添加母亲
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="default" size="sm" onClick={handleQuickAddChild} className="gap-1">
|
||||
<UserPlus className="h-4 w-4" /> 添加子女
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={handleQuickAddSpouse} className="gap-1">
|
||||
<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" size="sm" onClick={() => setIsEditing(true)} className="gap-1">
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
{/* Tab 导航栏 */}
|
||||
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
|
||||
<TabsTrigger value="family">家族关系</TabsTrigger>
|
||||
<TabsTrigger value="details">详细信息</TabsTrigger>
|
||||
<TabsTrigger value="life">生平影像</TabsTrigger>
|
||||
<TabsTrigger value="stories">家族故事</TabsTrigger>
|
||||
<TabsTrigger value="history">时间记录</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 始祖特殊提示框 */}
|
||||
{member.isFounder && (
|
||||
<div className="mt-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>
|
||||
)}
|
||||
|
||||
{/* Tab 内容区域 */}
|
||||
<TabsContent value="family" className="mt-6 space-y-6">
|
||||
<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-3 text-sm uppercase tracking-wider">
|
||||
父母
|
||||
</h3>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">父亲</p>
|
||||
{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>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">母亲</p>
|
||||
{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>
|
||||
</div>
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider">
|
||||
配偶与子女
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">配偶</p>
|
||||
<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>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">子女</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="details" className="mt-6 space-y-6">
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">详细信息</h3>
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6 gap-x-8 gap-y-4 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">生卒</dt>
|
||||
<dd className="font-medium space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>出生:{member.birthDate || "-"}</span>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>逝世:{member.deathDate || "-"}</span>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</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">{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>
|
||||
)}
|
||||
{member.courtesyName && (
|
||||
<p>
|
||||
字:<span className="text-foreground">{member.courtesyName}</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">字</dt>
|
||||
<dd className="font-medium">{member.courtesyName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.artName && (
|
||||
<p>
|
||||
号:<span className="text-foreground">{member.artName}</span>
|
||||
</p>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">号</dt>
|
||||
<dd className="font-medium">{member.artName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.ancestralHome && <p>籍贯:{member.ancestralHome}</p>}
|
||||
</div>
|
||||
{member.tags && member.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{member.ancestralHome && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">籍贯</dt>
|
||||
<dd className="font-medium">{member.ancestralHome}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.birthPlace && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">出生地</dt>
|
||||
<dd className="font-medium">{member.birthPlace}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.address && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">现居地址</dt>
|
||||
<dd className="font-medium">{member.address}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.phone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">手机号</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 mb-1">固定电话</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 mb-1">邮箱</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`mailto:${member.email}`} className="hover:text-primary">
|
||||
{member.email}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
{member.tags && member.tags.length > 0 && (
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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"
|
||||
className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-md border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="life" className="mt-6 space-y-6">
|
||||
{/* 生平事迹 */}
|
||||
<div className={`prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">生平事迹</h3>
|
||||
{member.bio ? (
|
||||
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">生平待书,欢迎补充这段故事。</p>
|
||||
)}
|
||||
</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-3 text-sm uppercase tracking-wider">
|
||||
父母
|
||||
</h3>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">父亲</p>
|
||||
{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>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">母亲</p>
|
||||
{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>
|
||||
{/* 影像记忆 */}
|
||||
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||||
<Images className="h-5 w-5 text-primary" />
|
||||
影像记忆
|
||||
</h3>
|
||||
{member.photoIds && member.photoIds.length > 0 ? (
|
||||
<PhotoGallery
|
||||
photoIds={member.photoIds}
|
||||
onChange={async (photoIds) => {
|
||||
await updateMember(member.id, { photoIds })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
readonly={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center py-10">
|
||||
暂无照片记录,欢迎补充珍贵影像。
|
||||
</div>
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider">
|
||||
配偶与子女
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">配偶</p>
|
||||
<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>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">子女</p>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stories" className="mt-6 space-y-6">
|
||||
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||||
<BookOpen className="h-5 w-5 text-primary" />
|
||||
家族故事
|
||||
</h3>
|
||||
{member.stories && member.stories.length > 0 ? (
|
||||
<StoryManager
|
||||
stories={member.stories}
|
||||
onChange={async (stories) => {
|
||||
await updateMember(member.id, { stories })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
readonly={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center py-10">
|
||||
暂无故事记录,期待您书写这段家族记忆。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<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-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6 gap-x-8 gap-y-4 text-sm">
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">生卒</dt>
|
||||
<dd className="font-medium space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>出生:{member.birthDate || "-"}</span>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>逝世:{member.deathDate || "-"}</span>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</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">{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={async (photoIds) => {
|
||||
await updateMember(member.id, { photoIds })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
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={async (stories) => {
|
||||
await updateMember(member.id, { stories })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
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>
|
||||
<TabsContent value="history" className="mt-6 space-y-6">
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-2">最近更新</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
最后保存于 {new Date(lastSaveTime).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">版本历史</h3>
|
||||
<MemberVersionHistory
|
||||
memberId={member.id}
|
||||
treeId={currentTree?.id || ''}
|
||||
lastUpdated={lastSaveTime}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -86,6 +86,10 @@ const MemberCard = memo(function MemberCard({
|
||||
: null
|
||||
const spouseName = spouse ? spouse.fullName : null
|
||||
|
||||
// 获取父母信息
|
||||
const father = member.fatherId ? allMembers.find(m => m.id === member.fatherId) : null
|
||||
const mother = member.motherId ? allMembers.find(m => m.id === member.motherId) : null
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}>
|
||||
@@ -118,6 +122,45 @@ const MemberCard = memo(function MemberCard({
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
{/* 父母信息 - 放在姓名右侧 */}
|
||||
{(father || mother) && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground flex-shrink-0">
|
||||
<span className="text-muted-foreground/70">父母:</span>
|
||||
{father && (
|
||||
<span
|
||||
className="hover:text-primary hover:underline cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
window.location.href = `/members/${father.id}${window.location.search}`
|
||||
}}
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={father.fullName}
|
||||
isDead={!!father.deathDate}
|
||||
dotPosition="top-left"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{father && mother && <span className="text-muted-foreground/50">·</span>}
|
||||
{mother && (
|
||||
<span
|
||||
className="hover:text-primary hover:underline cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
window.location.href = `/members/${mother.id}${window.location.search}`
|
||||
}}
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={mother.fullName}
|
||||
isDead={!!mother.deathDate}
|
||||
dotPosition="top-left"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isFounder ? (
|
||||
<p className="text-xs text-amber-700 font-semibold mt-0.5 truncate">
|
||||
|
||||
Reference in New Issue
Block a user