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