176 lines
5.9 KiB
TypeScript
176 lines
5.9 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Package, Download, FileUp, Eye, Users, TrendingUp, Monitor, Wifi } from "lucide-react";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
function getLast7Days() {
|
|
const days: string[] = [];
|
|
for (let i = 6; i >= 0; i--) {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() - i);
|
|
days.push(d.toISOString().slice(0, 10));
|
|
}
|
|
return days;
|
|
}
|
|
|
|
export default async function DashboardPage() {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const days = getLast7Days();
|
|
|
|
const onlineThreshold = new Date(Date.now() - 3 * 60 * 1000);
|
|
|
|
const [
|
|
addonCount,
|
|
totalDownloads,
|
|
releaseCount,
|
|
recentReleases,
|
|
todayPV,
|
|
totalPV,
|
|
todayUV,
|
|
pvByDay,
|
|
launcherDownloads,
|
|
launcherUpdateDownloads,
|
|
onlineCount,
|
|
] = await Promise.all([
|
|
prisma.addon.count(),
|
|
prisma.addon.aggregate({ _sum: { totalDownloads: true } }),
|
|
prisma.release.count(),
|
|
prisma.release.findMany({
|
|
take: 5,
|
|
orderBy: { createdAt: "desc" },
|
|
include: { addon: { select: { name: true } } },
|
|
}),
|
|
prisma.pageView.count({ where: { date: today } }),
|
|
prisma.pageView.count(),
|
|
prisma.pageView.groupBy({
|
|
by: ["ip"],
|
|
where: { date: today, ip: { not: "" } },
|
|
}).then((r) => r.length),
|
|
prisma.pageView.groupBy({
|
|
by: ["date"],
|
|
where: { date: { in: days } },
|
|
_count: true,
|
|
orderBy: { date: "asc" },
|
|
}),
|
|
prisma.softwareVersion.aggregate({ _sum: { downloadCount: true } }),
|
|
prisma.softwareVersion.aggregate({ _sum: { launcherDownloadCount: true } }),
|
|
prisma.launcherOnline.count({ where: { lastSeen: { gte: onlineThreshold } } }),
|
|
]);
|
|
|
|
const pvMap = new Map(pvByDay.map((d) => [d.date, d._count]));
|
|
const chartData = days.map((d) => ({
|
|
date: d.slice(5),
|
|
pv: pvMap.get(d) || 0,
|
|
}));
|
|
const maxPV = Math.max(...chartData.map((d) => d.pv), 1);
|
|
|
|
const totalSwDownloads = launcherDownloads._sum.downloadCount || 0;
|
|
const totalLauncherUpdates = launcherUpdateDownloads._sum.launcherDownloadCount || 0;
|
|
const webDownloads = totalSwDownloads - totalLauncherUpdates;
|
|
|
|
const stats = [
|
|
{ title: "插件总数", value: addonCount, icon: Package },
|
|
{ title: "插件总下载量", value: totalDownloads._sum.totalDownloads || 0, icon: Download },
|
|
{ title: "版本发布数", value: releaseCount, icon: FileUp },
|
|
{ title: "启动器下载量 (网页)", value: webDownloads, icon: Monitor },
|
|
{ title: "启动器更新量 (客户端)", value: totalLauncherUpdates, icon: Download },
|
|
{ title: "启动器在线人数", value: onlineCount, icon: Wifi },
|
|
{ title: "今日访问 (PV)", value: todayPV, icon: Eye },
|
|
{ title: "今日独立访客 (UV)", value: todayUV, icon: Users },
|
|
{ title: "累计访问量", value: totalPV, icon: TrendingUp },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-3xl font-bold">仪表盘</h1>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
|
|
{stats.map((stat) => (
|
|
<Card key={stat.title}>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{stat.title}
|
|
</CardTitle>
|
|
<stat.icon className="h-5 w-5 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">
|
|
{stat.value.toLocaleString()}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 7-day PV chart */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>近 7 天访问趋势</CardTitle>
|
|
<CardDescription>页面浏览量 (PV)</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex h-48 items-end gap-2">
|
|
{chartData.map((d) => (
|
|
<div key={d.date} className="flex flex-1 flex-col items-center gap-1">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
{d.pv}
|
|
</span>
|
|
<div
|
|
className="w-full rounded-t bg-primary/80 transition-all"
|
|
style={{
|
|
height: `${Math.max((d.pv / maxPV) * 160, 4)}px`,
|
|
}}
|
|
/>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{d.date}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>最近发布</CardTitle>
|
|
<CardDescription>最近发布的版本更新</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{recentReleases.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">暂无发布记录</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{recentReleases.map((release) => (
|
|
<div
|
|
key={release.id}
|
|
className="flex items-center justify-between border-b pb-3 last:border-0"
|
|
>
|
|
<div>
|
|
<p className="font-medium">{release.addon.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
v{release.version}
|
|
{release.gameVersion &&
|
|
` · WoW ${release.gameVersion}`}
|
|
</p>
|
|
</div>
|
|
<div className="text-right text-sm text-muted-foreground">
|
|
<p>{release.downloadCount} 次下载</p>
|
|
<p>{new Date(release.createdAt).toLocaleDateString("zh-CN")}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|