feat: Banner UI美化 & 新增文章/公告/图库/媒体管理等功能
- Banner: Ken Burns缩放动效、左右导航箭头、进度条指示器、hover暂停、暗角遮罩、shimmer按钮动画 - 新增文章管理(CRUD)与公开文章页 - 新增Banner/Gallery图片管理API - 新增媒体管理页面 - 新增更新日志页面 - 新增页面访问追踪 - 新增Markdown渲染组件 - .gitignore排除.cursor目录 Made-with: Cursor
This commit is contained in:
@@ -6,39 +6,70 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Package, Download, FileUp } from "lucide-react";
|
||||
import { Package, Download, FileUp, Eye, Users, TrendingUp } 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 [addonCount, totalDownloads, releaseCount, recentReleases] =
|
||||
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 } } },
|
||||
}),
|
||||
]);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const days = getLast7Days();
|
||||
|
||||
const [
|
||||
addonCount,
|
||||
totalDownloads,
|
||||
releaseCount,
|
||||
recentReleases,
|
||||
todayPV,
|
||||
totalPV,
|
||||
todayUV,
|
||||
pvByDay,
|
||||
] = 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" },
|
||||
}),
|
||||
]);
|
||||
|
||||
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 stats = [
|
||||
{
|
||||
title: "插件总数",
|
||||
value: addonCount,
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: "总下载量",
|
||||
value: totalDownloads._sum.totalDownloads || 0,
|
||||
icon: Download,
|
||||
},
|
||||
{
|
||||
title: "版本发布数",
|
||||
value: releaseCount,
|
||||
icon: FileUp,
|
||||
},
|
||||
{ title: "插件总数", value: addonCount, icon: Package },
|
||||
{ title: "总下载量", value: totalDownloads._sum.totalDownloads || 0, icon: Download },
|
||||
{ title: "版本发布数", value: releaseCount, icon: FileUp },
|
||||
{ title: "今日访问 (PV)", value: todayPV, icon: Eye },
|
||||
{ title: "今日独立访客 (UV)", value: todayUV, icon: Users },
|
||||
{ title: "累计访问量", value: totalPV, icon: TrendingUp },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -55,12 +86,42 @@ export default async function DashboardPage() {
|
||||
<stat.icon className="h-5 w-5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{stat.value}</div>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user