application [cat-admin-web] view page [pages/dashboard] development
This commit is contained in:
250
apps/cat-admin-web/src/pages/dashboard.tsx
Normal file
250
apps/cat-admin-web/src/pages/dashboard.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { entities } from "@/lib/nvwa";
|
||||
|
||||
interface CatInventoryStats {
|
||||
totalCats: number;
|
||||
availableCats: number;
|
||||
gradeDistribution: { [key: string]: number };
|
||||
}
|
||||
|
||||
interface ReservationStats {
|
||||
totalReservations: number;
|
||||
statusDistribution: { [key: string]: number };
|
||||
recentReservations: number; // 最近7天
|
||||
}
|
||||
|
||||
interface QueueStatusSummary {
|
||||
queuing: number;
|
||||
reserved: number;
|
||||
completed: number;
|
||||
cancelled: number;
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [catStats, setCatStats] = useState<CatInventoryStats>({
|
||||
totalCats: 0,
|
||||
availableCats: 0,
|
||||
gradeDistribution: {}
|
||||
});
|
||||
|
||||
const [reservationStats, setReservationStats] = useState<ReservationStats>({
|
||||
totalReservations: 0,
|
||||
statusDistribution: {},
|
||||
recentReservations: 0
|
||||
});
|
||||
|
||||
const [queueStats, setQueueStats] = useState<QueueStatusSummary>({
|
||||
queuing: 0,
|
||||
reserved: 0,
|
||||
completed: 0,
|
||||
cancelled: 0
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 获取猫咪库存数据
|
||||
const { data: cats } = await entities.from("cat").select("id, is_available, grade");
|
||||
if (cats) {
|
||||
const totalCats = cats.length;
|
||||
const availableCats = cats.filter(cat => cat.is_available).length;
|
||||
const gradeDistribution = cats.reduce((acc, cat) => {
|
||||
acc[cat.grade] = (acc[cat.grade] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as { [key: string]: number });
|
||||
|
||||
setCatStats({ totalCats, availableCats, gradeDistribution });
|
||||
}
|
||||
|
||||
// 获取预约数据
|
||||
const { data: reservations } = await entities.from("reservation").select("id, status, created_at");
|
||||
if (reservations) {
|
||||
const totalReservations = reservations.length;
|
||||
const statusDistribution = reservations.reduce((acc, res) => {
|
||||
acc[res.status] = (acc[res.status] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as { [key: string]: number });
|
||||
|
||||
// 计算最近7天的预约
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
const recentReservations = reservations.filter(res =>
|
||||
new Date(res.created_at) >= sevenDaysAgo
|
||||
).length;
|
||||
|
||||
setReservationStats({ totalReservations, statusDistribution, recentReservations });
|
||||
|
||||
// 设置排队状态摘要
|
||||
setQueueStats({
|
||||
queuing: statusDistribution.queuing || 0,
|
||||
reserved: statusDistribution.reserved || 0,
|
||||
completed: statusDistribution.completed || 0,
|
||||
cancelled: statusDistribution.cancelled || 0
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to load dashboard data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">仪表板</h1>
|
||||
<p className="text-muted-foreground">猫舍管理概览</p>
|
||||
</div>
|
||||
|
||||
{/* 猫咪库存概览 */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">总猫咪数量</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{catStats.totalCats}</div>
|
||||
<p className="text-xs text-muted-foreground">庇护所中的猫咪总数</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">可售卖猫咪</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{catStats.availableCats}</div>
|
||||
<p className="text-xs text-muted-foreground">当前可预约的猫咪数量</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">等级分布</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(catStats.gradeDistribution).map(([grade, count]) => (
|
||||
<div key={grade} className="flex items-center justify-between">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{grade}级
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">可售卖比例</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{catStats.totalCats > 0 ? Math.round((catStats.availableCats / catStats.totalCats) * 100) : 0}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">可售卖猫咪占总数比例</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 预约统计 */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>预约统计</CardTitle>
|
||||
<CardDescription>总体预约情况概览</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">总预约数</span>
|
||||
<span className="text-2xl font-bold">{reservationStats.totalReservations}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">最近7天</span>
|
||||
<span className="text-lg font-semibold text-blue-600">{reservationStats.recentReservations}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>预约状态分布</CardTitle>
|
||||
<CardDescription>不同状态的预约数量</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(reservationStats.statusDistribution).map(([status, count]) => (
|
||||
<div key={status} className="flex items-center justify-between">
|
||||
<Badge variant={
|
||||
status === 'completed' ? 'default' :
|
||||
status === 'reserved' ? 'secondary' :
|
||||
status === 'cancelled' ? 'destructive' :
|
||||
'outline'
|
||||
} className="text-xs">
|
||||
{status === 'queuing' ? '排队中' :
|
||||
status === 'reserved' ? '已预约' :
|
||||
status === 'completed' ? '已完成' :
|
||||
status === 'cancelled' ? '已取消' : status}
|
||||
</Badge>
|
||||
<span className="text-sm font-medium">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>排队状态摘要</CardTitle>
|
||||
<CardDescription>当前排队和预约状态</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-orange-600">排队中</span>
|
||||
<span className="text-lg font-bold">{queueStats.queuing}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-blue-600">已预约</span>
|
||||
<span className="text-lg font-bold">{queueStats.reserved}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-green-600">已完成</span>
|
||||
<span className="text-lg font-bold">{queueStats.completed}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-red-600">已取消</span>
|
||||
<span className="text-lg font-bold">{queueStats.cancelled}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user