77 lines
3.7 KiB
TypeScript
77 lines
3.7 KiB
TypeScript
import { h, renderList, signal } from '@ui-arch/frame';
|
||
import type { PaginationProps } from './types.js';
|
||
import { select } from './select.js';
|
||
|
||
/*** 页码序列:≤7 页全展开;否则保留首尾与当前页 ±1 窗口,缺口折叠为省略号(null 占位) ***/
|
||
function pageSequence(page: number, totalPages: number): (number | null)[] {
|
||
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||
const marks = [1, 2, page - 1, page, page + 1, totalPages - 1, totalPages].filter((p) => p >= 1 && p <= totalPages);
|
||
const seq: (number | null)[] = [];
|
||
let prev = 0;
|
||
for (const p of [...new Set(marks)].sort((a, b) => a - b)) {
|
||
if (p - prev > 1) seq.push(null); // 缺口 → 省略号
|
||
seq.push(p);
|
||
prev = p;
|
||
}
|
||
return seq;
|
||
}
|
||
|
||
/*** 独立分页器:受控组件,page 为双向绑定信号;页码过多折叠省略号,首末页禁用翻页,jumpable 可跳页,pageSizes 可切每页条数 ***/
|
||
|
||
export function pagination(props: PaginationProps): HTMLElement {
|
||
// —— 状态集中声明 ——
|
||
const size = signal(props.pageSize); // 每页条数:可从 switcher 改动,故用信号持有
|
||
const totalPages = () => Math.max(1, Math.ceil(props.total / size())); // 总页数随时可重算
|
||
const pages = h('div', { class: 'ua-pagination-pages' }); // 页码区,随页码重绘
|
||
const tail = h('div', { class: 'ua-pagination-tail' }); // 附加区(条数切换 + 跳页),只创建一次
|
||
const go = (p: number) => {
|
||
const next = Math.min(totalPages(), Math.max(1, p)); // 钳制到有效页码范围
|
||
props.page(next);
|
||
props.onChange?.(next);
|
||
};
|
||
|
||
// —— 每页条数切换器:传了候选才渲染,切换即回第 1 页 ——
|
||
if ((props.pageSizes ?? []).length > 1) {
|
||
const sizeValue = signal(String(size()));
|
||
tail.append(
|
||
select({
|
||
options: props.pageSizes!.map((n) => ({ value: String(n), label: `${n} 条/页` })),
|
||
value: sizeValue,
|
||
onChange: (v) => {
|
||
size(Number(v));
|
||
props.onPageSizeChange?.(Number(v));
|
||
go(1); // 条数变了旧页码无意义,统一回首页
|
||
},
|
||
}),
|
||
);
|
||
}
|
||
|
||
// —— 跳页输入:jumpable 且多页时渲染,回车跳转 ——
|
||
if (props.jumpable) {
|
||
const jump = h('input', { class: 'ua-page-jump', inputmode: 'numeric', 'aria-label': '跳转到指定页' }) as HTMLInputElement;
|
||
jump.addEventListener('change', () => {
|
||
const n = parseInt(jump.value, 10);
|
||
jump.value = ''; // 跳转后清空输入,避免残留旧页码
|
||
if (!Number.isNaN(n)) go(n); // 非数字忽略,由 go 钳制范围
|
||
});
|
||
tail.append(h('span', { class: 'ua-page-jump-wrap' }, jump, h('span', { class: 'ua-page-total' }, () => `/ ${totalPages()} 页`)));
|
||
}
|
||
|
||
const nav = h('nav', { class: 'ua-pagination', 'aria-label': '分页' }, pages, tail);
|
||
renderList(pages, () => {
|
||
const totalNow = totalPages();
|
||
const page = Math.min(totalNow, Math.max(1, props.page()));
|
||
const items: HTMLElement[] = [h('button', { class: 'ua-page-btn', disabled: page <= 1, 'aria-label': '上一页', onClick: () => go(page - 1) }, '‹')];
|
||
for (const it of pageSequence(page, totalNow)) {
|
||
items.push(
|
||
it == null
|
||
? h('span', { class: 'ua-page-gap', 'aria-hidden': 'true' }, '…')
|
||
: h('button', { class: `ua-page-btn${it === page ? ' ua-active' : ''}`, disabled: it === page, 'aria-current': it === page ? 'page' : null, 'aria-label': `第 ${it} 页`, onClick: () => go(it) }, String(it)),
|
||
);
|
||
}
|
||
items.push(h('button', { class: 'ua-page-btn', disabled: page >= totalNow, 'aria-label': '下一页', onClick: () => go(page + 1) }, '›'));
|
||
return items;
|
||
});
|
||
return nav;
|
||
}
|