feat: 就地通过评审,并为已过审用户提供默认可关的免审发布

帖/评管理入口可直接通过;站点默认信任已有 published 内容的用户后续直发。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 05:37:57 +08:00
parent b0170a99fd
commit f3145b24ff
14 changed files with 330 additions and 40 deletions

View File

@@ -58,7 +58,7 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
if (!normalized || saving) return;
setSaving(true);
try {
const res = await apiUpdateSiteSettings(submitHex);
const res = await apiUpdateSiteSettings({ accent: submitHex });
const next = (res.accent || "").toLowerCase();
setSavedHex(next);
applyAccentTheme(next || null);

View File

@@ -20,6 +20,7 @@ import {
apiAdminRejectPost,
apiAdminApproveComment,
apiAdminRejectComment,
apiUpdateSiteSettings,
type PendingCommentItem,
type PendingCounts,
type Post,
@@ -47,10 +48,15 @@ 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 });
@@ -73,12 +79,30 @@ 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]);
@@ -215,6 +239,40 @@ export default function ModerationAdmin({
</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 } from "@/lib/serverData";
import { canModerateAny } from "@/lib/roles";
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
import { canModerateAny, isSuperOrOwner } from "@/lib/roles";
import Forbidden from "../Forbidden";
import ModerationAdmin from "./ModerationAdmin";
@@ -33,11 +33,16 @@ 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}
/>
);
}