Files
juip-web/src/components/data-table.tsx
2026-09-11 17:06:29 +08:00

193 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: {
// Pagination 组件页码为 1 起始,tanstack 内部为 0 起始
pageIndex: (props.pagination?.page ?? 1) - 1,
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} />}
</>
)
}