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>
|
|||
|
|
);
|
|||
|
|
}
|