2026-05-25: 等级体系升级(20级)+论坛二级分类+会员空间

This commit is contained in:
ZhuiGuangAI Dev
2026-05-24 23:05:38 +08:00
parent 6c78c32157
commit 9bf3e7574a
118 changed files with 11842 additions and 3262 deletions
+185
View File
@@ -0,0 +1,185 @@
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export const dynamic = "force-dynamic";
interface Props {
params: { id: string };
}
export default async function TopicPage({ params }: Props) {
const session = await getServerSession(authOptions);
const topicId = parseInt(params.id);
const topic = await prisma.forumTopic.findUnique({
where: { id: topicId },
include: {
category: { select: { name: true, slug: true, icon: true } },
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
_count: { select: { posts: true } },
},
});
if (!topic) notFound();
await prisma.forumTopic.update({
where: { id: topicId },
data: { viewCount: { increment: 1 } },
});
const posts = await prisma.forumPost.findMany({
where: { topicId },
include: {
user: { select: { id: true, name: true, email: true, avatarUrl: true, level: true, points: true } },
},
orderBy: { createdAt: "asc" },
take: 50,
});
const currentUserId = session?.user?.id ? parseInt(session.user.id) : null;
const isAuthor = currentUserId === topic.userId;
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>
<div className="rounded-xl border border-border/50 bg-white shadow-[0_1px_2px_rgba(0,0,0,0.04)] overflow-hidden">
<div className="p-6 border-b border-border/40">
<div className="flex items-center gap-2 mb-3">
<span className="px-2 py-0.5 text-[10px] font-medium bg-primary/10 text-primary rounded-full">
{topic.category.name}
</span>
{topic.isPinned && (
<span className="px-2 py-0.5 text-[10px] font-medium bg-amber-50 text-amber-700 rounded-full">
置顶
</span>
)}
{topic.isLocked && (
<span className="px-2 py-0.5 text-[10px] font-medium bg-muted/30 text-muted-foreground rounded-full">
已锁定
</span>
)}
</div>
<h1 className="text-xl font-bold mb-4">{topic.title}</h1>
<div className="flex items-center gap-3 mb-6">
{topic.user.avatarUrl ? (
<img src={topic.user.avatarUrl} alt="" className="h-10 w-10 rounded-full border border-border/40 object-cover" />
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
{topic.user.name?.charAt(0).toUpperCase() || "?"}
</div>
)}
<div>
<p className="text-sm font-medium">{topic.user.name || topic.user.email.split("@")[0]}</p>
<p className="text-xs text-muted-foreground/50">
{topic.createdAt.toLocaleDateString("zh-CN")} {topic.createdAt.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })}
</p>
</div>
<div className="ml-auto flex items-center gap-3 text-xs text-muted-foreground/50">
<span>{topic.viewCount + 1}浏览</span>
<span>{topic._count.posts}回复</span>
</div>
</div>
<div className="prose prose-sm max-w-none text-muted-foreground/80 leading-relaxed whitespace-pre-wrap">
{topic.content}
</div>
</div>
</div>
<div className="mt-8">
<h2 className="text-lg font-semibold mb-4">回复 ({posts.length})</h2>
<div className="space-y-4">
{posts.map((post) => {
const initial = post.user.name?.charAt(0).toUpperCase() || "?";
const isTopicAuthor = post.userId === topic.userId;
return (
<div key={post.id} className="rounded-xl border border-border/40 bg-white p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
<div className="flex items-center gap-3 mb-3">
{post.user.avatarUrl ? (
<img src={post.user.avatarUrl} alt="" className="h-8 w-8 rounded-full border border-border/40 object-cover" />
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
{initial}
</div>
)}
<div>
<p className="text-sm font-medium">{post.user.name || post.user.email.split("@")[0]}</p>
<p className="text-xs text-muted-foreground/50">
{post.createdAt.toLocaleDateString("zh-CN")} {post.createdAt.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })}
</p>
</div>
{isTopicAuthor && (
<span className="ml-2 px-1.5 py-0.5 text-[10px] font-medium bg-primary/10 text-primary rounded-full">
楼主
</span>
)}
</div>
<div className="text-sm text-muted-foreground/80 leading-relaxed whitespace-pre-wrap">
{post.content}
</div>
</div>
);
})}
</div>
</div>
{!topic.isLocked && (
<div className="mt-8">
<form
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const content = formData.get("content") as string;
if (!content.trim()) return;
const res = await fetch(`/api/forum/topics/${topicId}/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
if (res.ok) {
window.location.reload();
}
}}
className="rounded-xl border border-border/50 bg-white p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
>
<h3 className="text-sm font-semibold mb-3">发表回复</h3>
<textarea
name="content"
placeholder="写下你的回复..."
rows={4}
maxLength={5000}
className="w-full resize-none rounded-lg border border-border/60 bg-white px-3 py-2 text-sm focus:outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/15 transition-colors placeholder:text-muted-foreground/40 mb-3"
/>
<div className="flex justify-end">
<button
type="submit"
className="rounded-lg bg-primary text-white text-sm px-5 py-2 hover:bg-primary/90 transition-colors shadow-sm shadow-primary/20"
>
发表回复
</button>
</div>
</form>
</div>
)}
{topic.isLocked && (
<div className="mt-8 text-center py-6 rounded-xl border border-border/40 bg-muted/20">
<p className="text-sm text-muted-foreground">此话题已锁定,无法回复</p>
</div>
)}
</div>
</div>
);
}