Customer Portal 列表分页 — 前端接入说明

适用于 GET /portal/orders 和 GET /portal/invoices(只有这两个,其它列表接口未变)

接口已在生产环境验证可用

一句话:想分页就带上 page 和 page_size;不带这两个参数时行为跟以前完全一样(返回全量、走缓存、响应里没有 meta.pagination)。所以这是纯粹的可选能力,现有调用方一行都不用改。

1. 快速开始

# 第 2 页,每页 25 条
curl -s "https://a1.shophive.ai/api/v1/portal/invoices?page=2&page_size=25" \
  -H "Accept: application/json" \
  -H "x-portal-token: <portal token>"

# 不带参数 = 老行为:全量 + 缓存 + 无 meta.pagination
curl -s "https://a1.shophive.ai/api/v1/portal/invoices" \
  -H "x-portal-token: <portal token>"

x-portal-token 就是 portal 现在每个请求都在带的那个 token;域名用 useGlobal().apiBaseUrl,别硬编码。

2. 请求参数

参数类型默认说明
page 整数 ≥ 1 1 页码,从 1 开始(不是 offset)
page_size 整数 ≥ 1 25 每页条数,上限 200。超过 200 会被夹到 200(不是报错),实际生效值看响应里的 meta.pagination.limit

3. 响应结构

分页时,响应体在原有的 data 之外多一个 meta.pagination:

{
  "data": [
    { "invoice_no": "9463330", "invoice_date": "2026-09-30", ... },   // 25 条
    ...
  ],
  "meta": {
    "pagination": {
      "total_count": 1176,   // 整个结果集的条数
      "offset": 25,          // 本页第一条的偏移量(0 基)
      "limit": 25,           // 实际生效的每页条数
      "current": 2           // 页码(1 基)
    }
  }
}

这是每个 portal 列表接口本来就有的信封格式,usePortalHttp 已经会自动把 meta.pagination 解到 pagination 上,不用自己解析。

字段类型说明
total_countnumber | null整个结果集的条数。可能是 null(当 ERP 不上报总数时),此时请用「返回不满一页」判断是不是最后一页,别把 null 当 0
offsetnumber本页第一条的偏移量,等于 (current - 1) * limit
limitnumber实际生效的每页条数(被夹过的 page_size 看这里)
currentnumber页码,1 基,可以直接喂给 antd Table 的 pagination.current

4. 前端接入

4.1 服务层

要能传 params,得在泛型里把 request 声明出来(现在是 request: never,TS 不让你给 fetchData 传参数):

export const usePortalInvoiceList = () =>
  usePortalHttp<{
    request: { page?: number; page_size?: number }   // ← 加这个
    response: IPortalInvoiceType[]
  }>({
    url: '/portal/invoices',
    // 想首屏就分页就在这里带上;不传 = 全量(老行为)
    params: { page: 1, page_size: 25 },
  })

4.2 组件里翻页

const {
  data, pagination, loading, fetchData,
} = usePortalInvoiceList()

const onPageChange = (page: number, pageSize: number) =>
  fetchData({ params: { page, page_size: pageSize } })

接到 antd Table 上:

<Table
  dataSource={data ?? []}
  loading={loading}
  pagination={{
    current: pagination.current || 1,
    pageSize: pagination.limit || 25,
    total: pagination.total_count ?? data?.length ?? 0,
    showSizeChanger: true,
    onChange: onPageChange,
  }}
/>
⚠️ fetchData({ params }) 是整体替换,不是合并 如果这个 service 的配置里还有别的 query 参数,翻页时要一起带上,否则会被覆盖掉。
⚠️ Makeswift 预览模式(builder)下 pagination 恒为 0 usePortalHttp 在预览模式下短路返回默认值,不请求接口,所以预览里分页控件会是空的。这是预期行为,不用处理。

5. 定制时必须知道的几件事

  1. 搜索 / 筛选 / 列排序 / CSV 导出 / 顶部统计,现在都是对内存里的全量数组算的。 改成按页取之后,它们只能作用于当前这一页——搜不到第 2 页的记录,导出的也只有当页。 要保住这些能力,得把筛选条件也下推到接口,目前接口没有这些参数。 这是本次定制最大的取舍点,请先和产品确认。
  2. 分页请求不走缓存。 不传参数的全量请求会走 portal 的缓存(15 分钟量级),分页请求每次都真去 ERP 取。所以「分页一定比全量快」不成立——首次加载可能反而更慢,换来的是响应体小、翻页稳定。
  3. 最后一页可能不满,甚至可能是空的。 total_count 是 ERP 侧的计数,接口还会按该连接的「只取最近 N 个月」设置再裁一次,两者可能差几行。 所以:不要假设 offset + data.length === total_count;页码超出时会返回空数组而不是报错,这不算失败。
  4. 详情页和 PDF 按钮不受影响。 它们的归属校验仍然用全量列表,所以客户点第 1 页以外的单据不会 404。前端也不需要为它们传分页参数。
  5. 建议按客户开关。 这是给一家客户做的定制。建议只对那家开启分页(或只在新页面上用),其它客户/页面传都不传,保持全量,互不影响。接口侧不需要任何开关配置。
  6. 目前只有 orders 和 invoices 支持。 quotes / purchases 等列表接口仍是全量;如果后面也要,走同样的模式加即可。

6. 自查清单

检查预期
GET /portal/invoices✓ 返回全部、无 meta.pagination
GET /portal/invoices?page=1&page_size=25✓ 25 条 + current: 1, offset: 0, limit: 25
GET /portal/invoices?page=2&page_size=25✓ 25 条 + current: 2, offset: 25,且和第一页的内容不重复
GET /portal/orders?page=1&page_size=5✓ 5 条 + total_count 是订单总数
翻到远超总页数的页码✓ 200 + 空数组,不报错
page=abc✓ 400