website-01/components/admin/PaginationTable.tsx

134 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-06-22 14:43:46 +08:00
'use client';
import { ReactNode, useState, useEffect } from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface Column<T> {
key: string;
title: string;
width?: number | string;
render?: (row: T, index: number) => ReactNode;
className?: string;
}
export interface PaginationTableProps<T> {
columns: Column<T>[];
rows: T[];
total: number;
page: number;
pageSize: number;
loading?: boolean;
onPageChange: (page: number) => void;
rowKey?: (row: T, index: number) => string;
emptyText?: string;
className?: string;
}
export function PaginationTable<T>({
columns,
rows,
total,
page,
pageSize,
loading,
onPageChange,
rowKey,
emptyText = '暂无数据',
className,
}: PaginationTableProps<T>) {
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const [cur, setCur] = useState(page);
useEffect(() => setCur(page), [page]);
const go = (p: number) => {
const target = Math.min(Math.max(1, p), totalPages);
setCur(target);
onPageChange(target);
};
const start = total === 0 ? 0 : (cur - 1) * pageSize + 1;
const end = Math.min(cur * pageSize, total);
return (
<div className={cn('w-full', className)}>
<div className="rounded-md border border-gray-200">
<Table>
<TableHeader>
<TableRow className="bg-gray-50">
{columns.map((c) => (
<TableHead key={c.key} style={{ width: c.width }} className={c.className}>
{c.title}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-32">
<div className="flex items-center justify-center gap-2 text-gray-400">
<Loader2 className="h-5 w-5 animate-spin" />
<span className="text-sm"></span>
</div>
</TableCell>
</TableRow>
) : rows.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-32 text-center text-gray-400">
{emptyText}
</TableCell>
</TableRow>
) : (
rows.map((row, idx) => (
<TableRow key={rowKey ? rowKey(row, idx) : String(idx)}>
{columns.map((c) => (
<TableCell key={c.key} className={c.className}>
{c.render ? c.render(row, idx) : String((row as Record<string, unknown>)[c.key] ?? '')}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="mt-3 flex flex-col items-center justify-between gap-2 sm:flex-row">
<p className="text-xs text-gray-500">
{total} {start}-{end}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={cur <= 1}
onClick={() => go(cur - 1)}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm text-gray-600">
{cur} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={cur >= totalPages}
onClick={() => go(cur + 1)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}