Revert "feat: 实现基础滚动位置恢复功能"

This reverts commit 4042395e3e.
This commit is contained in:
2026-08-08 02:11:10 +08:00
parent 4042395e3e
commit 1550ccb693
7 changed files with 0 additions and 521 deletions

View File

@@ -1,19 +0,0 @@
import { useEffect } from 'react';
import { restoreScrollPositions } from '../utils/scrollRestore';
/**
* 在布局组件挂载时恢复当前 URL 的滚动位置(仅刷新场景生效)。
*
* 使用 mount-only effect布局组件在 SPA 内导航时不会重新挂载,
* 因此此 effect 仅在页面实际重载(刷新)或跨布局切换时触发,
* 不会干扰 SPA 内导航的默认滚动行为。
*
* rAF 重试循环会等待异步内容加载完成后再设置 scrollTop
* 组件卸载时通过返回的取消函数终止重试。
*/
export function useScrollRestoration(): void {
useEffect(() => {
const cancel = restoreScrollPositions();
return cancel;
}, []);
}

View File

@@ -13,7 +13,6 @@ import BackToTop from '../components/BackToTop';
import { loginPath } from '../utils/authRedirect';
import { useSiteBranding } from '../hooks/useSiteBranding';
import { useNoIndexSEO } from '../hooks/usePageSEO';
import { useScrollRestoration } from '../hooks/useScrollRestoration';
import SiteBrandMark from '../components/SiteBrandMark';
import { api } from '../api/client';
@@ -81,7 +80,6 @@ export default function AdminLayout() {
const { theme, toggle } = useTheme();
const { branding } = useSiteBranding();
useNoIndexSEO('管理后台');
useScrollRestoration();
const isNarrow = useMediaQuery('(max-width: 768px)');
const [navOpen, setNavOpen] = useState(false);
const [pending, setPending] = useState<PendingCounts>({ posts: 0, comments: 0, reports: 0 });

View File

@@ -21,7 +21,6 @@ import Sidebar, { isNeutralSidebarRoute } from '../components/Sidebar';
import RightPanel from '../components/RightPanel';
import BackToTop from '../components/BackToTop';
import { useForumLimits } from '../hooks/useForumLimits';
import { useScrollRestoration } from '../hooks/useScrollRestoration';
import { buildHomeUrl, parseFeedSort } from '../components/FeedSortBar';
import { navigateFeed } from '../utils/feedCache';
import { notify } from '@/lib/notify';
@@ -75,7 +74,6 @@ export default function MainLayout() {
const [searchAdvanced, setSearchAdvanced] = useState(false);
const feedSort = parseFeedSort(params.get('sort'));
const { limits: forumLimits } = useForumLimits();
useScrollRestoration();
const asideDrawerRef = useRef<HTMLElement>(null);
const asideCloseRef = useRef<HTMLButtonElement>(null);

View File

@@ -1,11 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { applyTheme, getStoredTheme } from './utils/theme';
import { initScrollRestore } from './utils/scrollRestore';
import App from './App';
applyTheme(getStoredTheme());
initScrollRestore();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>

View File

@@ -1,126 +0,0 @@
/**
* 全局滚动位置恢复:基于 sessionStorage + pagehide。
*
* 背景本应用为固定视口100dvh + overflow:hiddenSPAwindow 不滚动,
* 浏览器原生 scrollRestoration 对内部滚动容器无效。
* 因此在 pagehide 时主动将各滚动容器的 scrollTop 存入 sessionStorage
* 页面重载后通过 rAF 重试机制在异步内容加载完成后恢复。
*
* 适用场景浏览器刷新F5 / Ctrl+R / Ctrl+F5后保持原阅读位置。
* 不干扰 SPA 内导航的默认滚动行为布局组件不重新挂载mount-only effect 不触发)。
*/
const STORAGE_PREFIX = 'j13-scroll:';
/** 候选滚动容器选择器(覆盖主站与后台各页面) */
const SCROLL_SELECTORS = [
'.main-content--feed-mobile-scroll', // 移动端首页 Feed 整栏滚动
'.post-list-scroll', // 桌面端首页 Feed 列表滚动
'.page-wrap', // 帖子详情 / 个人主页 / 消息等
'.admin-main', // 后台主内容区
] as const;
type SavedPositions = {
containers: Record<string, number>;
ts: number;
};
function storageKey(url: string): string {
return STORAGE_PREFIX + url;
}
function getCurrentUrl(): string {
return window.location.pathname + window.location.search;
}
/** 保存当前页面各滚动容器的位置到 sessionStorage */
export function saveScrollPositions(url: string = getCurrentUrl()): void {
try {
const containers: Record<string, number> = {};
for (const selector of SCROLL_SELECTORS) {
const el = document.querySelector<HTMLElement>(selector);
if (el && el.scrollTop > 0) {
containers[selector] = Math.round(el.scrollTop);
}
}
if (Object.keys(containers).length === 0) return;
const entry: SavedPositions = { containers, ts: Date.now() };
sessionStorage.setItem(storageKey(url), JSON.stringify(entry));
} catch {
// sessionStorage 不可用或已满,静默失败
}
}
type CancelFn = () => void;
/**
* 从 sessionStorage 读取并恢复指定 URL 的滚动位置。
* 使用 rAF 重试,等待异步内容加载完成后再设置 scrollTop。
* 返回取消函数,用于在组件卸载时终止重试循环。
*/
export function restoreScrollPositions(url: string = getCurrentUrl()): CancelFn {
let entry: SavedPositions | null = null;
try {
const raw = sessionStorage.getItem(storageKey(url));
if (raw) entry = JSON.parse(raw) as SavedPositions;
} catch {
return () => {};
}
if (!entry?.containers || Object.keys(entry.containers).length === 0) {
return () => {};
}
let cancelled = false;
let rafId = 0;
const deadline = performance.now() + 4000;
const pending = Object.entries(entry.containers);
const done = new Set<string>();
const tryRestore = (selector: string, target: number): boolean => {
const el = document.querySelector<HTMLElement>(selector);
if (!el) return false;
// 内容尚未加载到足以滚动到目标位置,等待重试
const maxScroll = el.scrollHeight - el.clientHeight;
if (maxScroll < target - 1) return false;
el.scrollTop = target;
return Math.abs(el.scrollTop - target) <= 1;
};
const tick = () => {
if (cancelled) return;
for (const [selector, target] of pending) {
if (done.has(selector)) continue;
if (tryRestore(selector, target)) {
done.add(selector);
}
}
if (done.size === pending.length || performance.now() > deadline) {
// 全部恢复完成或超时,清除存储条目(避免跨布局切换返回时错误恢复)
try {
sessionStorage.removeItem(storageKey(url));
} catch {
// ignore
}
return;
}
rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
return () => {
cancelled = true;
cancelAnimationFrame(rafId);
};
}
let initialized = false;
/** 注册全局 pagehide 监听器(在应用入口调用一次) */
export function initScrollRestore(): void {
if (typeof window === 'undefined' || initialized) return;
initialized = true;
window.addEventListener('pagehide', () => {
saveScrollPositions();
});
}