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 Silk from "@/components/silk/Silk";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import { Suspense } from "react";
|
import { Suspense, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
const modeColors: Record<string, string> = {
|
const modeColors: Record<string, string> = {
|
||||||
dev: "#F59E0B",
|
dev: "#F59E0B",
|
||||||
@@ -73,8 +73,26 @@ export function VersionCard({
|
|||||||
if (clickable) navigate("update");
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={cardRef}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative overflow-hidden rounded-xl border border-white/10 min-h-[200px]",
|
"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",
|
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 }} />
|
<div className="absolute inset-0" style={{ background: gradient }} />
|
||||||
|
|
||||||
{/* Silk 动画层 (非 simple 模式) */}
|
{/* Silk 动画层 (非 simple 模式;仅卡片可见时挂载,隐藏即暂停) */}
|
||||||
{!simple && (
|
{!simple && silkVisible && (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<div className="absolute inset-0 opacity-60 mix-blend-soft-light">
|
<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} />
|
<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() {
|
export function Setting() {
|
||||||
const [selected, setSelected] = useState("home");
|
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 routeNavigate = useRouteStore((s) => s.navigate);
|
||||||
|
|
||||||
const switchPage = useCallback((key: string) => {
|
const switchPage = useCallback((key: string) => {
|
||||||
if (key === selected) return;
|
setSelected((prev) => {
|
||||||
setSelected(key);
|
if (prev === key) return prev;
|
||||||
setAnimKey((k) => k + 1);
|
setVisited((v) => ({ ...v, [key]: true }));
|
||||||
}, [selected]);
|
// 两步重放进入动画:先移除类再回加(CSS 动画重新触发,页面无需重挂载)
|
||||||
|
setAnimOn(false);
|
||||||
|
requestAnimationFrame(() => setAnimOn(true));
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleItemClick = useCallback((item: MenuItem) => {
|
const handleItemClick = useCallback((item: MenuItem) => {
|
||||||
if (item.onSelect) {
|
if (item.onSelect) {
|
||||||
@@ -129,7 +137,7 @@ export function Setting() {
|
|||||||
|
|
||||||
const menuData = buildMenuData(switchPage, routeNavigate);
|
const menuData = buildMenuData(switchPage, routeNavigate);
|
||||||
const allItems = menuData.flatMap((g) => g.items);
|
const allItems = menuData.flatMap((g) => g.items);
|
||||||
const current = allItems.find((i) => i.key === selected);
|
const cachedItems = allItems.filter((i) => visited[i.key]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
@@ -172,11 +180,16 @@ export function Setting() {
|
|||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 内容区 */}
|
{/* 内容区(已访问页缓存保活;仅活动页可见并重放进入动画) */}
|
||||||
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
|
<main className="scroll-area flex-1 h-full overflow-y-auto p-8">
|
||||||
<div key={animKey} className="setting-page-enter">
|
{cachedItems.map((item) => {
|
||||||
{current?.component}
|
const active = selected === item.key;
|
||||||
</div>
|
return (
|
||||||
|
<div key={item.key} className={active ? (animOn ? "setting-page-enter" : undefined) : "hidden"}>
|
||||||
|
{item.component}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user