0.0.5.2
This commit is contained in:
@@ -7,7 +7,7 @@ import { useFamily } from "@/context/family-context"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { MemberForm } from "@/components/members/member-form"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen } from "lucide-react"
|
||||
import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import { db } from "@/lib/db"
|
||||
@@ -66,6 +66,28 @@ export default function MemberProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleQuickAddChild = () => {
|
||||
// 跳转到新增成员页面,并预填父母信息
|
||||
const params = new URLSearchParams()
|
||||
if (member.gender === 'MALE') {
|
||||
params.set('fatherId', member.id)
|
||||
} else {
|
||||
params.set('motherId', member.id)
|
||||
}
|
||||
params.set('generation', (member.generation + 1).toString())
|
||||
router.push(`/members/new?${params.toString()}`)
|
||||
}
|
||||
|
||||
const handleQuickAddSpouse = () => {
|
||||
// 跳转到新增成员页面,并预填配偶信息
|
||||
const params = new URLSearchParams()
|
||||
params.set('spouseId', member.id)
|
||||
params.set('generation', member.generation.toString())
|
||||
// 配偶性别与当前成员相反
|
||||
params.set('gender', member.gender === 'MALE' ? 'FEMALE' : 'MALE')
|
||||
router.push(`/members/new?${params.toString()}`)
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
@@ -145,7 +167,13 @@ export default function MemberProfilePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="default" onClick={handleQuickAddChild} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加子女
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleQuickAddSpouse} className="gap-2">
|
||||
<UserPlus className="h-4 w-4" /> 添加配偶
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => window.print()}>
|
||||
<Printer className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,18 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { MemberForm } from "@/components/members/member-form"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import { useMemo } from "react"
|
||||
|
||||
export default function NewMemberPage() {
|
||||
const { addMember, treeData } = useFamily()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { addMember, treeData, getMember } = useFamily()
|
||||
|
||||
const existingMembers = Object.values(treeData.members)
|
||||
|
||||
// 从URL参数获取预填信息
|
||||
const initialData = useMemo(() => {
|
||||
const fatherId = searchParams.get('fatherId')
|
||||
const motherId = searchParams.get('motherId')
|
||||
const spouseId = searchParams.get('spouseId')
|
||||
const generation = searchParams.get('generation')
|
||||
const gender = searchParams.get('gender')
|
||||
|
||||
if (!fatherId && !motherId && !spouseId && !generation && !gender) return undefined
|
||||
|
||||
const data: Partial<FamilyMember> = {}
|
||||
|
||||
// 处理父母信息
|
||||
if (fatherId) {
|
||||
data.fatherId = fatherId
|
||||
// 如果有父亲,尝试获取母亲(父亲的配偶)
|
||||
const father = getMember(fatherId)
|
||||
if (father?.spouseIds && father.spouseIds.length > 0) {
|
||||
data.motherId = father.spouseIds[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (motherId) {
|
||||
data.motherId = motherId
|
||||
// 如果有母亲,尝试获取父亲(母亲的配偶)
|
||||
const mother = getMember(motherId)
|
||||
if (mother?.spouseIds && mother.spouseIds.length > 0) {
|
||||
data.fatherId = mother.spouseIds[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 处理配偶信息
|
||||
if (spouseId) {
|
||||
data.spouseIds = [spouseId]
|
||||
}
|
||||
|
||||
// 处理世代
|
||||
if (generation) {
|
||||
data.generation = parseInt(generation)
|
||||
}
|
||||
|
||||
// 处理性别
|
||||
if (gender) {
|
||||
data.gender = gender.toUpperCase() as 'MALE' | 'FEMALE'
|
||||
}
|
||||
|
||||
return data
|
||||
}, [searchParams, getMember])
|
||||
|
||||
const handleSave = async (newMember: FamilyMember) => {
|
||||
try {
|
||||
@@ -39,7 +90,7 @@ export default function NewMemberPage() {
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold font-serif">新增家族成员</h1>
|
||||
</div>
|
||||
<MemberForm existingMembers={existingMembers} onSubmit={handleSave} onCancel={() => router.back()} />
|
||||
<MemberForm initialData={initialData} existingMembers={existingMembers} onSubmit={handleSave} onCancel={() => router.back()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+110
-10
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user