index.vue
11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
<template>
<div class="component-upload-image">
<el-row>
<draggable
v-if="fileList.length > 0"
v-model="fileList"
item-key="index"
@end="draggableEnd"
>
<template #item="{ element }">
<div class="fileItem el-upload-list__item is-success animated">
<img
:src="fillImgUrl(element.url)"
alt=""
class="el-upload-list__item-thumbnail"
>
<div class="hover-actions">
<span @click="handlePictureCardPreview(element)">
<el-icon><ZoomIn /></el-icon>
</span>
<span v-if="!disabled" @click="handleDelete(element)">
<el-icon><Delete /></el-icon>
</span>
</div>
</div>
</template>
</draggable>
<el-upload
ref="imageUpload"
:accept="accept"
:action="uploadImgUrl"
:before-remove="handleDelete"
:before-upload="handleBeforeUpload"
:class="{ hide: fileList.length >= limit }"
:disabled="disabled"
:file-list="fileListInUpload"
:headers="headers"
:limit="limit"
:multiple="limit > 1"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-preview="handlePictureCardPreview"
:on-success="handleUploadSuccess"
:show-file-list="true"
list-type="picture-card"
name="image"
>
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload>
</el-row>
<!-- 上传提示 -->
<div v-if="showTip && fileList.length < limit" class="el-upload__tip">
请上传
<template v-if="fileSize">
大小不超过
<b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为
<b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件
</div>
<el-dialog
v-model="dialogVisible"
append-to-body
title="预览"
width="800px"
>
<img
:src="fillImgUrl(dialogImageUrl)"
alt="" style="display: block; max-width: 100%; margin: 0 auto"
>
</el-dialog>
</div>
</template>
<script setup>
import { getToken } from '@/utils/auth'
import { computed, getCurrentInstance, ref, watch, nextTick } from 'vue'
import _ from 'lodash'
import { fillImgUrl } from '@/utils/ruoyi'
import draggable from 'vuedraggable'
const props = defineProps({
modelValue: [String, Object, Array],
// 图片数量限制
limit: {
type: Number,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ['png', 'jpg', 'jpeg']
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
})
const accept = computed(() => {
return _.map(props.fileType, (t) => {
if (t.indexOf('.') === 0) {
return t
} else {
return '.' + t
}
}).join(',')
})
const { proxy } = getCurrentInstance()
const emit = defineEmits(['update:modelValue'])
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = ref(
import.meta.env.VITE_APP_BASE_API + '/fileServer/uploadImg'
) // 上传的图片服务器地址
const headers = ref({ Authorization: 'Bearer ' + getToken() })
const fileList = ref([])
const fileListInUpload = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
)
// 监听外部传入的值
watch(
() => props.modelValue,
(val) => {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(',')
// 过滤掉空字符串
const validList = list.filter(item => item && item.trim())
// 然后将数组转为对象数组
fileList.value = validList.map((item) => {
if (typeof item === 'string') {
if (item.indexOf('http') === -1) {
return { name: item, url: item }
} else {
return { name: item, url: item }
}
}
return item
})
// 同步更新显示列表
updateFileListInUpload()
} else {
fileList.value = []
fileListInUpload.value = []
}
},
{ deep: true, immediate: true }
)
// 更新 fileListInUpload 的显示逻辑
function updateFileListInUpload() {
if (fileList.value.length > 1) {
// 多张图片时使用 draggable 组件显示,el-upload 不显示图片列表
fileListInUpload.value = []
} else {
// 单张图片时使用 el-upload 显示图片列表
fileListInUpload.value = [...fileList.value]
}
}
// 上传前loading加载
function handleBeforeUpload(file) {
let isImg = false
if (props.fileType.length) {
let fileExtension = ''
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
}
isImg = props.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
})
} else {
isImg = file.type.indexOf('image') > -1
}
if (!isImg) {
proxy.$modal.msgError(
`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`
)
return false
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传图片大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading('正在上传图片,请稍候...')
number.value++
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
const newFile = { name: file.name, url: res.data || res.msg }
uploadList.value.push(newFile)
uploadedSuccessfully()
} else {
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
// 移除上传失败的文件
const index = uploadList.value.findIndex(f => f.name === file.name)
if (index > -1) {
uploadList.value.splice(index, 1)
}
// 强制刷新视图
nextTick(() => {
if (proxy.$refs.imageUpload) {
proxy.$refs.imageUpload.clearFiles()
}
})
uploadedSuccessfully()
}
}
// 删除图片
function handleDelete(file) {
// 查找要删除的文件索引
const findex = fileList.value.findIndex((f) => f.url === file.url || f.name === file.name)
if (findex > -1) {
// 如果是正在上传过程中的文件,需要特殊处理
if (uploadList.value.length === number.value && number.value > 0) {
// 上传过程中的删除,从上传列表中移除
const uploadIndex = uploadList.value.findIndex((f) => f.name === file.name)
if (uploadIndex > -1) {
uploadList.value.splice(uploadIndex, 1)
number.value--
}
}
// 从文件列表中删除
fileList.value.splice(findex, 1)
// 强制刷新视图
nextTick(() => {
if (proxy.$refs.imageUpload) {
proxy.$refs.imageUpload.clearFiles()
}
})
// 更新外部绑定的值
emit('update:modelValue', listToString(fileList.value))
// 更新显示列表
updateFileListInUpload()
// 关闭loading
if (number.value === 0) {
proxy.$modal.closeLoading()
}
}
return false
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
// 过滤掉已经是 blob 的临时文件
const validUploadList = uploadList.value.filter(f => f.url && f.url.indexOf('blob:') !== 0)
// 合并原有文件和新增文件
fileList.value = [...fileList.value.filter((f) => f.url !== undefined && f.url.indexOf('blob:') !== 0), ...validUploadList]
// 清空上传临时列表
uploadList.value = []
number.value = 0
// 更新外部绑定的值
emit('update:modelValue', listToString(fileList.value))
// 关闭loading
proxy.$modal.closeLoading()
// 更新显示列表
updateFileListInUpload()
// 强制刷新el-upload组件显示
nextTick(() => {
if (fileList.value.length === 1 && proxy.$refs.imageUpload) {
// 触发el-upload重新渲染
proxy.$refs.imageUpload.clearFiles()
}
})
}
}
// 上传失败
function handleUploadError(err, file) {
console.error('上传图片失败:', err)
proxy.$modal.msgError('上传图片失败')
proxy.$modal.closeLoading()
// 移除上传失败的文件
const index = uploadList.value.findIndex(f => f.name === file.name)
if (index > -1) {
uploadList.value.splice(index, 1)
}
if (number.value > 0 && uploadList.value.length === number.value) {
uploadedSuccessfully()
}
}
// 预览
function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url
dialogVisible.value = true
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = ''
separator = separator || ','
for (const i in list) {
if (undefined !== list[i].url && list[i].url.indexOf('blob:') !== 0) {
strs += list[i].url + separator
}
}
return strs !== '' ? strs.substr(0, strs.length - 1) : ''
}
// 拖拽排序结束
function draggableEnd() {
emit('update:modelValue', listToString(fileList.value))
// 拖拽后也需要更新显示
updateFileListInUpload()
}
// 清空所有文件
function clearFile() {
fileList.value = []
fileListInUpload.value = []
uploadList.value = []
number.value = 0
emit('update:modelValue', '')
if (proxy.$refs.imageUpload) {
proxy.$refs.imageUpload.clearFiles()
}
}
// 重新上传(用于外部调用)
function reUpload() {
if (proxy.$refs.imageUpload) {
proxy.$refs.imageUpload.clearFiles()
}
fileList.value = []
fileListInUpload.value = []
uploadList.value = []
number.value = 0
emit('update:modelValue', '')
}
defineExpose({
clearFile,
reUpload
})
</script>
<style lang="scss" scoped>
// .el-upload--picture-card 控制加号部分
:deep(.hide .el-upload--picture-card) {
display: none;
}
// 当只有一张图片时,隐藏上传按钮的样式(可选)
:deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 148px;
height: 148px;
}
.fileItem {
position: relative;
width: 160px;
height: 160px;
overflow: hidden;
margin: 0 20px 20px 0;
float: left;
border-radius: 6px;
border: 1px solid #c0ccda;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.hover-actions {
background: rgba(0, 0, 0, 0.6);
width: 100%;
height: 100%;
position: absolute;
top: 100%;
left: 0;
color: #fff;
transition: top 0.2s;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
span {
cursor: pointer;
font-size: 24px;
padding: 8px;
margin: 0 8px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
}
}
}
.fileItem:hover .hover-actions {
top: 0;
}
// 上传组件样式优化
:deep(.el-upload--picture-card) {
width: 148px;
height: 148px;
line-height: 148px;
}
:deep(.el-upload-list__item) {
transition: all 0.3s;
}
// 动画效果
.animated {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>