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