Files
nanami-web/src/app/(public)/page.tsx
rucky bf92a69332 feat: Banner UI美化 & 新增文章/公告/图库/媒体管理等功能
- Banner: Ken Burns缩放动效、左右导航箭头、进度条指示器、hover暂停、暗角遮罩、shimmer按钮动画
- 新增文章管理(CRUD)与公开文章页
- 新增Banner/Gallery图片管理API
- 新增媒体管理页面
- 新增更新日志页面
- 新增页面访问追踪
- 新增Markdown渲染组件
- .gitignore排除.cursor目录

Made-with: Cursor
2026-03-25 09:17:35 +08:00

184 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from "next/link";
import { prisma } from "@/lib/db";
import { Button } from "@/components/ui/button";
import { AddonCard } from "@/components/public/AddonCard";
import { HeroBanner } from "@/components/public/HeroBanner";
import { GameGallery } from "@/components/public/GameGallery";
import { Sparkles, Shield, Zap, Calendar } from "lucide-react";
export const dynamic = "force-dynamic";
export default async function HomePage() {
const [featuredAddons, launcher, launcherDownloads, banners, galleryImages, latestArticles] =
await Promise.all([
prisma.addon.findMany({
where: { published: true },
include: {
releases: {
where: { isLatest: true },
select: { version: true },
},
},
orderBy: { totalDownloads: "desc" },
take: 6,
}),
prisma.software.findUnique({
where: { slug: "nanami-launcher" },
include: {
versions: {
where: { isLatest: true },
take: 1,
},
},
}),
prisma.softwareVersion.aggregate({
where: { software: { slug: "nanami-launcher" } },
_sum: { downloadCount: true },
}),
prisma.bannerImage.findMany({
where: { enabled: true },
orderBy: { sortOrder: "asc" },
select: { imageUrl: true },
}),
prisma.galleryImage.findMany({
where: { enabled: true },
orderBy: { sortOrder: "asc" },
select: { imageUrl: true, title: true },
}),
prisma.article.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
take: 3,
}),
]);
const launcherVersion = launcher?.versions[0]?.version ?? null;
const totalDownloads = launcherDownloads._sum.downloadCount ?? 0;
return (
<>
<HeroBanner
totalDownloads={totalDownloads || undefined}
launcherVersion={launcherVersion}
banners={banners}
/>
<GameGallery items={galleryImages} />
{/* Features */}
<section className="relative border-t border-amber-900/20 bg-gradient-to-b from-[#0d0b15] to-[#110f1a] dark:from-[#0d0b15] dark:to-[#110f1a]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(168,85,247,0.06)_0%,transparent_70%)]" />
<div className="relative mx-auto max-w-6xl px-3 py-10 sm:px-4 sm:py-16">
<div className="grid gap-4 sm:gap-8 md:grid-cols-3">
<div className="flex flex-col items-center rounded-xl border border-amber-500/10 bg-white/5 p-6 text-center backdrop-blur transition-colors hover:border-amber-500/25">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-500/10">
<Sparkles className="h-6 w-6 text-amber-400" />
</div>
<h3 className="mt-4 font-semibold text-amber-100"></h3>
<p className="mt-2 text-sm text-gray-400">
1.18.0
</p>
</div>
<div className="flex flex-col items-center rounded-xl border border-amber-500/10 bg-white/5 p-6 text-center backdrop-blur transition-colors hover:border-amber-500/25">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-500/10">
<Shield className="h-6 w-6 text-purple-400" />
</div>
<h3 className="mt-4 font-semibold text-amber-100">
</h3>
<p className="mt-2 text-sm text-gray-400">
Nanami
</p>
</div>
<div className="flex flex-col items-center rounded-xl border border-amber-500/10 bg-white/5 p-6 text-center backdrop-blur transition-colors hover:border-amber-500/25">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-cyan-500/10">
<Zap className="h-6 w-6 text-cyan-400" />
</div>
<h3 className="mt-4 font-semibold text-amber-100">
AI
</h3>
<p className="mt-2 text-sm text-gray-400">
</p>
</div>
</div>
</div>
</section>
{/* Latest Articles */}
{latestArticles.length > 0 && (
<section className="border-t border-amber-900/20 bg-gradient-to-b from-[#110f1a] to-[#0d0b15] dark:from-[#110f1a] dark:to-[#0d0b15]">
<div className="mx-auto max-w-6xl px-3 py-10 sm:px-4 sm:py-16">
<div className="mb-6 flex items-center justify-between sm:mb-8">
<h2 className="text-2xl font-bold text-amber-100"></h2>
<Button
variant="outline"
className="border-amber-500/20 text-amber-200 hover:border-amber-500/40 hover:bg-amber-500/10 hover:text-amber-100"
render={<Link href="/articles" />}
>
</Button>
</div>
<div className="grid gap-4 sm:gap-6 md:grid-cols-3">
{latestArticles.map((article) => (
<Link
key={article.id}
href={`/articles/${article.slug}`}
className="group overflow-hidden rounded-xl border border-amber-500/10 bg-white/[0.03] transition-colors hover:border-amber-500/25"
>
{article.coverImage && (
<div className="aspect-[16/9] overflow-hidden">
<img
src={article.coverImage}
alt={article.title}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
)}
<div className="p-4">
<h3 className="mb-1.5 font-semibold text-amber-100 group-hover:text-amber-200 line-clamp-1">
{article.title}
</h3>
{article.summary && (
<p className="mb-2 line-clamp-2 text-sm text-gray-400">
{article.summary}
</p>
)}
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<Calendar className="h-3 w-3" />
{new Date(article.createdAt).toLocaleDateString("zh-CN")}
</div>
</div>
</Link>
))}
</div>
</div>
</section>
)}
{/* Featured Addons */}
{featuredAddons.length > 0 && (
<section className="border-t border-amber-900/20 bg-gradient-to-b from-[#110f1a] to-[#0d0b15] dark:from-[#110f1a] dark:to-[#0d0b15]">
<div className="mx-auto max-w-6xl px-3 py-10 sm:px-4 sm:py-16">
<div className="mb-6 flex items-center justify-between sm:mb-8">
<h2 className="text-2xl font-bold text-amber-100"></h2>
<Button
variant="outline"
className="border-amber-500/20 text-amber-200 hover:border-amber-500/40 hover:bg-amber-500/10 hover:text-amber-100"
render={<Link href="/addons" />}
>
</Button>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{featuredAddons.map((addon) => (
<AddonCard key={addon.id} addon={addon} />
))}
</div>
</div>
</section>
)}
</>
);
}