feat(web): 按原型调整响应式研究工作台
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user