"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}
/>
))}
);
}