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
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../api/client';
import { useAuth } from '../lib/auth';
import { Button, PageHeader, EmptyState } from '../components/ui';
import { IconImage, IconBack } from '../components/icons';
import type { Photo } from '../types';
type Candidate = Photo & { modelCode: string; category: string };
export function AdminPhotosPage() {
const { user } = useAuth();
const [items, setItems] = useState<Candidate[]>([]);
const [busy, setBusy] = useState(false);
const isAdmin = user?.role === 'admin';
const load = () => api.candidatePhotos().then(setItems).catch(() => {});
useEffect(() => {
if (isAdmin) load();
}, [isAdmin]);
if (!user) {
return (
<div className="page">
<p className="muted"></p>
<Link to="/" className="back"><IconBack size={14} /> </Link>
</div>
);
}
if (!isAdmin) {
return (
<div className="page">
<p className="muted">访</p>
<Link to="/" className="back"><IconBack size={14} /> </Link>
</div>
);
}
const confirm = async (pid: number) => {
await api.confirmPhoto(pid).catch(() => {});
setItems((xs) => xs.filter((x) => x.id !== pid));
};
const remove = async (pid: number) => {
await api.deletePhoto(pid).catch(() => {});
setItems((xs) => xs.filter((x) => x.id !== pid));
};
const confirmAll = async () => {
setBusy(true);
for (const it of items) await api.confirmPhoto(it.id).catch(() => {});
setBusy(false);
load();
};
return (
<div className="page">
<PageHeader
title="候选审图"
subtitle="批量取图脚本入库的候选照片,确认后即成为图鉴封面与公共图库内容。"
actions={
items.length > 0 ? (
<Button variant="primary" onClick={confirmAll} disabled={busy}>
{busy ? '处理中…' : `全部确认(${items.length}`}
</Button>
) : undefined
}
/>
{items.length === 0 ? (
<EmptyState icon={<IconImage size={30} />} text="没有待确认的候选照片。可在本机运行 npm run fetch-images 灌入候选。" />
) : (
<div className="review-grid">
{items.map((p) => (
<div className="review-card" key={p.id}>
<a href={p.url} target="_blank" rel="noreferrer" className="review-img">
<img src={p.url} alt={p.modelCode} loading="lazy" />
</a>
<div className="review-meta">
<Link to={`/models/${p.modelId}`} className="review-code">
{p.modelCode}
</Link>
<span className="muted">{p.category}</span>
<span className="muted review-attr">
© {p.author || '未署名'}
{p.license ? ` · ${p.license}` : ''}
{p.sourceUrl ? (
<>
{' · '}
<a href={p.sourceUrl} target="_blank" rel="noreferrer"></a>
</>
) : null}
</span>
</div>
<div className="review-actions">
<Button variant="primary" size="sm" onClick={() => confirm(p.id)}></Button>
<Button variant="danger" size="sm" onClick={() => remove(p.id)}></Button>
</div>
</div>
))}
</div>
)}
</div>
);
}