feat: 实现完整的板块级RBAC内容审核系统

- 新增站长/超级管理员/管理员/板块管理员四级角色体系
- 实现实时权限快照加载与细粒度权限校验
- 新增内容审核队列与前后端页面
- 重构用户权限管理与站点管理逻辑
- 新增评论/帖子审核状态与通知推送
- 优化前端权限控制与角色徽章展示
- 修正数据库迁移与默认值问题
This commit is contained in:
2026-09-15 04:47:34 +08:00
parent 6da4309453
commit a7b6421840
44 changed files with 3149 additions and 585 deletions

View File

@@ -6,12 +6,15 @@ import Link from "next/link";
import { Trash2, MessagesSquare, ChevronRight, ChevronDown } from "lucide-react";
import ReactMarkdown from "react-markdown";
import { apiCreateComment, apiDeleteComment, type CommentNode, type User } from "@/lib/api";
import { canModerateBoard } from "@/lib/roles";
import { toast } from "@/lib/toast";
import Avatar from "./Avatar";
import Pagination from "./Pagination";
interface CommentSectionProps {
postId: string;
/** 帖子所属板块:板块管理员仅可管理授权板块的评论 */
boardId: number;
comments: CommentNode[];
total: number; // 楼层数(主评论数)→ 分页与楼层号计算
totalComments: number; // 全部评论数(含回复)→ 头部徽标
@@ -31,6 +34,7 @@ function countReplies(node: CommentNode): number {
export default function CommentSection({
postId,
boardId,
comments,
total,
totalComments,
@@ -58,8 +62,12 @@ export default function CommentSection({
const res = await apiCreateComment(postId, content);
if (res.comment) {
setContent("");
// 新楼层在最后一页:刷新后由服务端直出最新分页
router.refresh();
if (res.comment.status === "pending") {
toast("已提交,等待审核通过后公开");
} else {
// 新楼层在最后一页:刷新后由服务端直出最新分页
router.refresh();
}
} else if (res.error === "未登录") {
router.push(`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`);
} else {
@@ -81,8 +89,12 @@ export default function CommentSection({
if (res.comment) {
setReplyTarget(null);
setReplyContent("");
// 刷新后由 SSR 直出新树(子评论全量挂在本楼)
router.refresh();
if (res.comment.status === "pending") {
toast("已提交,等待审核通过后公开");
} else {
// 刷新后由 SSR 直出新树(子评论全量挂在本楼)
router.refresh();
}
} else if (res.error === "未登录") {
router.push(`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`);
} else {
@@ -122,7 +134,7 @@ export default function CommentSection({
// 引用功能已移除:与"回复"功能重叠,且会割裂讨论上下文(抖音风格无此功能)
const canDelete = (comment: CommentNode) =>
user && (user.id === comment.user.id || user.role === "admin");
!!user && (user.id === comment.user.id || canModerateBoard(user, boardId));
const openReply = (c: CommentNode) => {
setReplyTarget({ id: c.id, nickname: c.user.nickname || c.user.username });