Files
UI/packages/skin/ui/src/pagination.ts
T

77 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}