Files
juip-web/src/components/data-table.tsx

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