mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 11:46:36 +08:00
优化,添加滚动记忆
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { shouldRememberScroll } from './useScrollMemory.ts'
|
||||
|
||||
function check(name: string, condition: boolean) {
|
||||
if (!condition) throw new Error(`useScrollMemory: ${name}`)
|
||||
}
|
||||
|
||||
check('home remembers scroll', shouldRememberScroll('/'))
|
||||
check('library list remembers scroll', shouldRememberScroll('/libraries'))
|
||||
check('library detail remembers scroll', shouldRememberScroll('/library/lib-1'))
|
||||
check('media detail remembers scroll', shouldRememberScroll('/media/media-1'))
|
||||
check('settings does not remember scroll', !shouldRememberScroll('/settings'))
|
||||
check('player does not remember scroll', !shouldRememberScroll('/play/media-1'))
|
||||
|
||||
console.log('useScrollMemory.test.ts ok')
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useLayoutEffect } from 'react'
|
||||
|
||||
const SCROLL_STORAGE_PREFIX = 'mebox.scroll.'
|
||||
const scrollPositions = new Map<string, number>()
|
||||
|
||||
function readScrollPosition(key: string): number {
|
||||
const cached = scrollPositions.get(key)
|
||||
if (cached !== undefined) return cached
|
||||
if (typeof window === 'undefined') return 0
|
||||
try {
|
||||
const value = Number(window.sessionStorage.getItem(SCROLL_STORAGE_PREFIX + key))
|
||||
if (Number.isFinite(value) && value >= 0) {
|
||||
scrollPositions.set(key, value)
|
||||
return value
|
||||
}
|
||||
} catch {
|
||||
// sessionStorage 不可用时仍可使用内存缓存。
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function writeScrollPosition(key: string, value: number): void {
|
||||
const next = Math.max(0, Math.round(value))
|
||||
scrollPositions.set(key, next)
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
window.sessionStorage.setItem(SCROLL_STORAGE_PREFIX + key, String(next))
|
||||
} catch {
|
||||
// 私密模式等场景忽略存储失败,不影响页面滚动。
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldRememberScroll(pathname: string): boolean {
|
||||
return (
|
||||
pathname === '/' ||
|
||||
pathname === '/libraries' ||
|
||||
pathname.startsWith('/library/') ||
|
||||
pathname.startsWith('/media/')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住列表页的滚动位置。页面内容会异步长高,因此恢复期间会监听内容高度,
|
||||
* 直到目标位置可达;期间用户主动滚动会立即接管,避免和恢复逻辑抢滚动条。
|
||||
*/
|
||||
export function useScrollMemory(pathname: string, userKey = 'anonymous'): void {
|
||||
useLayoutEffect(() => {
|
||||
const el = document.getElementById('app-main-scroll')
|
||||
if (!el) return
|
||||
|
||||
if (!shouldRememberScroll(pathname)) {
|
||||
el.scrollTop = 0
|
||||
return
|
||||
}
|
||||
|
||||
const key = `${userKey}:${pathname}`
|
||||
const saved = readScrollPosition(key)
|
||||
let restoring = saved > 0
|
||||
let observer: ResizeObserver | null = null
|
||||
let restoreTimer: number | null = null
|
||||
let restoreFrame = 0
|
||||
let restorePumpUntil = 0
|
||||
let lastSaved = saved
|
||||
|
||||
const stopRestore = () => {
|
||||
if (restoreFrame) {
|
||||
window.cancelAnimationFrame(restoreFrame)
|
||||
restoreFrame = 0
|
||||
}
|
||||
if (restoreTimer != null) {
|
||||
window.clearTimeout(restoreTimer)
|
||||
restoreTimer = null
|
||||
}
|
||||
observer?.disconnect()
|
||||
observer = null
|
||||
}
|
||||
|
||||
const finishRestore = () => {
|
||||
if (!restoring) return
|
||||
restoring = false
|
||||
stopRestore()
|
||||
lastSaved = Math.round(el.scrollTop)
|
||||
writeScrollPosition(key, lastSaved)
|
||||
}
|
||||
|
||||
const tryRestore = () => {
|
||||
if (!restoring) return
|
||||
const max = Math.max(0, el.scrollHeight - el.clientHeight)
|
||||
const target = Math.min(saved, max)
|
||||
if (target > 0 && Math.abs(el.scrollTop - target) > 1) {
|
||||
el.scrollTop = target
|
||||
}
|
||||
if (target >= saved - 1) {
|
||||
finishRestore()
|
||||
}
|
||||
}
|
||||
|
||||
const pumpRestore = () => {
|
||||
if (!restoring) return
|
||||
tryRestore()
|
||||
if (restoring && performance.now() < restorePumpUntil) {
|
||||
restoreFrame = window.requestAnimationFrame(pumpRestore)
|
||||
} else {
|
||||
restoreFrame = 0
|
||||
}
|
||||
}
|
||||
|
||||
const saveNow = () => {
|
||||
if (restoring) return
|
||||
const current = Math.round(el.scrollTop)
|
||||
if (current === lastSaved) return
|
||||
lastSaved = current
|
||||
writeScrollPosition(key, current)
|
||||
}
|
||||
|
||||
const cancelRestore = () => {
|
||||
if (!restoring) return
|
||||
restoring = false
|
||||
stopRestore()
|
||||
lastSaved = Math.round(el.scrollTop)
|
||||
writeScrollPosition(key, lastSaved)
|
||||
}
|
||||
|
||||
if (saved <= 0) {
|
||||
el.scrollTop = 0
|
||||
} else {
|
||||
tryRestore()
|
||||
if (restoring) {
|
||||
const content = el.firstElementChild
|
||||
if (typeof ResizeObserver !== 'undefined' && content instanceof HTMLElement) {
|
||||
observer = new ResizeObserver(() => tryRestore())
|
||||
observer.observe(content)
|
||||
}
|
||||
restorePumpUntil = performance.now() + 1000
|
||||
restoreFrame = window.requestAnimationFrame(pumpRestore)
|
||||
restoreTimer = window.setTimeout(finishRestore, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
if (event.target === el) {
|
||||
cancelRestore()
|
||||
}
|
||||
saveNow()
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === 'PageDown' ||
|
||||
event.key === 'PageUp' ||
|
||||
event.key === 'Home' ||
|
||||
event.key === 'End' ||
|
||||
event.key === 'ArrowDown' ||
|
||||
event.key === 'ArrowUp' ||
|
||||
event.key === ' '
|
||||
) {
|
||||
cancelRestore()
|
||||
}
|
||||
saveNow()
|
||||
}
|
||||
|
||||
el.addEventListener('scroll', saveNow, { passive: true })
|
||||
el.addEventListener('pointerdown', onPointerDown, true)
|
||||
window.addEventListener('wheel', cancelRestore, { passive: true, capture: true })
|
||||
window.addEventListener('touchstart', cancelRestore, { passive: true, capture: true })
|
||||
window.addEventListener('keydown', onKeyDown, true)
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('scroll', saveNow)
|
||||
el.removeEventListener('pointerdown', onPointerDown, true)
|
||||
window.removeEventListener('wheel', cancelRestore, true)
|
||||
window.removeEventListener('touchstart', cancelRestore, true)
|
||||
window.removeEventListener('keydown', onKeyDown, true)
|
||||
stopRestore()
|
||||
}
|
||||
}, [pathname, userKey])
|
||||
}
|
||||
Reference in New Issue
Block a user