分页出现错位时,最显眼的是页码,所以第一反应往往是检查 current 绑定。

这一步没错,只是太靠近结果。一个页码从用户点击到表格完成渲染,中间至少经过分页事件、列表状态、参数组装、接口分页规则、响应解析和状态提交。前面任何一处把“第几页”解释错了,最后都会表现成分页器不对。

上一篇已经给查询、翻页、重置和删除刷新定过状态规则,这里不再重复“什么操作应该回第一页”。这一篇只回答一个更窄的问题:规则明明写了,页码和数据为什么仍会错位?

先别改 current,把一页数据走过的路画出来

我会让 Codex 先交付一条真实调用链,而不是直接给修复代码:

分页事件
  → 页面或分页 Hook 接收 current / size
  → 列表入口更新分页状态
  → 组装最终请求参数
  → 接口解释页码并返回 records / total
  → 前端判断这份响应能否提交
  → 更新 list、current、size、total
  → 分页器与表格重新渲染

这条链上的字段看起来都叫页码,职责却不一样。点击事件里的 current 是用户意图,请求里的 current 是接口契约,页面状态里的 current 是当前视图事实。把三者当成同一个值直接来回覆盖,问题就很难定位。

第一处断点:前后端对“第一页”的编号不同

有的接口从 1 开始计页,有的接口把第一页记为 0。分页组件显示第 1 页,不代表请求一定应该传 1

如果接口按 0 起算,而页面直接把组件页码传过去,用户点第 2 页时,接口可能收到 2,实际返回第三段数据。反过来,在多个位置都做 current - 1,也可能产生重复转换。

我会把转换限制在接口适配边界:

const toApiPage = ({ current, size }) => ({
  pageIndex: current - 1,
  pageSize: size
});

这只是结构示意,不是通用接口规范。真正要确认的是接口文档、现有 API 封装和相邻页面的请求。组件内部、页面事件和请求封装不能各自猜一次。

字段名也属于同一类契约问题。页面使用 currentsize,接口可能接收 pageNumpageSize,也可能要求分页对象放进请求体。如果 Codex 只在页面上改变量名,却没有追到 API 封装,界面状态会变化,实际请求却仍沿用旧字段。

第二处断点:同一份页码被两个地方持有

分页错位还有一种常见结构:分页组件内部有一份页码,列表页面或 tabMixin 里又有一份。

用户点击以后,内部页码先变成 3,外部请求失败仍停在 2;或者外部被重置成 1,内部组件没有同步。此时分页器和表格各自都在诚实展示自己的状态,只是两份状态已经分叉。

我会要求明确唯一事实来源:

  • 分页组件负责发出用户意图,不私自保存业务页码;

  • 列表状态持有已确认的 currentsizetotal

  • 请求失败时,是否提交目标页码要按项目交互约定处理;

  • 任何镜像状态都要说明同步方向,不能双向随意赋值。

当前 web-skills 的分页 Hook 将 currentsize 传回统一 getListtabMixin 再维护 listData.pageObj。这给出了一个清晰入口。不过,目标项目是否完全采用这份实现,仍要读实际文件,不能因为 Skill 里有示例就跳过代码取证。

第三处断点:请求发出去的值,不是页面以为的值

响应式对象很方便,也会把参数构造时机藏起来。

下面这类写法如果把同一个对象继续交给异步流程或拦截器,调试时看到的值未必等于请求发出瞬间的值:

const params = pageState;
listApi(params);

更容易核对的方式,是在请求边界生成一次快照:

const params = {
  current: pageState.current,
  size: pageState.size,
  ...fixedParams,
  ...toApiQuery(committedQuery)
};
​
return listApi(params);

我关心的不是对象展开这种写法本身,而是能否回答:这次请求最终带了哪个页码、哪些固定条件和哪一版已提交查询。只有页面状态,没有序列化后的请求参数,还不足以证明分页链路正确。

web-skills 当前的 tabMixin 会把 pageObjotherParams 和已保存查询组合后交给 listApi。审查时要继续看合并顺序,因为同名字段可能被后展开的对象覆盖。比如固定参数里意外带着 current,分页组件传来的新页码就会在请求前被悄悄改回去。

