@a-vision-software/vue-input-components 1.4.11 → 1.4.13
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/src/components/FileUpload.vue.d.ts +14 -14
- package/dist/src/components/Modal.vue.d.ts +1 -1
- package/dist/src/types/fileupload.d.ts +28 -11
- package/dist/vue-input-components.cjs.js +1 -1
- package/dist/vue-input-components.css +1 -1
- package/dist/vue-input-components.es.js +4585 -4400
- package/dist/vue-input-components.umd.js +1 -1
- package/package.json +3 -3
- package/src/components/FileUpload.vue +183 -70
- package/src/types/fileupload.ts +40 -21
- package/src/views/FileUploadTestView.vue +37 -28
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-vision-software/vue-input-components",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.13",
|
|
4
4
|
"description": "A collection of reusable Vue 3 input components with TypeScript support",
|
|
5
5
|
"author": "A-Vision Software",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/a-vision/vue-input-components.git"
|
|
9
|
+
"url": "git+https://github.com/a-vision/vue-input-components.git"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://a-vision.github.io/vue-input-components/",
|
|
12
12
|
"keywords": [
|
|
@@ -88,4 +88,4 @@
|
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
}
|
|
91
|
-
}
|
|
91
|
+
}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="file-upload"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
<div class="file-upload"
|
|
3
|
+
:class="{ 'label-position-top': labelPosition === 'top', 'label-position-left': labelPosition === 'left' }" :style="{
|
|
4
|
+
'--file-upload-width': width || 'auto',
|
|
5
|
+
'--file-upload-height': height || 'auto',
|
|
6
|
+
'--file-upload-color': error ? 'var(--ui-error-text-color)' : (color || 'var(--ui-upload-text-color, #888888)'),
|
|
7
|
+
'--file-upload-disabled-color': 'color-mix(in srgb, var(--file-upload-color), white 70%)',
|
|
8
|
+
'--file-upload-background-color': bgColor || 'var(--ui-upload-bg-color, #ffffff)',
|
|
9
|
+
'--file-upload-background-active-color': activeColor || 'var(--ui-upload-active-bg-color, #cccccc)',
|
|
10
|
+
'--file-upload-border-color': borderColor || 'var(--ui-upload-border-color, #888888)',
|
|
11
|
+
'--file-upload-icon-color': iconColor || 'var(--file-upload-color, #888888)',
|
|
12
|
+
'--file-upload-progress-color': progressColor || 'var(--ui-progress-color, #4444aa)',
|
|
13
|
+
'--file-upload-error-color': 'var(--ui-error-text-color, #aa0000)',
|
|
14
|
+
'--file-upload-success-color': 'var(--ui-success-text-color, #00aa00)',
|
|
15
|
+
}">
|
|
16
|
+
<label class="file-upload-label" v-if="label" for="fileInput">{{ label }}</label>
|
|
17
|
+
<div class="upload-area"
|
|
18
|
+
:class="{ 'is-dragging': isDragging, 'has-files': files.length > 0, 'is-disabled': disabled }">
|
|
19
|
+
<input ref="fileInput" type="file" :multiple="multiple" class="file-input" @change="handleFileSelect"
|
|
20
|
+
:accept="accept" :required="required" :disabled="disabled" :placeholder="placeholder"
|
|
21
|
+
@dragenter.prevent="handleDragEnter" @dragleave.prevent="handleDragLeave" @dragover.prevent
|
|
22
|
+
@drop.prevent="handleDrop" />
|
|
13
23
|
<div class="upload-content">
|
|
14
|
-
<font-awesome-icon :icon="['fas', icon
|
|
15
|
-
<p v-if="files.length === 0"
|
|
24
|
+
<font-awesome-icon v-if="icon" :icon="['fas', icon]" />
|
|
25
|
+
<p v-if="files.length === 0" v-text="placeholder || 'Drag & drop files here or click to select'"></p>
|
|
16
26
|
<div v-else class="selected-files">
|
|
17
27
|
<p>{{ files.length }} file(s) selected</p>
|
|
18
28
|
<div v-for="(file, index) in files" :key="index" class="file-info">
|
|
@@ -21,42 +31,42 @@
|
|
|
21
31
|
</div>
|
|
22
32
|
</div>
|
|
23
33
|
</div>
|
|
34
|
+
<div v-if="error" class="error-message">{{ error }}</div>
|
|
35
|
+
<div v-if="uploadStatus" class="status-message" :class="uploadStatus.type">
|
|
36
|
+
{{ uploadStatus.message }}
|
|
37
|
+
</div>
|
|
38
|
+
<div v-if="uploadProgress > 0 && uploadProgress < 100" class="progress-bar">
|
|
39
|
+
<div class="progress" :style="{ width: `${uploadProgress}%` }"></div>
|
|
40
|
+
</div>
|
|
24
41
|
</div>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
<div class="progress" :style="{ width: `${uploadProgress}%` }"></div>
|
|
28
|
-
</div>
|
|
29
|
-
<div v-if="uploadStatus" class="status-message" :class="uploadStatus.type">
|
|
30
|
-
{{ uploadStatus.message }}
|
|
31
|
-
</div>
|
|
32
|
-
<button v-if="files.length > 0 && !uploadUrl" class="upload-button" @click="handleStartUpload">
|
|
42
|
+
<button v-if="files.length > 0 && !uploadUrl && !disabled && showUploadButton" class="upload-button"
|
|
43
|
+
@click="handleStartUpload">
|
|
33
44
|
Upload Files
|
|
34
45
|
</button>
|
|
35
46
|
</div>
|
|
36
47
|
</template>
|
|
37
48
|
|
|
38
49
|
<script setup lang="ts">
|
|
39
|
-
import { ref, watch } from 'vue'
|
|
40
|
-
|
|
41
|
-
const props = defineProps<{
|
|
42
|
-
icon?: string
|
|
43
|
-
uploadUrl?: string
|
|
44
|
-
}>()
|
|
50
|
+
import { ref, watch, onMounted } from 'vue'
|
|
51
|
+
import type { FileUploadProps, FileUploadEmits, UploadStatus, UploadCallbackResponse } from '@/types/fileupload'
|
|
45
52
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
(e: 'upload-error', error: string): void
|
|
49
|
-
(e: 'files-selected', files: File[]): void
|
|
50
|
-
(e: 'start-upload', files: File[]): void
|
|
51
|
-
}>()
|
|
53
|
+
const props = defineProps<FileUploadProps>()
|
|
54
|
+
const emit = defineEmits<FileUploadEmits>()
|
|
52
55
|
|
|
53
|
-
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB in bytes
|
|
54
56
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
55
57
|
const files = ref<File[]>([])
|
|
56
58
|
const isDragging = ref(false)
|
|
59
|
+
const startUpload = ref(props.doUpload)
|
|
57
60
|
const uploadProgress = ref(0)
|
|
58
61
|
const error = ref('')
|
|
59
|
-
const uploadStatus = ref<
|
|
62
|
+
const uploadStatus = ref<UploadStatus>({
|
|
63
|
+
type: 'pending',
|
|
64
|
+
message: '',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
onMounted(() => {
|
|
68
|
+
startUpload.value = props.doUpload;
|
|
69
|
+
})
|
|
60
70
|
|
|
61
71
|
const formatFileSize = (bytes: number): string => {
|
|
62
72
|
if (bytes === 0) return '0 Bytes'
|
|
@@ -67,8 +77,8 @@ const formatFileSize = (bytes: number): string => {
|
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
const validateFileSize = (file: File): boolean => {
|
|
70
|
-
if (file.size >
|
|
71
|
-
error.value = `File "${file.name}" exceeds the maximum size of
|
|
80
|
+
if (props.maxSize && file.size > props.maxSize) {
|
|
81
|
+
error.value = `File "${file.name}" exceeds the maximum size of ${props.maxSize}MB`
|
|
72
82
|
return false
|
|
73
83
|
}
|
|
74
84
|
return true
|
|
@@ -93,10 +103,6 @@ const handleDrop = (e: DragEvent) => {
|
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
const triggerFileInput = () => {
|
|
97
|
-
fileInput.value?.click()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
106
|
const handleFileSelect = (e: Event) => {
|
|
101
107
|
error.value = ''
|
|
102
108
|
const input = e.target as HTMLInputElement
|
|
@@ -109,7 +115,7 @@ const handleFileSelect = (e: Event) => {
|
|
|
109
115
|
input.value = ''
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
const uploadFiles =
|
|
118
|
+
const uploadFiles = () => {
|
|
113
119
|
if (!props.uploadUrl) {
|
|
114
120
|
error.value = 'No upload URL provided'
|
|
115
121
|
return
|
|
@@ -123,13 +129,45 @@ const uploadFiles = async () => {
|
|
|
123
129
|
const formData = new FormData()
|
|
124
130
|
files.value.forEach((file) => {
|
|
125
131
|
formData.append('files', file)
|
|
126
|
-
})
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (props.uploadCallback) {
|
|
135
|
+
props.uploadCallback(formData)
|
|
136
|
+
.then((response: UploadCallbackResponse) => {
|
|
137
|
+
const { formData, headers } = response;
|
|
138
|
+
doUploadFiles(formData, headers)
|
|
139
|
+
})
|
|
140
|
+
.catch((err) => {
|
|
141
|
+
const errorMessage = err instanceof Error ? err.message : 'Upload failed'
|
|
142
|
+
error.value = errorMessage
|
|
143
|
+
uploadStatus.value = {
|
|
144
|
+
type: 'error',
|
|
145
|
+
message: errorMessage,
|
|
146
|
+
}
|
|
147
|
+
emit('upload-complete', files.value)
|
|
148
|
+
emit('upload-error', uploadStatus.value)
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
doUploadFiles(formData, {})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
127
154
|
|
|
155
|
+
const doUploadFiles = (formData: FormData, headers: Record<string, string>) => {
|
|
128
156
|
try {
|
|
129
157
|
const xhr = new XMLHttpRequest()
|
|
158
|
+
console.log('doUploadFiles:', formData, headers)
|
|
159
|
+
|
|
130
160
|
xhr.upload.addEventListener('progress', (e) => {
|
|
131
161
|
if (e.lengthComputable) {
|
|
132
162
|
uploadProgress.value = (e.loaded / e.total) * 100
|
|
163
|
+
emit('upload-progress', uploadProgress.value)
|
|
164
|
+
|
|
165
|
+
if (uploadProgress.value >= 100) {
|
|
166
|
+
uploadStatus.value = {
|
|
167
|
+
type: 'processing',
|
|
168
|
+
message: 'Please wait...',
|
|
169
|
+
}
|
|
170
|
+
}
|
|
133
171
|
}
|
|
134
172
|
})
|
|
135
173
|
|
|
@@ -140,8 +178,10 @@ const uploadFiles = async () => {
|
|
|
140
178
|
message: 'Upload completed successfully',
|
|
141
179
|
}
|
|
142
180
|
emit('upload-complete', files.value)
|
|
181
|
+
emit('upload-success', uploadStatus.value)
|
|
143
182
|
files.value = []
|
|
144
183
|
uploadProgress.value = 0
|
|
184
|
+
startUpload.value = false;
|
|
145
185
|
} else {
|
|
146
186
|
throw new Error(`Upload failed with status ${xhr.status}`)
|
|
147
187
|
}
|
|
@@ -151,7 +191,12 @@ const uploadFiles = async () => {
|
|
|
151
191
|
throw new Error('Upload failed')
|
|
152
192
|
})
|
|
153
193
|
|
|
154
|
-
xhr.open('POST', props.uploadUrl)
|
|
194
|
+
xhr.open('POST', props.uploadUrl || '')
|
|
195
|
+
|
|
196
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
197
|
+
xhr.setRequestHeader(key, value)
|
|
198
|
+
})
|
|
199
|
+
|
|
155
200
|
xhr.send(formData)
|
|
156
201
|
} catch (err) {
|
|
157
202
|
const errorMessage = err instanceof Error ? err.message : 'Upload failed'
|
|
@@ -160,67 +205,105 @@ const uploadFiles = async () => {
|
|
|
160
205
|
type: 'error',
|
|
161
206
|
message: errorMessage,
|
|
162
207
|
}
|
|
163
|
-
emit('upload-
|
|
208
|
+
emit('upload-complete', files.value)
|
|
209
|
+
emit('upload-error', uploadStatus.value)
|
|
164
210
|
}
|
|
165
211
|
}
|
|
166
212
|
|
|
167
213
|
const handleStartUpload = () => {
|
|
168
|
-
emit('
|
|
214
|
+
emit('upload-started', files.value)
|
|
169
215
|
}
|
|
170
216
|
|
|
171
217
|
// Watch for changes in files and automatically upload when files are selected
|
|
172
218
|
watch(files, (newFiles) => {
|
|
173
219
|
if (newFiles.length > 0) {
|
|
174
|
-
if (props.uploadUrl) {
|
|
220
|
+
if (props.uploadUrl && props.doUpload) {
|
|
175
221
|
uploadFiles()
|
|
176
222
|
} else {
|
|
177
223
|
emit('files-selected', newFiles)
|
|
178
224
|
}
|
|
179
225
|
}
|
|
180
226
|
})
|
|
227
|
+
|
|
228
|
+
watch(() => props.doUpload, (newDoUpload) => {
|
|
229
|
+
if (files.value && newDoUpload) {
|
|
230
|
+
startUpload.value = newDoUpload;
|
|
231
|
+
} else {
|
|
232
|
+
emit('files-selected', files.value)
|
|
233
|
+
startUpload.value = false;
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
watch(() => startUpload.value, (newStartUpload) => {
|
|
238
|
+
if (newStartUpload) {
|
|
239
|
+
uploadFiles()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
181
242
|
</script>
|
|
182
243
|
|
|
183
244
|
<style scoped>
|
|
184
245
|
.file-upload {
|
|
185
|
-
width:
|
|
186
|
-
|
|
246
|
+
width: var(--file-upload-width);
|
|
247
|
+
height: var(--file-upload-height);
|
|
187
248
|
margin: 0 auto;
|
|
249
|
+
|
|
250
|
+
&.label-position-left {
|
|
251
|
+
display: grid;
|
|
252
|
+
grid-template-columns: 10em calc(100% - 10em);
|
|
253
|
+
gap: 1rem;
|
|
254
|
+
}
|
|
255
|
+
|
|
188
256
|
}
|
|
189
257
|
|
|
190
258
|
.upload-area {
|
|
191
|
-
|
|
259
|
+
position: relative;
|
|
260
|
+
border: 2px dashed var(--file-upload-border-color);
|
|
192
261
|
border-radius: 0.75rem;
|
|
193
262
|
padding: 2rem;
|
|
194
263
|
text-align: center;
|
|
195
264
|
cursor: pointer;
|
|
196
265
|
transition: all 0.3s ease;
|
|
197
|
-
background-color: var(--upload-
|
|
266
|
+
background-color: var(--file-upload-background-color);
|
|
198
267
|
}
|
|
199
268
|
|
|
200
269
|
.upload-area.is-dragging {
|
|
201
|
-
|
|
202
|
-
background-color: var(--upload-dragging-bg-color);
|
|
270
|
+
background-color: var(--file-upload-background-active-color);
|
|
203
271
|
}
|
|
204
272
|
|
|
205
273
|
.upload-area.has-files {
|
|
206
|
-
|
|
207
|
-
|
|
274
|
+
background-color: var(--file-upload-background-active-color);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.upload-area.is-disabled {
|
|
278
|
+
color: var(--file-upload-disabled-color);
|
|
279
|
+
border-color: var(--file-upload-disabled-color);
|
|
280
|
+
cursor: not-allowed;
|
|
208
281
|
}
|
|
209
282
|
|
|
210
283
|
.file-input {
|
|
211
|
-
|
|
284
|
+
position: absolute;
|
|
285
|
+
top: 0;
|
|
286
|
+
left: 0;
|
|
287
|
+
right: 0;
|
|
288
|
+
bottom: 0;
|
|
289
|
+
opacity: 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.upload-area.is-disabled .file-input {
|
|
293
|
+
cursor: not-allowed;
|
|
212
294
|
}
|
|
213
295
|
|
|
214
296
|
.upload-content {
|
|
215
|
-
display:
|
|
216
|
-
|
|
217
|
-
align
|
|
297
|
+
display: grid;
|
|
298
|
+
grid-template-rows: 1fr 1fr;
|
|
299
|
+
text-align: center;
|
|
218
300
|
gap: 1rem;
|
|
219
301
|
}
|
|
220
302
|
|
|
221
303
|
.upload-content :deep(svg) {
|
|
222
304
|
font-size: 2rem;
|
|
223
|
-
color: var(--upload-icon-color);
|
|
305
|
+
color: var(--file-upload-icon-color);
|
|
306
|
+
margin: 0 auto;
|
|
224
307
|
}
|
|
225
308
|
|
|
226
309
|
.selected-files {
|
|
@@ -229,7 +312,7 @@ watch(files, (newFiles) => {
|
|
|
229
312
|
max-height: 200px;
|
|
230
313
|
overflow-y: auto;
|
|
231
314
|
font-size: 0.75rem;
|
|
232
|
-
color: var(--upload-
|
|
315
|
+
color: var(--file-upload-color);
|
|
233
316
|
}
|
|
234
317
|
|
|
235
318
|
.file-info {
|
|
@@ -255,23 +338,51 @@ watch(files, (newFiles) => {
|
|
|
255
338
|
flex-shrink: 0;
|
|
256
339
|
}
|
|
257
340
|
|
|
341
|
+
/*
|
|
342
|
+
.label-position-left {
|
|
343
|
+
line-height: 2em;
|
|
344
|
+
|
|
345
|
+
.upload-area {
|
|
346
|
+
padding: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.upload-content {
|
|
350
|
+
padding: 0;
|
|
351
|
+
grid-template-rows: 1fr;
|
|
352
|
+
grid-template-columns: 3em auto;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.label-position-left .upload-content :deep(svg) {
|
|
357
|
+
font-size: 1.25em;
|
|
358
|
+
margin-top: 0.25em;
|
|
359
|
+
}
|
|
360
|
+
*/
|
|
361
|
+
|
|
258
362
|
.error-message {
|
|
259
|
-
color: var(--error-
|
|
363
|
+
color: var(--file-upload-error-color);
|
|
260
364
|
margin-top: 1rem;
|
|
261
365
|
font-size: 0.875rem;
|
|
366
|
+
position: absolute;
|
|
367
|
+
bottom: 0;
|
|
368
|
+
left: 0;
|
|
369
|
+
right: 0;
|
|
262
370
|
}
|
|
263
371
|
|
|
264
372
|
.progress-bar {
|
|
265
373
|
height: 0.5rem;
|
|
266
|
-
background-color: var(--progress-bg-color);
|
|
267
374
|
border-radius: 0.25rem;
|
|
268
375
|
margin-top: 1rem;
|
|
269
376
|
overflow: hidden;
|
|
377
|
+
position: absolute;
|
|
378
|
+
bottom: 0;
|
|
379
|
+
left: 0;
|
|
380
|
+
right: 0;
|
|
270
381
|
}
|
|
271
382
|
|
|
272
383
|
.progress {
|
|
273
384
|
height: 100%;
|
|
274
|
-
background-color: var(--progress-color);
|
|
385
|
+
background-color: var(--file-upload-progress-color);
|
|
275
386
|
transition: width 0.3s ease;
|
|
276
387
|
}
|
|
277
388
|
|
|
@@ -280,22 +391,24 @@ watch(files, (newFiles) => {
|
|
|
280
391
|
padding: 0.5rem;
|
|
281
392
|
border-radius: 0.25rem;
|
|
282
393
|
font-size: 0.875rem;
|
|
394
|
+
position: absolute;
|
|
395
|
+
bottom: 0;
|
|
396
|
+
left: 0;
|
|
397
|
+
right: 0;
|
|
283
398
|
}
|
|
284
399
|
|
|
285
400
|
.status-message.success {
|
|
286
|
-
|
|
287
|
-
color: var(--success-text-color);
|
|
401
|
+
color: var(--file-upload-success-color);
|
|
288
402
|
}
|
|
289
403
|
|
|
290
404
|
.status-message.error {
|
|
291
|
-
|
|
292
|
-
color: var(--error-text-color);
|
|
405
|
+
color: var(--file-upload-error-color);
|
|
293
406
|
}
|
|
294
407
|
|
|
295
408
|
.upload-button {
|
|
296
409
|
margin-top: 1rem;
|
|
297
410
|
padding: 0.5rem 1rem;
|
|
298
|
-
background-color: var(--
|
|
411
|
+
background-color: var(--file-upload-color);
|
|
299
412
|
color: white;
|
|
300
413
|
border: none;
|
|
301
414
|
border-radius: 0.25rem;
|
|
@@ -305,6 +418,6 @@ watch(files, (newFiles) => {
|
|
|
305
418
|
}
|
|
306
419
|
|
|
307
420
|
.upload-button:hover {
|
|
308
|
-
background-color: var(--
|
|
421
|
+
background-color: var(--file-upload-color-light);
|
|
309
422
|
}
|
|
310
423
|
</style>
|
package/src/types/fileupload.ts
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
interface FileUploadProps {
|
|
2
|
+
width?: string // Width of the upload area.
|
|
3
|
+
height?: string // Height of the upload area.
|
|
4
|
+
label?: string // Label text displayed above the upload area.
|
|
5
|
+
labelPosition?: 'top' | 'left' // Position of the label.
|
|
6
|
+
icon?: string // Icon to display.
|
|
7
|
+
placeholder?: string // Placeholder text displayed when no files are selected.
|
|
8
|
+
error?: string // Error message to display when the upload fails.
|
|
9
|
+
disabled?: boolean // Whether to disable the upload.
|
|
10
|
+
required?: boolean // Whether to require the upload.
|
|
11
|
+
multiple?: boolean // Whether to allow multiple files to be selected.
|
|
12
|
+
accept?: string // File types to accept.
|
|
13
|
+
maxSize?: number // Maximum file size in MB.
|
|
14
|
+
uploadUrl?: string // URL to upload the files to.
|
|
15
|
+
showUploadButton?: boolean // Whether to show the upload button.
|
|
16
|
+
doUpload?: boolean // Whether to automatically upload files when selected. Also setting it from 'false' to 'true' will trigger the upload.
|
|
17
|
+
iconColor?: string // Icon color.
|
|
18
|
+
color?: string // Text color.
|
|
19
|
+
bgColor?: string // Background color.
|
|
20
|
+
borderColor?: string // Border color.
|
|
21
|
+
progressColor?: string // Progress color.
|
|
22
|
+
activeColor?: string // Active color.
|
|
23
|
+
uploadCallback?: (formData: FormData) => Promise<UploadCallbackResponse> // Callback function to handle the upload.
|
|
12
24
|
}
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
(e: 'start-upload', files: File[]): void
|
|
18
|
-
(e: 'upload-progress', progress: number): void
|
|
19
|
-
(e: 'upload-success', response: any): void
|
|
20
|
-
(e: 'upload-error', error: Error): void
|
|
26
|
+
interface UploadCallbackResponse {
|
|
27
|
+
formData: FormData
|
|
28
|
+
headers: Record<string, string>
|
|
21
29
|
}
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
interface UploadStatus {
|
|
32
|
+
type: 'pending' | 'success' | 'error' | 'processing'
|
|
33
|
+
message: string
|
|
26
34
|
}
|
|
35
|
+
|
|
36
|
+
interface FileUploadEmits {
|
|
37
|
+
(e: 'files-selected', files: File[]): void // Emitted when files are selected.
|
|
38
|
+
(e: 'upload-started', files: File[]): void // Emitted when the upload starts.
|
|
39
|
+
(e: 'upload-complete', files: File[]): void // Emitted when the upload completes.
|
|
40
|
+
(e: 'upload-progress', progress: number): void // Emitted when the upload progresses.
|
|
41
|
+
(e: 'upload-success', status: UploadStatus): void // Emitted when the upload succeeds.
|
|
42
|
+
(e: 'upload-error', status: UploadStatus): void // Emitted when the upload fails.
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type { FileUploadProps, FileUploadEmits, UploadStatus, UploadCallbackResponse }
|
|
@@ -10,30 +10,27 @@
|
|
|
10
10
|
<div class="file-upload-grid">
|
|
11
11
|
<div class="upload-section">
|
|
12
12
|
<h2>Basic Upload</h2>
|
|
13
|
-
<FileUpload
|
|
14
|
-
icon="upload"
|
|
15
|
-
upload-url="https://httpbin.org/post"
|
|
16
|
-
@upload-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<div
|
|
20
|
-
v-if="uploadStatus.basic && uploadStatus.basic.message"
|
|
21
|
-
:class="['status-message', uploadStatus.basic.type]"
|
|
22
|
-
>
|
|
13
|
+
<FileUpload label="Upload a file" placeholder="Select one or more files" label-position="left" width="100%"
|
|
14
|
+
error="File upload failed" :disabled="false" :required="true" :multiple="true" icon="upload"
|
|
15
|
+
upload-url="https://httpbin.org/post" @upload-complete="handleUploadComplete"
|
|
16
|
+
@upload-error="handleUploadError" :do-upload="true" />
|
|
17
|
+
<div v-if="uploadStatus.basic && uploadStatus.basic.message && false"
|
|
18
|
+
:class="['status-message', uploadStatus.basic.type]">
|
|
23
19
|
{{ uploadStatus.basic.message }}
|
|
24
20
|
</div>
|
|
25
21
|
</div>
|
|
26
22
|
<div class="upload-section">
|
|
27
|
-
<h2>Image Upload
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@
|
|
32
|
-
|
|
23
|
+
<h2>Multiple Image Upload (<span @click="imageDisabled = !imageDisabled"
|
|
24
|
+
v-text="imageDisabled ? 'disabled' : 'enabled'"></span>)</h2>
|
|
25
|
+
<FileUpload icon="image" accept="image/*" @start-upload="handleStartUpload" :disabled="imageDisabled"
|
|
26
|
+
upload-url="https://httpbin.org/post" :multiple="true" :do-upload="uploadImages"
|
|
27
|
+
@upload-complete="showImagesUpload = false" :upload-callback="handleUploadCallback"
|
|
28
|
+
@files-selected="showImagesUpload = true" />
|
|
29
|
+
<button @click="uploadImages = true" v-if="showImagesUpload">Upload Images</button>
|
|
33
30
|
</div>
|
|
34
31
|
<div class="upload-section">
|
|
35
32
|
<h2>Document Upload</h2>
|
|
36
|
-
<FileUpload icon="file" />
|
|
33
|
+
<FileUpload icon="file" accept="application/pdf" :show-upload-button="true" />
|
|
37
34
|
</div>
|
|
38
35
|
</div>
|
|
39
36
|
</div>
|
|
@@ -42,16 +39,32 @@
|
|
|
42
39
|
<script setup lang="ts">
|
|
43
40
|
import { ref } from 'vue'
|
|
44
41
|
import FileUpload from '@/components/FileUpload.vue'
|
|
42
|
+
import type { UploadStatus, UploadCallbackResponse } from '@/types/fileupload'
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
44
|
+
const imageDisabled = ref(true);
|
|
45
|
+
const uploadImages = ref(false);
|
|
46
|
+
const showImagesUpload = ref(false);
|
|
47
|
+
const imageUploadData = ref({ tag: 'test', type: 'image', name: 'test' });
|
|
50
48
|
|
|
51
49
|
const uploadStatus = ref<Record<string, UploadStatus>>({
|
|
52
50
|
basic: { type: 'success', message: '' },
|
|
53
51
|
})
|
|
54
52
|
|
|
53
|
+
const handleUploadCallback = (formData: FormData): Promise<UploadCallbackResponse> => {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
// Add imageUploadData to formData
|
|
56
|
+
Object.entries(imageUploadData.value).forEach(([key, value]) => {
|
|
57
|
+
formData.append(key, value)
|
|
58
|
+
})
|
|
59
|
+
resolve({
|
|
60
|
+
formData,
|
|
61
|
+
headers: {
|
|
62
|
+
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIqInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvqG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
const handleUploadComplete = (files: File[]) => {
|
|
56
69
|
uploadStatus.value.basic = {
|
|
57
70
|
type: 'success',
|
|
@@ -63,10 +76,10 @@ const handleUploadComplete = (files: File[]) => {
|
|
|
63
76
|
}, 3000)
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
const handleUploadError = (
|
|
79
|
+
const handleUploadError = (status: UploadStatus) => {
|
|
67
80
|
uploadStatus.value.basic = {
|
|
68
|
-
type:
|
|
69
|
-
message:
|
|
81
|
+
type: status.type,
|
|
82
|
+
message: status.message,
|
|
70
83
|
}
|
|
71
84
|
// Clear status after 3 seconds
|
|
72
85
|
setTimeout(() => {
|
|
@@ -74,10 +87,6 @@ const handleUploadError = (error: string) => {
|
|
|
74
87
|
}, 3000)
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
const handleFilesSelected = (files: File[]) => {
|
|
78
|
-
console.log('Files selected:', files)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
90
|
const handleStartUpload = (files: File[]) => {
|
|
82
91
|
console.log('Start upload:', files)
|
|
83
92
|
}
|