@meta-1/design 0.0.199 → 0.0.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/assets/locales/en-us.ts +4 -0
- package/src/assets/locales/zh-cn.ts +4 -0
- package/src/assets/locales/zh-tw.ts +4 -0
- package/src/assets/style/theme.css +72 -21
- package/src/components/ui/button.tsx +28 -18
- package/src/components/uix/action/index.tsx +7 -5
- package/src/components/uix/action/style.css +21 -0
- package/src/components/uix/alert/index.tsx +5 -5
- package/src/components/uix/alert-dialog/index.tsx +56 -16
- package/src/components/uix/badge/index.tsx +8 -2
- package/src/components/uix/button/index.tsx +7 -5
- package/src/components/uix/card/index.tsx +7 -4
- package/src/components/uix/checkbox-group/index.tsx +3 -4
- package/src/components/uix/combo-select/index.tsx +99 -129
- package/src/components/uix/command/index.tsx +126 -0
- package/src/components/uix/data-grid/index.tsx +625 -0
- package/src/components/uix/data-grid/overlays.tsx +26 -0
- package/src/components/uix/data-grid/style.css +6 -0
- package/src/components/uix/data-table/index.tsx +10 -7
- package/src/components/uix/date-picker/index.tsx +55 -46
- package/src/components/uix/date-range-picker/index.tsx +116 -36
- package/src/components/uix/dialog/index.tsx +5 -5
- package/src/components/uix/dropdown/index.tsx +1 -1
- package/src/components/uix/input/index.tsx +23 -0
- package/src/components/uix/message/index.tsx +4 -4
- package/src/components/uix/pagination/index.tsx +17 -31
- package/src/components/uix/popover/index.tsx +30 -0
- package/src/components/uix/radio-group/index.tsx +2 -2
- package/src/components/uix/select/index.tsx +6 -2
- package/src/components/uix/sheet/index.tsx +76 -0
- package/src/components/uix/tabs/index.tsx +38 -0
- package/src/components/uix/tags-input/index.tsx +22 -3
- package/src/components/uix/textarea/index.tsx +23 -0
- package/src/components/uix/tooltip/index.tsx +5 -4
- package/src/components/uix/tree/style.css +1 -0
- package/src/components/uix/tree-select/index.tsx +59 -64
- package/src/components/uix/uploader/index.tsx +1 -1
- package/src/components/uix/value-formatter/index.tsx +40 -46
- package/src/index.ts +19 -36
- package/src/lib/utils.ts +50 -1
- package/src/components/uix/breadcrumbs/index.tsx +0 -38
- package/src/components/uix/space/index.tsx +0 -24
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: DataGrid props
|
|
2
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { DotsHorizontalIcon, LayoutIcon } from "@radix-ui/react-icons";
|
|
4
|
+
import {
|
|
5
|
+
AllCommunityModule,
|
|
6
|
+
type ColDef,
|
|
7
|
+
colorSchemeDark,
|
|
8
|
+
colorSchemeLight,
|
|
9
|
+
type GridApi,
|
|
10
|
+
type GridReadyEvent,
|
|
11
|
+
ModuleRegistry,
|
|
12
|
+
themeBalham,
|
|
13
|
+
} from "ag-grid-community";
|
|
14
|
+
import { AgGridReact } from "ag-grid-react";
|
|
15
|
+
import get from "lodash/get";
|
|
16
|
+
import isArray from "lodash/isArray";
|
|
17
|
+
import isFunction from "lodash/isFunction";
|
|
18
|
+
import isString from "lodash/isString";
|
|
19
|
+
import isUndefined from "lodash/isUndefined";
|
|
20
|
+
import { useTheme } from "next-themes";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
Action,
|
|
24
|
+
cn,
|
|
25
|
+
Dropdown,
|
|
26
|
+
type DropdownMenuItemProps,
|
|
27
|
+
Filters,
|
|
28
|
+
type FiltersProps,
|
|
29
|
+
type Formatters,
|
|
30
|
+
generateColumnStorageKey,
|
|
31
|
+
Pagination,
|
|
32
|
+
type PaginationProps,
|
|
33
|
+
Spin,
|
|
34
|
+
ValueFormatter,
|
|
35
|
+
} from "@meta-1/design";
|
|
36
|
+
import { UIXContext } from "@meta-1/design/components/uix/config-provider";
|
|
37
|
+
import { LoadingOverlay, NoRowsOverlay } from "./overlays";
|
|
38
|
+
|
|
39
|
+
import "./style.css";
|
|
40
|
+
|
|
41
|
+
ModuleRegistry.registerModules([AllCommunityModule]);
|
|
42
|
+
|
|
43
|
+
export interface StickyColumnProps {
|
|
44
|
+
key: string;
|
|
45
|
+
position: "left" | "right";
|
|
46
|
+
size: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DataGridColumn<TData> = Partial<ColDef<TData>> & {
|
|
50
|
+
/** 列的 ID,如果不提供则使用 accessorKey 或 field */
|
|
51
|
+
id?: string;
|
|
52
|
+
/** 数据字段路径,支持点号分隔的嵌套路径,如 "user.name" */
|
|
53
|
+
accessorKey?: string;
|
|
54
|
+
/** 列标题,可以是字符串或返回 React 元素的函数 */
|
|
55
|
+
header?: string | ((params: { column: any; header: any; table: any }) => React.ReactNode);
|
|
56
|
+
/** 自定义单元格渲染函数 */
|
|
57
|
+
cell?: (params: {
|
|
58
|
+
getValue: () => any;
|
|
59
|
+
row: { original: TData; getValue: (key: string) => any };
|
|
60
|
+
column: any;
|
|
61
|
+
table: any;
|
|
62
|
+
}) => React.ReactNode;
|
|
63
|
+
/** 是否启用排序 */
|
|
64
|
+
enableSorting?: boolean;
|
|
65
|
+
/** 是否启用调整大小 */
|
|
66
|
+
enableResizing?: boolean;
|
|
67
|
+
/** 是否启用隐藏 */
|
|
68
|
+
enableHiding?: boolean;
|
|
69
|
+
/** 列宽 */
|
|
70
|
+
size?: number;
|
|
71
|
+
/** 最小列宽 */
|
|
72
|
+
minSize?: number;
|
|
73
|
+
/** 最大列宽 */
|
|
74
|
+
maxSize?: number;
|
|
75
|
+
/** 单元格 CSS 类名 */
|
|
76
|
+
className?: string;
|
|
77
|
+
/** 格式化函数数组,按顺序应用 */
|
|
78
|
+
formatters?: Formatters;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface DataGridProps<TData> {
|
|
82
|
+
columns: DataGridColumn<TData>[];
|
|
83
|
+
data: TData[];
|
|
84
|
+
showColumnVisibility?: boolean;
|
|
85
|
+
stickyColumns?: StickyColumnProps[];
|
|
86
|
+
checkbox?: boolean;
|
|
87
|
+
onRowSelectionChange?: (selectedRows: TData[]) => void;
|
|
88
|
+
rowActions?: DropdownMenuItemProps[] | ((cell: TData) => DropdownMenuItemProps[]);
|
|
89
|
+
onRowActionClick?: (item: DropdownMenuItemProps, row: TData) => void;
|
|
90
|
+
loading?: boolean;
|
|
91
|
+
load?: (params?: unknown) => Promise<unknown> | unknown;
|
|
92
|
+
filter?: FiltersProps;
|
|
93
|
+
pagination?: PaginationProps | boolean;
|
|
94
|
+
empty?: string;
|
|
95
|
+
showHeader?: boolean;
|
|
96
|
+
onRowClick?: (row: TData) => void;
|
|
97
|
+
/** 表格最大高度,设置后内容区将滚动,表头固定。支持 CSS 单位如 '500px', '50vh' 等 */
|
|
98
|
+
maxHeight?: string | number;
|
|
99
|
+
/** 获取行的唯一 ID,用于 ag-grid 的 getRowId */
|
|
100
|
+
getRowId?: (data: TData) => string;
|
|
101
|
+
/** 行高,可以是固定数值(像素)或函数返回动态高度 */
|
|
102
|
+
rowHeight?: number | ((params: { data: TData; node?: any }) => number | undefined);
|
|
103
|
+
/** 是否启用行高自适应,开启后 rowHeight 无效,行高将根据内容自动调整 */
|
|
104
|
+
autoRowHeight?: boolean;
|
|
105
|
+
/** 操作列单元格的 CSS 类名 */
|
|
106
|
+
actionsCellClassName?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const LOCAL_STORAGE_PREFIX = "datagrid_columns_";
|
|
110
|
+
|
|
111
|
+
const saveColumnVisibility = (storageKey: string, visibility: Record<string, boolean>) => {
|
|
112
|
+
try {
|
|
113
|
+
localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${storageKey}`, JSON.stringify(visibility));
|
|
114
|
+
} catch (_error) {
|
|
115
|
+
// console.warn('Failed to save column visibility to localStorage:', error)
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const loadColumnVisibility = (storageKey: string): Record<string, boolean> => {
|
|
120
|
+
try {
|
|
121
|
+
const stored = localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${storageKey}`);
|
|
122
|
+
return stored ? JSON.parse(stored) : {};
|
|
123
|
+
} catch (_error) {
|
|
124
|
+
// console.warn('Failed to load column visibility from localStorage:', error)
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const hasActions = (rowActions: DropdownMenuItemProps[] | ((cell: any) => DropdownMenuItemProps[])) => {
|
|
130
|
+
if (isFunction(rowActions)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (isArray(rowActions)) {
|
|
134
|
+
return !!rowActions.length;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// 将 DataGridColumn 转换为 ag-grid 的 ColDef
|
|
140
|
+
function convertColumnDefToAgGrid<TData>(
|
|
141
|
+
columnDef: DataGridColumn<TData>,
|
|
142
|
+
stickyColumns: StickyColumnProps[],
|
|
143
|
+
): ColDef<TData> {
|
|
144
|
+
const accessorKey = columnDef.accessorKey;
|
|
145
|
+
const colId =
|
|
146
|
+
columnDef.id || columnDef.colId || (typeof accessorKey === "string" ? accessorKey.replace(/\./g, "_") : undefined);
|
|
147
|
+
|
|
148
|
+
// 检查是否是粘性列
|
|
149
|
+
// 支持通过 id、accessorKey 或 colId 匹配
|
|
150
|
+
const stickyCol = stickyColumns.find((sc) => {
|
|
151
|
+
const key = sc.key;
|
|
152
|
+
// 直接匹配 colId
|
|
153
|
+
if (key === colId) return true;
|
|
154
|
+
// 匹配原始 accessorKey(如果 key 包含点号)
|
|
155
|
+
if (typeof accessorKey === "string" && key === accessorKey) return true;
|
|
156
|
+
// 匹配 id
|
|
157
|
+
if (columnDef.id && key === columnDef.id) return true;
|
|
158
|
+
return false;
|
|
159
|
+
});
|
|
160
|
+
const pinned = stickyCol ? (stickyCol.position === "left" ? "left" : "right") : undefined;
|
|
161
|
+
|
|
162
|
+
// 从 columnDef 中提取 ag-grid 支持的属性
|
|
163
|
+
const agColDef: ColDef<TData> = {
|
|
164
|
+
...columnDef,
|
|
165
|
+
colId: colId || columnDef.colId || String(Math.random()),
|
|
166
|
+
pinned: pinned || columnDef.pinned,
|
|
167
|
+
sortable: columnDef.enableSorting !== false ? columnDef.sortable !== false : false,
|
|
168
|
+
filter: columnDef.filter ?? false,
|
|
169
|
+
resizable: columnDef.enableResizing !== false ? columnDef.resizable !== false : false,
|
|
170
|
+
hide: columnDef.enableHiding === false ? false : columnDef.hide,
|
|
171
|
+
cellClass: columnDef.className || columnDef.cellClass,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (pinned) {
|
|
175
|
+
if (columnDef.size) agColDef.width = columnDef.size;
|
|
176
|
+
if (columnDef.minSize) agColDef.minWidth = columnDef.minSize;
|
|
177
|
+
if (columnDef.maxSize) agColDef.maxWidth = columnDef.maxSize;
|
|
178
|
+
} else {
|
|
179
|
+
if (columnDef.size) {
|
|
180
|
+
agColDef.width = columnDef.size;
|
|
181
|
+
if (columnDef.minSize) agColDef.minWidth = columnDef.minSize;
|
|
182
|
+
if (columnDef.maxSize) agColDef.maxWidth = columnDef.maxSize;
|
|
183
|
+
} else if (!agColDef.width && !agColDef.flex) {
|
|
184
|
+
agColDef.flex = 1;
|
|
185
|
+
agColDef.minWidth = columnDef.minSize || 100;
|
|
186
|
+
if (columnDef.maxSize) agColDef.maxWidth = columnDef.maxSize;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!agColDef.field && typeof accessorKey === "string") {
|
|
191
|
+
agColDef.field = accessorKey as any;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (isString(columnDef.header)) {
|
|
195
|
+
agColDef.headerName = columnDef.header;
|
|
196
|
+
} else if (isFunction(columnDef.header)) {
|
|
197
|
+
agColDef.headerComponent = (params: any) => {
|
|
198
|
+
const headerFn = columnDef.header as (params: { column: any; header: any; table: any }) => React.ReactNode;
|
|
199
|
+
const result = headerFn({
|
|
200
|
+
column: params.column,
|
|
201
|
+
header: params,
|
|
202
|
+
table: params.api,
|
|
203
|
+
});
|
|
204
|
+
if (typeof result === "string" || typeof result === "number") {
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
};
|
|
209
|
+
} else if (columnDef.headerName) {
|
|
210
|
+
agColDef.headerName = columnDef.headerName;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const formatters = columnDef.formatters || [];
|
|
214
|
+
const hasFormatters = formatters.length > 0;
|
|
215
|
+
const hasCustomCell = !!columnDef.cell;
|
|
216
|
+
|
|
217
|
+
if (hasCustomCell || hasFormatters) {
|
|
218
|
+
agColDef.cellRenderer = (params: any) => {
|
|
219
|
+
let cellContent: any;
|
|
220
|
+
|
|
221
|
+
if (hasCustomCell && isFunction(columnDef.cell)) {
|
|
222
|
+
const cellContext = {
|
|
223
|
+
getValue: () => params.value,
|
|
224
|
+
row: { original: params.data, getValue: (key: string) => get(params.data, key) },
|
|
225
|
+
column: params.column,
|
|
226
|
+
table: params.api,
|
|
227
|
+
};
|
|
228
|
+
cellContent = columnDef.cell(cellContext);
|
|
229
|
+
} else {
|
|
230
|
+
cellContent = params.value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (hasFormatters) {
|
|
234
|
+
return <ValueFormatter formatters={formatters} value={cellContent} />;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return cellContent;
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return agColDef;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function DataGrid<TData>(props: DataGridProps<TData>) {
|
|
245
|
+
const {
|
|
246
|
+
data,
|
|
247
|
+
columns,
|
|
248
|
+
showColumnVisibility = true,
|
|
249
|
+
rowActions,
|
|
250
|
+
checkbox = false,
|
|
251
|
+
onRowSelectionChange,
|
|
252
|
+
stickyColumns = [],
|
|
253
|
+
onRowActionClick,
|
|
254
|
+
filter,
|
|
255
|
+
loading,
|
|
256
|
+
pagination,
|
|
257
|
+
load,
|
|
258
|
+
maxHeight,
|
|
259
|
+
getRowId,
|
|
260
|
+
rowHeight = 34,
|
|
261
|
+
autoRowHeight = false,
|
|
262
|
+
actionsCellClassName,
|
|
263
|
+
} = props;
|
|
264
|
+
|
|
265
|
+
const config = useContext(UIXContext);
|
|
266
|
+
const { resolvedTheme } = useTheme();
|
|
267
|
+
const empty = props.empty || get(config.locale, "DataTable.empty");
|
|
268
|
+
|
|
269
|
+
const gridApiRef = useRef<GridApi<TData> | null>(null);
|
|
270
|
+
const [mounted, setMounted] = useState(false);
|
|
271
|
+
const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({});
|
|
272
|
+
|
|
273
|
+
const storageKey = useMemo(() => {
|
|
274
|
+
return generateColumnStorageKey(columns);
|
|
275
|
+
}, [columns]);
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
setMounted(true);
|
|
279
|
+
const storedVisibility = loadColumnVisibility(storageKey);
|
|
280
|
+
setColumnVisibility(storedVisibility);
|
|
281
|
+
}, [storageKey]);
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (mounted && gridApiRef.current) {
|
|
285
|
+
// 应用列可见性
|
|
286
|
+
const columnsToShow: string[] = [];
|
|
287
|
+
const columnsToHide: string[] = [];
|
|
288
|
+
Object.entries(columnVisibility).forEach(([colId, visible]) => {
|
|
289
|
+
if (visible) {
|
|
290
|
+
columnsToShow.push(colId);
|
|
291
|
+
} else {
|
|
292
|
+
columnsToHide.push(colId);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
if (columnsToShow.length > 0) {
|
|
296
|
+
gridApiRef.current.setColumnsVisible(columnsToShow, true);
|
|
297
|
+
}
|
|
298
|
+
if (columnsToHide.length > 0) {
|
|
299
|
+
gridApiRef.current.setColumnsVisible(columnsToHide, false);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}, [columnVisibility, mounted]);
|
|
303
|
+
|
|
304
|
+
// 转换列定义
|
|
305
|
+
const agGridColumns = useMemo<ColDef<TData>[]>(() => {
|
|
306
|
+
const newColumns: ColDef<TData>[] = [];
|
|
307
|
+
|
|
308
|
+
// 添加 checkbox 列
|
|
309
|
+
if (checkbox) {
|
|
310
|
+
// 检查是否在 stickyColumns 中明确配置,如果没有则默认固定在左侧
|
|
311
|
+
const stickySelect = stickyColumns.find((sc) => sc.key === "select");
|
|
312
|
+
const pinnedSelect = stickySelect
|
|
313
|
+
? stickySelect.position === "left"
|
|
314
|
+
? "left"
|
|
315
|
+
: stickySelect.position === "right"
|
|
316
|
+
? "right"
|
|
317
|
+
: undefined
|
|
318
|
+
: "left"; // 默认固定在左侧
|
|
319
|
+
|
|
320
|
+
newColumns.push({
|
|
321
|
+
colId: "select",
|
|
322
|
+
checkboxSelection: true,
|
|
323
|
+
headerCheckboxSelection: true,
|
|
324
|
+
width: 40,
|
|
325
|
+
pinned: pinnedSelect,
|
|
326
|
+
lockPosition: pinnedSelect === "left" ? "left" : pinnedSelect === "right" ? "right" : undefined,
|
|
327
|
+
suppressMovable: true,
|
|
328
|
+
resizable: false,
|
|
329
|
+
sortable: false,
|
|
330
|
+
filter: false,
|
|
331
|
+
headerName: "",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 转换用户定义的列
|
|
336
|
+
columns.forEach((col) => {
|
|
337
|
+
newColumns.push(convertColumnDefToAgGrid(col, stickyColumns));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 添加 actions 列
|
|
341
|
+
if (hasActions(rowActions!)) {
|
|
342
|
+
// 检查是否在 stickyColumns 中明确配置,如果没有则默认固定在右侧
|
|
343
|
+
const stickyActions = stickyColumns.find((sc) => sc.key === "actions");
|
|
344
|
+
const pinnedActions = stickyActions
|
|
345
|
+
? stickyActions.position === "left"
|
|
346
|
+
? "left"
|
|
347
|
+
: stickyActions.position === "right"
|
|
348
|
+
? "right"
|
|
349
|
+
: undefined
|
|
350
|
+
: "right"; // 默认固定在右侧
|
|
351
|
+
|
|
352
|
+
newColumns.push({
|
|
353
|
+
colId: "actions",
|
|
354
|
+
pinned: pinnedActions,
|
|
355
|
+
lockPosition: pinnedActions === "left" ? "left" : pinnedActions === "right" ? "right" : undefined,
|
|
356
|
+
width: 42,
|
|
357
|
+
resizable: false,
|
|
358
|
+
sortable: false,
|
|
359
|
+
filter: false,
|
|
360
|
+
headerName: "",
|
|
361
|
+
cellClass: cn("!px-0", actionsCellClassName),
|
|
362
|
+
cellRenderer: (params: any) => {
|
|
363
|
+
let items = rowActions;
|
|
364
|
+
if (isFunction(rowActions)) {
|
|
365
|
+
items = rowActions(params.data);
|
|
366
|
+
}
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex w-full justify-center py-1" onClick={(e) => e.stopPropagation()}>
|
|
369
|
+
<Dropdown
|
|
370
|
+
align="end"
|
|
371
|
+
asChild={true}
|
|
372
|
+
items={items as DropdownMenuItemProps[]}
|
|
373
|
+
onItemClick={(item) => onRowActionClick?.(item, params.data)}
|
|
374
|
+
>
|
|
375
|
+
<Action className="!p-0 h-6 w-6">
|
|
376
|
+
<DotsHorizontalIcon />
|
|
377
|
+
</Action>
|
|
378
|
+
</Dropdown>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return newColumns;
|
|
386
|
+
}, [columns, checkbox, rowActions, onRowActionClick, stickyColumns]);
|
|
387
|
+
|
|
388
|
+
// ag-grid 主题
|
|
389
|
+
const gridTheme = useMemo(() => {
|
|
390
|
+
const isDark = resolvedTheme === "dark";
|
|
391
|
+
return themeBalham.withPart(isDark ? colorSchemeDark : colorSchemeLight);
|
|
392
|
+
}, [resolvedTheme]);
|
|
393
|
+
|
|
394
|
+
// 默认列配置
|
|
395
|
+
const defaultColDef = useMemo<ColDef<TData>>(
|
|
396
|
+
() => ({
|
|
397
|
+
resizable: true,
|
|
398
|
+
filter: false,
|
|
399
|
+
sortable: true,
|
|
400
|
+
...(autoRowHeight
|
|
401
|
+
? {
|
|
402
|
+
autoHeight: true,
|
|
403
|
+
wrapText: true,
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
406
|
+
}),
|
|
407
|
+
[autoRowHeight],
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// 注册自定义组件
|
|
411
|
+
const components = useMemo(
|
|
412
|
+
() => ({
|
|
413
|
+
loadingOverlay: LoadingOverlay,
|
|
414
|
+
noRowsOverlay: NoRowsOverlay,
|
|
415
|
+
}),
|
|
416
|
+
[],
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const onGridReady = useCallback(
|
|
420
|
+
(event: GridReadyEvent<TData>) => {
|
|
421
|
+
gridApiRef.current = event.api;
|
|
422
|
+
|
|
423
|
+
// 应用存储的列可见性
|
|
424
|
+
const storedVisibility = loadColumnVisibility(storageKey);
|
|
425
|
+
const columnsToShow: string[] = [];
|
|
426
|
+
const columnsToHide: string[] = [];
|
|
427
|
+
Object.entries(storedVisibility).forEach(([colId, visible]) => {
|
|
428
|
+
if (visible) {
|
|
429
|
+
columnsToShow.push(colId);
|
|
430
|
+
} else {
|
|
431
|
+
columnsToHide.push(colId);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
if (columnsToShow.length > 0) {
|
|
435
|
+
event.api.setColumnsVisible(columnsToShow, true);
|
|
436
|
+
}
|
|
437
|
+
if (columnsToHide.length > 0) {
|
|
438
|
+
event.api.setColumnsVisible(columnsToHide, false);
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
[storageKey],
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const onSelectionChanged = useCallback(() => {
|
|
445
|
+
if (!gridApiRef.current || !checkbox) return;
|
|
446
|
+
const selected = gridApiRef.current.getSelectedRows() || [];
|
|
447
|
+
onRowSelectionChange?.(selected);
|
|
448
|
+
}, [checkbox, onRowSelectionChange]);
|
|
449
|
+
|
|
450
|
+
const onColumnVisible = useCallback(() => {
|
|
451
|
+
if (!gridApiRef.current) return;
|
|
452
|
+
const allColumns = gridApiRef.current.getColumns() || [];
|
|
453
|
+
const visibility: Record<string, boolean> = {};
|
|
454
|
+
allColumns.forEach((col) => {
|
|
455
|
+
if (col.getColId() !== "select" && col.getColId() !== "actions") {
|
|
456
|
+
visibility[col.getColId()] = col.isVisible() ?? true;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
setColumnVisibility(visibility);
|
|
460
|
+
saveColumnVisibility(storageKey, visibility);
|
|
461
|
+
}, [storageKey]);
|
|
462
|
+
|
|
463
|
+
// 列设置菜单项
|
|
464
|
+
const columnSettings = useMemo<DropdownMenuItemProps[]>(() => {
|
|
465
|
+
if (!gridApiRef.current) return [];
|
|
466
|
+
const allColumns = gridApiRef.current.getColumns() || [];
|
|
467
|
+
return allColumns
|
|
468
|
+
.filter((col) => {
|
|
469
|
+
const colId = col.getColId();
|
|
470
|
+
return colId !== "select" && colId !== "actions" && col.getUserProvidedColDef()?.hide !== false;
|
|
471
|
+
})
|
|
472
|
+
.map((col) => {
|
|
473
|
+
const colId = col.getColId();
|
|
474
|
+
const columnDef = columns.find((c) => {
|
|
475
|
+
const accessorKey = (c as any).accessorKey;
|
|
476
|
+
const key = c.id || (typeof accessorKey === "string" ? accessorKey.replace(/\./g, "_") : undefined);
|
|
477
|
+
return key === colId;
|
|
478
|
+
});
|
|
479
|
+
let label = colId;
|
|
480
|
+
if (columnDef) {
|
|
481
|
+
if (isString(columnDef.header)) {
|
|
482
|
+
label = columnDef.header;
|
|
483
|
+
} else if (isFunction(columnDef.header)) {
|
|
484
|
+
// 简化处理,使用 colId 作为 label
|
|
485
|
+
label = colId;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const checked = columnVisibility[colId];
|
|
489
|
+
return {
|
|
490
|
+
id: colId,
|
|
491
|
+
label,
|
|
492
|
+
type: "checkbox",
|
|
493
|
+
checked: isUndefined(checked) ? (col.isVisible() ?? true) : checked,
|
|
494
|
+
onCheckedChange: (_item, value) => {
|
|
495
|
+
gridApiRef.current?.setColumnsVisible([colId], value);
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
}, [columns, columnVisibility]);
|
|
500
|
+
|
|
501
|
+
const showVisibilityControl = useMemo(
|
|
502
|
+
() => showColumnVisibility && columnSettings.length,
|
|
503
|
+
[showColumnVisibility, columnSettings],
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const showToolbar = useMemo(() => filter || showVisibilityControl, [filter, showVisibilityControl]);
|
|
507
|
+
|
|
508
|
+
const showPagination = useMemo(() => {
|
|
509
|
+
if (!pagination) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
if (typeof pagination === "boolean") {
|
|
513
|
+
return pagination;
|
|
514
|
+
}
|
|
515
|
+
return pagination.total > 0;
|
|
516
|
+
}, [pagination]);
|
|
517
|
+
|
|
518
|
+
const handleRowClick = useCallback(
|
|
519
|
+
(event: { data?: TData }) => {
|
|
520
|
+
if (props.onRowClick && event.data) {
|
|
521
|
+
props.onRowClick(event.data);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
[props],
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// 根据 loading 状态控制覆盖层的显示
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
if (!gridApiRef.current) return;
|
|
530
|
+
gridApiRef.current.setGridOption("loading", loading || false);
|
|
531
|
+
}, [loading]);
|
|
532
|
+
|
|
533
|
+
const gridHeight = useMemo(() => {
|
|
534
|
+
if (!maxHeight) return undefined;
|
|
535
|
+
return typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
|
|
536
|
+
}, [maxHeight]);
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<div className={cn("relative")}>
|
|
540
|
+
<div className={"flex flex-col gap-md"}>
|
|
541
|
+
<div>
|
|
542
|
+
{showToolbar ? (
|
|
543
|
+
<div
|
|
544
|
+
className={cn(
|
|
545
|
+
"border-0 border-secondary border-b border-solid pb-2",
|
|
546
|
+
"flex items-end",
|
|
547
|
+
filter ? "justify-between" : "justify-end",
|
|
548
|
+
)}
|
|
549
|
+
>
|
|
550
|
+
{filter ? <Filters {...filter} load={load} loading={loading} /> : null}
|
|
551
|
+
{showVisibilityControl ? (
|
|
552
|
+
<Dropdown align="end" asChild={true} items={columnSettings}>
|
|
553
|
+
<Action className="size-button p-0">
|
|
554
|
+
<LayoutIcon className="size-4" />
|
|
555
|
+
</Action>
|
|
556
|
+
</Dropdown>
|
|
557
|
+
) : null}
|
|
558
|
+
</div>
|
|
559
|
+
) : null}
|
|
560
|
+
<div
|
|
561
|
+
className={cn(
|
|
562
|
+
"data-grid",
|
|
563
|
+
gridHeight ? "h-full" : "",
|
|
564
|
+
"[&_.ag-root-wrapper]:!border-none",
|
|
565
|
+
"relative",
|
|
566
|
+
!mounted && "invisible",
|
|
567
|
+
)}
|
|
568
|
+
style={gridHeight ? { height: gridHeight } : undefined}
|
|
569
|
+
>
|
|
570
|
+
<AgGridReact<TData>
|
|
571
|
+
columnDefs={agGridColumns}
|
|
572
|
+
components={components}
|
|
573
|
+
defaultColDef={defaultColDef}
|
|
574
|
+
domLayout={gridHeight ? undefined : "autoHeight"}
|
|
575
|
+
getRowHeight={
|
|
576
|
+
autoRowHeight
|
|
577
|
+
? undefined
|
|
578
|
+
: typeof rowHeight === "function"
|
|
579
|
+
? (params: any) => rowHeight({ data: params.data, node: params.node })
|
|
580
|
+
: undefined
|
|
581
|
+
}
|
|
582
|
+
getRowId={getRowId ? (params) => getRowId(params.data) : undefined}
|
|
583
|
+
loadingOverlayComponent="loadingOverlay"
|
|
584
|
+
noRowsOverlayComponent="noRowsOverlay"
|
|
585
|
+
noRowsOverlayComponentParams={empty ? { noRowsMessage: empty } : undefined}
|
|
586
|
+
onColumnVisible={onColumnVisible}
|
|
587
|
+
onGridReady={onGridReady}
|
|
588
|
+
onRowClicked={handleRowClick}
|
|
589
|
+
onSelectionChanged={onSelectionChanged}
|
|
590
|
+
rowData={loading ? [] : data}
|
|
591
|
+
rowHeight={autoRowHeight ? undefined : typeof rowHeight === "number" ? rowHeight : undefined}
|
|
592
|
+
rowSelection={checkbox ? "multiple" : undefined}
|
|
593
|
+
suppressAnimationFrame={false}
|
|
594
|
+
suppressColumnVirtualisation={false}
|
|
595
|
+
suppressHorizontalScroll={false}
|
|
596
|
+
suppressRowClickSelection={!checkbox}
|
|
597
|
+
suppressRowVirtualisation={gridHeight ? false : true}
|
|
598
|
+
theme={gridTheme}
|
|
599
|
+
tooltipHideDelay={200}
|
|
600
|
+
tooltipShowDelay={0}
|
|
601
|
+
/>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
{showPagination ? (
|
|
605
|
+
<div className={cn(!mounted && "invisible")}>
|
|
606
|
+
<Pagination
|
|
607
|
+
{...(pagination as PaginationProps)}
|
|
608
|
+
onChange={(page: number) => {
|
|
609
|
+
load?.({ page });
|
|
610
|
+
}}
|
|
611
|
+
onPageSizeChange={(size: number) => {
|
|
612
|
+
load?.({ size, page: 1 });
|
|
613
|
+
}}
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
616
|
+
) : null}
|
|
617
|
+
</div>
|
|
618
|
+
{loading || !mounted ? (
|
|
619
|
+
<div className="dark:!bg-black/5 absolute top-0 right-0 bottom-0 left-0 z-50 flex items-center justify-center bg-white/50">
|
|
620
|
+
<Spin />
|
|
621
|
+
</div>
|
|
622
|
+
) : null}
|
|
623
|
+
</div>
|
|
624
|
+
);
|
|
625
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ILoadingOverlayParams, type INoRowsOverlayParams } from "ag-grid-community";
|
|
2
|
+
|
|
3
|
+
export const LoadingOverlay = (_params: ILoadingOverlayParams) => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex h-full w-full items-center justify-center bg-background/80 backdrop-blur-sm">
|
|
6
|
+
<div className="flex flex-col items-center space-y-3">
|
|
7
|
+
<div className="relative">
|
|
8
|
+
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
9
|
+
</div>
|
|
10
|
+
<div className="text-muted-foreground text-sm">加载中...</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const NoRowsOverlay = (params: INoRowsOverlayParams) => {
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: <noRowsMessage>
|
|
18
|
+
const message = (params as any).noRowsMessage || "暂无数据";
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
21
|
+
<div className="flex flex-col items-center space-y-2">
|
|
22
|
+
<div className="text-muted-foreground">{message}</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|