Files
zhixing-system/zhixing-web/src/shared/ui/pagination.tsx
T

139 lines
4.4 KiB
TypeScript
Raw Normal View History

import { ChevronLeft, ChevronRight } from "lucide-react"
import type { ChangeEvent } from "react"
import { Button } from "@/shared/ui/button"
import { cn } from "@/shared/ui/utils"
interface PaginationProps {
page: number
pageSize: number
total: number
pageSizeOptions?: readonly number[]
onPageChange: (page: number) => void
onPageSizeChange: (pageSize: number) => void
}
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50] as const
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value))
}
function rangeLabel(page: number, pageSize: number, total: number) {
if (total <= 0) return "共 0 条"
const start = (page - 1) * pageSize + 1
const end = Math.min(page * pageSize, total)
return `第 ${start}–${end} 条,共 ${total} 条`
}
function buildPageItems(page: number, pageCount: number) {
if (pageCount <= 1) return [1]
const items: Array<number | "ellipsis"> = [1]
const start = Math.max(2, page - 1)
const end = Math.min(pageCount - 1, page + 1)
if (start > 2) items.push("ellipsis")
for (let p = start; p <= end; p += 1) items.push(p)
if (end < pageCount - 1) items.push("ellipsis")
if (pageCount > 1) items.push(pageCount)
return items
}
export function Pagination({
page,
pageSize,
total,
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
onPageChange,
onPageSizeChange,
}: PaginationProps) {
const pageCount = Math.max(1, Math.ceil(total / pageSize))
const currentPage = clamp(page, 1, pageCount)
const hasPrev = currentPage > 1
const hasNext = currentPage < pageCount
const items = buildPageItems(currentPage, pageCount)
function handlePageSizeChange(event: ChangeEvent<HTMLSelectElement>) {
onPageSizeChange(Number(event.target.value))
}
return (
<footer
aria-label="表格分页"
className="flex min-h-12 flex-wrap items-center gap-3 border-t border-border/60 px-3 py-2 text-xs text-muted-foreground"
>
<span className="tabular-nums" aria-live="polite">
{rangeLabel(currentPage, pageSize, total)}
</span>
<label className="ml-auto flex items-center gap-1.5">
每页
<select
className="h-11 rounded-md border border-input bg-card px-2 text-xs text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:h-7"
aria-label="每页数量"
value={pageSize}
onChange={handlePageSizeChange}
>
{pageSizeOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
条
</label>
<div className="flex items-center gap-1">
<Button
aria-label="上一页"
disabled={!hasPrev}
onClick={() => onPageChange(currentPage - 1)}
className="size-11 md:size-7"
size="icon-sm"
variant="ghost"
>
<ChevronLeft aria-hidden="true" />
</Button>
{items.map((item, index) => {
if (item === "ellipsis") {
return (
<span
key={`ellipsis-${index}`}
className="px-1.5 text-muted-foreground/70"
aria-hidden="true"
>
…
</span>
)
}
const active = item === currentPage
return (
<button
key={item}
type="button"
aria-current={active ? "page" : undefined}
aria-label={`第 ${item} 页`}
onClick={() => onPageChange(item)}
className={cn(
"size-11 rounded-md border border-transparent text-xs tabular-nums transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:size-7",
active
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
{item}
</button>
)
})}
<Button
aria-label="下一页"
disabled={!hasNext}
onClick={() => onPageChange(currentPage + 1)}
className="size-11 md:size-7"
size="icon-sm"
variant="ghost"
>
<ChevronRight aria-hidden="true" />
</Button>
</div>
</footer>
)
}