webcake-ui-kit 1.2.16 → 1.2.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-ui-kit",
3
- "version": "1.2.16",
3
+ "version": "1.2.18",
4
4
  "description": "UI Kit for Vue 2 && 3 - Pure Options API",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -66,6 +66,7 @@
66
66
  "./WkToggleGroup": "./src/components/toggle-group/ToggleGroup.vue",
67
67
  "./WkTooltip": "./src/components/tooltip/Tooltip.vue",
68
68
  "./WkTypography": "./src/components/typography/Typography.vue",
69
+ "./WkUpload": "./src/components/upload/Upload.vue",
69
70
  "./components/*": "./src/components/*",
70
71
  "./icons": "./src/icons/index.js",
71
72
  "./package.json": "./package.json"
@@ -18,7 +18,7 @@
18
18
  <span
19
19
  :ref="triggerRef"
20
20
  class="ui-dropdown__anchor"
21
- @click.stop="triggerHasClick && handleClick()"
21
+ @click.stop="triggerHasClick && handleClick($event)"
22
22
  @mouseenter.stop="triggerHasHover && handleMouseEnter()"
23
23
  @mouseleave.stop="triggerHasHover && handleMouseLeave()"
24
24
  >
@@ -136,8 +136,10 @@ export default {
136
136
  this.$emit('change', v)
137
137
  this.$emit('update:modelValue', v)
138
138
  },
