"use client" import type React from "react" import Link from "next/link" import Image from "next/image" import dynamic from "next/dynamic" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription, ChineseDivider } from "@/components/ui/chinese-card" import { Dialog, DialogContent, DialogTitle, DialogHeader, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { SiteHeader } from "@/components/site-header" import { ScrollArea } from "@/components/ui/scroll-area" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator, Loader2, Images, ChevronDown, ChevronUp, EyeOff, Play, Video, Edit } from "lucide-react" import { useFamily } from "@/context/family-context" import { useMemo, useState, useEffect, useRef, useCallback } from "react" import { useRouter } from "next/navigation" import { format } from "date-fns" import { useSession } from "next-auth/react" import { useSearchParams } from "next/navigation" import { CollaboratorDialog } from "@/components/tree/collaborator-dialog" import { permissions } from "@/lib/permissions" import { isInCurrentMonth, solar2lunar, lunar2solar } from "@/lib/lunar-calendar" import { MemberNameWithStatus } from "@/components/member-name-with-status" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Switch } from "@/components/ui/switch" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" import type { FamilyPhoto } from "@/types/family" import { useDialog } from "@/components/ui/alert-dialog-custom" import { PhotoCard } from "@/components/dashboard/photo-card" // 动态导入统计图表组件(减少初始加载体积) const StatisticsCharts = dynamic( () => import("@/components/dashboard/statistics-charts").then(mod => ({ default: mod.StatisticsCharts })), { ssr: false, loading: () => (
加载统计图表...
) } ) interface ActivityLog { id: string action: string entityType: string entityId?: string | null entityName?: string | null changes?: any timestamp: string user?: { name?: string | null email?: string | null } } // 判断是否为视频文件 const isVideoFile = (url: string) => { const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'] return videoExtensions.some(ext => url.toLowerCase().endsWith(ext)) } // 计算事件日期的辅助函数 const calculateEventDate = ( dateStr: string, isLunar: boolean, now: Date ): { date: Date; lunarDisplay?: string } | null => { const date = new Date(dateStr) let eventDate: Date let lunarDisplay: string | undefined if (isLunar) { const lunarInfo = solar2lunar(date) if (lunarInfo) { const thisYearLunar = lunar2solar( now.getFullYear(), lunarInfo.lunarMonth, lunarInfo.lunarDay, lunarInfo.isLeap ) if (thisYearLunar) { eventDate = thisYearLunar lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}` } else { eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate()) } } else { eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate()) } } else { eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate()) } return { date: eventDate, lunarDisplay } } // 创建即将到来的事件 const createUpcomingEvent = ( member: any, type: 'birth' | 'death', dateStr: string, isLunar: boolean, now: Date, threeMonthsLater: Date ) => { const result = calculateEventDate(dateStr, isLunar, now) if (!result) return null const { date, lunarDisplay } = result if (date >= now && date <= threeMonthsLater) { return { member, type, date, originalDate: dateStr, isLunar, lunarDisplay, month: date.getMonth() + 1, day: date.getDate() } } return null } export default function DashboardPage() { const { treeData, isLoading, currentTree, updateMember } = useFamily() const { data: session } = useSession() const { showAlert } = useDialog() const [recentActivities, setRecentActivities] = useState([]) const [selectedPhoto, setSelectedPhoto] = useState(null) const [statsExpanded, setStatsExpanded] = useState(true) const [recentActivityExpanded, setRecentActivityExpanded] = useState(false) const [adminToggleLoading, setAdminToggleLoading] = useState(null) const [editDialogOpen, setEditDialogOpen] = useState(false) const [editForm, setEditForm] = useState({ name: '', description: '' }) const [isSaving, setIsSaving] = useState(false) const router = useRouter() // 首次登录跳转到使用帮助 useEffect(() => { if (!session?.user?.id) return if (session.user.hasSeenHelp) return router.replace('/help') }, [router, session?.user?.id, session?.user?.hasSeenHelp]) // 加载最近的操作日志 useEffect(() => { // 只有在有家族树且已登录时才获取活动日志 if (!currentTree?.id || !session?.user?.id) { setRecentActivities([]) return } const controller = new AbortController() fetch(`/api/trees/${currentTree.id}/activity-logs?limit=10`, { signal: controller.signal }) .then(res => { if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`) } return res.json() }) .then(data => { if (data.logs && Array.isArray(data.logs)) { setRecentActivities(data.logs) } }) .catch(err => { // 忽略 abort 错误 if (err.name !== 'AbortError') { console.error('获取活动日志失败:', err) } setRecentActivities([]) }) return () => controller.abort() }, [currentTree?.id, session?.user?.id]) // 计算统计数据 - 单次遍历优化 const stats = useMemo(() => { const members = Object.values(treeData.members) const totalMembers = members.length // 单次遍历计算所有统计数据 let livingMembers = 0 let deceasedMembers = 0 let maleCount = 0 let femaleCount = 0 let maxGeneration = 0 const birthYears: number[] = [] let totalAge = 0 let deceasedWithAgeCount = 0 members.forEach(m => { // 生死统计 if (m.deathDate) { deceasedMembers++ } else { livingMembers++ } // 性别统计 if (m.gender === 'MALE') maleCount++ else if (m.gender === 'FEMALE') femaleCount++ // 代数统计 if (m.generation && m.generation > maxGeneration) { maxGeneration = m.generation } // 出生年份和寿命统计 if (m.birthDate) { const birthYear = new Date(m.birthDate).getFullYear() birthYears.push(birthYear) if (m.deathDate) { const deathYear = new Date(m.deathDate).getFullYear() totalAge += deathYear - birthYear deceasedWithAgeCount++ } } }) const earliestYear = birthYears.length > 0 ? Math.min(...birthYears) : new Date().getFullYear() const yearsSpan = birthYears.length > 0 ? new Date().getFullYear() - earliestYear : 0 const averageLifespan = deceasedWithAgeCount > 0 ? Math.round(totalAge / deceasedWithAgeCount) : 0 return { totalMembers, livingMembers, deceasedMembers, maleCount, femaleCount, maxGeneration, yearsSpan, earliestYear, averageLifespan } }, [treeData.members]) // 获取最近的成员(按ID排序,取最新的5个) const recentMembers = useMemo(() => { return Object.values(treeData.members) .sort((a, b) => parseInt(b.id) - parseInt(a.id)) .slice(0, 5) }, [treeData.members]) // 收集所有成员的照片 const isOwner = session && currentTree?.ownerId === session.user?.id const allPhotos = useMemo(() => { const photos: Array<{ url: string caption?: string uploadedAt: string memberId: string memberName: string isDead: boolean adminVisibleOverride: boolean visibleInOverview: boolean }> = [] Object.values(treeData.members).forEach(member => { if (member.photos && member.photos.length > 0) { (member.photos as FamilyPhoto[]).forEach(photo => { const visible = photo.visibleInOverview ?? false if (!visible) return const adminAllowed = photo.adminVisibleOverride ?? true if (!adminAllowed && !isOwner) return photos.push({ url: photo.url, caption: photo.caption, uploadedAt: photo.uploadedAt, memberId: member.id, memberName: member.fullName, isDead: !!member.deathDate, adminVisibleOverride: adminAllowed, visibleInOverview: visible, }) }) } }) return photos.sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()) }, [treeData.members, isOwner]) const groupedPhotosByMonth = useMemo(() => { const sortedPhotos = [...allPhotos].sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime() ) const groupedByMonth: Record = {} sortedPhotos.forEach(photo => { const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月') if (!groupedByMonth[monthKey]) { groupedByMonth[monthKey] = [] } groupedByMonth[monthKey].push(photo) }) return Object.entries(groupedByMonth) }, [allPhotos]) const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => { if (!isOwner) return const member = treeData.members[memberId] if (!member || !member.photos || member.photos.length === 0) return const key = `${memberId}|${photoUrl}` setAdminToggleLoading(key) try { const updatedPhotos = (member.photos as FamilyPhoto[]).map(photo => photo.url === photoUrl ? { ...photo, adminVisibleOverride: value } : photo ) await updateMember(memberId, { photos: updatedPhotos }) } catch (error) { console.error('更新管理员展示权限失败:', error) } finally { setAdminToggleLoading(null) } }, [isOwner, updateMember]) // 保存编辑 const handleSaveEdit = async () => { if (!currentTree?.id || !editForm.name.trim()) return setIsSaving(true) try { const response = await fetch(`/api/trees/${currentTree.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: editForm.name.trim(), description: editForm.description.trim() || null, }), }) if (!response.ok) { throw new Error('更新失败') } setEditDialogOpen(false) // 刷新页面以显示更新后的数据 window.location.reload() } catch (error) { console.error('更新家族信息失败:', error) await showAlert('更新失败,请重试', "错误") } finally { setIsSaving(false) } } // 获取本月纪念日(生日和忌日)- 支持农历 const monthlyAnniversaries = useMemo(() => { const members = Object.values(treeData.members) const anniversaries: Array<{ member: any type: 'birth' | 'death' date: string day: number month: number isLunar: boolean lunarDisplay?: string }> = [] members.forEach(member => { // 检查生日 if (member.birthDate) { const isLunar = member.isLunarDate || false const result = isInCurrentMonth(member.birthDate, isLunar) if (result) { let lunarDisplay = undefined if (isLunar) { const lunar = solar2lunar(new Date(member.birthDate)) if (lunar) { lunarDisplay = `${lunar.monthName}${lunar.dayName}` } } anniversaries.push({ member, type: 'birth', date: member.birthDate, day: result.day, month: result.month, isLunar, lunarDisplay }) } } // 检查忌日 if (member.deathDate) { const isLunar = member.isLunarDate || false const result = isInCurrentMonth(member.deathDate, isLunar) if (result) { let lunarDisplay = undefined if (isLunar) { const lunar = solar2lunar(new Date(member.deathDate)) if (lunar) { lunarDisplay = `${lunar.monthName}${lunar.dayName}` } } anniversaries.push({ member, type: 'death', date: member.deathDate, day: result.day, month: result.month, isLunar, lunarDisplay }) } } }) // 按日期排序 const sorted = anniversaries.sort((a, b) => a.day - b.day) return sorted }, [treeData.members]) // 计算未来三月纪念日 - 优化版本 const upcomingEvents = useMemo(() => { const now = new Date() const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate()) const members = Object.values(treeData.members) const events: Array<{ member: any type: 'birth' | 'death' date: Date originalDate: string isLunar: boolean lunarDisplay?: string month: number day: number }> = [] members.forEach(member => { // 生日 if (member.birthDate) { const birthEvent = createUpcomingEvent( member, 'birth', member.birthDate, member.isLunarDate || false, now, threeMonthsLater ) if (birthEvent) events.push(birthEvent) } // 忌日 if (member.deathDate) { const deathEvent = createUpcomingEvent( member, 'death', member.deathDate, member.isLunarDate || false, now, threeMonthsLater ) if (deathEvent) events.push(deathEvent) } }) return events.sort((a, b) => a.date.getTime() - b.date.getTime()) }, [treeData.members]) // 计算家族迁徙记录 const locationGroups = useMemo(() => { const members = Object.values(treeData.members) const groups: Record = {} members.forEach(member => { if (member.ancestralHome) { if (!groups[member.ancestralHome]) { groups[member.ancestralHome] = [] } groups[member.ancestralHome].push(member) } }) return Object.entries(groups).sort((a, b) => b[1].length - a[1].length) }, [treeData.members]) // 如果用户没有家族树,显示欢迎页面 if (!isLoading && !currentTree) { return (
{/* 背景装饰图片 */}
{/* 文字图片 - 左上角,轻微向右倾斜 */}
{/* 树图片1 - 右上角,轻微向左倾斜,对称布局 */}
{/* 建筑图片 - 左下角,向右倾斜,营造动感 */}
{/* 树图片 - 右下角,向左倾斜,与建筑对称 */}
{/* 内容区域 */}
{/* Slogan */}

