完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
280
frontend/app/admin/settings/AccessSettings.tsx
Normal file
280
frontend/app/admin/settings/AccessSettings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user