0.0.0.4
This commit is contained in:
+318
-81
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -5,9 +7,89 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
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 } from "lucide-react"
|
||||
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3 } from "lucide-react"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useMemo } from "react"
|
||||
import { StatisticsCharts } from "@/components/dashboard/statistics-charts"
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { treeData } = useFamily()
|
||||
|
||||
// 计算统计数据
|
||||
const stats = useMemo(() => {
|
||||
const members = Object.values(treeData.members)
|
||||
const totalMembers = members.length
|
||||
|
||||
// 计算最大代数
|
||||
const maxGeneration = Math.max(...members.map(m => m.generation || 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 = new Date().getFullYear() - earliestYear
|
||||
|
||||
return {
|
||||
totalMembers,
|
||||
maxGeneration,
|
||||
yearsSpan,
|
||||
earliestYear
|
||||
}
|
||||
}, [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 monthlyAnniversaries = useMemo(() => {
|
||||
const currentMonth = new Date().getMonth() + 1
|
||||
const members = Object.values(treeData.members)
|
||||
|
||||
const anniversaries: Array<{
|
||||
member: any
|
||||
type: 'birth' | 'death'
|
||||
date: string
|
||||
day: number
|
||||
}> = []
|
||||
|
||||
members.forEach(member => {
|
||||
// 检查生日
|
||||
if (member.birthDate) {
|
||||
const birthMonth = new Date(member.birthDate).getMonth() + 1
|
||||
const birthDay = new Date(member.birthDate).getDate()
|
||||
if (birthMonth === currentMonth) {
|
||||
anniversaries.push({
|
||||
member,
|
||||
type: 'birth',
|
||||
date: member.birthDate,
|
||||
day: birthDay
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 检查忌日
|
||||
if (member.deathDate) {
|
||||
const deathMonth = new Date(member.deathDate).getMonth() + 1
|
||||
const deathDay = new Date(member.deathDate).getDate()
|
||||
if (deathMonth === currentMonth) {
|
||||
anniversaries.push({
|
||||
member,
|
||||
type: 'death',
|
||||
date: member.deathDate,
|
||||
day: deathDay
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 按日期排序
|
||||
return anniversaries.sort((a, b) => a.day - b.day).slice(0, 5)
|
||||
}, [treeData])
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background font-sans">
|
||||
<SiteHeader />
|
||||
@@ -35,9 +117,24 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<StatCard title="家族成员" value="1,248" subtitle="较上月新增 12 人" icon={<Users className="h-4 w-4" />} />
|
||||
<StatCard title="记录年代" value="642 年" subtitle="始于 明朝洪武年间" icon={<Clock className="h-4 w-4" />} />
|
||||
<StatCard title="繁衍代数" value="24 代" subtitle="平均代差 26.5 年" icon={<Network className="h-4 w-4" />} />
|
||||
<StatCard
|
||||
title="家族成员"
|
||||
value={stats.totalMembers.toString()}
|
||||
subtitle={`记录在册的家族成员`}
|
||||
icon={<Users className="h-4 w-4" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="记录年代"
|
||||
value={`${stats.yearsSpan} 年`}
|
||||
subtitle={`始于 ${stats.earliestYear} 年`}
|
||||
icon={<Clock className="h-4 w-4" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="繁衍代数"
|
||||
value={`${stats.maxGeneration} 代`}
|
||||
subtitle={`当前最高世代`}
|
||||
icon={<Network className="h-4 w-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="recent" className="w-full">
|
||||
@@ -45,11 +142,14 @@ export default function DashboardPage() {
|
||||
<TabsList className="bg-muted/50">
|
||||
<TabsTrigger value="recent">最近更新</TabsTrigger>
|
||||
<TabsTrigger value="anniversaries">近期纪念日</TabsTrigger>
|
||||
<TabsTrigger value="statistics">统计图表</TabsTrigger>
|
||||
<TabsTrigger value="migration">迁徙记录</TabsTrigger>
|
||||
</TabsList>
|
||||
<Button variant="ghost" size="sm" className="text-muted-foreground gap-1">
|
||||
查看全部 <ArrowRight className="h-3 w-3" />
|
||||
</Button>
|
||||
<Link href="/members">
|
||||
<Button variant="ghost" size="sm" className="text-muted-foreground gap-1">
|
||||
查看全部 <ArrowRight className="h-3 w-3" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<TabsContent value="recent" className="mt-0">
|
||||
@@ -62,27 +162,38 @@ export default function DashboardPage() {
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-6">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="flex gap-4 group">
|
||||
<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 bg-primary ring-4 ring-background"></div>
|
||||
</div>
|
||||
<div className="pb-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium">李明远</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary/20 text-secondary-foreground">
|
||||
第 22 世
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">2小时前</span>
|
||||
{recentMembers.length > 0 ? (
|
||||
recentMembers.map((member, i) => (
|
||||
<Link href={`/members/${member.id}`} key={member.id}>
|
||||
<div className="flex gap-4 group hover:bg-muted/30 p-2 rounded-lg transition-colors cursor-pointer">
|
||||
<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 bg-primary ring-4 ring-background"></div>
|
||||
</div>
|
||||
<div className="pb-2 flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium">{member.fullName}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary/20 text-secondary-foreground">
|
||||
第 {member.generation} 世
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{member.gender === 'male' ? '男' : '女'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{member.birthDate && `生于 ${member.birthDate.split('-')[0]} 年`}
|
||||
{member.deathDate && ` · 卒于 ${member.deathDate.split('-')[0]} 年`}
|
||||
{!member.deathDate && member.birthDate && ` · 在世`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
更新了其长子 <span className="text-foreground font-medium">李子豪</span>{" "}
|
||||
的教育经历与职业信息。
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
暂无成员记录
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
@@ -97,67 +208,193 @@ export default function DashboardPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="flex items-center gap-4 p-3 rounded-lg bg-background/60 border border-border/50">
|
||||
<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">农历</span>
|
||||
<span className="font-bold text-lg">十五</span>
|
||||
{monthlyAnniversaries.length > 0 ? (
|
||||
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}`}>
|
||||
<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>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">
|
||||
{anniversary.member.fullName} {anniversary.type === 'birth' ? '诞辰' : '忌日'}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
第 {anniversary.member.generation} 世 · 距今 {yearsAgo} 年
|
||||
{anniversary.type === 'death' && ' · 已故'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-8 text-sm">
|
||||
本月暂无纪念日
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">李公讳文德 诞辰</p>
|
||||
<p className="text-sm text-muted-foreground">第 18 世祖 · 距今 120 年</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 p-3 rounded-lg bg-background/60 border border-border/50">
|
||||
<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">农历</span>
|
||||
<span className="font-bold text-lg">廿二</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">冬至祭祖</p>
|
||||
<p className="text-sm text-muted-foreground">全族公祭 · 陇西堂旧址</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-none shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">快捷入口</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto py-4 flex flex-col gap-2 hover:border-primary hover:text-primary bg-transparent"
|
||||
>
|
||||
<Map className="h-5 w-5" />
|
||||
<span className="text-xs">迁徙地图</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto py-4 flex flex-col gap-2 hover:border-primary hover:text-primary bg-transparent"
|
||||
>
|
||||
<BookOpen className="h-5 w-5" />
|
||||
<span className="text-xs">家训家规</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto py-4 flex flex-col gap-2 hover:border-primary hover:text-primary bg-transparent"
|
||||
>
|
||||
<Search className="h-5 w-5" />
|
||||
<span className="text-xs">字辈查询</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto py-4 flex flex-col gap-2 hover:border-primary hover:text-primary bg-transparent"
|
||||
>
|
||||
<Users className="h-5 w-5" />
|
||||
<span className="text-xs">关系计算</span>
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 近期纪念日标签页 */}
|
||||
<TabsContent value="anniversaries" className="mt-0">
|
||||
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">近期纪念日</CardTitle>
|
||||
<CardDescription>未来三个月内的生日和忌日</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{(() => {
|
||||
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
|
||||
}> = []
|
||||
|
||||
members.forEach(member => {
|
||||
// 生日
|
||||
if (member.birthDate) {
|
||||
const birthDate = new Date(member.birthDate)
|
||||
const thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||
if (thisYearBirth >= now && thisYearBirth <= threeMonthsLater) {
|
||||
upcomingEvents.push({
|
||||
member,
|
||||
type: 'birth',
|
||||
date: thisYearBirth,
|
||||
originalDate: member.birthDate
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 忌日
|
||||
if (member.deathDate) {
|
||||
const deathDate = new Date(member.deathDate)
|
||||
const thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||
if (thisYearDeath >= now && thisYearDeath <= threeMonthsLater) {
|
||||
upcomingEvents.push({
|
||||
member,
|
||||
type: 'death',
|
||||
date: thisYearDeath,
|
||||
originalDate: member.deathDate
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
|
||||
return upcomingEvents.length > 0 ? (
|
||||
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}`}>
|
||||
<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>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm">
|
||||
{event.member.fullName} {event.type === 'birth' ? '诞辰' : '忌日'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
第 {event.member.generation} 世 · {yearsAgo} 年
|
||||
{event.type === 'death' && ' · 已故'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="col-span-full text-center py-12 text-muted-foreground">
|
||||
未来三个月暂无纪念日
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 统计图表标签页 */}
|
||||
<TabsContent value="statistics" className="mt-0">
|
||||
<StatisticsCharts members={Object.values(treeData.members)} />
|
||||
</TabsContent>
|
||||
|
||||
{/* 迁徙记录标签页 */}
|
||||
<TabsContent value="migration" className="mt-0">
|
||||
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">家族迁徙记录</CardTitle>
|
||||
<CardDescription>记录家族成员的籍贯分布</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{useMemo(() => {
|
||||
const members = Object.values(treeData.members)
|
||||
const locationGroups: Record<string, any[]> = {}
|
||||
|
||||
members.forEach(member => {
|
||||
if (member.ancestralHome) {
|
||||
if (!locationGroups[member.ancestralHome]) {
|
||||
locationGroups[member.ancestralHome] = []
|
||||
}
|
||||
locationGroups[member.ancestralHome].push(member)
|
||||
}
|
||||
})
|
||||
|
||||
const sortedLocations = Object.entries(locationGroups)
|
||||
.sort((a, b) => b[1].length - a[1].length)
|
||||
|
||||
return sortedLocations.length > 0 ? (
|
||||
sortedLocations.map(([location, locationMembers]) => (
|
||||
<div key={location} className="border-l-4 border-primary pl-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-serif font-bold">{location}</h3>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{locationMembers.length} 人
|
||||
</span>
|
||||
</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}>
|
||||
<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>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
暂无迁徙记录
|
||||
</div>
|
||||
)
|
||||
}, [treeData])}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user