mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat(settings, version-card): 新增页面保活缓存并暂停离屏动画
为VersionCard添加IntersectionObserver,仅在卡片可见时运行Silk动画以节省GPU资源 在设置页实现页面保活缓存,保留已访问的子页组件优化切换流畅度
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user