Files
Edu/packages/ui-components/src/data-table.tsx
SpecialX 9cedf0c437 feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- 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 路由生成成功
2026-07-17 16:10:05 +08:00

130 lines
3.6 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 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>
);
}