139
- handleClick() {
139
+ handleClick(e) {
140
140
  if (this.disabled) return
141
+ // Don't close when clicking a focusable element inside the trigger (e.g. input, textarea)
142
+ if (this.isOpen && e && e.target && e.target.closest('input, textarea, select, [contenteditable]')) return
141
143
  this.onMenuChange(!this.isOpen)
142
144
  },
143
145
  handleMouseEnter() {
@@ -90,6 +90,7 @@
90
90
 
91
91
  .ui-input-number--disabled {
92
92
  opacity: 0.5;
93
+ background: var(--secondary-bg);
93
94
  }
94
95
 
95
96
  .ui-input-number--disabled:focus-within {
@@ -198,7 +198,12 @@ export default {
198
198
  this._kmSetFloatEl(float)
199
199
  this._pmAttach(this._triggerEl, float)
200
200
  this._coAttach()
201
- if (this.autoFocus) this._fmFocus(float)
201
+ if (this.autoFocus) {
202
+ // Don't steal focus when the trigger itself already holds it (e.g. an input inside the anchor)
203
+ const active = typeof document !== 'undefined' && document.activeElement
204
+ const triggerHasFocus = active && this._triggerEl && this._triggerEl.contains(active)
205
+ if (!triggerHasFocus) this._fmFocus(float)
206
+ }
202
207
  })
203
208
  },
204
209
  handleClose() {
@@ -10,6 +10,7 @@
10
10
  @close="onMenuClose"
11
11
  :overlay-class-name="overlayClassName"
12
12
  :overlay-style="overlayStyle"
13
+ :width="overlayWidth"
13
14
  >
14
15
  <template #trigger="{ toggle, isOpen: open, attrs, triggerRef }">
15
16
  <div
@@ -218,7 +219,8 @@ export default {
218
219
  },
219
220
  overlayClassName: { type: [String, Array, Object], default: null },
220
221
  overlayStyle: { type: [Object, Array], default: null },
221
- dropdownMatchSelectWidth: { type: Boolean, default: true }
222
+ dropdownMatchSelectWidth: { type: Boolean, default: true },
223
+ overlayWidth: { type: [String, Number], default: null }
222
224
  },
223
225
 
224
226
  emits: ['change', 'update:modelValue'],
@@ -158,8 +158,9 @@ export default {
158
158
 
159
159
  var style = {
160
160
  overflowY: 'auto',
161
+ overflowX: 'hidden',
161
162
  maxHeight: `calc(${this.toCssSize(this.scroll.y)} - ${deduct})`,
162
- minHeight: this.minHeight > 0 ? `calc(${this.toCssSize(this.minHeight)} - ${deduct})` : undefined
163
+ minHeight: this.minHeight ? `calc(${this.toCssSize(this.minHeight)} - ${deduct})` : undefined
163
164
  }
164
165
  if (this.hasScrollX) {
165
166
  style.overflowX = 'auto'
@@ -68,7 +68,7 @@ export default {
68
68
  return String(this.currentValue).length
69
69
  },
70
70
  showCounter() {
71
- return this.showCount
71
+ return this.showCount || this.maxLength !== undefined
72
72
  }
73
73
  },
74
74
  watch: {
@@ -184,22 +184,22 @@
184
184
  padding: var(--spacing-3xs);
185
185
  }
186
186
  .ui-toggle--icon-only.ui-toggle--xs {
187
- min-width: 28px;
187
+ width: 28px;
188
188
  height: 28px;
189
189
  padding: var(--spacing-6) var(--spacing-6) var(--spacing-6) var(--spacing-xs);
190
190
  }
191
191
  .ui-toggle--icon-only.ui-toggle--sm {
192
- min-width: 32px;
192
+ width: 32px;
193
193
  height: 32px;
194
194
  padding: var(--spacing-6);
195
195
  }
196
196
  .ui-toggle--icon-only.ui-toggle--md {
197
- min-width: 36px;
197
+ width: 36px;
198
198
  height: 36px;
199
199
  padding: var(--spacing-xs);
200
200
  }
201
201
  .ui-toggle--icon-only.ui-toggle--lg {
202
- min-width: 40px;
202
+ width: 40px;
203
203
  height: 40px;
204
204
  padding: var(--spacing-10);
205
205
  }
@@ -0,0 +1,534 @@
1
+ <template>
2
+ <div
3
+ class="ui-upload"
4
+ :class="[
5
+ `ui-upload--${listType}`,
6
+ {
7
+ 'ui-upload--disabled': disabled,
8
+ 'ui-upload--drag-over': dragOver
9
+ }
10
+ ]"
11
+ >
12
+ <!-- Dropzone trigger (default / picture list types) -->
13
+ <div
14
+ v-if="!isCard"
15
+ class="ui-upload__dropzone"
16
+ :class="{ 'ui-upload__dropzone--over': dragOver, 'ui-upload__dropzone--disabled': disabled }"
17
+ role="button"
18
+ :tabindex="disabled ? -1 : 0"
19
+ :aria-disabled="disabled ? 'true' : null"
20
+ @click="openFileDialog"
21
+ @keydown.enter.prevent="openFileDialog"
22
+ @keydown.space.prevent="openFileDialog"
23
+ @dragover.prevent="onDragOver"
24
+ @dragleave.prevent="onDragLeave"
25
+ @drop.prevent="onDrop"
26
+ @paste="onPaste"
27
+ >
28
+ <slot name="trigger">
29
+ <div class="ui-upload__dropzone-inner">
30
+ <span class="ui-upload__feature-icon">
31
+ <WkiUpload :size="20" />
32
+ </span>
33
+ <div class="ui-upload__dropzone-text">
34
+ <p class="ui-upload__dropzone-title">
35
+ {{ title }} <span class="ui-upload__link">{{ browseText }}</span>
36
+ </p>
37
+ <p class="ui-upload__dropzone-hint">{{ hint }}</p>
38
+ </div>
39
+ </div>
40
+ </slot>
41
+ </div>
42
+
43
+ <!-- File list (rows for default/picture, grid of tiles for picture-card/circle) -->
44
+ <UploadList
45
+ :items="visibleFileList"
46
+ :list-type="listType"
47
+ :show-preview-icon="showFlags.showPreviewIcon"
48
+ :show-remove-icon="showFlags.showRemoveIcon"
49
+ :show-download-icon="showFlags.showDownloadIcon"
50
+ :hide-list="showUploadList === false"
51
+ >
52
+ <template v-if="isCard" #trigger>
53
+ <div
54
+ v-if="showCardTrigger"
55
+ class="ui-upload__card-trigger"
56
+ :class="{ 'ui-upload__card-trigger--over': dragOver, 'ui-upload__card-trigger--disabled': disabled }"
57
+ role="button"
58
+ :tabindex="disabled ? -1 : 0"
59
+ :aria-disabled="disabled ? 'true' : null"
60
+ @click="openFileDialog"
61
+ @keydown.enter.prevent="openFileDialog"
62
+ @keydown.space.prevent="openFileDialog"
63
+ @dragover.prevent="onDragOver"
64
+ @dragleave.prevent="onDragLeave"
65
+ @drop.prevent="onDrop"
66
+ @paste="onPaste"
67
+ >
68
+ <slot name="trigger">
69
+ <WkiPlus :size="20" />
70
+ <span v-if="browseText" class="ui-upload__card-trigger-text">{{ browseText }}</span>
71
+ </slot>
72
+ </div>
73
+ </template>
74
+ </UploadList>
75
+
76
+ <input
77
+ ref="input"
78
+ type="file"
79
+ class="ui-upload__input"
80
+ :accept="accept"
81
+ :multiple="multiple"
82
+ :webkitdirectory="directory ? '' : null"
83
+ :directory="directory ? '' : null"
84
+ @change="onInputChange"
85
+ />
86
+
87
+ <Dialog
88
+ v-if="previewVisible"
89
+ :open="previewVisible"
90
+ centered
91
+ :title="previewTitle"
92
+ :footer="false"
93
+ :width="520"
94
+ @change="onPreviewChange"
95
+ >
96
+ <div class="ui-upload__preview">
97
+ <img :src="previewImageSrc" :alt="previewTitle" class="ui-upload__preview-img" />
98
+ </div>
99
+ </Dialog>
100
+ </div>
101
+ </template>
102
+
103
+ <script>
104
+ import Dialog from '../dialog/Dialog.vue'
105
+ import UploadList from './UploadList.vue'
106
+ import WkiUpload from '../../icons/Upload.vue'
107
+ import WkiPlus from '../../icons/Plus.vue'
108
+ import {
109
+ LIST_IGNORE,
110
+ attrAccept,
111
+ defaultRequest,
112
+ file2Obj,
113
+ genUid,
114
+ getFileItem,
115
+ isImageFileType,
116
+ isImageUrl,
117
+ previewImage,
118
+ removeFileItem,
119
+ updateFileList
120
+ } from './upload-utils.js'
121
+
122
+ export default {
123
+ name: 'Upload',
124
+
125
+ components: { Dialog, UploadList, WkiUpload, WkiPlus },
126
+
127
+ provide() {
128
+ return { wkUpload: this }
129
+ },
130
+
131
+ props: {
132
+ // --- request config ---
133
+ action: { type: [String, Function], default: '' },
134
+ method: { type: String, default: 'post' },
135
+ headers: { type: Object, default: () => ({}) },
136
+ data: { type: [Object, Function], default: () => ({}) },
137
+ name: { type: String, default: 'file' },
138
+ withCredentials: { type: Boolean, default: false },
139
+
140
+ // --- data (controlled + uncontrolled) ---
141
+ fileList: { type: Array, default: null },
142
+ defaultFileList: { type: Array, default: () => [] },
143
+
144
+ // --- selection behavior ---
145
+ multiple: { type: Boolean, default: false },
146
+ maxCount: { type: Number, default: 0 },
147
+ maxSize: { type: Number, default: 0 },
148
+ accept: { type: String, default: '' },
149
+ disabled: { type: Boolean, default: false },
150
+ directory: { type: Boolean, default: false },
151
+ pastable: { type: Boolean, default: false },
152
+
153
+ // --- presentation ---
154
+ listType: {
155
+ type: String,
156
+ default: 'default',
157
+ validator: v => ['default', 'picture', 'picture-card', 'picture-circle'].includes(v)
158
+ },
159
+ showUploadList: { type: [Boolean, Object], default: true },
160
+
161
+ // --- trigger copy (dropzone / add-tile) ---
162
+ title: { type: String, default: 'Drop files here or' },
163
+ browseText: { type: String, default: 'browse files' },
164
+ hint: { type: String, default: 'SVG, PNG, JPG or GIF' },
165
+
166
+ // --- logic hooks (function props) ---
167
+ beforeUpload: { type: Function, default: null },
168
+ transformFile: { type: Function, default: null },
169
+ customRequest: { type: Function, default: null },
170
+ beforeRemove: { type: Function, default: null },
171
+ previewFile: { type: Function, default: null },
172
+ isImageUrl: { type: Function, default: null }
173
+ },
174
+
175
+ emits: ['change', 'preview', 'remove', 'download', 'progress', 'drop', 'reject', 'update:fileList'],
176
+
177
+ data() {
178
+ // NB: stateFileList is populated in created(), not here — Vue 3 does not guarantee
179
+ // methods (normalize/ensureUid) are bound during data(), unlike Vue 2.
180
+ return {
181
+ stateFileList: [],
182
+ dragOver: false,
183
+ previewVisible: false,
184
+ previewImageSrc: '',
185
+ previewTitle: ''
186
+ }
187
+ },
188
+
189
+ computed: {
190
+ isCard() {
191
+ return this.listType === 'picture-card' || this.listType === 'picture-circle'
192
+ },
193
+ visibleFileList() {
194
+ return this.stateFileList.filter(f => f.status !== 'removed')
195
+ },
196
+ showCardTrigger() {
197
+ if (this.disabled) return false
198
+ if (this.maxCount && this.visibleFileList.length >= this.maxCount) return false
199
+ // Single-file card (maxCount 1 or !multiple) hides the add-tile once a file exists —
200
+ // the existing tile itself is the replace surface.
201
+ if (!this.multiple && this.visibleFileList.length >= 1) return false
202
+ return true
203
+ },
204
+ showFlags() {
205
+ const s = this.showUploadList
206
+ if (s && typeof s === 'object') {
207
+ return {
208
+ showPreviewIcon: s.showPreviewIcon !== false,
209
+ showRemoveIcon: s.showRemoveIcon !== false,
210
+ showDownloadIcon: s.showDownloadIcon === true
211
+ }
212
+ }
213
+ return { showPreviewIcon: true, showRemoveIcon: true, showDownloadIcon: false }
214
+ }
215
+ },
216
+
217
+ watch: {
218
+ fileList(val) {
219
+ // Controlled mode: parent is the source of truth — re-sync our working copy.
220
+ if (val != null) this.stateFileList = this.normalize(val)
221
+ }
222
+ },
223
+
224
+ created() {
225
+ this.reqs = {}
226
+ this._uidCache = typeof WeakMap !== 'undefined' ? new WeakMap() : null
227
+ this.stateFileList = this.normalize(this.fileList != null ? this.fileList : this.defaultFileList)
228
+ },
229
+
230
+ // Declare both teardown hooks: Vue 2.7 fires beforeDestroy, Vue 3.4 fires beforeUnmount.
231
+ beforeUnmount() {
232
+ this.abortAll()
233
+ },
234
+ // eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle, vue/no-dupe-keys
235
+ beforeDestroy() {
236
+ this.abortAll()
237
+ },
238
+
239
+ methods: {
240
+ // ---- normalization -------------------------------------------------
241
+ ensureUid(item) {
242
+ if (!this._uidCache) return genUid()
243
+ if (this._uidCache.has(item)) return this._uidCache.get(item)
244
+ const id = genUid()
245
+ this._uidCache.set(item, id)
246
+ return id
247
+ },
248
+ normalize(list) {
249
+ const self = this
250
+ return (list || []).map(item => ({
251
+ uid: item.uid != null ? item.uid : self.ensureUid(item),
252
+ name: item.name,
253
+ size: item.size,
254
+ type: item.type,
255
+ percent: item.percent != null ? item.percent : item.status === 'uploading' ? 0 : 100,
256
+ status: item.status || 'done',
257
+ url: item.url,
258
+ thumbUrl: item.thumbUrl,
259
+ originFileObj: item.originFileObj,
260
+ response: item.response,
261
+ error: item.error,
262
+ loaded: item.loaded,
263
+ total: item.total
264
+ }))
265
+ },
266
+
267
+ // ---- file dialog / drop / paste -----------------------------------
268
+ openFileDialog() {
269
+ if (this.disabled) return
270
+ if (this.$refs.input) this.$refs.input.click()
271
+ },
272
+ onInputChange(e) {
273
+ const files = e.target && e.target.files ? Array.prototype.slice.call(e.target.files) : []
274
+ this.uploadFiles(files)
275
+ // Reset so selecting the same file again re-triggers change.
276
+ if (e.target) e.target.value = ''
277
+ },
278
+ onDragOver() {
279
+ if (this.disabled) return
280
+ this.dragOver = true
281
+ },
282
+ onDragLeave() {
283
+ this.dragOver = false
284
+ },
285
+ onDrop(e) {
286
+ this.dragOver = false
287
+ if (this.disabled) return
288
+ this.$emit('drop', e)
289
+ const dt = e.dataTransfer
290
+ if (!dt) return
291
+ const files = dt.files ? Array.prototype.slice.call(dt.files) : []
292
+ if (files.length) this.uploadFiles(files)
293
+ },
294
+ onPaste(e) {
295
+ if (!this.pastable || this.disabled) return
296
+ const cd = e.clipboardData
297
+ if (!cd) return
298
+ const files = []
299
+ if (cd.items) {
300
+ Array.prototype.slice.call(cd.items).forEach(it => {
301
+ if (it.kind === 'file') {
302
+ const f = it.getAsFile()
303
+ if (f) files.push(f)
304
+ }
305
+ })
306
+ }
307
+ if (files.length) {
308
+ e.preventDefault()
309
+ this.uploadFiles(files)
310
+ }
311
+ },
312
+
313
+ // ---- pipeline ------------------------------------------------------
314
+ uploadFiles(rawFiles) {
315
+ let files = rawFiles.slice()
316
+ // maxCount === 1 → replace with the last selected.
317
+ if (this.maxCount === 1) {
318
+ files = files.slice(-1)
319
+ } else if (this.maxCount) {
320
+ const remaining = this.maxCount - this.visibleFileList.length
321
+ if (remaining <= 0) {
322
+ files.forEach(f => this.$emit('reject', { file: f, reason: 'maxCount' }))
323
+ return
324
+ }
325
+ if (files.length > remaining) {
326
+ files.slice(remaining).forEach(f => this.$emit('reject', { file: f, reason: 'maxCount' }))
327
+ files = files.slice(0, remaining)
328
+ }
329
+ }
330
+ const batch = files
331
+ files.forEach(raw => this.processFile(raw, batch))
332
+ },
333
+
334
+ async processFile(raw, batch) {
335
+ // Static validation first — fail fast without touching the list.
336
+ if (!attrAccept(raw, this.accept)) {
337
+ this.$emit('reject', { file: raw, reason: 'accept' })
338
+ return
339
+ }
340
+ if (this.maxSize && raw.size > this.maxSize) {
341
+ this.$emit('reject', { file: raw, reason: 'size' })
342
+ return
343
+ }
344
+
345
+ const uid = genUid()
346
+ try {
347
+ raw.uid = uid
348
+ } catch (err) {
349
+ // Some environments expose File as non-extensible — tracking still works via file2Obj.
350
+ }
351
+
352
+ let file = raw
353
+ let before = true
354
+ if (this.beforeUpload) {
355
+ try {
356
+ before = this.beforeUpload(raw, batch)
357
+ if (before && typeof before.then === 'function') before = await before
358
+ } catch (err) {
359
+ before = false
360
+ }
361
+ }
362
+
363
+ if (before === LIST_IGNORE) return
364
+
365
+ // beforeUpload may return a replacement File/Blob to upload.
366
+ if (before && before !== true && typeof before === 'object' && (before instanceof Blob || before.name)) {
367
+ file = before
368
+ }
369
+
370
+ if (before === false) {
371
+ // Added to the list but not auto-uploaded (consumer will upload manually).
372
+ this.addFile(file2Obj(file, uid), { autoUpload: false })
373
+ return
374
+ }
375
+
376
+ if (this.transformFile) {
377
+ try {
378
+ let t = this.transformFile(file)
379
+ if (t && typeof t.then === 'function') t = await t
380
+ if (t) file = t
381
+ } catch (err) {
382
+ // keep original file on transform failure
383
+ }
384
+ }
385
+
386
+ const item = file2Obj(file, uid)
387
+ this.addFile(item, { autoUpload: true, rawFile: file })
388
+ },
389
+
390
+ addFile(item, opts) {
391
+ const options = opts || {}
392
+ let nextList
393
+ if (this.maxCount === 1) nextList = [item]
394
+ else nextList = updateFileList(item, this.stateFileList)
395
+ this.commit(item, nextList)
396
+
397
+ // Generate a local thumbnail for image files (picture list types).
398
+ if (isImageFileType(item.type)) {
399
+ previewImage(item).then(url => {
400
+ if (!url) return
401
+ const current = getFileItem(item, this.stateFileList)
402
+ if (current) this.setFile({ ...current, thumbUrl: url })
403
+ })
404
+ }
405
+
406
+ if (options.autoUpload) this.post(item, options.rawFile || item.originFileObj)
407
+ else this.setFile({ ...item, status: 'done', percent: 100 })
408
+ },
409
+
410
+ post(item, rawFile) {
411
+ const actionVal = typeof this.action === 'function' ? this.action(rawFile) : this.action
412
+ const dataVal = typeof this.data === 'function' ? this.data(rawFile) : this.data
413
+ const uid = item.uid
414
+
415
+ // No endpoint and no custom uploader → treat as a manual/selected entry.
416
+ if (!this.customRequest && !actionVal) {
417
+ this.setFile({ ...item, status: 'done', percent: 100 })
418
+ return
419
+ }
420
+
421
+ const request = this.customRequest || defaultRequest
422
+ const handle = request({
423
+ action: actionVal,
424
+ method: this.method,
425
+ headers: this.headers,
426
+ data: dataVal,
427
+ name: this.name,
428
+ filename: this.name,
429
+ withCredentials: this.withCredentials,
430
+ file: rawFile,
431
+ onProgress: e => this.onFileProgress(uid, e),
432
+ onSuccess: (res, xhr) => this.onFileSuccess(uid, res, xhr),
433
+ onError: (err, res) => this.onFileError(uid, err, res)
434
+ })
435
+
436
+ if (handle && typeof handle.abort === 'function') this.reqs[uid] = handle
437
+ },
438
+
439
+ onFileProgress(uid, e) {
440
+ const item = getFileItem({ uid }, this.stateFileList)
441
+ if (!item || item.status !== 'uploading') return
442
+ const percent = Math.round(e.percent || 0)
443
+ const updated = { ...item, percent, status: 'uploading', loaded: e.loaded, total: e.total }
444
+ this.setFile(updated)
445
+ this.$emit('progress', { percent, loaded: e.loaded, total: e.total }, updated)
446
+ },
447
+ onFileSuccess(uid, res) {
448
+ this.clearReq(uid)
449
+ const item = getFileItem({ uid }, this.stateFileList)
450
+ if (!item) return
451
+ this.setFile({ ...item, status: 'done', percent: 100, response: res })
452
+ },
453
+ onFileError(uid, err, res) {
454
+ this.clearReq(uid)
455
+ const item = getFileItem({ uid }, this.stateFileList)
456
+ if (!item) return
457
+ this.setFile({ ...item, status: 'error', error: err, response: res })
458
+ },
459
+
460
+ // ---- list mutation (immutable + emit) ------------------------------
461
+ setFile(file) {
462
+ const nextList = updateFileList(file, this.stateFileList)
463
+ this.commit(file, nextList)
464
+ },
465
+ commit(file, nextList) {
466
+ this.stateFileList = nextList
467
+ this.$emit('change', { file, fileList: nextList })
468
+ this.$emit('update:fileList', nextList)
469
+ },
470
+
471
+ // ---- item actions (called by UploadItem via inject) ----------------
472
+ async handleRemove(file) {
473
+ if (this.beforeRemove) {
474
+ try {
475
+ let r = this.beforeRemove(file, this.stateFileList)
476
+ if (r && typeof r.then === 'function') r = await r
477
+ if (r === false) return
478
+ } catch (err) {
479
+ return
480
+ }
481
+ }
482
+ this.abortReq(file.uid)
483
+ const removedList = removeFileItem(file, this.stateFileList)
484
+ if (removedList === null) return
485
+ const removedFile = { ...file, status: 'removed' }
486
+ this.stateFileList = removedList
487
+ this.$emit('remove', removedFile)
488
+ this.$emit('change', { file: removedFile, fileList: removedList })
489
+ this.$emit('update:fileList', removedList)
490
+ },
491
+ handleRetry(file) {
492
+ if (!file.originFileObj) return
493
+ const item = { ...file, status: 'uploading', percent: 0, error: undefined }
494
+ this.setFile(item)
495
+ this.post(item, file.originFileObj)
496
+ },
497
+ handlePreview(file) {
498
+ this.$emit('preview', file)
499
+ const src = file.url || file.thumbUrl
500
+ if (src && this.resolveIsImage(file)) {
501
+ this.previewImageSrc = src
502
+ this.previewTitle = file.name || 'Preview'
503
+ this.previewVisible = true
504
+ }
505
+ },
506
+ handleDownload(file) {
507
+ this.$emit('download', file)
508
+ if (file.url && typeof window !== 'undefined') window.open(file.url, '_blank')
509
+ },
510
+
511
+ // ---- helpers -------------------------------------------------------
512
+ resolveIsImage(file) {
513
+ if (typeof this.isImageUrl === 'function') return this.isImageUrl(file)
514
+ return isImageUrl(file)
515
+ },
516
+ abortReq(uid) {
517
+ const req = this.reqs[uid]
518
+ if (req && typeof req.abort === 'function') req.abort()
519
+ this.clearReq(uid)
520
+ },
521
+ clearReq(uid) {
522
+ if (this.reqs[uid]) delete this.reqs[uid]
523
+ },
524
+ abortAll() {
525
+ Object.keys(this.reqs || {}).forEach(uid => this.abortReq(uid))
526
+ },
527
+ onPreviewChange(v) {
528
+ this.previewVisible = v
529
+ }
530
+ }
531
+ }
532
+ </script>
533
+
534
+ <style src="./upload.css" scoped></style>