feat: 全站评论开关,并统一后台表单控件
关闭评论后前台隐藏评论能力;外观/设置/公告改用 AdminField 与 Stepper。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import { toast } from "@/lib/toast";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog";
|
||||
import {
|
||||
AdminEmpty,
|
||||
AdminField,
|
||||
AdminIconButton,
|
||||
AdminPageHeader,
|
||||
AdminSegmented,
|
||||
@@ -155,29 +156,24 @@ export default function AnnouncementAdmin({
|
||||
{form.id == null ? "新建公告" : "编辑公告"}
|
||||
</p>
|
||||
|
||||
<label className="field-label" htmlFor="anno-title">
|
||||
标题
|
||||
</label>
|
||||
<input
|
||||
<AdminField
|
||||
id="anno-title"
|
||||
className="field mb-4"
|
||||
className="mb-4"
|
||||
label="标题"
|
||||
maxLength={200}
|
||||
value={form.title}
|
||||
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
||||
onChange={(title) => setForm({ ...form, title })}
|
||||
placeholder="请输入公告标题"
|
||||
/>
|
||||
|
||||
<div className="flex items-end gap-4 mb-4 flex-wrap">
|
||||
<div className="w-36">
|
||||
<label className="field-label" htmlFor="anno-tag">
|
||||
标签
|
||||
</label>
|
||||
<input
|
||||
<AdminField
|
||||
id="anno-tag"
|
||||
className="field"
|
||||
label="标签"
|
||||
maxLength={32}
|
||||
value={form.tag}
|
||||
onChange={(e) => setForm({ ...form, tag: e.target.value })}
|
||||
onChange={(tag) => setForm({ ...form, tag })}
|
||||
placeholder="公告"
|
||||
/>
|
||||
</div>
|
||||
@@ -213,14 +209,13 @@ export default function AnnouncementAdmin({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="field-label" htmlFor="anno-content">
|
||||
正文(支持 Markdown)
|
||||
</label>
|
||||
<textarea
|
||||
<AdminField
|
||||
id="anno-content"
|
||||
className="field min-h-[260px]"
|
||||
label="正文(支持 Markdown)"
|
||||
as="textarea"
|
||||
minHeight="260px"
|
||||
value={form.content}
|
||||
onChange={(e) => setForm({ ...form, content: e.target.value })}
|
||||
onChange={(content) => setForm({ ...form, content })}
|
||||
placeholder="请输入公告正文,支持 Markdown 语法…"
|
||||
/>
|
||||
|
||||
|
||||
@@ -5,14 +5,22 @@ import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Palette,
|
||||
Check,
|
||||
Code2,
|
||||
Loader2,
|
||||
Save,
|
||||
Undo2,
|
||||
RotateCcw,
|
||||
Pipette,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { AdminPageHeader, AdminSection } from "@/components/admin";
|
||||
import { apiUpdateSiteSettings } from "@/lib/api";
|
||||
import {
|
||||
AdminPageHeader,
|
||||
AdminSection,
|
||||
AdminSettingRow,
|
||||
AdminStepper,
|
||||
AdminSwitch,
|
||||
} from "@/components/admin";
|
||||
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
import {
|
||||
ACCENT_PRESETS,
|
||||
@@ -23,13 +31,27 @@ import {
|
||||
normalizeHex,
|
||||
} from "@/lib/theme";
|
||||
|
||||
// 后台「外观设置」:站点主题色预设 + 自定义取色 + 实时预览。
|
||||
// 修改时立即对整页实时预览(仅本地),保存后才写库并对所有用户生效。
|
||||
export default function AppearanceAdmin({ initialAccent }: { initialAccent: string }) {
|
||||
// 后台「外观设置」:主题色、代码块折叠与界面动画。
|
||||
// 主题色修改时立即对整页实时预览(仅本地),保存后才写库并对所有用户生效。
|
||||
export default function AppearanceAdmin({ initial }: { initial: PublicSettings }) {
|
||||
const router = useRouter();
|
||||
const initialAccent = initial.accent || "";
|
||||
// 已保存值:空串=内置默认(靛蓝)
|
||||
const [savedHex, setSavedHex] = useState(initialAccent || "");
|
||||
const [savedHex, setSavedHex] = useState(initialAccent);
|
||||
const [draft, setDraft] = useState(initialAccent || DEFAULT_ACCENT);
|
||||
const [codeAutoFold, setCodeAutoFold] = useState(initial.code_block_auto_fold !== false);
|
||||
const [foldLines, setFoldLines] = useState(
|
||||
Number.isFinite(initial.code_block_fold_lines) ? initial.code_block_fold_lines : 5
|
||||
);
|
||||
const [togglingFold, setTogglingFold] = useState(false);
|
||||
const [savingFoldLines, setSavingFoldLines] = useState(false);
|
||||
const [uiAnimations, setUiAnimations] = useState(initial.ui_animations !== false);
|
||||
const [animCodeFold, setAnimCodeFold] = useState(initial.anim_code_fold !== false);
|
||||
const [animSmoothScroll, setAnimSmoothScroll] = useState(
|
||||
initial.anim_smooth_scroll !== false
|
||||
);
|
||||
const [animChrome, setAnimChrome] = useState(initial.anim_chrome !== false);
|
||||
const [togglingMotion, setTogglingMotion] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const savedRef = useRef(savedHex);
|
||||
savedRef.current = savedHex;
|
||||
@@ -76,12 +98,79 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
|
||||
applyAccentTheme(savedHex || null);
|
||||
};
|
||||
|
||||
const toggleCodeFold = async () => {
|
||||
if (togglingFold) return;
|
||||
const next = !codeAutoFold;
|
||||
setTogglingFold(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ code_block_auto_fold: next });
|
||||
setCodeAutoFold(res.code_block_auto_fold);
|
||||
toast(
|
||||
res.code_block_auto_fold
|
||||
? `已开启代码块自动折叠(超过 ${res.code_block_fold_lines} 行)`
|
||||
: "已关闭代码块自动折叠",
|
||||
"ok"
|
||||
);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setTogglingFold(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveFoldLines = async () => {
|
||||
if (savingFoldLines) return;
|
||||
const raw = Math.round(Number(foldLines));
|
||||
const n = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : 5;
|
||||
setSavingFoldLines(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ code_block_fold_lines: n });
|
||||
setFoldLines(res.code_block_fold_lines);
|
||||
toast(
|
||||
res.code_block_fold_lines === 0
|
||||
? "折叠预览已设为 0 行(无预览)"
|
||||
: `折叠阈值已设为超过 ${res.code_block_fold_lines} 行`,
|
||||
"ok"
|
||||
);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setSavingFoldLines(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMotionField = async (
|
||||
key: "ui_animations" | "anim_code_fold" | "anim_smooth_scroll" | "anim_chrome",
|
||||
next: boolean
|
||||
) => {
|
||||
if (togglingMotion) return;
|
||||
setTogglingMotion(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ [key]: next });
|
||||
setUiAnimations(res.ui_animations);
|
||||
setAnimCodeFold(res.anim_code_fold);
|
||||
setAnimSmoothScroll(res.anim_smooth_scroll);
|
||||
setAnimChrome(res.anim_chrome);
|
||||
const labels: Record<typeof key, [string, string]> = {
|
||||
ui_animations: ["已开启界面动画", "已关闭界面动画(全站减少动态效果)"],
|
||||
anim_code_fold: ["已开启代码折叠动画", "已关闭代码折叠动画"],
|
||||
anim_smooth_scroll: ["已开启平滑滚动", "已关闭平滑滚动"],
|
||||
anim_chrome: ["已开启装饰入场动画", "已关闭装饰入场动画"],
|
||||
};
|
||||
toast(next ? labels[key][0] : labels[key][1], "ok");
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setTogglingMotion(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<AdminPageHeader
|
||||
icon={Palette}
|
||||
title="外观设置"
|
||||
description="配置站点主题色,全站按钮、链接、选中态会统一联动"
|
||||
description="配置站点主题色、代码块折叠与界面动画"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4 mb-6">
|
||||
@@ -157,15 +246,13 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
|
||||
</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className="flex items-center rounded-[var(--r-sm)] border bg-[var(--panel)] px-3 h-11 font-mono text-[14px] w-40"
|
||||
style={{ borderColor: invalid ? "var(--danger)" : "var(--line-2)" }}
|
||||
className={`admin-field-shell px-3 h-11 font-mono text-[14px] w-40${invalid ? " is-invalid" : ""}`}
|
||||
>
|
||||
<span style={{ color: "var(--ink-3)" }}>#</span>
|
||||
<input
|
||||
value={draft.startsWith("#") ? draft.slice(1) : draft}
|
||||
onChange={(e) => setDraft("#" + e.target.value.replace(/[^0-9a-fA-F#]/g, ""))}
|
||||
className="flex-1 min-w-0 bg-transparent outline-none ml-1 uppercase"
|
||||
style={{ color: "var(--ink)" }}
|
||||
className="admin-field-input flex-1 min-w-0 px-0 py-0 ml-1 uppercase"
|
||||
maxLength={7}
|
||||
spellCheck={false}
|
||||
aria-label="主题色 HEX 值"
|
||||
@@ -253,6 +340,131 @@ export default function AppearanceAdmin({ initialAccent }: { initialAccent: stri
|
||||
</div>
|
||||
</div>
|
||||
</AdminSection>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<Code2 size={15} /> 代码块自动折叠
|
||||
</>
|
||||
}
|
||||
description="开启后,帖子与评论中超过阈值行数的 Markdown 代码块默认折叠,读者可点「展开全部」。关闭则始终完整展示。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={codeAutoFold}
|
||||
disabled={togglingFold}
|
||||
onChange={() => void toggleCodeFold()}
|
||||
label="代码块自动折叠"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{codeAutoFold ? (
|
||||
<>
|
||||
<label className="field-label" htmlFor="fold-lines">
|
||||
折叠行数阈值
|
||||
</label>
|
||||
<p className="meta text-[12.5px] mb-2.5 leading-relaxed">
|
||||
代码块行数超过该值时折叠;折叠后约露出同样行数。设为 0 表示无预览。范围 0–100,默认 5。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<AdminStepper
|
||||
id="fold-lines"
|
||||
value={foldLines}
|
||||
onChange={setFoldLines}
|
||||
min={0}
|
||||
max={100}
|
||||
unit="行"
|
||||
ariaLabel="折叠行数阈值"
|
||||
disabled={savingFoldLines}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={savingFoldLines}
|
||||
onClick={() => void saveFoldLines()}
|
||||
>
|
||||
{savingFoldLines ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</AdminSettingRow>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<Sparkles size={15} /> 界面动画
|
||||
</>
|
||||
}
|
||||
description="总开关关闭后全站即时切换(等同减少动态效果)。系统若已开启「减少动态效果」,始终优先生效。开启总开关后可细调子项。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={uiAnimations}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("ui_animations", !uiAnimations)}
|
||||
label="界面动画总开关"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{uiAnimations ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
代码折叠动画
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
代码块展开/收起的高度过渡与跟滚。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animCodeFold}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_code_fold", !animCodeFold)}
|
||||
label="代码折叠动画"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
平滑滚动
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
目录跳转、回顶、新内容提示等 scroll 行为。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animSmoothScroll}
|
||||
disabled={togglingMotion}
|
||||
onChange={() =>
|
||||
void toggleMotionField("anim_smooth_scroll", !animSmoothScroll)
|
||||
}
|
||||
label="平滑滚动"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
装饰入场
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
列表入场、Toast、点赞弹出、在线脉冲等装饰性动画。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animChrome}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_chrome", !animChrome)}
|
||||
label="装饰入场"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</AdminSettingRow>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 sticky bottom-5 panel !p-3.5 pl-5 z-10">
|
||||
|
||||
@@ -18,6 +18,6 @@ export default async function AdminAppearancePage() {
|
||||
if (!me.user || !isSuperOrOwner(me.user.role)) {
|
||||
return <Forbidden text="外观设置仅超级管理员和站长可操作。" />;
|
||||
}
|
||||
const { accent } = await getPublicSettingsCached();
|
||||
return <AppearanceAdmin initialAccent={accent || ""} />;
|
||||
const settings = await getPublicSettingsCached();
|
||||
return <AppearanceAdmin initial={settings} />;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Code2,
|
||||
Globe,
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
Save,
|
||||
Sparkles,
|
||||
UserPlus,
|
||||
} from "lucide-react";
|
||||
import { Globe, Loader2, MessageSquare, Save, UserPlus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AdminField,
|
||||
AdminPageHeader,
|
||||
AdminSection,
|
||||
AdminSettingRow,
|
||||
@@ -24,33 +17,13 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
|
||||
const [siteDescription, setSiteDescription] = useState(initial.site_description);
|
||||
const [allowRegister, setAllowRegister] = useState(initial.allow_register);
|
||||
const [allowComments, setAllowComments] = useState(initial.allow_comments !== false);
|
||||
const [codeAutoFold, setCodeAutoFold] = useState(
|
||||
initial.code_block_auto_fold !== false
|
||||
);
|
||||
const [foldLines, setFoldLines] = useState(
|
||||
Number.isFinite(initial.code_block_fold_lines) ? initial.code_block_fold_lines : 5
|
||||
);
|
||||
const [savedFoldLines, setSavedFoldLines] = useState(
|
||||
Number.isFinite(initial.code_block_fold_lines) ? initial.code_block_fold_lines : 5
|
||||
);
|
||||
const [savedName, setSavedName] = useState(initial.site_name);
|
||||
const [savedDesc, setSavedDesc] = useState(initial.site_description);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [togglingReg, setTogglingReg] = useState(false);
|
||||
const [togglingComments, setTogglingComments] = useState(false);
|
||||
const [togglingFold, setTogglingFold] = useState(false);
|
||||
const [savingFoldLines, setSavingFoldLines] = useState(false);
|
||||
const [uiAnimations, setUiAnimations] = useState(initial.ui_animations !== false);
|
||||
const [animCodeFold, setAnimCodeFold] = useState(initial.anim_code_fold !== false);
|
||||
const [animSmoothScroll, setAnimSmoothScroll] = useState(
|
||||
initial.anim_smooth_scroll !== false
|
||||
);
|
||||
const [animChrome, setAnimChrome] = useState(initial.anim_chrome !== false);
|
||||
const [togglingMotion, setTogglingMotion] = useState(false);
|
||||
|
||||
const dirty =
|
||||
siteName.trim() !== savedName || siteDescription.trim() !== savedDesc;
|
||||
const foldLinesDirty = foldLines !== savedFoldLines;
|
||||
const dirty = siteName.trim() !== savedName || siteDescription.trim() !== savedDesc;
|
||||
|
||||
const saveIdentity = async () => {
|
||||
if (saving) return;
|
||||
@@ -107,108 +80,35 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCodeFold = async () => {
|
||||
if (togglingFold) return;
|
||||
const next = !codeAutoFold;
|
||||
setTogglingFold(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ code_block_auto_fold: next });
|
||||
setCodeAutoFold(res.code_block_auto_fold);
|
||||
toast(
|
||||
res.code_block_auto_fold
|
||||
? `已开启代码块自动折叠(超过 ${res.code_block_fold_lines} 行)`
|
||||
: "已关闭代码块自动折叠",
|
||||
"ok"
|
||||
);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setTogglingFold(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveFoldLines = async () => {
|
||||
if (savingFoldLines) return;
|
||||
const raw = Math.round(Number(foldLines));
|
||||
const n = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : 5;
|
||||
setSavingFoldLines(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ code_block_fold_lines: n });
|
||||
setFoldLines(res.code_block_fold_lines);
|
||||
setSavedFoldLines(res.code_block_fold_lines);
|
||||
toast(
|
||||
res.code_block_fold_lines === 0
|
||||
? "折叠预览已设为 0 行(无预览)"
|
||||
: `折叠阈值已设为超过 ${res.code_block_fold_lines} 行`,
|
||||
"ok"
|
||||
);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setSavingFoldLines(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMotionField = async (
|
||||
key: "ui_animations" | "anim_code_fold" | "anim_smooth_scroll" | "anim_chrome",
|
||||
next: boolean
|
||||
) => {
|
||||
if (togglingMotion) return;
|
||||
setTogglingMotion(true);
|
||||
try {
|
||||
const res = await apiUpdateSiteSettings({ [key]: next });
|
||||
setUiAnimations(res.ui_animations);
|
||||
setAnimCodeFold(res.anim_code_fold);
|
||||
setAnimSmoothScroll(res.anim_smooth_scroll);
|
||||
setAnimChrome(res.anim_chrome);
|
||||
const labels: Record<typeof key, [string, string]> = {
|
||||
ui_animations: ["已开启界面动画", "已关闭界面动画(全站减少动态效果)"],
|
||||
anim_code_fold: ["已开启代码折叠动画", "已关闭代码折叠动画"],
|
||||
anim_smooth_scroll: ["已开启平滑滚动", "已关闭平滑滚动"],
|
||||
anim_chrome: ["已开启装饰入场动画", "已关闭装饰入场动画"],
|
||||
};
|
||||
toast(next ? labels[key][0] : labels[key][1], "ok");
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "保存失败");
|
||||
} finally {
|
||||
setTogglingMotion(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<AdminPageHeader
|
||||
icon={Globe}
|
||||
title="站点设置"
|
||||
description="配置站点名称、简介与注册/评论开关;主题色请到「外观」页调整"
|
||||
description="配置站点名称、简介与注册/评论开关;主题色、代码折叠与动画请到「外观」页调整"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<AdminSection
|
||||
title="基本信息"
|
||||
description="用于页眉品牌、页脚版权与 SEO 元信息"
|
||||
>
|
||||
<label className="field-label" htmlFor="site-name">
|
||||
站点名称
|
||||
</label>
|
||||
<input
|
||||
<AdminSection title="基本信息" description="用于页眉品牌、页脚版权与 SEO 元信息">
|
||||
<AdminField
|
||||
id="site-name"
|
||||
className="field mb-4"
|
||||
className="mb-4"
|
||||
label="站点名称"
|
||||
maxLength={32}
|
||||
value={siteName}
|
||||
onChange={(e) => setSiteName(e.target.value)}
|
||||
onChange={setSiteName}
|
||||
placeholder="姜十三论坛"
|
||||
/>
|
||||
|
||||
<label className="field-label" htmlFor="site-desc">
|
||||
站点简介
|
||||
</label>
|
||||
<textarea
|
||||
<AdminField
|
||||
id="site-desc"
|
||||
className="field min-h-[96px] mb-4"
|
||||
className="mb-4"
|
||||
label="站点简介"
|
||||
as="textarea"
|
||||
minHeight="96px"
|
||||
maxLength={160}
|
||||
value={siteDescription}
|
||||
onChange={(e) => setSiteDescription(e.target.value)}
|
||||
onChange={setSiteDescription}
|
||||
placeholder="一句话介绍社区定位…"
|
||||
/>
|
||||
|
||||
@@ -248,7 +148,7 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
|
||||
<MessageSquare size={15} /> 开放评论
|
||||
</>
|
||||
}
|
||||
description="关闭后全站禁止发表新评论与回复,已有评论仍可浏览。适用于 ICP/IPC 备案审核等合规需要;前台仅提示「暂停评论」,不会展示备案事由。"
|
||||
description="关闭后前台不展示评论入口、评论数量与已有评论,并提示本站不支持评论。适用于备案审核等合规需要。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={allowComments}
|
||||
@@ -258,132 +158,6 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<Code2 size={15} /> 代码块自动折叠
|
||||
</>
|
||||
}
|
||||
description="开启后,帖子与评论中超过阈值行数的 Markdown 代码块默认折叠,读者可点「展开全部」。关闭则始终完整展示。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={codeAutoFold}
|
||||
disabled={togglingFold}
|
||||
onChange={() => void toggleCodeFold()}
|
||||
label="代码块自动折叠"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{codeAutoFold ? (
|
||||
<>
|
||||
<label className="field-label" htmlFor="fold-lines">
|
||||
折叠行数阈值
|
||||
</label>
|
||||
<p className="meta text-[12.5px] mb-2.5 leading-relaxed">
|
||||
代码块行数超过该值时折叠;折叠后约露出同样行数。设为 0 表示无预览。范围 0–100,默认 5。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<input
|
||||
id="fold-lines"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
className="field w-24"
|
||||
value={foldLines}
|
||||
onChange={(e) => setFoldLines(Number(e.target.value))}
|
||||
aria-label="折叠行数阈值"
|
||||
/>
|
||||
<span className="meta text-[12.5px]">行</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-line btn-sm"
|
||||
disabled={!foldLinesDirty || savingFoldLines}
|
||||
onClick={() => void saveFoldLines()}
|
||||
>
|
||||
{savingFoldLines ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</AdminSettingRow>
|
||||
|
||||
<AdminSettingRow
|
||||
title={
|
||||
<>
|
||||
<Sparkles size={15} /> 界面动画
|
||||
</>
|
||||
}
|
||||
description="总开关关闭后全站即时切换(等同减少动态效果)。系统若已开启「减少动态效果」,始终优先生效。开启总开关后可细调子项。"
|
||||
control={
|
||||
<AdminSwitch
|
||||
checked={uiAnimations}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("ui_animations", !uiAnimations)}
|
||||
label="界面动画总开关"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{uiAnimations ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
代码折叠动画
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
代码块展开/收起的高度过渡与跟滚。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animCodeFold}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_code_fold", !animCodeFold)}
|
||||
label="代码折叠动画"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
平滑滚动
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
目录跳转、回顶、新内容提示等 scroll 行为。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animSmoothScroll}
|
||||
disabled={togglingMotion}
|
||||
onChange={() =>
|
||||
void toggleMotionField("anim_smooth_scroll", !animSmoothScroll)
|
||||
}
|
||||
label="平滑滚动"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[13.5px] font-medium" style={{ color: "var(--ink)" }}>
|
||||
装饰入场
|
||||
</p>
|
||||
<p className="meta text-[12.5px] mt-0.5 leading-relaxed">
|
||||
列表入场、Toast、点赞弹出、在线脉冲等装饰性动画。
|
||||
</p>
|
||||
</div>
|
||||
<AdminSwitch
|
||||
checked={animChrome}
|
||||
disabled={togglingMotion}
|
||||
onChange={() => void toggleMotionField("anim_chrome", !animChrome)}
|
||||
label="装饰入场"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</AdminSettingRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user