Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
ci / backend (pull_request) Has been cancelled
ci / frontend (pull_request) Has been cancelled
ci / delivery (pull_request) Has been cancelled
192 lines
7.2 KiB
TypeScript
192 lines
7.2 KiB
TypeScript
import {
|
||
flexRender,
|
||
getCoreRowModel,
|
||
getSortedRowModel,
|
||
useReactTable,
|
||
type ColumnDef,
|
||
type SortingState,
|
||
} from '@tanstack/react-table'
|
||
import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react'
|
||
import { useMemo, useState } from 'react'
|
||
import TableScrollRegion from '../../../components/Scrollbar/TableScrollRegion'
|
||
import { Button } from '../ui/button'
|
||
|
||
interface DataTableProps<TData> {
|
||
columns: Array<ColumnDef<TData>>
|
||
data: TData[]
|
||
getRowId?: (row: TData, index: number) => string
|
||
getRowClassName?: (row: TData) => string | undefined
|
||
selection?: {
|
||
selectedRowIds: Set<string>
|
||
onToggleAllVisible: (rowIds: string[]) => void
|
||
onToggleRow: (rowId: string, row: TData) => void
|
||
getCheckboxLabel?: (row: TData) => string
|
||
isRowSelectable?: (row: TData) => boolean
|
||
}
|
||
loading?: boolean
|
||
emptyText?: string
|
||
className?: string
|
||
footer?: React.ReactNode
|
||
onRowClick?: (row: TData) => void
|
||
}
|
||
|
||
export function DataTable<TData>({
|
||
columns,
|
||
data,
|
||
getRowId,
|
||
getRowClassName,
|
||
selection,
|
||
loading = false,
|
||
emptyText = '暂无数据',
|
||
className = '',
|
||
footer,
|
||
onRowClick,
|
||
}: DataTableProps<TData>) {
|
||
const [sorting, setSorting] = useState<SortingState>([])
|
||
const memoizedColumns = useMemo(() => columns, [columns])
|
||
|
||
const table = useReactTable({
|
||
data,
|
||
columns: memoizedColumns,
|
||
state: { sorting },
|
||
onSortingChange: setSorting,
|
||
getRowId,
|
||
getCoreRowModel: getCoreRowModel(),
|
||
getSortedRowModel: getSortedRowModel(),
|
||
})
|
||
const visibleSelectableRows = selection
|
||
? table.getRowModel().rows.filter((row) => selection.isRowSelectable?.(row.original) ?? true)
|
||
: []
|
||
const visibleSelectableIds = visibleSelectableRows.map((row) => row.id)
|
||
const allVisibleSelected = visibleSelectableIds.length > 0 && visibleSelectableIds.every((rowId) => selection?.selectedRowIds.has(rowId))
|
||
const someVisibleSelected = visibleSelectableIds.some((rowId) => selection?.selectedRowIds.has(rowId))
|
||
const columnCount = columns.length + (selection ? 1 : 0)
|
||
|
||
return (
|
||
<div className={`an-data-table ${className}`}>
|
||
<TableScrollRegion className="an-data-table__scroll" targetSelector=".an-data-table__viewport">
|
||
<div className="an-data-table__viewport">
|
||
<table className="an-data-table__table">
|
||
<thead>
|
||
{table.getHeaderGroups().map((headerGroup) => (
|
||
<tr key={headerGroup.id}>
|
||
{selection ? (
|
||
<th className="an-data-table__selection-cell">
|
||
<input
|
||
type="checkbox"
|
||
aria-label="选择当前可见数据"
|
||
checked={allVisibleSelected}
|
||
disabled={!visibleSelectableIds.length}
|
||
ref={(element) => {
|
||
if (element) element.indeterminate = someVisibleSelected && !allVisibleSelected
|
||
}}
|
||
onChange={() => selection.onToggleAllVisible(visibleSelectableIds)}
|
||
/>
|
||
</th>
|
||
) : null}
|
||
{headerGroup.headers.map((header) => {
|
||
const sorted = header.column.getIsSorted()
|
||
const stickyEnd = header.column.id === 'actions' || header.column.id === 'action'
|
||
return (
|
||
<th key={header.id} style={{ width: header.getSize() }} data-sticky-end={stickyEnd || undefined}>
|
||
{header.isPlaceholder ? null : (
|
||
<button
|
||
type="button"
|
||
className="an-data-table__sort"
|
||
disabled={!header.column.getCanSort()}
|
||
onClick={header.column.getToggleSortingHandler()}
|
||
>
|
||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||
{header.column.getCanSort() ? (
|
||
sorted === 'asc' ? <ArrowUp size={14} /> : sorted === 'desc' ? <ArrowDown size={14} /> : <ChevronsUpDown size={14} />
|
||
) : null}
|
||
</button>
|
||
)}
|
||
</th>
|
||
)
|
||
})}
|
||
</tr>
|
||
))}
|
||
</thead>
|
||
<tbody>
|
||
{loading ? (
|
||
<tr>
|
||
<td colSpan={columnCount}>
|
||
<div className="an-data-table__state">
|
||
<span className="an-spinner" />
|
||
加载中
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
) : table.getRowModel().rows.length > 0 ? (
|
||
table.getRowModel().rows.map((row) => (
|
||
<tr
|
||
key={row.id}
|
||
className={getRowClassName?.(row.original)}
|
||
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
||
data-clickable={onRowClick ? 'true' : undefined}
|
||
>
|
||
{selection ? (
|
||
<td className="an-data-table__selection-cell">
|
||
<input
|
||
type="checkbox"
|
||
aria-label={selection.getCheckboxLabel?.(row.original) || '选择行'}
|
||
checked={selection.selectedRowIds.has(row.id)}
|
||
disabled={selection.isRowSelectable ? !selection.isRowSelectable(row.original) : false}
|
||
onClick={(event) => event.stopPropagation()}
|
||
onChange={() => selection.onToggleRow(row.id, row.original)}
|
||
/>
|
||
</td>
|
||
) : null}
|
||
{row.getVisibleCells().map((cell) => (
|
||
<td key={cell.id} data-sticky-end={cell.column.id === 'actions' || cell.column.id === 'action' || undefined}>
|
||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))
|
||
) : (
|
||
<tr>
|
||
<td colSpan={columnCount}>
|
||
<div className="an-data-table__state">{emptyText}</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</TableScrollRegion>
|
||
{footer ? <div className="an-data-table__footer">{footer}</div> : null}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function DataTablePager({
|
||
page,
|
||
pageSize,
|
||
total,
|
||
onPageChange,
|
||
}: {
|
||
page: number
|
||
pageSize: number
|
||
total: number
|
||
onPageChange: (page: number) => void
|
||
}) {
|
||
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||
return (
|
||
<div className="an-data-table__pager">
|
||
<span>
|
||
第 {page} / {totalPages} 页,共 {total.toLocaleString()} 条
|
||
</span>
|
||
<div className="an-data-table__pager-actions">
|
||
<Button size="sm" variant="subtle" disabled={page <= 1} onClick={() => onPageChange(page - 1)}>
|
||
上一页
|
||
</Button>
|
||
<Button size="sm" variant="subtle" disabled={page >= totalPages} onClick={() => onPageChange(page + 1)}>
|
||
下一页
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|