@finema/core 3.0.2 → 3.2.0
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/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/components/Form/Fields.vue +5 -1
- package/dist/runtime/components/Form/InputSelect/index.d.vue.ts +4 -0
- package/dist/runtime/components/Form/InputSelect/index.vue +2 -2
- package/dist/runtime/components/Form/InputSelect/index.vue.d.ts +4 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/index.d.vue.ts +25 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/index.vue +110 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/index.vue.d.ts +25 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/types.d.ts +27 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/types.js +0 -0
- package/dist/runtime/components/Form/fileState/MultipleFilesState.d.vue.ts +30 -0
- package/dist/runtime/components/Form/fileState/MultipleFilesState.vue +172 -0
- package/dist/runtime/components/Form/fileState/MultipleFilesState.vue.d.ts +30 -0
- package/dist/runtime/components/Form/fileState/useUploadStateMultiple.d.ts +126 -0
- package/dist/runtime/components/Form/fileState/useUploadStateMultiple.js +238 -0
- package/dist/runtime/components/Form/types.d.ts +2 -1
- package/dist/runtime/theme/uploadFileDropzone.d.ts +23 -0
- package/dist/runtime/theme/uploadFileDropzone.js +31 -3
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import FormInputTime from "./InputTime/index.vue";
|
|
|
37
37
|
import FormInputMonth from "./InputMonth/index.vue";
|
|
38
38
|
import FormInputDateTimeRange from "./InputDateTimeRange/index.vue";
|
|
39
39
|
import FormInputUploadDropzoneAuto from "./InputUploadDropzoneAuto/index.vue";
|
|
40
|
+
import FormInputUploadDropzoneAutoMultiple from "./InputUploadDropzoneAutoMultiple/index.vue";
|
|
40
41
|
import FormInputUploadDropzone from "./InputUploadDropzone/index.vue";
|
|
41
42
|
import FormInputTag from "./InputTags/index.vue";
|
|
42
43
|
import FormInputWYSIWYG from "./InputWYSIWYG/index.vue";
|
|
@@ -150,7 +151,10 @@ const componentMap = {
|
|
|
150
151
|
component: FormInputUploadDropzoneAuto,
|
|
151
152
|
props: {}
|
|
152
153
|
},
|
|
153
|
-
[INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE]:
|
|
154
|
+
[INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE]: {
|
|
155
|
+
component: FormInputUploadDropzoneAutoMultiple,
|
|
156
|
+
props: {}
|
|
157
|
+
},
|
|
154
158
|
[INPUT_TYPES.UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE]: void 0,
|
|
155
159
|
[INPUT_TYPES.WYSIWYG]: {
|
|
156
160
|
component: FormInputWYSIWYG,
|
|
@@ -5,6 +5,10 @@ declare const __VLS_export: import("vue").DefineComponent<ISelectFieldProps, {},
|
|
|
5
5
|
onChange?: ((...args: any[]) => any) | undefined;
|
|
6
6
|
}>, {
|
|
7
7
|
clearIcon: string;
|
|
8
|
+
searchInput: {
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
} | boolean;
|
|
8
12
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
13
|
declare const _default: typeof __VLS_export;
|
|
10
14
|
export default _default;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:placeholder="wrapperProps.placeholder"
|
|
7
7
|
:disabled="wrapperProps.disabled"
|
|
8
8
|
:loading="loading"
|
|
9
|
-
:search-input="searchInput"
|
|
9
|
+
:search-input="searchInput ?? void 0"
|
|
10
10
|
:selected-icon="selectedIcon"
|
|
11
11
|
value-key="value"
|
|
12
12
|
label-key="label"
|
|
@@ -60,7 +60,7 @@ const props = defineProps({
|
|
|
60
60
|
trailingIcon: { type: String, required: false },
|
|
61
61
|
clearIcon: { type: String, required: false, default: "ph:x-circle-fill" },
|
|
62
62
|
selectedIcon: { type: String, required: false },
|
|
63
|
-
searchInput: { type: [Object, Boolean], required: false },
|
|
63
|
+
searchInput: { type: [Object, Boolean], required: false, default: void 0 },
|
|
64
64
|
clearable: { type: Boolean, required: false },
|
|
65
65
|
loading: { type: Boolean, required: false },
|
|
66
66
|
options: { type: Array, required: true },
|
|
@@ -5,6 +5,10 @@ declare const __VLS_export: import("vue").DefineComponent<ISelectFieldProps, {},
|
|
|
5
5
|
onChange?: ((...args: any[]) => any) | undefined;
|
|
6
6
|
}>, {
|
|
7
7
|
clearIcon: string;
|
|
8
|
+
searchInput: {
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
} | boolean;
|
|
8
12
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
13
|
declare const _default: typeof __VLS_export;
|
|
10
14
|
export default _default;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IUploadDropzoneAutoMultipleProps } from './types.js';
|
|
2
|
+
import type { IFileValue } from '#core/components/Form/types';
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<IUploadDropzoneAutoMultipleProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
4
|
+
success: (res: IFileValue) => any;
|
|
5
|
+
delete: (index: number) => any;
|
|
6
|
+
change: (value: File[] | undefined) => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<IUploadDropzoneAutoMultipleProps> & Readonly<{
|
|
8
|
+
onSuccess?: ((res: IFileValue) => any) | undefined;
|
|
9
|
+
onDelete?: ((index: number) => any) | undefined;
|
|
10
|
+
onChange?: ((value: File[] | undefined) => any) | undefined;
|
|
11
|
+
}>, {
|
|
12
|
+
selectFileLabel: string;
|
|
13
|
+
selectFileSubLabel: string;
|
|
14
|
+
uploadingLabel: string;
|
|
15
|
+
uploadFailedLabel: string;
|
|
16
|
+
retryLabel: string;
|
|
17
|
+
bodyKey: string;
|
|
18
|
+
responseURL: string;
|
|
19
|
+
responsePath: string;
|
|
20
|
+
responseName: string;
|
|
21
|
+
responseSize: string;
|
|
22
|
+
responseID: string;
|
|
23
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
24
|
+
declare const _default: typeof __VLS_export;
|
|
25
|
+
export default _default;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper v-bind="wrapperProps">
|
|
3
|
+
<div
|
|
4
|
+
ref="dropzoneRef"
|
|
5
|
+
:class="theme.base()"
|
|
6
|
+
>
|
|
7
|
+
<div :class="theme.wrapper()">
|
|
8
|
+
<!-- Empty State -->
|
|
9
|
+
<EmptyState
|
|
10
|
+
:theme="theme"
|
|
11
|
+
:select-file-label="selectFileLabel"
|
|
12
|
+
:select-file-sub-label="selectFileSubLabel"
|
|
13
|
+
:placeholder="placeholder"
|
|
14
|
+
@open-file="uploadState.handleOpenFile"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<!-- Multiple Files State -->
|
|
19
|
+
<MultipleFilesState
|
|
20
|
+
v-if="!uploadState.isEmpty.value"
|
|
21
|
+
:theme="theme"
|
|
22
|
+
:file-items="uploadState.fileItems.value"
|
|
23
|
+
:disabled="wrapperProps.disabled"
|
|
24
|
+
:readonly="wrapperProps.readonly"
|
|
25
|
+
:upload-failed-label="uploadFailedLabel"
|
|
26
|
+
:retry-label="retryLabel"
|
|
27
|
+
@preview="uploadState.handlePreview"
|
|
28
|
+
@download="handleDownloadFile"
|
|
29
|
+
@delete="uploadState.handleDeleteFile"
|
|
30
|
+
@retry="uploadState.handleRetryUpload"
|
|
31
|
+
/>
|
|
32
|
+
</FieldWrapper>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import EmptyState from "../fileState/EmptyState.vue";
|
|
37
|
+
import MultipleFilesState from "../fileState/MultipleFilesState.vue";
|
|
38
|
+
import { useUploadStateMultiple } from "../fileState/useUploadStateMultiple";
|
|
39
|
+
import { computed, useTemplateRef } from "#imports";
|
|
40
|
+
import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
|
|
41
|
+
import { useFieldHOC } from "#core/composables/useForm";
|
|
42
|
+
import { uploadFileDropzoneTheme } from "#core/theme/uploadFileDropzone";
|
|
43
|
+
import { useUiConfig } from "#core/composables/useConfig";
|
|
44
|
+
import { downloadFileFromURL } from "#core/helpers/componentHelper";
|
|
45
|
+
const emits = defineEmits(["change", "success", "delete"]);
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
requestOptions: { type: Object, required: true },
|
|
48
|
+
uploadPathURL: { type: String, required: false },
|
|
49
|
+
bodyKey: { type: String, required: false, default: "file" },
|
|
50
|
+
responseURL: { type: String, required: false, default: "url" },
|
|
51
|
+
responsePath: { type: String, required: false, default: "path" },
|
|
52
|
+
responseName: { type: String, required: false, default: "name" },
|
|
53
|
+
responseSize: { type: String, required: false, default: "size" },
|
|
54
|
+
responseID: { type: String, required: false, default: "id" },
|
|
55
|
+
accept: { type: [Array, String], required: false },
|
|
56
|
+
maxSize: { type: Number, required: false },
|
|
57
|
+
maxFiles: { type: Number, required: false },
|
|
58
|
+
selectFileLabel: { type: String, required: false, default: "\u0E04\u0E25\u0E34\u0E01\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E44\u0E1F\u0E25\u0E4C" },
|
|
59
|
+
selectFileSubLabel: { type: String, required: false, default: "\u0E2B\u0E23\u0E37\u0E2D \u0E25\u0E32\u0E01\u0E41\u0E25\u0E30\u0E27\u0E32\u0E07\u0E17\u0E35\u0E48\u0E19\u0E35\u0E48" },
|
|
60
|
+
uploadingLabel: { type: String, required: false, default: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14..." },
|
|
61
|
+
uploadFailedLabel: { type: String, required: false, default: "\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14\u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27, \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" },
|
|
62
|
+
retryLabel: { type: String, required: false, default: "\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" },
|
|
63
|
+
form: { type: Object, required: false },
|
|
64
|
+
name: { type: String, required: true },
|
|
65
|
+
errorMessage: { type: String, required: false },
|
|
66
|
+
label: { type: null, required: false },
|
|
67
|
+
description: { type: String, required: false },
|
|
68
|
+
hint: { type: String, required: false },
|
|
69
|
+
rules: { type: null, required: false },
|
|
70
|
+
autoFocus: { type: Boolean, required: false },
|
|
71
|
+
placeholder: { type: String, required: false },
|
|
72
|
+
disabled: { type: Boolean, required: false },
|
|
73
|
+
readonly: { type: Boolean, required: false },
|
|
74
|
+
required: { type: Boolean, required: false },
|
|
75
|
+
help: { type: String, required: false },
|
|
76
|
+
ui: { type: null, required: false }
|
|
77
|
+
});
|
|
78
|
+
const {
|
|
79
|
+
wrapperProps,
|
|
80
|
+
handleChange: onChange,
|
|
81
|
+
setErrors,
|
|
82
|
+
value
|
|
83
|
+
} = useFieldHOC(props);
|
|
84
|
+
const acceptedFileTypes = computed(
|
|
85
|
+
() => typeof props.accept === "string" ? props.accept : props.accept?.join(",")
|
|
86
|
+
);
|
|
87
|
+
const dropzoneRef = useTemplateRef("dropzoneRef");
|
|
88
|
+
const uploadState = useUploadStateMultiple(
|
|
89
|
+
props,
|
|
90
|
+
emits,
|
|
91
|
+
onChange,
|
|
92
|
+
setErrors,
|
|
93
|
+
value,
|
|
94
|
+
acceptedFileTypes,
|
|
95
|
+
wrapperProps,
|
|
96
|
+
dropzoneRef
|
|
97
|
+
);
|
|
98
|
+
const theme = computed(
|
|
99
|
+
() => useUiConfig(uploadFileDropzoneTheme, "uploadFileDropzone")({
|
|
100
|
+
dragover: uploadState.dropzone.isOverDropZone.value && uploadState.isEmpty.value,
|
|
101
|
+
disabled: wrapperProps.value.disabled,
|
|
102
|
+
failed: false
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
const handleDownloadFile = (fileValue) => {
|
|
106
|
+
if (fileValue?.url && fileValue?.name) {
|
|
107
|
+
downloadFileFromURL(fileValue.url, fileValue.name);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IUploadDropzoneAutoMultipleProps } from './types.js';
|
|
2
|
+
import type { IFileValue } from '#core/components/Form/types';
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<IUploadDropzoneAutoMultipleProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
4
|
+
success: (res: IFileValue) => any;
|
|
5
|
+
delete: (index: number) => any;
|
|
6
|
+
change: (value: File[] | undefined) => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<IUploadDropzoneAutoMultipleProps> & Readonly<{
|
|
8
|
+
onSuccess?: ((res: IFileValue) => any) | undefined;
|
|
9
|
+
onDelete?: ((index: number) => any) | undefined;
|
|
10
|
+
onChange?: ((value: File[] | undefined) => any) | undefined;
|
|
11
|
+
}>, {
|
|
12
|
+
selectFileLabel: string;
|
|
13
|
+
selectFileSubLabel: string;
|
|
14
|
+
uploadingLabel: string;
|
|
15
|
+
uploadFailedLabel: string;
|
|
16
|
+
retryLabel: string;
|
|
17
|
+
bodyKey: string;
|
|
18
|
+
responseURL: string;
|
|
19
|
+
responsePath: string;
|
|
20
|
+
responseName: string;
|
|
21
|
+
responseSize: string;
|
|
22
|
+
responseID: string;
|
|
23
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
24
|
+
declare const _default: typeof __VLS_export;
|
|
25
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
2
|
+
import type { IFieldProps, IFormFieldBase, INPUT_TYPES } from '../types.js';
|
|
3
|
+
export interface IUploadDropzoneAutoMultipleProps extends IFieldProps {
|
|
4
|
+
requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
|
|
5
|
+
baseURL: string;
|
|
6
|
+
};
|
|
7
|
+
uploadPathURL?: string;
|
|
8
|
+
bodyKey?: string;
|
|
9
|
+
responseURL?: string;
|
|
10
|
+
responsePath?: string;
|
|
11
|
+
responseName?: string;
|
|
12
|
+
responseSize?: string;
|
|
13
|
+
responseID?: string;
|
|
14
|
+
accept?: string[] | string;
|
|
15
|
+
maxSize?: number;
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
selectFileLabel?: string;
|
|
18
|
+
selectFileSubLabel?: string;
|
|
19
|
+
uploadingLabel?: string;
|
|
20
|
+
uploadFailedLabel?: string;
|
|
21
|
+
retryLabel?: string;
|
|
22
|
+
}
|
|
23
|
+
export type IUploadDropzoneAutoMultipleField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE, IUploadDropzoneAutoMultipleProps, {
|
|
24
|
+
change: (value: File[] | undefined) => void;
|
|
25
|
+
success: (res: any) => void;
|
|
26
|
+
delete: (index: number) => void;
|
|
27
|
+
}>;
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IFileValue } from '#core/components/Form/types';
|
|
2
|
+
import { UploadState } from './useUploadStateMultiple.js';
|
|
3
|
+
interface FileUploadItem {
|
|
4
|
+
file: File;
|
|
5
|
+
state: UploadState;
|
|
6
|
+
progress: number;
|
|
7
|
+
value?: IFileValue;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
theme: any;
|
|
12
|
+
fileItems: FileUploadItem[];
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
readonly?: boolean;
|
|
15
|
+
uploadFailedLabel?: string;
|
|
16
|
+
retryLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
delete: (index: number) => any;
|
|
20
|
+
preview: (value: IFileValue) => any;
|
|
21
|
+
download: (value: IFileValue) => any;
|
|
22
|
+
retry: (index: number) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
24
|
+
onDelete?: ((index: number) => any) | undefined;
|
|
25
|
+
onPreview?: ((value: IFileValue) => any) | undefined;
|
|
26
|
+
onDownload?: ((value: IFileValue) => any) | undefined;
|
|
27
|
+
onRetry?: ((index: number) => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="theme.multipleFilesWrapper()">
|
|
3
|
+
<div
|
|
4
|
+
v-for="(item, index) in fileItems"
|
|
5
|
+
:key="index"
|
|
6
|
+
:class="theme.fileItemWrapper()"
|
|
7
|
+
>
|
|
8
|
+
<!-- Uploading State -->
|
|
9
|
+
<div
|
|
10
|
+
v-if="item.state === UploadState.UPLOADING"
|
|
11
|
+
:class="theme.uploadingItemWrapper()"
|
|
12
|
+
>
|
|
13
|
+
<div :class="theme.uploadingIconWrapper()">
|
|
14
|
+
<Icon
|
|
15
|
+
:name="icons.filePreviewIcon"
|
|
16
|
+
:class="theme.uploadingIconClass()"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
<div :class="theme.uploadingTextWrapper()">
|
|
20
|
+
<h1 class="truncate font-bold">
|
|
21
|
+
{{ item.file.name }}
|
|
22
|
+
</h1>
|
|
23
|
+
<div class="flex items-center gap-2">
|
|
24
|
+
<div :class="theme.progressBarWrapper()">
|
|
25
|
+
<div
|
|
26
|
+
:class="theme.progressBarFill()"
|
|
27
|
+
:style="{ width: `${item.progress}%` }"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="text-sm text-gray-400">
|
|
31
|
+
{{ item.progress }}%
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Success State -->
|
|
38
|
+
<div
|
|
39
|
+
v-else-if="item.state === UploadState.SUCCESS && item.value"
|
|
40
|
+
:class="theme.successItemWrapper()"
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
v-if="isImageFromPath(item.value.path)"
|
|
44
|
+
:class="theme.successImgWrapper()"
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
:src="item.value.url"
|
|
48
|
+
:class="theme.successImgClass()"
|
|
49
|
+
alt="img-preview"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
<div
|
|
53
|
+
v-else
|
|
54
|
+
:class="theme.successFileWrapper()"
|
|
55
|
+
>
|
|
56
|
+
<Icon
|
|
57
|
+
:name="icons.filePreviewIcon"
|
|
58
|
+
:class="theme.successFileClass()"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<div :class="theme.successTextWrapper()">
|
|
62
|
+
<div class="truncate">
|
|
63
|
+
<h1 class="truncate font-bold">
|
|
64
|
+
{{ item.value.name }}
|
|
65
|
+
</h1>
|
|
66
|
+
<p class="truncate text-sm font-light text-gray-400">
|
|
67
|
+
{{ getFileSizeFromValue(item.value) }}
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div :class="theme.actionWrapper()">
|
|
71
|
+
<a
|
|
72
|
+
v-if="isPDFFromPath(item.value.path)"
|
|
73
|
+
:href="item.value.url"
|
|
74
|
+
target="_blank"
|
|
75
|
+
class="flex"
|
|
76
|
+
>
|
|
77
|
+
<Icon
|
|
78
|
+
:name="icons.actionPreviewIcon"
|
|
79
|
+
:class="theme.actionIconClass()"
|
|
80
|
+
title="ดูตัวอย่าง"
|
|
81
|
+
/>
|
|
82
|
+
</a>
|
|
83
|
+
<Icon
|
|
84
|
+
v-if="isImageFromPath(item.value.path) || isVideoFromPath(item.value.path)"
|
|
85
|
+
:name="icons.actionPreviewIcon"
|
|
86
|
+
:class="theme.actionIconClass()"
|
|
87
|
+
title="ดูตัวอย่าง"
|
|
88
|
+
@click="$emit('preview', item.value)"
|
|
89
|
+
/>
|
|
90
|
+
<Icon
|
|
91
|
+
:name="icons.actionDownloadIcon"
|
|
92
|
+
:class="theme.actionIconClass()"
|
|
93
|
+
title="ดาวน์โหลดไฟล์"
|
|
94
|
+
@click="$emit('download', item.value)"
|
|
95
|
+
/>
|
|
96
|
+
<Icon
|
|
97
|
+
v-if="!disabled && !readonly"
|
|
98
|
+
:name="icons.actionDeleteIcon"
|
|
99
|
+
:class="theme.actionIconClass()"
|
|
100
|
+
title="ลบไฟล์"
|
|
101
|
+
@click="$emit('delete', index)"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Error State -->
|
|
108
|
+
<div
|
|
109
|
+
v-else-if="item.state === UploadState.ERROR"
|
|
110
|
+
:class="theme.errorItemWrapper()"
|
|
111
|
+
>
|
|
112
|
+
<div :class="theme.errorIconWrapper()">
|
|
113
|
+
<Icon
|
|
114
|
+
:name="icons.errorIcon"
|
|
115
|
+
:class="theme.errorIconClass()"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div :class="theme.errorTextWrapper()">
|
|
119
|
+
<h1 class="truncate font-bold">
|
|
120
|
+
{{ item.file.name }}
|
|
121
|
+
</h1>
|
|
122
|
+
<p class="text-error-500 text-sm">
|
|
123
|
+
{{ item.error || uploadFailedLabel }}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
<div :class="theme.errorActionWrapper()">
|
|
127
|
+
<Button
|
|
128
|
+
variant="link"
|
|
129
|
+
:icon="icons.actionRetryIcon"
|
|
130
|
+
:class="theme.actionRetryBtnClass()"
|
|
131
|
+
color="primary"
|
|
132
|
+
@click="$emit('retry', index)"
|
|
133
|
+
>
|
|
134
|
+
{{ retryLabel }}
|
|
135
|
+
</Button>
|
|
136
|
+
<Icon
|
|
137
|
+
v-if="!disabled && !readonly"
|
|
138
|
+
:name="icons.actionDeleteIcon"
|
|
139
|
+
:class="theme.actionIconClass()"
|
|
140
|
+
title="ลบไฟล์"
|
|
141
|
+
@click="$emit('delete', index)"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<script setup>
|
|
150
|
+
import {
|
|
151
|
+
isImageFromPath,
|
|
152
|
+
isPDFFromPath,
|
|
153
|
+
isVideoFromPath,
|
|
154
|
+
useFileSize
|
|
155
|
+
} from "#core/helpers/componentHelper";
|
|
156
|
+
import { useUiIconConfig } from "#core/composables/useConfig";
|
|
157
|
+
import { UploadState } from "./useUploadStateMultiple";
|
|
158
|
+
defineEmits(["preview", "download", "delete", "retry"]);
|
|
159
|
+
defineProps({
|
|
160
|
+
theme: { type: null, required: true },
|
|
161
|
+
fileItems: { type: Array, required: true },
|
|
162
|
+
disabled: { type: Boolean, required: false },
|
|
163
|
+
readonly: { type: Boolean, required: false },
|
|
164
|
+
uploadFailedLabel: { type: String, required: false },
|
|
165
|
+
retryLabel: { type: String, required: false }
|
|
166
|
+
});
|
|
167
|
+
const icons = useUiIconConfig("uploadFileDropzone");
|
|
168
|
+
const getFileSizeFromValue = (fileValue) => {
|
|
169
|
+
const allocate = useFileSize(fileValue.size || 0);
|
|
170
|
+
return allocate.isSelectedFileUseMb.value ? `${allocate.selectedFileSizeMb.value} MB` : `${allocate.selectedFileSizeKb.value} KB`;
|
|
171
|
+
};
|
|
172
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IFileValue } from '#core/components/Form/types';
|
|
2
|
+
import { UploadState } from './useUploadStateMultiple.js';
|
|
3
|
+
interface FileUploadItem {
|
|
4
|
+
file: File;
|
|
5
|
+
state: UploadState;
|
|
6
|
+
progress: number;
|
|
7
|
+
value?: IFileValue;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
theme: any;
|
|
12
|
+
fileItems: FileUploadItem[];
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
readonly?: boolean;
|
|
15
|
+
uploadFailedLabel?: string;
|
|
16
|
+
retryLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
delete: (index: number) => any;
|
|
20
|
+
preview: (value: IFileValue) => any;
|
|
21
|
+
download: (value: IFileValue) => any;
|
|
22
|
+
retry: (index: number) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
24
|
+
onDelete?: ((index: number) => any) | undefined;
|
|
25
|
+
onPreview?: ((value: IFileValue) => any) | undefined;
|
|
26
|
+
onDownload?: ((value: IFileValue) => any) | undefined;
|
|
27
|
+
onRetry?: ((index: number) => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Ref, TemplateRef } from 'vue';
|
|
2
|
+
import type { IUploadDropzoneAutoMultipleProps } from '../InputUploadDropzoneAutoMultiple/types.js';
|
|
3
|
+
import type { IFileValue } from '#core/components/Form/types';
|
|
4
|
+
import { useUploadLoader } from '#core/composables/useUpload';
|
|
5
|
+
export declare enum UploadState {
|
|
6
|
+
EMPTY = "empty",
|
|
7
|
+
UPLOADING = "uploading",
|
|
8
|
+
SUCCESS = "success",
|
|
9
|
+
ERROR = "error"
|
|
10
|
+
}
|
|
11
|
+
export interface FileUploadItem {
|
|
12
|
+
file: File;
|
|
13
|
+
state: UploadState;
|
|
14
|
+
progress: number;
|
|
15
|
+
value?: IFileValue;
|
|
16
|
+
error?: string;
|
|
17
|
+
uploadLoader?: ReturnType<typeof useUploadLoader>;
|
|
18
|
+
percentRef?: Ref<number>;
|
|
19
|
+
}
|
|
20
|
+
export declare const useUploadStateMultiple: (props: IUploadDropzoneAutoMultipleProps, emits: any, onChange: (value: IFileValue[] | undefined) => void, setErrors: (error: string) => void, value: any, acceptedFileTypes: any, wrapperProps: any, dropzoneRef: TemplateRef<HTMLDivElement | undefined>) => {
|
|
21
|
+
fileItems: Ref<{
|
|
22
|
+
file: {
|
|
23
|
+
readonly lastModified: number;
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly webkitRelativePath: string;
|
|
26
|
+
readonly size: number;
|
|
27
|
+
readonly type: string;
|
|
28
|
+
arrayBuffer: {
|
|
29
|
+
(): Promise<ArrayBuffer>;
|
|
30
|
+
(): Promise<ArrayBuffer>;
|
|
31
|
+
};
|
|
32
|
+
bytes: {
|
|
33
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
34
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
35
|
+
};
|
|
36
|
+
slice: {
|
|
37
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
38
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
39
|
+
};
|
|
40
|
+
stream: {
|
|
41
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
42
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
43
|
+
};
|
|
44
|
+
text: {
|
|
45
|
+
(): Promise<string>;
|
|
46
|
+
(): Promise<string>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
state: UploadState;
|
|
50
|
+
progress: number;
|
|
51
|
+
value?: {
|
|
52
|
+
url: string;
|
|
53
|
+
path?: string | undefined;
|
|
54
|
+
name?: string | undefined;
|
|
55
|
+
size?: number | undefined;
|
|
56
|
+
id?: string | undefined;
|
|
57
|
+
} | undefined;
|
|
58
|
+
error?: string | undefined;
|
|
59
|
+
uploadLoader?: {
|
|
60
|
+
status: import("#imports").IStatus;
|
|
61
|
+
data: any;
|
|
62
|
+
options: import("#imports").IAPIOptions;
|
|
63
|
+
run: (payload?: import("../../../helpers/apiObjectHelper.js").IObjectRunLoaderOptions<any, any> | undefined) => Promise<void>;
|
|
64
|
+
clear: () => void;
|
|
65
|
+
setLoading: () => void;
|
|
66
|
+
setData: (data: any) => void;
|
|
67
|
+
} | undefined;
|
|
68
|
+
percentRef?: number | undefined;
|
|
69
|
+
}[], FileUploadItem[] | {
|
|
70
|
+
file: {
|
|
71
|
+
readonly lastModified: number;
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly webkitRelativePath: string;
|
|
74
|
+
readonly size: number;
|
|
75
|
+
readonly type: string;
|
|
76
|
+
arrayBuffer: {
|
|
77
|
+
(): Promise<ArrayBuffer>;
|
|
78
|
+
(): Promise<ArrayBuffer>;
|
|
79
|
+
};
|
|
80
|
+
bytes: {
|
|
81
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
82
|
+
(): Promise<Uint8Array<ArrayBuffer>>;
|
|
83
|
+
};
|
|
84
|
+
slice: {
|
|
85
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
86
|
+
(start?: number, end?: number, contentType?: string): Blob;
|
|
87
|
+
};
|
|
88
|
+
stream: {
|
|
89
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
90
|
+
(): ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
91
|
+
};
|
|
92
|
+
text: {
|
|
93
|
+
(): Promise<string>;
|
|
94
|
+
(): Promise<string>;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
state: UploadState;
|
|
98
|
+
progress: number;
|
|
99
|
+
value?: {
|
|
100
|
+
url: string;
|
|
101
|
+
path?: string | undefined;
|
|
102
|
+
name?: string | undefined;
|
|
103
|
+
size?: number | undefined;
|
|
104
|
+
id?: string | undefined;
|
|
105
|
+
} | undefined;
|
|
106
|
+
error?: string | undefined;
|
|
107
|
+
uploadLoader?: {
|
|
108
|
+
status: import("#imports").IStatus;
|
|
109
|
+
data: any;
|
|
110
|
+
options: import("#imports").IAPIOptions;
|
|
111
|
+
run: (payload?: import("../../../helpers/apiObjectHelper.js").IObjectRunLoaderOptions<any, any> | undefined) => Promise<void>;
|
|
112
|
+
clear: () => void;
|
|
113
|
+
setLoading: () => void;
|
|
114
|
+
setData: (data: any) => void;
|
|
115
|
+
} | undefined;
|
|
116
|
+
percentRef?: number | undefined;
|
|
117
|
+
}[]>;
|
|
118
|
+
isEmpty: import("vue").ComputedRef<boolean>;
|
|
119
|
+
hasUploading: import("vue").ComputedRef<boolean>;
|
|
120
|
+
allSuccess: import("vue").ComputedRef<boolean>;
|
|
121
|
+
dropzone: import("@vueuse/core").UseDropZoneReturn;
|
|
122
|
+
handleOpenFile: () => void;
|
|
123
|
+
handleDeleteFile: (index: number) => void;
|
|
124
|
+
handleRetryUpload: (index: number) => void;
|
|
125
|
+
handlePreview: (fileValue: IFileValue) => void;
|
|
126
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useDropZone, useFileDialog } from "@vueuse/core";
|
|
2
|
+
import PreviewModal from "./PreviewModal.vue";
|
|
3
|
+
import { computed, ref, useOverlay, watch } from "#imports";
|
|
4
|
+
import { useUploadLoader } from "#core/composables/useUpload";
|
|
5
|
+
import { useFileAllocate, useFileProgress } from "#core/helpers/componentHelper";
|
|
6
|
+
import { StringHelper } from "#core/utils/StringHelper";
|
|
7
|
+
import { _get } from "#core/utils/lodash";
|
|
8
|
+
export var UploadState = /* @__PURE__ */ ((UploadState2) => {
|
|
9
|
+
UploadState2["EMPTY"] = "empty";
|
|
10
|
+
UploadState2["UPLOADING"] = "uploading";
|
|
11
|
+
UploadState2["SUCCESS"] = "success";
|
|
12
|
+
UploadState2["ERROR"] = "error";
|
|
13
|
+
return UploadState2;
|
|
14
|
+
})(UploadState || {});
|
|
15
|
+
export const useUploadStateMultiple = (props, emits, onChange, setErrors, value, acceptedFileTypes, wrapperProps, dropzoneRef) => {
|
|
16
|
+
const overlay = useOverlay();
|
|
17
|
+
const previewModal = overlay.create(PreviewModal);
|
|
18
|
+
const fileItems = ref([]);
|
|
19
|
+
const fileAllocate = useFileAllocate(computed(() => fileItems.value[0]?.file), props);
|
|
20
|
+
const validateFile = (file) => {
|
|
21
|
+
if (props.accept && fileAllocate.acceptFile.value) {
|
|
22
|
+
const acceptedTypes = fileAllocate.acceptFile.value;
|
|
23
|
+
const acceptedTypesList = acceptedTypes.split(",").map((type) => type.trim());
|
|
24
|
+
const fileExtension = file.name.toLowerCase().split(".").pop();
|
|
25
|
+
const isValidFileType = acceptedTypesList.some((acceptedType) => {
|
|
26
|
+
if (acceptedType.startsWith(".")) {
|
|
27
|
+
return fileExtension === acceptedType.slice(1).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
if (!acceptedType.includes("/") && !acceptedType.includes("*")) {
|
|
30
|
+
return fileExtension === acceptedType.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
if (acceptedType.endsWith("/*")) {
|
|
33
|
+
return file.type.startsWith(acceptedType.slice(0, -2) + "/");
|
|
34
|
+
}
|
|
35
|
+
return file.type === acceptedType;
|
|
36
|
+
});
|
|
37
|
+
if (!isValidFileType) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
error: `\u0E1B\u0E23\u0E30\u0E40\u0E20\u0E17\u0E44\u0E1F\u0E25\u0E4C\u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07 (\u0E23\u0E2D\u0E07\u0E23\u0E31\u0E1A\u0E40\u0E09\u0E1E\u0E32\u0E30 ${acceptedTypesList.join(", ")})`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (props.maxSize) {
|
|
45
|
+
const maxSizeBytes = (fileAllocate.acceptFileSizeKb.value || 0) * 1024;
|
|
46
|
+
if (file.size > maxSizeBytes) {
|
|
47
|
+
const sizeLabel = fileAllocate.isAcceptFileUseMb.value ? `${fileAllocate.acceptFileSizeMb.value} MB` : `${fileAllocate.acceptFileSizeKb.value} KB`;
|
|
48
|
+
return {
|
|
49
|
+
valid: false,
|
|
50
|
+
error: `\u0E02\u0E19\u0E32\u0E14\u0E44\u0E1F\u0E25\u0E4C\u0E15\u0E49\u0E2D\u0E07\u0E44\u0E21\u0E48\u0E40\u0E01\u0E34\u0E19 ${sizeLabel}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
valid: true
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const updateFileItem = (index, updates) => {
|
|
59
|
+
fileItems.value = fileItems.value.map(
|
|
60
|
+
(item, i) => i === index ? {
|
|
61
|
+
...item,
|
|
62
|
+
...updates
|
|
63
|
+
} : item
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
const setupUpload = (fileItem, index) => {
|
|
67
|
+
const {
|
|
68
|
+
percent,
|
|
69
|
+
onDownloadProgress,
|
|
70
|
+
onUploadProgress
|
|
71
|
+
} = useFileProgress();
|
|
72
|
+
const request = {
|
|
73
|
+
requestOptions: {
|
|
74
|
+
...props.requestOptions,
|
|
75
|
+
onDownloadProgress,
|
|
76
|
+
onUploadProgress
|
|
77
|
+
},
|
|
78
|
+
pathURL: props.uploadPathURL
|
|
79
|
+
};
|
|
80
|
+
const uploadLoader = useUploadLoader(request);
|
|
81
|
+
updateFileItem(index, {
|
|
82
|
+
uploadLoader,
|
|
83
|
+
percentRef: percent
|
|
84
|
+
});
|
|
85
|
+
const stopProgressWatch = watch(percent, (newProgress) => {
|
|
86
|
+
updateFileItem(index, {
|
|
87
|
+
progress: newProgress
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const stopSuccessWatch = watch(
|
|
91
|
+
() => uploadLoader.status.value.isSuccess,
|
|
92
|
+
(isSuccess) => {
|
|
93
|
+
if (isSuccess && uploadLoader.data.value) {
|
|
94
|
+
const fileValue = {
|
|
95
|
+
url: _get(uploadLoader.data.value, props.responseURL),
|
|
96
|
+
path: _get(uploadLoader.data.value, props.responsePath),
|
|
97
|
+
name: _get(uploadLoader.data.value, props.responseName) || fileItem.file.name,
|
|
98
|
+
size: Number(_get(uploadLoader.data.value, props.responseSize) || fileItem.file.size),
|
|
99
|
+
id: _get(uploadLoader.data.value, props.responseID)
|
|
100
|
+
};
|
|
101
|
+
updateFileItem(index, {
|
|
102
|
+
value: fileValue,
|
|
103
|
+
state: "success" /* SUCCESS */
|
|
104
|
+
});
|
|
105
|
+
updateFormValue();
|
|
106
|
+
emits("success", fileValue);
|
|
107
|
+
stopProgressWatch();
|
|
108
|
+
stopSuccessWatch();
|
|
109
|
+
stopErrorWatch();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
const stopErrorWatch = watch(
|
|
114
|
+
() => uploadLoader.status.value.isError,
|
|
115
|
+
(isError) => {
|
|
116
|
+
if (isError) {
|
|
117
|
+
updateFileItem(index, {
|
|
118
|
+
state: "error" /* ERROR */,
|
|
119
|
+
error: "\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14: " + StringHelper.getError(
|
|
120
|
+
uploadLoader.status.value.errorData
|
|
121
|
+
)
|
|
122
|
+
});
|
|
123
|
+
stopProgressWatch();
|
|
124
|
+
stopSuccessWatch();
|
|
125
|
+
stopErrorWatch();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
return uploadLoader;
|
|
130
|
+
};
|
|
131
|
+
const processFile = (file) => {
|
|
132
|
+
const validation = validateFile(file);
|
|
133
|
+
if (!validation.valid) {
|
|
134
|
+
setErrors(validation.error || "Invalid file");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (props.maxFiles && fileItems.value.length >= props.maxFiles) {
|
|
138
|
+
setErrors(`\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14\u0E44\u0E14\u0E49\u0E2A\u0E39\u0E07\u0E2A\u0E38\u0E14 ${props.maxFiles} \u0E44\u0E1F\u0E25\u0E4C`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setErrors("");
|
|
142
|
+
const fileItem = {
|
|
143
|
+
file,
|
|
144
|
+
state: "uploading" /* UPLOADING */,
|
|
145
|
+
progress: 0
|
|
146
|
+
};
|
|
147
|
+
fileItems.value = [...fileItems.value, fileItem];
|
|
148
|
+
const newIndex = fileItems.value.length - 1;
|
|
149
|
+
const uploadLoader = setupUpload(fileItem, newIndex);
|
|
150
|
+
const formData = new FormData();
|
|
151
|
+
formData.append(props.bodyKey, file);
|
|
152
|
+
uploadLoader.run({
|
|
153
|
+
data: formData
|
|
154
|
+
});
|
|
155
|
+
emits("change", fileItems.value.map((item) => item.file));
|
|
156
|
+
};
|
|
157
|
+
const updateFormValue = () => {
|
|
158
|
+
const successfulFiles = fileItems.value.filter((item) => item.state === "success" /* SUCCESS */ && item.value).map((item) => item.value);
|
|
159
|
+
onChange(successfulFiles.length > 0 ? successfulFiles : void 0);
|
|
160
|
+
};
|
|
161
|
+
const handleFileDrop = (files) => {
|
|
162
|
+
if (wrapperProps.value.disabled || wrapperProps.value.readonly || !files?.length) return;
|
|
163
|
+
files.forEach((file) => processFile(file));
|
|
164
|
+
};
|
|
165
|
+
const fileDialog = useFileDialog({
|
|
166
|
+
accept: acceptedFileTypes.value || "",
|
|
167
|
+
directory: false,
|
|
168
|
+
multiple: true
|
|
169
|
+
});
|
|
170
|
+
const dropzone = useDropZone(dropzoneRef, {
|
|
171
|
+
onDrop: handleFileDrop,
|
|
172
|
+
multiple: true,
|
|
173
|
+
preventDefaultForUnhandled: false
|
|
174
|
+
});
|
|
175
|
+
const isEmpty = computed(() => fileItems.value.length === 0);
|
|
176
|
+
const hasUploading = computed(
|
|
177
|
+
() => fileItems.value.some((item) => item.state === "uploading" /* UPLOADING */)
|
|
178
|
+
);
|
|
179
|
+
const allSuccess = computed(
|
|
180
|
+
() => fileItems.value.length > 0 && fileItems.value.every((item) => item.state === "success" /* SUCCESS */)
|
|
181
|
+
);
|
|
182
|
+
fileDialog.onChange((files) => {
|
|
183
|
+
if (files?.length) {
|
|
184
|
+
Array.from(files).forEach((file) => processFile(file));
|
|
185
|
+
fileDialog.reset();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const handleOpenFile = () => {
|
|
189
|
+
if (props.maxFiles && fileItems.value.length >= props.maxFiles) {
|
|
190
|
+
setErrors(`\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E2D\u0E31\u0E1E\u0E42\u0E2B\u0E25\u0E14\u0E44\u0E14\u0E49\u0E2A\u0E39\u0E07\u0E2A\u0E38\u0E14 ${props.maxFiles} \u0E44\u0E1F\u0E25\u0E4C`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (wrapperProps.value.disabled || wrapperProps.value.readonly) return;
|
|
194
|
+
fileDialog.open();
|
|
195
|
+
};
|
|
196
|
+
const handleDeleteFile = (index) => {
|
|
197
|
+
fileItems.value.splice(index, 1);
|
|
198
|
+
updateFormValue();
|
|
199
|
+
if (fileItems.value.length === 0) {
|
|
200
|
+
setErrors("");
|
|
201
|
+
}
|
|
202
|
+
emits("delete", index);
|
|
203
|
+
};
|
|
204
|
+
const handleRetryUpload = (index) => {
|
|
205
|
+
const fileItem = fileItems.value[index];
|
|
206
|
+
if (!fileItem) return;
|
|
207
|
+
updateFileItem(index, {
|
|
208
|
+
state: "uploading" /* UPLOADING */,
|
|
209
|
+
error: void 0,
|
|
210
|
+
progress: 0
|
|
211
|
+
});
|
|
212
|
+
const uploadLoader = setupUpload(fileItem, index);
|
|
213
|
+
const formData = new FormData();
|
|
214
|
+
formData.append(props.bodyKey, fileItem.file);
|
|
215
|
+
uploadLoader.run({
|
|
216
|
+
data: formData
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
const handlePreview = (fileValue) => {
|
|
220
|
+
previewModal.open({
|
|
221
|
+
value: fileValue
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
// State
|
|
226
|
+
fileItems,
|
|
227
|
+
isEmpty,
|
|
228
|
+
hasUploading,
|
|
229
|
+
allSuccess,
|
|
230
|
+
// Upload utilities
|
|
231
|
+
dropzone,
|
|
232
|
+
// Handlers
|
|
233
|
+
handleOpenFile,
|
|
234
|
+
handleDeleteFile,
|
|
235
|
+
handleRetryUpload,
|
|
236
|
+
handlePreview
|
|
237
|
+
};
|
|
238
|
+
};
|
|
@@ -2,6 +2,7 @@ import type { Component } from '@nuxt/schema';
|
|
|
2
2
|
import type { FormContext } from 'vee-validate';
|
|
3
3
|
import type { IUploadDropzoneField } from './InputUploadDropzone/types.js';
|
|
4
4
|
import type { IUploadDropzoneAutoField } from './InputUploadDropzoneAuto/types.js';
|
|
5
|
+
import type { IUploadDropzoneAutoMultipleField } from './InputUploadDropzoneAutoMultiple/types.js';
|
|
5
6
|
import type { IDateTimeRangeField } from './InputDateTimeRange/date_range_time_field.types.js';
|
|
6
7
|
import type { ITextField } from '#core/components/Form/InputText/types';
|
|
7
8
|
import type { ISearchField } from '#core/components/Form/InputSearch/types';
|
|
@@ -75,7 +76,7 @@ export interface IFormFieldBase<I extends INPUT_TYPES, P extends IFieldProps, O>
|
|
|
75
76
|
props: P;
|
|
76
77
|
on?: O;
|
|
77
78
|
}
|
|
78
|
-
export type IFormField = ITextField | ISearchField | INumberField | ICurrencyField | ITextareaField | IToggleField | ISelectField | ICheckboxField | ISelectMultipleField | IRadioField | IDateTimeField | ITimeField | IMonthField | IDateTimeRangeField | IUploadDropzoneField | IUploadDropzoneAutoField | IWYSIWYGField | IComponentField | ITagsField | IFormFieldBase<INPUT_TYPES.COMPONENT, any, any>;
|
|
79
|
+
export type IFormField = ITextField | ISearchField | INumberField | ICurrencyField | ITextareaField | IToggleField | ISelectField | ICheckboxField | ISelectMultipleField | IRadioField | IDateTimeField | ITimeField | IMonthField | IDateTimeRangeField | IUploadDropzoneField | IUploadDropzoneAutoField | IUploadDropzoneAutoMultipleField | IWYSIWYGField | IComponentField | ITagsField | IFormFieldBase<INPUT_TYPES.COMPONENT, any, any>;
|
|
79
80
|
export interface IFileValue {
|
|
80
81
|
url: string;
|
|
81
82
|
path?: string;
|
|
@@ -9,6 +9,7 @@ export declare const uploadFileDropzoneTheme: {
|
|
|
9
9
|
actionDownloadIcon: string;
|
|
10
10
|
actionDeleteIcon: string;
|
|
11
11
|
actionRetryIcon: string;
|
|
12
|
+
errorIcon: string;
|
|
12
13
|
};
|
|
13
14
|
slots: {
|
|
14
15
|
base: string;
|
|
@@ -38,6 +39,28 @@ export declare const uploadFileDropzoneTheme: {
|
|
|
38
39
|
actionIconClass: string;
|
|
39
40
|
actionDeleteIconClass: string;
|
|
40
41
|
actionRetryBtnClass: string;
|
|
42
|
+
multipleFilesWrapper: string;
|
|
43
|
+
fileItemWrapper: string;
|
|
44
|
+
uploadingItemWrapper: string;
|
|
45
|
+
uploadingIconWrapper: string;
|
|
46
|
+
uploadingIconClass: string;
|
|
47
|
+
uploadingTextWrapper: string;
|
|
48
|
+
progressBarWrapper: string;
|
|
49
|
+
progressBarFill: string;
|
|
50
|
+
successItemWrapper: string;
|
|
51
|
+
successImgWrapper: string;
|
|
52
|
+
successImgClass: string;
|
|
53
|
+
successFileWrapper: string;
|
|
54
|
+
successFileClass: string;
|
|
55
|
+
successTextWrapper: string;
|
|
56
|
+
errorItemWrapper: string;
|
|
57
|
+
errorIconWrapper: string;
|
|
58
|
+
errorIconClass: string;
|
|
59
|
+
errorTextWrapper: string;
|
|
60
|
+
errorActionWrapper: string;
|
|
61
|
+
addMoreWrapper: string;
|
|
62
|
+
addMoreButton: string;
|
|
63
|
+
addMoreIcon: string;
|
|
41
64
|
};
|
|
42
65
|
variants: {
|
|
43
66
|
dragover: {
|
|
@@ -8,7 +8,8 @@ export const uploadFileDropzoneTheme = {
|
|
|
8
8
|
actionPreviewIcon: "ic:outline-remove-red-eye",
|
|
9
9
|
actionDownloadIcon: "material-symbols:download",
|
|
10
10
|
actionDeleteIcon: "material-symbols:delete",
|
|
11
|
-
actionRetryIcon: "stash:arrow-retry"
|
|
11
|
+
actionRetryIcon: "stash:arrow-retry",
|
|
12
|
+
errorIcon: "i-heroicons:exclamation-circle-solid"
|
|
12
13
|
},
|
|
13
14
|
slots: {
|
|
14
15
|
base: "relative w-full text-base p-4 transition rounded-lg flex items-center justify-center ring-1 bg-white ring-accented",
|
|
@@ -28,7 +29,7 @@ export const uploadFileDropzoneTheme = {
|
|
|
28
29
|
// Preview state
|
|
29
30
|
onPreviewWrapper: "flex items-center space-x-4 rounded-lg w-full",
|
|
30
31
|
onPreviewImgWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden bg-gray-100",
|
|
31
|
-
onPreviewImgClass: "w-full h-full object-
|
|
32
|
+
onPreviewImgClass: "w-full h-full object-fit",
|
|
32
33
|
onPreviewFileWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden",
|
|
33
34
|
onPreviewFileClass: "size-8 text-gray-400 m-auto",
|
|
34
35
|
onPreviewTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
|
|
@@ -41,7 +42,34 @@ export const uploadFileDropzoneTheme = {
|
|
|
41
42
|
actionWrapper: "flex items-center space-x-2",
|
|
42
43
|
actionIconClass: "size-6 text-dimmed hover:text-dimmed-600 cursor-pointer transition-colors",
|
|
43
44
|
actionDeleteIconClass: "size-6 text-(--ui-color-error-500) hover:text-(--ui-color-error-600) cursor-pointer transition-colors",
|
|
44
|
-
actionRetryBtnClass: "px-0"
|
|
45
|
+
actionRetryBtnClass: "px-0",
|
|
46
|
+
// Multiple files
|
|
47
|
+
multipleFilesWrapper: "flex flex-col gap-3 w-full mt-4",
|
|
48
|
+
fileItemWrapper: "w-full ring-1 bg-white ring-accented rounded-lg p-3",
|
|
49
|
+
// Uploading item
|
|
50
|
+
uploadingItemWrapper: "flex items-center space-x-4 w-full",
|
|
51
|
+
uploadingIconWrapper: "flex-shrink-0",
|
|
52
|
+
uploadingIconClass: "size-12 text-gray-400",
|
|
53
|
+
uploadingTextWrapper: "flex-1 min-w-0",
|
|
54
|
+
progressBarWrapper: "w-full bg-gray-200 rounded-full h-2 overflow-hidden",
|
|
55
|
+
progressBarFill: "bg-primary h-full transition-all duration-300",
|
|
56
|
+
// Success item
|
|
57
|
+
successItemWrapper: "flex items-center space-x-4 rounded-lg w-full",
|
|
58
|
+
successImgWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden bg-gray-100",
|
|
59
|
+
successImgClass: "w-full h-full object-fit",
|
|
60
|
+
successFileWrapper: "flex-shrink-0 w-16 h-16 flex justify-center items-center rounded-lg overflow-hidden",
|
|
61
|
+
successFileClass: "size-8 text-gray-400 m-auto",
|
|
62
|
+
successTextWrapper: "flex-1 min-w-0 flex items-center justify-between",
|
|
63
|
+
// Error item
|
|
64
|
+
errorItemWrapper: "flex items-center space-x-4 w-full rounded-lg bg-(--ui-color-error-50) p-3",
|
|
65
|
+
errorIconWrapper: "flex-shrink-0",
|
|
66
|
+
errorIconClass: "size-12 text-(--ui-color-error-500)",
|
|
67
|
+
errorTextWrapper: "flex-1 min-w-0",
|
|
68
|
+
errorActionWrapper: "flex items-center space-x-2",
|
|
69
|
+
// Add more button
|
|
70
|
+
addMoreWrapper: "w-full mt-3 pt-3 border-t border-gray-200",
|
|
71
|
+
addMoreButton: "w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-primary border border-primary rounded-lg hover:bg-primary-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
|
72
|
+
addMoreIcon: "size-4"
|
|
45
73
|
},
|
|
46
74
|
variants: {
|
|
47
75
|
dragover: {
|