feat: add timeline page with year-based layout

This commit is contained in:
2026-01-27 21:40:14 +08:00
parent 11a876ebbe
commit 1e99c9a4e6
+157
View File
@@ -0,0 +1,157 @@
import { getAIEvents } from '@/hooks/useAIEvents';
import { Metadata } from 'next';
export const revalidate = 3600; // ISR 1小时
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
return {
title: locale === 'zh' ? 'AI 发展时间轴' : 'AI Timeline',
description: locale === 'zh'
? '探索人工智能大语言模型的发展历程,从 2017 年 Transformer 到今天'
: 'Explore the evolution of AI large language models from 2017 Transformer to today',
};
}
export default async function TimelinePage() {
const events = await getAIEvents();
// 按年份分组
const eventsByYear = events.reduce((acc, event) => {
const year = new Date(event.eventDate).getFullYear();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(event);
return acc;
}, {} as Record<number, typeof events>);
// 按年份降序排序
const sortedYears = Object.keys(eventsByYear)
.map(Number)
.sort((a, b) => b - a);
if (events.length === 0) {
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark flex items-center justify-center">
<div className="text-center">
<h1 className="font-display font-black text-4xl mb-4">
</h1>
<p className="font-mono text-gray-600">
...
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background-light dark:bg-background-dark">
{/* Header */}
<header className="pt-32 pb-16 px-4 max-w-[1600px] mx-auto">
<div className="text-center">
<h1 className="font-display font-black text-6xl md:text-8xl tracking-tight leading-none text-black dark:text-white mb-6">
AI
</h1>
<p className="font-mono text-lg md:text-xl max-w-2xl mx-auto bg-white dark:bg-black border border-black dark:border-white p-4 shadow-hard">
Transformer AGI: 大语言模型的进化之路
</p>
</div>
</header>
{/* Timeline */}
<main className="px-4 md:px-12 max-w-[1600px] mx-auto pb-32">
{sortedYears.map((year, index) => (
<section
key={year}
className={`relative min-h-[500px] mb-32 flex ${
index % 2 === 0 ? 'flex-row' : 'flex-row-reverse'
}`}
>
{/* Year Label */}
<div
className={`absolute ${index % 2 === 0 ? 'left-0' : 'right-0'} -top-8 z-20`}
>
<div
className={`${
index % 2 === 0 ? 'bg-primary' : 'bg-secondary'
} border-4 border-black px-6 py-2 ${
index % 2 === 0 ? '-rotate-2' : 'rotate-2'
} shadow-hard`}
>
<span className="font-display font-black text-3xl md:text-5xl">
{year}
</span>
</div>
</div>
{/* Events Container */}
<div
className={`w-full ${
index % 2 === 0 ? 'pl-0 md:pl-8 pr-0 md:pr-32' : 'pl-0 md:pl-32 pr-0 md:pr-8'
} pt-20`}
>
<div className="flex flex-nowrap overflow-x-visible items-center justify-start">
{eventsByYear[year].map((event, eventIndex) => (
<div
key={event.id}
className="stack-card relative w-72 h-96 flex-shrink-0 bg-surface-light dark:bg-surface-dark border-4 border-black dark:border-white p-4 shadow-hard -mr-48 md:-mr-56 hover:z-50 hover:-translate-y-5 hover:scale-105 transition-all duration-300"
style={{
zIndex: Math.max(1, 50 - eventIndex * 10),
transform: `rotate(${(Math.random() - 0.5) * 6}deg)`,
}}
>
{/* Tape decoration */}
<div
className="tape absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-6"
/>
{/* Image */}
<div className="h-40 bg-primary border-2 border-black dark:border-white mb-4 flex items-center justify-center overflow-hidden">
<img
src={event.imageUrl}
alt={event.title}
className="w-full h-full object-cover"
/>
</div>
{/* Content */}
<h3 className="font-display font-bold text-xl leading-none mb-2 uppercase">
{event.title}
</h3>
<p className="text-xs leading-snug opacity-80 line-clamp-4 mb-4">
{event.description}
</p>
{/* Date */}
<div className="absolute bottom-4 left-4 text-[10px] font-bold bg-black text-white px-2">
{new Date(event.eventDate).toLocaleDateString('zh-CN')}
</div>
{/* Source Link */}
{event.sourceUrl && (
<a
href={event.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-4 right-4 text-[10px] font-bold underline"
>
</a>
)}
</div>
))}
</div>
</div>
</section>
))}
</main>
</div>
);
}