@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,137 @@
|
|
|
1
|
+
import { type FC, type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { CaretSortIcon, Cross2Icon } from "@radix-ui/react-icons";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
cn,
|
|
7
|
+
Empty,
|
|
8
|
+
type EmptyProps,
|
|
9
|
+
Popover,
|
|
10
|
+
PopoverContent,
|
|
11
|
+
PopoverTrigger,
|
|
12
|
+
ScrollArea,
|
|
13
|
+
Spin,
|
|
14
|
+
Tree,
|
|
15
|
+
type TreeData,
|
|
16
|
+
useSize,
|
|
17
|
+
} from "@meta-1/design";
|
|
18
|
+
import { Button } from "@meta-1/design/components/ui/button";
|
|
19
|
+
|
|
20
|
+
export type TreeSelectProps = {
|
|
21
|
+
className?: string;
|
|
22
|
+
treeData: TreeData[];
|
|
23
|
+
clearable?: boolean;
|
|
24
|
+
value?: string;
|
|
25
|
+
onChange?: (value?: string) => void;
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
contentClassName?: string;
|
|
29
|
+
emptyProps?: EmptyProps;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getTitleFromTreeData = (treeData: TreeData[], key: string): ReactNode => {
|
|
33
|
+
// 迭代所有 children
|
|
34
|
+
for (const item of treeData) {
|
|
35
|
+
if (item.key === key) {
|
|
36
|
+
return item.title;
|
|
37
|
+
}
|
|
38
|
+
if (item.children) {
|
|
39
|
+
const title = getTitleFromTreeData(item.children, key);
|
|
40
|
+
if (title) {
|
|
41
|
+
return title;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const TreeSelect: FC<TreeSelectProps> = (props) => {
|
|
48
|
+
const {
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
className,
|
|
52
|
+
treeData,
|
|
53
|
+
clearable = false,
|
|
54
|
+
loading = false,
|
|
55
|
+
placeholder,
|
|
56
|
+
contentClassName,
|
|
57
|
+
emptyProps,
|
|
58
|
+
} = props;
|
|
59
|
+
const [open, setOpen] = useState(false);
|
|
60
|
+
const containerRef = useRef(null);
|
|
61
|
+
const size = useSize(containerRef);
|
|
62
|
+
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
|
63
|
+
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
|
64
|
+
|
|
65
|
+
const showClear = useMemo(() => clearable && selectedKeys.length, [clearable, selectedKeys]);
|
|
66
|
+
const showValue = useMemo(() => {
|
|
67
|
+
if (selectedKeys.length > 0) {
|
|
68
|
+
return getTitleFromTreeData(treeData, selectedKeys[0]);
|
|
69
|
+
}
|
|
70
|
+
return <span className="text-secondary-foreground/50">{placeholder}</span>;
|
|
71
|
+
}, [treeData, selectedKeys, placeholder]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (value) {
|
|
75
|
+
setSelectedKeys([value]);
|
|
76
|
+
} else {
|
|
77
|
+
setSelectedKeys([]);
|
|
78
|
+
}
|
|
79
|
+
}, [value]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Popover onOpenChange={setOpen} open={open}>
|
|
83
|
+
<PopoverTrigger asChild disabled={loading}>
|
|
84
|
+
<Button
|
|
85
|
+
aria-expanded={open}
|
|
86
|
+
className={cn(
|
|
87
|
+
"group h-9 min-w-[150px] items-center justify-between px-2 py-1 align-middle hover:bg-secondary/40",
|
|
88
|
+
className,
|
|
89
|
+
)}
|
|
90
|
+
disabled={loading}
|
|
91
|
+
ref={containerRef}
|
|
92
|
+
variant="outline"
|
|
93
|
+
>
|
|
94
|
+
<div className="flex flex-1 items-center justify-center">
|
|
95
|
+
<div className="flex flex-1">{showValue}</div>
|
|
96
|
+
{loading ? (
|
|
97
|
+
<Spin />
|
|
98
|
+
) : (
|
|
99
|
+
<div className="flex items-center">
|
|
100
|
+
<CaretSortIcon className={cn("block h-4 w-4", showClear ? "group-hover:hidden" : "")} />
|
|
101
|
+
<Cross2Icon
|
|
102
|
+
className={cn("hidden h-4 w-4", showClear ? "group-hover:block" : "")}
|
|
103
|
+
onClick={(e) => {
|
|
104
|
+
setSelectedKeys([]);
|
|
105
|
+
onChange?.(undefined);
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</Button>
|
|
113
|
+
</PopoverTrigger>
|
|
114
|
+
<PopoverContent className="p-0" style={{ width: size.width }}>
|
|
115
|
+
<ScrollArea className={classNames("flex max-h-[30vh] flex-col", contentClassName)}>
|
|
116
|
+
{treeData?.length ? (
|
|
117
|
+
<Tree
|
|
118
|
+
expandedKeys={expandedKeys}
|
|
119
|
+
onExpand={(expandedKeys) => setExpandedKeys(expandedKeys as string[])}
|
|
120
|
+
onSelect={(selectedKeys, { selected }) => {
|
|
121
|
+
if (selected) {
|
|
122
|
+
onChange?.(selectedKeys[0] as string);
|
|
123
|
+
setSelectedKeys(selectedKeys as string[]);
|
|
124
|
+
}
|
|
125
|
+
setOpen(false);
|
|
126
|
+
}}
|
|
127
|
+
selectedKeys={selectedKeys}
|
|
128
|
+
treeData={treeData}
|
|
129
|
+
/>
|
|
130
|
+
) : (
|
|
131
|
+
<Empty {...(emptyProps ?? {})} />
|
|
132
|
+
)}
|
|
133
|
+
</ScrollArea>
|
|
134
|
+
</PopoverContent>
|
|
135
|
+
</Popover>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
|
|
5
|
+
export type ExpandActionProps = {
|
|
6
|
+
enable: boolean;
|
|
7
|
+
expanded: boolean;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const ExpandAction: FC<ExpandActionProps> = (props) => {
|
|
12
|
+
const { enable, expanded, onClick } = props;
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={classNames(
|
|
16
|
+
"flex h-6 w-6 items-center justify-center rounded-sm",
|
|
17
|
+
enable ? "cursor-pointer bg-[rgba(0,0,0,0.05)] hover:bg-[rgba(0,0,0,0.1)]" : "",
|
|
18
|
+
)}
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
>
|
|
21
|
+
{enable ? expanded ? <ChevronDownIcon /> : <ChevronRightIcon /> : null}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
|
2
|
+
import cloneDeep from "lodash/cloneDeep";
|
|
3
|
+
import remove from "lodash/remove";
|
|
4
|
+
import uniq from "lodash/uniq";
|
|
5
|
+
|
|
6
|
+
import { Empty, type EmptyProps, Spin, Table, TableBody, TableHead, TableHeader, TableRow } from "@meta-1/design";
|
|
7
|
+
import { renderRow } from "./utils";
|
|
8
|
+
|
|
9
|
+
export type TreeTableColumn<TData> = {
|
|
10
|
+
className?: string;
|
|
11
|
+
headerClassName?: string;
|
|
12
|
+
formatters?: string[];
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
dataKey: keyof TData;
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: <value>
|
|
16
|
+
render?: (value: any, data: TData) => ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type TreeTableProps<TData> = {
|
|
20
|
+
data: TData[];
|
|
21
|
+
columns: TreeTableColumn<TData>[];
|
|
22
|
+
rowKey?: keyof TData;
|
|
23
|
+
childrenProperty?: string;
|
|
24
|
+
showHeader?: boolean;
|
|
25
|
+
indentWidth?: number;
|
|
26
|
+
expandedKeys?: string[];
|
|
27
|
+
defaultExpandedKeys?: string[];
|
|
28
|
+
onExpand?: (expandedKeys: string[]) => void;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
emptyProps?: EmptyProps;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function TreeTable<TData>(props: TreeTableProps<TData>) {
|
|
34
|
+
const { columns, data, showHeader = true, defaultExpandedKeys = [], loading = false, emptyProps } = props;
|
|
35
|
+
const [expandedKeys, setExpandedKeys] = useState<string[]>(defaultExpandedKeys);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setExpandedKeys(props.expandedKeys ?? []);
|
|
39
|
+
}, [props.expandedKeys]);
|
|
40
|
+
|
|
41
|
+
const onExpand = useCallback(
|
|
42
|
+
(key: string, expanded: boolean) => {
|
|
43
|
+
const keys = cloneDeep(expandedKeys);
|
|
44
|
+
if (expanded) {
|
|
45
|
+
setExpandedKeys(uniq([...keys, key]));
|
|
46
|
+
} else {
|
|
47
|
+
setExpandedKeys(remove(keys, (item) => item !== key));
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[expandedKeys],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="relative">
|
|
55
|
+
<Table>
|
|
56
|
+
{showHeader && (
|
|
57
|
+
<TableHeader>
|
|
58
|
+
<TableRow>
|
|
59
|
+
{columns.map((col) => (
|
|
60
|
+
<TableHead className={col.headerClassName} key={col.dataKey as string}>
|
|
61
|
+
{col.title}
|
|
62
|
+
</TableHead>
|
|
63
|
+
))}
|
|
64
|
+
</TableRow>
|
|
65
|
+
</TableHeader>
|
|
66
|
+
)}
|
|
67
|
+
<TableBody>
|
|
68
|
+
{renderRow<TData>({
|
|
69
|
+
data,
|
|
70
|
+
tableProps: props,
|
|
71
|
+
deep: 0,
|
|
72
|
+
expandedKeys,
|
|
73
|
+
onExpand,
|
|
74
|
+
})}
|
|
75
|
+
</TableBody>
|
|
76
|
+
</Table>
|
|
77
|
+
{loading ? (
|
|
78
|
+
<div className="absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center bg-white/50">
|
|
79
|
+
<Spin />
|
|
80
|
+
</div>
|
|
81
|
+
) : data && data.length > 0 ? null : (
|
|
82
|
+
<Empty {...(emptyProps ?? {})} />
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { TableCell, TableRow, type TreeTableProps } from "@meta-1/design";
|
|
4
|
+
import { ExpandAction } from "./action";
|
|
5
|
+
import { DEFAULT_CHILDREN_PROPERTY, DEFAULT_INDENT_WIDTH } from "./config";
|
|
6
|
+
|
|
7
|
+
export type RenderRowProps<TData> = {
|
|
8
|
+
data: TData[];
|
|
9
|
+
tableProps: TreeTableProps<TData>;
|
|
10
|
+
deep: number;
|
|
11
|
+
expandedKeys: string[];
|
|
12
|
+
onExpand?: (key: string, expanded: boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function renderRow<TData>(props: RenderRowProps<TData>): ReactNode {
|
|
16
|
+
const {
|
|
17
|
+
childrenProperty = DEFAULT_CHILDREN_PROPERTY,
|
|
18
|
+
rowKey,
|
|
19
|
+
columns,
|
|
20
|
+
indentWidth = DEFAULT_INDENT_WIDTH,
|
|
21
|
+
} = props.tableProps;
|
|
22
|
+
const { data, deep, expandedKeys, onExpand } = props;
|
|
23
|
+
|
|
24
|
+
return data?.map((item) => {
|
|
25
|
+
const children = item[childrenProperty as keyof TData] as TData[];
|
|
26
|
+
const hasChildren = children && children.length > 0;
|
|
27
|
+
const key = item[rowKey as keyof TData] as string;
|
|
28
|
+
const expanded = expandedKeys.includes(key);
|
|
29
|
+
return [
|
|
30
|
+
<TableRow key={key}>
|
|
31
|
+
{columns.map((col, index) => {
|
|
32
|
+
const content = col.render ? col.render(item[col.dataKey], item) : String(item[col.dataKey]);
|
|
33
|
+
return (
|
|
34
|
+
<TableCell className={col.className} key={col.dataKey as string}>
|
|
35
|
+
{index === 0 ? (
|
|
36
|
+
<div className="flex items-center justify-start">
|
|
37
|
+
<div
|
|
38
|
+
className="mr-2"
|
|
39
|
+
style={{
|
|
40
|
+
paddingLeft: (indentWidth + 8) * deep,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<ExpandAction enable={hasChildren} expanded={expanded} onClick={() => onExpand?.(key, !expanded)} />
|
|
44
|
+
</div>
|
|
45
|
+
{content}
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
content
|
|
49
|
+
)}
|
|
50
|
+
</TableCell>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</TableRow>,
|
|
54
|
+
expanded
|
|
55
|
+
? renderRow({
|
|
56
|
+
...props,
|
|
57
|
+
data: children,
|
|
58
|
+
deep: deep + 1,
|
|
59
|
+
})
|
|
60
|
+
: null,
|
|
61
|
+
];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { forwardRef, type PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { CheckCircledIcon, Cross2Icon, CrossCircledIcon, FileIcon } from "@radix-ui/react-icons";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import get from "lodash/get";
|
|
5
|
+
import remove from "lodash/remove";
|
|
6
|
+
import { type DropzoneOptions, useDropzone } from "react-dropzone";
|
|
7
|
+
import { v4 as uuidv4 } from "uuid";
|
|
8
|
+
|
|
9
|
+
import { Button, Progress, useMessage } from "@meta-1/design";
|
|
10
|
+
import { UIXContext } from "@meta-1/design/components/uix/config-provider";
|
|
11
|
+
import { cn } from "@meta-1/design/lib";
|
|
12
|
+
import type { HandleProps, UploadFile } from "./type";
|
|
13
|
+
import { defaultUploadHandle } from "./utils";
|
|
14
|
+
|
|
15
|
+
export type UploaderProps = PropsWithChildren<{
|
|
16
|
+
showFileList?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
action?: string;
|
|
20
|
+
uploadText?: string;
|
|
21
|
+
value?: UploadFile[];
|
|
22
|
+
onChange?: (value: UploadFile[]) => void;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
showButton?: boolean;
|
|
26
|
+
uploadHandle?: (props: HandleProps) => void;
|
|
27
|
+
}> &
|
|
28
|
+
DropzoneOptions;
|
|
29
|
+
|
|
30
|
+
const initFile = (file: UploadFile) => {
|
|
31
|
+
file.uid ??= uuidv4();
|
|
32
|
+
file.status ??= "done";
|
|
33
|
+
return file;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Uploader = forwardRef<HTMLDivElement, UploaderProps>((props, forwardedRef) => {
|
|
37
|
+
const {
|
|
38
|
+
className,
|
|
39
|
+
onDropAccepted,
|
|
40
|
+
action,
|
|
41
|
+
onChange,
|
|
42
|
+
value,
|
|
43
|
+
headers,
|
|
44
|
+
data,
|
|
45
|
+
showButton = true,
|
|
46
|
+
showFileList = true,
|
|
47
|
+
children,
|
|
48
|
+
uploadHandle = defaultUploadHandle,
|
|
49
|
+
maxFiles,
|
|
50
|
+
...rest
|
|
51
|
+
} = props;
|
|
52
|
+
const elementRef = forwardedRef;
|
|
53
|
+
|
|
54
|
+
const [files, setFiles] = useState<UploadFile[]>((value || []).map(initFile));
|
|
55
|
+
const filesRef = useRef<UploadFile[]>(files);
|
|
56
|
+
const config = useContext(UIXContext);
|
|
57
|
+
const message = useMessage();
|
|
58
|
+
const placeholder = props.placeholder || get(config.locale, "Uploader.placeholder");
|
|
59
|
+
const uploadText = props.uploadText || get(config.locale, "Uploader.uploadText");
|
|
60
|
+
const maxFilesExceededText = get(config.locale, "Uploader.maxFilesExceeded");
|
|
61
|
+
const partialFilesAddedText =
|
|
62
|
+
get(config.locale, "Uploader.partialFilesAdded") || ", only %count more files can be added";
|
|
63
|
+
|
|
64
|
+
// 检查是否达到文件上限
|
|
65
|
+
const isMaxFilesReached = maxFiles !== undefined && files.length >= maxFiles;
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (value) {
|
|
69
|
+
setFiles(value.map(initFile));
|
|
70
|
+
filesRef.current = value.map(initFile);
|
|
71
|
+
}
|
|
72
|
+
}, [value]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
filesRef.current = files;
|
|
76
|
+
}, [files]);
|
|
77
|
+
|
|
78
|
+
const { getRootProps, getInputProps } = useDropzone({
|
|
79
|
+
...rest,
|
|
80
|
+
maxFiles,
|
|
81
|
+
disabled: isMaxFilesReached,
|
|
82
|
+
onDropAccepted: (list: File[], event) => {
|
|
83
|
+
onDropAccepted?.(list, event);
|
|
84
|
+
|
|
85
|
+
// 检查 maxFiles 限制
|
|
86
|
+
let filesToAdd = list;
|
|
87
|
+
if (maxFiles !== undefined) {
|
|
88
|
+
const currentFileCount = filesRef.current.length;
|
|
89
|
+
const availableSlots = maxFiles - currentFileCount;
|
|
90
|
+
|
|
91
|
+
if (availableSlots <= 0) {
|
|
92
|
+
// 已达到最大文件数,不添加任何文件
|
|
93
|
+
message.warning(maxFilesExceededText);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 只添加可用槽位数量的文件
|
|
98
|
+
if (list.length > availableSlots) {
|
|
99
|
+
filesToAdd = list.slice(0, availableSlots);
|
|
100
|
+
message.warning(
|
|
101
|
+
`${maxFilesExceededText}${partialFilesAddedText.replace("%count", availableSlots.toString())}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const newFiles = [
|
|
107
|
+
...filesRef.current,
|
|
108
|
+
...filesToAdd.map((file) => {
|
|
109
|
+
const uploadFile = file as UploadFile;
|
|
110
|
+
uploadFile.uid = uuidv4();
|
|
111
|
+
uploadFile.status = "init";
|
|
112
|
+
return uploadFile;
|
|
113
|
+
}),
|
|
114
|
+
];
|
|
115
|
+
setFiles(newFiles);
|
|
116
|
+
onChange?.(newFiles as UploadFile[]);
|
|
117
|
+
},
|
|
118
|
+
onDropRejected(fileRejections) {
|
|
119
|
+
// 收集所有文件名和错误信息
|
|
120
|
+
const errorMessages = fileRejections.map((rejection) => {
|
|
121
|
+
return `${rejection.file.name}: ${rejection.errors[0].message}`;
|
|
122
|
+
});
|
|
123
|
+
message.warning(errorMessages.map((item) => <div key={item}>{item}</div>));
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const onUpload = useCallback(() => {
|
|
128
|
+
const updateFile = (file: UploadFile, status: "uploading" | "done" | "error") => {
|
|
129
|
+
const idx = filesRef.current.findIndex((f) => (f as UploadFile).uid === file.uid);
|
|
130
|
+
if (idx !== -1) {
|
|
131
|
+
file.status = status;
|
|
132
|
+
filesRef.current[idx] = {
|
|
133
|
+
...filesRef.current[idx],
|
|
134
|
+
...file,
|
|
135
|
+
name: filesRef.current[idx].name,
|
|
136
|
+
size: filesRef.current[idx].size,
|
|
137
|
+
type: filesRef.current[idx].type,
|
|
138
|
+
};
|
|
139
|
+
setFiles([...filesRef.current]);
|
|
140
|
+
onChange?.([...filesRef.current] as UploadFile[]);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
for (const f of filesRef.current) {
|
|
144
|
+
const file = f as UploadFile;
|
|
145
|
+
if (file.status === "init") {
|
|
146
|
+
file.status = "uploading";
|
|
147
|
+
file.progress = 0;
|
|
148
|
+
uploadHandle({
|
|
149
|
+
file,
|
|
150
|
+
action,
|
|
151
|
+
headers,
|
|
152
|
+
data,
|
|
153
|
+
onProgress: (file) => updateFile(file, "uploading"),
|
|
154
|
+
onSuccess: (file) => updateFile(file, "done"),
|
|
155
|
+
onError: (file) => updateFile(file, "error"),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
setFiles([...filesRef.current]);
|
|
160
|
+
onChange?.([...filesRef.current] as UploadFile[]);
|
|
161
|
+
}, [action, headers, data, uploadHandle, onChange]);
|
|
162
|
+
|
|
163
|
+
const renderFile = (file: UploadFile) => {
|
|
164
|
+
return (
|
|
165
|
+
<div className={cn("flex items-center justify-center border-b", "last:border-none")} key={file.uid}>
|
|
166
|
+
<div className="flex h-8 w-8 items-center justify-center">
|
|
167
|
+
<FileIcon />
|
|
168
|
+
</div>
|
|
169
|
+
<div className="w-[50%] overflow-hidden overflow-ellipsis whitespace-nowrap text-sm">{file.name}</div>
|
|
170
|
+
<div className="mx-1 flex-1">
|
|
171
|
+
{file.status === "uploading" && <Progress className="w-full" value={file.progress} />}
|
|
172
|
+
{file.status === "error" && (
|
|
173
|
+
<div className="flex items-center justify-end text-red-500">
|
|
174
|
+
<CrossCircledIcon className="mr-1" />
|
|
175
|
+
<span className="text-sm"> {file.error?.message}</span>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
{file.status === "done" && (
|
|
179
|
+
<div className="flex items-center justify-end text-green-500">
|
|
180
|
+
<CheckCircledIcon />
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
<div
|
|
185
|
+
className={cn("flex h-8 w-8 items-center justify-center", "cursor-pointer hover:bg-[var(--action-hover)]")}
|
|
186
|
+
onClick={() => {
|
|
187
|
+
remove(filesRef.current, (item: UploadFile) => item.uid === file.uid);
|
|
188
|
+
file.controller?.abort();
|
|
189
|
+
setFiles([...filesRef.current]);
|
|
190
|
+
onChange?.([...filesRef.current]);
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
<Cross2Icon />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="flex flex-col space-y-2" ref={elementRef}>
|
|
201
|
+
<div {...getRootProps()} className={classNames(children ? "inline-block w-auto" : "", "outline-none")}>
|
|
202
|
+
<input {...getInputProps()} />
|
|
203
|
+
{children ? (
|
|
204
|
+
children
|
|
205
|
+
) : (
|
|
206
|
+
<div
|
|
207
|
+
className={cn(
|
|
208
|
+
"flex items-center justify-center p-6 text-card-foreground/50",
|
|
209
|
+
"rounded-md border-2 border-border border-dashed bg-card",
|
|
210
|
+
"cursor-default",
|
|
211
|
+
"focus:border-primary",
|
|
212
|
+
isMaxFilesReached && "cursor-not-allowed opacity-50",
|
|
213
|
+
className,
|
|
214
|
+
)}
|
|
215
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: <tabIndex>
|
|
216
|
+
tabIndex={0}
|
|
217
|
+
>
|
|
218
|
+
<p>{isMaxFilesReached ? maxFilesExceededText : placeholder}</p>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
{showFileList && files.length ? <div className="rounded-sm border">{files.map(renderFile)}</div> : null}
|
|
223
|
+
{!!files.filter(({ status }) => status === "init").length && showButton ? (
|
|
224
|
+
<div>
|
|
225
|
+
<Button
|
|
226
|
+
loading={!!files.filter(({ status }) => status === "uploading").length}
|
|
227
|
+
onClick={onUpload}
|
|
228
|
+
type="button"
|
|
229
|
+
variant="outline"
|
|
230
|
+
>
|
|
231
|
+
{uploadText}
|
|
232
|
+
</Button>
|
|
233
|
+
</div>
|
|
234
|
+
) : null}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type UploadFile = File & {
|
|
2
|
+
uid: string;
|
|
3
|
+
status: "init" | "uploading" | "done" | "error";
|
|
4
|
+
progress: number;
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: <response>
|
|
6
|
+
response?: any;
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: <error>
|
|
8
|
+
error?: any;
|
|
9
|
+
controller?: AbortController;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type HandleProps = {
|
|
13
|
+
file: UploadFile;
|
|
14
|
+
action?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
data?: unknown;
|
|
17
|
+
onProgress: (file: UploadFile) => void;
|
|
18
|
+
onSuccess: (file: UploadFile) => void;
|
|
19
|
+
onError: (file: UploadFile) => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
import type { HandleProps } from "./type";
|
|
4
|
+
|
|
5
|
+
export const defaultUploadHandle = (props: HandleProps) => {
|
|
6
|
+
const { file, action, headers, data, onProgress, onSuccess, onError } = props;
|
|
7
|
+
if (!action) {
|
|
8
|
+
file.status = "error";
|
|
9
|
+
file.error = new Error("action is required");
|
|
10
|
+
onError(file);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
file.controller = controller;
|
|
15
|
+
const formData = new FormData();
|
|
16
|
+
formData.append("file", file);
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: <appendData>
|
|
18
|
+
const appendData = (data || {}) as unknown as any;
|
|
19
|
+
for (const key of Object.keys(appendData)) {
|
|
20
|
+
formData.append(key, appendData[key]);
|
|
21
|
+
}
|
|
22
|
+
axios
|
|
23
|
+
.post(action, formData, {
|
|
24
|
+
headers,
|
|
25
|
+
onUploadProgress: (progressEvent) => {
|
|
26
|
+
file.progress = Math.ceil(progressEvent.progress! * 100);
|
|
27
|
+
onProgress(file);
|
|
28
|
+
},
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
})
|
|
31
|
+
.then((response) => {
|
|
32
|
+
file.status = "done";
|
|
33
|
+
file.response = response;
|
|
34
|
+
onSuccess(file);
|
|
35
|
+
})
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
file.status = "error";
|
|
38
|
+
file.error = error;
|
|
39
|
+
onError(file);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type FC, forwardRef, type PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
import * as BUILT_IN from "@meta-1/design/lib/formatters";
|
|
4
|
+
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: <formatters>
|
|
6
|
+
export type Formatters = ([string, any[]] | string)[];
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: <functionMap>
|
|
8
|
+
export type FunctionMap = Record<string, (value: any, ...args: any[]) => any>;
|
|
9
|
+
|
|
10
|
+
const _handles: FunctionMap = {};
|
|
11
|
+
|
|
12
|
+
export const register = (handles: FunctionMap) => {
|
|
13
|
+
Object.assign(_handles, handles);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// biome-ignore lint/suspicious/noExplicitAny: <v>
|
|
17
|
+
export const formatValue = (v: any, formatters: Formatters, all?: FunctionMap) => {
|
|
18
|
+
// biome-ignore lint/suspicious/noExplicitAny: <merged>
|
|
19
|
+
const merged: any = { ...BUILT_IN, ..._handles, ...(all ?? {}) };
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: <fallback>
|
|
21
|
+
const fallback = (v: any, ..._p: any[]) => v;
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: <params>
|
|
23
|
+
let params: any[] = [];
|
|
24
|
+
let result = v;
|
|
25
|
+
|
|
26
|
+
for (const filter of formatters) {
|
|
27
|
+
let call = fallback;
|
|
28
|
+
if (typeof filter === "string") {
|
|
29
|
+
call = merged[filter] || fallback;
|
|
30
|
+
} else if (typeof filter === "function") {
|
|
31
|
+
call = filter;
|
|
32
|
+
} else if (typeof filter === "object" && filter.length) {
|
|
33
|
+
call = merged[filter[0]] || fallback;
|
|
34
|
+
params = filter[1];
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
result = call(result, ...params);
|
|
38
|
+
} catch {
|
|
39
|
+
console.log("formatter error", filter);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface ValueFormatterProps extends PropsWithChildren {
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: <value>
|
|
47
|
+
value?: any;
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: <formatters>
|
|
49
|
+
formatters?: any[];
|
|
50
|
+
handles?: FunctionMap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ValueFormatter: FC<ValueFormatterProps> = forwardRef((props, _ref) => {
|
|
54
|
+
const { handles, formatters = [], value, children } = props;
|
|
55
|
+
let v = value === 0 ? value : value || children;
|
|
56
|
+
if (!formatters.includes("defaultValue")) formatters.push("defaultValue");
|
|
57
|
+
v = formatValue(v, formatters, handles);
|
|
58
|
+
return <>{v}</>;
|
|
59
|
+
});
|