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:
1 parent
a00fca942d
commit
678db0499d
1 file changed
+70
-11
@@ -28,42 +28,101 @@ export function usePagedList<T, F extends object, D extends PageData<T> = PageDa
|
|||||||
/** 自增请求序号:仅最新请求的响应可写入状态 */
|
/** 自增请求序号:仅最新请求的响应可写入状态 */
|
||||||
let requestSeq = 0
|
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 seq = ++requestSeq
|
||||||
|
const reqPageNum = pageNum.value
|
||||||
|
const reqPageSize = pageSize.value
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await fetcher({
|
const data = await fetcher({
|
||||||
...getFilters(),
|
...getFilters(),
|
||||||
pageNum: pageNum.value,
|
pageNum: reqPageNum,
|
||||||
pageSize: pageSize.value,
|
pageSize: reqPageSize,
|
||||||
})
|
})
|
||||||
if (seq !== requestSeq) return // 已有更新的请求发出,丢弃过期响应
|
if (seq !== requestSeq) return { ok: false, seq } // 已有更新的请求发出,丢弃过期响应
|
||||||
records.value = data.list
|
records.value = data.list
|
||||||
total.value = data.total
|
total.value = data.total
|
||||||
|
shownPageNum = reqPageNum
|
||||||
|
shownPageSize = reqPageSize
|
||||||
onData?.(data)
|
onData?.(data)
|
||||||
|
return { ok: true, seq }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handledError(error) // 失败提示由请求拦截器统一完成
|
handledError(error) // 失败提示由请求拦截器统一完成
|
||||||
|
return { ok: false, seq }
|
||||||
} finally {
|
} finally {
|
||||||
if (seq === requestSeq) loading.value = false
|
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
|
pageNum.value = 1
|
||||||
void load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPageChange(page: number): void {
|
/**
|
||||||
|
* 翻页:失败则退回屏幕上正在显示的那一页。
|
||||||
|
*
|
||||||
|
* 不回滚的后果是分页器高亮与列表数据永久错位 —— 用户看到的是
|
||||||
|
* 「点了没反应,再点一下又跳回去」,且必须连点两次才能对齐。
|
||||||
|
*/
|
||||||
|
async function onPageChange(page: number): Promise<void> {
|
||||||
pageNum.value = page
|
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
|
pageSize.value = size
|
||||||
pageNum.value = 1
|
pageNum.value = 1
|
||||||
void load()
|
const { ok, seq } = await fetchPage()
|
||||||
|
if (!ok) restoreShownPage(seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除后刷新:当前页已被删空且不在第一页时,先回退页码再加载 */
|
/** 删除后刷新:当前页已被删空且不在第一页时,先回退页码再加载 */
|
||||||
|
|||||||
Reference in new issue
Block a user