2025-12-29 18:01:16 +08:00
|
|
|
import {
|
2026-03-27 18:02:38 +08:00
|
|
|
type Column,
|
2025-12-29 18:01:16 +08:00
|
|
|
type ColumnDef,
|
|
|
|
|
flexRender,
|
|
|
|
|
getCoreRowModel,
|
2026-03-28 17:07:34 +08:00
|
|
|
getExpandedRowModel,
|
2025-12-29 18:01:16 +08:00
|
|
|
useReactTable,
|
|
|
|
|
} from "@tanstack/react-table"
|
|
|
|
|
import { Loader } from "lucide-react"
|
2026-03-27 18:02:38 +08:00
|
|
|
import { type CSSProperties, useCallback, useMemo } from "react"
|
2025-12-29 18:01:16 +08:00
|
|
|
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>[]
|
2026-04-01 13:14:28 +08:00
|
|
|
pagination?: PaginationProps
|
2025-12-29 18:01:16 +08:00
|
|
|
classNames?: {
|
2026-03-23 17:49:47 +08:00
|
|
|
root?: string
|
2025-12-29 18:01:16 +08:00
|
|
|
headRow?: string
|
|
|
|
|
dataRow?: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function DataTable<T extends Record<string, unknown>>(
|
|
|
|
|
props: DataTableProps<T>,
|
|
|
|
|
) {
|
|
|
|
|
const table = useReactTable({
|
|
|
|
|
data: props.data,
|
|
|
|
|
columns: props.columns,
|
|
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
|
manualPagination: true,
|
2026-04-01 13:14:28 +08:00
|
|
|
rowCount: props.pagination?.total,
|
2025-12-29 18:01:16 +08:00
|
|
|
state: {
|
2026-04-01 13:14:28 +08:00
|
|
|
pagination: props.pagination && {
|
2025-12-29 18:01:16 +08:00
|
|
|
pageIndex: props.pagination.page,
|
|
|
|
|
pageSize: props.pagination.size,
|
|
|
|
|
},
|
|
|
|
|
columnFilters: [],
|
|
|
|
|
},
|
2026-03-27 18:02:38 +08:00
|
|
|
initialState: {
|
|
|
|
|
columnPinning: {
|
|
|
|
|
left: props.columns
|
|
|
|
|
.map(column =>
|
|
|
|
|
column.meta?.pin === "left"
|
|
|
|
|
? column.id || column.accessorKey
|
|
|
|
|
: undefined,
|
|
|
|
|
)
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
right: props.columns
|
|
|
|
|
.map(column =>
|
|
|
|
|
column.meta?.pin === "right"
|
|
|
|
|
? column.id || column.accessorKey
|
|
|
|
|
: undefined,
|
|
|
|
|
)
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-12-29 18:01:16 +08:00
|
|
|
})
|
|
|
|
|
|
2026-03-27 18:02:38 +08:00
|
|
|
const pinStyle = (column: Column<T>) => {
|
|
|
|
|
const pinned = column.getIsPinned()
|
|
|
|
|
if (!pinned) return {}
|
|
|
|
|
return {
|
|
|
|
|
position: pinned ? ("sticky" as const) : undefined,
|
|
|
|
|
backgroundColor: "white",
|
|
|
|
|
zIndex: 1,
|
|
|
|
|
...{
|
|
|
|
|
left: {
|
|
|
|
|
left: column.getStart(pinned),
|
|
|
|
|
boxShadow: "inset 1px 0 var(--border)",
|
|
|
|
|
},
|
|
|
|
|
right: {
|
|
|
|
|
right: column.getAfter(pinned),
|
|
|
|
|
boxShadow: "inset 1px 0 var(--border)",
|
|
|
|
|
},
|
|
|
|
|
}[pinned],
|
|
|
|
|
} as CSSProperties
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 18:01:16 +08:00
|
|
|
return (
|
2026-03-23 17:49:47 +08:00
|
|
|
<div className={cn("flex flex-col gap-3", props.classNames?.root)}>
|
2025-12-29 18:01:16 +08:00
|
|
|
{/* 数据表 */}
|
|
|
|
|
<div className="rounded-md relative bg-card">
|
|
|
|
|
<TableRoot>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
{table.getHeaderGroups().map(group => (
|
|
|
|
|
<TableRow key={group.id}>
|
|
|
|
|
{group.headers.map(header => (
|
2026-03-27 18:02:38 +08:00
|
|
|
<TableHead
|
|
|
|
|
key={header.id}
|
|
|
|
|
colSpan={header.colSpan}
|
|
|
|
|
style={pinStyle(header.column)}
|
|
|
|
|
>
|
2025-12-29 18:01:16 +08:00
|
|
|
{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 => (
|
2026-03-27 18:02:38 +08:00
|
|
|
<TableCell key={cell.id} style={pinStyle(cell.column)}>
|
2025-12-29 18:01:16 +08:00
|
|
|
{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>
|
|
|
|
|
|
|
|
|
|
{/* 分页器 */}
|
2026-04-01 13:14:28 +08:00
|
|
|
{props.pagination && <Pagination {...props.pagination} />}
|
2025-12-29 18:01:16 +08:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|