Files
jiang13-bbs/frontend/app/admin/settings/AccessSettings.tsx

281 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}