传承千年文脉 · 记录家族荣光

{/* 图标 */}
{/* 标题 */}

华夏家谱

{/* 哲理文字 - 滚动显示 */}

家族是根,文化是魂 · 记录过往,传承未来

让每一个故事都成为永恒 · 血脉相连,世代相传

铭记历史,启迪后人 · 家风永续,德泽绵长

{/* 重复内容实现无缝滚动 */}

家族是根,文化是魂 · 记录过往,传承未来

让每一个故事都成为永恒 · 血脉相连,世代相传

铭记历史,启迪后人 · 家风永续,德泽绵长

{/* 创建按钮 */}
) } return (

{currentTree?.name || '家族族谱'}

{currentTree?.currentUserRole && ( {currentTree.currentUserRole === 'OWNER' ? '所有者' : currentTree.currentUserRole === 'EDITOR' ? '编辑者' : '查看者'} )}

{currentTree?.description || '记录家族历史,传承家族文化'} {stats.maxGeneration > 0 && ` · 共 ${stats.maxGeneration} 代传人`}

{session && currentTree?.ownerId === session.user?.id && ( <> )} {permissions.canCreate(currentTree?.currentUserRole) && ( )}
{/* 第一个卡片:家族成员 + 在世/已故 */}

家族成员

{stats.totalMembers} 人
在世 {stats.livingMembers} · 已故 {stats.deceasedMembers}
{/* 第二个卡片:繁衍代数 + 记录年代 + 平均寿命 */}

家族历史

{stats.maxGeneration} 代
跨越 {stats.yearsSpan} 年 · 均寿 {stats.averageLifespan > 0 ? `${stats.averageLifespan}岁` : '-'}
{/* 第三个卡片:性别比例 */}

性别比例

{stats.maleCount > 0 ? '1' : '0'} : {stats.maleCount > 0 ? (stats.femaleCount / stats.maleCount).toFixed(2) : '0'}
男性 {stats.maleCount} · 女性 {stats.femaleCount}
照片 动态 图表 籍贯
{/* 照片展示标签页 */} 家族影像 珍藏的家族照片,记录美好时光 {allPhotos.length > 0 ? (
{/* 按月份分组显示 */} {groupedPhotosByMonth.map(([month, monthPhotos]) => (

{month} ({monthPhotos.length})

{monthPhotos.map((photo, index) => (
))}
))}
) : (

暂无照片或视频

在成员详情页可以添加照片或视频

)}
{/* 第一行:本月祭祖和未来三月纪念日 */}
本月祭祖/纪念 本月需要纪念的家族成员生日与忌日 {monthlyAnniversaries.length > 0 ? (
{/* 诞辰列 */}

诞辰 {monthlyAnniversaries.filter(a => a.type === 'birth').length}

{monthlyAnniversaries.filter(a => a.type === 'birth').length > 0 ? ( monthlyAnniversaries.filter(a => a.type === 'birth').map((anniversary) => { const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear() return (
{anniversary.isLunar && anniversary.lunarDisplay ? (
{anniversary.lunarDisplay.replace('月', '')}
) : (
{anniversary.month}月 {anniversary.day}
)}

{anniversary.isLunar && ( 农历 )}

第 {anniversary.member.generation} 世 · {yearsAgo} 岁

) }) ) : (

此月无诞辰

)}
{/* 忌日列 */}

忌日 {monthlyAnniversaries.filter(a => a.type === 'death').length}

{monthlyAnniversaries.filter(a => a.type === 'death').length > 0 ? ( monthlyAnniversaries.filter(a => a.type === 'death').map((anniversary) => { const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear() return (
{anniversary.isLunar && anniversary.lunarDisplay ? (
{anniversary.lunarDisplay.replace('月', '')}
) : (
{anniversary.month}月 {anniversary.day}
)}

{anniversary.isLunar && ( 农历 )}

第 {anniversary.member.generation} 世 · {yearsAgo} 年

) }) ) : (

此月无忌日

)}
) : (

此月无忆

)}
未来三月纪念日 即将到来的生日与忌日提醒 {(() => { const now = new Date() const birthEvents = upcomingEvents.filter(e => e.type === 'birth') const deathEvents = upcomingEvents.filter(e => e.type === 'death') return upcomingEvents.length > 0 ? (
{/* 诞辰列 */}

诞辰 {birthEvents.length}

{birthEvents.length > 0 ? ( birthEvents.map((event, index) => { const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear() const daysUntil = Math.ceil((event.date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) return (
{event.isLunar && event.lunarDisplay ? (
{event.lunarDisplay}
) : (
{event.month}月 {event.day}
)}

{event.isLunar && ( 农历 )}

第 {event.member.generation} 世 · {yearsAgo} 岁 · {daysUntil}天

) }) ) : (

无诞辰

)}
{/* 忌日列 */}

忌日 {deathEvents.length}

{deathEvents.length > 0 ? ( deathEvents.map((event, index) => { const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear() const daysUntil = Math.ceil((event.date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) return (
{event.isLunar && event.lunarDisplay ? (
{event.lunarDisplay}
) : (
{event.month}月 {event.day}
)}

{event.isLunar && ( 农历 )}

第 {event.member.generation} 世 · {yearsAgo} 年 · {daysUntil}天

) }) ) : (

无忌日

)}
) : (

来日无期

) })()}
{/* 第二行:近期修谱动态(折叠状态) */}
近期修谱动态
记录家族成员的最新变动与修订
{recentActivities.length > 0 ? ( recentActivities.map((activity, i) => (
{activity.action === 'CREATE' ? '创建' : activity.action === 'UPDATE' ? '更新' : activity.action === 'DELETE' ? '删除' : '导入'} {activity.entityId && activity.entityType === 'MEMBER' ? ( (() => { const member = treeData.members[activity.entityId!] if (member) { return ( ) } // 成员不存在(可能已删除),不显示状态点,但保留链接(如果不是删除操作) return activity.action === 'DELETE' ? ( {activity.entityName || '未知'} ) : ( {activity.entityName || '未知'} ) })() ) : ( {activity.entityName || '未知'} )} {format(new Date(activity.timestamp), 'MM-dd HH:mm')}
{activity.action === 'UPDATE' && activity.changes ? (
{(() => { const changes = activity.changes as Record const fieldLabels: Record = { fullName: '姓名', surname: '姓氏', givenName: '名字', gender: '性别', birthDate: '出生日期', deathDate: '去世日期', birthPlace: '出生地', ancestralHome: '祖籍', generation: '世代', generationName: '字辈', courtesyName: '字', artName: '号', posthumousName: '谥号', rank: '排行', bio: '简介', phone: '手机', telephone: '电话', email: '邮箱', address: '地址', photoIds: '照片', spouseIds: '配偶', childrenIds: '子女', motherId: '母亲', fatherId: '父亲', isFounder: '始祖', isLunarDate: '农历日期', burialPlace: '安葬地', tags: '标签', } const entries = Object.entries(changes) .filter(([key]) => fieldLabels[key]) .slice(0, 2) // 只显示前2个变更 return entries.length > 0 ? ( <> {entries.map(([key]) => (
• 修改了{fieldLabels[key]}
))} {Object.keys(changes).length > 2 && (
等 {Object.keys(changes).length} 项变更
)} ) :
更新了成员
})()}
) : (

{activity.action === 'CREATE' && '创建了成员'} {activity.action === 'UPDATE' && '更新了成员'} {activity.action === 'DELETE' && '删除了成员'} {activity.action === 'IMPORT' && '导入了数据'}

)}
)) ) : (

静待修谱

)}
{/* 统计图表标签页 */} {/* 迁徙记录标签页 */} 家族籍贯记录 记录家族成员的籍贯分布
{locationGroups.length > 0 ? ( locationGroups.map(([location, locationMembers]) => (

{location}

{locationMembers.length} 人
{locationMembers.map((member: any) => (

{member.fullName}

第 {member.generation} 世

))}
)) ) : (

未有籍贯记录

)}
{/* 媒体放大预览 Dialog */} setSelectedPhoto(null)}> {selectedPhoto && isVideoFile(selectedPhoto) ? '视频播放' : '照片预览'} {selectedPhoto && ( isVideoFile(selectedPhoto) ? ( {/* 编辑家族信息对话框 */} 编辑家族信息 修改家族名称和说明
setEditForm({ ...editForm, name: e.target.value })} placeholder="请输入家族名称" maxLength={50} />