"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"
// 动态导入统计图表组件(减少初始加载体积)
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))
}
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
// 在世和已故人数
const livingMembers = members.filter(m => !m.deathDate).length
const deceasedMembers = members.filter(m => m.deathDate).length
// 性别统计
const maleCount = members.filter(m => m.gender === 'MALE').length
const femaleCount = members.filter(m => m.gender === 'FEMALE').length
// 计算最大代数(处理空数组的情况)
const generations = members.map(m => m.generation || 0)
const maxGeneration = generations.length > 0 ? Math.max(...generations) : 0
// 计算最早出生年份
const birthYears = members
.map(m => m.birthDate ? new Date(m.birthDate).getFullYear() : null)
.filter(y => y !== null) as number[]
const earliestYear = birthYears.length > 0 ? Math.min(...birthYears) : new Date().getFullYear()
const yearsSpan = birthYears.length > 0 ? new Date().getFullYear() - earliestYear : 0
// 计算平均寿命(只统计已故成员)
const deceasedWithAge = members.filter(m => m.birthDate && m.deathDate)
const totalAge = deceasedWithAge.reduce((sum, m) => {
const birthYear = new Date(m.birthDate!).getFullYear()
const deathYear = new Date(m.deathDate!).getFullYear()
return sum + (deathYear - birthYear)
}, 0)
const averageLifespan = deceasedWithAge.length > 0
? Math.round(totalAge / deceasedWithAge.length)
: 0
return {
totalMembers,
livingMembers,
deceasedMembers,
maleCount,
femaleCount,
maxGeneration,
yearsSpan,
earliestYear,
averageLifespan
}
}, [treeData])
// 获取最近的成员(按ID排序,取最新的5个)
const recentMembers = useMemo(() => {
return Object.values(treeData.members)
.sort((a, b) => parseInt(b.id) - parseInt(a.id))
.slice(0, 5)
}, [treeData])
// 收集所有成员的照片
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, isOwner])
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, treeData.members, 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])
// 计算家族迁徙记录
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])
// 如果用户没有家族树,显示欢迎页面
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 ? (
{/* 按月份分组显示 */}
{(() => {
// 按月份分组
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).map(([month, monthPhotos]) => (
{month}
({monthPhotos.length})
{monthPhotos.map((photo, index) => (
{photo.adminVisibleOverride === false ? (
来自
{format(new Date(photo.uploadedAt), 'MM-dd')}
{isOwner ? (
允许展示
handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
/>
) : (
管理员已隐藏
)}
) : (
<>
{/* 媒体区域 - 点击放大/播放 */}
setSelectedPhoto(photo.url)}
>
{isVideoFile(photo.url) ? (
) : (

)}
{/* 底部显示信息 */}
{/* 照片说明 */}
{photo.caption ? (
{photo.caption}
) : (
暂无说明
)}
{/* 分享人和时间 */}
来自
{format(new Date(photo.uploadedAt), 'MM-dd')}
{isOwner && (
允许展示
handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
/>
)}
>
)}
))}
))
})()}
) : (
)}
{/* 第一行:本月祭祖和未来三月纪念日 */}
本月祭祖/纪念
本月需要纪念的家族成员生日与忌日
{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 threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
const members = Object.values(treeData.members)
const upcomingEvents: 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 birthDate = new Date(member.birthDate)
let thisYearBirth: Date
let lunarDisplay: string | undefined
// 检查是否按农历计算
if (member.isLunarDate) {
// 农历生日:计算今年对应的公历日期
const lunarInfo = solar2lunar(birthDate)
if (lunarInfo) {
const thisYearLunar = lunar2solar(
now.getFullYear(),
lunarInfo.lunarMonth,
lunarInfo.lunarDay,
lunarInfo.isLeap
)
if (thisYearLunar) {
thisYearBirth = thisYearLunar
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
} else {
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
}
} else {
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
}
} else {
// 公历生日
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
}
if (thisYearBirth >= now && thisYearBirth <= threeMonthsLater) {
upcomingEvents.push({
member,
type: 'birth',
date: thisYearBirth,
originalDate: member.birthDate,
isLunar: member.isLunarDate || false,
lunarDisplay,
month: thisYearBirth.getMonth() + 1,
day: thisYearBirth.getDate()
})
}
}
// 忌日
if (member.deathDate) {
const deathDate = new Date(member.deathDate)
let thisYearDeath: Date
let lunarDisplay: string | undefined
// 检查是否按农历计算
if (member.isLunarDate) {
// 农历忌日:计算今年对应的公历日期
const lunarInfo = solar2lunar(deathDate)
if (lunarInfo) {
const thisYearLunar = lunar2solar(
now.getFullYear(),
lunarInfo.lunarMonth,
lunarInfo.lunarDay,
lunarInfo.isLeap
)
if (thisYearLunar) {
thisYearDeath = thisYearLunar
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
} else {
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
}
} else {
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
}
} else {
// 公历忌日
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
}
if (thisYearDeath >= now && thisYearDeath <= threeMonthsLater) {
upcomingEvents.push({
member,
type: 'death',
date: thisYearDeath,
originalDate: member.deathDate,
isLunar: member.isLunarDate || false,
lunarDisplay,
month: thisYearDeath.getMonth() + 1,
day: thisYearDeath.getDate()
})
}
}
})
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
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 */}
{/* 编辑家族信息对话框 */}
)
}
function StatCard({
title,
value,
subtitle,
icon,
href,
}: { title: string; value: string; subtitle: string; icon: React.ReactNode; href?: string }) {
const cardContent = (
)
if (href) {
return {cardContent}
}
return cardContent
}