281 lines
10 KiB
TypeScript
281 lines
10 KiB
TypeScript
"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>
|
||
);
|
||
}
|