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 |
- 两个都不传 → 全量,不走下面这套分页逻辑。
- 只传一个 → 另一个用默认值(比如只传
page=3就是第 3 页、每页 25 条)。 - 参数非法(
page=0、page=abc、page_size=1.5)→ 400,不会静默忽略。
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_count | number | null | 整个结果集的条数。可能是 null(当 ERP 不上报总数时),此时请用「返回不满一页」判断是不是最后一页,别把 null 当 0 |
offset | number | 本页第一条的偏移量,等于 (current - 1) * limit |
limit | number | 实际生效的每页条数(被夹过的 page_size 看这里) |
current | number | 页码,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. 定制时必须知道的几件事
- 搜索 / 筛选 / 列排序 / CSV 导出 / 顶部统计,现在都是对内存里的全量数组算的。 改成按页取之后,它们只能作用于当前这一页——搜不到第 2 页的记录,导出的也只有当页。 要保住这些能力,得把筛选条件也下推到接口,目前接口没有这些参数。 这是本次定制最大的取舍点,请先和产品确认。
- 分页请求不走缓存。 不传参数的全量请求会走 portal 的缓存(15 分钟量级),分页请求每次都真去 ERP 取。所以「分页一定比全量快」不成立——首次加载可能反而更慢,换来的是响应体小、翻页稳定。
-
最后一页可能不满,甚至可能是空的。
total_count是 ERP 侧的计数,接口还会按该连接的「只取最近 N 个月」设置再裁一次,两者可能差几行。 所以:不要假设offset + data.length === total_count;页码超出时会返回空数组而不是报错,这不算失败。 - 详情页和 PDF 按钮不受影响。 它们的归属校验仍然用全量列表,所以客户点第 1 页以外的单据不会 404。前端也不需要为它们传分页参数。
- 建议按客户开关。 这是给一家客户做的定制。建议只对那家开启分页(或只在新页面上用),其它客户/页面传都不传,保持全量,互不影响。接口侧不需要任何开关配置。
- 目前只有 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 |