196 lines
8.7 KiB
TypeScript
196 lines
8.7 KiB
TypeScript
"use client"
|
|
import { SiteHeader } from "@/components/site-header"
|
|
import { useFamily } from "@/context/family-context"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import { Clock, Map, ArrowRight } from "lucide-react"
|
|
|
|
export default function TimelinePage() {
|
|
const { treeData, getMember } = useFamily()
|
|
const members = Object.values(treeData.members)
|
|
|
|
// -- Timeline Logic --
|
|
// Flatten events: Births and Deaths
|
|
const events = members
|
|
.flatMap((m) => {
|
|
const evs = []
|
|
if (m.birthDate) {
|
|
evs.push({
|
|
date: m.birthDate,
|
|
year: Number.parseInt(m.birthDate.substring(0, 4)),
|
|
type: "birth",
|
|
member: m,
|
|
})
|
|
}
|
|
if (m.deathDate) {
|
|
evs.push({
|
|
date: m.deathDate,
|
|
year: Number.parseInt(m.deathDate.substring(0, 4)),
|
|
type: "death",
|
|
member: m,
|
|
})
|
|
}
|
|
return evs
|
|
})
|
|
.sort((a, b) => a.date.localeCompare(b.date))
|
|
|
|
// -- Migration Logic --
|
|
// Find moves between generations (Father -> Child location change)
|
|
const migrations = members
|
|
.flatMap((m) => {
|
|
if (!m.fatherId) return []
|
|
const father = getMember(m.fatherId)
|
|
if (!father) return []
|
|
|
|
// Check if location exists and is different
|
|
const loc1 = father.ancestralHome || father.birthPlace
|
|
const loc2 = m.ancestralHome || m.birthPlace
|
|
|
|
if (loc1 && loc2 && loc1 !== loc2) {
|
|
return [
|
|
{
|
|
year: m.birthDate ? Number.parseInt(m.birthDate.substring(0, 4)) : null,
|
|
from: loc1,
|
|
to: loc2,
|
|
person: m,
|
|
father: father,
|
|
},
|
|
]
|
|
}
|
|
return []
|
|
})
|
|
.sort((a, b) => (a.year || 0) - (b.year || 0))
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background flex flex-col font-sans">
|
|
<SiteHeader />
|
|
<main className="container mx-auto py-8 px-4 md:px-6 flex-1">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-serif font-bold mb-2">家族历史 (History)</h1>
|
|
<p className="text-muted-foreground">纵览家族发展的时间脉络与迁徙足迹。</p>
|
|
</div>
|
|
|
|
<Tabs defaultValue="timeline" className="w-full">
|
|
<TabsList className="mb-6 w-full md:w-auto">
|
|
<TabsTrigger value="timeline" className="flex items-center gap-2">
|
|
<Clock className="h-4 w-4" /> 时间轴 (Chronology)
|
|
</TabsTrigger>
|
|
<TabsTrigger value="migration" className="flex items-center gap-2">
|
|
<Map className="h-4 w-4" /> 迁徙图 (Migration)
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="timeline">
|
|
<Card className="border-none shadow-none bg-transparent">
|
|
<CardContent className="p-0">
|
|
<div className="relative border-l-2 border-primary/20 ml-6 md:ml-12 space-y-8 py-4">
|
|
{events.map((event, idx) => (
|
|
<div key={idx} className="relative pl-8 md:pl-12 group">
|
|
{/* Dot */}
|
|
<div
|
|
className={`absolute -left-[9px] top-1 h-4 w-4 rounded-full border-2 border-background transition-colors group-hover:scale-110 ${event.type === "birth" ? "bg-primary" : "bg-neutral-400"
|
|
}`}
|
|
/>
|
|
|
|
{/* Year Label (Mobile vs Desktop) */}
|
|
<div className="absolute -left-16 md:-left-24 top-0 w-12 md:w-20 text-right text-sm font-mono text-muted-foreground font-bold">
|
|
{event.year}
|
|
</div>
|
|
|
|
{/* Content Card */}
|
|
<div className="bg-card border border-border rounded-lg p-4 shadow-sm transition-all hover:shadow-md hover:border-primary/50 max-w-xl">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span
|
|
className={`text-xs font-bold px-2 py-0.5 rounded-full ${event.type === "birth" ? "bg-primary/10 text-primary" : "bg-neutral-100 text-neutral-500"
|
|
}`}
|
|
>
|
|
{event.type === "birth" ? "诞生" : "逝世"}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground font-mono">{event.date}</span>
|
|
</div>
|
|
<h3 className="text-lg font-serif font-bold">
|
|
{event.member.fullName}
|
|
<span className="text-sm font-normal text-muted-foreground ml-2">
|
|
({event.member.generation}世)
|
|
</span>
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{event.type === "birth"
|
|
? `出生于 ${event.member.birthPlace || event.member.ancestralHome || "未知地点"}`
|
|
: `享年 ${event.year - Number.parseInt(event.member.birthDate?.substring(0, 4) || "0")} 岁`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{events.length === 0 && (
|
|
<div className="pl-8 text-muted-foreground italic">暂无时间记录。请完善成员的出生日期信息。</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="migration">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="font-serif">迁徙记录 (Records)</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[600px] pr-4">
|
|
<div className="space-y-6">
|
|
{migrations.map((mig, idx) => (
|
|
<div key={idx} className="flex flex-col gap-2 p-4 rounded-lg bg-muted/30 border border-border">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
|
|
<span className="font-mono bg-background px-1 rounded">
|
|
{mig.year ? `${mig.year}年` : "未知年代"}
|
|
</span>
|
|
<span>第 {mig.person.generation} 世</span>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex-1 text-center p-2 bg-background rounded border border-border/50">
|
|
<div className="text-xs text-muted-foreground mb-1">迁出地</div>
|
|
<div className="font-bold">{mig.from}</div>
|
|
<div className="text-xs text-muted-foreground mt-1">{mig.father.fullName}</div>
|
|
</div>
|
|
<ArrowRight className="text-muted-foreground" />
|
|
<div className="flex-1 text-center p-2 bg-background rounded border border-border/50">
|
|
<div className="text-xs text-muted-foreground mb-1">迁入地</div>
|
|
<div className="font-bold text-primary">{mig.to}</div>
|
|
<div className="text-xs text-muted-foreground mt-1">{mig.person.fullName}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{migrations.length === 0 && (
|
|
<p className="text-center text-muted-foreground py-10">
|
|
暂无迁徙记录。
|
|
<br />
|
|
系统会自动分析父子代之间的籍贯/出生地差异来生成迁徙路径。
|
|
</p>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-neutral-50 dark:bg-neutral-900 border-dashed">
|
|
<CardContent className="flex flex-col items-center justify-center h-[600px] text-muted-foreground">
|
|
<Map className="h-16 w-16 mb-4 opacity-20" />
|
|
<p className="max-w-xs text-center">
|
|
地图可视化需要地理编码服务支持。
|
|
<br />
|
|
当前版本仅支持列表展示迁徙路径。
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|