init: AI培训与智能巡检系统

This commit is contained in:
selfrelease
2026-06-16 00:55:20 +08:00
commit c55598494b
201 changed files with 53131 additions and 0 deletions
+156
View File
@@ -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>
);
}