This commit is contained in:
freedakgmail
2025-11-24 14:02:34 +08:00
parent b7a8c9ee6e
commit 3d075c6076
941 changed files with 25613 additions and 27641 deletions
+387 -167
View File
@@ -17,6 +17,8 @@ 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"
interface ActivityLog {
id: string
@@ -24,6 +26,7 @@ interface ActivityLog {
entityType: string
entityId?: string | null
entityName?: string | null
changes?: any
timestamp: string
user?: {
name?: string | null
@@ -49,26 +52,37 @@ export default function DashboardPage() {
// 加载最近的操作日志
useEffect(() => {
if (currentTree?.id && session?.user?.id) {
fetch(`/api/trees/${currentTree.id}/activity-logs?limit=10`)
.then(res => {
if (!res.ok) {
throw new Error('获取活动日志失败')
}
return res.json()
})
.then(data => {
if (data.logs) {
setRecentActivities(data.logs)
}
})
.catch(err => {
console.error('获取活动日志失败:', err)
setRecentActivities([])
})
} else {
// 只有在有家族树且已登录时才获取活动日志
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])
// 计算统计数据
@@ -126,9 +140,8 @@ export default function DashboardPage() {
.slice(0, 5)
}, [treeData])
// 获取本月纪念日(生日和忌日)
// 获取本月纪念日(生日和忌日)- 支持农历
const monthlyAnniversaries = useMemo(() => {
const currentMonth = new Date().getMonth() + 1
const members = Object.values(treeData.members)
const anniversaries: Array<{
@@ -136,33 +149,58 @@ export default function DashboardPage() {
type: 'birth' | 'death'
date: string
day: number
month: number
isLunar: boolean
lunarDisplay?: string
}> = []
members.forEach(member => {
// 检查生日
if (member.birthDate) {
const birthMonth = new Date(member.birthDate).getMonth() + 1
const birthDay = new Date(member.birthDate).getDate()
if (birthMonth === currentMonth) {
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: birthDay
day: result.day,
month: result.month,
isLunar,
lunarDisplay
})
}
}
// 检查忌日
if (member.deathDate) {
const deathMonth = new Date(member.deathDate).getMonth() + 1
const deathDay = new Date(member.deathDate).getDate()
if (deathMonth === currentMonth) {
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: deathDay
day: result.day,
month: result.month,
isLunar,
lunarDisplay
})
}
}
@@ -195,11 +233,33 @@ export default function DashboardPage() {
<div className="flex min-h-screen flex-col bg-background font-sans">
<SiteHeader />
<main className="flex-1 flex items-center justify-center px-4 md:px-6 relative">
<main className="flex-1 flex items-center justify-center px-4 md:px-6 relative -mt-24">
{/* 背景装饰图片 */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
{/* 建筑图片 - 左 */}
<div className="absolute left-24 top-[60%] -translate-y-1/2 w-[30rem] h-[30rem] opacity-30 rotate-12">
{/* 文字图片 - 左上角,轻微向右倾斜 */}
<div className="absolute left-32 top-[20%] w-[20rem] h-[20rem] opacity-20 rotate-3">
<Image
src="/wxz.png"
alt=""
fill
className="object-contain"
priority
/>
</div>
{/* 树图片1 - 右上角,轻微向左倾斜,对称布局 */}
<div className="absolute right-32 top-[20%] w-[20rem] h-[20rem] opacity-20 -rotate-3">
<Image
src="/tree1.png"
alt=""
fill
className="object-contain"
priority
/>
</div>
{/* 建筑图片 - 左下角,向右倾斜,营造动感 */}
<div className="absolute left-24 bottom-[8%] w-[28rem] h-[28rem] opacity-15 rotate-6">
<Image
src="/building.png"
alt=""
@@ -209,8 +269,8 @@ export default function DashboardPage() {
/>
</div>
{/* 树图片 - 右 */}
<div className="absolute right-24 top-[40%] -translate-y-1/2 w-[30rem] h-[30rem] opacity-30 -rotate-12">
{/* 树图片 - 右下角,向左倾斜,与建筑对称 */}
<div className="absolute right-24 bottom-[8%] w-[28rem] h-[28rem] opacity-15 -rotate-6">
<Image
src="/tree.png"
alt=""
@@ -223,6 +283,11 @@ export default function DashboardPage() {
{/* 内容区域 */}
<div className="max-w-3xl mx-auto text-center space-y-12">
{/* Slogan */}
<p className="text-2xl md:text-3xl text-foreground font-serif font-medium tracking-wider">
·
</p>
{/* 图标 */}
<div className="inline-flex items-center justify-center w-16 h-16 text-primary/20">
<BookOpen className="h-16 w-16" strokeWidth={1} />
@@ -237,33 +302,24 @@ export default function DashboardPage() {
{/* 哲理文字 - 滚动显示 */}
<div className="h-48 overflow-hidden relative">
<div className="space-y-8 text-muted-foreground antialiased animate-scroll">
<p className="text-lg md:text-xl font-normal leading-relaxed">
<p className="text-lg md:text-xl font-serif font-normal leading-relaxed tracking-wide">
·
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-85">
<p className="text-base md:text-lg font-serif font-normal leading-relaxed opacity-85 tracking-wide">
·
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-70">
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-60">
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-50">
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-40">
<p className="text-base md:text-lg font-serif font-normal leading-relaxed opacity-70 tracking-wide">
·
</p>
{/* 重复内容实现无缝滚动 */}
<p className="text-lg md:text-xl font-normal leading-relaxed">
<p className="text-lg md:text-xl font-serif font-normal leading-relaxed tracking-wide">
·
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-85">
<p className="text-base md:text-lg font-serif font-normal leading-relaxed opacity-85 tracking-wide">
·
</p>
<p className="text-base md:text-lg font-normal leading-relaxed opacity-70">
<p className="text-base md:text-lg font-serif font-normal leading-relaxed opacity-70 tracking-wide">
·
</p>
</div>
</div>
@@ -274,10 +330,10 @@ export default function DashboardPage() {
<Link href="/trees/new">
<Button
size="lg"
className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-300"
className="gap-3 px-8 py-6 text-lg font-serif bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-300 shadow-lg hover:shadow-xl"
>
<ArrowRight className="h-4 w-4" />
<ArrowRight className="h-5 w-5" />
</Button>
</Link>
</div>
@@ -321,7 +377,7 @@ export default function DashboardPage() {
<CollaboratorDialog />
)}
{permissions.canCreate(currentTree?.currentUserRole) && (
<Link href="/members/new">
<Link href={currentTree?.id ? `/members/new?treeId=${currentTree.id}` : '/members/new'}>
<Button className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90">
<UserPlus className="h-4 w-4" />
@@ -336,57 +392,64 @@ export default function DashboardPage() {
title="家族成员"
value={stats.totalMembers.toString()}
subtitle={`${stats.maleCount} · 女 ${stats.femaleCount}`}
icon={<Users className="h-4 w-4" />}
icon={<Users className="h-4 w-4" />}
href={currentTree?.id ? `/members?treeId=${currentTree.id}` : undefined}
/>
<StatCard
title="记录年代"
value={`${stats.yearsSpan}`}
subtitle={`始于 ${stats.earliestYear}`}
icon={<Clock className="h-4 w-4" />}
icon={<Clock className="h-4 w-4" />}
href={currentTree?.id ? `/timeline?treeId=${currentTree.id}` : undefined}
/>
<StatCard
title="繁衍代数"
value={`${stats.maxGeneration}`}
subtitle={`当前最高世代`}
icon={<Network className="h-4 w-4" />}
icon={<Network className="h-4 w-4" />}
href={currentTree?.id ? `/tree?treeId=${currentTree.id}` : undefined}
/>
<StatCard
title="在世 / 已故"
value={`${stats.livingMembers} / ${stats.deceasedMembers}`}
subtitle={`在世率 ${stats.totalMembers > 0 ? Math.round(stats.livingMembers / stats.totalMembers * 100) : 0}%`}
icon={<Users className="h-4 w-4" />}
icon={<Users className="h-4 w-4" />}
href={currentTree?.id ? `/members?treeId=${currentTree.id}` : undefined}
/>
<StatCard
title="平均寿命"
value={stats.averageLifespan > 0 ? `${stats.averageLifespan}` : '暂无数据'}
subtitle={`已统计 ${stats.deceasedMembers}`}
icon={<Clock className="h-4 w-4" />}
icon={<Clock className="h-4 w-4" />}
href={currentTree?.id ? `/timeline?treeId=${currentTree.id}` : undefined}
/>
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors">
<Users className="h-4 w-4" />
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}>
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors">
<Users className="h-4 w-4" />
</div>
</div>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-center gap-3">
<span className="text-3xl font-serif font-bold text-primary tracking-tight">
{stats.maleCount > 0 ? '1' : '0'}
</span>
<span className="text-2xl font-bold text-muted-foreground">:</span>
<span className="text-3xl font-serif font-bold text-primary tracking-tight">
{stats.maleCount > 0 ? (stats.femaleCount / stats.maleCount).toFixed(2) : '0'}
</span>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-center gap-3">
<span className="text-3xl font-serif font-bold text-primary tracking-tight">
{stats.maleCount > 0 ? '1' : '0'}
</span>
<span className="text-2xl font-bold text-muted-foreground">:</span>
<span className="text-3xl font-serif font-bold text-primary tracking-tight">
{stats.maleCount > 0 ? (stats.femaleCount / stats.maleCount).toFixed(2) : '0'}
</span>
</div>
<div className="text-center text-xs text-muted-foreground/80 font-medium mt-1">
{stats.maleCount} · {stats.femaleCount}
</div>
</div>
<div className="text-center text-xs text-muted-foreground/80 font-medium mt-1">
{stats.maleCount} · {stats.femaleCount}
</div>
</div>
</CardContent>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" />
</Card>
</CardContent>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" />
</Card>
</Link>
</div>
<Tabs defaultValue="recent" className="w-full">
@@ -397,7 +460,7 @@ export default function DashboardPage() {
<TabsTrigger value="statistics"></TabsTrigger>
<TabsTrigger value="migration"></TabsTrigger>
</TabsList>
<Link href="/members">
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '/members'}>
<Button variant="ghost" size="sm" className="text-muted-foreground gap-1">
<ArrowRight className="h-3 w-3" />
</Button>
@@ -405,63 +468,7 @@ export default function DashboardPage() {
</div>
<TabsContent value="recent" className="mt-0">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card className="lg:col-span-2 border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader>
<CardTitle className="font-serif"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-4">
{recentActivities.length > 0 ? (
recentActivities.map((activity, i) => (
<div key={activity.id} className="flex gap-3 group hover:bg-muted/30 p-2 rounded-lg transition-colors">
<div className="relative flex flex-col items-center">
<div className="h-full w-px bg-border group-last:hidden"></div>
<div className={`absolute top-2 h-2 w-2 rounded-full ring-4 ring-background ${
activity.action === 'create' ? 'bg-green-500' :
activity.action === 'update' ? 'bg-blue-500' :
activity.action === 'delete' ? 'bg-red-500' :
'bg-purple-500'
}`}></div>
</div>
<div className="pb-2 flex-1">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded-full ${
activity.action === 'create' ? 'bg-green-100 text-green-700' :
activity.action === 'update' ? 'bg-blue-100 text-blue-700' :
activity.action === 'delete' ? 'bg-red-100 text-red-700' :
'bg-purple-100 text-purple-700'
}`}>
{activity.action === 'create' ? '创建' :
activity.action === 'update' ? '更新' :
activity.action === 'delete' ? '删除' : '导入'}
</span>
<span className="font-medium">{activity.entityName || '未知'}</span>
<span className="text-xs text-muted-foreground">
{format(new Date(activity.timestamp), 'MM-dd HH:mm')}
</span>
</div>
<p className="text-xs text-muted-foreground">
{activity.action === 'create' && '新增了家族成员'}
{activity.action === 'update' && '更新了成员信息'}
{activity.action === 'delete' && '删除了成员'}
{activity.action === 'import' && '导入了家族数据'}
</p>
</div>
</div>
))
) : (
<div className="text-center py-12">
<p className="text-sm text-foreground font-light tracking-wide"></p>
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
<Card className="border-none shadow-sm bg-primary/5">
<CardHeader>
@@ -475,21 +482,42 @@ export default function DashboardPage() {
monthlyAnniversaries.map((anniversary, index) => {
const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear()
return (
<Link href={`/members/${anniversary.member.id}`} key={`${anniversary.member.id}-${anniversary.type}`}>
<Link href={`/members/${anniversary.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${anniversary.member.id}-${anniversary.type}`}>
<div className="flex items-center gap-4 p-3 rounded-lg bg-background/60 border border-border/50 hover:bg-background transition-colors cursor-pointer">
<div className="flex flex-col items-center justify-center w-12 h-12 rounded bg-primary/10 text-primary font-serif">
<span className="text-xs">
{new Date(anniversary.date).getMonth() + 1}
</span>
<span className="font-bold text-lg">{anniversary.day}</span>
</div>
{anniversary.isLunar && anniversary.lunarDisplay ? (
// 农历日期框 - 红色主题
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-gradient-to-br from-red-50 to-amber-50 border-2 border-red-200/50 text-red-800 font-serif shadow-sm">
<span className="text-[10px] leading-tight text-center px-1">
{anniversary.lunarDisplay.replace('月', '')}
</span>
</div>
) : (
// 公历日期框 - 蓝色主题
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-gradient-to-br from-blue-50 to-sky-50 border-2 border-blue-200/50 text-blue-800 font-serif shadow-sm">
<span className="text-xs font-medium">
{anniversary.month}
</span>
<span className="font-bold text-xl leading-tight">{anniversary.day}</span>
</div>
)}
<div className="flex-1">
<p className="font-medium">
{anniversary.member.fullName} {anniversary.type === 'birth' ? '诞辰' : '忌日'}
<p className="font-medium flex items-center gap-2">
<MemberNameWithStatus
name={anniversary.member.fullName}
isDead={!!anniversary.member.deathDate}
/> {anniversary.type === 'birth' ? '诞辰' : '忌日'}
{anniversary.isLunar && (
<span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
</span>
)}
</p>
<p className="text-sm text-muted-foreground">
{anniversary.member.generation} · {yearsAgo}
{anniversary.type === 'death' && ' · 已故'}
{anniversary.isLunar && anniversary.lunarDisplay && (
<span className="ml-1">· {anniversary.lunarDisplay}</span>
)}
</p>
</div>
</div>
@@ -504,6 +532,102 @@ export default function DashboardPage() {
</CardContent>
</Card>
</div>
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader>
<CardTitle className="font-serif"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-4">
{recentActivities.length > 0 ? (
recentActivities.map((activity, i) => (
<div key={activity.id} className="flex gap-3 group hover:bg-muted/30 p-2 rounded-lg transition-colors">
<div className="relative flex flex-col items-center">
<div className="h-full w-px bg-border group-last:hidden"></div>
<div className={`absolute top-2 h-2 w-2 rounded-full ring-4 ring-background ${
activity.action === 'CREATE' ? 'bg-green-500' :
activity.action === 'UPDATE' ? 'bg-blue-500' :
activity.action === 'DELETE' ? 'bg-red-500' :
'bg-purple-500'
}`}></div>
</div>
<div className="pb-2 flex-1">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded-full ${
activity.action === 'CREATE' ? 'bg-green-100 text-green-700' :
activity.action === 'UPDATE' ? 'bg-blue-100 text-blue-700' :
activity.action === 'DELETE' ? 'bg-red-100 text-red-700' :
'bg-purple-100 text-purple-700'
}`}>
{activity.action === 'CREATE' ? '创建' :
activity.action === 'UPDATE' ? '更新' :
activity.action === 'DELETE' ? '删除' : '导入'}
</span>
{activity.entityId && activity.entityType === 'MEMBER' ? (
<Link
href={`/members/${activity.entityId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
className="font-medium hover:text-primary hover:underline transition-colors"
>
{activity.entityName || '未知'}
</Link>
) : (
<span className="font-medium">{activity.entityName || '未知'}</span>
)}
<span className="text-xs text-muted-foreground">
{format(new Date(activity.timestamp), 'MM-dd HH:mm')}
</span>
</div>
{activity.action === 'UPDATE' && activity.changes ? (
<div className="text-xs text-muted-foreground space-y-0.5">
{(() => {
const changes = activity.changes as Record<string, { old: any, new: any }>
const fieldLabels: Record<string, string> = {
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]) => (
<div key={key}> {fieldLabels[key]}</div>
))}
{Object.keys(changes).length > 2 && (
<div className="italic"> {Object.keys(changes).length} </div>
)}
</>
) : <div></div>
})()}
</div>
) : (
<p className="text-xs text-muted-foreground">
{activity.action === 'CREATE' && '创建了成员'}
{activity.action === 'UPDATE' && '更新了成员'}
{activity.action === 'DELETE' && '删除了成员'}
{activity.action === 'IMPORT' && '导入了数据'}
</p>
)}
</div>
</div>
))
) : (
<div className="text-center py-12">
<p className="text-sm text-foreground font-light tracking-wide"></p>
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
</TabsContent>
@@ -525,19 +649,54 @@ export default function DashboardPage() {
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)
const thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
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
originalDate: member.birthDate,
isLunar: member.isLunarDate || false,
lunarDisplay,
month: thisYearBirth.getMonth() + 1,
day: thisYearBirth.getDate()
})
}
}
@@ -545,13 +704,44 @@ export default function DashboardPage() {
// 忌日
if (member.deathDate) {
const deathDate = new Date(member.deathDate)
const thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
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
originalDate: member.deathDate,
isLunar: member.isLunarDate || false,
lunarDisplay,
month: thisYearDeath.getMonth() + 1,
day: thisYearDeath.getDate()
})
}
}
@@ -563,21 +753,44 @@ export default function DashboardPage() {
upcomingEvents.map((event, index) => {
const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear()
return (
<Link href={`/members/${event.member.id}`} key={`${event.member.id}-${event.type}-${index}`}>
<Link href={`/members/${event.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${event.member.id}-${event.type}-${index}`}>
<Card className="hover:border-primary transition-colors cursor-pointer">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-primary/10 text-primary font-serif">
<span className="text-xs">{event.date.getMonth() + 1}</span>
<span className="font-bold text-lg">{event.date.getDate()}</span>
</div>
{event.isLunar && event.lunarDisplay ? (
// 农历日期框 - 红色主题
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-gradient-to-br from-red-50 to-amber-50 border-2 border-red-200/50 text-red-800 font-serif shadow-sm">
<span className="text-[10px] leading-tight text-center px-1">
{event.lunarDisplay}
</span>
</div>
) : (
// 公历日期框 - 蓝色主题
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-gradient-to-br from-blue-50 to-sky-50 border-2 border-blue-200/50 text-blue-800 font-serif shadow-sm">
<span className="text-xs font-medium">
{event.month}
</span>
<span className="font-bold text-xl leading-tight">{event.day}</span>
</div>
)}
<div className="flex-1">
<p className="font-medium text-sm">
{event.member.fullName} {event.type === 'birth' ? '诞辰' : '忌日'}
<p className="font-medium text-sm flex items-center gap-2">
<MemberNameWithStatus
name={event.member.fullName}
isDead={!!event.member.deathDate}
/> {event.type === 'birth' ? '诞辰' : '忌日'}
{event.isLunar && (
<span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
</span>
)}
</p>
<p className="text-xs text-muted-foreground">
{event.member.generation} · {yearsAgo}
{event.type === 'death' && ' · 已故'}
{event.isLunar && event.lunarDisplay && (
<span className="ml-1">· {event.lunarDisplay}</span>
)}
</p>
</div>
</div>
@@ -622,7 +835,7 @@ export default function DashboardPage() {
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{locationMembers.map((member: any) => (
<Link href={`/members/${member.id}`} key={member.id}>
<Link href={`/members/${member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={member.id}>
<div className="px-3 py-2 bg-muted rounded hover:bg-muted/80 transition-colors">
<p className="text-sm font-medium truncate">{member.fullName}</p>
<p className="text-xs text-muted-foreground"> {member.generation} </p>
@@ -652,9 +865,10 @@ function StatCard({
value,
subtitle,
icon,
}: { title: string; value: string; subtitle: string; icon: React.ReactNode }) {
return (
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group">
href,
}: { title: string; value: string; subtitle: string; icon: React.ReactNode; href?: string }) {
const cardContent = (
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">{title}</p>
@@ -670,4 +884,10 @@ function StatCard({
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" />
</Card>
)
if (href) {
return <Link href={href}>{cardContent}</Link>
}
return cardContent
}