Files
Train/apps/web/src/pages/ListPage.tsx
T
2026-06-16 00:55:20 +08:00

157 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../api/client';
import type { Category, ModelListItem, ModelQuery, Paged } from '../types';
import { FilterBar } from '../components/FilterBar';
import { TimelineView } from '../components/TimelineView';
import { GalleryView } from '../components/GalleryView';
import { useCollection } from '../lib/useCollection';
import { IconGallery, IconTimeline, IconFilter } from '../components/icons';
import { Button } from '../components/ui';
type ViewMode = 'gallery' | 'timeline';
export function ListPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [categories, setCategories] = useState<Category[]>([]);
const [query, setQuery] = useState<ModelQuery>({
page: 1,
pageSize: 1000,
sort: 'first_year',
order: 'asc',
category: searchParams.get('category') ?? undefined,
});
const [data, setData] = useState<Paged<ModelListItem> | null>(null);
const [view, setView] = useState<ViewMode>('gallery');
const [showFilters, setShowFilters] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [kw, setKw] = useState('');
const { collectedIds, toggle } = useCollection();
useEffect(() => {
api.categories().then(setCategories).catch(() => {});
}, []);
// URL ?category= 变化时同步(从首页章节跳入)
useEffect(() => {
const cat = searchParams.get('category') ?? undefined;
setQuery((q) => (q.category === cat ? q : { ...q, category: cat, page: 1 }));
}, [searchParams]);
useEffect(() => {
setLoading(true);
setError('');
api
.models(query)
.then(setData)
.catch((e) => setError(String(e)))
.finally(() => setLoading(false));
}, [query]);
const patch = (p: Partial<ModelQuery>) => {
setQuery((q) => ({ ...q, ...p, page: 1 }));
// 任意筛选变化都回到图鉴第 1 页
const next = new URLSearchParams(searchParams);
next.delete('gp');
if ('category' in p) {
if (p.category) next.set('category', p.category);
else next.delete('category');
}
setSearchParams(next, { replace: true });
};
// 图鉴分页:页码存于 URL ?gp=,从详情返回时可回到原页
const galleryPage = Math.max(1, Number(searchParams.get('gp')) || 1);
const setGalleryPage = (gp: number) => {
const next = new URLSearchParams(searchParams);
if (gp <= 1) next.delete('gp');
else next.set('gp', String(gp));
setSearchParams(next, { replace: true });
};
// 关键字(型号)输入防抖 → query.q
useEffect(() => {
const t = setTimeout(() => {
setQuery((q) =>
q.q === (kw || undefined) ? q : { ...q, q: kw || undefined, page: 1 },
);
}, 300);
return () => clearTimeout(t);
}, [kw]);
const activeFilterCount = useMemo(() => {
let n = 0;
if (query.category) n++;
if (query.status) n++;
if (query.country) n++;
if (query.yearFrom) n++;
if (query.yearTo) n++;
if (query.speedMin) n++;
return n;
}, [query]);
return (
<div className="page">
<div className="explore-bar">
<div className="viewswitch">
<button
className={view === 'gallery' ? 'active' : ''}
onClick={() => setView('gallery')}
>
<IconGallery size={16} />
</button>
<button
className={view === 'timeline' ? 'active' : ''}
onClick={() => setView('timeline')}
>
<IconTimeline size={16} />
</button>
</div>
<input
className="kw-input"
type="search"
placeholder="输入型号 / 关键字筛选…"
value={kw}
onChange={(e) => {
setKw(e.target.value);
setGalleryPage(1);
}}
/>
<Button
variant="secondary"
className="filter-toggle"
onClick={() => setShowFilters((s) => !s)}
>
<IconFilter size={16} /> {activeFilterCount ? ` · ${activeFilterCount}` : ''}
</Button>
{data && <span className="result-count muted">{data.total} </span>}
</div>
{showFilters && (
<div className="filter-panel">
<FilterBar categories={categories} query={query} onChange={patch} />
</div>
)}
{error && <p className="error">{error}</p>}
{loading && <p className="muted"></p>}
{data && !loading && (
<>
{view === 'gallery' && (
<GalleryView
models={data.items}
collectedIds={collectedIds}
onToggle={toggle}
page={galleryPage}
onPage={setGalleryPage}
/>
)}
{view === 'timeline' && <TimelineView models={data.items} />}
</>
)}
</div>
);
}