feat(settings): 站点界面动画总开关与子开关

管理后台可关全站动画,并细调代码折叠、平滑滚动、装饰入场;统一 lib/motion,尊重系统减少动态效果。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 06:59:26 +08:00
parent c6df1b3b7e
commit 2b27703e6c
14 changed files with 511 additions and 46 deletions

View File

@@ -6,6 +6,7 @@ import {
Globe,
Loader2,
Save,
Sparkles,
UserPlus,
} from "lucide-react";
import {
@@ -63,6 +64,13 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
const [togglingReg, setTogglingReg] = 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;
@@ -151,6 +159,32 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
}
};
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">
<div className="mb-5">
@@ -286,6 +320,79 @@ export default function SettingsAdmin({ initial }: { initial: PublicSettings })
</div>
) : null}
</section>
<section className="j13-module p-5 sm:p-6 mb-5">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="min-w-0 flex-1">
<h2 className="text-[15px] font-semibold flex items-center gap-1.5" style={{ color: "var(--ink)" }}>
<Sparkles size={15} /> 界面动画
</h2>
<p className="meta text-[12.5px] mt-1 leading-relaxed">
总开关关闭后全站即时切换(等同减少动态效果)。系统若已开启「减少动态效果」,始终优先生效。开启总开关后可细调子项。
</p>
</div>
<Switch
checked={uiAnimations}
disabled={togglingMotion}
onChange={() => void toggleMotionField("ui_animations", !uiAnimations)}
label="界面动画总开关"
/>
</div>
{uiAnimations ? (
<div className="mt-4 pt-4 border-t border-[var(--line)] 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>
<Switch
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>
<Switch
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>
<Switch
checked={animChrome}
disabled={togglingMotion}
onChange={() => void toggleMotionField("anim_chrome", !animChrome)}
label="装饰入场"
/>
</div>
</div>
) : null}
</section>
</div>
);
}