@finema/core 1.4.49 → 1.4.51

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.
Files changed (59) hide show
  1. package/README.md +63 -63
  2. package/dist/module.d.mts +5 -4
  3. package/dist/module.d.ts +5 -4
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +1 -1
  6. package/dist/runtime/components/Alert.vue +49 -49
  7. package/dist/runtime/components/Avatar.vue +27 -27
  8. package/dist/runtime/components/Badge.vue +54 -54
  9. package/dist/runtime/components/Breadcrumb.vue +45 -45
  10. package/dist/runtime/components/Button/Group.vue +37 -37
  11. package/dist/runtime/components/Button/index.vue +77 -77
  12. package/dist/runtime/components/Card.vue +38 -38
  13. package/dist/runtime/components/Core.vue +13 -13
  14. package/dist/runtime/components/Dialog/index.vue +108 -108
  15. package/dist/runtime/components/Dropdown/index.vue +71 -71
  16. package/dist/runtime/components/Form/FieldWrapper.vue +23 -23
  17. package/dist/runtime/components/Form/Fields.vue +153 -145
  18. package/dist/runtime/components/Form/InputCheckbox/index.vue +21 -21
  19. package/dist/runtime/components/Form/InputDateTime/index.vue +51 -51
  20. package/dist/runtime/components/Form/InputRadio/index.vue +27 -27
  21. package/dist/runtime/components/Form/InputSelect/index.vue +36 -37
  22. package/dist/runtime/components/Form/InputStatic/index.vue +16 -16
  23. package/dist/runtime/components/Form/InputText/index.vue +54 -54
  24. package/dist/runtime/components/Form/InputTextarea/index.vue +25 -25
  25. package/dist/runtime/components/Form/InputToggle/index.vue +14 -14
  26. package/dist/runtime/components/Form/InputUploadDropzone/index.vue +149 -170
  27. package/dist/runtime/components/Form/InputUploadDropzoneAuto/index.vue +238 -0
  28. package/dist/runtime/components/Form/InputUploadDropzoneAuto/types.d.ts +19 -0
  29. package/dist/runtime/components/Form/InputUploadDropzoneAuto/types.mjs +0 -0
  30. package/dist/runtime/components/Form/InputUploadFileClassic/index.vue +36 -36
  31. package/dist/runtime/components/Form/InputUploadFileClassicAuto/index.vue +165 -165
  32. package/dist/runtime/components/Form/index.vue +6 -6
  33. package/dist/runtime/components/Form/types.d.ts +4 -2
  34. package/dist/runtime/components/Form/types.mjs +1 -0
  35. package/dist/runtime/components/Icon.vue +23 -23
  36. package/dist/runtime/components/Image.vue +36 -36
  37. package/dist/runtime/components/Loader.vue +14 -14
  38. package/dist/runtime/components/Modal/index.vue +146 -146
  39. package/dist/runtime/components/SimplePagination.vue +96 -96
  40. package/dist/runtime/components/Slideover/index.vue +110 -110
  41. package/dist/runtime/components/Table/Base.vue +133 -132
  42. package/dist/runtime/components/Table/ColumnDate.vue +16 -16
  43. package/dist/runtime/components/Table/ColumnDateTime.vue +18 -18
  44. package/dist/runtime/components/Table/ColumnImage.vue +13 -13
  45. package/dist/runtime/components/Table/ColumnNumber.vue +14 -14
  46. package/dist/runtime/components/Table/Simple.vue +57 -57
  47. package/dist/runtime/components/Table/index.vue +59 -52
  48. package/dist/runtime/components/Tabs/index.vue +65 -65
  49. package/dist/runtime/core.config.d.ts +1 -0
  50. package/dist/runtime/core.config.mjs +2 -1
  51. package/dist/runtime/helpers/componentHelper.d.ts +3 -0
  52. package/dist/runtime/helpers/componentHelper.mjs +16 -0
  53. package/dist/runtime/types/utils.d.ts +29 -29
  54. package/dist/runtime/ui.config/uploadFileDropzone.d.ts +4 -1
  55. package/dist/runtime/ui.config/uploadFileDropzone.mjs +7 -4
  56. package/dist/runtime/ui.css +32 -32
  57. package/dist/runtime/utils/StringHelper.d.ts +1 -1
  58. package/dist/runtime/utils/StringHelper.mjs +2 -2
  59. package/package.json +11 -11
