Skip to content

Commit 00f441c

Browse files
committed
refactor(scroll): remove KeepAlive mode and simplify restore logic
- Remove KeepAlive wrapping from AppLayout - Replace ResizeObserver with requestAnimationFrame loop - Remove useActivate/useUnactivate lifecycle hooks - Simplify useScrollRestore options (remove useKeepAlive, stabilityDelay) - Add 3xl/4xl responsive breakpoints for ultra-wide screens - Adjust Cards grid container structure
1 parent b0f315f commit 00f441c

8 files changed

Lines changed: 55 additions & 1089 deletions

File tree

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
"pathe": "^2.0.3",
5353
"pinyin-pro": "^3.28.1",
5454
"react": "^18.3.1",
55-
"react-activation": "^0.13.4",
5655
"react-dom": "^18.3.1",
5756
"react-i18next": "^17.0.4",
5857
"react-router-dom": "^7.14.2",

pnpm-lock.yaml

Lines changed: 3 additions & 941 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import "./App.css";
22
import "@/utils/i18n";
33
import { SnackbarProvider } from "notistack";
4-
import { AliveScope } from "react-activation";
54
import { useTranslation } from "react-i18next";
65
import { Outlet } from "react-router-dom";
76
import WindowsHandler from "@/components/Windows";
@@ -31,9 +30,7 @@ const App: React.FC = () => {
3130
<SnackbarUtilsConfigurator />
3231
<ToolpadReactRouterAppProvider navigation={Navigation}>
3332
<WindowsHandler />
34-
<AliveScope>
35-
<Outlet />
36-
</AliveScope>
33+
<Outlet />
3734
</ToolpadReactRouterAppProvider>
3835
</SnackbarProvider>
3936
);

src/components/AppLayout.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Tooltip from "@mui/material/Tooltip";
99
import Typography from "@mui/material/Typography";
1010
import { DashboardLayout } from "@toolpad/core/DashboardLayout";
1111
import { PageContainer } from "@toolpad/core/PageContainer";
12-
import { KeepAlive } from "react-activation";
1312
import { useTranslation } from "react-i18next";
1413
import { Outlet, useLocation, useNavigate } from "react-router-dom";
1514
import AddModal from "@/components/AddModal";
@@ -153,13 +152,7 @@ export const Layout: React.FC = () => {
153152
},
154153
}}
155154
>
156-
<KeepAlive
157-
name="libraries"
158-
cacheKey="libraries"
159-
saveScrollPosition={false}
160-
>
161-
<Outlet />
162-
</KeepAlive>
155+
<Outlet />
163156
</PageContainer>
164157
) : (
165158
<Outlet />

src/components/Cards.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ export const CardItem = memo(
368368
return (
369369
<Card
370370
ref={ref}
371-
className={`group relative min-w-24 max-w-full transition-transform [content-visibility:auto] [contain-intrinsic-size:auto_280px] ${isActive ? "scale-y-105" : "scale-y-100"}`}
371+
className={`group relative min-w-24 max-w-full transition-transform ${isActive ? "scale-y-105" : "scale-y-100"}`}
372372
onContextMenu={onContextMenu}
373373
{...props}
374374
>
@@ -754,11 +754,7 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
754754
onRemoveFromCategory={handleRemoveFromCategory}
755755
/>
756756
)}
757-
<div
758-
className={
759-
"text-center grid lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8 gap-4"
760-
}
761-
>
757+
<div className="flex-1 min-h-0">
762758
<RightMenu
763759
id={menuPosition?.cardId}
764760
isopen={Boolean(menuPosition)}
@@ -772,14 +768,20 @@ const Cards: React.FC<CardsProps> = ({ gamesData, categoryId }) => {
772768
}}
773769
/>
774770

775-
{games.map((card) => {
776-
const props = getCardProps(card);
777-
return isSortable ? (
778-
<SortableCardItem key={card.id} {...props} />
779-
) : (
780-
<CardItem key={card.id} {...props} />
781-
);
782-
})}
771+
<div
772+
className={
773+
"text-center grid lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8 3xl:grid-cols-10 4xl:grid-cols-12 gap-4"
774+
}
775+
>
776+
{games.map((card) => {
777+
const props = getCardProps(card);
778+
return isSortable ? (
779+
<SortableCardItem key={card.id} {...props} />
780+
) : (
781+
<CardItem key={card.id} {...props} />
782+
);
783+
})}
784+
</div>
783785
</div>
784786
</>
785787
);
Lines changed: 21 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useCallback, useEffect, useRef } from "react";
2-
import { useActivate, useUnactivate } from "react-activation";
32
import { useLocation } from "react-router-dom";
43

