186 lines
5.3 KiB
TypeScript
186 lines
5.3 KiB
TypeScript
import {
|
|
type AccessorKeyColumnDef,
|
|
type Column,
|
|
type ColumnDef,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table"
|
|
import { Loader } from "lucide-react"
|
|
import type { CSSProperties } from "react"
|
|
import { Pagination, type PaginationProps } from "@/components/ui/pagination"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
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?: {
|
|
root?: string
|
|
headRow?: string
|
|
dataRow?: string
|
|
}
|
|
serial?: boolean
|
|
}
|
|
|
|
export function DataTable<T extends Record<string, unknown>>(
|
|
props: DataTableProps<T>,
|
|
) {
|
|
const table = useReactTable({
|
|
data: props.data,
|
|
columns: [
|
|
{
|
|
id: "serial",
|
|
header: "#",
|
|
cell: ({ row }) => row.index + 1,
|
|
},
|
|
...props.columns,
|
|
],
|
|
getCoreRowModel: getCoreRowModel(),
|
|
manualPagination: true,
|
|
rowCount: props.pagination?.total,
|
|
state: {
|
|
pagination: props.pagination && {
|
|
pageIndex: props.pagination.page,
|
|
pageSize: props.pagination.size,
|
|
},
|
|
columnFilters: [],
|
|
},
|
|
initialState: {
|
|
columnPinning: {
|
|
left: props.columns
|
|
.map(column =>
|
|
(column.meta as Record<string, unknown>)?.pin === "left"
|
|
? column.id || (column as AccessorKeyColumnDef<T>).accessorKey
|
|
: undefined,
|
|
)
|
|
.filter(Boolean) as string[],
|
|
right: props.columns
|
|
.map(column =>
|
|
(column.meta as Record<string, unknown>)?.pin === "right"
|
|
? column.id || (column as AccessorKeyColumnDef<T>).accessorKey
|
|
: undefined,
|
|
)
|
|
.filter(Boolean) as string[],
|
|
},
|
|
},
|
|
})
|
|
|
|
const pinStyle = (column: Column<T>, header?: boolean) => {
|
|
const pinned = column.getIsPinned()
|
|
if (!pinned) return {}
|
|
return {
|
|
left: {
|
|
left: column.getStart(pinned),
|
|
boxShadow: header
|
|
? "inset -1px -1px var(--border)"
|
|
: "inset -1px 0 var(--border)",
|
|
} as CSSProperties,
|
|
right: {
|
|
right: column.getAfter(pinned),
|
|
boxShadow: header
|
|
? "inset 1px -1px var(--border)"
|
|
: "inset 1px 0 var(--border)",
|
|
} as CSSProperties,
|
|
}[pinned]
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex-auto flex flex-col gap-3 overflow-hidden",
|
|
props.classNames?.root,
|
|
)}
|
|
>
|
|
{/* 数据表 */}
|
|
<div className="flex-auto overflow-hidden relative">
|
|
<Table className="max-w-full max-h-full">
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map(group => (
|
|
<TableRow key={group.id}>
|
|
{group.headers.map(header => (
|
|
<TableHead
|
|
key={header.id}
|
|
colSpan={header.colSpan}
|
|
className="sticky top-0 bg-card z-20 shadow-[inset_0_-1px_var(--border)]"
|
|
style={pinStyle(header.column, true)}
|
|
>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext(),
|
|
)}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{props.status === "fail" ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={props.columns.length}
|
|
className="text-center text-fail"
|
|
>
|
|
加载失败
|
|
</TableCell>
|
|
</TableRow>
|
|
) : !props.data?.length ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={props.columns.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}
|
|
className="sticky bg-card z-10"
|
|
style={pinStyle(cell.column)}
|
|
>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext(),
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
{props.status === "load" && (
|
|
<div className="absolute inset-0 bg-white/10 backdrop-blur-xs flex items-center justify-center gap-2 transition z-50">
|
|
<Loader className="animate-spin" />
|
|
<span>加载中</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 分页器 */}
|
|
{props.pagination && (
|
|
<Pagination {...props.pagination} className="flex-none self-center" />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|