939 lines
44 KiB
TypeScript
939 lines
44 KiB
TypeScript
"use client"
|
||
|
||
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card"
|
||
import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus, Camera, History } 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"
|
||
// useDialog 已导入,用于 showAlert 和 showConfirm
|
||
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"
|
||
import { useEffect, useState, useCallback } from "react"
|
||
import { useParams, useRouter } from "next/navigation"
|
||
import Link from "next/link"
|
||
import { useFamily } from "@/context/family-context"
|
||
import { BackButton } from "@/components/ui/back-button"
|
||
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 { AddRelationDialog, type RelationType } from "@/components/tree/add-relation-dialog"
|
||
|
||
export default function MemberProfilePage() {
|
||
const { id } = useParams()
|
||
const { treeData, currentTree, getMember, updateMember, deleteMember, isLoading } = useFamily()
|
||
const { showConfirm, showAlert } = useDialog()
|
||
const router = useRouter()
|
||
const { data: session, status } = useSession()
|
||
const [isEditing, setIsEditing] = useState(false)
|
||
const [editActiveTab, setEditActiveTab] = useState("info")
|
||
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | undefined>(undefined)
|
||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||
const [lastSaveTime, setLastSaveTime] = useState(Date.now())
|
||
|
||
// 添加成员对话框状态
|
||
const [relationDialog, setRelationDialog] = useState<{
|
||
type: RelationType
|
||
} | null>(null)
|
||
|
||
// 未登录时重定向到登录页
|
||
useEffect(() => {
|
||
if (status === 'unauthenticated') {
|
||
router.push('/auth/signin')
|
||
}
|
||
}, [status, router])
|
||
|
||
const member = getMember(id as string)
|
||
const allMembers = Object.values(treeData.members)
|
||
|
||
// 判断是否已故
|
||
const isDead = !!member?.deathDate
|
||
|
||
// 根据在世/已故状态设置卡片样式
|
||
// 优先级:始祖 > 已故 > 在世
|
||
const cardStyle = member?.isFounder
|
||
? '!border-2 !border-amber-400 !bg-amber-50/30 dark:!bg-amber-900/20 shadow-md' // 始祖:金色边框(优先级最高)
|
||
: isDead
|
||
? '!border-2 !border-gray-400 dark:!border-gray-600 !bg-gray-50/50 dark:!bg-gray-900/30' // 已故:灰色深边框
|
||
: '!border-2 !border-amber-300/70 !bg-card dark:!border-amber-600/50' // 在世:浅金色边框
|
||
|
||
// 计算年龄或享年
|
||
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()
|
||
|
||
// 获取用户角色
|
||
const role = currentTree?.currentUserRole
|
||
|
||
// Load avatar
|
||
useEffect(() => {
|
||
if (member?.avatarUrl) {
|
||
setAvatarBlobUrl(member.avatarUrl)
|
||
} else {
|
||
setAvatarBlobUrl(undefined)
|
||
}
|
||
}, [member?.avatarUrl])
|
||
|
||
// 姓名分解函数
|
||
const parseName = (fullName: string): { surname: string; givenName: string } => {
|
||
const name = fullName.trim()
|
||
if (!name) return { surname: '', givenName: '' }
|
||
|
||
const compoundSurnames = [
|
||
'欧阳', '太史', '端木', '上官', '司马', '东方', '独孤', '南宫', '万俟', '闻人',
|
||
'夏侯', '诸葛', '尉迟', '公羊', '赫连', '澹台', '皇甫', '宗政', '濮阳', '公冶',
|
||
'太叔', '申屠', '公孙', '慕容', '仲孙', '钟离', '长孙', '宇文', '司徒', '鲜于',
|
||
'司空', '闾丘', '子车', '亓官', '司寇', '巫马', '公西', '颛孙', '壤驷', '公良',
|
||
'漆雕', '乐正', '宰父', '谷梁', '拓跋', '夹谷', '轩辕', '令狐', '段干', '百里',
|
||
'呼延', '东郭', '南门', '羊舌', '微生', '公户', '公玉', '公仪', '梁丘', '公仲',
|
||
'公上', '公门', '公山', '公坚', '左丘', '公伯', '西门', '公祖', '第五', '公乘',
|
||
'贯丘', '公皙', '南荣', '东里', '东宫', '仲长', '子书', '子桑', '即墨', '达奚', '褚师'
|
||
]
|
||
|
||
for (const compound of compoundSurnames) {
|
||
if (name.startsWith(compound) && name.length > compound.length) {
|
||
return { surname: compound, givenName: name.slice(compound.length) }
|
||
}
|
||
}
|
||
|
||
if (name.length >= 2) {
|
||
return { surname: name.charAt(0), givenName: name.slice(1) }
|
||
}
|
||
|
||
return { surname: name, givenName: '' }
|
||
}
|
||
|
||
// 创建新成员的处理函数 - 必须在条件return之前定义
|
||
const handleCreateNewRelation = useCallback((prefillName: string, relationType: RelationType) => {
|
||
if (!member) return
|
||
const params = new URLSearchParams()
|
||
|
||
const { surname, givenName } = parseName(prefillName)
|
||
if (surname) params.set("surname", surname)
|
||
if (givenName) params.set("givenName", givenName)
|
||
|
||
if (relationType === "child") {
|
||
params.set("generation", String(member.generation + 1))
|
||
if (member.gender === "MALE") {
|
||
params.set("fatherId", member.id)
|
||
if (member.spouseIds?.length) {
|
||
params.set("motherId", member.spouseIds[0])
|
||
}
|
||
} else if (member.gender === "FEMALE") {
|
||
params.set("motherId", member.id)
|
||
if (member.spouseIds?.length) {
|
||
params.set("fatherId", member.spouseIds[0])
|
||
}
|
||
}
|
||
} else if (relationType === "spouse") {
|
||
params.set("generation", String(member.generation))
|
||
params.set("spouseId", member.id)
|
||
params.set("gender", member.gender === "MALE" ? "FEMALE" : "MALE")
|
||
if (member.childrenIds && member.childrenIds.length > 0) {
|
||
member.childrenIds.forEach(id => {
|
||
params.append('childrenIds', id)
|
||
})
|
||
}
|
||
} else if (relationType === "father") {
|
||
params.set("generation", String(member.generation === 1 ? 1 : member.generation - 1))
|
||
params.set("childId", member.id)
|
||
params.set("gender", "MALE")
|
||
if (member.motherId) {
|
||
params.set("spouseId", member.motherId)
|
||
}
|
||
} else if (relationType === "mother") {
|
||
params.set("generation", String(member.generation === 1 ? 1 : member.generation - 1))
|
||
params.set("childId", member.id)
|
||
params.set("gender", "FEMALE")
|
||
if (member.fatherId) {
|
||
params.set("spouseId", member.fatherId)
|
||
}
|
||
}
|
||
|
||
if (currentTree?.id) {
|
||
params.set("treeId", currentTree.id)
|
||
}
|
||
|
||
setRelationDialog(null)
|
||
router.push(`/members/new?${params.toString()}`)
|
||
}, [member, currentTree?.id, router])
|
||
|
||
// 显示加载状态
|
||
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 = async (updatedMember: FamilyMember) => {
|
||
await updateMember(updatedMember.id, updatedMember)
|
||
setIsEditing(false)
|
||
setLastSaveTime(Date.now())
|
||
}
|
||
|
||
const handleDelete = async () => {
|
||
// 检查是否正在删除始祖
|
||
let confirmMessage = "确定要删除这位成员吗?这将同时删除其关联关系。"
|
||
if (member.generation === 1 && (!member.fatherId && !member.motherId)) {
|
||
confirmMessage = "您正在删除始祖,这将导致全族代数重新计算。\n原第2代将变为第1代,以此类推。\n\n确定要删除这位成员吗?"
|
||
}
|
||
|
||
const confirmed = await showConfirm(confirmMessage, "删除成员", "destructive")
|
||
if (confirmed) {
|
||
try {
|
||
const result = await deleteMember(member.id)
|
||
|
||
// 构建跳转参数,只保留 treeId
|
||
const treeId = new URLSearchParams(window.location.search).get('treeId')
|
||
const params = new URLSearchParams()
|
||
if (treeId) params.set('treeId', treeId)
|
||
|
||
// 如果有提示信息(代数调整),显示给用户,等待确认后再跳转
|
||
if (result.message) {
|
||
await showAlert(result.message, "提示")
|
||
}
|
||
|
||
// 删除后跳转到成员列表页
|
||
router.push(`/members?${params.toString()}`)
|
||
} catch (error) {
|
||
console.error('删除成员失败:', error)
|
||
await showAlert('删除成员失败: ' + (error as Error).message, "错误")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建新成员的处理函数 - 已移到条件return之前
|
||
|
||
const handleQuickAddChild = () => {
|
||
setRelationDialog({ type: 'child' })
|
||
}
|
||
|
||
const handleQuickAddSpouse = () => {
|
||
setRelationDialog({ type: 'spouse' })
|
||
}
|
||
|
||
const handleQuickAddFather = () => {
|
||
setRelationDialog({ type: 'father' })
|
||
}
|
||
|
||
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)
|
||
setLastSaveTime(Date.now())
|
||
} catch (error) {
|
||
console.error('上传头像失败:', error)
|
||
showAlert('上传头像失败: ' + (error instanceof Error ? error.message : '未知错误'), "错误")
|
||
} finally {
|
||
setIsUploadingAvatar(false)
|
||
}
|
||
}
|
||
|
||
const handleQuickAddMother = () => {
|
||
setRelationDialog({ type: 'mother' })
|
||
}
|
||
|
||
if (isEditing) {
|
||
return (
|
||
<div className="min-h-screen flex flex-col bg-background">
|
||
<SiteHeader />
|
||
<main className="container mx-auto py-4 md:py-8 px-3 md:px-6">
|
||
<Tabs value={editActiveTab} onValueChange={setEditActiveTab} className="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-8rem)]">
|
||
{/* 固定头部区域 */}
|
||
<div className="flex-shrink-0 pb-4 border-b border-border">
|
||
{/* 返回按钮 */}
|
||
<BackButton onClick={() => setIsEditing(false)} label="返回" />
|
||
|
||
{/* Header Section */}
|
||
<div className="flex flex-col md:flex-row justify-between items-start gap-3 md:gap-6 pb-3 md:pb-4">
|
||
<div className="flex items-start gap-3 md:gap-6">
|
||
<div className="h-16 w-16 md:h-24 md:w-24 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden flex-shrink-0">
|
||
{member.avatarUrl ? (
|
||
<img
|
||
src={member.avatarUrl}
|
||
alt={member.fullName}
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
) : (
|
||
member.gender === 'FEMALE' ? (
|
||
<CircleUserRound className="h-10 w-10 md:h-16 md:w-16 text-pink-400" />
|
||
) : (
|
||
<CircleUser className="h-10 w-10 md:h-16 md:w-16 text-blue-400" />
|
||
)
|
||
)}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2 md:gap-3 mb-1 md:mb-2 flex-wrap">
|
||
<h1 className="text-xl md:text-3xl font-bold font-serif text-foreground flex items-center gap-1 md:gap-2">
|
||
{member.isFounder && <span className="text-amber-600 text-2xl">👑</span>}
|
||
<MemberNameWithStatus
|
||
name={member.fullName}
|
||
isDead={!!member.deathDate}
|
||
/>
|
||
</h1>
|
||
{member.isFounder ? (
|
||
<span className="px-2 md:px-3 py-0.5 md:py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-xs md:text-sm font-semibold rounded-full border-2 border-amber-300 shadow-sm">
|
||
始祖 · {member.generation}世
|
||
</span>
|
||
) : (
|
||
<span className="px-1.5 md:px-2 py-0.5 md:py-1 bg-primary/10 text-primary text-xs md:text-sm font-medium rounded-full border border-primary/20">
|
||
{member.generation}世
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-sm md:text-base text-muted-foreground">
|
||
{member.isFounder
|
||
? '修改始祖信息将影响整个家族树'
|
||
: '更新成员信息'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-2 flex-wrap">
|
||
<Button type="button" variant="outline" onClick={() => setIsEditing(false)}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||
onClick={() => {
|
||
const form = document.querySelector('form')
|
||
if (form) {
|
||
form.requestSubmit()
|
||
}
|
||
}}
|
||
>
|
||
保存成员
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab 导航栏 */}
|
||
<TabsList className="grid grid-cols-4 md:flex md:flex-wrap gap-1 md:gap-2 bg-muted/70 p-1 w-full md:w-fit h-auto">
|
||
<TabsTrigger value="info" className="text-xs md:text-sm px-2 md:px-3 py-1.5">基本信息</TabsTrigger>
|
||
<TabsTrigger value="details" className="text-xs md:text-sm px-2 md:px-3 py-1.5">详细资料</TabsTrigger>
|
||
<TabsTrigger value="photos" className="text-xs md:text-sm px-2 md:px-3 py-1.5">生平影像</TabsTrigger>
|
||
<TabsTrigger value="stories" className="text-xs md:text-sm px-2 md:px-3 py-1.5">家族故事</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 始祖特殊提示框 */}
|
||
{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>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tab 内容区域 - 可滚动 */}
|
||
<div className="flex-1 overflow-y-auto">
|
||
<MemberForm
|
||
initialData={member}
|
||
existingMembers={allMembers}
|
||
onSubmit={handleSave}
|
||
onCancel={() => setIsEditing(false)}
|
||
isFounder={member.isFounder}
|
||
hideTabs={true}
|
||
activeTab={editActiveTab}
|
||
/>
|
||
</div>
|
||
</Tabs>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen flex flex-col bg-background">
|
||
<SiteHeader />
|
||
<main className="container mx-auto py-4 md:py-8 px-3 md:px-6">
|
||
<Tabs defaultValue="info" className="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-8rem)]">
|
||
{/* 固定头部区域 */}
|
||
<div className="flex-shrink-0 pb-3 md:pb-4 border-b border-border">
|
||
{/* 返回按钮 */}
|
||
<BackButton
|
||
onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}
|
||
label="返回"
|
||
/>
|
||
|
||
{/* Header Section */}
|
||
<div className="flex flex-col md:flex-row justify-between items-start gap-3 md:gap-6 pb-3 md:pb-4">
|
||
<div className="flex items-start gap-3 md:gap-6">
|
||
<div className="relative group flex-shrink-0">
|
||
<div className="h-16 w-16 md:h-24 md: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-10 w-10 md:h-16 md:w-16 text-pink-400" />
|
||
) : (
|
||
<CircleUser className="h-10 w-10 md:h-16 md: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-5 w-5 md:h-6 md: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 className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2 md:gap-3 mb-1 md:mb-2 flex-wrap">
|
||
<h1 className="text-xl md:text-3xl font-bold font-serif text-foreground flex items-center gap-1 md:gap-2">
|
||
{member.isFounder && <span className="text-amber-600 text-lg md:text-2xl">👑</span>}
|
||
<MemberNameWithStatus
|
||
name={member.fullName}
|
||
isDead={!!member.deathDate}
|
||
memberId={member.id}
|
||
treeId={currentTree?.id}
|
||
/>
|
||
</h1>
|
||
{member.isFounder ? (
|
||
<span className="px-2 md:px-3 py-0.5 md:py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-xs md:text-sm font-semibold rounded-full border-2 border-amber-300 shadow-sm">
|
||
始祖 · {member.generation}世
|
||
</span>
|
||
) : (
|
||
<span className="px-1.5 md:px-2 py-0.5 md:py-1 bg-primary/10 text-primary text-xs md:text-sm font-medium rounded-full border border-primary/20">
|
||
{member.generation}世
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-wrap gap-x-2 md:gap-x-4 gap-y-1 text-xs md: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-1.5 md:gap-2 flex-wrap">
|
||
{permissions.canCreate(role) && (
|
||
<>
|
||
<Button variant="default" size="sm" onClick={handleQuickAddChild} className="gap-1 text-xs md:text-sm h-7 md:h-8 px-2 md:px-3">
|
||
<UserPlus className="h-3 w-3 md:h-4 md:w-4" /> <span className="hidden sm:inline">添加</span>子女
|
||
</Button>
|
||
<Button variant="secondary" size="sm" onClick={handleQuickAddSpouse} className="gap-1 text-xs md:text-sm h-7 md:h-8 px-2 md:px-3">
|
||
<UserPlus className="h-3 w-3 md:h-4 md:w-4" /> <span className="hidden sm:inline">添加</span>配偶
|
||
</Button>
|
||
</>
|
||
)}
|
||
<Button variant="outline" size="icon" onClick={() => window.print()} title="打印" className="h-7 w-7 md:h-8 md:w-8">
|
||
<Printer className="h-3 w-3 md:h-4 md:w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon" title="分享" className="h-7 w-7 md:h-8 md:w-8">
|
||
<Share2 className="h-3 w-3 md:h-4 md:w-4" />
|
||
</Button>
|
||
{permissions.canUpdate(role) && (
|
||
<Button variant="outline" size="sm" onClick={() => setIsEditing(true)} className="gap-1 text-xs md:text-sm h-7 md:h-8 px-2 md:px-3">
|
||
<Edit className="h-3 w-3 md:h-4 md:w-4" /> 编辑
|
||
</Button>
|
||
)}
|
||
{permissions.canDeleteMember(role) && (
|
||
<Button variant="destructive" size="icon" onClick={handleDelete} title="删除" className="h-7 w-7 md:h-8 md:w-8">
|
||
<Trash className="h-3 w-3 md:h-4 md:w-4" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab 导航栏 */}
|
||
<TabsList className="grid grid-cols-5 md:flex md:flex-wrap gap-1 md:gap-2 bg-muted/70 p-1 w-full md:w-fit h-auto">
|
||
<TabsTrigger value="info" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5">基本信息</TabsTrigger>
|
||
<TabsTrigger value="details" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5">详细资料</TabsTrigger>
|
||
<TabsTrigger value="photos" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5">生平影像</TabsTrigger>
|
||
<TabsTrigger value="stories" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5">家族故事</TabsTrigger>
|
||
<TabsTrigger value="history" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5">时间线</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 始祖特殊提示框 - 移动端简化 */}
|
||
{member.isFounder && (
|
||
<div className="mt-3 md:mt-6 p-3 md: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-2 md:gap-4">
|
||
<div className="flex-shrink-0 w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
|
||
<span className="text-xl md:text-3xl">👑</span>
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<h2 className="text-lg md:text-2xl font-serif font-bold text-amber-900 mb-1 md:mb-3">
|
||
家族始祖
|
||
</h2>
|
||
<p className="text-sm md: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>传人。
|
||
<span className="hidden md:inline"><br />血脉由此而始,世系因此而传。</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tab 内容区域 - 可滚动 */}
|
||
<div className="flex-1 overflow-y-auto">
|
||
{/* 基本信息Tab - 包含基本信息和家庭关系 */}
|
||
<TabsContent value="info" className="mt-3 md:mt-6 space-y-4 md:space-y-6">
|
||
{/* 基本信息 */}
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-3 md:p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4">基本信息</ChineseCardTitle>
|
||
<dl className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-x-8 gap-y-4 text-sm">
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">姓名</dt>
|
||
<dd className="font-medium text-lg">{member.fullName}</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">性别</dt>
|
||
<dd className="font-medium">{member.gender === 'MALE' ? '男' : '女'}</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">世系</dt>
|
||
<dd className="font-medium">第 {member.generation} 世</dd>
|
||
</div>
|
||
{member.occupation && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">职业</dt>
|
||
<dd className="font-medium">{member.occupation}</dd>
|
||
</div>
|
||
)}
|
||
</dl>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
{/* 家庭关系 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<ChineseCard variant="default" cornerOrnament className={`rounded-lg ${cardStyle}`}>
|
||
<ChineseCardContent className="p-4">
|
||
<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>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
<ChineseCard variant="default" cornerOrnament className={`rounded-lg ${cardStyle}`}>
|
||
<ChineseCardContent className="p-4">
|
||
<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>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
{/* 详细资料Tab */}
|
||
<TabsContent value="details" className="mt-6 space-y-6">
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4">传统称谓与生平</ChineseCardTitle>
|
||
<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.generationName && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">字辈</dt>
|
||
<dd className="font-medium">{member.generationName}</dd>
|
||
</div>
|
||
)}
|
||
{member.courtesyName && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">字</dt>
|
||
<dd className="font-medium">{member.courtesyName}</dd>
|
||
</div>
|
||
)}
|
||
{member.artName && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">号</dt>
|
||
<dd className="font-medium">{member.artName}</dd>
|
||
</div>
|
||
)}
|
||
{member.posthumousName && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">谥号</dt>
|
||
<dd className="font-medium">{member.posthumousName}</dd>
|
||
</div>
|
||
)}
|
||
{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.deathPlace && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">逝世地</dt>
|
||
<dd className="font-medium">{member.deathPlace}</dd>
|
||
</div>
|
||
)}
|
||
{member.burialPlace && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">安葬地</dt>
|
||
<dd className="font-medium">{member.burialPlace}</dd>
|
||
</div>
|
||
)}
|
||
{member.address && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">居住地</dt>
|
||
<dd className="font-medium">{member.address}</dd>
|
||
</div>
|
||
)}
|
||
{member.occupation && (
|
||
<div>
|
||
<dt className="text-muted-foreground mb-1">职业</dt>
|
||
<dd className="font-medium">{member.occupation}</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>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
|
||
{/* 个人简介 */}
|
||
{member.bio && (
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4">个人简介</ChineseCardTitle>
|
||
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
)}
|
||
|
||
{/* 标签 */}
|
||
{member.tags && member.tags.length > 0 && (
|
||
<ChineseCard variant="default" cornerOrnament className={`shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4">标签</ChineseCardTitle>
|
||
<div className="flex flex-wrap gap-2">
|
||
{member.tags.map((tag) => (
|
||
<span
|
||
key={tag}
|
||
className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-md border border-primary/20"
|
||
>
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
)}
|
||
</TabsContent>
|
||
|
||
{/* 生平影像Tab */}
|
||
<TabsContent value="photos" className="mt-6 space-y-6">
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||
<Images className="h-5 w-5 text-primary" />
|
||
影像记忆
|
||
</ChineseCardTitle>
|
||
<PhotoGallery
|
||
photos={member.photos}
|
||
photoIds={member.photoIds}
|
||
onChange={async (photos) => {
|
||
await updateMember(member.id, { photos })
|
||
setLastSaveTime(Date.now())
|
||
}}
|
||
readonly={false}
|
||
/>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
|
||
{/* 家族故事Tab */}
|
||
<TabsContent value="stories" className="mt-6 space-y-6">
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||
<BookOpen className="h-5 w-5 text-primary" />
|
||
家族故事
|
||
</ChineseCardTitle>
|
||
{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>
|
||
)}
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
|
||
{/* 时间线Tab - 仅显示视图 */}
|
||
<TabsContent value="history" className="mt-6 space-y-6">
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-2">最近更新</ChineseCardTitle>
|
||
<p className="text-sm text-muted-foreground">
|
||
最后保存于 {new Date(lastSaveTime).toLocaleString('zh-CN')}
|
||
</p>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||
<ChineseCardContent className="p-6">
|
||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||
<History className="h-5 w-5 text-primary" />
|
||
版本历史
|
||
</ChineseCardTitle>
|
||
<MemberVersionHistory
|
||
memberId={member.id}
|
||
treeId={currentTree?.id || ''}
|
||
lastUpdated={lastSaveTime}
|
||
/>
|
||
</ChineseCardContent>
|
||
</ChineseCard>
|
||
</TabsContent>
|
||
</div>
|
||
</Tabs>
|
||
</main>
|
||
|
||
{/* 添加成员对话框 */}
|
||
{relationDialog && member && (
|
||
<AddRelationDialog
|
||
open={!!relationDialog}
|
||
onOpenChange={(open) => {
|
||
if (!open) setRelationDialog(null)
|
||
}}
|
||
member={member}
|
||
relationType={relationDialog.type}
|
||
onCreateNew={handleCreateNewRelation}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|