54
const scrollPositions: Record<string, number> = {};
@@ -21,41 +20,23 @@ interface UseScrollRestoreOptions {
2120
timeout?: number;
2221
/** 是否启用调试日志 */
2322
debug?: boolean;
24-
/** 内容稳定检测的等待时间(ms),默认 150 */
25-
stabilityDelay?: number;
26-
/** 是否在 KeepAlive 中使用 */
27-
useKeepAlive?: boolean;
2823
}
2924

3025
const DEFAULT_OPTIONS: Required<Omit<UseScrollRestoreOptions, "isLoading">> = {
3126
containerSelector: "main",
3227
timeout: 1500,
3328
debug: false,
34-
stabilityDelay: 0,
35-
useKeepAlive: false,
3629
};
3730

3831
/**
39-
* 滚动位置还原 Hook (优化版)
40-
*
41-
* 特性:
42-
* - 智能检测内容是否渲染完成(高度稳定检测)
43-
* - 支持滚动到底部的场景
44-
* - 避免滚动抖动和跳跃
45-
* - 自动清理资源,防止内存泄漏
32+
* 滚动位置还原 Hook
33+
* 等页面数据加载完成后,用 requestAnimationFrame 等待内容高度足够再恢复。
4634
*/
4735
export function useScrollRestore(
4836
scrollPath: string,
4937
options: UseScrollRestoreOptions = {},
5038
) {
51-
const {
52-
containerSelector,
53-
isLoading,
54-
timeout,
55-
debug,
56-
stabilityDelay,
57-
useKeepAlive,
58-
} = {
39+
const { containerSelector, isLoading, timeout, debug } = {
5940
...DEFAULT_OPTIONS,
6041
...options,
6142
};
@@ -65,8 +46,6 @@ export function useScrollRestore(
6546
const cleanupRef = useRef<(() => void) | null>(null);
6647
const settledRef = useRef(false);
6748
const lastPathRef = useRef<string>("");
68-
const lastHeightRef = useRef(0);
69-
const stabilityTimerRef = useRef<number | null>(null);
7049

7150
const log = useCallback(
7251
(...args: Parameters<Console["log"]>) => {
@@ -87,7 +66,6 @@ export function useScrollRestore(
8766
if (lastPathRef.current !== location.pathname) {
8867
settledRef.current = false;
8968
lastPathRef.current = location.pathname;
90-
lastHeightRef.current = 0;
9169
}
9270

9371
// 清理上一次的副作用
@@ -126,28 +104,26 @@ export function useScrollRestore(
126104
return;
127105
}
128106

129-
let ro: ResizeObserver | null = null;
107+
let frameId: number | null = null;
130108
let fallbackTimer: number | null = null;
109+
let cancelled = false;
131110

132111
// 清理函数(先定义,避免在 performRestore 中引用未定义的变量)
133112
const cleanup = () => {
134-
if (ro) {
135-
ro.disconnect();
136-
ro = null;
113+
cancelled = true;
114+
if (frameId !== null) {
115+
window.cancelAnimationFrame(frameId);
116+
frameId = null;
137117
}
138118
if (fallbackTimer !== null) {
139119
window.clearTimeout(fallbackTimer);
140120
fallbackTimer = null;
141121
}
142-
if (stabilityTimerRef.current !== null) {
143-
window.clearTimeout(stabilityTimerRef.current);
144-
stabilityTimerRef.current = null;
145-
}
146122
};
147123

148124
// 执行滚动恢复
149125
const performRestore = (reason: string) => {
150-
if (settledRef.current) return;
126+
if (settledRef.current || cancelled) return;
151127

152128
const maxScroll = Math.max(
153129
0,
@@ -172,67 +148,24 @@ export function useScrollRestore(
172148
cleanup();
173149
};
174150

175-
// 检查内容高度是否稳定
176-
const checkStability = () => {
177-
const currentHeight = container.scrollHeight;
178-
const maxScroll = currentHeight - container.clientHeight;
151+
const tryRestore = () => {
152+
if (settledRef.current || cancelled) return;
179153

180-
log("Height check:", {
181-
current: currentHeight,
182-
last: lastHeightRef.current,
183-
maxScroll,
184-
target,
185-
});
154+
const maxScroll = Math.max(
155+
0,
156+
container.scrollHeight - container.clientHeight,
157+
);
186158

187-
// 情况1: 内容已经足够高,可以直接恢复
188159
if (maxScroll >= target) {
189160
performRestore("content sufficient");
190161
return;
191162
}
192163

193-
// 情况2: 高度稳定(不再增长)
194-
if (
195-
lastHeightRef.current > 0 &&
196-
currentHeight === lastHeightRef.current
197-
) {
198-
// 高度不再变化,说明内容已渲染完成
199-
// 即使 maxScroll < target,也恢复到最大可滚动位置
200-
performRestore("content stable");
201-
return;
202-
}
203-
204-
// 更新上次高度
205-
lastHeightRef.current = currentHeight;
206-
207-
// 清除旧的稳定性计时器
208-
if (stabilityTimerRef.current !== null) {
209-
window.clearTimeout(stabilityTimerRef.current);
210-
}
211-
212-
// 设置新的稳定性计时器
213-
// 如果在 stabilityDelay 时间内高度没有变化,认为内容已稳定
214-
stabilityTimerRef.current = window.setTimeout(() => {
215-
if (!settledRef.current) {
216-
checkStability();
217-
}
218-
}, stabilityDelay);
164+
log("Content not high enough yet:", { maxScroll, target });
165+
frameId = window.requestAnimationFrame(tryRestore);
219166
};
220167

221-
// 立即检查一次
222-
checkStability();
223-
224-
// 使用 ResizeObserver 监听容器尺寸变化
225-
try {
226-
ro = new ResizeObserver(() => {
227-
if (!settledRef.current) {
228-
checkStability();
229-
}
230-
});
231-
ro.observe(container);
232-
log("ResizeObserver attached");
233-
} catch (err) {
234-
log("ResizeObserver not available", err);
235-
}
168+
frameId = window.requestAnimationFrame(tryRestore);
236169

237170
// 超时保护
238171
fallbackTimer = window.setTimeout(() => {
@@ -250,41 +183,11 @@ export function useScrollRestore(
250183
isLoading,
251184
containerSelector,
252185
timeout,
253-
stabilityDelay,
254186
log,
255187
]);
256188

257189
// 普通模式:使用 useEffect
258190
useEffect(() => {
259-
if (!useKeepAlive) {
260-
performScrollRestore();
261-
}
262-
}, [useKeepAlive, performScrollRestore]);
263-
264-
// KeepAlive 模式:使用 useActivate
265-
useActivate(() => {
266-
if (useKeepAlive) {
267-
log("[KeepAlive] 组件激活,触发滚动恢复");
268-
// 重置状态,因为可能是从其他页面返回
269-
settledRef.current = false;
270-
lastHeightRef.current = 0;
271-
performScrollRestore();
272-
}
273-
});
274-
275-
// KeepAlive 失活时清理
276-
useUnactivate(() => {
277-
if (!useKeepAlive) return;
278-
279-
if (cleanupRef.current) {
280-
cleanupRef.current();
281-
cleanupRef.current = null;
282-
}
283-
284-
// KeepAlive 页面失活时将共享滚动容器归零,避免下一页继承旧滚动位置。
285-
const container = document.querySelector<HTMLElement>(containerSelector);
286-
if (container) {
287-
container.scrollTop = 0;
288-
}
289-
});
191+
performScrollRestore();
192+
}, [performScrollRestore]);
290193
}

src/pages/LibrariesPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useScrollRestore } from "@/hooks/common/useScrollRestore";
33
import { useGameListFacade } from "@/hooks/features/games/useGameListFacade";
44

55
export const Libraries: React.FC = () => {
6-
useScrollRestore("/libraries", { useKeepAlive: true });
7-
const { games } = useGameListFacade();
6+
const { games, isLoading } = useGameListFacade();
7+
useScrollRestore("/libraries", { isLoading });
88
return <Cards gamesData={games} />;
99
};

uno.config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { defineConfig } from "unocss";
22

33
export default defineConfig({
4-
// ...UnoCSS options
4+
theme: {
5+
breakpoints: {
6+
sm: "640px",
7+
md: "768px",
8+
lg: "1024px",
9+
xl: "1280px",
10+
"2xl": "1536px",
11+
"3xl": "1920px",
12+
"4xl": "2560px",
13+
},
14+
},
515
});

0 commit comments

Comments
 (0)