2025-12-29 18:01:16 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
ChevronLeftIcon,
|
|
|
|
|
|
ChevronRightIcon,
|
|
|
|
|
|
MoreHorizontalIcon,
|
|
|
|
|
|
} from "lucide-react"
|
|
|
|
|
|
import type * as React from "react"
|
|
|
|
|
|
import { useEffect, useState } from "react"
|
|
|
|
|
|
|
|
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from "./select"
|
|
|
|
|
|
|
|
|
|
|
|
export interface PaginationProps {
|
|
|
|
|
|
page: number
|
|
|
|
|
|
size: number
|
|
|
|
|
|
total: number
|
|
|
|
|
|
sizeOptions?: number[]
|
|
|
|
|
|
onPageChange?: (page: number) => void
|
|
|
|
|
|
onSizeChange?: (size: number) => void
|
|
|
|
|
|
className?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Pagination({
|
|
|
|
|
|
page,
|
|
|
|
|
|
size,
|
|
|
|
|
|
total,
|
|
|
|
|
|
sizeOptions = [10, 20, 50, 100],
|
|
|
|
|
|
onPageChange,
|
|
|
|
|
|
onSizeChange,
|
|
|
|
|
|
className,
|
|
|
|
|
|
}: PaginationProps) {
|
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(page)
|
|
|
|
|
|
const totalPages = Math.ceil(total / size)
|
|
|
|
|
|
|
|
|
|
|
|
// 同步外部 page 变化
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setCurrentPage(page)
|
|
|
|
|
|
}, [page])
|
|
|
|
|
|
|
|
|
|
|
|
// 分页器逻辑
|
|
|
|
|
|
const generatePaginationItems = () => {
|
|
|
|
|
|
// 最多显示7个页码,其余用省略号
|
|
|
|
|
|
const SIBLINGS = 1 // 当前页左右各显示的页码数
|
|
|
|
|
|
const DOTS = -1 // 省略号标记
|
|
|
|
|
|
|
|
|
|
|
|
if (totalPages <= 7) {
|
|
|
|
|
|
// 总页数少于7,全部显示
|
|
|
|
|
|
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 是否需要显示左边的省略号
|
|
|
|
|
|
const showLeftDots = currentPage > 2 + SIBLINGS
|
|
|
|
|
|
|
|
|
|
|
|
// 是否需要显示右边的省略号
|
|
|
|
|
|
const showRightDots = currentPage < totalPages - (2 + SIBLINGS)
|
|
|
|
|
|
|
|
|
|
|
|
if (showLeftDots && showRightDots) {
|
|
|
|
|
|
// 两边都有省略号
|
|
|
|
|
|
const leftSiblingIndex = Math.max(currentPage - SIBLINGS, 1)
|
|
|
|
|
|
const rightSiblingIndex = Math.min(currentPage + SIBLINGS, totalPages)
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
1,
|
|
|
|
|
|
DOTS,
|
|
|
|
|
|
...Array.from(
|
|
|
|
|
|
{ length: rightSiblingIndex - leftSiblingIndex + 1 },
|
|
|
|
|
|
(_, i) => leftSiblingIndex + i,
|
|
|
|
|
|
),
|
|
|
|
|
|
DOTS,
|
|
|
|
|
|
totalPages,
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!showLeftDots && showRightDots) {
|
|
|
|
|
|
// 只有右边有省略号
|
|
|
|
|
|
return [
|
|
|
|
|
|
...Array.from({ length: 3 + SIBLINGS * 2 }, (_, i) => i + 1),
|
|
|
|
|
|
DOTS,
|
|
|
|
|
|
totalPages,
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showLeftDots && !showRightDots) {
|
|
|
|
|
|
// 只有左边有省略号
|
|
|
|
|
|
return [
|
|
|
|
|
|
1,
|
|
|
|
|
|
DOTS,
|
|
|
|
|
|
...Array.from(
|
|
|
|
|
|
{ length: 3 + SIBLINGS * 2 },
|
|
|
|
|
|
(_, i) => totalPages - (3 + SIBLINGS * 2) + i + 1,
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePageChange = (newPage: number) => {
|
|
|
|
|
|
if (newPage < 1 || newPage > totalPages || newPage === currentPage) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setCurrentPage(newPage)
|
|
|
|
|
|
onPageChange?.(newPage)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handlePageSizeChange = (newSize: string) => {
|
|
|
|
|
|
const parsedSize = parseInt(newSize, 10)
|
|
|
|
|
|
if (onSizeChange) {
|
|
|
|
|
|
onSizeChange(parsedSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const paginationItems = generatePaginationItems()
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-26 15:27:52 +08:00
|
|
|
|
<div className={`flex flex-wrap items-center gap-4 ${className || ""}`}>
|
2025-12-29 18:01:16 +08:00
|
|
|
|
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
|
共 {total} 条记录,每页
|
|
|
|
|
|
<Select value={size.toString()} onValueChange={handlePageSizeChange}>
|
2026-03-26 15:27:52 +08:00
|
|
|
|
<SelectTrigger className="h-8 w-20">
|
2025-12-29 18:01:16 +08:00
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{sizeOptions.map(option => (
|
|
|
|
|
|
<SelectItem key={option} value={option.toString()}>
|
|
|
|
|
|
{option}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
条
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<PaginationLayout>
|
|
|
|
|
|
<PaginationContent>
|
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
|
<PaginationPrevious
|
|
|
|
|
|
onClick={() => handlePageChange(currentPage - 1)}
|
|
|
|
|
|
className={
|
|
|
|
|
|
currentPage === 1 ? "opacity-50 pointer-events-none" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
|
|
|
|
|
|
{paginationItems.map((pageNum, index) => {
|
|
|
|
|
|
if (pageNum === -1) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PaginationItem key={`dots-${index}`}>
|
|
|
|
|
|
<PaginationEllipsis />
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PaginationItem key={pageNum}>
|
|
|
|
|
|
<PaginationLink
|
|
|
|
|
|
isActive={pageNum === currentPage}
|
|
|
|
|
|
onClick={() => handlePageChange(pageNum)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pageNum}
|
|
|
|
|
|
</PaginationLink>
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
|
<PaginationNext
|
|
|
|
|
|
onClick={() => handlePageChange(currentPage + 1)}
|
|
|
|
|
|
className={
|
|
|
|
|
|
currentPage === totalPages
|
|
|
|
|
|
? "opacity-50 pointer-events-none"
|
|
|
|
|
|
: ""
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
</PaginationContent>
|
|
|
|
|
|
</PaginationLayout>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationLayout({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.ComponentProps<"nav">) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<nav
|
|
|
|
|
|
aria-label="pagination"
|
|
|
|
|
|
data-slot="pagination"
|
|
|
|
|
|
className={cn("flex-none", className)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationContent({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.ComponentProps<"ul">) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ul
|
|
|
|
|
|
data-slot="pagination-content"
|
|
|
|
|
|
className={cn("flex flex-row items-center gap-1", className)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
|
|
|
|
|
return <li data-slot="pagination-item" {...props} />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type PaginationLinkProps = {
|
|
|
|
|
|
isActive?: boolean
|
|
|
|
|
|
} & React.ComponentProps<"a">
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationLink({
|
|
|
|
|
|
className,
|
|
|
|
|
|
isActive,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: PaginationLinkProps) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<a
|
|
|
|
|
|
aria-current={isActive ? "page" : undefined}
|
|
|
|
|
|
data-slot="pagination-link"
|
|
|
|
|
|
data-active={isActive}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-9 w-9 rounded-md border border-input hover:bg-secondary hover:text-secondary-foreground",
|
|
|
|
|
|
`bg-card`,
|
|
|
|
|
|
isActive &&
|
|
|
|
|
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
|
|
|
|
|
|
className,
|
|
|
|
|
|
)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationPrevious({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.ComponentProps<typeof PaginationLink>) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PaginationLink
|
|
|
|
|
|
aria-label="Go to previous page"
|
|
|
|
|
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronLeftIcon />
|
|
|
|
|
|
</PaginationLink>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationNext({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.ComponentProps<typeof PaginationLink>) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PaginationLink
|
|
|
|
|
|
aria-label="Go to next page"
|
|
|
|
|
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronRightIcon />
|
|
|
|
|
|
</PaginationLink>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PaginationEllipsis({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.ComponentProps<"span">) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden
|
|
|
|
|
|
data-slot="pagination-ellipsis"
|
|
|
|
|
|
className={cn("flex size-9 items-center justify-center", className)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MoreHorizontalIcon className="size-4" />
|
|
|
|
|
|
<span className="sr-only">More pages</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export {
|
|
|
|
|
|
Pagination,
|
|
|
|
|
|
PaginationLayout,
|
|
|
|
|
|
PaginationContent,
|
|
|
|
|
|
PaginationLink,
|
|
|
|
|
|
PaginationItem,
|
|
|
|
|
|
PaginationPrevious,
|
|
|
|
|
|
PaginationNext,
|
|
|
|
|
|
PaginationEllipsis,
|
|
|
|
|
|
}
|