全项目扫描修复: Docker数据卷修复+安全requireAdmin+12页SEO+API白名单+脚本超时+常量提取+假数据删除

This commit is contained in:
ZhuiGuangAI Dev
2026-06-12 17:27:47 +08:00
parent 464924aea0
commit 4c325c8699
454 changed files with 33647 additions and 980 deletions
+206 -104
View File
@@ -1,5 +1,12 @@
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
import Link from "next/link";
import ForumTopicRow from "@/components/forum/ForumTopicRow";
import NewTopicDialog from "@/components/forum/NewTopicDialog";
import FollowButton from "@/components/forum/FollowButton";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { ArrowLeft, MessageSquare } from "lucide-react";
export const dynamic = "force-dynamic";
@@ -9,6 +16,7 @@ interface Props {
}
export default async function CategoryPage({ params, searchParams }: Props) {
const session = await getServerSession(authOptions);
const category = await prisma.forumCategory.findUnique({
where: { slug: params.category },
});
@@ -17,7 +25,8 @@ export default async function CategoryPage({ params, searchParams }: Props) {
const childCategories = await prisma.forumCategory.findMany({
where: { parentId: category.id },
select: { id: true, name: true, slug: true },
select: { id: true, name: true, slug: true, icon: true, topicCount: true },
orderBy: [{ sortOrder: "asc" }],
});
const sort = searchParams.sort || "latest";
@@ -28,130 +37,223 @@ export default async function CategoryPage({ params, searchParams }: Props) {
if (sort === "latest") orderBy.createdAt = "desc";
else if (sort === "hot") orderBy.viewCount = "desc";
else if (sort === "active") orderBy.lastReplyAt = "desc";
else if (sort === "likes") orderBy.likeCount = "desc";
else orderBy.createdAt = "desc";
const [topics, totalCount] = await Promise.all([
const currentUserId = session?.user?.id
? parseInt(session.user.id)
: null;
const [topics, totalCount, follow] = await Promise.all([
prisma.forumTopic.findMany({
where: { categoryId: category.id },
include: {
category: { select: { name: true, slug: true, icon: true } },
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true, isBot: true } },
_count: { select: { posts: true } },
tags: { include: { tag: true } },
},
orderBy: [{ isPinned: "desc" }, orderBy],
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.forumTopic.count({ where: { categoryId: category.id } }),
currentUserId
? prisma.forumCategoryFollow.findUnique({
where: {
categoryId_userId: {
categoryId: category.id,
userId: currentUserId,
},
},
})
: Promise.resolve(null),
]);
const sortTabs = [
{ key: "latest", label: "最新" },
{ key: "active", label: "活跃" },
{ key: "hot", label: "热门" },
{ key: "likes", label: "高赞" },
];
return (
<div className="min-h-screen bg-[#f8fafc]">
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="mb-6">
<a href="/community" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
← 返回社区
</a>
<div className="min-h-screen bg-gradient-to-b from-slate-50/30 to-white">
<div className="max-w-6xl mx-auto px-4 py-6">
{/* Breadcrumb */}
<div className="mb-4 flex items-center gap-2 text-sm">
<Link
href="/community"
className="inline-flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="h-3.5 w-3.5" />
返回社区
</Link>
</div>
<div className="mb-6">
<h1 className="text-2xl font-bold">{category.name}</h1>
{category.description && (
<p className="mt-1 text-sm text-muted-foreground">{category.description}</p>
{/* Category header */}
<div className="rounded-2xl border border-border/50 bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] overflow-hidden mb-6">
<div className="px-5 py-5 sm:px-6 sm:py-6 flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-50 to-indigo-50 border border-sky-200/40 text-2xl">
{category.icon || "📌"}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h1 className="text-xl font-bold text-foreground">
{category.name}
</h1>
<span className="text-[11px] text-muted-foreground/50 tabular-nums">
· {totalCount} 帖 · {category.followerCount} 关注
</span>
</div>
{category.description && (
<p className="mt-1 text-sm text-muted-foreground">
{category.description}
</p>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<FollowButton
categoryId={category.id}
initialFollowed={!!follow}
followerCount={category.followerCount}
/>
<NewTopicDialog
categories={[
{ id: category.id, name: category.name, slug: category.slug, icon: category.icon },
...childCategories,
]}
defaultCategorySlug={category.slug}
variant="primary"
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_240px] gap-6">
<div className="min-w-0 space-y-4">
{/* Sort tabs */}
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="inline-flex items-center rounded-xl border border-border/50 bg-white p-1 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
{sortTabs.map((tab) => (
<Link
key={tab.key}
href={`/community/${category.slug}?sort=${tab.key}`}
className={`inline-flex items-center justify-center rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
sort === tab.key
? "bg-sky-500 text-white shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab.label}
</Link>
))}
</div>
<span className="text-[11px] text-muted-foreground/50">
共 {totalCount} 个话题
</span>
</div>
{/* Topics list */}
<section className="rounded-2xl border border-border/50 bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] overflow-hidden">
<div className="divide-y divide-border/20">
{topics.length === 0 ? (
<div className="py-20 text-center">
<MessageSquare className="h-8 w-8 text-muted-foreground/30 mx-auto mb-2" />
<p className="text-sm text-muted-foreground/60">
暂无话题,来发第一帖吧 ✨
</p>
</div>
) : (
topics.map((topic) => (
<ForumTopicRow
key={topic.id}
topic={{
...topic,
replyCount: topic._count.posts,
tags: topic.tags?.map((tt) => tt.tag),
}}
showCategory={false}
/>
))
)}
</div>
</section>
{/* Pagination */}
{totalCount > pageSize && (
<div className="flex justify-center items-center gap-1.5 pt-2">
{page > 1 && (
<Link
href={`/community/${category.slug}?sort=${sort}&page=${page - 1}`}
className="rounded-lg border border-border/40 bg-white px-3 py-1.5 text-xs text-muted-foreground hover:border-sky-200 hover:text-sky-700"
>
上一页
</Link>
)}
{Array.from(
{ length: Math.ceil(totalCount / pageSize) },
(_, i) => i + 1
)
.filter((p) => Math.abs(p - page) <= 2)
.map((p) => (
<Link
key={p}
href={`/community/${category.slug}?sort=${sort}&page=${p}`}
className={`min-w-[32px] inline-flex items-center justify-center rounded-lg px-2.5 py-1.5 text-xs font-medium transition-colors ${
page === p
? "bg-sky-500 text-white shadow-sm"
: "border border-border/40 bg-white text-muted-foreground hover:border-sky-200 hover:text-sky-700"
}`}
>
{p}
</Link>
))}
{page < Math.ceil(totalCount / pageSize) && (
<Link
href={`/community/${category.slug}?sort=${sort}&page=${page + 1}`}
className="rounded-lg border border-border/40 bg-white px-3 py-1.5 text-xs text-muted-foreground hover:border-sky-200 hover:text-sky-700"
>
下一页
</Link>
)}
</div>
)}
</div>
{/* Right sidebar: sub-categories */}
{childCategories.length > 0 && (
<aside className="hidden lg:block">
<div className="sticky top-[68px] rounded-2xl border border-border/50 bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] overflow-hidden">
<div className="px-4 py-3 border-b border-border/30">
<h3 className="text-sm font-semibold text-foreground">子板块</h3>
</div>
<div className="p-2">
<Link
href={`/community/${category.slug}`}
className="flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs font-medium text-sky-700 bg-sky-50"
>
<span>{category.icon || "📌"}</span>
<span>全部</span>
</Link>
{childCategories.map((child) => (
<Link
key={child.id}
href={`/community/${child.slug}`}
className="flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-slate-50 hover:text-foreground transition-colors"
>
<span>{child.icon || "📌"}</span>
<span className="truncate flex-1">{child.name}</span>
<span className="text-[10px] text-muted-foreground/40 tabular-nums">
{child.topicCount}
</span>
</Link>
))}
</div>
</div>
</aside>
)}
</div>
<div className="flex items-center gap-2 mb-6">
{["latest", "hot", "active"].map((s) => (
<a
key={s}
href={`/community/${category.slug}?sort=${s}`}
className={`px-3 py-1.5 rounded-lg text-sm transition-colors ${
sort === s ? "bg-primary text-white" : "bg-white border border-border/40 text-muted-foreground hover:border-border/60"
}`}
>
{s === "latest" ? "最新" : s === "hot" ? "热门" : "活跃"}
</a>
))}
</div>
{childCategories.length > 0 && (
<div className="flex flex-wrap items-center gap-2 mb-6">
<a
href={`/community/${category.slug}`}
className="px-3 py-1.5 rounded-lg text-sm bg-primary text-white"
>
全部
</a>
{childCategories.map((child) => (
<a
key={child.id}
href={`/community/${child.slug}`}
className="px-3 py-1.5 rounded-lg text-sm bg-white border border-border/40 text-muted-foreground hover:border-border/60 hover:text-foreground transition-colors"
>
{child.name}
</a>
))}
</div>
)}
<div className="space-y-3">
{topics.map((topic) => {
const initial = topic.user.name?.charAt(0).toUpperCase() || "?";
return (
<a
key={topic.id}
href={`/community/topic/${topic.id}`}
className="flex items-start gap-3 p-4 rounded-xl border border-border/40 bg-white transition-all hover:border-border/60 hover:shadow-sm block"
>
{topic.user.avatarUrl ? (
<img src={topic.user.avatarUrl} alt="" className="h-9 w-9 rounded-full border border-border/40 object-cover flex-shrink-0" />
) : (
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary flex-shrink-0">
{initial}
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{topic.isPinned && (
<span className="text-[10px] font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded-full">置顶</span>
)}
{topic.isLocked && (
<span className="text-[10px] font-medium text-muted-foreground bg-muted/30 px-1.5 py-0.5 rounded-full">已锁定</span>
)}
<h3 className="text-sm font-medium hover:text-primary transition-colors truncate">
{topic.title}
</h3>
</div>
<div className="mt-1.5 flex items-center gap-3 text-xs text-muted-foreground/50">
<span>{topic.user.name || topic.user.email?.split("@")[0]}{topic.user.isBot && <span className="ml-1 px-1 py-0.5 text-[9px] font-medium bg-sky-100 text-sky-600 rounded">AI</span>}</span>
<span>{topic.createdAt?.toLocaleDateString("zh-CN")}</span>
<span>{topic._count.posts}回复</span>
<span>{topic.viewCount}浏览</span>
</div>
</div>
</a>
);
})}
</div>
{totalCount > pageSize && (
<div className="flex justify-center gap-2 mt-6">
{Array.from({ length: Math.ceil(totalCount / pageSize) }, (_, i) => i + 1).map((p) => (
<a
key={p}
href={`/community/${category.slug}?sort=${sort}&page=${p}`}
className={`px-3 py-1.5 rounded-lg text-sm transition-colors ${
page === p
? "bg-primary text-white"
: "bg-white border border-border/40 text-muted-foreground hover:border-border/60"
}`}
>
{p}
</a>
))}
</div>
)}
</div>
</div>
);