This commit is contained in:
truewhile
2026-09-15 22:05:34 +08:00
parent ff86c2f597
commit a9b3110b2c
5 changed files with 264 additions and 65 deletions
@@ -0,0 +1,62 @@
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import {
forgetListPosition,
normalizeListPosition,
readListPosition,
useRememberedListPosition,
writeListPosition,
} from './useListPositionMemory.ts'
function check(name: string, condition: boolean) {
if (!condition) throw new Error(`useListPositionMemory: ${name}`)
}
check('unknown key falls back', readListPosition('unknown-key', 3) === 3)
check('missing value uses default fallback', readListPosition('unknown-key') === 1)
writeListPosition('demo', 4)
check('round trip keeps value', readListPosition('demo') === 4)
writeListPosition('demo', 9)
check('rewrite keeps latest value', readListPosition('demo') === 9)
forgetListPosition('demo')
check('forget resets to fallback', readListPosition('demo', 2) === 2)
check('normalize parses numeric string', normalizeListPosition('7', 1) === 7)
check('normalize floors decimals', normalizeListPosition(3.8, 1) === 3)
check('normalize rejects zero', normalizeListPosition(0, 2) === 2)
check('normalize rejects negatives', normalizeListPosition(-5, 2) === 2)
check('normalize rejects NaN input', normalizeListPosition('abc', 2) === 2)
check('normalize rejects infinity', normalizeListPosition(Number.POSITIVE_INFINITY, 2) === 2)
check('normalize clamps to max', normalizeListPosition(99, 1, 8) === 8)
check('normalize keeps value under max', normalizeListPosition(6, 1, 8) === 6)
// 详情页返回时列表组件会重新挂载:挂载即可读到上次页码,而不是先回第一页。
function PageProbe({ positionKey, max }: { positionKey: string; max?: number }) {
const [position] = useRememberedListPosition(positionKey, 1, max)
return createElement('span', null, String(position))
}
writeListPosition('grid:/libraries', 3)
check(
'remount restores remembered page',
renderToStaticMarkup(createElement(PageProbe, { positionKey: 'grid:/libraries' })) === '<span>3</span>',
)
check(
'remount restores independent key',
renderToStaticMarkup(createElement(PageProbe, { positionKey: 'grid:/' })) === '<span>1</span>',
)
writeListPosition('shelves', 40)
check(
'remount clamps to restore cap',
renderToStaticMarkup(createElement(PageProbe, { positionKey: 'shelves', max: 15 })) === '<span>15</span>',
)
check(
'live state can grow past restore cap',
readListPosition('shelves') === 40,
)
console.log('useListPositionMemory.test.ts ok')
+87
View File
@@ -0,0 +1,87 @@
import { useCallback, useEffect, useState } from 'react'
// 列表分页位置(媒体库入口当前页、媒体库已加载页数、货架解锁数量)需要在
// 进入详情页再返回后依然生效,所以用 sessionStorage 记录,并在内存里留一份
// 兜底缓存;同一标签页内刷新也仍然记得。
const LIST_POSITION_PREFIX = 'mebox.listpos.'
const positions = new Map<string, number>()
/**
* 详情页返回时最多自动补拉的分页数量:滚动到很深的用户重新进入列表时
* 依次补拉这些页,剩下的继续由底部哨兵按需加载,避免一次打太多请求。
*/
export const MAX_RESTORE_PAGES = 8
/** 把存储里的任意值收敛成 >= 1 的整数分页位置。 */
export function normalizeListPosition(
value: unknown,
fallback = 1,
max = Number.MAX_SAFE_INTEGER,
): number {
const numeric = typeof value === 'number' ? value : Number(value)
if (!Number.isFinite(numeric)) return fallback
const rounded = Math.floor(numeric)
if (rounded < 1) return fallback
return Math.min(rounded, max)
}
export function readListPosition(key: string, fallback = 1, max = Number.MAX_SAFE_INTEGER): number {
const cached = positions.get(key)
if (cached !== undefined) return Math.min(cached, max)
if (typeof window === 'undefined') return fallback
try {
const raw = window.sessionStorage.getItem(LIST_POSITION_PREFIX + key)
if (raw === null) return fallback
const value = normalizeListPosition(raw, fallback, max)
positions.set(key, value)
return value
} catch {
return fallback
}
}
export function writeListPosition(key: string, value: number): void {
const next = normalizeListPosition(value)
positions.set(key, next)
if (typeof window === 'undefined') return
try {
window.sessionStorage.setItem(LIST_POSITION_PREFIX + key, String(next))
} catch {
// 私密模式等场景忽略存储失败,不影响分页。
}
}
export function forgetListPosition(key: string): void {
positions.delete(key)
if (typeof window === 'undefined') return
try {
window.sessionStorage.removeItem(LIST_POSITION_PREFIX + key)
} catch {
// 同上,存储不可用时无需处理。
}
}
/**
* 记住列表当前分页位置。key 需要在一份挂载生命周期内保持稳定:路由切换会
* 让列表重新挂载,因此每次挂载都能直接读到上次记录的位置(不会先闪回第一页)。
*/
export function useRememberedListPosition(
key: string,
fallback = 1,
max = Number.MAX_SAFE_INTEGER,
): [number, (next: number | ((prev: number) => number)) => void] {
const [position, setPosition] = useState(() => readListPosition(key, fallback, max))
useEffect(() => {
writeListPosition(key, position)
}, [key, position])
const remember = useCallback((next: number | ((prev: number) => number)) => {
setPosition((prev) => {
const value = typeof next === 'function' ? next(prev) : next
return normalizeListPosition(value, prev)
})
}, [])
return [position, remember]
}