feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系 - 实现实时权限快照加载与细粒度权限校验 - 新增内容审核队列与前后端页面 - 重构用户权限管理与站点管理逻辑 - 新增评论/帖子审核状态与通知推送 - 优化前端权限控制与角色徽章展示 - 修正数据库迁移与默认值问题
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
@@ -31,6 +32,8 @@ import ReadingProgress from "@/components/ReadingProgress";
|
||||
import PostToc, { type TocItem } from "@/components/PostToc";
|
||||
import MobilePostBar from "@/components/MobilePostBar";
|
||||
import Avatar from "@/components/Avatar";
|
||||
import RoleBadge from "@/components/RoleBadge";
|
||||
import { isStaff } from "@/lib/roles";
|
||||
import { IconComment, IconEye } from "@/components/Icons";
|
||||
|
||||
interface PageProps {
|
||||
@@ -42,7 +45,8 @@ interface PageProps {
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const { post } = await fetchPostDetail(id);
|
||||
// 带登录态:作者/管理团队看待审帖时标签标题也应是真实标题而非"帖子不存在"
|
||||
const { post } = await fetchPostDetail(id, authCookieHeader(await cookies()));
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.content.slice(0, 160),
|
||||
@@ -157,14 +161,7 @@ function AuthorRailCard({ profile }: { profile: UserProfile }) {
|
||||
>
|
||||
{u.nickname}
|
||||
</Link>
|
||||
{u.role === "admin" && (
|
||||
<span
|
||||
className="chip mt-1.5"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||||
>
|
||||
管理员
|
||||
</span>
|
||||
)}
|
||||
{u.role !== "user" && <RoleBadge role={u.role} boardIds={u.board_ids} />}
|
||||
</div>
|
||||
</div>
|
||||
<p className="meta mt-3">{formatYearMonth(u.created_at)} 加入</p>
|
||||
@@ -295,13 +292,8 @@ function AuthorCardStacked({ profile }: { profile: UserProfile }) {
|
||||
>
|
||||
{profile.user.nickname}
|
||||
</Link>
|
||||
{profile.user.role === "admin" && (
|
||||
<span
|
||||
className="chip"
|
||||
style={{ background: "var(--accent-soft)", color: "var(--accent)", borderColor: "transparent" }}
|
||||
>
|
||||
管理员
|
||||
</span>
|
||||
{profile.user.role !== "user" && (
|
||||
<RoleBadge role={profile.user.role} boardIds={profile.user.board_ids} />
|
||||
)}
|
||||
</div>
|
||||
<p className="meta mt-1">
|
||||
@@ -322,11 +314,17 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
const sp = await searchParams;
|
||||
const commentPage = Math.max(1, parseInt(sp.page || "1", 10) || 1);
|
||||
const cookie = authCookieHeader(await cookies());
|
||||
const { post } = await fetchPostDetail(id, cookie);
|
||||
// 待审/被拒帖子对无权访问者由后端返回 404,这里落到全局 not-found 页而非 500
|
||||
let post: Awaited<ReturnType<typeof fetchPostDetail>>["post"];
|
||||
try {
|
||||
({ post } = await fetchPostDetail(id, cookie));
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// 评论 / 当前用户 / 作者资料 / 同板块热议并行拉取;侧栏数据失败一律静默降级,不阻塞正文
|
||||
const [commentsData, me, profile, relatedData] = await Promise.all([
|
||||
fetchComments(id, commentPage),
|
||||
fetchComments(id, commentPage, cookie),
|
||||
getMeCached(cookie || undefined),
|
||||
fetchUserProfile(String(post.user.id), 1, cookie).catch(() => null),
|
||||
fetchPosts(1, 8, post.board_id, "hot", "", false, cookie).catch(() => null),
|
||||
@@ -340,6 +338,10 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
const edited =
|
||||
new Date(post.updated_at).getTime() - new Date(post.created_at).getTime() > 60_000;
|
||||
|
||||
// 非公开帖(待审/未通过)仅作者本人与管理团队可看,顶部给出醒目状态
|
||||
const isPublished = post.status === "published";
|
||||
const canSeeStatus = !!me.user && (me.user.id === post.user.id || isStaff(me.user.role));
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "DiscussionForumPosting",
|
||||
@@ -409,6 +411,22 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
<div className="flex items-center gap-2 mb-4 flex-wrap">
|
||||
{post.pinned > 0 && <span className="badge-pin">置顶</span>}
|
||||
{post.recommended && <span className="badge-rec">推荐</span>}
|
||||
{!isPublished && canSeeStatus && post.status === "pending" && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11.5px] font-bold"
|
||||
style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
|
||||
>
|
||||
等待审核
|
||||
</span>
|
||||
)}
|
||||
{!isPublished && canSeeStatus && post.status === "rejected" && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11.5px] font-bold"
|
||||
style={{ background: "var(--danger-soft)", color: "var(--danger)" }}
|
||||
>
|
||||
未通过审核
|
||||
</span>
|
||||
)}
|
||||
<Link href={`/board/${post.board.id}`} className="chip">
|
||||
{post.board.name}
|
||||
</Link>
|
||||
@@ -435,7 +453,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
</Link>
|
||||
</div>
|
||||
{/* 管理操作已收进单个下拉按钮;普通读者此处不渲染内容 */}
|
||||
<PostActions postId={id} authorId={post.user.id} pinned={post.pinned} recommended={post.recommended} />
|
||||
<PostActions postId={id} authorId={post.user.id} boardId={post.board_id} pinned={post.pinned} recommended={post.recommended} />
|
||||
</div>
|
||||
|
||||
{/* 元信息条:阅读数据居左、发布/更新时间居右(窄屏自动折行);
|
||||
@@ -496,15 +514,32 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CommentSection
|
||||
postId={id}
|
||||
comments={comments}
|
||||
total={commentsData.total}
|
||||
totalComments={commentsData.total_comments}
|
||||
page={commentsData.page}
|
||||
size={commentsData.size}
|
||||
user={me.user}
|
||||
/>
|
||||
{/* 非公开帖不开放评论(后端同样拒绝),给出状态提示 */}
|
||||
{isPublished ? (
|
||||
<CommentSection
|
||||
postId={id}
|
||||
boardId={post.board_id}
|
||||
comments={comments}
|
||||
total={commentsData.total}
|
||||
totalComments={commentsData.total_comments}
|
||||
page={commentsData.page}
|
||||
size={commentsData.size}
|
||||
user={me.user}
|
||||
/>
|
||||
) : (
|
||||
<section className="panel p-6 sm:p-8 mt-6 text-center" aria-label="评论区">
|
||||
<p className="text-[14px] font-bold" style={{ color: "var(--ink-2)" }}>
|
||||
{post.status === "pending" ? "帖子审核通过后开放评论" : "帖子未通过审核,评论已关闭"}
|
||||
</p>
|
||||
<p className="meta mt-1.5 text-[12.5px]">
|
||||
{me.user?.id === post.user.id
|
||||
? post.status === "pending"
|
||||
? "你的帖子正在等待管理员审核,请留意通知"
|
||||
: "如有疑问可联系管理团队"
|
||||
: "审核通过后评论区将自动开放"}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 窄屏相关讨论(lg 起由右栏承载) */}
|
||||
<div className="lg:hidden mt-6">
|
||||
|
||||
Reference in New Issue
Block a user