Files
planet/frontend/src/admin-next/components/data-table/DataTable.tsx
rayd1o 65e6a96c0d
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
release: bump version to 0.65.0
2026-05-21 05:41:49 +08:00

192 lines
7.2 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 {
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>
)
}