import type { LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; import { Button } from "~/components/ui/button.tsx"; import { cn } from "~/lib/utils.ts"; import { EmptyState, ErrorState, LoadingState } from "./empty-state.tsx"; export type DataColumn = { /** 列 key(React key 用) */ key: string; header: ReactNode; cell: (row: T) => ReactNode; /** 窄屏隐藏该列,避免横向溢出 */ hideBelow?: "sm" | "md" | "lg"; align?: "left" | "right"; className?: string; }; const HIDE: Record["hideBelow"]>, string> = { sm: "hidden sm:table-cell", md: "hidden md:table-cell", lg: "hidden lg:table-cell", }; /** * 数据表:内置 loading / error / empty 三态与"加载更多"游标翻页。 * 外层自带 pg-slab 卡片(plain 时不带);表格在卡片内横向滚动,页面本身不溢出。 * * r.id} * loading={q.isPending} error={q.error} onRetry={q.refetch} * empty={{ title: "还没有记录" }} onRowClick={(r) => open(r.id)} * hasMore={!!next} onLoadMore={...} loadingMore={...} /> */ export function DataTable({ columns, rows, rowKey, loading = false, error, onRetry, empty, onRowClick, selectedKey, hasMore = false, onLoadMore, loadingMore = false, plain = false, className, }: { columns: DataColumn[]; rows: T[] | undefined; rowKey: (row: T) => string; loading?: boolean; error?: unknown; onRetry?: () => void; empty?: { title: string; description?: ReactNode; icon?: LucideIcon; action?: ReactNode }; onRowClick?: (row: T) => void; selectedKey?: string | null; hasMore?: boolean; onLoadMore?: () => void; loadingMore?: boolean; plain?: boolean; className?: string; }) { let body: ReactNode; if (loading && !rows) { body = ; } else if (error && !rows) { body = ; } else if (!rows || rows.length === 0) { body = ( ); } else { body = (
{columns.map((c) => ( ))} {rows.map((row) => { const key = rowKey(row); return ( onRowClick(row) : undefined} onKeyDown={ onRowClick ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRowClick(row); } } : undefined } tabIndex={onRowClick ? 0 : undefined} aria-selected={selectedKey != null ? selectedKey === key : undefined} className={cn( "border-b border-border/60 transition-colors duration-micro last:border-b-0", onRowClick && "cursor-pointer outline-none hover:bg-foreground/[0.04] focus-visible:bg-foreground/[0.06]", selectedKey === key && "bg-foreground/[0.05]", )} > {columns.map((c) => ( ))} ); })}
{c.header}
{c.cell(row)}
); } return (
{body} {rows && rows.length > 0 && (hasMore || error) ? (
{error ? ( {String((error as Error).message ?? error)} ) : null} {hasMore && onLoadMore ? ( ) : null}
) : null}
); }