This commit is contained in:
freedakgmail
2025-11-23 16:12:05 +08:00
parent 3375a79ed4
commit 222824f438
40 changed files with 38962 additions and 624 deletions
+110 -10
View File
@@ -63,6 +63,14 @@ export default function DashboardPage() {
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
@@ -74,11 +82,27 @@ export default function DashboardPage() {
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
earliestYear,
averageLifespan
}
}, [treeData])
@@ -184,11 +208,11 @@ export default function DashboardPage() {
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<StatCard
title="家族成员"
value={stats.totalMembers.toString()}
subtitle={`记录在册的家族成员`}
subtitle={`${stats.maleCount} · 女 ${stats.femaleCount}`}
icon={<Users className="h-4 w-4" />}
/>
<StatCard
@@ -203,6 +227,79 @@ export default function DashboardPage() {
subtitle={`当前最高世代`}
icon={<Network className="h-4 w-4" />}
/>
<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" />}
/>
<StatCard
title="平均寿命"
value={stats.averageLifespan > 0 ? `${stats.averageLifespan}` : '暂无数据'}
subtitle={`已统计 ${stats.deceasedMembers}`}
icon={<Clock className="h-4 w-4" />}
/>
<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" />
</div>
</div>
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-center gap-3 py-2">
<span className="text-5xl font-serif font-bold text-blue-600 dark:text-blue-400 tracking-wider">
1
</span>
<span className="text-4xl font-bold text-muted-foreground">:</span>
<span className="text-5xl font-serif font-bold text-pink-600 dark:text-pink-400 tracking-wider">
{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-2">
男性 : 女性
</div>
{/* 双色进度条 */}
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
{stats.maleCount}
</span>
<span className="text-pink-600 dark:text-pink-400 font-medium flex items-center gap-1">
{stats.femaleCount}
<span className="w-2 h-2 rounded-full bg-pink-500"></span>
</span>
</div>
<div className="h-3 bg-muted rounded-full overflow-hidden flex">
<div
className="h-full bg-gradient-to-r from-blue-600 to-blue-500 transition-all duration-500 flex items-center justify-center"
style={{ width: `${stats.totalMembers > 0 ? (stats.maleCount / stats.totalMembers * 100) : 0}%` }}
>
{stats.totalMembers > 0 && stats.maleCount > 0 && (
<span className="text-[10px] text-white font-bold px-1">
{Math.round(stats.maleCount / stats.totalMembers * 100)}%
</span>
)}
</div>
<div
className="h-full bg-gradient-to-r from-pink-500 to-pink-600 transition-all duration-500 flex items-center justify-center"
style={{ width: `${stats.totalMembers > 0 ? (stats.femaleCount / stats.totalMembers * 100) : 0}%` }}
>
{stats.totalMembers > 0 && stats.femaleCount > 0 && (
<span className="text-[10px] text-white font-bold px-1">
{Math.round(stats.femaleCount / stats.totalMembers * 100)}%
</span>
)}
</div>
</div>
</div>
</div>
</CardContent>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 opacity-0 group-hover:opacity-100 transition-opacity" />
</Card>
</div>
<Tabs defaultValue="recent" className="w-full">
@@ -487,17 +584,20 @@ function StatCard({
icon,
}: { title: string; value: string; subtitle: string; icon: React.ReactNode }) {
return (
<Card className="border-none shadow-sm bg-card/50 backdrop-blur transition-all hover:bg-card hover:shadow-md">
<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 space-y-0 pb-2">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
<div className="text-muted-foreground bg-muted p-2 rounded-full">{icon}</div>
<div className="flex items-center justify-between mb-4">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">{title}</p>
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors">
{icon}
</div>
</div>
<div className="flex flex-col mt-2">
<span className="text-3xl font-serif font-bold text-foreground">{value}</span>
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
<div className="flex flex-col space-y-1">
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{value}</span>
<p className="text-xs text-muted-foreground/80 font-medium">{subtitle}</p>
</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>
)
}