@meta-1/design 0.0.159
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/README.md +412 -0
- package/package.json +138 -0
- package/src/assets/icons/empty.svg +1 -0
- package/src/assets/icons/spin.svg +1 -0
- package/src/assets/locales/en-us.ts +74 -0
- package/src/assets/locales/zh-cn.ts +74 -0
- package/src/assets/locales/zh-tw.ts +74 -0
- package/src/assets/style/theme.css +173 -0
- package/src/components/icons/Empty.tsx +18 -0
- package/src/components/icons/Spin.tsx +16 -0
- package/src/components/icons/index.ts +2 -0
- package/src/components/ui/alert-dialog.tsx +111 -0
- package/src/components/ui/alert.tsx +49 -0
- package/src/components/ui/avatar.tsx +32 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/breadcrumb.tsx +92 -0
- package/src/components/ui/button.tsx +52 -0
- package/src/components/ui/calendar.tsx +56 -0
- package/src/components/ui/card.tsx +56 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/command.tsx +137 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +217 -0
- package/src/components/ui/form.tsx +138 -0
- package/src/components/ui/hover-card.tsx +36 -0
- package/src/components/ui/input-otp.tsx +66 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/navigation-menu.tsx +142 -0
- package/src/components/ui/pagination.tsx +118 -0
- package/src/components/ui/popover.tsx +40 -0
- package/src/components/ui/progress.tsx +22 -0
- package/src/components/ui/radio-group.tsx +31 -0
- package/src/components/ui/resizable.tsx +46 -0
- package/src/components/ui/scroll-area.tsx +46 -0
- package/src/components/ui/select.tsx +158 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +101 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/switch.tsx +26 -0
- package/src/components/ui/table.tsx +73 -0
- package/src/components/ui/tabs.tsx +40 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +46 -0
- package/src/components/uix/action/index.tsx +37 -0
- package/src/components/uix/alert/index.tsx +43 -0
- package/src/components/uix/alert-dialog/index.tsx +109 -0
- package/src/components/uix/avatar/index.tsx +25 -0
- package/src/components/uix/breadcrumbs/index.tsx +38 -0
- package/src/components/uix/broadcast-channel-context/index.tsx +28 -0
- package/src/components/uix/button/index.tsx +29 -0
- package/src/components/uix/card/index.tsx +32 -0
- package/src/components/uix/checkbox/index.tsx +79 -0
- package/src/components/uix/checkbox-group/index.tsx +60 -0
- package/src/components/uix/combo-select/index.tsx +364 -0
- package/src/components/uix/config-provider/index.tsx +31 -0
- package/src/components/uix/data-table/index.tsx +491 -0
- package/src/components/uix/data-table/style.css +40 -0
- package/src/components/uix/date-picker/index.tsx +88 -0
- package/src/components/uix/date-range-picker/index.tsx +71 -0
- package/src/components/uix/dialog/index.tsx +70 -0
- package/src/components/uix/divider/index.tsx +23 -0
- package/src/components/uix/dropdown/index.tsx +117 -0
- package/src/components/uix/empty/index.tsx +29 -0
- package/src/components/uix/filters/index.tsx +105 -0
- package/src/components/uix/form/index.tsx +274 -0
- package/src/components/uix/image/index.tsx +13 -0
- package/src/components/uix/loading/index.tsx +24 -0
- package/src/components/uix/message/index.tsx +21 -0
- package/src/components/uix/pagination/index.tsx +180 -0
- package/src/components/uix/radio-group/index.tsx +35 -0
- package/src/components/uix/result/index.tsx +45 -0
- package/src/components/uix/select/index.tsx +93 -0
- package/src/components/uix/space/index.tsx +24 -0
- package/src/components/uix/spin/index.tsx +12 -0
- package/src/components/uix/steps/index.tsx +67 -0
- package/src/components/uix/switch/index.tsx +33 -0
- package/src/components/uix/tooltip/index.tsx +29 -0
- package/src/components/uix/tree/index.tsx +39 -0
- package/src/components/uix/tree/style.css +75 -0
- package/src/components/uix/tree-select/index.tsx +137 -0
- package/src/components/uix/tree-table/action.tsx +24 -0
- package/src/components/uix/tree-table/config.ts +2 -0
- package/src/components/uix/tree-table/index.tsx +86 -0
- package/src/components/uix/tree-table/utils.tsx +63 -0
- package/src/components/uix/uploader/index.tsx +237 -0
- package/src/components/uix/uploader/type.ts +20 -0
- package/src/components/uix/uploader/utils.ts +41 -0
- package/src/components/uix/value-formatter/index.tsx +59 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/resize.ts +29 -0
- package/src/hooks/use.outside.ts +30 -0
- package/src/index.ts +159 -0
- package/src/lib/formatters.ts +13 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/is.ts +6 -0
- package/src/lib/react-dom.ts +98 -0
- package/src/lib/utils.ts +39 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
|
|
3
|
+
import {
|
|
4
|
+
type ColumnDef,
|
|
5
|
+
flexRender,
|
|
6
|
+
getCoreRowModel,
|
|
7
|
+
getSortedRowModel,
|
|
8
|
+
type Row,
|
|
9
|
+
type SortingState,
|
|
10
|
+
useReactTable,
|
|
11
|
+
type VisibilityState,
|
|
12
|
+
} from "@tanstack/react-table";
|
|
13
|
+
import isArray from "lodash/isArray";
|
|
14
|
+
import isFunction from "lodash/isFunction";
|
|
15
|
+
import isString from "lodash/isString";
|
|
16
|
+
import isUndefined from "lodash/isUndefined";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
Action,
|
|
20
|
+
Checkbox,
|
|
21
|
+
cn,
|
|
22
|
+
Dropdown,
|
|
23
|
+
type DropdownMenuItemProps,
|
|
24
|
+
Filters,
|
|
25
|
+
type FiltersProps,
|
|
26
|
+
generateColumnStorageKey,
|
|
27
|
+
Pagination,
|
|
28
|
+
type PaginationProps,
|
|
29
|
+
Spin,
|
|
30
|
+
} from "@meta-1/design";
|
|
31
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../ui/table";
|
|
32
|
+
import "./style.css";
|
|
33
|
+
|
|
34
|
+
import { LayoutIcon } from "@radix-ui/react-icons";
|
|
35
|
+
import classNames from "classnames";
|
|
36
|
+
import get from "lodash/get";
|
|
37
|
+
|
|
38
|
+
import { UIXContext } from "@meta-1/design/components/uix/config-provider";
|
|
39
|
+
import { type Formatters, type FunctionMap, formatValue } from "@meta-1/design/components/uix/value-formatter";
|
|
40
|
+
|
|
41
|
+
export interface StickyColumnProps {
|
|
42
|
+
key: string;
|
|
43
|
+
position: "left" | "right";
|
|
44
|
+
size: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type DataTableColumn<TData> = ColumnDef<TData, unknown> & {
|
|
48
|
+
className?: string;
|
|
49
|
+
formatters?: Formatters;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface DataTableProps<TData> {
|
|
53
|
+
autoHidePagination?: boolean;
|
|
54
|
+
inCard?: boolean;
|
|
55
|
+
columns: DataTableColumn<TData>[];
|
|
56
|
+
data: TData[];
|
|
57
|
+
showColumnVisibility?: boolean;
|
|
58
|
+
stickyColumns?: StickyColumnProps[];
|
|
59
|
+
checkbox?: boolean;
|
|
60
|
+
rowActions?: DropdownMenuItemProps[] | ((cell: TData) => DropdownMenuItemProps[]);
|
|
61
|
+
onRowActionClick?: (item: DropdownMenuItemProps, row: Row<TData>) => void;
|
|
62
|
+
loading?: boolean;
|
|
63
|
+
load?: (params?: unknown) => Promise<unknown> | unknown;
|
|
64
|
+
filter?: FiltersProps;
|
|
65
|
+
pagination?: PaginationProps | boolean;
|
|
66
|
+
cellHandles?: FunctionMap;
|
|
67
|
+
empty?: string;
|
|
68
|
+
showHeader?: boolean;
|
|
69
|
+
onRowClick?: (row: Row<TData>) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 本地存储相关函数
|
|
73
|
+
const LOCAL_STORAGE_PREFIX = "datatable_columns_";
|
|
74
|
+
|
|
75
|
+
const saveColumnVisibility = (storageKey: string, visibility: VisibilityState) => {
|
|
76
|
+
try {
|
|
77
|
+
localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${storageKey}`, JSON.stringify(visibility));
|
|
78
|
+
} catch (_error) {
|
|
79
|
+
// console.warn('Failed to save column visibility to localStorage:', error)
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const loadColumnVisibility = (storageKey: string): VisibilityState => {
|
|
84
|
+
try {
|
|
85
|
+
const stored = localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${storageKey}`);
|
|
86
|
+
return stored ? JSON.parse(stored) : {};
|
|
87
|
+
} catch (_error) {
|
|
88
|
+
// console.warn('Failed to load column visibility from localStorage:', error)
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const getSticky = (
|
|
94
|
+
id: string,
|
|
95
|
+
leftStickyColumns: StickyColumnProps[],
|
|
96
|
+
rightStickyColumns: StickyColumnProps[],
|
|
97
|
+
) => {
|
|
98
|
+
const inLeft = !!leftStickyColumns.find(({ key }) => key === id);
|
|
99
|
+
if (inLeft) {
|
|
100
|
+
let offset = 0;
|
|
101
|
+
let width = 0;
|
|
102
|
+
let index = 0;
|
|
103
|
+
for (const col of leftStickyColumns) {
|
|
104
|
+
index++;
|
|
105
|
+
if (col.key === id) {
|
|
106
|
+
width = col.size;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
offset += col.size;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
width,
|
|
113
|
+
offset,
|
|
114
|
+
enable: true,
|
|
115
|
+
position: "left",
|
|
116
|
+
last: index === leftStickyColumns.length,
|
|
117
|
+
first: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const inRight = !!rightStickyColumns.find(({ key }) => key === id);
|
|
121
|
+
if (inRight) {
|
|
122
|
+
let offset = 0;
|
|
123
|
+
let width = 0;
|
|
124
|
+
let i = 0;
|
|
125
|
+
for (let index = rightStickyColumns.length - 1; index >= 0; index--) {
|
|
126
|
+
i++;
|
|
127
|
+
const col = rightStickyColumns[index];
|
|
128
|
+
if (col.key === id) {
|
|
129
|
+
width = col.size;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
offset += col.size;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
width,
|
|
136
|
+
offset,
|
|
137
|
+
enable: true,
|
|
138
|
+
position: "right",
|
|
139
|
+
last: false,
|
|
140
|
+
first: i === rightStickyColumns.length,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { enable: false };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// biome-ignore lint/suspicious/noExplicitAny: <hasActions>
|
|
147
|
+
const hasActions = (rowActions: DropdownMenuItemProps[] | ((cell: any) => DropdownMenuItemProps[])) => {
|
|
148
|
+
if (isFunction(rowActions)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (isArray(rowActions)) {
|
|
152
|
+
return !!rowActions.length;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export function DataTable<TData>(props: DataTableProps<TData>) {
|
|
158
|
+
const {
|
|
159
|
+
data,
|
|
160
|
+
columns,
|
|
161
|
+
showColumnVisibility = true,
|
|
162
|
+
rowActions,
|
|
163
|
+
checkbox = false,
|
|
164
|
+
stickyColumns = [],
|
|
165
|
+
onRowActionClick,
|
|
166
|
+
filter,
|
|
167
|
+
loading,
|
|
168
|
+
pagination,
|
|
169
|
+
load,
|
|
170
|
+
cellHandles,
|
|
171
|
+
showHeader = true,
|
|
172
|
+
autoHidePagination = true,
|
|
173
|
+
inCard = false,
|
|
174
|
+
} = props;
|
|
175
|
+
|
|
176
|
+
const config = useContext(UIXContext);
|
|
177
|
+
const empty = props.empty || get(config.locale, "DataTable.empty");
|
|
178
|
+
// 生成存储key
|
|
179
|
+
const storageKey = useMemo(() => {
|
|
180
|
+
return generateColumnStorageKey(columns);
|
|
181
|
+
}, [columns]);
|
|
182
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
183
|
+
// 初始化时不从本地存储读取,避免 SSR 水合错误
|
|
184
|
+
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
|
185
|
+
const [rowSelection, setRowSelection] = useState({});
|
|
186
|
+
const [mounted, setMounted] = useState(false);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
// 只在客户端挂载后才读取本地存储的列可见性
|
|
190
|
+
setColumnVisibility(loadColumnVisibility(storageKey));
|
|
191
|
+
setMounted(true);
|
|
192
|
+
}, [storageKey]);
|
|
193
|
+
|
|
194
|
+
// 当列可见性改变时,保存到本地存储(但跳过初始的空状态)
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (mounted) {
|
|
197
|
+
saveColumnVisibility(storageKey, columnVisibility);
|
|
198
|
+
}
|
|
199
|
+
}, [columnVisibility, storageKey, mounted]);
|
|
200
|
+
|
|
201
|
+
// 当columns改变时,重新加载存储的状态
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (mounted) {
|
|
204
|
+
const newStorageKey = generateColumnStorageKey(columns);
|
|
205
|
+
const storedVisibility = loadColumnVisibility(newStorageKey);
|
|
206
|
+
setColumnVisibility(storedVisibility);
|
|
207
|
+
}
|
|
208
|
+
}, [columns, mounted]);
|
|
209
|
+
|
|
210
|
+
const tableColumns = useMemo<ColumnDef<TData, unknown>[]>(() => {
|
|
211
|
+
const newColumns: ColumnDef<TData, unknown>[] = [];
|
|
212
|
+
if (checkbox) {
|
|
213
|
+
newColumns.push({
|
|
214
|
+
id: "select",
|
|
215
|
+
enableSorting: false,
|
|
216
|
+
enableHiding: false,
|
|
217
|
+
// biome-ignore lint/suspicious/noExplicitAny: <header>
|
|
218
|
+
header: ({ table }: { table: any }) => (
|
|
219
|
+
<div className="flex items-center justify-center pr-2">
|
|
220
|
+
<Checkbox
|
|
221
|
+
aria-label="Select all"
|
|
222
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
223
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
),
|
|
227
|
+
// biome-ignore lint/suspicious/noExplicitAny: <cell>
|
|
228
|
+
cell: ({ row }: { row: any }) => (
|
|
229
|
+
<div className="flex items-center justify-center pr-2">
|
|
230
|
+
<Checkbox
|
|
231
|
+
aria-label="Select row"
|
|
232
|
+
checked={row.getIsSelected()}
|
|
233
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
newColumns.push(...columns);
|
|
240
|
+
if (hasActions(rowActions!)) {
|
|
241
|
+
newColumns.push({
|
|
242
|
+
id: "actions",
|
|
243
|
+
enableHiding: false,
|
|
244
|
+
size: 50,
|
|
245
|
+
enableResizing: false,
|
|
246
|
+
// biome-ignore lint/suspicious/noExplicitAny: <cell>
|
|
247
|
+
cell: ({ row }: { row: any }) => {
|
|
248
|
+
let items = rowActions;
|
|
249
|
+
if (isFunction(rowActions)) {
|
|
250
|
+
items = rowActions(row.original);
|
|
251
|
+
}
|
|
252
|
+
return (
|
|
253
|
+
<div className="flex w-full justify-end" onClick={(e) => e.stopPropagation()}>
|
|
254
|
+
<Dropdown
|
|
255
|
+
align="end"
|
|
256
|
+
asChild={true}
|
|
257
|
+
items={items as DropdownMenuItemProps[]}
|
|
258
|
+
onItemClick={(item) => onRowActionClick?.(item, row)}
|
|
259
|
+
>
|
|
260
|
+
<Action className="!p-0 h-6 w-6">
|
|
261
|
+
<DotsHorizontalIcon />
|
|
262
|
+
</Action>
|
|
263
|
+
</Dropdown>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return newColumns;
|
|
270
|
+
}, [columns, checkbox, rowActions, onRowActionClick]);
|
|
271
|
+
|
|
272
|
+
const table = useReactTable({
|
|
273
|
+
data,
|
|
274
|
+
columns: tableColumns,
|
|
275
|
+
getCoreRowModel: getCoreRowModel(),
|
|
276
|
+
onSortingChange: setSorting,
|
|
277
|
+
getSortedRowModel: getSortedRowModel(),
|
|
278
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
279
|
+
onRowSelectionChange: setRowSelection,
|
|
280
|
+
state: {
|
|
281
|
+
sorting,
|
|
282
|
+
columnVisibility,
|
|
283
|
+
rowSelection,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const leftStickyColumns = useMemo<StickyColumnProps[]>(() => {
|
|
288
|
+
const columns: StickyColumnProps[] = [];
|
|
289
|
+
if (checkbox) {
|
|
290
|
+
columns.push({
|
|
291
|
+
key: "select",
|
|
292
|
+
position: "left",
|
|
293
|
+
size: 40,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
columns.push(...stickyColumns.filter(({ position }) => position === "left"));
|
|
297
|
+
return columns;
|
|
298
|
+
}, [stickyColumns, checkbox]);
|
|
299
|
+
|
|
300
|
+
const rightStickyColumns = useMemo<StickyColumnProps[]>(() => {
|
|
301
|
+
const columns: StickyColumnProps[] = [];
|
|
302
|
+
columns.push(...stickyColumns.filter(({ position }) => position === "right"));
|
|
303
|
+
if (hasActions(rowActions!)) {
|
|
304
|
+
columns.push({
|
|
305
|
+
key: "actions",
|
|
306
|
+
position: "right",
|
|
307
|
+
size: 40,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return columns;
|
|
311
|
+
}, [stickyColumns, rowActions]);
|
|
312
|
+
|
|
313
|
+
const columnSettings = useMemo<DropdownMenuItemProps[]>(() => {
|
|
314
|
+
return table
|
|
315
|
+
.getAllColumns()
|
|
316
|
+
.filter((column) => column.getCanHide())
|
|
317
|
+
.map((column) => {
|
|
318
|
+
// biome-ignore lint/suspicious/noExplicitAny: <column>
|
|
319
|
+
const item = columns.find((col: any) => {
|
|
320
|
+
const key = col.id || col.accessorKey;
|
|
321
|
+
return key.replace(/\./g, "_") === column.id;
|
|
322
|
+
});
|
|
323
|
+
let label = column.id;
|
|
324
|
+
if (item) {
|
|
325
|
+
if (isFunction(item.header)) {
|
|
326
|
+
label = item.header({ table, column, header: table.getFlatHeaders().find((col) => col.id === column.id)! });
|
|
327
|
+
} else if (isString(item.header)) {
|
|
328
|
+
label = item.header;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const checked = columnVisibility[column.id];
|
|
332
|
+
return {
|
|
333
|
+
id: column.id,
|
|
334
|
+
label,
|
|
335
|
+
type: "checkbox",
|
|
336
|
+
checked: isUndefined(checked) ? column.getIsVisible() : checked,
|
|
337
|
+
onCheckedChange: (_item, value) => column.toggleVisibility(value),
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}, [table, columns, columnVisibility]);
|
|
341
|
+
|
|
342
|
+
const showVisibilityControl = useMemo(
|
|
343
|
+
() => showColumnVisibility && columnSettings.length,
|
|
344
|
+
[showColumnVisibility, columnSettings],
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const showToolbar = useMemo(() => filter || showVisibilityControl, [filter, showVisibilityControl]);
|
|
348
|
+
|
|
349
|
+
const showPagination = useMemo(() => {
|
|
350
|
+
if (autoHidePagination) {
|
|
351
|
+
return pagination && (pagination as PaginationProps).total > (pagination as PaginationProps).size;
|
|
352
|
+
}
|
|
353
|
+
return pagination;
|
|
354
|
+
}, [pagination, autoHidePagination]);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<>
|
|
358
|
+
{showToolbar ? (
|
|
359
|
+
<div
|
|
360
|
+
className={cn(
|
|
361
|
+
"border-0 border-secondary border-b border-solid pb-2",
|
|
362
|
+
"flex items-end",
|
|
363
|
+
filter ? "justify-between" : "justify-end",
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{filter ? <Filters {...filter} load={load} loading={loading} /> : null}
|
|
367
|
+
{showVisibilityControl ? (
|
|
368
|
+
<Dropdown align="end" asChild={true} items={columnSettings}>
|
|
369
|
+
<Action className="!p-0 h-9 w-9">
|
|
370
|
+
<LayoutIcon className="h-[18px] w-[18px]" />
|
|
371
|
+
</Action>
|
|
372
|
+
</Dropdown>
|
|
373
|
+
) : null}
|
|
374
|
+
</div>
|
|
375
|
+
) : null}
|
|
376
|
+
<div className={cn("relative")}>
|
|
377
|
+
<Table className={classNames("data-table", inCard ? "in-card" : null, !mounted && "invisible")}>
|
|
378
|
+
<TableHeader className={cn(!showHeader && "hidden")}>
|
|
379
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
380
|
+
<TableRow key={headerGroup.id}>
|
|
381
|
+
{headerGroup.headers.map((header) => {
|
|
382
|
+
// 在未挂载时禁用粘性列功能,避免 SSR 水合错误
|
|
383
|
+
const sticky = mounted
|
|
384
|
+
? getSticky(header.column.id, leftStickyColumns, rightStickyColumns)
|
|
385
|
+
: { enable: false };
|
|
386
|
+
const content = header.isPlaceholder
|
|
387
|
+
? null
|
|
388
|
+
: flexRender(header.column.columnDef.header, header.getContext());
|
|
389
|
+
return (
|
|
390
|
+
<TableHead
|
|
391
|
+
className={cn(
|
|
392
|
+
sticky.enable ? "table-sticky-col sticky" : null,
|
|
393
|
+
sticky.last ? "table-sticky-col-last" : null,
|
|
394
|
+
sticky.first ? "table-sticky-col-first" : null,
|
|
395
|
+
// biome-ignore lint/suspicious/noExplicitAny: <className>
|
|
396
|
+
(header.column.columnDef as any).className,
|
|
397
|
+
)}
|
|
398
|
+
key={header.id}
|
|
399
|
+
style={
|
|
400
|
+
sticky.enable
|
|
401
|
+
? {
|
|
402
|
+
zIndex: 10,
|
|
403
|
+
minWidth: sticky.width,
|
|
404
|
+
[sticky.position as string]: sticky.offset,
|
|
405
|
+
}
|
|
406
|
+
: undefined
|
|
407
|
+
}
|
|
408
|
+
>
|
|
409
|
+
{sticky.enable ? <div className="inner flex h-10 items-center px-2">{content}</div> : content}
|
|
410
|
+
</TableHead>
|
|
411
|
+
);
|
|
412
|
+
})}
|
|
413
|
+
</TableRow>
|
|
414
|
+
))}
|
|
415
|
+
</TableHeader>
|
|
416
|
+
<TableBody>
|
|
417
|
+
{table.getRowModel().rows?.length ? (
|
|
418
|
+
table.getRowModel().rows.map((row) => (
|
|
419
|
+
<TableRow
|
|
420
|
+
data-state={row.getIsSelected() && "selected"}
|
|
421
|
+
key={row.id}
|
|
422
|
+
onClick={() => props.onRowClick?.(row)}
|
|
423
|
+
>
|
|
424
|
+
{row.getVisibleCells().map((cell) => {
|
|
425
|
+
// 在未挂载时禁用粘性列功能,避免 SSR 水合错误
|
|
426
|
+
const sticky = mounted
|
|
427
|
+
? getSticky(cell.column.id, leftStickyColumns, rightStickyColumns)
|
|
428
|
+
: { enable: false };
|
|
429
|
+
const ctx = cell.getContext();
|
|
430
|
+
const render = ctx.renderValue;
|
|
431
|
+
// biome-ignore lint/suspicious/noExplicitAny: <formatters>
|
|
432
|
+
const formatters = (cell.column.columnDef as any).formatters || [];
|
|
433
|
+
ctx.renderValue = () => {
|
|
434
|
+
return formatValue(render(), formatters, cellHandles);
|
|
435
|
+
};
|
|
436
|
+
const content = flexRender(cell.column.columnDef.cell, ctx);
|
|
437
|
+
return (
|
|
438
|
+
<TableCell
|
|
439
|
+
className={cn(
|
|
440
|
+
sticky.enable ? "table-sticky-col sticky" : null,
|
|
441
|
+
sticky.last ? "table-sticky-col-last" : null,
|
|
442
|
+
sticky.first ? "table-sticky-col-first" : null,
|
|
443
|
+
)}
|
|
444
|
+
key={cell.id}
|
|
445
|
+
style={
|
|
446
|
+
sticky.enable
|
|
447
|
+
? {
|
|
448
|
+
zIndex: 10,
|
|
449
|
+
minWidth: sticky.width,
|
|
450
|
+
[sticky.position as string]: sticky.offset,
|
|
451
|
+
}
|
|
452
|
+
: undefined
|
|
453
|
+
}
|
|
454
|
+
>
|
|
455
|
+
{sticky.enable ? <div className="inner flex h-10 items-center px-2">{content}</div> : content}
|
|
456
|
+
</TableCell>
|
|
457
|
+
);
|
|
458
|
+
})}
|
|
459
|
+
</TableRow>
|
|
460
|
+
))
|
|
461
|
+
) : (
|
|
462
|
+
<TableRow>
|
|
463
|
+
<TableCell className="h-24 text-center" colSpan={tableColumns.length}>
|
|
464
|
+
{empty}
|
|
465
|
+
</TableCell>
|
|
466
|
+
</TableRow>
|
|
467
|
+
)}
|
|
468
|
+
</TableBody>
|
|
469
|
+
</Table>
|
|
470
|
+
<div className={cn("py-4", !mounted && "invisible")}>
|
|
471
|
+
{showPagination && (
|
|
472
|
+
<Pagination
|
|
473
|
+
{...(pagination as PaginationProps)}
|
|
474
|
+
onChange={(page: number) => {
|
|
475
|
+
load?.({ page });
|
|
476
|
+
}}
|
|
477
|
+
onSizeChange={(size: number) => {
|
|
478
|
+
load?.({ size, page: 1 });
|
|
479
|
+
}}
|
|
480
|
+
/>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
{loading || !mounted ? (
|
|
484
|
+
<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">
|
|
485
|
+
<Spin />
|
|
486
|
+
</div>
|
|
487
|
+
) : null}
|
|
488
|
+
</div>
|
|
489
|
+
</>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@import "../../../assets/style/theme.css";
|
|
2
|
+
|
|
3
|
+
.data-table {
|
|
4
|
+
--sticky-row-hover-color: rgba(250, 250, 250, 1);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.dark .data-table {
|
|
8
|
+
--sticky-row-hover-color: #1c1c1c;
|
|
9
|
+
}
|
|
10
|
+
.dark .data-table.in-card {
|
|
11
|
+
--sticky-row-hover-color: #181818;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.data-table tr {
|
|
15
|
+
@apply transition-none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.data-table tr th.table-sticky-col,
|
|
19
|
+
.data-table tr td.table-sticky-col {
|
|
20
|
+
@apply p-0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.data-table tr th.table-sticky-col .inner,
|
|
24
|
+
.data-table tr td.table-sticky-col .inner {
|
|
25
|
+
@apply bg-background;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.data-table.in-card tr th.table-sticky-col .inner,
|
|
29
|
+
.data-table.in-card tr td.table-sticky-col .inner {
|
|
30
|
+
@apply bg-card;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.data-table tr:hover th.table-sticky-col .inner,
|
|
34
|
+
.data-table tr:hover td.table-sticky-col .inner {
|
|
35
|
+
@apply bg-[var(--sticky-row-hover-color)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.data-table tr[data-state="selected"] td.table-sticky-col .inner {
|
|
39
|
+
@apply bg-[hsl(var(--muted))];
|
|
40
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { forwardRef, useContext, useState } from "react";
|
|
2
|
+
import { Cross2Icon } from "@radix-ui/react-icons";
|
|
3
|
+
import { addDays, format } from "date-fns";
|
|
4
|
+
import get from "lodash/get";
|
|
5
|
+
import { Calendar as CalendarIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { Button, Calendar, cn, Popover, PopoverContent, PopoverTrigger, Select } from "@meta-1/design";
|
|
8
|
+
import { UIXContext } from "@meta-1/design/components/uix/config-provider";
|
|
9
|
+
|
|
10
|
+
export type DatePickerProps = {
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
format?: string;
|
|
13
|
+
preset?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
allowClear?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>((props, _ref) => {
|
|
19
|
+
const { preset = false, allowClear = false } = props;
|
|
20
|
+
const [date, setDate] = useState<Date>();
|
|
21
|
+
const [presetValue, setPresetValue] = useState<string>("");
|
|
22
|
+
|
|
23
|
+
const config = useContext(UIXContext);
|
|
24
|
+
const locale = get(config.locale, "DatePicker.locale");
|
|
25
|
+
const formatConfig = props.format || get(config.locale, "DatePicker.format") || "yyyy-MM-dd";
|
|
26
|
+
const options = get(config.locale, "DatePicker.options");
|
|
27
|
+
|
|
28
|
+
const calendar = (
|
|
29
|
+
<Calendar
|
|
30
|
+
locale={locale}
|
|
31
|
+
mode="single"
|
|
32
|
+
onSelect={(v) => {
|
|
33
|
+
setDate(v);
|
|
34
|
+
setPresetValue("");
|
|
35
|
+
}}
|
|
36
|
+
selected={date}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Popover>
|
|
42
|
+
<PopoverTrigger asChild>
|
|
43
|
+
<Button
|
|
44
|
+
className={cn(
|
|
45
|
+
"group w-full justify-start space-x-1 text-left font-normal",
|
|
46
|
+
!date && "text-muted-foreground",
|
|
47
|
+
props.className,
|
|
48
|
+
)}
|
|
49
|
+
variant="outline"
|
|
50
|
+
>
|
|
51
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
52
|
+
<span className="flex-1">{date ? format(date, formatConfig) : props.placeholder}</span>
|
|
53
|
+
{allowClear && date ? (
|
|
54
|
+
<Cross2Icon
|
|
55
|
+
className="hidden group-hover:block"
|
|
56
|
+
onClick={(e) => {
|
|
57
|
+
setDate(undefined);
|
|
58
|
+
setPresetValue("");
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
) : null}
|
|
63
|
+
</Button>
|
|
64
|
+
</PopoverTrigger>
|
|
65
|
+
<PopoverContent align="start" className={cn("flex w-fit", preset ? "flex-col space-y-2 p-2" : "p-0")}>
|
|
66
|
+
{preset ? (
|
|
67
|
+
<>
|
|
68
|
+
<Select
|
|
69
|
+
className="w-full"
|
|
70
|
+
onChange={(value) => {
|
|
71
|
+
setDate(addDays(new Date(), Number.parseInt(value, 10)));
|
|
72
|
+
setPresetValue(value);
|
|
73
|
+
}}
|
|
74
|
+
options={options || []}
|
|
75
|
+
placeholder="请选择"
|
|
76
|
+
value={presetValue}
|
|
77
|
+
/>
|
|
78
|
+
<div className="rounded-md border">{calendar}</div>
|
|
79
|
+
</>
|
|
80
|
+
) : (
|
|
81
|
+
calendar
|
|
82
|
+
)}
|
|
83
|
+
</PopoverContent>
|
|
84
|
+
</Popover>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
DatePicker.displayName = "DatePicker";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, useContext, useState } from "react";
|
|
2
|
+
import { format } from "date-fns";
|
|
3
|
+
import get from "lodash/get";
|
|
4
|
+
import { Calendar as CalendarIcon } from "lucide-react";
|
|
5
|
+
import type { DateRange } from "react-day-picker";
|
|
6
|
+
|
|
7
|
+
import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "@meta-1/design";
|
|
8
|
+
import { UIXContext } from "@meta-1/design/components/uix/config-provider";
|
|
9
|
+
import { cn } from "@meta-1/design/lib";
|
|
10
|
+
|
|
11
|
+
export type DateRangePickerProps = {
|
|
12
|
+
value?: DateRange;
|
|
13
|
+
onChange?: (value: DateRange) => void;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
format?: string;
|
|
16
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
17
|
+
|
|
18
|
+
export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((props, forwardedRef) => {
|
|
19
|
+
const { className, value, onChange } = props;
|
|
20
|
+
const elementRef = forwardedRef;
|
|
21
|
+
|
|
22
|
+
const config = useContext(UIXContext);
|
|
23
|
+
const locale = get(config.locale, "DateRangePicker.locale");
|
|
24
|
+
const formatConfig = props.format || get(config.locale, "DateRangePicker.format") || "yyyy-MM-dd";
|
|
25
|
+
|
|
26
|
+
const [date, setDate] = useState<DateRange | undefined>(value);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={cn("grid gap-2", className)} ref={elementRef}>
|
|
30
|
+
<Popover>
|
|
31
|
+
<PopoverTrigger asChild>
|
|
32
|
+
<Button
|
|
33
|
+
className={cn("justify-start text-left font-normal", !date && "text-muted-foreground")}
|
|
34
|
+
id="date"
|
|
35
|
+
type="button"
|
|
36
|
+
variant="outline"
|
|
37
|
+
>
|
|
38
|
+
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
39
|
+
{date?.from ? (
|
|
40
|
+
date.to ? (
|
|
41
|
+
<>
|
|
42
|
+
{format(date.from, formatConfig)} - {format(date.to, formatConfig)}
|
|
43
|
+
</>
|
|
44
|
+
) : (
|
|
45
|
+
format(date.from, formatConfig)
|
|
46
|
+
)
|
|
47
|
+
) : (
|
|
48
|
+
<span>{props.placeholder}</span>
|
|
49
|
+
)}
|
|
50
|
+
</Button>
|
|
51
|
+
</PopoverTrigger>
|
|
52
|
+
<PopoverContent align="start" className="w-auto p-0">
|
|
53
|
+
<Calendar
|
|
54
|
+
defaultMonth={date?.from}
|
|
55
|
+
initialFocus
|
|
56
|
+
locale={locale}
|
|
57
|
+
mode="range"
|
|
58
|
+
numberOfMonths={2}
|
|
59
|
+
onSelect={(v) => {
|
|
60
|
+
setDate(v);
|
|
61
|
+
onChange?.(v!);
|
|
62
|
+
}}
|
|
63
|
+
selected={date}
|
|
64
|
+
/>
|
|
65
|
+
</PopoverContent>
|
|
66
|
+
</Popover>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
DateRangePicker.displayName = "DateRangePicker";
|