@finema/core 1.4.91 → 1.4.93
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 +17 -0
- package/dist/runtime/components/Form/InputUploadDropzone/index.vue +9 -17
- package/dist/runtime/components/Form/InputUploadDropzoneAuto/index.vue +7 -1
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/Item.vue +283 -0
- package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/index.vue +126 -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/InputUploadDropzoneImageAutoMultiple/index.vue +134 -0
- package/dist/runtime/components/Form/InputUploadDropzoneImageAutoMultiple/item.vue +198 -0
- package/dist/runtime/components/Form/InputUploadDropzoneImageAutoMultiple/types.d.ts +20 -0
- package/dist/runtime/components/Form/InputUploadDropzoneImageAutoMultiple/types.mjs +0 -0
- package/dist/runtime/components/Form/InputUploadFileClassic/index.vue +5 -3
- package/dist/runtime/components/Form/InputUploadFileClassicAuto/index.vue +9 -5
- package/dist/runtime/components/Form/types.d.ts +6 -2
- package/dist/runtime/components/Form/types.mjs +2 -0
- package/dist/runtime/composables/useNotification.d.ts +7 -0
- package/dist/runtime/composables/useNotification.mjs +39 -0
- package/dist/runtime/types/config.d.ts +1 -1
- package/dist/runtime/ui.config/index.d.ts +1 -0
- package/dist/runtime/ui.config/index.mjs +1 -0
- package/dist/runtime/ui.config/uploadDropzoneImage.d.ts +83 -0
- package/dist/runtime/ui.config/uploadDropzoneImage.mjs +26 -0
- package/dist/runtime/ui.config/uploadFileDropzone.d.ts +4 -0
- package/dist/runtime/ui.config/uploadFileDropzone.mjs +5 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
v-else-if="option.type === INPUT_TYPES.UPLOAD_FILE_CLASSIC_AUTO"
|
|
105
105
|
:class="option.class"
|
|
106
106
|
:form="form"
|
|
107
|
+
:request-options="option.props.requestOptions"
|
|
107
108
|
v-bind="getFieldBinding(option)"
|
|
108
109
|
v-on="option.on ?? {}"
|
|
109
110
|
/>
|
|
@@ -122,6 +123,22 @@
|
|
|
122
123
|
v-bind="getFieldBinding(option)"
|
|
123
124
|
v-on="option.on ?? {}"
|
|
124
125
|
/>
|
|
126
|
+
<FormInputUploadDropzoneAutoMultiple
|
|
127
|
+
v-else-if="option.type === INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE"
|
|
128
|
+
:class="option.class"
|
|
129
|
+
:form="form"
|
|
130
|
+
:request-options="option.props.requestOptions"
|
|
131
|
+
v-bind="getFieldBinding(option)"
|
|
132
|
+
v-on="option.on ?? {}"
|
|
133
|
+
/>
|
|
134
|
+
<FormInputUploadDropzoneImageAutoMultiple
|
|
135
|
+
v-else-if="option.type === INPUT_TYPES.UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE"
|
|
136
|
+
:class="option.class"
|
|
137
|
+
:form="form"
|
|
138
|
+
:request-options="option.props.requestOptions"
|
|
139
|
+
v-bind="getFieldBinding(option)"
|
|
140
|
+
v-on="option.on ?? {}"
|
|
141
|
+
/>
|
|
125
142
|
</template>
|
|
126
143
|
</div>
|
|
127
144
|
</template>
|
|
@@ -61,24 +61,12 @@
|
|
|
61
61
|
</p>
|
|
62
62
|
</div>
|
|
63
63
|
<div :class="[ui.action.wrapper]">
|
|
64
|
-
<
|
|
64
|
+
<Icon
|
|
65
65
|
v-if="isImage(selectedFile)"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<template #image>
|
|
71
|
-
<Icon :name="ui.action.previewIcon" :class="[ui.action.iconClass]" />
|
|
72
|
-
</template>
|
|
73
|
-
<template #preview="slotProps">
|
|
74
|
-
<img
|
|
75
|
-
:src="generateURL(selectedFile)"
|
|
76
|
-
alt="preview"
|
|
77
|
-
:style="slotProps.style"
|
|
78
|
-
@click="slotProps.previewCallback"
|
|
79
|
-
/>
|
|
80
|
-
</template>
|
|
81
|
-
</PImage>
|
|
66
|
+
:name="ui.action.previewIcon"
|
|
67
|
+
:class="[ui.action.iconClass]"
|
|
68
|
+
@click="() => (isPreviewOpen = true)"
|
|
69
|
+
/>
|
|
82
70
|
<Icon
|
|
83
71
|
:name="ui.action.downloadIcon"
|
|
84
72
|
:class="[ui.action.iconClass]"
|
|
@@ -89,6 +77,9 @@
|
|
|
89
77
|
:class="[ui.action.iconClass]"
|
|
90
78
|
@click="handleDeleteFile"
|
|
91
79
|
/>
|
|
80
|
+
<Modal v-model="isPreviewOpen" :title="selectedFile?.name">
|
|
81
|
+
<img :src="generateURL(selectedFile)" alt="image-preview" />
|
|
82
|
+
</Modal>
|
|
92
83
|
</div>
|
|
93
84
|
</div>
|
|
94
85
|
</div>
|
|
@@ -118,6 +109,7 @@ const config = useUiConfig<typeof uploadFileDropzone>(uploadFileDropzone, 'uploa
|
|
|
118
109
|
|
|
119
110
|
const props = withDefaults(defineProps<IUploadDropzoneProps>(), {})
|
|
120
111
|
const emit = defineEmits(['change', 'delete'])
|
|
112
|
+
const isPreviewOpen = ref<boolean>(false)
|
|
121
113
|
|
|
122
114
|
const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File>(props)
|
|
123
115
|
|
|
@@ -192,7 +192,13 @@ import i18next from 'i18next'
|
|
|
192
192
|
|
|
193
193
|
const config = useUiConfig<typeof uploadFileDropzone>(uploadFileDropzone, 'uploadFileDropzone')
|
|
194
194
|
|
|
195
|
-
const props = withDefaults(defineProps<IUploadDropzoneAutoProps>(), {
|
|
195
|
+
const props = withDefaults(defineProps<IUploadDropzoneAutoProps>(), {
|
|
196
|
+
bodyKey: 'file',
|
|
197
|
+
responseKey: 'url',
|
|
198
|
+
selectFileLabel: 'คลิกเพื่อเลือกไฟล์',
|
|
199
|
+
selectFileSubLabel: 'หรือ ลากและวางที่นี่',
|
|
200
|
+
})
|
|
201
|
+
|
|
196
202
|
const emits = defineEmits(['change', 'success', 'delete'])
|
|
197
203
|
|
|
198
204
|
const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File>(props)
|
|
@@ -0,0 +1,283 @@
|
|
|
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]"
|
|
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="generateURL(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
|
+
generateURL,
|
|
153
|
+
isImage,
|
|
154
|
+
} from '#core/helpers/componentHelper'
|
|
155
|
+
import { type uploadFileDropzone } from '#core/ui.config'
|
|
156
|
+
import type { IUploadDropzoneAutoMultipleProps } from '#core/components/Form/InputUploadDropzoneAutoMultiple/types'
|
|
157
|
+
import {
|
|
158
|
+
computed,
|
|
159
|
+
type IUploadRequest,
|
|
160
|
+
onMounted,
|
|
161
|
+
ref,
|
|
162
|
+
StringHelper,
|
|
163
|
+
useUploadLoader,
|
|
164
|
+
useWatchTrue,
|
|
165
|
+
} from '#imports'
|
|
166
|
+
import i18next from 'i18next'
|
|
167
|
+
|
|
168
|
+
const emits = defineEmits(['success', 'error', 'delete'])
|
|
169
|
+
const props = defineProps<
|
|
170
|
+
{
|
|
171
|
+
ui: typeof uploadFileDropzone
|
|
172
|
+
selectedFile: File
|
|
173
|
+
} & IUploadDropzoneAutoMultipleProps
|
|
174
|
+
>()
|
|
175
|
+
|
|
176
|
+
const request: IUploadRequest = {
|
|
177
|
+
pathURL: props.uploadPathURL,
|
|
178
|
+
requestOptions: props.requestOptions,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isPreviewOpen = ref<boolean>(false)
|
|
182
|
+
const percent = ref<number | string>(0)
|
|
183
|
+
const upload = useUploadLoader(request)
|
|
184
|
+
const errMsg = ref<string>('')
|
|
185
|
+
|
|
186
|
+
const acceptFile = computed(() =>
|
|
187
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const fileAllocate = computed(() =>
|
|
191
|
+
getFileAllocate(props.maxSize ?? 0, props.selectedFile?.size ?? 0)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const handleDeleteFile = () => {
|
|
195
|
+
emits('delete')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const handleRetryUpload = () => {
|
|
199
|
+
const formData = new FormData()
|
|
200
|
+
|
|
201
|
+
formData.append(props.bodyKey!, props.selectedFile)
|
|
202
|
+
upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const handleDownloadFile = () => {
|
|
206
|
+
downloadFileFromURL(URL.createObjectURL(props.selectedFile), props.selectedFile.name)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const onUploadProgress = (progressEvent: ProgressEvent) => {
|
|
210
|
+
percent.value = StringHelper.withFixed(
|
|
211
|
+
(Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const onDownloadProgress = (progressEvent: ProgressEvent) => {
|
|
216
|
+
if (progressEvent.total === 0) {
|
|
217
|
+
percent.value = 100
|
|
218
|
+
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
percent.value = StringHelper.withFixed(
|
|
223
|
+
(Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const handleCheckFileCondition = (file: File | undefined): boolean => {
|
|
228
|
+
if (!file) return false
|
|
229
|
+
|
|
230
|
+
const fileType = checkFileType(file, acceptFile.value ?? '')
|
|
231
|
+
|
|
232
|
+
if (!fileType) {
|
|
233
|
+
errMsg.value = i18next.t('custom:invalid_file_type')
|
|
234
|
+
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const maxSize = checkMaxSize(file, fileAllocate.value.acceptFileSizeKb ?? 0)
|
|
239
|
+
|
|
240
|
+
if (!maxSize) {
|
|
241
|
+
if (fileAllocate.value.isAcceptFileUseMb) {
|
|
242
|
+
errMsg.value = i18next.t('custom:invalid_file_size_mb', {
|
|
243
|
+
size: fileAllocate.value.acceptFileSizeMb,
|
|
244
|
+
})
|
|
245
|
+
} else {
|
|
246
|
+
errMsg.value = i18next.t('custom:invalid_file_size_kb', {
|
|
247
|
+
size: fileAllocate.value.acceptFileSizeKb,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
errMsg.value = ''
|
|
255
|
+
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
onMounted(() => {
|
|
260
|
+
const result = handleCheckFileCondition(props.selectedFile)
|
|
261
|
+
|
|
262
|
+
if (result) {
|
|
263
|
+
const formData = new FormData()
|
|
264
|
+
|
|
265
|
+
formData.append(props.bodyKey!, props.selectedFile)
|
|
266
|
+
upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
useWatchTrue(
|
|
271
|
+
() => upload.status.value.isSuccess,
|
|
272
|
+
() => {
|
|
273
|
+
emits('success', upload.data.value)
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
useWatchTrue(
|
|
278
|
+
() => upload.status.value.isError,
|
|
279
|
+
() => {
|
|
280
|
+
emits('error', upload.status.value.errorData)
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
</script>
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
bodyKey: 'file',
|
|
65
|
+
responseKey: 'url',
|
|
66
|
+
selectFileLabel: 'คลิกเพื่อเลือกไฟล์',
|
|
67
|
+
selectFileSubLabel: 'หรือ ลากและวางที่นี่',
|
|
68
|
+
retryLabel: 'ลองอีกครั้ง',
|
|
69
|
+
uploadingLabel: 'กำลังอัพโหลด...',
|
|
70
|
+
uploadFailedLabel: 'อัพโหลดล้มเหลว, กรุณาลองอีกครั้ง',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const emits = defineEmits(['change', 'success', 'delete'])
|
|
74
|
+
const selectedFiles = ref<File[]>([])
|
|
75
|
+
|
|
76
|
+
const { wrapperProps, handleChange: onChange, setErrors, value } = useFieldHOC<File[]>(props)
|
|
77
|
+
|
|
78
|
+
const { ui } = useUI('uploadFileDropzone', toRef(props, 'ui'), config)
|
|
79
|
+
|
|
80
|
+
const fileInputRef = ref<HTMLInputElement>()
|
|
81
|
+
const dropzoneRef = ref<HTMLDivElement>()
|
|
82
|
+
|
|
83
|
+
const acceptFile = computed(() =>
|
|
84
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const onDrop = (files: File[] | null) => {
|
|
88
|
+
if (props.isDisabled || files?.length === 0 || !files) return
|
|
89
|
+
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
selectedFiles.value = [file, ...selectedFiles.value]
|
|
92
|
+
emits('change', value.value)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { isOverDropZone } = useDropZone(dropzoneRef as unknown as HTMLElement, {
|
|
97
|
+
onDrop,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const handleChange = (e: Event) => {
|
|
101
|
+
if (props.isDisabled) return
|
|
102
|
+
|
|
103
|
+
for (const file of (e.target as HTMLInputElement).files ?? []) {
|
|
104
|
+
selectedFiles.value = [file, ...selectedFiles.value]
|
|
105
|
+
emits('change', value.value)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const handleOpenFile = () => {
|
|
110
|
+
fileInputRef.value?.click()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleDeleteFile = (index: number) => {
|
|
114
|
+
selectedFiles.value.splice(index, 1)
|
|
115
|
+
onChange(undefined)
|
|
116
|
+
emits('delete')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleError = (index: number, error: any) => {}
|
|
120
|
+
|
|
121
|
+
const handleSuccess = (index: number, any: any) => {
|
|
122
|
+
value.value = [_get(any, props.responseKey), ...(value.value || [])]
|
|
123
|
+
emits('change', value.value)
|
|
124
|
+
emits('success', value.value)
|
|
125
|
+
}
|
|
126
|
+
</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
|
|
@@ -0,0 +1,134 @@
|
|
|
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 v-if="selectedFiles.length === 0" :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
|
+
|
|
37
|
+
<div v-else :class="ui.imageItemWrapper">
|
|
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
|
+
|
|
49
|
+
<Item v-bind="$props" is-adding-btn :ui="ui" @add="handleOpenFile" />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</FieldWrapper>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script lang="ts" setup>
|
|
58
|
+
import { useDropZone } from '@vueuse/core'
|
|
59
|
+
import { type IUploadDropzoneImageAutoMultipleProps } from './types'
|
|
60
|
+
import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
|
|
61
|
+
import { useFieldHOC } from '#core/composables/useForm'
|
|
62
|
+
import { _get, computed, ref, toRef, useUI, useUiConfig } from '#imports'
|
|
63
|
+
import { uploadFileDropzoneImage } from '#core/ui.config'
|
|
64
|
+
import Item from './item.vue'
|
|
65
|
+
|
|
66
|
+
const config = useUiConfig<typeof uploadFileDropzoneImage>(
|
|
67
|
+
uploadFileDropzoneImage,
|
|
68
|
+
'uploadFileDropzoneImage'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const props = withDefaults(defineProps<IUploadDropzoneImageAutoMultipleProps>(), {
|
|
72
|
+
accept: 'image/*',
|
|
73
|
+
bodyKey: 'file',
|
|
74
|
+
responseKey: 'url',
|
|
75
|
+
selectFileLabel: 'คลิกเพื่อเลือกไฟล์',
|
|
76
|
+
selectFileSubLabel: 'หรือ ลากและวางที่นี่',
|
|
77
|
+
uploadAddLabel: 'อัพโหลด',
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const emits = defineEmits(['change', 'success', 'delete'])
|
|
81
|
+
const selectedFiles = ref<File[]>([])
|
|
82
|
+
|
|
83
|
+
const { wrapperProps, handleChange: onChange, value } = useFieldHOC<File[]>(props)
|
|
84
|
+
|
|
85
|
+
const { ui } = useUI('uploadFileDropzoneImage', toRef(props, 'ui'), config)
|
|
86
|
+
|
|
87
|
+
const fileInputRef = ref<HTMLInputElement>()
|
|
88
|
+
const dropzoneRef = ref<HTMLDivElement>()
|
|
89
|
+
|
|
90
|
+
const acceptFile = computed(() =>
|
|
91
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const onDrop = (files: File[] | null) => {
|
|
95
|
+
if (props.isDisabled || files?.length === 0 || !files) return
|
|
96
|
+
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
selectedFiles.value = [...selectedFiles.value, file]
|
|
99
|
+
|
|
100
|
+
emits('change', value.value)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { isOverDropZone } = useDropZone(dropzoneRef as unknown as HTMLElement, {
|
|
105
|
+
onDrop,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const handleChange = (e: Event) => {
|
|
109
|
+
if (props.isDisabled) return
|
|
110
|
+
|
|
111
|
+
for (const file of (e.target as HTMLInputElement).files ?? []) {
|
|
112
|
+
selectedFiles.value = [...selectedFiles.value, file]
|
|
113
|
+
emits('change', value.value)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleOpenFile = () => {
|
|
118
|
+
fileInputRef.value?.click()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const handleDeleteFile = (index: number) => {
|
|
122
|
+
selectedFiles.value.splice(index, 1)
|
|
123
|
+
onChange(undefined)
|
|
124
|
+
emits('delete')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const handleError = (index: number, error: any) => {}
|
|
128
|
+
|
|
129
|
+
const handleSuccess = (index: number, any: any) => {
|
|
130
|
+
value.value = [_get(any, props.responseKey), ...(value.value || [])]
|
|
131
|
+
emits('change', value.value)
|
|
132
|
+
emits('success', value.value)
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="[ui.imageItem.wrapper]">
|
|
3
|
+
<div v-if="isAddingBtn" class="w-full">
|
|
4
|
+
<div :class="[ui.action.addingWrapper]" @click="$emit('add')">
|
|
5
|
+
<Icon :name="ui.action.addingIcon" :class="[ui.action.addingBtnClass]" />
|
|
6
|
+
<p :class="[ui.action.addingTextClass]">{{ uploadAddLabel }}</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<!-- Loading State -->
|
|
11
|
+
<div v-if="selectedFile && upload.status.value.isLoading" class="w-full">
|
|
12
|
+
<div :class="[ui.imageItem.onLoading.wrapper]">
|
|
13
|
+
<div :class="[ui.imageItem.onLoading.percentClass]">{{ percent }}%</div>
|
|
14
|
+
<UProgress :value="percent" />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Success State -->
|
|
19
|
+
<div v-if="selectedFile && upload.status.value.isSuccess" class="w-full">
|
|
20
|
+
<div :class="[ui.imageItem.onPreview.wrapper]">
|
|
21
|
+
<img
|
|
22
|
+
:class="[ui.imageItem.onPreview.previewImgClass]"
|
|
23
|
+
:src="upload.data.value[responseKey]"
|
|
24
|
+
alt="img"
|
|
25
|
+
/>
|
|
26
|
+
<div :class="[ui.imageItem.onPreview.previewActionWrapper]">
|
|
27
|
+
<Icon
|
|
28
|
+
title="ดูตัวอย่าง"
|
|
29
|
+
:name="ui.action.previewIcon"
|
|
30
|
+
:class="[ui.imageItem.onPreview.actionBtnClass]"
|
|
31
|
+
@click="() => (isPreviewOpen = true)"
|
|
32
|
+
/>
|
|
33
|
+
<Icon
|
|
34
|
+
title="ลบไฟล์"
|
|
35
|
+
:name="ui.action.deleteIcon"
|
|
36
|
+
:class="[ui.imageItem.onPreview.actionBtnClass]"
|
|
37
|
+
@click="handleDeleteFile"
|
|
38
|
+
/>
|
|
39
|
+
<Modal v-model="isPreviewOpen" :title="selectedFile?.name">
|
|
40
|
+
<img :src="upload.data.value[responseKey]" alt="image-preview" />
|
|
41
|
+
</Modal>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div :class="[ui.imageItem.onPreview.previewTextWrapper]">
|
|
46
|
+
<p :class="[ui.imageItem.onPreview.previewText]">{{ selectedFile.name }}</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Failed State -->
|
|
51
|
+
<div v-if="selectedFile && upload.status.value.isError" class="w-full">
|
|
52
|
+
<div :class="[ui.imageItem.onFailed.wrapper]">
|
|
53
|
+
<img
|
|
54
|
+
:class="[ui.imageItem.onFailed.failedImgClass]"
|
|
55
|
+
:src="generateURL(selectedFile)"
|
|
56
|
+
alt="img"
|
|
57
|
+
/>
|
|
58
|
+
<div :class="[ui.imageItem.onFailed.failedActionWrapper]">
|
|
59
|
+
<Icon
|
|
60
|
+
title="ลบไฟล์"
|
|
61
|
+
:name="ui.action.deleteIcon"
|
|
62
|
+
:class="[ui.imageItem.onFailed.actionBtnClass]"
|
|
63
|
+
@click="handleDeleteFile"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script lang="ts" setup>
|
|
72
|
+
import {
|
|
73
|
+
checkFileType,
|
|
74
|
+
checkMaxSize,
|
|
75
|
+
generateURL,
|
|
76
|
+
getFileAllocate,
|
|
77
|
+
} from '#core/helpers/componentHelper'
|
|
78
|
+
import { type uploadFileDropzoneImage } from '#core/ui.config'
|
|
79
|
+
import type { IUploadDropzoneImageAutoMultipleProps } from '#core/components/Form/InputUploadDropzoneImageAutoMultiple/types'
|
|
80
|
+
import {
|
|
81
|
+
computed,
|
|
82
|
+
type IUploadRequest,
|
|
83
|
+
onMounted,
|
|
84
|
+
ref,
|
|
85
|
+
StringHelper,
|
|
86
|
+
useUploadLoader,
|
|
87
|
+
useWatchTrue,
|
|
88
|
+
} from '#imports'
|
|
89
|
+
import i18next from 'i18next'
|
|
90
|
+
|
|
91
|
+
const emits = defineEmits(['success', 'error', 'delete', 'add'])
|
|
92
|
+
const props = defineProps<
|
|
93
|
+
{
|
|
94
|
+
ui: typeof uploadFileDropzoneImage
|
|
95
|
+
selectedFile?: File
|
|
96
|
+
isAddingBtn?: boolean
|
|
97
|
+
} & IUploadDropzoneImageAutoMultipleProps
|
|
98
|
+
>()
|
|
99
|
+
|
|
100
|
+
const request: IUploadRequest = {
|
|
101
|
+
pathURL: props.uploadPathURL,
|
|
102
|
+
requestOptions: props.requestOptions,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isPreviewOpen = ref<boolean>(false)
|
|
106
|
+
const percent = ref<number | string>(0)
|
|
107
|
+
const upload = useUploadLoader(request)
|
|
108
|
+
const errMsg = ref<string>('')
|
|
109
|
+
|
|
110
|
+
const acceptFile = computed(() =>
|
|
111
|
+
typeof props.accept === 'string' ? props.accept : props.accept?.join(',')
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const fileAllocate = computed(() =>
|
|
115
|
+
getFileAllocate(props.maxSize ?? 0, props.selectedFile?.size ?? 0)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const handleDeleteFile = () => {
|
|
119
|
+
emits('delete')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const onUploadProgress = (progressEvent: ProgressEvent) => {
|
|
123
|
+
percent.value = StringHelper.withFixed(
|
|
124
|
+
(Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.8
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const onDownloadProgress = (progressEvent: ProgressEvent) => {
|
|
129
|
+
if (progressEvent.total === 0) {
|
|
130
|
+
percent.value = 100
|
|
131
|
+
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
percent.value = StringHelper.withFixed(
|
|
136
|
+
(Math.floor((progressEvent.loaded * 100) / progressEvent.total) || 0) * 0.2 + 80
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const handleCheckFileCondition = (file: File | undefined): boolean => {
|
|
141
|
+
if (!file) return false
|
|
142
|
+
|
|
143
|
+
const fileType = checkFileType(file, acceptFile.value ?? '')
|
|
144
|
+
|
|
145
|
+
if (!fileType) {
|
|
146
|
+
errMsg.value = i18next.t('custom:invalid_file_type')
|
|
147
|
+
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const maxSize = checkMaxSize(file, fileAllocate.value.acceptFileSizeKb ?? 0)
|
|
152
|
+
|
|
153
|
+
if (!maxSize) {
|
|
154
|
+
if (fileAllocate.value.isAcceptFileUseMb) {
|
|
155
|
+
errMsg.value = i18next.t('custom:invalid_file_size_mb', {
|
|
156
|
+
size: fileAllocate.value.acceptFileSizeMb,
|
|
157
|
+
})
|
|
158
|
+
} else {
|
|
159
|
+
errMsg.value = i18next.t('custom:invalid_file_size_kb', {
|
|
160
|
+
size: fileAllocate.value.acceptFileSizeKb,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
errMsg.value = ''
|
|
168
|
+
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
onMounted(() => {
|
|
173
|
+
if (props.isAddingBtn) return
|
|
174
|
+
|
|
175
|
+
const result = handleCheckFileCondition(props.selectedFile)
|
|
176
|
+
|
|
177
|
+
if (result) {
|
|
178
|
+
const formData = new FormData()
|
|
179
|
+
|
|
180
|
+
formData.append(props.bodyKey!, props.selectedFile!)
|
|
181
|
+
upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
useWatchTrue(
|
|
186
|
+
() => upload.status.value.isSuccess,
|
|
187
|
+
() => {
|
|
188
|
+
emits('success', upload.data.value)
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
useWatchTrue(
|
|
193
|
+
() => upload.status.value.isError,
|
|
194
|
+
() => {
|
|
195
|
+
emits('error', upload.status.value.errorData)
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AxiosRequestConfig } from 'axios';
|
|
2
|
+
import { type IFieldProps, type IFormFieldBase, type INPUT_TYPES } from '../types';
|
|
3
|
+
export interface IUploadDropzoneImageAutoMultipleProps extends IFieldProps {
|
|
4
|
+
requestOptions: Omit<AxiosRequestConfig, 'baseURL'> & {
|
|
5
|
+
baseURL: string;
|
|
6
|
+
};
|
|
7
|
+
uploadPathURL?: string;
|
|
8
|
+
selectFileLabel?: string;
|
|
9
|
+
selectFileSubLabel?: string;
|
|
10
|
+
uploadAddLabel?: string;
|
|
11
|
+
accept?: string[] | string;
|
|
12
|
+
bodyKey?: string;
|
|
13
|
+
responseKey?: string;
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
}
|
|
16
|
+
export type IUploadDropzoneImageAutoMultipleField = IFormFieldBase<INPUT_TYPES.UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE, IUploadDropzoneImageAutoMultipleProps, {
|
|
17
|
+
change: (value: File[] | undefined) => void;
|
|
18
|
+
success: (res: any) => void;
|
|
19
|
+
delete: () => void;
|
|
20
|
+
}>;
|
|
File without changes
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
/>
|
|
13
13
|
<div :class="[ui.wrapper]">
|
|
14
14
|
<div :class="[ui.selectFileBox]">
|
|
15
|
-
<Button size="2xs" @click="handleOpenFile">{{ selectFileLabel
|
|
15
|
+
<Button size="2xs" @click="handleOpenFile">{{ selectFileLabel }}</Button>
|
|
16
16
|
<p :class="ui.placeholder">
|
|
17
|
-
{{ value?.name ?? placeholder ?? '
|
|
17
|
+
{{ value?.name ?? placeholder ?? 'ยังไม่ได้เลือกไฟล์' }}
|
|
18
18
|
</p>
|
|
19
19
|
<Badge v-if="value" size="xs" variant="outline">
|
|
20
20
|
{{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
|
|
@@ -39,7 +39,9 @@ const config = useUiConfig<typeof uploadFileInputClassicAuto>(
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
const emits = defineEmits(['change'])
|
|
42
|
-
const props = withDefaults(defineProps<IUploadFileClassicFieldProps>(), {
|
|
42
|
+
const props = withDefaults(defineProps<IUploadFileClassicFieldProps>(), {
|
|
43
|
+
selectFileLabel: 'เลือกไฟล์',
|
|
44
|
+
})
|
|
43
45
|
|
|
44
46
|
const { value, wrapperProps, setErrors } = useFieldHOC<File | undefined>(props)
|
|
45
47
|
const selectedFileSizeKb = computed(() => ((value.value?.size || 0) / 1000).toFixed(2))
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
/>
|
|
13
13
|
<div :class="[ui.wrapper]">
|
|
14
14
|
<div :class="[ui.selectFileBox]">
|
|
15
|
-
<Button size="2xs" @click="handleOpenFile">{{ selectFileLabel
|
|
15
|
+
<Button size="2xs" @click="handleOpenFile">{{ selectFileLabel }}</Button>
|
|
16
16
|
<p :class="ui.placeholder">
|
|
17
|
-
{{ selectedFile?.name ?? placeholder ?? '
|
|
17
|
+
{{ selectedFile?.name ?? placeholder ?? 'ยังไม่ได้เลือกไฟล์' }}
|
|
18
18
|
</p>
|
|
19
19
|
<Badge v-if="selectedFile" size="xs" variant="outline">
|
|
20
20
|
{{ isSelectedFileUseMb ? `${selectedFileSizeMb} MB` : `${selectedFileSizeKb} KB` }}
|
|
@@ -67,7 +67,11 @@ const config = useUiConfig<typeof uploadFileInputClassicAuto>(
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
const emits = defineEmits(['success'])
|
|
70
|
-
const props = withDefaults(defineProps<IUploadFileProps>(), {
|
|
70
|
+
const props = withDefaults(defineProps<IUploadFileProps>(), {
|
|
71
|
+
bodyKey: 'file',
|
|
72
|
+
responseKey: 'url',
|
|
73
|
+
selectFileLabel: 'เลือกไฟล์',
|
|
74
|
+
})
|
|
71
75
|
|
|
72
76
|
const { wrapperProps, setErrors, value } = useFieldHOC<string>(props)
|
|
73
77
|
|
|
@@ -110,7 +114,7 @@ const handleChange = (e: Event) => {
|
|
|
110
114
|
selectedFile.value = file
|
|
111
115
|
const formData = new FormData()
|
|
112
116
|
|
|
113
|
-
formData.append(props.bodyKey
|
|
117
|
+
formData.append(props.bodyKey, file)
|
|
114
118
|
upload.run(formData, { data: { onUploadProgress, onDownloadProgress } })
|
|
115
119
|
}
|
|
116
120
|
}
|
|
@@ -160,7 +164,7 @@ const onDownloadProgress = (progressEvent: ProgressEvent) => {
|
|
|
160
164
|
useWatchTrue(
|
|
161
165
|
() => upload.status.value.isSuccess,
|
|
162
166
|
() => {
|
|
163
|
-
value.value = _get(upload.data.value, props.responseKey
|
|
167
|
+
value.value = _get(upload.data.value, props.responseKey)
|
|
164
168
|
emits('success', upload.data.value)
|
|
165
169
|
}
|
|
166
170
|
)
|
|
@@ -13,6 +13,8 @@ import { type IUploadFileField } from '#core/components/Form/InputUploadFileClas
|
|
|
13
13
|
import { type IUploadDropzoneField } from '#core/components/Form/InputUploadDropzone/types';
|
|
14
14
|
import { type IUploadDropzoneAutoField } from '#core/components/Form/InputUploadDropzoneAuto/types';
|
|
15
15
|
import type { INumberField } from '#core/components/Form/InputNumber/types';
|
|
16
|
+
import type { IUploadDropzoneAutoMultipleField } from '#core/components/Form/InputUploadDropzoneAutoMultiple/types';
|
|
17
|
+
import type { IUploadDropzoneImageAutoMultipleField } from '#core/components/Form/InputUploadDropzoneImageAutoMultiple/types';
|
|
16
18
|
export declare const enum INPUT_TYPES {
|
|
17
19
|
TEXT = "TEXT",
|
|
18
20
|
NUMBER = "NUMBER",
|
|
@@ -29,7 +31,9 @@ export declare const enum INPUT_TYPES {
|
|
|
29
31
|
UPLOAD_FILE_CLASSIC = "UPLOAD_FILE_CLASSIC",
|
|
30
32
|
UPLOAD_FILE_CLASSIC_AUTO = "UPLOAD_FILE_CLASSIC_AUTO",
|
|
31
33
|
UPLOAD_DROPZONE = "UPLOAD_DROPZONE",
|
|
32
|
-
UPLOAD_DROPZONE_AUTO = "UPLOAD_DROPZONE_AUTO"
|
|
34
|
+
UPLOAD_DROPZONE_AUTO = "UPLOAD_DROPZONE_AUTO",
|
|
35
|
+
UPLOAD_DROPZONE_AUTO_MULTIPLE = "UPLOAD_DROPZONE_AUTO_MULTIPLE",
|
|
36
|
+
UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE = "UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE"
|
|
33
37
|
}
|
|
34
38
|
export interface IFieldProps {
|
|
35
39
|
form?: FormContext;
|
|
@@ -58,4 +62,4 @@ export interface IFormFieldBase<I extends INPUT_TYPES, P extends IFieldProps, O>
|
|
|
58
62
|
props: P;
|
|
59
63
|
on?: O;
|
|
60
64
|
}
|
|
61
|
-
export type IFormField = ITextField | INumberField | IStaticField | ICheckboxField | IRadioField | ISelectField | IToggleField | ITextareaField | IDateTimeField | IUploadFileClassicField | IUploadFileField | IUploadDropzoneField | IUploadDropzoneAutoField;
|
|
65
|
+
export type IFormField = ITextField | INumberField | IStaticField | ICheckboxField | IRadioField | ISelectField | IToggleField | ITextareaField | IDateTimeField | IUploadFileClassicField | IUploadFileField | IUploadDropzoneField | IUploadDropzoneAutoField | IUploadDropzoneAutoMultipleField | IUploadDropzoneImageAutoMultipleField;
|
|
@@ -15,5 +15,7 @@ export var INPUT_TYPES = /* @__PURE__ */ ((INPUT_TYPES2) => {
|
|
|
15
15
|
INPUT_TYPES2["UPLOAD_FILE_CLASSIC_AUTO"] = "UPLOAD_FILE_CLASSIC_AUTO";
|
|
16
16
|
INPUT_TYPES2["UPLOAD_DROPZONE"] = "UPLOAD_DROPZONE";
|
|
17
17
|
INPUT_TYPES2["UPLOAD_DROPZONE_AUTO"] = "UPLOAD_DROPZONE_AUTO";
|
|
18
|
+
INPUT_TYPES2["UPLOAD_DROPZONE_AUTO_MULTIPLE"] = "UPLOAD_DROPZONE_AUTO_MULTIPLE";
|
|
19
|
+
INPUT_TYPES2["UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE"] = "UPLOAD_DROPZONE_IMAGE_AUTO_MULTIPLE";
|
|
18
20
|
return INPUT_TYPES2;
|
|
19
21
|
})(INPUT_TYPES || {});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useToast } from "#imports";
|
|
2
|
+
export const useNotification = () => {
|
|
3
|
+
const toast = useToast();
|
|
4
|
+
const info = (notification) => {
|
|
5
|
+
toast.add({
|
|
6
|
+
icon: "ph:info",
|
|
7
|
+
color: "info",
|
|
8
|
+
...notification
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
const success = (notification) => {
|
|
12
|
+
toast.add({
|
|
13
|
+
icon: "ph:check-circle",
|
|
14
|
+
color: "success",
|
|
15
|
+
...notification
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const warning = (notification) => {
|
|
19
|
+
toast.add({
|
|
20
|
+
icon: "ph:warning",
|
|
21
|
+
color: "warning",
|
|
22
|
+
...notification
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const error = (notification) => {
|
|
26
|
+
toast.add({
|
|
27
|
+
icon: "ph:x-circle",
|
|
28
|
+
color: "danger",
|
|
29
|
+
...notification
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
info,
|
|
34
|
+
success,
|
|
35
|
+
warning,
|
|
36
|
+
error,
|
|
37
|
+
remove: toast.remove
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type UIComponentList = 'modal' | 'slideover' | 'dropdown' | 'icon' | 'button' | 'buttonGroup' | 'tabs' | 'card' | 'breadcrumb' | 'badge' | 'input' | 'pagination' | 'notification' | 'uploadFileInputClassicAuto' | 'uploadFileDropzone';
|
|
1
|
+
export type UIComponentList = 'modal' | 'slideover' | 'dropdown' | 'icon' | 'button' | 'buttonGroup' | 'tabs' | 'card' | 'breadcrumb' | 'badge' | 'input' | 'pagination' | 'notification' | 'uploadFileInputClassicAuto' | 'uploadFileDropzone' | 'uploadFileDropzoneImage';
|
|
@@ -11,3 +11,4 @@ export { slideover } from './slideover';
|
|
|
11
11
|
export { breadcrumb } from './breadcrumb';
|
|
12
12
|
export { uploadFileInputClassicAuto } from './uploadFileInputClassicAuto';
|
|
13
13
|
export { uploadFileDropzone } from './uploadFileDropzone';
|
|
14
|
+
export { uploadFileDropzoneImage } from './uploadDropzoneImage';
|
|
@@ -11,3 +11,4 @@ export { slideover } from "./slideover.mjs";
|
|
|
11
11
|
export { breadcrumb } from "./breadcrumb.mjs";
|
|
12
12
|
export { uploadFileInputClassicAuto } from "./uploadFileInputClassicAuto.mjs";
|
|
13
13
|
export { uploadFileDropzone } from "./uploadFileDropzone.mjs";
|
|
14
|
+
export { uploadFileDropzoneImage } from "./uploadDropzoneImage.mjs";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export declare const uploadFileDropzoneImage: {
|
|
2
|
+
imageItemWrapper: string;
|
|
3
|
+
imageItem: {
|
|
4
|
+
wrapper: string;
|
|
5
|
+
onLoading: {
|
|
6
|
+
wrapper: string;
|
|
7
|
+
percentClass: string;
|
|
8
|
+
};
|
|
9
|
+
onPreview: {
|
|
10
|
+
wrapper: string;
|
|
11
|
+
previewImgClass: string;
|
|
12
|
+
previewActionWrapper: string;
|
|
13
|
+
actionBtnClass: string;
|
|
14
|
+
previewTextWrapper: string;
|
|
15
|
+
previewText: string;
|
|
16
|
+
};
|
|
17
|
+
onFailed: {
|
|
18
|
+
wrapper: string;
|
|
19
|
+
failedImgClass: string;
|
|
20
|
+
failedActionWrapper: string;
|
|
21
|
+
actionBtnClass: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
base: string;
|
|
25
|
+
wrapper: string;
|
|
26
|
+
disabled: string;
|
|
27
|
+
failed: string;
|
|
28
|
+
placeholderWrapper: string;
|
|
29
|
+
placeholder: string;
|
|
30
|
+
labelWrapper: string;
|
|
31
|
+
onLoading: {
|
|
32
|
+
wrapper: string;
|
|
33
|
+
placeholderWrapper: string;
|
|
34
|
+
placeholderImgIcon: string;
|
|
35
|
+
placeholderFileIcon: string;
|
|
36
|
+
placeholderIconClass: string;
|
|
37
|
+
textWrapper: string;
|
|
38
|
+
loadingIcon: string;
|
|
39
|
+
loadingIconClass: string;
|
|
40
|
+
};
|
|
41
|
+
onPreview: {
|
|
42
|
+
wrapper: string;
|
|
43
|
+
previewImgWrapper: string;
|
|
44
|
+
previewImgClass: string;
|
|
45
|
+
previewFileIcon: string;
|
|
46
|
+
previewFileClass: string;
|
|
47
|
+
textWrapper: string;
|
|
48
|
+
};
|
|
49
|
+
onFailed: {
|
|
50
|
+
wrapper: string;
|
|
51
|
+
failedImgWrapper: string;
|
|
52
|
+
failedImgIcon: string;
|
|
53
|
+
failedFileIcon: string;
|
|
54
|
+
failedIconClass: string;
|
|
55
|
+
textWrapper: string;
|
|
56
|
+
};
|
|
57
|
+
action: {
|
|
58
|
+
wrapper: string;
|
|
59
|
+
addingWrapper: string;
|
|
60
|
+
iconClass: string;
|
|
61
|
+
deleteIconClass: string;
|
|
62
|
+
retryBtnClass: string;
|
|
63
|
+
addingBtnClass: string;
|
|
64
|
+
addingTextClass: string;
|
|
65
|
+
previewIcon: string;
|
|
66
|
+
downloadIcon: string;
|
|
67
|
+
deleteIcon: string;
|
|
68
|
+
retryIcon: string;
|
|
69
|
+
addingIcon: string;
|
|
70
|
+
};
|
|
71
|
+
background: {
|
|
72
|
+
default: string;
|
|
73
|
+
dragover: string;
|
|
74
|
+
};
|
|
75
|
+
labelIcon: string;
|
|
76
|
+
default: {
|
|
77
|
+
filePreviewIcon: string;
|
|
78
|
+
uploadIcon: string;
|
|
79
|
+
placeholderImgIcon: string;
|
|
80
|
+
failedImgIcon: string;
|
|
81
|
+
loadingIcon: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { uploadFileDropzone } from "./uploadFileDropzone.mjs";
|
|
2
|
+
export const uploadFileDropzoneImage = {
|
|
3
|
+
...uploadFileDropzone,
|
|
4
|
+
imageItemWrapper: "flex w-full flex-wrap gap-4",
|
|
5
|
+
imageItem: {
|
|
6
|
+
wrapper: "max-w-[96px]",
|
|
7
|
+
onLoading: {
|
|
8
|
+
wrapper: "flex size-24 flex-col items-center justify-center overflow-hidden rounded-lg border-[1px] border-dashed p-2",
|
|
9
|
+
percentClass: "text-primary"
|
|
10
|
+
},
|
|
11
|
+
onPreview: {
|
|
12
|
+
wrapper: "relative size-24 overflow-hidden rounded-lg ring-[1px] ring-gray-100",
|
|
13
|
+
previewImgClass: "size-full object-cover",
|
|
14
|
+
previewActionWrapper: "absolute inset-0 z-10 flex items-center justify-center space-x-2 opacity-0 transition hover:bg-black/50 hover:opacity-100",
|
|
15
|
+
actionBtnClass: "size-7 cursor-pointer text-white",
|
|
16
|
+
previewTextWrapper: "mt-2 truncate",
|
|
17
|
+
previewText: "truncate text-center text-sm"
|
|
18
|
+
},
|
|
19
|
+
onFailed: {
|
|
20
|
+
wrapper: "ring-danger relative size-24 overflow-hidden rounded-lg ring-[1px]",
|
|
21
|
+
failedImgClass: "size-full object-cover",
|
|
22
|
+
failedActionWrapper: "absolute inset-0 z-10 flex items-center justify-center space-x-2 bg-white/50",
|
|
23
|
+
actionBtnClass: "text-danger size-7 cursor-pointer"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -34,13 +34,17 @@ export declare const uploadFileDropzone: {
|
|
|
34
34
|
};
|
|
35
35
|
action: {
|
|
36
36
|
wrapper: string;
|
|
37
|
+
addingWrapper: string;
|
|
37
38
|
iconClass: string;
|
|
38
39
|
deleteIconClass: string;
|
|
39
40
|
retryBtnClass: string;
|
|
41
|
+
addingBtnClass: string;
|
|
42
|
+
addingTextClass: string;
|
|
40
43
|
previewIcon: string;
|
|
41
44
|
downloadIcon: string;
|
|
42
45
|
deleteIcon: string;
|
|
43
46
|
retryIcon: string;
|
|
47
|
+
addingIcon: string;
|
|
44
48
|
};
|
|
45
49
|
background: {
|
|
46
50
|
default: string;
|
|
@@ -34,13 +34,17 @@ export const uploadFileDropzone = {
|
|
|
34
34
|
},
|
|
35
35
|
action: {
|
|
36
36
|
wrapper: "flex items-center space-x-4",
|
|
37
|
+
addingWrapper: "flex size-24 flex-col items-center justify-center overflow-hidden rounded-lg border-[1px] border-dashed p-2 cursor-pointer transition hover:bg-gray-100",
|
|
37
38
|
iconClass: "size-6 text-gray-400 cursor-pointer",
|
|
38
39
|
deleteIconClass: "size-6 text-danger cursor-pointer",
|
|
39
40
|
retryBtnClass: "px-0",
|
|
41
|
+
addingBtnClass: "size-7 text-gray-500",
|
|
42
|
+
addingTextClass: "text-center text-sm text-gray-500",
|
|
40
43
|
previewIcon: "i-ic:outline-remove-red-eye",
|
|
41
44
|
downloadIcon: "i-ic:outline-file-download",
|
|
42
45
|
deleteIcon: "ph:trash",
|
|
43
|
-
retryIcon: "ph:arrow-counter-clockwise"
|
|
46
|
+
retryIcon: "ph:arrow-counter-clockwise",
|
|
47
|
+
addingIcon: "i-material-symbols:add"
|
|
44
48
|
},
|
|
45
49
|
background: {
|
|
46
50
|
default: "bg-white border-gray-border",
|