feat: add timeline page with year-based layout
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user