193 lines
5.7 KiB
TypeScript
193 lines
5.7 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: {
|
||
// 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} />}
|
||
</>
|
||
)
|
||
}
|