fix(frontend): 分页失败回滚到实际显示页
records 只在请求成功时更新,而分页器高亮绑的是 pageNum,失败不回滚就会 出现「高亮第 1 页、列表是第 2 页的数据」的永久错位,用户来回点才恢复。 - 用 shownPageNum / shownPageSize 记录实际显示的页,失败时 restoreShownPage 退回该页 - 回滚本身也要防竞态:只有本次请求仍是最新请求(seq === requestSeq)才回滚, 否则连点时旧请求失败会把用户刚点出的新页码覆盖掉,表现为页码自己跳回去 - 页码与条数在 await 之前取快照,避免等待期间被别的点击改动后记错「显示的页」 对外接口不变(load / search / onPageChange / onSizeChange / reloadAfterDelete), 现有调用方无需改动。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
a00fca942d
commit
678db0499d
@ -28,42 +28,101 @@ export function usePagedList<T, F extends object, D extends PageData<T> = PageDa
|
||||
/** 自增请求序号:仅最新请求的响应可写入状态 */
|
||||
let requestSeq = 0
|
||||
|
||||
/** 按当前页码与筛选条件加载 */
|
||||
async function load(): Promise<void> {
|
||||
/**
|
||||
* 列表当前**实际显示**的是哪一页(成功写入 records 时同步)。
|
||||
*
|
||||
* 这是「分页器高亮」与「列表内容」保持一致的唯一依据:失败回滚要退回
|
||||
* 这个页码,而不是退回点击前的页码 —— 连点两次时前一次请求的响应会被
|
||||
* 序号守卫丢弃,退回「点击前」的页码可能与屏幕上的数据对不上。
|
||||
*/
|
||||
let shownPageNum = 1
|
||||
let shownPageSize = 10
|
||||
|
||||
/**
|
||||
* 按当前页码与筛选条件加载,返回是否成功以及本次请求的序号。
|
||||
*
|
||||
* 返回值是翻页失败回滚的依据:`records` 只在成功时更新,而分页器
|
||||
* 高亮绑的是 `pageNum`,两者必须同进同退,否则会出现
|
||||
* 「页码已跳到第 2 页、列表还停在第 1 页」的错位(用户来回点才恢复)。
|
||||
*
|
||||
* 带出 `seq` 是因为**回滚本身也要防竞态**:连点两次时旧请求失败,
|
||||
* 若无条件把页码退回它那一轮的旧值,就会把用户刚点出的新页码覆盖掉,
|
||||
* 表现为「页码自己跳回去」。
|
||||
*
|
||||
* 页码与条数在 await **之前**取快照:等待期间另一次点击可能已改动
|
||||
* `pageNum`,用 await 之后的值记录「显示的页」会记错。
|
||||
*
|
||||
* 对外不暴露返回值:`load` 被大量页面当 `() => void | Promise<void>`
|
||||
* 的回调使用,多一个返回值会让那些赋值类型不兼容。
|
||||
*/
|
||||
async function fetchPage(): Promise<{ ok: boolean; seq: number }> {
|
||||
const seq = ++requestSeq
|
||||
const reqPageNum = pageNum.value
|
||||
const reqPageSize = pageSize.value
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await fetcher({
|
||||
...getFilters(),
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
pageNum: reqPageNum,
|
||||
pageSize: reqPageSize,
|
||||
})
|
||||
if (seq !== requestSeq) return // 已有更新的请求发出,丢弃过期响应
|
||||
if (seq !== requestSeq) return { ok: false, seq } // 已有更新的请求发出,丢弃过期响应
|
||||
records.value = data.list
|
||||
total.value = data.total
|
||||
shownPageNum = reqPageNum
|
||||
shownPageSize = reqPageSize
|
||||
onData?.(data)
|
||||
return { ok: true, seq }
|
||||
} catch (error) {
|
||||
handledError(error) // 失败提示由请求拦截器统一完成
|
||||
return { ok: false, seq }
|
||||
} finally {
|
||||
if (seq === requestSeq) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求失败后把分页状态退回到「屏幕上的数据」。
|
||||
*
|
||||
* 只在本次请求仍是最新请求时执行:否则用户已经点了别的页,
|
||||
* 这里的回滚会把他的点击覆盖掉。
|
||||
*/
|
||||
function restoreShownPage(seq: number): void {
|
||||
if (seq !== requestSeq) return
|
||||
pageNum.value = shownPageNum
|
||||
pageSize.value = shownPageSize
|
||||
}
|
||||
|
||||
/** 按当前页码与筛选条件加载(供页面直接调用) */
|
||||
async function load(): Promise<void> {
|
||||
const { ok, seq } = await fetchPage()
|
||||
if (!ok) restoreShownPage(seq)
|
||||
}
|
||||
|
||||
/** 条件查询:回到第一页再加载(停留旧页码会查到空页) */
|
||||
function search(): void {
|
||||
async function search(): Promise<void> {
|
||||
pageNum.value = 1
|
||||
void load()
|
||||
await load()
|
||||
}
|
||||
|
||||
function onPageChange(page: number): void {
|
||||
/**
|
||||
* 翻页:失败则退回屏幕上正在显示的那一页。
|
||||
*
|
||||
* 不回滚的后果是分页器高亮与列表数据永久错位 —— 用户看到的是
|
||||
* 「点了没反应,再点一下又跳回去」,且必须连点两次才能对齐。
|
||||
*/
|
||||
async function onPageChange(page: number): Promise<void> {
|
||||
pageNum.value = page
|
||||
void load()
|
||||
const { ok, seq } = await fetchPage()
|
||||
if (!ok) restoreShownPage(seq)
|
||||
}
|
||||
|
||||
function onSizeChange(size: number): void {
|
||||
/** 改每页条数:回到第一页;失败同时退回页码与条数 */
|
||||
async function onSizeChange(size: number): Promise<void> {
|
||||
pageSize.value = size
|
||||
pageNum.value = 1
|
||||
void load()
|
||||
const { ok, seq } = await fetchPage()
|
||||
if (!ok) restoreShownPage(seq)
|
||||
}
|
||||
|
||||
/** 删除后刷新:当前页已被删空且不在第一页时,先回退页码再加载 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user