127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import { api } from '../api/client';
|
||
import type { ModelListItem } from '../types';
|
||
import {
|
||
computeSpeedEvolution,
|
||
ERAS,
|
||
pickRepresentatives,
|
||
} from '../lib/eras';
|
||
import { Thumb } from '../components/Thumb';
|
||
import { CategoryIcon, IconForward } from '../components/icons';
|
||
import { fmtEra, fmtValueUnit } from '../lib/format';
|
||
import { EvolutionCurve } from '../components/EvolutionCurve';
|
||
|
||
export function HomePage() {
|
||
const [models, setModels] = useState<ModelListItem[]>([]);
|
||
|
||
useEffect(() => {
|
||
api.models({ pageSize: 1000 }).then((d) => setModels(d.items)).catch(() => {});
|
||
}, []);
|
||
|
||
const evo = computeSpeedEvolution(models);
|
||
const total = models.length;
|
||
const years = models
|
||
.map((m) => m.first_year)
|
||
.filter((y): y is number => typeof y === 'number');
|
||
const minYear = years.length ? Math.min(...years) : 1881;
|
||
const topSpeed = Math.max(0, ...models.map((m) => m.max_speed_value ?? 0));
|
||
|
||
return (
|
||
<div className="home">
|
||
{/* 英雄区 + 时速演进:并排一行 */}
|
||
<section className="hero-row">
|
||
<div className="hero">
|
||
<h1>一部跑在铁轨上的中国工业史</h1>
|
||
<p className="hero-sub">
|
||
从蒸汽的轰鸣到复兴号的风驰,跟随这条主线,看中国机车如何
|
||
<strong>引进、消化、再到自主领跑</strong>。
|
||
</p>
|
||
<div className="hero-stats">
|
||
<div>
|
||
<b>{total || '—'}</b>
|
||
<span>收录车型</span>
|
||
</div>
|
||
<div>
|
||
<b>{minYear}</b>
|
||
<span>起始年代</span>
|
||
</div>
|
||
<div>
|
||
<b>{topSpeed || '—'}</b>
|
||
<span>最高时速 km/h</span>
|
||
</div>
|
||
<div>
|
||
<b>4</b>
|
||
<span>技术时代</span>
|
||
</div>
|
||
</div>
|
||
<Link to="/explore" className="hero-cta">
|
||
进入图鉴探索 <IconForward size={16} />
|
||
</Link>
|
||
</div>
|
||
|
||
<div className="hero-evo">
|
||
<h2>时速的跃迁</h2>
|
||
<p className="muted eras-intro">
|
||
一条曲线读懂百年提速:从蒸汽机车的数十公里,到高铁的数百公里。
|
||
</p>
|
||
{evo.length >= 2 && <EvolutionCurve points={evo} />}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 主线:四个时代章节 */}
|
||
<section className="eras">
|
||
<h2>沿着时间的铁轨</h2>
|
||
<div className="era-rail">
|
||
{ERAS.map((era, i) => {
|
||
const reps = pickRepresentatives(models, era, 4);
|
||
return (
|
||
<article
|
||
className="era"
|
||
key={era.key}
|
||
style={{ ['--accent' as string]: era.accent }}
|
||
>
|
||
<div className="era-head">
|
||
<span className="era-icon">
|
||
<CategoryIcon category={era.categories[0]} size={26} />
|
||
</span>
|
||
<div>
|
||
<h3>
|
||
<span className="era-step">{i + 1}</span>
|
||
{era.title}
|
||
</h3>
|
||
<span className="era-period">{era.period}</span>
|
||
</div>
|
||
</div>
|
||
<p className="era-blurb">{era.blurb}</p>
|
||
<div className="era-reps">
|
||
{reps.map((m) => (
|
||
<Link key={m.id} to={`/models/${m.id}`} className="mini">
|
||
{m.cover_url ? (
|
||
<img src={m.cover_url} alt={m.model_code} loading="lazy" />
|
||
) : (
|
||
<Thumb category={m.category} code={m.model_code} size="sm" />
|
||
)}
|
||
<span className="mini-code">{m.model_code}</span>
|
||
<span className="mini-meta">
|
||
{fmtValueUnit(m.max_speed_value, m.max_speed_unit)} ·{' '}
|
||
{fmtEra(m.first_year, m.last_year)}
|
||
</span>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
<Link
|
||
to={`/explore?category=${encodeURIComponent(era.categories[0])}`}
|
||
className="era-more"
|
||
>
|
||
探索全部{era.title.replace('时代', '')}车型 <IconForward size={14} />
|
||
</Link>
|
||
</article>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|