feat(settings, version-card): 新增页面保活缓存并暂停离屏动画

为VersionCard添加IntersectionObserver,仅在卡片可见时运行Silk动画以节省GPU资源
在设置页实现页面保活缓存,保留已访问的子页组件优化切换流畅度
This commit is contained in:
2026-09-05 20:57:29 +08:00
parent 4acdad6535
commit aef49b173f
3 changed files with 45 additions and 14 deletions
File diff suppressed because one or more lines are too long
+21 -3
View File
@@ -6,7 +6,7 @@ import { useRouteStore } from "@/stores/routeStore";
import Silk from "@/components/silk/Silk";
import clsx from "clsx";
import { ChevronRight } from "lucide-react";
import { Suspense } from "react";
import { Suspense, useEffect, useRef, useState } from "react";
const modeColors: Record<string, string> = {
dev: "#F59E0B",
@@ -73,8 +73,26 @@ export function VersionCard({
if (clickable) navigate("update");
};
// Silk(WebGL 动画)仅在卡片可见时运行:
// 设置子页 keep-alive 后隐藏页仍挂在 DOM,若动画照跑会白白占 GPU/rAF →
// 用 IntersectionObserver 按可见性挂载/卸载 Silk(隐藏/滚出视野即暂停,观感不变)
const cardRef = useRef<HTMLDivElement | null>(null);
const [silkVisible, setSilkVisible] = useState(true);
useEffect(() => {
if (simple || !cardRef.current || typeof IntersectionObserver === "undefined") return;
const io = new IntersectionObserver(
(entries) => {
for (const en of entries) setSilkVisible(en.isIntersecting);
},
{ root: null, threshold: 0.02 },
);
io.observe(cardRef.current);
return () => io.disconnect();
}, [simple]);
return (
<div
ref={cardRef}
className={clsx(
"relative overflow-hidden rounded-xl border border-white/10 min-h-[200px]",
clickable && "cursor-pointer hover:scale-[1.01] active:scale-[0.99] transition-transform",
@@ -93,8 +111,8 @@ export function VersionCard({
{/* 背景层 */}
<div className="absolute inset-0" style={{ background: gradient }} />
{/* Silk 动画层 (非 simple 模式) */}
{!simple && (
{/* Silk 动画层 (非 simple 模式;仅卡片可见时挂载,隐藏即暂停) */}
{!simple && silkVisible && (
<Suspense fallback={null}>
<div className="absolute inset-0 opacity-60 mix-blend-soft-light">
<Silk speed={3} scale={1.2} color={color} noiseIntensity={1.2} rotation={0.3} />
+23 -10
View File
@@ -110,14 +110,22 @@ function buildMenuData(
export function Setting() {
const [selected, setSelected] = useState("home");
const [animKey, setAnimKey] = useState(0);
const [animOn, setAnimOn] = useState(true);
// 已访问过的子页缓存(keep-alive):切换时不再全量卸载/重挂载,
// 避免每个子页的 VersionCard/Silk、发布说明请求等重活反复执行;隐藏页不卸载。
const [visited, setVisited] = useState<Record<string, boolean>>({ home: true });
const routeNavigate = useRouteStore((s) => s.navigate);
const switchPage = useCallback((key: string) => {
if (key === selected) return;
setSelected(key);
setAnimKey((k) => k + 1);
}, [selected]);
setSelected((prev) => {
if (prev === key) return prev;
setVisited((v) => ({ ...v, [key]: true }));
// 两步重放进入动画:先移除类再回加(CSS 动画重新触发,页面无需重挂载)
setAnimOn(false);
requestAnimationFrame(() => setAnimOn(true));
return key;
});
}, []);
const handleItemClick = useCallback((item: MenuItem) => {
if (item.onSelect) {
@@ -129,7 +137,7 @@ export function Setting() {
const menuData = buildMenuData(switchPage, routeNavigate);
const allItems = menuData.flatMap((g) => g.items);
const current = allItems.find((i) => i.key === selected);
const cachedItems = allItems.filter((i) => visited[i.key]);
return (
<div className="flex h-full">
@@ -172,11 +180,16 @@ export function Setting() {
</nav>
</aside>
{/* 内容区 */}
{/* 内容区(已访问页缓存保活;仅活动页可见并重放进入动画) */}
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
<div key={animKey} className="setting-page-enter">
{current?.component}
</div>
{cachedItems.map((item) => {
const active = selected === item.key;
return (
<div key={item.key} className={active ? (animOn ? "setting-page-enter" : undefined) : "hidden"}>
{item.component}
</div>
);
})}
</main>
</div>
);