完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 00:17:00 +08:00
parent acde7ee288
commit 6f054a903c
98 changed files with 8239 additions and 2073 deletions

View File

@@ -34,12 +34,10 @@ import {
apiAdminRejectPost,
apiAdminApproveComment,
apiAdminRejectComment,
apiUpdateSiteSettings,
type AdminContentComment,
type AdminContentCounts,
type AdminContentPost,
type AdminContentStatus,
type PublicSettings,
} from "@/lib/api";
import { canModerateAuthor } from "@/lib/roles";
import { formatRelative } from "@/lib/format";
@@ -58,8 +56,6 @@ import {
AdminSearchField,
AdminSegmented,
AdminStatusChip,
AdminStepper,
AdminSwitch,
contentStatusChip,
} from "@/components/admin";
@@ -184,29 +180,6 @@ function PendingActions({
);
}
/** 策略项:标题、控件与保存按钮成组换行,不拆散单项 */
function PolicyChip({
title,
hint,
control,
instant,
}: {
title: string;
hint?: string;
control: ReactNode;
instant?: boolean;
}) {
return (
<div className="admin-content-policy-chip" data-tip={hint || undefined}>
<span className="admin-content-policy-chip-copy">
<span className="admin-content-policy-chip-label">{title}</span>
{instant ? <span className="admin-content-policy-chip-how">立即生效</span> : null}
</span>
<div className="admin-content-policy-chip-ctrl shrink-0 flex items-center gap-1.5">{control}</div>
</div>
);
}
function ColumnShell({
icon,
title,
@@ -290,10 +263,8 @@ export default function ContentAdmin({
initialCommentsTotal,
initialCounts,
initError,
canConfigureSettings,
canPurge,
viewerRole,
initialSettings,
}: {
initialKind: ContentKind;
initialStatus: AdminContentStatus;
@@ -303,10 +274,8 @@ export default function ContentAdmin({
initialCommentsTotal: number;
initialCounts: AdminContentCounts;
initError: string;
canConfigureSettings: boolean;
canPurge: boolean;
viewerRole: string;
initialSettings: PublicSettings;
}) {
// 窄屏分段:lg 以下只显示一栏;PC 双栏同时可见
const [mobileKind, setMobileKind] = useState<ContentKind>(initialKind);
@@ -338,11 +307,6 @@ export default function ContentAdmin({
const [actingKey, setActingKey] = useState<string | null>(null);
const [trustOn, setTrustOn] = useState(initialSettings.trust_reviewed_publish);
const [cooldown, setCooldown] = useState(initialSettings.post_cooldown_hours);
const [postLinkNewTab, setPostLinkNewTab] = useState(initialSettings.post_link_new_tab !== false);
const [savingPolicy, setSavingPolicy] = useState(false);
const [confirm, setConfirm] = useState<{
title: string;
message: string;
@@ -463,65 +427,6 @@ export default function ContentAdmin({
);
};
const toggleTrust = async () => {
if (!canConfigureSettings || savingPolicy) return;
setSavingPolicy(true);
try {
const res = await apiUpdateSiteSettings({ trust_reviewed_publish: !trustOn });
setTrustOn(res.trust_reviewed_publish);
toast(
res.trust_reviewed_publish
? "已开启过审免审(帖子与评论)"
: "已关闭过审免审(帖子与评论将按规则待审)",
"ok"
);
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSavingPolicy(false);
}
};
const saveCooldown = async () => {
if (!canConfigureSettings || savingPolicy) return;
const n = Math.max(0, Math.min(168, Math.round(Number(cooldown)) || 0));
setSavingPolicy(true);
try {
const res = await apiUpdateSiteSettings({ post_cooldown_hours: n });
setCooldown(res.post_cooldown_hours);
toast(
res.post_cooldown_hours === 0
? "已关闭新用户发帖冷静期"
: `新用户发帖冷静期已设为 ${res.post_cooldown_hours} 小时(评论与聊天不受限)`,
"ok"
);
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSavingPolicy(false);
}
};
const togglePostLinkNewTab = async () => {
if (!canConfigureSettings || savingPolicy) return;
const next = !postLinkNewTab;
setSavingPolicy(true);
try {
const res = await apiUpdateSiteSettings({ post_link_new_tab: next });
setPostLinkNewTab(res.post_link_new_tab !== false);
toast(
res.post_link_new_tab !== false
? "帖子与评论外链将在新标签打开"
: "帖子与评论外链将在当前页打开",
"ok"
);
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSavingPolicy(false);
}
};
const postsToolbar = (
<div className="admin-content-col-toolbar">
<div className="admin-content-status-scroll">
@@ -922,73 +827,9 @@ export default function ContentAdmin({
<AdminPageHeader
icon={FolderOpen}
title="内容管理"
description="审核与管理帖子、评论。共用策略作用于全站发帖与评论,与下方列表筛选无关。"
description="审核与管理帖子、评论。"
/>
{canConfigureSettings && (
<div className="admin-content-policy" role="group" aria-label="全站内容策略">
<div className="admin-content-policy-lead">
<span className="admin-content-policy-title">共用策略</span>
<span className="admin-content-policy-scope">
过审免审与外链开关改完立即写入;冷静期需点保存。失败时恢复为已确认值。
</span>
</div>
<div className="admin-content-policy-row">
<PolicyChip
title="过审免审"
instant
hint="帖子 / 评论:用户曾有过审内容后,后续发帖与评论直接公开,不再进待审。修改后立即生效。"
control={
<AdminSwitch
checked={trustOn}
disabled={savingPolicy}
onChange={() => void toggleTrust()}
label="过审免审(帖子与评论)"
/>
}
/>
<PolicyChip
title="新用户冷静期"
hint="仅限制新注册用户发帖(0=关闭,上限 168 小时)。评论与聊天不受此限制。需点保存后生效。"
control={
<div className="flex items-center gap-1.5">
<AdminStepper
value={cooldown}
onChange={setCooldown}
min={0}
max={168}
unit="小时"
ariaLabel="新用户发帖冷静期小时数"
disabled={savingPolicy}
/>
<button
type="button"
className="btn btn-line btn-sm"
disabled={savingPolicy}
onClick={() => void saveCooldown()}
>
{savingPolicy ? "保存中…" : "保存"}
</button>
</div>
}
/>
<PolicyChip
title="外链新开"
instant
hint="帖子与评论中的外链在新标签打开。修改后立即生效。"
control={
<AdminSwitch
checked={postLinkNewTab}
disabled={savingPolicy}
onChange={() => void togglePostLinkNewTab()}
label="外链新开(帖子与评论)"
/>
}
/>
</div>
</div>
)}
{/* 窄屏:顶部分段切换双栏 */}
<div className="admin-content-kind lg:hidden shrink-0" role="tablist" aria-label="内容类型">
<button

View File

@@ -9,8 +9,8 @@ import {
type AdminContentPost,
type AdminContentStatus,
} from "@/lib/api";
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
import { canModerateAny, isSuperOrOwner, isOwner } from "@/lib/roles";
import { getMeCached } from "@/lib/serverData";
import { canModerateAny, isOwner } from "@/lib/roles";
import Forbidden from "../Forbidden";
import ContentAdmin, { type ContentKind } from "./ContentAdmin";
@@ -51,7 +51,7 @@ export default async function AdminContentPage({ searchParams }: ContentPageProp
let initError = "";
// PC 双栏同时展示:SSR 并行预取帖子与评论
const [postsSettled, commentsSettled, counts, settings] = await Promise.all([
const [postsSettled, commentsSettled, counts] = await Promise.all([
fetchAdminContentPosts(1, initialStatus, cookie || undefined)
.then((res) => ({ ok: true as const, res }))
.catch((e) => ({
@@ -65,7 +65,6 @@ export default async function AdminContentPage({ searchParams }: ContentPageProp
error: e instanceof Error ? e.message : "获取评论失败",
})),
fetchAdminContentCounts(cookie || undefined),
getPublicSettingsCached(),
]);
if (postsSettled.ok) {
@@ -92,10 +91,8 @@ export default async function AdminContentPage({ searchParams }: ContentPageProp
initialCommentsTotal={initialCommentsTotal}
initialCounts={counts}
initError={initError}
canConfigureSettings={isSuperOrOwner(me.user.role)}
canPurge={isOwner(me.user.role)}
viewerRole={me.user.role}
initialSettings={settings}
/>
);
}