diff --git a/frontend/src/composables/usePagedList.ts b/frontend/src/composables/usePagedList.ts index 91297c6..571fec8 100644 --- a/frontend/src/composables/usePagedList.ts +++ b/frontend/src/composables/usePagedList.ts @@ -28,42 +28,101 @@ export function usePagedList = PageDa /** 自增请求序号:仅最新请求的响应可写入状态 */ let requestSeq = 0 - /** 按当前页码与筛选条件加载 */ - async function load(): Promise { + /** + * 列表当前**实际显示**的是哪一页(成功写入 records 时同步)。 + * + * 这是「分页器高亮」与「列表内容」保持一致的唯一依据:失败回滚要退回 + * 这个页码,而不是退回点击前的页码 —— 连点两次时前一次请求的响应会被 + * 序号守卫丢弃,退回「点击前」的页码可能与屏幕上的数据对不上。 + */ + let shownPageNum = 1 + let shownPageSize = 10 + + /** + * 按当前页码与筛选条件加载,返回是否成功以及本次请求的序号。 + * + * 返回值是翻页失败回滚的依据:`records` 只在成功时更新,而分页器 + * 高亮绑的是 `pageNum`,两者必须同进同退,否则会出现 + * 「页码已跳到第 2 页、列表还停在第 1 页」的错位(用户来回点才恢复)。 + * + * 带出 `seq` 是因为**回滚本身也要防竞态**:连点两次时旧请求失败, + * 若无条件把页码退回它那一轮的旧值,就会把用户刚点出的新页码覆盖掉, + * 表现为「页码自己跳回去」。 + * + * 页码与条数在 await **之前**取快照:等待期间另一次点击可能已改动 + * `pageNum`,用 await 之后的值记录「显示的页」会记错。 + * + * 对外不暴露返回值:`load` 被大量页面当 `() => void | Promise` + * 的回调使用,多一个返回值会让那些赋值类型不兼容。 + */ + 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 { + const { ok, seq } = await fetchPage() + if (!ok) restoreShownPage(seq) + } + /** 条件查询:回到第一页再加载(停留旧页码会查到空页) */ - function search(): void { + async function search(): Promise { pageNum.value = 1 - void load() + await load() } - function onPageChange(page: number): void { + /** + * 翻页:失败则退回屏幕上正在显示的那一页。 + * + * 不回滚的后果是分页器高亮与列表数据永久错位 —— 用户看到的是 + * 「点了没反应,再点一下又跳回去」,且必须连点两次才能对齐。 + */ + async function onPageChange(page: number): Promise { 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 { pageSize.value = size pageNum.value = 1 - void load() + const { ok, seq } = await fetchPage() + if (!ok) restoreShownPage(seq) } /** 删除后刷新:当前页已被删空且不在第一页时,先回退页码再加载 */