feat: api client, auth store, login, authed shell, data-table + CRUD kit
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from '@tanstack/react-table'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import type { Pagination } from '@/api/types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
interface DataTableProps<T> {
|
||||
columns: ColumnDef<T, any>[]
|
||||
data: T[]
|
||||
pagination?: Pagination
|
||||
loading?: boolean
|
||||
emptyText?: string
|
||||
onPageChange?: (page: number) => void
|
||||
onPageSizeChange?: (pageSize: number) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-paginated table bound to the backend's Pagination envelope
|
||||
* (page / page_size request params).
|
||||
*/
|
||||
export function DataTable<T>({
|
||||
columns,
|
||||
data,
|
||||
pagination,
|
||||
loading = false,
|
||||
emptyText = '暂无数据',
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}: DataTableProps<T>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="overflow-x-auto rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{columns.map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full max-w-32" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : table.getRowModel().rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground">
|
||||
{emptyText}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div>共 {pagination.total_count} 条</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{onPageSizeChange && (
|
||||
<Select
|
||||
value={String(pagination.page_size)}
|
||||
onValueChange={(value) => onPageSizeChange(Number(value))}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-28">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 50, 100].map((size) => (
|
||||
<SelectItem key={size} value={String(size)}>
|
||||
{size} 条 / 页
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
disabled={!pagination.prev_page || loading}
|
||||
onClick={() => onPageChange?.(pagination.prev_page ?? 1)}
|
||||
aria-label="上一页"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
<span className="min-w-16 text-center tabular-nums">
|
||||
{pagination.current_page} / {Math.max(pagination.total_pages, 1)}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
disabled={!pagination.next_page || loading}
|
||||
onClick={() => onPageChange?.(pagination.next_page ?? 1)}
|
||||
aria-label="下一页"
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user