- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import type { ReactNode } from "react";
|
||
import { cn } from "./utils/cn";
|
||
import { Loading } from "./loading";
|
||
import { Empty } from "./empty";
|
||
|
||
/**
|
||
* DataTable - 通用数据表格
|
||
*
|
||
* 用途:列表数据的标准展示容器,内置加载态、空态、行交互。
|
||
* 复用 Loading / Empty 组件保持一致的降级 UI。
|
||
*
|
||
* @example
|
||
* const columns: Column<User>[] = [
|
||
* { key: "name", header: "姓名" },
|
||
* { key: "score", header: "分数", align: "right", render: (u) => u.score },
|
||
* ];
|
||
* <DataTable columns={columns} data={users} loading={isLoading} rowKey={(u) => u.id} />
|
||
*/
|
||
|
||
/** 列对齐方式 */
|
||
export type ColumnAlign = "left" | "center" | "right";
|
||
|
||
/** 列定义 */
|
||
export interface Column<T> {
|
||
/** 唯一标识(用于 key) */
|
||
key: string;
|
||
/** 表头内容 */
|
||
header: ReactNode;
|
||
/** 自定义单元格渲染;未提供时取 row[key] */
|
||
render?: (row: T) => ReactNode;
|
||
/** 自定义单元格类名 */
|
||
className?: string;
|
||
/** 对齐方式 */
|
||
align?: ColumnAlign;
|
||
/** 列宽(CSS 值,如 "200px" / "20%") */
|
||
width?: string;
|
||
}
|
||
|
||
export interface DataTableProps<T> {
|
||
/** 列定义 */
|
||
columns: Column<T>[];
|
||
/** 数据数组 */
|
||
data: T[];
|
||
/** 加载态;为 true 时显示 Loading */
|
||
loading?: boolean;
|
||
/** 空态自定义内容;未提供时使用默认 Empty */
|
||
empty?: ReactNode;
|
||
/** 行点击回调 */
|
||
onRowClick?: (row: T) => void;
|
||
/** 行 key 生成函数;未提供时使用索引 */
|
||
rowKey?: (row: T) => string;
|
||
/** 自定义类名 */
|
||
className?: string;
|
||
}
|
||
|
||
const ALIGN_CLASS: Record<ColumnAlign, string> = {
|
||
left: "text-left",
|
||
center: "text-center",
|
||
right: "text-right",
|
||
};
|
||
|
||
export function DataTable<T>({
|
||
columns,
|
||
data,
|
||
loading,
|
||
empty,
|
||
onRowClick,
|
||
rowKey,
|
||
className,
|
||
}: DataTableProps<T>): ReactNode {
|
||
if (loading) {
|
||
return <Loading lines={Math.max(columns.length, 3)} />;
|
||
}
|
||
|
||
if (data.length === 0) {
|
||
return empty ?? <Empty title="暂无数据" description="调整筛选条件后重试" />;
|
||
}
|
||
|
||
return (
|
||
<div className={cn("overflow-x-auto rounded-xl border", className)}>
|
||
<table className="w-full text-sm">
|
||
<thead className="bg-muted">
|
||
<tr className="border-b">
|
||
{columns.map((col) => (
|
||
<th
|
||
key={col.key}
|
||
style={col.width ? { width: col.width } : undefined}
|
||
className={cn(
|
||
"py-2 px-3 text-xs uppercase tracking-wide text-muted-foreground font-medium",
|
||
col.align ? ALIGN_CLASS[col.align] : "text-left",
|
||
)}
|
||
>
|
||
{col.header}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.map((row, index) => {
|
||
const key = rowKey ? rowKey(row) : String(index);
|
||
return (
|
||
<tr
|
||
key={key}
|
||
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
||
className={cn(
|
||
"border-b",
|
||
onRowClick && "cursor-pointer hover:bg-muted",
|
||
)}
|
||
>
|
||
{columns.map((col) => (
|
||
<td
|
||
key={col.key}
|
||
className={cn(
|
||
"py-2 px-3 text-foreground",
|
||
col.align ? ALIGN_CLASS[col.align] : "text-left",
|
||
col.className,
|
||
)}
|
||
>
|
||
{col.render ? col.render(row) : null}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|