feat(admin): 完善站点设置、内容回收站与后台菜单分组

新增站点设置与内容管理(含已删除恢复/清除),用户页改用独有运营指标,并按概览/内容/成员与经济/站点重组侧栏。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 05:56:27 +08:00
parent be47b38370
commit dd7bd010b8
25 changed files with 2193 additions and 323 deletions

View File

@@ -20,7 +20,6 @@ import {
apiAdminRejectPost,
apiAdminApproveComment,
apiAdminRejectComment,
apiUpdateSiteSettings,
type PendingCommentItem,
type PendingCounts,
type Post,
@@ -48,15 +47,10 @@ export default function ModerationAdmin({
initialPosts,
initialPostsTotal,
initError,
canConfigureSettings,
initialTrustReviewedPublish,
}: {
initialPosts: Post[];
initialPostsTotal: number;
initError: string;
/** 超管/站长可改站点免审策略(后端 PermSettings) */
canConfigureSettings: boolean;
initialTrustReviewedPublish: boolean;
}) {
const [tab, setTab] = useState<Tab>("posts");
const [counts, setCounts] = useState<PendingCounts>({ posts: initialPostsTotal, comments: 0 });
@@ -79,30 +73,12 @@ export default function ModerationAdmin({
const [confirmKey, setConfirmKey] = useState<string | null>(null);
const confirmTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const [trustReviewedPublish, setTrustReviewedPublish] = useState(initialTrustReviewedPublish);
const [savingTrust, setSavingTrust] = useState(false);
const refreshCounts = useCallback(() => {
apiAdminPendingCounts()
.then(setCounts)
.catch(() => {});
}, []);
const toggleTrustReviewedPublish = async () => {
if (!canConfigureSettings || savingTrust) return;
const next = !trustReviewedPublish;
setSavingTrust(true);
try {
const res = await apiUpdateSiteSettings({ trust_reviewed_publish: next });
setTrustReviewedPublish(res.trust_reviewed_publish);
toast(res.trust_reviewed_publish ? "已开启:过审用户后续免审" : "已关闭:所有普通用户均需审核", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSavingTrust(false);
}
};
useEffect(() => {
refreshCounts();
}, [refreshCounts]);
@@ -235,44 +211,10 @@ export default function ModerationAdmin({
内容审核
</h1>
<p className="meta mt-1.5 text-[12.5px]">
普通用户发布的帖子与评论需审核通过后公开;板块管理员仅能处理授权板块的内容
普通用户发布的帖子与评论需审核通过后公开;板块管理员仅能处理授权板块的内容。内容策略与回收站见「内容管理」。
</p>
</div>
{canConfigureSettings && (
<section
className="panel p-4 sm:p-5 mb-5 flex items-start justify-between gap-4 flex-wrap"
aria-label="审核策略"
>
<div className="min-w-0 flex-1">
<h2 className="text-[14.5px] font-semibold" style={{ color: "var(--ink)" }}>
已过审用户后续免审
</h2>
<p className="meta mt-1 text-[12.5px] leading-relaxed">
开启后,若用户已有通过审核的帖子或评论,后续发帖与评论将直接公开,无需再次进入待审队列。默认开启。
</p>
</div>
<button
type="button"
role="switch"
aria-checked={trustReviewedPublish}
disabled={savingTrust}
onClick={() => void toggleTrustReviewedPublish()}
className="shrink-0 relative h-7 w-12 rounded-full transition-colors disabled:opacity-60"
style={{
background: trustReviewedPublish ? "var(--accent)" : "var(--line-2)",
}}
data-tip={trustReviewedPublish ? "点击关闭" : "点击开启"}
>
<span
className="absolute top-0.5 left-0.5 h-6 w-6 rounded-full bg-[var(--panel)] shadow transition-transform"
style={{ transform: trustReviewedPublish ? "translateX(1.25rem)" : "translateX(0)" }}
/>
<span className="sr-only">{trustReviewedPublish ? "已开启" : "已关闭"}</span>
</button>
</section>
)}
{/* 队列切换 */}
<div className="flex items-center gap-1.5 mb-4 flex-wrap" role="tablist" aria-label="审核队列">
{(

View File

@@ -2,8 +2,8 @@ import type { Metadata } from "next";
import { cookies } from "next/headers";
import { authCookieHeader } from "@/lib/cookies";
import { fetchAdminPendingPosts, type Post } from "@/lib/api";
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
import { canModerateAny, isSuperOrOwner } from "@/lib/roles";
import { getMeCached } from "@/lib/serverData";
import { canModerateAny } from "@/lib/roles";
import Forbidden from "../Forbidden";
import ModerationAdmin from "./ModerationAdmin";
@@ -17,7 +17,6 @@ export default async function ModerationPage() {
const cookie = authCookieHeader(await cookies());
const me = await getMeCached(cookie || undefined);
// 板块管理员及以上且(对板块管理员)拥有授权板块
if (!me.user || !canModerateAny(me.user)) {
return <Forbidden text="你所在的角色没有内容审核权限,或尚未被授权任何板块。" />;
}
@@ -33,16 +32,11 @@ export default async function ModerationPage() {
initError = e instanceof Error ? e.message : "获取待审核内容失败";
}
const canConfigureSettings = isSuperOrOwner(me.user.role);
const settings = await getPublicSettingsCached();
return (
<ModerationAdmin
initialPosts={initialPosts}
initialPostsTotal={initialTotal}
initError={initError}
canConfigureSettings={canConfigureSettings}
initialTrustReviewedPublish={settings.trust_reviewed_publish !== false}
/>
);
}