移动端 element + vue 二次封装上传图片/视频的组件
功能:支持同时上传图片和视频,或者只上传图片、只上传视频,可以预览、删除
项目中使用:
1、图片和视频都支持
<el-form-item label="上传影像" class="ml30">
<e-upload-img-video
v-model="fileList"
:limit="6"
accept="video/mp4,video/ogg,video/webm,image/png,image/jpg,image/gif,image/jpeg"
:acceptData="acceptData"
tip="(注:最多上传6个文件,大小不超过20M,仅支持 mp4, ogg, webm, png, jpg, gif, jpeg 格式)"
@change="onFileChange"
>
</e-upload-img-video>
</el-form-item>
data() {
return {
acceptData: {
// 附件格式
accept: '.mp4,.ogg,.webm,.png,.jpg,.gif,.jpeg',
maxsize: 1024 * 1024 * 20,
maxsize_text: '20M',
videoTypeList: ['video/mp4', 'video/ogg', 'video/webm'],
imageTypeList: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']
}
}
}
onFileChange(fileList) {
this.form.fileList = fileList
}
注意:在编辑页面,回显图片的时候,要调用一下initFileList方法,不然之前新增时上传的图片不会显示哦:
this.image = JSON.parse(res.picture)
this.$nextTick(() => {
this.$refs.picture.initFileList(res.picture)
}
2、仅支持图片
<e-upload-img-video v-model="imgList" :limit="16" :onlyImage="true" accept="image/png,image/jpg,image/gif,image/jpeg" :acceptData="imgageData" tip="(注:最多上传16个文件,大小不超过50M,仅支持 png, jpg, gif, jpeg 格式)" @change="onImageChange" > </e-upload-img-video> data() { return { imgList: [], imgageData:{ accept: '.png,.jpg,.gif,.jpeg', maxsize: 1024 * 1024 * 50, maxsize_text: '50M', imageTypeList: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'] } } }, methods: { onImageChange(fileList) { this.form.imgList= fileList this.imgList = fileList } }
注册组件 src/components/index.js
import EUploadImgVideo from './EUploadImgVideo' export default { install(Vue) { Vue.component('EUploadImgVideo', EUploadImgVideo) } }
main.js引入自定义组件:
import Components from '@/components/index'
Vue.use(Components)
二次封装好的上传图片的组件 EUploadImgVideo.vue:
1 <template> 2 <!-- 支持同时上传图片和视频,或者只上传图片、只上传视频,可以预览、删除 --> 3 <div class="any-upload-img-video"> 4 <el-upload 5 :action="uploadApi()" 6 list-type="picture-card" 7 :data="acceptData" 8 :limit="limit" 9 :disabled="disabled" 10 :multiple="multiple" 11 :accept="accept" 12 :file-list="fileList" 13 :on-exceed="handleExceed" 14 :on-change="(file, fileList) => handleChange(file, fileList)" 15 :on-success="(response, file, fileList) => handleSuccess(response, file, fileList)" 16 :before-upload="file => beforeUpload(file)" 17 > 18 <i slot="default" class="el-icon-camera"></i> 19 <div slot="file" slot-scope="{ file }" class="custom-file-list"> 20 <!-- 删除 --> 21 <i class="el-icon-close" v-if="!disabled" @click="handleRemove(file)"></i> 22 23 <!-- 视频 --> 24 <video 25 class="el-upload-list__item-thumbnail" 26 :src="file.url" 27 alt="" 28 @click="handlePreview(file, 'video')" 29 v-if=" 30 ['mp4', 'ogg', 'webm', 'avi', 'wma', 'mpeg', 'mov', 'mlv'].indexOf( 31 file.name.substring(file.name.indexOf('.') + 1) 32 ) > -1"> 33 </video> 34 35 <!-- 图片 --> 36 <img 37 class="el-upload-list__item-thumbnail" 38 :src="file.url" 39 alt="" 40 @click="handlePreview(file, 'img')" 41 v-if=" 42 ['jpg', 'png', 'gif', 'jpeg', 'bmp', 'tif', 'avif', 'pcx', 'tga'].indexOf( 43 file.name.substring(file.name.indexOf('.') + 1) 44 ) > -1" 45 /> 46 47 <el-progress 48 type="circle" 49 v-if="showProgress && file.url == uploadUrl" 50 :percentage="Number(file.percentage)" 51 :width="64"> 52 </el-progress> 54 </div> 55 </el-upload> 56 57 <!-- 图片预览 --> 58 <van-image-preview 59 v-model="isShowPreview" 60 :images="previewImages" 61 @change="onChange" 62 :closeable="true" 63 :startPosition="startPosition"> 64 <template v-slot:index>第{{ imgIndex + 1 }}页</template> 65 </van-image-preview> 66 67 <!-- 视频预览 --> 68 <div class="video-box" v-if="isShowVideoPreview"> 69 <van-icon name="cross" class="close" @click="isShowVideoPreview = !isShowVideoPreview" /> 70 <video :src="videoUrl" controls alt=""></video> 71 </div> 72 73 <!-- 提示 --> 74 <el-row class="limit-tip"> 75 <span>{{ tip }}</span> 76 </el-row> 77 </div> 78 </template> 79 80 <script> 81 const uidGenerator = () => { 82 return '-' + parseInt(Math.random() * 10000 + 1, 10) 83 } 84 85 const getFileName = path => { 86 if (path.lastIndexOf('\\') >= 0) { 87 const reg = new RegExp('\\\\', 'g') 88 path = path.replace(reg, '/') 89 } 90 return path.substring(path.lastIndexOf('/') + 1) 91 } 92 93 export default { 94 name: 'EUploadImgVideo', 95 props: { 96 // 是否支持批量上传 97 multiple: { 98 type: Boolean, 99 default: () => false 100 }, 101 102 // 绑定的数据 103 value: { 104 type: [String, Array], 105 required: false 106 }, 107 108 // 禁用上传 109 disabled: { 110 type: Boolean, 111 default: false, 112 required: false 113 }, 114 115 // 文件类型 116 accept: { 117 type: String, 118 default: 'video/mp4,video/ogg,video/webm,image/png,image/jpg,image/gif,image/jpeg' 119 }, 120 121 // 上传时附带的额外参数 122 acceptData: { 123 type: Object, 124 default: () => { 125 return { 126 accept: '.mp4,.ogg,.webm,.png,.jpg,.gif,.jpeg', // 同时支持视频和图片时 127 maxsize: 1024 * 1024 * 500, 128 maxsize_text: '500M', 129 videoTypeList: ['video/mp4', 'video/ogg', 'video/webm'], 130 imageTypeList: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'] 131 } 132 } 133 }, 134 135 // 提示 136 tip: { 137 type: String, 138 default: '最多上传10个文件,仅支持 mp4, ogg, webm, png, jpg, gif, jpeg 格式' 139 }, 140 141 // 最大数量 142 limit: { 143 type: Number, 144 default: 10 145 }, 146 147 // 仅上传图片 148 onlyImage: { 149 type: Boolean, 150 default: () => false 151 }, 152 153 // 仅上传视频 154 onlyVideo: { 155 type: Boolean, 156 default: () => false 157 } 158 }, 159 160 watch: { 161 value: { 162 handler(val, oldValue) { 163 // 单选上传可以改变 fileList 的值,不用手动回显 164 // 批量上传时不能改变 fileList 的值,页面会报错,如果有新增页面和编辑页面,需要手动调用 $refs.xxx.initFileList() 来回显图片或视频 165 if (!this.multiple) { 166 this.initFileList(val) 167 } 168 }, 169 // 立刻执行handler 170 immediate: true 171 } 172 }, 173 174 data() { 175 return { 176 fileList: [], // 上传的文件数据 177 isShowPreview: false, // 预览图片 178 isShowVideoPreview: false, // 预览视频 179 previewImages: [], // 预览的图片数据 180 previewVideo: [], // 预览的视频数据 181 imgIndex: 0, // 预览图片的索引 182 videoUrl: '', // 预览视频的url 183 startPosition: 0, // 预览图片的初始索引 184 showProgress: false, 185 uploadPercentage: 0 186 } 187 }, 188 189 methods: { 190 // 图片/视频回显 191 initFileList(array) { 192 if (!array || array.length === 0) { 193 this.fileList = [] 194 return 195 } 196 const fileList = [] 197 const arr = array || [] 198 for (let a = 0; a < arr.length; a++) { 199 const url = this.getFileAccessHttpUrl(arr[a].url) 200 fileList.push({ 201 name: getFileName(arr[a].name), 202 url: url, 203 uid: uidGenerator(), 204 type: arr[a].type, 205 response: { 206 status: 'history', 207 message: url 208 } 209 }) 210 } 211 this.fileList = fileList 212 }, 213 214 // url加/dispatch前缀,用于预览使用 215 getFileAccessHttpUrl(url) { 216 if (url.indexOf('/dispatch') > -1) { 217 return url 218 } else { 219 return '/dispatch' + url 220 } 221 }, 222 223 // 上传文件地址 224 uploadApi() { 225 return '/dispatch/upload' 226 }, 227 228 // 其他附件上传大小限制处理 229 beforeUpload(file) { 230 const { videoTypeList, imageTypeList } = this.acceptData 231 let isVideo = true 232 if (videoTypeList) { 233 isVideo = videoTypeList.indexOf(file.type) !== -1 234 } 235 236 let isImg = true 237 if (imageTypeList) { 238 isImg = imageTypeList.indexOf(file.type) !== -1 239 } 240 241 // 只支持视频 242 if (this.onlyVideo) { 243 if (!isVideo) { 244 this.$toast(`只能上传${this.acceptData.accept}的文件!`) 245 return false 246 } 247 } 248 249 // 只支持图片 250 if (this.onlyImage) { 251 if (!isImg) { 252 this.$toast(`只能上传${this.acceptData.accept}格式的文件!`) 253 return false 254 } 255 } 256 257 // 默认:视频和图片都支持 258 if (!isVideo && !isImg) { 259 this.$toast(`只能上传${this.acceptData.accept}格式的文件!`) 260 return false 261 } 262 263 // 文件大小限制 264 const isLessThanMaxSize = file.size < this.acceptData.maxsize 265 if (!isLessThanMaxSize) { 266 this.$toast(`上传文件大小不能超过${this.acceptData.maxsize_text}!`) 267 return false 268 } else { 269 this.$toast.loading({ 270 message: '请您耐心等待,文件上传中...', 271 forbidClick: true, 272 duration: 0, 273 loadingType: 'spinner' 274 }) 275 } 276 return true 277 }, 278 279 // 文件个数限制 280 handleExceed() { 281 this.$toast(`当前限制最多上传 ${this.limit} 个文件!`) 282 }, 283 284 // 预览图片 285 onChange(index) { 286 this.imgIndex = index 287 }, 288 289 handleChange(file, fileList, type) { 290 this.uploadPercentage = 0 291 if (file.status === 'ready') { 292 this.showProgress = true 293 this.uploadUrl = file.url 294 const interval = setInterval(() => { 295 if (this.uploadPercentage >= 95) { 296 clearInterval(interval) 297 } 298 this.uploadPercentage += 1 299 }, 20) 300 } 301 if (file.status === 'success') { 302 this.uploadUrl = file.url 303 this.uploadPercentage = 100 304 this.showProgress = false 305 } 306 }, 307 308 // 上传图片成功 309 handleSuccess(response, file, fileList) { 310 this.$toast.clear() 311 this.showProgress = false 312 if (response.code === 200) { 313 this.$toast('上传成功') 314 const attachment = fileList.map(item => { 315 if (item.response && item.response.data) { 316 item.url = '/dispatch' + item.response.data || '' 317 } 318 return { 319 name: item.name, 320 url: item.url, 321 uid: item.uid, 322 type: item.raw ? item.raw.type : item.type 323 } 324 }) 325 this.fileList = attachment 326 // 回显给业务,不携带 /dispatch 前缀 327 this.$emit('change', this.fileList) 328 } else { 329 for (let i = 0; i < fileList.length; i++) { 330 if (i + 1 === fileList.length) { 331 fileList.splice(i, 1) 332 } 333 } 334 this.$toast(response.msg) 335 } 336 }, 337 338 // 图片/视频预览 339 handlePreview(file, type) { 340 this.previewImages = [] 341 this.previewVideo = [] 342 if (type === 'img') { 343 this.fileList.forEach((item, index) => { 344 if (item.url === file.url) { 345 this.startPosition = index 346 } 347 }) 348 } 349 350 this.fileList.map(item => { 351 // 预览图片的数据 352 const { imageTypeList, videoTypeList } = this.acceptData 353 let isImg = true 354 if (imageTypeList) { 355 isImg = imageTypeList.indexOf(item.type) !== -1 356 } 357 if (isImg) { 358 this.previewImages.push(item.url) 359 } 360 361 // 预览视频的数据 362 let isVideo = true 363 if (videoTypeList) { 364 isVideo = videoTypeList.indexOf(item.type) !== -1 365 } 366 if (isVideo) { 367 this.previewVideo.push(item.url) 368 } 369 }) 370 371 if (type === 'img') { 372 this.isShowPreview = true 373 } else { 374 this.videoUrl = file.url 375 this.isShowVideoPreview = true 376 } 377 }, 378 379 // 删除 380 handleRemove(file) { 381 let index 382 this.fileList.find((item, idx) => { 383 if (item.uid === file.uid) { 384 index = idx 385 return 386 } 387 }) 388 if (typeof index !== 'undefined') { 389 this.fileList.splice(index, 1) 390 } 391 // 回显给业务 392 this.$emit('change', this.fileList) 393 }, 394 395 // 长按保存图片 396 touchStart(url, file) { 397 clearTimeout(this.time) 398 this.time = setTimeout(() => { 399 // 这里就按照chrome等新版浏览器来处理 400 const link = document.createElement('a') 401 link.href = url 402 link.setAttribute('download', file.name) 403 link.click() 404 }, 1000) 405 }, 406 407 touchEnd() { 408 clearTimeout(this.time) 409 } 410 } 411 } 412 </script> 413 414 <style lang="scss" scoped> 415 .custom-box-img { 416 position: fixed; 417 z-index: 1002; 418 left: 0; 419 right: 0; 420 top: 1rem; 421 display: flex; 422 bottom: 0; 423 background-color: #ffffff; 424 align-items: center; 425 .download-icon { 426 margin: 0 auto; 427 z-index: 1003; 428 color: #ffffff; 429 font-size: 18px; 430 margin-top: 10px; 431 ::v-deep .el-icon-download:before { 432 font-size: 20px; 433 color: #ffffff; 434 vertical-align: middle; 435 } 436 .el-button--primary.is-disabled { 437 background-color: #409eff; 438 border-color: #409eff; 439 } 440 } 441 .el-carousel__container { 442 height: calc(100vh - 26px); 443 } 444 .el-carousel__arrow { 445 background-color: rgba(31, 45, 61, 0.5); 446 line-height: 36px; 447 } 448 .el-carousel__indicators--outside { 449 bottom: 0; 450 line-height: 26px; 451 } 452 } 453 454 /deep/ .el-upload-list--picture-card .el-upload-list__item-thumbnail { 455 width: 100%; 456 height: 100%; 457 } 458 .limit-tip { 459 color: #999999; 460 display: inline-block; 461 line-height: 20px; 462 margin-top: 5px; 463 font-size: 14px; 464 } 465 .video-box { 466 position: fixed; 467 top: 0; 468 left: 0; 469 width: 100%; 470 height: 100%; 471 background: #000; 472 z-index: 9999; 473 vertical-align: middle; 474 text-align: center; 475 video { 476 height: 60%; 477 margin-top: 20%; 478 width: 100%; 479 } 480 .close { 481 position: absolute; 482 top: 10px; 483 right: 10px; 484 color: #fff; 485 font-size: 28px; 486 } 487 } 488 489 .custom-file-list { 490 position: relative; 491 height: 75px; 492 font-size: 16px; 493 .el-icon-close { 494 position: absolute; 495 right: 0; 496 top: 0; 497 z-index: 1000; 498 display: block !important; 499 font-size: 18px; 500 color: #ffffff; 501 background-color: #a9a6a6; 502 border-radius: 100%; 503 } 504 } 505 506 ::v-deep .el-upload-list--picture-card .el-upload-list__item { 507 width: 75px; 508 height: 75px; 509 } 510 ::v-deep .el-upload--picture-card { 511 width: 75px; 512 height: 73px; 513 line-height: 78px; 514 background-color: #f9f9f9; 515 } 516 ::v-deep .el-upload-list--picture-card .el-progress { 517 width: 75px !important; 518 } 519 ::v-deep .el-progress-circle { 520 width: 75px !important; 521 height: 75px !important; 522 } 523 ::v-deep .van-image-preview { 524 top: 40px; 525 height: 95%; 526 } 527 </style>
生活是痛苦的白天,死亡是凉爽的夜晚。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具