init: AI培训与智能巡检系统
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user