@finema/core 1.4.90 → 1.4.92
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 +63 -63
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -2
- package/dist/runtime/components/Alert.vue +48 -48
- package/dist/runtime/components/Avatar.vue +27 -27
- package/dist/runtime/components/Badge.vue +53 -53
- package/dist/runtime/components/Breadcrumb.vue +44 -44
- package/dist/runtime/components/Button/Group.vue +37 -37
- package/dist/runtime/components/Button/index.vue +76 -76
- package/dist/runtime/components/Card.vue +38 -38
- package/dist/runtime/components/Core.vue +13 -13
- package/dist/runtime/components/Dialog/index.vue +108 -108
- package/dist/runtime/components/Dropdown/index.vue +70 -70
- package/dist/runtime/components/FlexDeck/Base.vue +90 -90
- package/dist/runtime/components/FlexDeck/index.vue +66 -66
- package/dist/runtime/components/Form/FieldWrapper.vue +23 -23
- package/dist/runtime/components/Form/Fields.vue +168 -160
- package/dist/runtime/components/Form/InputCheckbox/index.vue +21 -21
- package/dist/runtime/components/Form/InputDateTime/index.vue +60 -60
- package/dist/runtime/components/Form/InputNumber/index.vue +27 -27
- package/dist/runtime/components/Form/InputRadio/index.vue +27 -27
- package/dist/runtime/components/Form/InputSelect/index.vue +36 -36
- package/dist/runtime/components/Form/InputStatic/index.vue +16 -16
- package/dist/runtime/components/Form/InputText/index.vue +67 -67
- package/dist/runtime/components/Form/InputTextarea/index.vue +25 -25
- package/dist/runtime/components/Form/InputToggle/index.vue +14 -14
- package/dist/runtime/components/Form/InputUploadDropzone/index.vue +205 -158
- package/dist/runtime/components/Form/InputUploadDropzoneAuto/index.vue +349 -243
- package/dist/runtime/components/Form/InputUploadDropzoneAuto/types.d.ts +3 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/Item.vue +282 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/index.vue +117 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/types.d.ts +22 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/types.mjs +0 -0
- package/dist/runtime/components/Form/InputUploadFileClassic/index.vue +101 -101
- package/dist/runtime/components/Form/InputUploadFileClassicAuto/index.vue +174 -174
- package/dist/runtime/components/Form/index.vue +6 -6
- package/dist/runtime/components/Form/types.d.ts +4 -2
- package/dist/runtime/components/Form/types.mjs +1 -0
- package/dist/runtime/components/Icon.vue +23 -23
- package/dist/runtime/components/Image.vue +36 -36
- package/dist/runtime/components/Loader.vue +27 -27
- package/dist/runtime/components/Modal/index.vue +146 -146
- package/dist/runtime/components/SimplePagination.vue +96 -96
- package/dist/runtime/components/Slideover/index.vue +110 -110
- package/dist/runtime/components/Table/Base.vue +139 -139
- package/dist/runtime/components/Table/ColumnDate.vue +16 -16
- package/dist/runtime/components/Table/ColumnDateTime.vue +18 -18
- package/dist/runtime/components/Table/ColumnImage.vue +15 -15
- package/dist/runtime/components/Table/ColumnNumber.vue +14 -14
- package/dist/runtime/components/Table/ColumnText.vue +25 -25
- package/dist/runtime/components/Table/Simple.vue +69 -69
- package/dist/runtime/components/Table/index.vue +65 -65
- package/dist/runtime/components/Tabs/index.vue +64 -64
- package/dist/runtime/helpers/componentHelper.d.ts +9 -0
- package/dist/runtime/helpers/componentHelper.mjs +30 -0
- package/dist/runtime/ui.config/uploadFileDropzone.d.ts +36 -13
- package/dist/runtime/ui.config/uploadFileDropzone.mjs +42 -19
- package/dist/runtime/utils/StringHelper.d.ts +1 -0
- package/dist/runtime/utils/StringHelper.mjs +6 -0
- package/package.json +88 -88
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="[
|
|
4
|
+
ui.base,
|
|
5
|
+
ui.background.default,
|
|
6
|
+
{
|
|
7
|
+
[ui.failed]: upload.status.value.isError || errMsg,
|
|
8
|
+
},
|
|
9
|
+
]"
|
|
10
|
+
>
|
|
11
|
+
<!-- Loading State -->
|
|
12
|
+
<div v-if="selectedFile && upload.status.value.isLoading" :class="[ui.onLoading.wrapper]">
|
|
13
|
+
<div :class="[ui.onLoading.placeholderWrapper]">
|
|
14
|
+
<Icon
|
|
15
|
+
:name="
|
|
16
|
+
isImage(selectedFile)
|
|
17
|
+
? ui.onLoading.placeholderImgIcon || ui.default.placeholderImgIcon
|
|
18
|
+
: ui.onLoading.placeholderFileIcon || ui.default.filePreviewIcon
|
|
19
|
+
"
|
|
20
|
+
:class="[ui.onLoading.placeholderIconClass]"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<div :class="[ui.onLoading.textWrapper]">
|
|
24
|
+
<div class="truncate">
|
|
25
|
+
<h1 class="truncate font-bold">{{ selectedFile?.name }}</h1>
|
|
26
|
+
<p class="truncate font-light text-gray-400">
|
|
27
|
+
{{
|
|
28
|
+
fileAllocate.isSelectedFileUseMb
|
|
29
|
+
? `${fileAllocate.selectedFileSizeMb} MB`
|
|
30
|
+
: `${fileAllocate.selectedFileSizeKb} KB`
|
|
31
|
+
}}
|
|
32
|
+
- {{ percent }}% {{ uploadingLabel ?? 'กำลังอัพโหลด...' }}
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<Icon
|
|
37
|
+
:name="ui.onLoading.loadingIcon || ui.default.loadingIcon"
|
|
38
|
+
:class="[ui.onLoading.loadingIconClass]"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Success State -->
|
|
45
|
+
<div v-if="selectedFile && upload.status.value.isSuccess" :class="[ui.onPreview.wrapper]">
|
|
46
|
+
<div :class="[ui.onPreview.previewImgWrapper]">
|
|
47
|
+
<div v-if="isImage(selectedFile)" class="size-full overflow-hidden">
|
|
48
|
+
<img
|
|
49
|
+
:src="upload.data.value[responseKey || 'url']"
|
|
50
|
+
:class="[ui.onPreview.previewImgClass]"
|
|
51
|
+
alt="image-preview"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div v-else>
|
|
55
|
+
<Icon
|
|
56
|
+
:name="ui.onPreview.previewFileIcon || ui.default.filePreviewIcon"
|
|
57
|
+
:class="[ui.onPreview.previewFileClass]"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div :class="[ui.onPreview.textWrapper]">
|
|
62
|
+
<div class="truncate">
|
|
63
|
+
<h1 class="truncate font-bold" :title="selectedFile?.name">
|
|
64
|
+
{{ StringHelper.truncate(selectedFile?.name, 40) }}
|
|
65
|
+
</h1>
|
|
66
|
+
<p class="truncate text-sm font-light text-gray-400">
|
|
67
|
+
{{
|
|
68
|
+
fileAllocate.isSelectedFileUseMb
|
|
69
|
+
? `${fileAllocate.selectedFileSizeMb} MB`
|
|
70
|
+
: `${fileAllocate.selectedFileSizeKb} KB`
|
|
71
|
+
}}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div :class="[ui.action.wrapper]">
|
|
75
|
+
<Icon
|
|
76
|
+
v-if="isImage(selectedFile)"
|
|
77
|
+
:name="ui.action.previewIcon"
|
|
78
|
+
:class="[ui.action.iconClass]"
|
|
79
|
+
title="ดูตัวอย่าง"
|
|
80
|
+
@click="() => (isPreviewOpen = true)"
|
|
81
|
+
/>
|
|
82
|
+
<Icon
|
|
83
|
+
:name="ui.action.downloadIcon"
|
|
84
|
+
:class="[ui.action.iconClass]"
|
|
85
|
+
title="ดาวน์โหลดไฟล์"
|
|
86
|
+
@click="handleDownloadFile"
|
|
87
|
+
/>
|
|
88
|
+
<Icon
|
|
89
|
+
:name="ui.action.deleteIcon"
|
|
90
|
+
:class="[ui.action.iconClass]"
|
|
91
|
+
title="ลบไฟล์"
|
|
92
|
+
@click="handleDeleteFile"
|
|
93
|
+
/>
|
|
94
|
+
<Modal v-model="isPreviewOpen" :title="selectedFile?.name">
|
|
95
|
+
<img :src="URL.createObjectURL(props.selectedFile)" alt="image-preview" />
|
|
96
|
+
</Modal>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- Failed State -->
|
|
102
|
+
<div
|
|
103
|
+
v-if="selectedFile && (upload.status.value.isError || errMsg)"
|
|
104
|
+
:class="[ui.onFailed.wrapper]"
|
|
105
|
+
>
|
|
106
|
+
<div :class="[ui.onFailed.failedImgWrapper]">
|
|
107
|
+
<Icon
|
|
108
|
+
:name="
|
|
109
|
+
isImage(selectedFile)
|
|
110
|
+
? ui.onFailed.failedImgIcon || ui.default.placeholderImgIcon
|
|
111
|
+
: ui.onFailed.failedFileIcon || ui.default.filePreviewIcon
|
|
112
|
+
"
|
|
113
|
+
:class="[ui.onFailed.failedIconClass]"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
<div :class="[ui.onFailed.textWrapper]">
|
|
117
|
+
<div class="truncate">
|
|
118
|
+
<h1 class="truncate font-bold" :title="selectedFile?.name">
|
|
119
|
+
{{ StringHelper.truncate(selectedFile?.name, 40) }}
|
|
120
|
+
</h1>
|
|
121
|
+
<p class="text-danger truncate font-light">
|
|
122
|
+
{{ errMsg ? errMsg : uploadFailedLabel || 'อัพโหลดล้มเหลว, กรุณาลองอีกครั้ง' }}
|
|
123
|
+
</p>
|
|
124
|
+
<Button
|
|
125
|
+
v-if="upload.status.value.isError"
|
|
126
|
+
variant="ghost"
|
|
127
|
+
:label="retryLabel || 'ลองอีกครั้ง'"
|
|
128
|
+
:leading-icon="ui.action.retryIcon"
|
|
129
|
+
:class="[ui.action.retryBtnClass]"
|
|
130
|
+
size="sm"
|
|
131
|
+
@click="handleRetryUpload"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
<div :class="[ui.action.wrapper]">
|
|
135
|
+
<Icon
|
|
136
|
+
title="ลบไฟล์"
|
|
137
|
+
:name="ui.action.deleteIcon"
|
|
138
|
+
:class="[ui.action.deleteIconClass]"
|
|
139
|
+
@click="handleDeleteFile"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
<script lang="ts" setup>
|
|
147
|
+
import {
|
|
148
|
+
checkFileType,
|
|
149
|
+
checkMaxSize,
|
|
150
|
+
downloadFileFromURL,
|
|
151
|
+
getFileAllocate,
|
|
152
|
+
isImage,
|
|
153
|
+
} from '#core/helpers/componentHelper'
|
|
154
|
+
import { type uploadFileDropzone } from '#core/ui.config'
|
|
155
|
+
import type { IUploadDropzoneAutoMultipleProps } from '#core/components/Form/InputUploadDropzoneAutoMultiple/types'
|
|
156
|
+
import {
|
|
157
|
+
computed,
|
|
158
|
+
type IUploadRequest,
|
|
159
|
+
onMounted,
|
|
160
|
+
ref,
|
|
161
|
+
StringHelper,
|
|
162
|
+
useUploadLoader,
|
|
163
|
+
useWatchTrue,
|
|
164
|
+
} from '#imports'
|
|
165
|
+
import i18next from 'i18next'
|
|
166
|
+
|
|
167
|
+
const emits = defineEmits(['success', 'error', 'delete'])
|
|
168
|
+
const props = defineProps<
|
|
169
|
+
{
|
|
170
|
+
ui: typeof uploadFileDropzone
|
|
171
|
+
selectedFile: File
|
|
172
|
+
} & IUploadDropzoneAutoMultipleProps
|
|
173
|
+
>()
|
|
174
|
+
|
|
175
|
+
const request: IUploadRequest = {
|
|
176
|
+
pathURL: props.uploadPathURL,
|
|
177
|
+
requestOptions: props.requestOptions,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const isPreviewOpen = ref<boolean>(false)
|
|
181
|
+
const percent = ref<number | string>(0)
|
|
182
|
+
const upload = useUploadLoader(request)
|
|
183
|
+
const errMsg = ref<string>('')
|
|
184
|
+
|
|
185
|
+
const acceptFile = computed(() =>
|
|
186
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const fileAllocate = computed(() =>
|
|
190
|
+
getFileAllocate(props.maxSize ?? 0, props.selectedFile?.size ?? 0)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
const handleDeleteFile = () => {
|
|
194
|
+
emits('delete')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const handleRetryUpload = () => {
|
|
198
|
+
const formData = new FormData()
|
|
199
|
+
|
|
200
|
+
formData.append(props.bodyKey || 'file', props.selectedFile)
|
|
201
|
+
upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const handleDownloadFile = () => {
|
|
205
|
+
downloadFileFromURL(URL.createObjectURL(props.selectedFile), props.selectedFile.name)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const onUploadProgress = (progressEvent: ProgressEvent) => {
|
|
209
|
+
percent.value = StringHelper.withFixed(
|
|
210
|
+
(Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const onDownloadProgress = (progressEvent: ProgressEvent) => {
|
|
215
|
+
if (progressEvent.total === 0) {
|
|
216
|
+
percent.value = 100
|
|
217
|
+
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
percent.value = StringHelper.withFixed(
|
|
222
|
+
(Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const handleCheckFileCondition = (file: File | undefined): boolean => {
|
|
227
|
+
if (!file) return false
|
|
228
|
+
|
|
229
|
+
const fileType = checkFileType(file, acceptFile.value ?? '')
|
|
230
|
+
|
|
231
|
+
if (!fileType) {
|
|
232
|
+
errMsg.value = i18next.t('custom:invalid_file_type')
|
|
233
|
+
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const maxSize = checkMaxSize(file, fileAllocate.value.acceptFileSizeKb ?? 0)
|
|
238
|
+
|
|
239
|
+
if (!maxSize) {
|
|
240
|
+
if (fileAllocate.value.isAcceptFileUseMb) {
|
|
241
|
+
errMsg.value = i18next.t('custom:invalid_file_size_mb', {
|
|
242
|
+
size: fileAllocate.value.acceptFileSizeMb,
|
|
243
|
+
})
|
|
244
|
+
} else {
|
|
245
|
+
errMsg.value = i18next.t('custom:invalid_file_size_kb', {
|
|
246
|
+
size: fileAllocate.value.acceptFileSizeKb,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
errMsg.value = ''
|
|
254
|
+
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onMounted(() => {
|
|
259
|
+
const result = handleCheckFileCondition(props.selectedFile)
|
|
260
|
+
|
|
261
|
+
if (result) {
|
|
262
|
+
const formData = new FormData()
|
|
263
|
+
|
|
264
|
+
formData.append(props.bodyKey || 'file', props.selectedFile)
|
|
265
|
+
upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
useWatchTrue(
|
|
270
|
+
() => upload.status.value.isSuccess,
|
|
271
|
+
() => {
|
|
272
|
+
emits('success', upload.data.value)
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
useWatchTrue(
|
|
277
|
+
() => upload.status.value.isError,
|
|
278
|
+
() => {
|
|
279
|
+
emits('error', upload.status.value.errorData)
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
</script>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper v-bind="wrapperProps">
|
|
3
|
+
<div class="space-y-3">
|
|
4
|
+
<div
|
|
5
|
+
ref="dropzoneRef"
|
|
6
|
+
:class="[
|
|
7
|
+
ui.base,
|
|
8
|
+
{
|
|
9
|
+
[ui.disabled]: isDisabled,
|
|
10
|
+
[ui.background.default]: !isOverDropZone && !isDisabled,
|
|
11
|
+
[ui.background.dragover]: isOverDropZone && !isDisabled,
|
|
12
|
+
},
|
|
13
|
+
]"
|
|
14
|
+
>
|
|
15
|
+
<input
|
|
16
|
+
ref="fileInputRef"
|
|
17
|
+
type="file"
|
|
18
|
+
class="hidden"
|
|
19
|
+
:name="name"
|
|
20
|
+
:accept="acceptFile"
|
|
21
|
+
:disabled="isDisabled"
|
|
22
|
+
multiple
|
|
23
|
+
@change="handleChange"
|
|
24
|
+
/>
|
|
25
|
+
<div :class="[ui.wrapper]">
|
|
26
|
+
<div :class="[ui.placeholderWrapper]">
|
|
27
|
+
<Icon :name="ui.default.uploadIcon" :class="[ui.labelIcon]" />
|
|
28
|
+
<div :class="[ui.labelWrapper]">
|
|
29
|
+
<p class="text-primary cursor-pointer" @click="handleOpenFile">
|
|
30
|
+
{{ selectFileLabel ?? 'คลิกเพื่อเลือกไฟล์' }}
|
|
31
|
+
</p>
|
|
32
|
+
<p>{{ selectFileSubLabel ?? 'หรือ ลากและวางที่นี่' }}</p>
|
|
33
|
+
</div>
|
|
34
|
+
<p v-if="placeholder" :class="[ui.placeholder]">{{ placeholder }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<Item
|
|
39
|
+
v-for="(file, index) in selectedFiles"
|
|
40
|
+
:key="file.name + index"
|
|
41
|
+
v-bind="$props"
|
|
42
|
+
:ui="ui"
|
|
43
|
+
:selected-file="file"
|
|
44
|
+
@success="handleSuccess(index, $event)"
|
|
45
|
+
@delete="handleDeleteFile(index)"
|
|
46
|
+
@error="handleError(index, $event)"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</FieldWrapper>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script lang="ts" setup>
|
|
53
|
+
import { useDropZone } from '@vueuse/core'
|
|
54
|
+
import { type IUploadDropzoneAutoMultipleProps } from './types'
|
|
55
|
+
import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
|
|
56
|
+
import { useFieldHOC } from '#core/composables/useForm'
|
|
57
|
+
import { _get, computed, ref, toRef, useUI, useUiConfig } from '#imports'
|
|
58
|
+
import { uploadFileDropzone } from '#core/ui.config'
|
|
59
|
+
import Item from './Item.vue'
|
|
60
|
+
|
|
61
|
+
const config = useUiConfig<typeof uploadFileDropzone>(uploadFileDropzone, 'uploadFileDropzone')
|
|
62
|
+
|
|
63
|
+
const props = withDefaults(defineProps<IUploadDropzoneAutoMultipleProps>(), {})
|
|
64
|
+
const emits = defineEmits(['change', 'success', 'delete'])
|
|
65
|
+
const selectedFiles = ref<File[]>([])
|
|
66
|
+
|
|
67
|
+
const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File[]>(props)
|
|
68
|
+
|
|
69
|
+
const { ui } = useUI('uploadFileDropzone', toRef(props, 'ui'), config)
|
|
70
|
+
|
|
71
|
+
const fileInputRef = ref<HTMLInputElement>()
|
|
72
|
+
const dropzoneRef = ref<HTMLDivElement>()
|
|
73
|
+
|
|
74
|
+
const acceptFile = computed(() =>
|
|
75
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const onDrop = (files: File[] | null) => {
|
|
79
|
+
if (props.isDisabled || files?.length === 0 || !files) return
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
selectedFiles.value = [file, ...selectedFiles.value]
|
|
83
|
+
emits('change', value.value)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { isOverDropZone } = useDropZone(dropzoneRef as unknown as HTMLElement, {
|
|
88
|
+
onDrop,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const handleChange = (e: Event) => {
|
|
92
|
+
if (props.isDisabled) return
|
|
93
|
+
|
|
94
|
+
for (const file of (e.target as HTMLInputElement).files ?? []) {
|
|
95
|
+
selectedFiles.value = [file, ...selectedFiles.value]
|
|
96
|
+
emits('change', value.value)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleOpenFile = () => {
|
|
101
|
+
fileInputRef.value?.click()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleDeleteFile = (index: number) => {
|
|
105
|
+
selectedFiles.value.splice(index, 1)
|
|
106
|
+
onChange(undefined)
|
|
107
|
+
emits('delete')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handleError = (index: number, error: any) => {}
|
|
111
|
+
|
|
112
|
+
const handleSuccess = (index: number, any: any) => {
|
|
113
|
+
value.value = [_get(any, props.responseKey || 'url'), ...(value.value || [])]
|
|
114
|
+
emits('change', value.value)
|
|
115
|
+
emits('success', value.value)
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { type IFieldProps, type IFormFieldBase, type INPUT_TYPES } from '../types';
|
|
3
|
+
export interface IUploadDropzoneAutoMultipleProps extends IFieldProps {
|
|
4
|
+
requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
|
|
5
|
+
baseURL: string;
|
|
6
|
+
};
|
|
7
|
+
uploadPathURL?: string;
|
|
8
|
+
selectFileLabel?: string;
|
|
9
|
+
selectFileSubLabel?: string;
|
|
10
|
+
uploadingLabel?: string;
|
|
11
|
+
uploadFailedLabel?: string;
|
|
12
|
+
retryLabel?: string;
|
|
13
|
+
accept?: string[] | string;
|
|
14
|
+
bodyKey?: string;
|
|
15
|
+
responseKey?: string;
|
|
16
|
+
maxSize?: number;
|
|
17
|
+
}
|
|
18
|
+
export type IUploadDropzoneAutoMultipleField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE, IUploadDropzoneAutoMultipleProps, {
|
|
19
|
+
change: (value: File[] | undefined) => void;
|
|
20
|
+
success: (res: any) => void;
|
|
21
|
+
delete: () => void;
|
|
22
|
+
}>;
|
|
File without changes
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<FieldWrapper v-bind="wrapperProps">
|
|
3
|
-
<div :class="[ui.base]">
|
|
4
|
-
<input
|
|
5
|
-
ref="fileInput"
|
|
6
|
-
type="file"
|
|
7
|
-
class="hidden"
|
|
8
|
-
:name="name"
|
|
9
|
-
:accept="acceptFile"
|
|
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
|
-
{{ value?.name ?? placeholder ?? 'No file chosen' }}
|
|
18
|
-
</p>
|
|
19
|
-
<Badge v-if="value" size="xs" variant="outline">
|
|
20
|
-
{{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
|
|
21
|
-
</Badge>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
</FieldWrapper>
|
|
26
|
-
</template>
|
|
27
|
-
<script lang="ts" setup>
|
|
28
|
-
import { useFieldHOC } from '#core/composables/useForm'
|
|
29
|
-
import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
|
|
30
|
-
import { type IUploadFileClassicFieldProps } from '#core/components/Form/InputUploadFileClassic/types'
|
|
31
|
-
import { useUiConfig } from '#core/composables/useConfig'
|
|
32
|
-
import { uploadFileInputClassicAuto } from '#core/ui.config'
|
|
33
|
-
import { computed, ref, toRef, useUI } from '#imports'
|
|
34
|
-
import i18next from 'i18next'
|
|
35
|
-
|
|
36
|
-
const config = useUiConfig<typeof uploadFileInputClassicAuto>(
|
|
37
|
-
uploadFileInputClassicAuto,
|
|
38
|
-
'uploadFileInputClassicAuto'
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
const emits = defineEmits(['change'])
|
|
42
|
-
const props = withDefaults(defineProps<IUploadFileClassicFieldProps>(), {})
|
|
43
|
-
|
|
44
|
-
const { value, wrapperProps, setErrors } = useFieldHOC<File | undefined>(props)
|
|
45
|
-
const selectedFileSizeKb = computed(() => ((value.value?.size || 0) / 1000).toFixed(2))
|
|
46
|
-
const selectedFileSizeMb = computed(() => ((value.value?.size || 0) / 1000 / 1000).toFixed(2))
|
|
47
|
-
|
|
48
|
-
const isSelectedFileUseMb = computed(() => (value.value?.size || 0) / 1000 > 1024)
|
|
49
|
-
const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
|
|
50
|
-
const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value || 0) / 1024).toFixed(2))
|
|
51
|
-
const acceptFileSizeKb = computed(() => props.maxSize)
|
|
52
|
-
const acceptFile = computed(() =>
|
|
53
|
-
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
const fileInput = ref<HTMLInputElement>()
|
|
57
|
-
|
|
58
|
-
const handleOpenFile = () => {
|
|
59
|
-
fileInput.value?.click()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const { ui } = useUI('uploadFileInputClassicAuto', toRef(props, 'ui'), config)
|
|
63
|
-
|
|
64
|
-
const checkMaxSize = (file: File): boolean => {
|
|
65
|
-
if (acceptFileSizeKb.value) {
|
|
66
|
-
return file.size / 1000 <= acceptFileSizeKb.value
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return true
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const handleCheckFileCondition = (file: File | undefined): boolean => {
|
|
73
|
-
if (!file) return false
|
|
74
|
-
|
|
75
|
-
const maxSize = checkMaxSize(file)
|
|
76
|
-
|
|
77
|
-
if (!maxSize) {
|
|
78
|
-
if (isAcceptFileUseMb.value) {
|
|
79
|
-
setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
|
|
80
|
-
} else {
|
|
81
|
-
setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
setErrors('')
|
|
88
|
-
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const handleChange = (e: Event) => {
|
|
93
|
-
const file = (e.target as HTMLInputElement).files?.[0]
|
|
94
|
-
const result = handleCheckFileCondition(file)
|
|
95
|
-
|
|
96
|
-
if (result && file) {
|
|
97
|
-
value.value = file
|
|
98
|
-
emits('change', file)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
</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
|
+
:name="name"
|
|
9
|
+
:accept="acceptFile"
|
|
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
|
+
{{ value?.name ?? placeholder ?? 'No file chosen' }}
|
|
18
|
+
</p>
|
|
19
|
+
<Badge v-if="value" size="xs" variant="outline">
|
|
20
|
+
{{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
|
|
21
|
+
</Badge>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</FieldWrapper>
|
|
26
|
+
</template>
|
|
27
|
+
<script lang="ts" setup>
|
|
28
|
+
import { useFieldHOC } from '#core/composables/useForm'
|
|
29
|
+
import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
|
|
30
|
+
import { type IUploadFileClassicFieldProps } from '#core/components/Form/InputUploadFileClassic/types'
|
|
31
|
+
import { useUiConfig } from '#core/composables/useConfig'
|
|
32
|
+
import { uploadFileInputClassicAuto } from '#core/ui.config'
|
|
33
|
+
import { computed, ref, toRef, useUI } from '#imports'
|
|
34
|
+
import i18next from 'i18next'
|
|
35
|
+
|
|
36
|
+
const config = useUiConfig<typeof uploadFileInputClassicAuto>(
|
|
37
|
+
uploadFileInputClassicAuto,
|
|
38
|
+
'uploadFileInputClassicAuto'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const emits = defineEmits(['change'])
|
|
42
|
+
const props = withDefaults(defineProps<IUploadFileClassicFieldProps>(), {})
|
|
43
|
+
|
|
44
|
+
const { value, wrapperProps, setErrors } = useFieldHOC<File | undefined>(props)
|
|
45
|
+
const selectedFileSizeKb = computed(() => ((value.value?.size || 0) / 1000).toFixed(2))
|
|
46
|
+
const selectedFileSizeMb = computed(() => ((value.value?.size || 0) / 1000 / 1000).toFixed(2))
|
|
47
|
+
|
|
48
|
+
const isSelectedFileUseMb = computed(() => (value.value?.size || 0) / 1000 > 1024)
|
|
49
|
+
const isAcceptFileUseMb = computed(() => acceptFileSizeKb.value && acceptFileSizeKb.value > 1024)
|
|
50
|
+
const acceptFileSizeMb = computed(() => ((acceptFileSizeKb.value || 0) / 1024).toFixed(2))
|
|
51
|
+
const acceptFileSizeKb = computed(() => props.maxSize)
|
|
52
|
+
const acceptFile = computed(() =>
|
|
53
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const fileInput = ref<HTMLInputElement>()
|
|
57
|
+
|
|
58
|
+
const handleOpenFile = () => {
|
|
59
|
+
fileInput.value?.click()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { ui } = useUI('uploadFileInputClassicAuto', toRef(props, 'ui'), config)
|
|
63
|
+
|
|
64
|
+
const checkMaxSize = (file: File): boolean => {
|
|
65
|
+
if (acceptFileSizeKb.value) {
|
|
66
|
+
return file.size / 1000 <= acceptFileSizeKb.value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleCheckFileCondition = (file: File | undefined): boolean => {
|
|
73
|
+
if (!file) return false
|
|
74
|
+
|
|
75
|
+
const maxSize = checkMaxSize(file)
|
|
76
|
+
|
|
77
|
+
if (!maxSize) {
|
|
78
|
+
if (isAcceptFileUseMb.value) {
|
|
79
|
+
setErrors(i18next.t('custom:invalid_file_size_mb', { size: acceptFileSizeMb.value }))
|
|
80
|
+
} else {
|
|
81
|
+
setErrors(i18next.t('custom:invalid_file_size_kb', { size: acceptFileSizeKb.value }))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setErrors('')
|
|
88
|
+
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const handleChange = (e: Event) => {
|
|
93
|
+
const file = (e.target as HTMLInputElement).files?.[0]
|
|
94
|
+
const result = handleCheckFileCondition(file)
|
|
95
|
+
|
|
96
|
+
if (result && file) {
|
|
97
|
+
value.value = file
|
|
98
|
+
emits('change', file)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
</script>
|