feat: Banner UI美化 & 新增文章/公告/图库/媒体管理等功能

- Banner: Ken Burns缩放动效、左右导航箭头、进度条指示器、hover暂停、暗角遮罩、shimmer按钮动画
- 新增文章管理(CRUD)与公开文章页
- 新增Banner/Gallery图片管理API
- 新增媒体管理页面
- 新增更新日志页面
- 新增页面访问追踪
- 新增Markdown渲染组件
- .gitignore排除.cursor目录

Made-with: Cursor
This commit is contained in:
rucky
2026-03-25 09:17:35 +08:00
parent 241a76caeb
commit bf92a69332
66 changed files with 5241 additions and 155 deletions

View File

@@ -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>