feat: 数据概览/操作日志/批量操作 + 首页合格档案数
后台新增「数据概览」(商品/合格/按状态/品牌/分类/待审核) 与「操作日志」(全局审计分页);商品列表支持多选批量改状态/分类。公开首页标题改为「天工」并展示合格档案数;新增公开接口 /api/v1/stats 与后台 /api/stats、/api/audit、/api/products/bulk。合格口径=quality_score≥0.6 且在用。 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, ApiError } from "../api";
|
||||
import type { AdminStats } from "../types";
|
||||
import { BarChart3, Package, CheckCircle2, Tag, FolderTree, Inbox } from "lucide-react";
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
active: "在用",
|
||||
merged: "已合并",
|
||||
deprecated: "已停用",
|
||||
};
|
||||
|
||||
function Card({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
hint?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-white p-5">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-gray-800">{value}</div>
|
||||
{hint && <div className="mt-1 text-xs text-gray-400">{hint}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StatsPage() {
|
||||
const [stats, setStats] = useState<AdminStats | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.stats()
|
||||
.then(setStats)
|
||||
.catch((e) => setError(e instanceof ApiError ? e.message : "加载失败"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-gray-800">
|
||||
<BarChart3 className="h-5 w-5 text-emerald-600" /> 数据概览
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-3 rounded bg-red-50 px-4 py-2 text-sm text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
{!stats ? (
|
||||
<div className="text-sm text-gray-400">加载中…</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||
<Card
|
||||
icon={<Package className="h-4 w-4" />}
|
||||
label="商品总数"
|
||||
value={stats.products.toLocaleString()}
|
||||
/>
|
||||
<Card
|
||||
icon={<CheckCircle2 className="h-4 w-4 text-emerald-600" />}
|
||||
label="合格档案"
|
||||
value={stats.qualified.toLocaleString()}
|
||||
hint={`质量分 ≥ ${stats.min_score} 且在用`}
|
||||
/>
|
||||
<Card
|
||||
icon={<BarChart3 className="h-4 w-4" />}
|
||||
label="平均质量分"
|
||||
value={Math.round(stats.avg_quality * 100)}
|
||||
hint="满分 100"
|
||||
/>
|
||||
<Card
|
||||
icon={<Tag className="h-4 w-4" />}
|
||||
label="品牌数"
|
||||
value={stats.brands.toLocaleString()}
|
||||
/>
|
||||
<Card
|
||||
icon={<FolderTree className="h-4 w-4" />}
|
||||
label="分类数"
|
||||
value={stats.categories.toLocaleString()}
|
||||
/>
|
||||
<Card
|
||||
icon={<Inbox className="h-4 w-4" />}
|
||||
label="待审核投稿"
|
||||
value={stats.pending_submissions.toLocaleString()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-lg border bg-white p-5">
|
||||
<h3 className="mb-3 text-sm font-medium text-gray-700">按状态分布</h3>
|
||||
<div className="space-y-2">
|
||||
{Object.keys(stats.by_status).length === 0 ? (
|
||||
<div className="text-sm text-gray-400">暂无数据</div>
|
||||
) : (
|
||||
Object.entries(stats.by_status).map(([st, n]) => {
|
||||
const pct = stats.products > 0 ? (n / stats.products) * 100 : 0;
|
||||
return (
|
||||
<div key={st} className="flex items-center gap-3 text-sm">
|
||||
<span className="w-16 text-gray-600">
|
||||
{STATUS_LABEL[st] || st}
|
||||
</span>
|
||||
<div className="h-3 flex-1 overflow-hidden rounded bg-gray-100">
|
||||
<div
|
||||
className="h-full bg-emerald-500"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-12 text-right text-gray-500">{n}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user