Appearance
分页(客户端)
传 pagination option 就有分页器,默认由表格自己切片——把全量数据交给它,翻页、切换每页条数、排序、筛选全都不用管。
ts
new VirtTable(el, {
list, // 全量数据
columns,
itemKey: 'id',
estimatedSize: 40,
pagination: { pageSize: 20, pageSizes: [20, 50, 100] },
});不需要 onPageChange,也不需要 total。
与服务端模式的差别
| 客户端(默认) | 服务端(manualPagination: true) | |
|---|---|---|
| 谁切数据 | 表格 | 使用方(在 onPageChange 里 setList) |
total | 由筛选后的行数派生,setTotal() 会告警并跳过 | 使用方给(option 或 setTotal()) |
| 排序 / 筛选 | 作用于全量数据,再切当前页 | 交给服务端(配合 manualSorting / manualFiltering) |
| 适用 | 数据量能一次拿完 | 数据在服务端分页 |
三个 manual 开关是对称的:manualSorting / manualFiltering / manualPagination 各自让表格跳过管线里对应的那一段。
行为细节
- 总数随筛选变化,页码在超出范围时自动 clamp。
- 筛选后回到第 1 页(行集合变了);排序不重置页码(还是那些行,只是顺序变了)。
- 树形 / 分组按根行切片:每页 N 个根行,子节点跟着父节点走,不会被切到两页去。
total数的也是根行数。 - 合计行(
showSummary)算的是当前页——与「聚合反映当前视图」一致。要全量合计请用服务端模式自己算。 - 与无限滚动互斥:同时配
infinite时分页切片不生效(两者都在管「该显示哪些行」)。 - 翻页后自动滚回顶部。
与状态持久化一起用
getState() 会带上 pagination: { page, pageSize }(不带 total——它是派生的,存下来只会在数据变了以后骗人):
ts
localStorage.setItem('k', JSON.stringify(table.getState()));
table.setState(JSON.parse(localStorage.getItem('k')!)); // 列宽/列序/排序/筛选/页码一起还原还原时页码不会被「筛选回第 1 页」那条规则顶掉。
示例
微应用尚未挂载。
源码
点击查看源码
ts
import { faker } from '@faker-js/faker';
import { VirtTable, type VirtTableColumn } from '@virt-table/vanilla';
/**
* 客户端分页(默认模式):把全量数据交给表格,翻页 / 切换每页条数 / 排序 / 筛选
* 全部由表格自己处理,**不用写 onPageChange,也不用管 total**。
*
* 与服务端模式(`manualPagination: true`,见 feature/pagination)的差别就是那一个开关。
*/
const ROW_COUNT = 2000;
interface Row {
id: number;
name: string;
dept: string;
score: number;
}
const DEPTS = ['工程部', '设计部', '市场部'];
export function bootstrapTablePaginationClient(root: HTMLElement): () => void {
const list: Row[] = Array.from({ length: ROW_COUNT }, (_, i) => ({
id: i + 1,
name: faker.person.fullName(),
dept: DEPTS[i % DEPTS.length]!,
score: 40 + ((i * 17) % 60),
}));
const columns: VirtTableColumn<Row>[] = [
{ key: 'id', title: 'ID', width: 90, align: 'center', sortable: true },
{ key: 'name', title: '姓名', width: 200 },
{
key: 'dept',
title: '部门',
width: 160,
align: 'center',
filters: DEPTS.map((d) => ({ label: d, value: d })),
},
{ key: 'score', title: '分数', width: 120, align: 'right', sortable: true },
];
root.innerHTML = `
<div class="demo-hint">
客户端分页(默认):全量 ${ROW_COUNT} 条一次性交给表格,翻页只换切片。
total 由筛选后的行数派生 —— 试试筛选部门,总数会跟着变,页码自动回到第 1 页。
排序作用于全量数据而不是当前页。
</div>
<div class="virt-table-controls">
<button id="btnPage7">跳到第 7 页</button>
<button id="btnFilter">筛「工程部」</button>
<button id="btnClear">清空筛选</button>
<button id="btnSave">存状态</button>
<button id="btnLoad">还原状态</button>
</div>
<div class="status-text" id="status"></div>
<div style="width:100%;height:520px;" class="demo-container" id="tableContainer"></div>
`;
const container = root.querySelector('#tableContainer') as HTMLElement;
const statusEl = root.querySelector('#status') as HTMLElement;
const table = new VirtTable<Row>(container, {
list,
columns,
itemKey: 'id',
estimatedSize: 40,
buffer: 6,
border: true,
stripe: true,
// 传了 pagination 就有分页器;不开 manualPagination 即客户端分页
pagination: { pageSize: 20, pageSizes: [20, 50, 100] },
});
const syncStatus = (): void => {
const p = table.getPagination();
statusEl.textContent = `第 ${p.page} 页 · 每页 ${p.pageSize} 条 · 共 ${p.total} 条(筛选后)`;
};
syncStatus();
const q = (id: string) => root.querySelector(id) as HTMLButtonElement;
let saved: string | null = null;
q('#btnPage7').onclick = () => {
table.setPage(7);
syncStatus();
};
q('#btnFilter').onclick = () => {
table.setColumnFilter('dept', ['工程部']);
syncStatus();
};
q('#btnClear').onclick = () => {
table.clearAllFilters();
syncStatus();
};
q('#btnSave').onclick = () => {
saved = JSON.stringify(table.getState());
statusEl.textContent = '已存下当前列宽 / 列序 / 排序 / 筛选 / 分页';
};
q('#btnLoad').onclick = () => {
if (!saved) return;
table.setState(JSON.parse(saved));
syncStatus();
};
return () => {
table.destroy();
root.innerHTML = '';
};
}