Files
goods/admin-frontend/src/components/StatsPage.tsx
T
sulaimaannaasif6866 01593dcbdc
CI / Go (api) (pull_request) Successful in 8s
CI / Python (ingestion) (pull_request) Successful in 13s
CI / Migrations (postgres) (pull_request) Successful in 14s
style(admin): 后台各页容器统一居中 (mx-auto)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-21 03:05:26 +00:00

126 lines
4.0 KiB
TypeScript

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="mx-auto 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>
);
}