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

@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { ArrowUp, MessageSquare } from "lucide-react";
import LikeButton from "./LikeButton";
import { scrollBehavior } from "@/lib/motion";
/**
* 帖子详情移动端浮动操作条(lg 以下显示):
@@ -28,7 +29,7 @@ export default function MobilePostBar({
const scrollToComments = () => {
document.getElementById("comment-input")?.scrollIntoView({
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
behavior: scrollBehavior(),
block: "center",
});
};
@@ -36,7 +37,7 @@ export default function MobilePostBar({
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
behavior: scrollBehavior(),
});
};

View File

@@ -0,0 +1,52 @@
"use client";
import { useEffect, type ReactNode } from "react";
import {
applyMotionSiteConfig,
getMotionSiteConfig,
motionConfigFromSettings,
type MotionSiteConfig,
} from "@/lib/motion";
import { realtime, RT_SETTINGS_CHANGED, type RtSettingsData } from "@/lib/realtime";
/**
* 根布局注入:SSR 初值写入 motion 模块 + html class;settings:changed 热更新。
*/
export default function MotionProvider({
initial,
children,
}: {
initial: MotionSiteConfig;
children: ReactNode;
}) {
useEffect(() => {
applyMotionSiteConfig(motionConfigFromSettings(initial));
}, [initial]);
useEffect(() => {
return realtime.on<RtSettingsData>(RT_SETTINGS_CHANGED, (data) => {
if (!data) return;
const cur = getMotionSiteConfig();
applyMotionSiteConfig(
motionConfigFromSettings({
ui_animations:
typeof data.ui_animations === "boolean"
? data.ui_animations
: cur.ui_animations,
anim_code_fold:
typeof data.anim_code_fold === "boolean"
? data.anim_code_fold
: cur.anim_code_fold,
anim_smooth_scroll:
typeof data.anim_smooth_scroll === "boolean"
? data.anim_smooth_scroll
: cur.anim_smooth_scroll,
anim_chrome:
typeof data.anim_chrome === "boolean" ? data.anim_chrome : cur.anim_chrome,
})
);
});
}, []);
return children;
}

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { scrollBehavior } from "@/lib/motion";
export interface TocItem {
level: 2 | 3;
@@ -44,13 +45,12 @@ export default function PostToc({ items }: { items: TocItem[] }) {
}
const el = navRef.current?.querySelector<HTMLElement>(`[data-toc-id="${active}"]`);
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
el.scrollIntoView({ block: "nearest", behavior: reduce ? "auto" : "smooth" });
el.scrollIntoView({ block: "nearest", behavior: scrollBehavior() });
}, [active]);
const jump = (id: string) => (e: React.MouseEvent) => {
e.preventDefault();
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
document.getElementById(id)?.scrollIntoView({ behavior: scrollBehavior(), block: "start" });
};
return (

View File

@@ -9,6 +9,7 @@ import {
type RtFeedChangedData,
} from "@/lib/realtime";
import type { FeedSort } from "@/components/home/FeedTabs";
import { scrollBehavior } from "@/lib/motion";
interface FeedNewTipProps {
sort: FeedSort;
@@ -52,7 +53,7 @@ export default function FeedNewTip({
const handleClick = () => {
setVisible(false);
router.refresh();
window.scrollTo({ top: 0, behavior: "smooth" });
window.scrollTo({ top: 0, behavior: scrollBehavior() });
};
return (

View File

@@ -11,6 +11,7 @@ import {
} from "react";
import { Check, Copy, ChevronDown, ChevronUp } from "lucide-react";
import { toast } from "@/lib/toast";
import { allowMotion } from "@/lib/motion";
import { useCodeFoldConfig } from "./CodeFoldProvider";
const FOLD_ANIM_MS = 360;
@@ -34,10 +35,7 @@ function measureCollapsedPx(host: HTMLElement, foldLines: number): number {
}
function prefersReducedMotion(): boolean {
return (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
return !allowMotion("codeFold");
}
type MdCodeBlockProps = HTMLAttributes<HTMLPreElement> & {