Files
yingqing/core/frontend/src/components/pager.tsx
T
2026-08-19 17:44:56 +08:00

87 lines
3.8 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.
// 通用列表分页器:mono 总数 + 页码窗口 + 每页条数 + 跳页输入。
// 父组件持有 page state;纯前端切片或服务端分页都适用(onChange 回页码)。
// 新增能力全部可选 —— 旧调用方只传 page/total/pageSize/onChange 时退化为原只读分页器:
// onPageSizeChange 传则「每页 N 条」变成可点击循环按钮(默认循环 12/24/48/96)
// pageSizeOptions 自定义每页条数候选
// sticky 传则分页器吸底浮动
// alwaysShow 单页也渲染(admin 列表用:让「共 N 条 · 每页 10 条」可见,否则数据少时像没接分页)
// 默认 total <= pageSize 且不可改每页条数时不渲染(单页无需分页器)。
import { useState } from "react";
// 页码窗口:超过 7 页折叠成 1 … cur-1 cur cur+1 … last
export function pageWindow(current: number, total: number): Array<number | "ellipsis"> {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const items: Array<number | "ellipsis"> = [1];
const start = Math.max(2, current - 1);
const end = Math.min(total - 1, current + 1);
if (start > 2) items.push("ellipsis");
for (let p = start; p <= end; p++) items.push(p);
if (end < total - 1) items.push("ellipsis");
items.push(total);
return items;
}
export function Pager({ page, total, pageSize, onChange, onPageSizeChange, pageSizeOptions = [12, 24, 48, 96], sticky = false, alwaysShow = false }: {
page: number;
total: number;
pageSize: number;
onChange: (page: number) => void;
onPageSizeChange?: (size: number) => void;
pageSizeOptions?: number[];
sticky?: boolean;
alwaysShow?: boolean;
}) {
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const cur = Math.min(Math.max(1, page), totalPages);
const [jump, setJump] = useState("");
// 单页且不可改每页条数 → 不渲染;可改条数或 alwaysShow 时仍渲染
if (total <= pageSize && !onPageSizeChange && !alwaysShow) return null;
function cycleSize() {
if (!onPageSizeChange) return;
const idx = pageSizeOptions.indexOf(pageSize);
onPageSizeChange(pageSizeOptions[(idx + 1) % pageSizeOptions.length]);
}
function commitJump() {
const n = Number.parseInt(jump, 10);
if (Number.isFinite(n)) onChange(Math.min(Math.max(1, n), totalPages));
setJump("");
}
return (
<div className={`list-pager${sticky ? " sticky" : ""}`}>
<span className="total">共 {total} 条 · 第 {cur} / {totalPages} 页</span>
<div className="pages">
<button type="button" disabled={cur === 1} onClick={() => onChange(cur - 1)} aria-label="上一页">‹</button>
{pageWindow(cur, totalPages).map((p, i) => (
p === "ellipsis"
? <span key={`e${i}`} className="ellipsis">…</span>
: <button type="button" key={p} className={p === cur ? "active" : ""} onClick={() => onChange(p)}>{p}</button>
))}
<button type="button" disabled={cur === totalPages} onClick={() => onChange(cur + 1)} aria-label="下一页">›</button>
</div>
{totalPages > 7 && (
<span className="jump">
跳至
<input
type="number"
min={1}
max={totalPages}
value={jump}
placeholder={String(cur)}
onChange={(event) => setJump(event.target.value)}
onKeyDown={(event) => { if (event.key === "Enter") commitJump(); }}
onBlur={() => { if (jump) commitJump(); }}
aria-label="跳至页码"
/>
页
</span>
)}
{onPageSizeChange
? <button type="button" className="page-size size-cycle" onClick={cycleSize} title="点击切换每页条数">每页 {pageSize} 条</button>
: <span className="page-size">每页 {pageSize} 条</span>}
</div>
);
}