Files
zhixing-system/zhixing-web/src/shared/ui/pagination.tsx
T
2026-08-10 10:07:55 +08:00

139 lines
4.4 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 { 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>
)
}