"use client"; import { ExternalLink, LogIn, MessageSquare, MessagesSquare, ShieldCheck, ShieldOff, Timer, Lock, Hourglass, Coins, History } 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 {saving ? "正在保存…" : "修改后立即生效"}; } /** 内容锁定 4 键(小时上限 8760 / 积分上限 100,后端裁剪) */ type LockKeys = Pick< PublicSettings, "post_edit_lock_hours" | "comment_edit_lock_hours" | "necro_reply_after_hours" | "necro_reply_penalty" >; type LockKey = keyof LockKeys; const LOCK_META: { key: LockKey; icon: React.ReactNode; label: string; hint: string; max: number; unit: string; toast: (v: number) => string }[] = [ { key: "post_edit_lock_hours", icon: , label: "帖子可编辑时长", hint: "发帖后仅在该时长内可编辑(0=不限制,上限 8760 小时),到期后编辑入口隐藏;管理团队不受限。", max: 8760, unit: "小时", toast: (v) => (v === 0 ? "已关闭帖子编辑时限" : `帖子可编辑时长已设为 ${v} 小时`), }, { key: "comment_edit_lock_hours", icon: , label: "评论可编辑时长", hint: "评论发布后仅在该时长内可编辑(0=不限制,上限 8760 小时);管理团队不受限。", max: 8760, unit: "小时", toast: (v) => (v === 0 ? "已关闭评论编辑时限" : `评论可编辑时长已设为 ${v} 小时`), }, { key: "necro_reply_after_hours", icon: , label: "旧帖回复判定时长", hint: "回复超过该时长的帖子将提醒作者并按下方分值扣分(0=关闭,上限 8760 小时);管理团队不受限。", max: 8760, unit: "小时", toast: (v) => (v === 0 ? "已关闭旧帖回复提醒" : `旧帖判定时长已设为 ${v} 小时`), }, { key: "necro_reply_penalty", icon: , label: "旧帖回复扣除积分", hint: "触发旧帖回复时扣除的积分(0=仅提醒不扣分,上限 100);积分不足将无法回复。", max: 100, unit: "分", toast: (v) => (v === 0 ? "旧帖回复仅提醒不扣分" : `旧帖回复将扣除 ${v} 积分`), }, ]; const pickLock = (s: PublicSettings): LockKeys => ({ post_edit_lock_hours: s.post_edit_lock_hours, comment_edit_lock_hours: s.comment_edit_lock_hours, necro_reply_after_hours: s.necro_reply_after_hours, necro_reply_penalty: s.necro_reply_penalty, }); /** 内容互动开关 + 访问安全模块 */ 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 [withoutReviewOn, setWithoutReviewOn] = useState(initial.publish_without_review === true); 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 [togglingWithoutReview, setTogglingWithoutReview] = useState(false); const [savingCooldown, setSavingCooldown] = useState(false); const [lockForm, setLockForm] = useState(() => pickLock(initial)); const [savedLock, setSavedLock] = useState(() => pickLock(initial)); const [savingLockKey, setSavingLockKey] = useState(null); const [moduleDirty, setModuleDirty] = useState(false); const lockDirty = (Object.keys(savedLock) as LockKey[]).some((k) => lockForm[k] !== savedLock[k]); const leave = useUnsavedGuard(moduleDirty || lockDirty); 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 toggleWithoutReview = async () => { if (togglingWithoutReview) return; setTogglingWithoutReview(true); try { const res = await apiUpdateSiteSettings({ publish_without_review: !withoutReviewOn }); setWithoutReviewOn(res.publish_without_review === true); toast( res.publish_without_review ? "已开启全站免审(发帖与评论直接公开,无需过审记录)" : "已关闭全站免审(将按过审免审规则决定是否待审)", "ok", ); } catch (e) { toast(e instanceof Error ? e.message : "保存失败"); } finally { setTogglingWithoutReview(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); } }; const saveLock = async (key: LockKey) => { if (savingLockKey) return; const n = Math.max(0, Math.min(8760, Math.round(Number(lockForm[key])) || 0)); setSavingLockKey(key); try { const res = await apiUpdateSiteSettings({ [key]: n }); const saved = pickLock(res); setLockForm((f) => ({ ...f, [key]: saved[key] })); setSavedLock((f) => ({ ...f, [key]: saved[key] })); toast(LOCK_META.find((m) => m.key === key)?.toast(saved[key]) || "已保存", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "保存失败"); } finally { setSavingLockKey(null); } }; return (
开放评论 } hintBelow="关闭后全站不提供评论功能。" >
void toggleComments()} label="开放评论" />
登录可见评论 } hintBelow="开启后游客无法查看评论内容,需登录后阅读与参与。" >
void toggleCommentsRequireLogin()} label="登录可见评论" />
开放消息 } hintBelow="关闭后前台隐藏私聊与群聊入口。" >
void toggleMessages()} label="开放消息" />
外链新标签打开 } hintBelow="帖子与评论中的外链是否在新标签打开。" >
void togglePostLinkNewTab()} label="外链新标签打开" />
全站免审 } hintBelow="所有人发帖与评论直接公开,无需有过审记录;开启后优先生效。" >
void toggleWithoutReview()} label="全站免审" />
过审免审 } hintBelow={ withoutReviewOn ? "全站免审已开启,所有内容直接公开,本开关暂不参与判定。" : "用户曾有过审内容后,后续发帖与评论直接公开。" } >
void toggleTrust()} label="过审免审" />
新用户发帖冷静期 } hintBelow="仅限制新注册用户发帖(0=关闭,上限 168 小时)。评论与聊天不受限。" >
{LOCK_META.map((m) => ( {m.icon} {m.label}} hintBelow={m.hint}>
setLockForm((f) => ({ ...f, [m.key]: v }))} min={0} max={m.max} unit={m.unit} ariaLabel={`${m.label}${m.unit}`} disabled={savingLockKey !== null} />
))}
); }