134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
'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>
|
||
);
|
||
}
|