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

104 lines
3.5 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, 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>
);
}