2026-05-25: 等级体系升级(20级)+论坛二级分类+会员空间
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user