---
description: "信息流的形状：不给页码，让可见窗口一页页往下长 —— 游标记着上一页停在哪，「加载更多」按钮和滚动近底自动触发各走一条路。数据只管顺着看、页码没有意义（动态、日志、消息），就是它"
---

# 无限滚动（游标加载列表）

信息流的形状：不给页码，让可见窗口一页页往下长 —— 游标记着上一页停在哪，「加载更多」按钮和滚动近底自动触发各走一条路。数据只管顺着看、页码没有意义（动态、日志、消息），就是它。

<iframe src="/skills/dashboard/assets/demo-infinite-list.html"
        style="width:100%;height:580px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：无限滚动"></iframe>

试这几下：

- 点**加载更多**，或直接把列表滚到近底 —— 两条路都翻页，底部先出骨架行（模拟 400ms 延迟）
- 看「已加载 x / 共 y」一路涨到头 —— 到底出现明确的**没有更多了**
- 搜索姓名或部门 —— 游标重置、从头加载；快速改词也不乱序（过期响应按请求序号丢弃）
- 勾上**模拟下次加载失败**再翻页 —— 自动触发暂停，出现错误提示和**重试**按钮

## 规矩

- **「加载更多」按钮永远是主路径**：滚动近底自动触发（IntersectionObserver 观察底部哨兵）只是增强，坏了也能点。demo 里两者共用同一个 `loadMore` 入口，加载中/出错/到底时统一拦截
- **进度与终点要说清**：始终显示「已加载 x / 共 y」，到底给明确完结态，不能让按钮悄悄消失
- **远程形态的 loading 和 error 都要显式**：下一页加载给骨架行 + 按钮置灰；失败暂停自动触发、给重试按钮（否则滚动会无限重试）。demo 用 400ms 定时器模拟，真实版换 TanStack Query 的 `useInfiniteQuery`（`isFetchingNextPage` / `error` + `refetch`）
- **游标保证不重不漏**：demo 的游标 = 上一页最后一行 id，「服务端」恒按 id 升序切片，客户端追加前再按 id 去重兜底；真实版把游标交给 `getNextPageParam`，搜索/筛选放 URL，一变就重置游标从第一页拉起

> 蓝本：`add-infinite-list.md`[^add-infinite-list]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-infinite-list.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-infinite-list.html"
    ```

[^add-infinite-list]: [`add-infinite-list.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-infinite-list.md)，2026-07-28 读。