@@ -0,0 +1,238 @@
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <div
4
+ ref="dropzoneRef"
5
+ :class="[
6
+ ui.base,
7
+ {
8
+ [ui.disabled]: isDisabled,
9
+ [ui.background.default]: !isOverDropZone && !isDisabled,
10
+ [ui.background.dragover]: isOverDropZone && !isDisabled,
11
+ },
12
+ ]"
13
+ >
14
+ <Icon
15
+ v-if="selectedFile"
16
+ :name="ui.default.closeIcon"
17
+ :class="[ui.button.delete]"
18
+ @click="handleDeleteFile"
19
+ />
20
+ <input
21
+ ref="fileInputRef"
22
+ type="file"
23
+ class="hidden"
24
+ :accept="acceptFile"
25
+ :disabled="isDisabled"
26
+ @change="handleChange"
27
+ />
28
+ <div :class="[ui.wrapper]">
29
+ <div v-if="selectedFile" :class="[ui.preview.wrapper]">
30
+ <div
31
+ v-if="isImage(selectedFile) && upload.status.value.isSuccess"
32
+ :class="[ui.preview.image.wrapper]"
33
+ >
34
+ <img
35
+ :src="upload.data.value[props.responseKey || 'url']"
36
+ :class="[ui.preview.image.img]"
37
+ alt="file"
38
+ />
39
+ </div>
40
+ <Icon
41
+ v-else-if="!isImage(selectedFile) && upload.status.value.isSuccess"
42
+ :name="ui.default.filePreviewIcon"
43
+ :class="[ui.preview.icon]"
44
+ />
45
+ <div :class="[ui.preview.filename]">
46
+ <p class="truncate">{{ selectedFile.name }}</p>
47
+ </div>
48
+ <div class="flex items-center space-x-1">
49
+ <Badge size="xs" variant="outline" class="mt-1">
50
+ {{ isSelectedFileUseMb ? `${selectedFileSizeMb} Mb` : `${selectedFileSizeKb} Kb` }}
51
+ </Badge>
52
+ <div>
53
+ <Icon
54
+ v-if="upload.status.value.isSuccess"
55
+ name="heroicons:check-circle-20-solid"
56
+ class="text-success"
57
+ />
58
+ <Icon
59
+ v-if="upload.status.value.isError"
60
+ name="heroicons:x-circle-20-solid"
61
+ class="text-danger"
62
+ />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ <div v-if="selectedFile" :class="[ui.uploadStatus.wrapper]">
67
+ <UProgress :class="[ui.uploadStatus.progressBar]" :value="100" />
68
+ </div>
69
+ <div v-if="!selectedFile" :class="[ui.placeholderWrapper]">
70
+ <Icon :name="ui.default.uploadIcon" :class="[ui.labelIcon]" />
71
+ <div :class="[ui.labelWrapper]">
72
+ <p class="text-primary cursor-pointer" @click="handleOpenFile">
73
+ {{ selectFileLabel ?? 'คลิกเพื่อเลือกไฟล์' }}
74
+ </p>
75
+ <p>{{ selectFileSubLabel ?? 'หรือ ลากและวางที่นี่' }}</p>
76
+ </div>
77
+ <p v-if="placeholder" :class="[ui.placeholder]">{{ placeholder }}</p>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </FieldWrapper>
82
+ </template>
83
+
84
+ <script lang="ts" setup>
85
+ import { useDropZone } from '@vueuse/core'
86
+ import { type IUploadDropzoneAutoProps } from './types'
87
+ import { isImage, checkMaxSize } from '#core/helpers/componentHelper'
88
+ import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
89
+ import { useFieldHOC } from '#core/composables/useForm'
90
+ import {
91
+ type IUploadRequest,
92
+ computed,
93
+ ref,
94
+ toRef,
95
+ useUI,
96
+ useUiConfig,
97
+ useUploadLoader,
98
+ useWatchTrue,
99
+ StringHelper,
100
+ } from '#imports'
101
+ import { uploadFileDropzone } from '#core/ui.config'
102
+ import i18next from 'i18next'
103
+
104
+ const config = useUiConfig<typeof uploadFileDropzone>(uploadFileDropzone, 'uploadFileDropzone')
105
+
106
+ const props = withDefaults(defineProps<IUploadDropzoneAutoProps>(), {})
107
+ const emits = defineEmits(['change', 'success', 'delete'])
108
+
109
+ const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File>(props)
110
+
111
+ const request: IUploadRequest = {
112
+ pathURL: props.uploadPathURL,
113
+ requestOptions: props.requestOptions,
114
+ }
115
+
116
+ const upload = useUploadLoader(request)
117
+
118
+ const { ui } = useUI('uploadFileDropzone', toRef(props, 'ui'), config)
119
+
120
+ const fileInputRef = ref<HTMLInputElement>()
121
+ const dropzoneRef = ref<HTMLDivElement>()
122
+
123
+ const selectedFile = ref<File | undefined>()
124
+ const percent = ref<number>(0)
125
+
126
+ const acceptFile = computed(() =>
127
+ typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
128
+ )
129
+
130
+ const acceptFileSizeKb = computed(() => props.maxSize)
131
+ const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value ?? 0) / 1024).toFixed(2))
132
+ const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
133
+
134
+ const selectedFileSizeKb = computed(() => ((selectedFile.value?.size || 0) / 1000).toFixed(2))
135
+ const selectedFileSizeMb = computed(() =>
136
+ ((selectedFile.value?.size || 0) / 1000 / 1000).toFixed(2)
137
+ )
138
+
139
+ const isSelectedFileUseMb = computed(() => (selectedFile.value?.size || 0) / 1000 > 1024)
140
+
141
+ const onDrop = (files: File[] | null) => {
142
+ if (props.isDisabled || files?.length === 0 || !files) return
143
+
144
+ const file = files[0]
145
+ const result = handleCheckFileCondition(file)
146
+
147
+ if (result && file) {
148
+ selectedFile.value = file
149
+ const formData = new FormData()
150
+
151
+ emits('change', file)
152
+
153
+ formData.append(props.bodyKey || 'file', file)
154
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
155
+ }
156
+ }
157
+
158
+ const { isOverDropZone } = useDropZone(dropzoneRef, {
159
+ onDrop,
160
+ })
161
+
162
+ const handleChange = (e: Event) => {
163
+ if (props.isDisabled) return
164
+
165
+ const file = (e.target as HTMLInputElement).files?.[0]
166
+ const result = handleCheckFileCondition(file)
167
+
168
+ if (result && file) {
169
+ selectedFile.value = file
170
+ const formData = new FormData()
171
+
172
+ emits('change', file)
173
+
174
+ formData.append(props.bodyKey || 'file', file)
175
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
176
+ }
177
+ }
178
+
179
+ const handleOpenFile = () => {
180
+ fileInputRef.value?.click()
181
+ }
182
+
183
+ const handleDeleteFile = () => {
184
+ fileInputRef.value?.value && (fileInputRef.value.value = '')
185
+ selectedFile.value = undefined
186
+ onChange(undefined)
187
+ emits('delete')
188
+ }
189
+
190
+ const handleCheckFileCondition = (file: File | undefined): boolean => {
191
+ if (!file) return false
192
+
193
+ const maxSize = checkMaxSize(file, acceptFileSizeKb.value ?? 0)
194
+
195
+ if (!maxSize) {
196
+ if (isAcceptFileUseMb.value) {
197
+ setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
198
+ } else {
199
+ setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
200
+ }
201
+
202
+ return false
203
+ }
204
+
205
+ setErrors('')
206
+
207
+ return true
208
+ }
209
+
210
+ const onUploadProgress = (progressEvent: ProgressEvent) => {
211
+ percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
212
+ }
213
+
214
+ const onDownloadProgress = (progressEvent: ProgressEvent) => {
215
+ if (progressEvent.total === 0) {
216
+ percent.value = 100
217
+
218
+ return
219
+ }
220
+
221
+ percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
222
+ }
223
+
224
+ useWatchTrue(
225
+ () => upload.status.value.isSuccess,
226
+ () => {
227
+ value.value = upload.data.value[props.responseKey || 'url']
228
+ emits('success', upload.data.value)
229
+ }
230
+ )
231
+
232
+ useWatchTrue(
233
+ () => upload.status.value.isError,
234
+ () => {
235
+ setErrors(StringHelper.getError(upload.status.value.errorData))
236
+ }
237
+ )
238
+ </script>
@@ -0,0 +1,19 @@
1
+ import { type AxiosRequestConfig } from 'axios';
2
+ import { type IFieldProps, type IFormFieldBase, type INPUT_TYPES } from '../types';
3
+ export interface IUploadDropzoneAutoProps extends IFieldProps {
4
+ requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
5
+ baseURL: string;
6
+ };
7
+ uploadPathURL?: string;
8
+ selectFileLabel?: string;
9
+ selectFileSubLabel?: string;
10
+ accept?: string[] | string;
11
+ bodyKey?: string;
12
+ responseKey?: string;
13
+ maxSize?: number;
14
+ }
15
+ export type IUploadDropzoneAutoField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_AUTO, IUploadDropzoneAutoProps, {
16
+ change: (value: File | undefined) => void;
17
+ success: (value: string) => void;
18
+ delete: () => void;
19
+ }>;
@@ -1,36 +1,36 @@
1
- <template>
2
- <FieldWrapper v-bind="wrapperProps">
3
- <UInput
4
- type="file"
5
- trailing
6
- :loading-icon="loadingIcon"
7
- :icon="icon"
8
- :leading-icon="leadingIcon"
9
- :trailing-icon="trailingIcon"
10
- :disabled="wrapperProps.isDisabled"
11
- :name="name"
12
- :placeholder="wrapperProps.placeholder"
13
- :autofocus="!!autoFocus"
14
- :readonly="isReadonly"
15
- :accept="accept"
16
- :ui="ui"
17
- @change="handleChange"
18
- />
19
- </FieldWrapper>
20
- </template>
21
- <script lang="ts" setup>
22
- import { useFieldHOC } from '#core/composables/useForm'
23
- import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
24
- import { type IUploadFileClassicFieldProps } from '#core/components/Form/InputUploadFileClassic/types'
25
-
26
- const props = withDefaults(defineProps<IUploadFileClassicFieldProps>(), {})
27
-
28
- const { value, wrapperProps } = useFieldHOC<File | undefined>(props)
29
-
30
- const emits = defineEmits(['change'])
31
-
32
- const handleChange = (e: Event) => {
33
- value.value = (e.target as HTMLInputElement).files?.[0]
34
- emits('change', e)
35
- }
36
- </script>
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <UInput
4
+ type="file"
5
+ trailing
6
+ :loading-icon="loadingIcon"
7
+ :icon="icon"
8
+ :leading-icon="leadingIcon"
9
+ :trailing-icon="trailingIcon"
10
+ :disabled="wrapperProps.isDisabled"
11
+ :name="name"
12
+ :placeholder="wrapperProps.placeholder"
13
+ :autofocus="!!autoFocus"
14
+ :readonly="isReadonly"
15
+ :accept="accept"
16
+ :ui="ui"
17
+ @change="handleChange"
18
+ />
19
+ </FieldWrapper>
20
+ </template>
21
+ <script lang="ts" setup>
22
+ import { useFieldHOC } from '#core/composables/useForm'
23
+ import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
24
+ import { type IUploadFileClassicFieldProps } from '#core/components/Form/InputUploadFileClassic/types'
25
+
26
+ const props = withDefaults(defineProps<IUploadFileClassicFieldProps>(), {})
27
+
28
+ const { value, wrapperProps } = useFieldHOC<File | undefined>(props)
29
+
30
+ const emits = defineEmits(['change'])
31
+
32
+ const handleChange = (e: Event) => {
33
+ value.value = (e.target as HTMLInputElement).files?.[0]
34
+ emits('change', e)
35
+ }
36
+ </script>
@@ -1,165 +1,165 @@
1
- <template>
2
- <FieldWrapper v-bind="wrapperProps">
3
- <div :class="[ui.base]">
4
- <input
5
- ref="fileInput"
6
- type="file"
7
- class="hidden"
8
- :accept="acceptFile"
9
- :required="isRequired"
10
- :disabled="isDisabled"
11
- @change="handleChange"
12
- />
13
- <div :class="[ui.wrapper]">
14
- <div :class="[ui.selectFileBox]">
15
- <Button size="2xs" @click="handleOpenFile">{{ selectFileLabel || 'Choose File' }}</Button>
16
- <p :class="ui.placeholder">
17
- {{ selectedFile?.name ?? placeholder ?? 'No file chosen' }}
18
- </p>
19
- <Badge v-if="selectedFile" size="xs" variant="outline">
20
- {{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
21
- </Badge>
22
- </div>
23
- <div v-if="selectedFile">
24
- <Icon
25
- v-if="upload.status.value.isSuccess"
26
- name="heroicons:check-circle-20-solid"
27
- class="text-success"
28
- />
29
- <Icon
30
- v-if="upload.status.value.isError"
31
- name="heroicons:x-circle-20-solid"
32
- class="text-danger"
33
- />
34
- <Icon
35
- v-if="upload.status.value.isLoading"
36
- name="i-svg-spinners:180-ring-with-bg"
37
- class="text-primary"
38
- />
39
- </div>
40
- </div>
41
- </div>
42
- <img v-if="imagePreviewURL && value" :src="imagePreviewURL" alt="" :class="ui.previewURL" />
43
- </FieldWrapper>
44
- </template>
45
-
46
- <script lang="tsx" setup>
47
- import { computed, ref, StringHelper, toRef, useUI, useUiConfig, useWatchTrue } from '#imports'
48
- import { type IUploadFileProps } from './types'
49
- import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
50
- import { useFieldHOC } from '#core/composables/useForm'
51
- import { type IUploadRequest, useUploadLoader } from '#core/composables/useUpload'
52
- import { uploadFileInputClassicAuto } from '#core/ui.config'
53
- import i18next from 'i18next'
54
-
55
- const config = useUiConfig<typeof uploadFileInputClassicAuto>(
56
- uploadFileInputClassicAuto,
57
- 'uploadFileInputClassicAuto'
58
- )
59
-
60
- const emits = defineEmits(['success'])
61
- const props = withDefaults(defineProps<IUploadFileProps>(), {})
62
-
63
- const { wrapperProps, setErrors, value } = useFieldHOC<string>(props)
64
-
65
- const request: IUploadRequest = {
66
- pathURL: props.uploadPathURL,
67
- requestOptions: props.requestOptions,
68
- }
69
-
70
- const upload = useUploadLoader(request)
71
-
72
- const fileInput = ref<HTMLInputElement>()
73
- const selectedFile = ref<File | undefined>()
74
- const percent = ref<number>(0)
75
-
76
- const acceptFileSizeKb = computed(() => props.maxSize)
77
- const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value ?? 0) / 1024).toFixed(2))
78
- const selectedFileSizeKb = computed(() => ((selectedFile.value?.size || 0) / 1000).toFixed(2))
79
- const selectedFileSizeMb = computed(() =>
80
- ((selectedFile.value?.size || 0) / 1000 / 1000).toFixed(2)
81
- )
82
-
83
- const isSelectedFileUseMb = computed(() => (selectedFile.value?.size || 0) / 1000 > 1024)
84
- const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
85
-
86
- const acceptFile = computed(() =>
87
- typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
88
- )
89
-
90
- const { ui } = useUI('uploadFileInputClassicAuto', toRef(props, 'ui'), config)
91
-
92
- const handleOpenFile = () => {
93
- fileInput.value?.click()
94
- }
95
-
96
- const handleChange = (e: Event) => {
97
- const file = (e.target as HTMLInputElement).files?.[0]
98
- const result = handleCheckFileCondition(file)
99
-
100
- if (result && file) {
101
- selectedFile.value = file
102
- const formData = new FormData()
103
-
104
- formData.append(props.bodyKey || 'file', file)
105
- upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
106
- }
107
- }
108
-
109
- const handleCheckFileCondition = (file: File | undefined): boolean => {
110
- if (!file) return false
111
-
112
- const maxSize = checkMaxSize(file)
113
-
114
- if (!maxSize) {
115
- if (isAcceptFileUseMb.value) {
116
- setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
117
- } else {
118
- setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
119
- }
120
-
121
- return false
122
- }
123
-
124
- setErrors('')
125
-
126
- return true
127
- }
128
-
129
- const checkMaxSize = (file: File): boolean => {
130
- if (acceptFileSizeKb.value) {
131
- return file.size / 1000 <= acceptFileSizeKb.value
132
- }
133
-
134
- return true
135
- }
136
-
137
- const onUploadProgress = (progressEvent: ProgressEvent) => {
138
- percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
139
- }
140
-
141
- const onDownloadProgress = (progressEvent: ProgressEvent) => {
142
- if (progressEvent.total === 0) {
143
- percent.value = 100
144
-
145
- return
146
- }
147
-
148
- percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
149
- }
150
-
151
- useWatchTrue(
152
- () => upload.status.value.isSuccess,
153
- () => {
154
- value.value = upload.data.value[props.responseKey || 'url']
155
- emits('success', upload.data.value)
156
- }
157
- )
158
-
159
- useWatchTrue(
160
- () => upload.status.value.isError,
161
- () => {
162
- setErrors(StringHelper.getError(upload.status.value.errorData))
163
- }
164
- )
165
- </script>
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <div :class="[ui.base]">
4
+ <input
5
+ ref="fileInput"
6
+ type="file"
7
+ class="hidden"
8
+ :accept="acceptFile"
9
+ :required="isRequired"
10
+ :disabled="isDisabled"
11
+ @change="handleChange"
12
+ />
13
+ <div :class="[ui.wrapper]">
14
+ <div :class="[ui.selectFileBox]">
15
+ <Button size="2xs" @click="handleOpenFile">{{ selectFileLabel || 'Choose File' }}</Button>
16
+ <p :class="ui.placeholder">
17
+ {{ selectedFile?.name ?? placeholder ?? 'No file chosen' }}
18
+ </p>
19
+ <Badge v-if="selectedFile" size="xs" variant="outline">
20
+ {{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
21
+ </Badge>
22
+ </div>
23
+ <div v-if="selectedFile">
24
+ <Icon
25
+ v-if="upload.status.value.isSuccess"
26
+ name="heroicons:check-circle-20-solid"
27
+ class="text-success"
28
+ />
29
+ <Icon
30
+ v-if="upload.status.value.isError"
31
+ name="heroicons:x-circle-20-solid"
32
+ class="text-danger"
33
+ />
34
+ <Icon
35
+ v-if="upload.status.value.isLoading"
36
+ name="i-svg-spinners:180-ring-with-bg"
37
+ class="text-primary"
38
+ />
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <img v-if="imagePreviewURL && value" :src="imagePreviewURL" alt="" :class="ui.previewURL" />
43
+ </FieldWrapper>
44
+ </template>
45
+
46
+ <script lang="tsx" setup>
47
+ import { computed, ref, StringHelper, toRef, useUI, useUiConfig, useWatchTrue } from '#imports'
48
+ import { type IUploadFileProps } from './types'
49
+ import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
50
+ import { useFieldHOC } from '#core/composables/useForm'
51
+ import { type IUploadRequest, useUploadLoader } from '#core/composables/useUpload'
52
+ import { uploadFileInputClassicAuto } from '#core/ui.config'
53
+ import i18next from 'i18next'
54
+
55
+ const config = useUiConfig<typeof uploadFileInputClassicAuto>(
56
+ uploadFileInputClassicAuto,
57
+ 'uploadFileInputClassicAuto'
58
+ )
59
+
60
+ const emits = defineEmits(['success'])
61
+ const props = withDefaults(defineProps<IUploadFileProps>(), {})
62
+
63
+ const { wrapperProps, setErrors, value } = useFieldHOC<string>(props)
64
+
65
+ const request: IUploadRequest = {
66
+ pathURL: props.uploadPathURL,
67
+ requestOptions: props.requestOptions,
68
+ }
69
+
70
+ const upload = useUploadLoader(request)
71
+
72
+ const fileInput = ref<HTMLInputElement>()
73
+ const selectedFile = ref<File | undefined>()
74
+ const percent = ref<number>(0)
75
+
76
+ const selectedFileSizeKb = computed(() => ((selectedFile.value?.size || 0) / 1000).toFixed(2))
77
+ const selectedFileSizeMb = computed(() =>
78
+ ((selectedFile.value?.size || 0) / 1000 / 1000).toFixed(2)
79
+ )
80
+
81
+ const isSelectedFileUseMb = computed(() => (selectedFile.value?.size || 0) / 1000 > 1024)
82
+
83
+ const acceptFileSizeKb = computed(() => props.maxSize)
84
+ const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value || 0) / 1024).toFixed(2))
85
+ const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
86
+ const acceptFile = computed(() =>
87
+ typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
88
+ )
89
+
90
+ const { ui } = useUI('uploadFileInputClassicAuto', toRef(props, 'ui'), config)
91
+
92
+ const handleOpenFile = () => {
93
+ fileInput.value?.click()
94
+ }
95
+
96
+ const handleChange = (e: Event) => {
97
+ const file = (e.target as HTMLInputElement).files?.[0]
98
+ const result = handleCheckFileCondition(file)
99
+
100
+ if (result && file) {
101
+ selectedFile.value = file
102
+ const formData = new FormData()
103
+
104
+ formData.append(props.bodyKey || 'file', file)
105
+ upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
106
+ }
107
+ }
108
+
109
+ const handleCheckFileCondition = (file: File | undefined): boolean => {
110
+ if (!file) return false
111
+
112
+ const maxSize = checkMaxSize(file)
113
+
114
+ if (!maxSize) {
115
+ if (isAcceptFileUseMb.value) {
116
+ setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
117
+ } else {
118
+ setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
119
+ }
120
+
121
+ return false
122
+ }
123
+
124
+ setErrors('')
125
+
126
+ return true
127
+ }
128
+
129
+ const checkMaxSize = (file: File): boolean => {
130
+ if (acceptFileSizeKb.value) {
131
+ return file.size / 1000 <= acceptFileSizeKb.value
132
+ }
133
+
134
+ return true
135
+ }
136
+
137
+ const onUploadProgress = (progressEvent: ProgressEvent) => {
138
+ percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
139
+ }
140
+
141
+ const onDownloadProgress = (progressEvent: ProgressEvent) => {
142
+ if (progressEvent.total === 0) {
143
+ percent.value = 100
144
+
145
+ return
146
+ }
147
+
148
+ percent.value = (Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
149
+ }
150
+
151
+ useWatchTrue(
152
+ () => upload.status.value.isSuccess,
153
+ () => {
154
+ value.value = upload.data.value[props.responseKey || 'url']
155
+ emits('success', upload.data.value)
156
+ }
157
+ )
158
+
159
+ useWatchTrue(
160
+ () => upload.status.value.isError,
161
+ () => {
162
+ setErrors(StringHelper.getError(upload.status.value.errorData))
163
+ }
164
+ )
165
+ </script>
@@ -1,6 +1,6 @@
1
- <template>
2
- <form class="form">
3
- <slot />
4
- </form>
5
- </template>
6
- <script lang="ts" setup></script>
1
+ <template>
2
+ <form class="form">
3
+ <slot />
4
+ </form>
5
+ </template>
6
+ <script lang="ts" setup></script>