完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 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

@@ -0,0 +1,280 @@
"use client";
import { ExternalLink, LogIn, MessageSquare, MessagesSquare, ShieldCheck, Timer } from "lucide-react";
import { useState } from "react";
import {
AdminSettingsGroup,
AdminSettingsRow,
AdminStepper,
AdminSwitch,
} from "@/components/admin";
import ConfirmDialog from "@/components/ConfirmDialog";
import useUnsavedGuard from "@/hooks/useUnsavedGuard";
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
import { toast } from "@/lib/toast";
import OperationsPanel from "./OperationsPanel";
function InstantNote({ saving }: { saving?: boolean }) {
return <span className="admin-settings-instant">{saving ? "正在保存…" : "修改后立即生效"}</span>;
}
/** 内容互动开关 + 访问安全模块 */
export default function AccessSettings({ initial }: { initial: PublicSettings }) {
const [allowComments, setAllowComments] = useState(initial.allow_comments !== false);
const [commentsRequireLogin, setCommentsRequireLogin] = useState(
initial.comments_require_login === true,
);
const [allowMessages, setAllowMessages] = useState(initial.allow_messages !== false);
const [postLinkNewTab, setPostLinkNewTab] = useState(initial.post_link_new_tab !== false);
const [trustOn, setTrustOn] = useState(initial.trust_reviewed_publish !== false);
const [cooldown, setCooldown] = useState(initial.post_cooldown_hours);
const [savedCooldown, setSavedCooldown] = useState(initial.post_cooldown_hours);
const [togglingComments, setTogglingComments] = useState(false);
const [togglingCommentsLogin, setTogglingCommentsLogin] = useState(false);
const [togglingMessages, setTogglingMessages] = useState(false);
const [togglingLink, setTogglingLink] = useState(false);
const [togglingTrust, setTogglingTrust] = useState(false);
const [savingCooldown, setSavingCooldown] = useState(false);
const [moduleDirty, setModuleDirty] = useState(false);
const leave = useUnsavedGuard(moduleDirty);
const toggleComments = async () => {
if (togglingComments) return;
const next = !allowComments;
setTogglingComments(true);
try {
const res = await apiUpdateSiteSettings({ allow_comments: next });
setAllowComments(res.allow_comments);
toast(res.allow_comments ? "已开放评论" : "已关闭评论", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setTogglingComments(false);
}
};
const toggleCommentsRequireLogin = async () => {
if (togglingCommentsLogin || !allowComments) return;
const next = !commentsRequireLogin;
setTogglingCommentsLogin(true);
try {
const res = await apiUpdateSiteSettings({ comments_require_login: next });
setCommentsRequireLogin(res.comments_require_login === true);
toast(res.comments_require_login ? "评论仅登录用户可见" : "游客也可查看评论", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setTogglingCommentsLogin(false);
}
};
const toggleMessages = async () => {
if (togglingMessages) return;
const next = !allowMessages;
setTogglingMessages(true);
try {
const res = await apiUpdateSiteSettings({ allow_messages: next });
setAllowMessages(res.allow_messages);
toast(res.allow_messages ? "已开放消息" : "已关闭消息", "ok");
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setTogglingMessages(false);
}
};
const togglePostLinkNewTab = async () => {
if (togglingLink) return;
const next = !postLinkNewTab;
setTogglingLink(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 {
setTogglingLink(false);
}
};
const toggleTrust = async () => {
if (togglingTrust) return;
setTogglingTrust(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 {
setTogglingTrust(false);
}
};
const saveCooldown = async () => {
if (savingCooldown) return;
const n = Math.max(0, Math.min(168, Math.round(Number(cooldown)) || 0));
setSavingCooldown(true);
try {
const res = await apiUpdateSiteSettings({ post_cooldown_hours: n });
setCooldown(res.post_cooldown_hours);
setSavedCooldown(res.post_cooldown_hours);
toast(
res.post_cooldown_hours === 0
? "已关闭新用户发帖冷静期"
: `新用户发帖冷静期已设为 ${res.post_cooldown_hours} 小时(评论与聊天不受限)`,
"ok",
);
} catch (e) {
toast(e instanceof Error ? e.message : "保存失败");
} finally {
setSavingCooldown(false);
}
};
return (
<div className="admin-settings-stack">
<AdminSettingsGroup title="内容与互动">
<AdminSettingsRow
label={
<span className="inline-flex items-center gap-1.5">
<MessageSquare size={15} /> 开放评论
</span>
}
hintBelow="关闭后全站不提供评论功能。"
>
<div className="flex items-center gap-2">
<AdminSwitch
checked={allowComments}
disabled={togglingComments}
onChange={() => void toggleComments()}
label="开放评论"
/>
<InstantNote saving={togglingComments} />
</div>
</AdminSettingsRow>
<AdminSettingsRow
label={
<span className="inline-flex items-center gap-1.5">
<LogIn size={15} /> 登录可见评论
</span>
}
hintBelow="开启后游客无法查看评论内容,需登录后阅读与参与。"
>
<div className="flex items-center gap-2">
<AdminSwitch
checked={commentsRequireLogin}
disabled={!allowComments || togglingCommentsLogin}
onChange={() => void toggleCommentsRequireLogin()}
label="登录可见评论"
/>
<InstantNote saving={togglingCommentsLogin} />
</div>
</AdminSettingsRow>
<AdminSettingsRow
label={
<span className="inline-flex items-center gap-1.5">
<MessagesSquare size={15} /> 开放消息
</span>
}
hintBelow="关闭后前台隐藏私聊与群聊入口。"
>
<div className="flex items-center gap-2">
<AdminSwitch
checked={allowMessages}
disabled={togglingMessages}
onChange={() => void toggleMessages()}
label="开放消息"
/>
<InstantNote saving={togglingMessages} />
</div>
</AdminSettingsRow>
<AdminSettingsRow
label={
<span className="inline-flex items-center gap-1.5">
<ExternalLink size={15} /> 外链新标签打开
</span>
}
hintBelow="帖子与评论中的外链是否在新标签打开。"
>
<div className="flex items-center gap-2">
<AdminSwitch
checked={postLinkNewTab}
disabled={togglingLink}
onChange={() => void togglePostLinkNewTab()}
label="外链新标签打开"
/>
<InstantNote saving={togglingLink} />
</div>
</AdminSettingsRow>
<AdminSettingsRow
label={
<span className="inline-flex items-center gap-1.5">
<ShieldCheck size={15} /> 过审免审
</span>
}
hintBelow="用户曾有过审内容后,后续发帖与评论直接公开。"
>
<div className="flex items-center gap-2">
<AdminSwitch
checked={trustOn}
disabled={togglingTrust}
onChange={() => void toggleTrust()}
label="过审免审"
/>
<InstantNote saving={togglingTrust} />
</div>
</AdminSettingsRow>
<AdminSettingsRow
label={
<span className="inline-flex items-center gap-1.5">
<Timer size={15} /> 新用户发帖冷静期
</span>
}
hintBelow="仅限制新注册用户发帖(0=关闭,上限 168 小时)。评论与聊天不受限。"
>
<div className="flex items-center gap-2 flex-wrap">
<AdminStepper
value={cooldown}
onChange={setCooldown}
min={0}
max={168}
unit="小时"
ariaLabel="新用户发帖冷静期小时数"
disabled={savingCooldown}
/>
<button
type="button"
className="btn btn-line btn-sm"
disabled={savingCooldown || cooldown === savedCooldown}
onClick={() => void saveCooldown()}
>
{savingCooldown ? "保存中…" : "保存"}
</button>
</div>
</AdminSettingsRow>
</AdminSettingsGroup>
<OperationsPanel name="security" onDirtyChange={setModuleDirty} />
<ConfirmDialog
open={leave.leaveOpen}
title="有未保存的修改"
message="离开后,当前保存单元里尚未写入的修改会丢失。确定离开?"
confirmLabel="离开"
danger
onCancel={leave.cancelLeave}
onConfirm={leave.confirmLeave}
/>
</div>
);
}