82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
|
|
import { useEffect, useState } from "react";
|
||
|
|
import { Package, ClipboardList, Loader2, AlertCircle } from "lucide-react";
|
||
|
|
import { fetchDashboardStats, type DashboardStats } from "../../services/dashboardApi";
|
||
|
|
|
||
|
|
function StatCard({
|
||
|
|
label,
|
||
|
|
total,
|
||
|
|
pending,
|
||
|
|
progress,
|
||
|
|
done,
|
||
|
|
icon: Icon,
|
||
|
|
}: {
|
||
|
|
label: string;
|
||
|
|
total: number;
|
||
|
|
pending: number;
|
||
|
|
progress: number;
|
||
|
|
done: number;
|
||
|
|
icon: React.ComponentType<{ className?: string }>;
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<div className="rounded-xl bg-white p-5 shadow-sm">
|
||
|
|
<div className="mb-4 flex items-center gap-2">
|
||
|
|
<Icon className="h-5 w-5 text-blue-600" />
|
||
|
|
<h3 className="font-semibold text-gray-800">{label}</h3>
|
||
|
|
<span className="ml-auto text-2xl font-bold text-gray-800">{total}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex h-2 overflow-hidden rounded-full bg-gray-100">
|
||
|
|
{pending > 0 && (
|
||
|
|
<div className="bg-yellow-400" style={{ width: `${(pending / Math.max(total, 1)) * 100}%` }} />
|
||
|
|
)}
|
||
|
|
{progress > 0 && (
|
||
|
|
<div className="bg-blue-500" style={{ width: `${(progress / Math.max(total, 1)) * 100}%` }} />
|
||
|
|
)}
|
||
|
|
{done > 0 && (
|
||
|
|
<div className="bg-green-500" style={{ width: `${(done / Math.max(total, 1)) * 100}%` }} />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="mt-3 flex gap-4 text-xs text-gray-500">
|
||
|
|
<span className="flex items-center gap-1"><span className="inline-block h-2 w-2 rounded-full bg-yellow-400" />待处理 {pending}</span>
|
||
|
|
<span className="flex items-center gap-1"><span className="inline-block h-2 w-2 rounded-full bg-blue-500" />进行中 {progress}</span>
|
||
|
|
<span className="flex items-center gap-1"><span className="inline-block h-2 w-2 rounded-full bg-green-500" />已完成 {done}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function AdminDashboard() {
|
||
|
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchDashboardStats()
|
||
|
|
.then(setStats)
|
||
|
|
.catch(() => setError("加载统计数据失败,请确认后端已启动"))
|
||
|
|
.finally(() => setLoading(false));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return <div className="flex items-center justify-center py-20"><Loader2 className="h-8 w-8 animate-spin text-blue-500" /></div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error) {
|
||
|
|
return <div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"><AlertCircle className="h-4 w-4" />{error}</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!stats) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<div className="mb-6">
|
||
|
|
<h2 className="text-xl font-bold text-gray-800">全局生产概览</h2>
|
||
|
|
<p className="mt-1 text-sm text-gray-500">PC端与移动端共享同一后台数据</p>
|
||
|
|
</div>
|
||
|
|
<div className="grid gap-5 md:grid-cols-2">
|
||
|
|
<StatCard label="产品统计" total={stats.products_total} pending={stats.products_pending} progress={stats.products_in_progress} done={stats.products_completed} icon={Package} />
|
||
|
|
<StatCard label="任务统计" total={stats.tasks_total} pending={stats.tasks_pending} progress={stats.tasks_in_progress} done={stats.tasks_completed} icon={ClipboardList} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|