第四处断点:接口校正了页码,前端却没有接住

总数变化后,请求页码可能超出有效范围。部分后端会返回空数组,部分会把页码校正到最后一页,还有的会在响应里返回实际 currentsize

如果接口已经把第 5 页校正为第 4 页,前端只读取 recordstotal,却继续保留本地 current = 5,页面就会出现“第 5 页显示第 4 页数据”的错位。

当前 web-skillstabMixin 示例在分页响应中更新 recordstotal,没有直接采用响应里的 current。这并不自动构成错误:若本项目约定前端页码始终有效,或者接口不会校正页码,本地状态就是权威值。只有接口契约明确返回了实际页码,才需要决定是否同步。

因此,我不会让 Codex 看见 res.data.current 就立即补一行赋值。先查清楚三件事:

  1. 接口是否真的会修正越界页码;

  2. 响应页码与请求页码使用相同起点吗;

  3. 前端接受校正后,分页器、序号列和后续刷新是否一起更新。

第五处断点:晚返回的旧响应覆盖了新页面

用户快速点击第 2 页和第 3 页,请求 A、B 依次发出。B 先返回,表格已经显示第 3 页;随后 A 返回,又把列表覆盖成第 2 页数据。如果页码状态仍是 3,错位就出现了。

这是响应提交问题,不是分页事件问题。继续在点击回调里设置 current,修不好它。

常用办法是取消旧请求,或只允许最新请求提交:

let latestRequestId = 0;
​
const requestList = async (params) => {
  const requestId = ++latestRequestId;
  const res = await listApi(params);
​
  if (requestId !== latestRequestId) return;
​
  listData.list = res.data.records;
  listData.pageObj.total = res.data.total;
};

是否采用序号、取消请求或请求库已有能力,要看项目现状。这段示意只强调一点:响应能返回,不等于它仍有资格更新当前页面。

我会怎样给断点排序

分页错位不适合同时改五处。我通常按能最快排除错误解释的顺序检查:

检查位置 要核对的事实 发现差异后的方向
最终请求 页码、每页条数、查询条件是否符合接口 修参数构造或契约适配
接口响应 返回的是哪一页、多少条、总数多少 确认服务端校正与响应字段
响应提交 是否由旧请求覆盖新请求 限制过期响应提交
页面状态 currentsizetotal 是否同源 收敛状态所有权
分页渲染 组件绑定值是否来自列表状态 修绑定或组件契约

这张表故意先看网络事实,再看 UI。因为“分页器显示错了”只能说明结果不一致,不能证明根因就在分页器。

交给 Codex 的排查任务应该写到这里

这张列表出现页码与数据错位。先定位,不要直接修改:
​
- 追踪分页点击到列表渲染的完整调用链;
- 标出组件页码、列表页码、请求页码和响应页码各自的起点与所有者;
- 给出一次请求的最终参数,而不是只读响应式对象;
- 核对响应是否返回或校正 current、size、total;
- 检查多个请求返回顺序以及响应提交条件;
- 每个判断附文件位置或运行证据,并指出仍未确认的接口约定。
​
找到第一个断点后,只在对应边界给最小修正方案。不要在多个事件里重复设置 current,也不要绕开项目已有的 tabMixin、paging 和请求封装。

如果缺少可运行环境,Codex 可以完成静态调用链和风险判断,但不能把接口行为写成已验证。页码起点、后端校正和并发返回顺序,最终都需要网络证据。

写在最后

分页器只是链路末端的一块显示。页码与数据错位,可能来自编号契约、参数覆盖、双份状态、服务端校正,也可能只是旧响应晚到了。

把这些位置画成一条链,修复才有明确落点。否则在查询、翻页和重置事件里反复补 current = 1,表面上挡住一个路径,下一次连续操作还会复发。

下一篇继续做排查,但不再扩展根因。我会把请求前、响应后和状态提交时的证据整理成一份最小记录,让 Codex 能根据同一份现场判断断点,而不是靠日志数量猜问题。

本系列持续更新。分页链路收束后,将进入 Loading 的页面、按钮和局部状态边界。

参考资料

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