优化登录流程,添加白名单管理功能,调整页面布局与样式

This commit is contained in:
2025-04-07 15:42:09 +08:00
parent a16faadaab
commit a2c18a1be8
21 changed files with 1388 additions and 102 deletions

View File

@@ -0,0 +1,284 @@
'use client'
import * as React from 'react'
import {useState, useEffect} from 'react'
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from 'lucide-react'
import {merge} from '@/lib/utils'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './select'
export interface PaginationProps {
page: number
size: number
total: number
pageSizeOptions?: number[]
onPageChange: (page: number) => void
onPageSizeChange?: (size: number) => void
className?: string
}
function Pagination({
page,
size,
total,
pageSizeOptions = [10, 20, 50, 100],
onPageChange,
onPageSizeChange,
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 (onPageSizeChange) {
onPageSizeChange(parsedSize)
}
}
const paginationItems = generatePaginationItems()
return (
<div className={`flex items-center justify-between gap-4 ${className || ''}`}>
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
{total}
<Select
value={size.toString()}
onValueChange={handlePageSizeChange}
>
<SelectTrigger className="h-8 w-20">
<SelectValue/>
</SelectTrigger>
<SelectContent>
{pageSizeOptions.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
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={merge('flex-none', className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<'ul'>) {
return (
<ul
data-slot="pagination-content"
className={merge('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={merge(
'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-accent hover:text-accent-foreground',
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={merge('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={merge('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={merge('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,
}