192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import {
|
|
type ColumnDef,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table"
|
|
import { Loader } from "lucide-react"
|
|
import { useMemo } from "react"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import { Pagination, type PaginationProps } from "@/components/ui/pagination"
|
|
import {
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
Table as TableRoot,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export type DataTableProps<T> = {
|
|
data: T[]
|
|
status: "load" | "done" | "fail"
|
|
columns: ColumnDef<T>[]
|
|
pagination?: PaginationProps
|
|
classNames?: {
|
|
headRow?: string
|
|
dataRow?: string
|
|
}
|
|
enableSelection?: boolean
|
|
rowSelection?: Record<string, boolean>
|
|
onSelectionChange?: (selection: Record<string, boolean>) => void
|
|
getRowId?: (row: T, index: number) => string
|
|
stickyHeader?: boolean
|
|
maxHeight?: string
|
|
}
|
|
|
|
export default function DataTable<T extends Record<string, unknown>>(
|
|
props: DataTableProps<T>,
|
|
) {
|
|
// 动态注入复选框列
|
|
const finalColumns = useMemo<ColumnDef<T>[]>(() => {
|
|
if (!props.enableSelection) return props.columns
|
|
|
|
const selectColumn: ColumnDef<T> = {
|
|
id: "select",
|
|
size: 40,
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
header: ({ table }) => (
|
|
<Checkbox
|
|
checked={
|
|
table.getIsAllPageRowsSelected()
|
|
? true
|
|
: table.getIsSomePageRowsSelected()
|
|
? "indeterminate"
|
|
: false
|
|
}
|
|
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
|
|
aria-label="全选当前页"
|
|
className="rounded-none translate-y-0.5"
|
|
/>
|
|
),
|
|
cell: ({ row }) => (
|
|
<Checkbox
|
|
checked={row.getIsSelected()}
|
|
onCheckedChange={value => row.toggleSelected(!!value)}
|
|
aria-label="选择行"
|
|
className="rounded-none translate-y-0.5"
|
|
/>
|
|
),
|
|
}
|
|
|
|
return [selectColumn, ...props.columns]
|
|
}, [props.enableSelection, props.columns])
|
|
|
|
const table = useReactTable({
|
|
data: props.data,
|
|
columns: finalColumns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
manualPagination: true,
|
|
rowCount: props.pagination?.total ?? props.data.length,
|
|
// 行选择核心配置
|
|
enableRowSelection: props.enableSelection ?? false,
|
|
getRowId: props.getRowId,
|
|
onRowSelectionChange: updaterOrValue => {
|
|
if (!props.onSelectionChange) return
|
|
|
|
const newSelection =
|
|
typeof updaterOrValue === "function"
|
|
? updaterOrValue(props.rowSelection ?? {})
|
|
: updaterOrValue
|
|
|
|
props.onSelectionChange(newSelection)
|
|
},
|
|
state: {
|
|
pagination: {
|
|
pageIndex: props.pagination?.page ?? 0,
|
|
pageSize: props.pagination?.size ?? props.data.length,
|
|
},
|
|
columnFilters: [],
|
|
...(props.rowSelection !== undefined && {
|
|
rowSelection: props.rowSelection,
|
|
}),
|
|
},
|
|
autoResetPageIndex: false,
|
|
})
|
|
|
|
return (
|
|
<>
|
|
{/* 数据表 */}
|
|
<div className="rounded-md relative bg-card">
|
|
<TableRoot
|
|
containerClassName={
|
|
props.stickyHeader
|
|
? cn("overflow-y-auto", props.maxHeight ?? "max-h-[600px]")
|
|
: undefined
|
|
}
|
|
>
|
|
<TableHeader
|
|
className={
|
|
props.stickyHeader ? "sticky top-0 z-10 bg-card" : undefined
|
|
}
|
|
>
|
|
{table.getHeaderGroups().map(group => (
|
|
<TableRow key={group.id} className={props.classNames?.headRow}>
|
|
{group.headers.map(header => (
|
|
<TableHead key={header.id} colSpan={header.colSpan}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext(),
|
|
)}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{props.status === "fail" ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={finalColumns.length}
|
|
className="text-center text-fail"
|
|
>
|
|
加载失败
|
|
</TableCell>
|
|
</TableRow>
|
|
) : !props.data?.length ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={finalColumns.length}
|
|
className="text-center"
|
|
>
|
|
暂无数据
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
table.getRowModel().rows.map(row => (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && "selected"}
|
|
className={cn("h-14", props.classNames?.dataRow)}
|
|
>
|
|
{row.getVisibleCells().map(cell => (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext(),
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</TableRoot>
|
|
{props.status === "load" && (
|
|
<div className="absolute inset-0 bg-white/10 backdrop-blur-xs flex items-center justify-center gap-2 transition">
|
|
<Loader className="animate-spin" />
|
|
<span>加载中</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 分页器 */}
|
|
{props.pagination && <Pagination {...props.pagination} />}
|
|
</>
|
|
)
|
|
}
|