封装:el-upload上传图片组件(解决图片闪动、多选问题)_upload上传图片闪屏问题怎么解决-程序员宅基地

技术标签: Vue+admin后台管理系统  elementui  vue.js  javascript  

流程描述:使用el-upload组件,自定义上传方法(调后台接口),传图片file给后台,后台返回对应阿里云的oss链接,前端临时保存,最后点击页面提交按钮,再传后台oss数组链接。

项目遇到问题描述&解决

  1. 将后台返回的oss直接赋值给el-upload对应的file-list属性绑定值对象的url字段时,会出现闪动问题。--默认组件绑定的url是blob链接,如果替换url取值,就会出现闪动问题。
  2. 设置multiple多选后,因后台接口限制1秒内不能有重复请求,添加时间戳参数,也会出现上传多张,部分图片的时间戳有的一样的问题,需使用uid(图片文件唯一标志)字段来代替时间戳。

其他待解决问题:(1)粘贴上传图片,不点击输入框,点击粘贴元素所在的行空白处,然后按ctrl+v也会触发粘贴paste事件,即也会自动上传,但此时点击确定所在行,再按ctrl+v又不触发了,失焦导致? 

注意点:(1)需让后台在返回的文件URL中添加,唯一字符串后缀,如UUID等,确保图片文件名不会重复! 

效果图

一、vue2+TS版上传组件

<!--
 * @Description: 上传图片组件(可粘贴上传)
 * @Version:
 * @Author: xxx
 * @Date: 2022-10-11 10:26:13
 * @LastEditors: Please set LastEditors
 * @LastEditTime: 2022-10-10 13:33:20
-->

<template>
  <div class="upload-imgs-box">
    <div class="main-content">
      <el-upload
        ref="uploadRef"
        list-type="picture-card"
        action="#"
        :accept="accept"
        :class="{'upload__disabled': isDisableUpload}"
        :auto-upload="true"
        :multiple="multiple"
        :file-list="allFileList"
        :show-file-list="true"
        :limit="limit"
        :before-upload="handleBeforeUpload"
        :http-request="uploadOSS"
        :on-success="handleSuccess"
        :on-error="handleError"
        :on-remove="handleRemove"
        :on-exceed="handleExceed"
        :on-preview="handlePreview"
        v-loading="loading"
        element-loading-text="正在上传中,请稍候..."
        element-loading-background="rgba(0, 0, 0, 0.5)"
        >
        <i class="el-icon-plus"></i>
      </el-upload>
      <!-- 显示复制元素 & 手机端 & 上传个数不大于限制时:才显示粘贴图片上传组件 -->
      <div v-if="showCopy && !isMobile && (allFileList.length < limit)" class="copy_wrap mt-10" @paste="handlePaste" :contenteditable="false">
        <input type="text" ref="copyInputRef" class="copy_main_content" autosize placeholder="点击粘贴图片到此处" maxlength="0">
      </div>
       <slot name="tip"></slot>
    </div>
    <el-dialog :visible.sync="showPreviewImage" append-to-body>
      <img width="100%" :src="priviewImageUrl" alt="">
    </el-dialog>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop, Ref } from 'vue-property-decorator'
import { isPC } from '@/utils/index'
import { getOSSUrlByFile } from '@/api/common'

// element中回调函数中返回的file
export interface ElFile {
  name: string // 文件名:如:截屏2021-12-14 下午8.31.25.png
  percentage: number
  raw: File
  size: number
  status: 'fail' | 'ready' | 'success' // 文件的状态(3种)
  uid: number // 文件唯一标志:ID
  url: string // 如:"blob:http://localhost:9527/27499ca8-6ff1-4fa1-a3de-c64e4d8708e2"
  oss?: string // 自己附加的属性
}

// 使用 http-request属性后,默认回调中返回的信息
interface HttpRequestInfo {
  action?: string
  file: File & { uid: number }
  [key: string]: any
}

@Component({
  name: 'UploadImgs'
})
export default class UploadImgs extends Vue {
  @Prop({ default: 9 }) private limit!: number // 最多上传个数, 默认是9张
  @Prop({ default: false }) private showCopy!: boolean // 是否需要展示粘贴图片插件,默认不展示
  @Prop({ default: 5 }) private maxFileSize!: number // 图片上传的最大文件大小为(单位是兆,M),默认是5M
  @Prop({ default: '.png,.PNG,.jpg,.JPG,jpeg,.JPEG,.bmp,.BMP,.gif,.GIF' }) private accept!: string // 可上传的图片类型
  @Prop({ default: true }) private multiple!: boolean // 是否支持多选
  @Prop({ default: () => [] }) private list!: string[] // 用于实时更新父组件的变量值
  @Ref() copyInputRef!: HTMLElement // 复制图片的input

  private priviewImageUrl = ''
  private showPreviewImage = false
  private disabled = false
  private allFileList: Partial<ElFile>[] = []
  private loading = false

  get isMobile() {
    return !isPC()
  }

  get isDisableUpload() {
    return this.allFileList.length === this.limit
  }

  // 上传OSS:获取OSS链接
  private uploadOSS(infoObj: HttpRequestInfo, isCopyUploadFlag: string) {
    this.handleMultipleUpload(infoObj, isCopyUploadFlag)
  }

  private handleMultipleUpload(infoObj: HttpRequestInfo, isCopyUploadFlag: string) {
    this.loading = true
    const { file } = infoObj
    const { uid, name } = file
    const formData = new FormData()
    formData.append('file', file)
    formData.append('uid', uid + '')
    formData.append('name', name)
    formData.append('upload_time_stamp', Date.now() + '') // 注意:需让后台确保每个文件有一个唯一后缀名,防止文件名重名问题!
    getOSSUrlByFile(formData).then(res => {
      if (res.code === 200) {
        if (isCopyUploadFlag === 'isCopy') {
          this.copyInputRef.blur() // 移除复制图片框中的光标
          const { oss } = res.data
          if (oss) {
            this.allFileList.push({ url: oss })
            this.$emit('update:list', this.allFileList.map(item => item?.url))
          }
        } else {
          infoObj.onSuccess(res)
        }
      } else {
        const isObj = Object.prototype.toString.call(res) === '[object Object]' // true 代表为对象
        let tip = ''
        if (isObj) {
          if (Object.keys(res).length === 0) {
            tip = '1个账号不能多端登录或其他原因'
          } else {
            tip = res.message || JSON.stringify(res)
          }
        } else {
          tip = res
        }
        this.$message.warning(`上传失败, 原因可能为:${tip}`)
        infoObj.onError(res)
      }
    }).finally(() => {
      this.loading = false
    })
  }

  private handleSuccess(res: { code: number, data: { oss: string } }, file: ElFile, fileList: ElFile[]) {
    const { oss } = res.data
    console.log('文件上传成功', res, file, fileList)
    if (res.code === 200) {
      file.oss = oss // 注意:此处不能直接将后台返回的oss赋值给file.url字段,会出现闪动现象。
      this.allFileList = fileList
      this.$emit('update:list', this.allFileList.map(item => item?.oss)) // 更新父组件的变量值
    } else {
      console.log('接口报错非200')
    }
  }

  private handleError() {
    // 注意:该回调函数必须写,不然:上传失败,页面上还显示图片。写了之后:页面会自动删除失败的图片。
    console.log('文件上传失败')
  }

  private handleRemove(file: ElFile, fileList: ElFile[]) {
    this.allFileList = fileList
    this.$emit('update:list', this.allFileList.map(item => item?.oss || item?.url)) // 更新父组件的变量值
    console.log('文件删除了', this.allFileList, file, fileList)
  }

  // 上传前:钩子函数
  private handleBeforeUpload(file: File) {
    const { size } = file
    const isLessLimitMaxFileSize = size / 1024 / 1024 < this.maxFileSize // 将文件大小转为M单位,并跟最大限制做比较
    if (!isLessLimitMaxFileSize) {
      this.$message.warning(`上传图片大小,不能超过 ${this.maxFileSize}M!`)
      return false
    } else {
      return true
    }
  }

  // 文件勾选超出限制
  private handleExceed(files: File[], fileList: ElFile[]) {
    const emptyNum = this.limit - (fileList.length)
    console.log('目前还能上传个数为', emptyNum)
    this.$message.warning(`上传文件个数,超出限制(最多${this.limit}张)`)
  }

  // 图片粘贴触发事件
  private handlePaste(event: any) {
    const items = (event.clipboardData || (window as any).clipboardData).items
    // 去除粘贴到div事件
    event.preventDefault()
    event.returnValue = false
    let file = null
    if (!items || items.length === 0) {
      this.$message.warning('当前不支持本地图片上传')
      return
    }
    // 搜索剪切板items
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.includes('image')) {
        file = items[i].getAsFile()
        break
      }
    }
    if (!file) {
      this.$message.warning('粘贴内容非图片')
      return
    }
    if (!this.handleBeforeUpload(file)) {
      return false
    }
    this.uploadOSS({ file: file }, 'isCopy')
  }

  // 预览:当前图片
  private handlePreview(file: ElFile) {
    this.priviewImageUrl = file.url
    this.showPreviewImage = true
  }
}
</script>

<style lang="scss" scoped>
  .upload-imgs-box {
    .copy_wrap {
      width: 148px;
      height: 148px;
      text-align: center;
      .copy_main_content {
        width: 148px;
        height: 148px;
        text-align: center;
        border: 1px dashed #c0ccda;
        border-radius: 6px;
        outline-color: #409EFF; // 设置input点击后出现的边框的颜色
      }
    }
    .upload__disabled {
      ::v-deep .el-upload--picture-card {
        display: none; // 隐藏上传组件
      }
    }
  }
</style>

二、父组件中如何使用&获取上传的列表数据

方法1:通过.sync修饰符

子组件中通过 $emit('update:prop变量名', '传递给父组件的新值') 来实时更新父组件的数据。注意:冒号后不能有空格!否则不生效。

 <upload-imgs ref="upload_image_box" :list.sync="parentForm.imgsList" :showCopy="false"></upload-imgs>

方法2:通过 ref 获取子组件实例

通过 ref 获取上传组件实例,从而获取对应上传url列表数组数据。

<template>
   <div>
      <upload-imgs ref="upload_image_box" :showCopy="false">
        <div class="tip-color">最多上传9张图片,只支持jpg、png、jpeg、bmp或gif格式,且单个图片文件不能超过5M。</div>
      </upload-imgs>
      <el-button type="primary" @click="submit">确定</el-button>
   </div>
</template>


<script lang="ts">
... // 省略code
interface UploadImageComponent {
  allFileList: Partial<ElFile>[] // 最后上传的所有图片数组[{ url: 'http:xxx', ... }, { oss: 'http:xx', ... }]
}
@Ref('upload_image_box') readonly uploadImageCompoent!: UploadImageComponent

private submit() {
    const { allFileList = [] } = this.uploadImageCompoent
    ... // 省略code
    const imagesList: string[] = []
    allFileList.forEach((item: Partial<ElFile>) => {
      if (item.oss) {
        imagesList.push(item.oss) // 通过el-uplod上传的
      } else if (item.url) {
        imagesList.push(item.url) // 通过复制粘贴上传的
      }
    })
    // 最后提交给后台的参数:files为所有oss图片链接的数组字段。
    const params = { files: imagesList, .... }
    ... // 调后台接口
}
</script>

其他涉及代码

// 判断是否是PC端显示
export const isPC = () => {
  const userAgentInfo = navigator.userAgent
  const Agents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod']
  let isPCFlag = true
  for (let v = 0; v < Agents.length; v++) {
    if (userAgentInfo.indexOf(Agents[v]) > 0) {
      isPCFlag = false
      break
    }
  }
  return isPCFlag
}


// 上传File文件获取OSS链接(仅单个,不支持批量!)
export const getOSSUrlByFile = (data = {}) => {
  return request({ // axios库相关封装的实现,注意需判断formdata类型的post请求不能序列化。
    url: 'xxxxxxxx',
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    method: 'post',
    data
  })
}


// 上传类接口(传参为FormData类型的):不能序列化,否则会被序列化为空对象!
... // axios请求拦截器
if (request.method === 'post' && request.data) {
    if (!(request.data instanceof FormData)) {
      request.data = qs.stringify(request.data)
    }
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_38969618/article/details/127266782

智能推荐

使用lua_next()遍历表-程序员宅基地

文章浏览阅读193次。转自:http://hi.baidu.com/bitbull/blog/item/bc27581eca1886f61bd5768e.html问题概要: 用lua写了个函数,返回的是一个表.需要在C里对返回的表里元素做二次处理.在C里我们可以通过lua_gettable()或者lua_rawget()来获取表里元素值,但使用这两个接口的前提是你得知道key,它才能给你value. 当然对于顺..._lua_next遍歷

JAVA关键字替换-程序员宅基地

文章浏览阅读832次。import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import org.apache.commons.lang3.StringUtils;/** * 关键字替换类 * */public class Keywor..._java 多个关键词全词匹配替换

Android View坐标系详解(getTop()、getX、getTranslationX...)_findviewbyposition-程序员宅基地

文章浏览阅读3.7w次,点赞20次,收藏59次。View 提供了如下 5 种方法获取 View 的坐标:1. View.getTop()、View.getLeft()、View.getBottom()、View.getRight();2. View.getX()、View.getY();3. View.getTranslationX()、View.getTranslationY();4. View.getLocationOnScreen(_findviewbyposition

QT的QQmlProperty类的使用_qqmlproperty::read-程序员宅基地

文章浏览阅读817次。详细说明QQmlProperty类抽象了从QML创建的对象的访问属性。由于QML使用Qt的元类型系统,因此所有现有的QMetaObject类都可以用于自省和与QML创建的对象进行交互。但是,QML提供的一些新功能(例如类型安全性和附加属性)最容易通过QQmlProperty类使用,从而简化了某些自然复杂性。与QMetaProperty代表类类型的属性不同,QQmlProperty封装特定对象实例上的属性。为了读取属性的值,程序员创建一个QQmlProperty实例并调用read()方法。同样地,使用w_qqmlproperty::read

PL SQL基本语法要术_\pl/sql的基本语法:_____________、_____________、_________-程序员宅基地

文章浏览阅读321次。转自: http://www.cnblogs.com/HondaHsu/archive/2007/06/20/776303.html常量 1. 定义常量的语法格式 常量名 constant 类型标识符 [not null]:=值; 常量,包括后面的变量名都必须以字母开头,不能有空格,不能超过30个字符长度,同时不能和保留字同名,常(变)量名称不区分大小_\pl/sql的基本语法:_____________、_____________、_____________、_____________

QT的QAudioRecorder类的使用-程序员宅基地

文章浏览阅读1.5k次。QAudioRecorder类用于记录音频。QAudioRecorder类是高级媒体录制类,并且包含与QMediaRecorder相同的功能。 audioRecorder = new QAudioRecorder; QAudioEncoderSettings audioSettings; audioSettings.setCodec("audio/amr"); audioSettings.setQuality(QMultimedia::HighQuality); audioRecor_qaudiorecorder

随便推点

[android源码分析]enable_native中的hci dev注册和up-程序员宅基地

文章浏览阅读193次。2、enableNative的分析 enable Native是真正的蓝牙使能的函数,蓝牙打开的一系列操作都是通过他来真正实现的。可以认为,这个函数蓝牙使能的主干,其余几个方面都可以认为是旁枝末节而已,因此,无论如何,我们必须了解到这个函数真正的精髓所在。 先来看jni层究竟是如何实现这个函数的:static jint enableNative(JNIEnv *env, jobje..._hci_dev没有voice_setting 成员

canal-使用-总结_canal使用心得-程序员宅基地

文章浏览阅读200次。typora-root-url: imagesAdminGuideCanal-Admin-Guide简介canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。工作原理canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal._canal使用心得

JMX 连接 Docker 内 java 应用注意事项_-dcom.sun.management.jmxremote=true-程序员宅基地

文章浏览阅读295次。由于近期可能需要观察程序运行的内存以及线程等指标,需要监控在Docker中java应用,在尝试与之前的方式添加后,并不能生效,因此在这里记录,和大家分享。需要添加JMX配置如下-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local_-dcom.sun.management.jmxremote=true

jacob合并几个word文件到一个word文件-程序员宅基地

文章浏览阅读466次。 因项目需要将几个word文件合并到一个word文件,后面附项目运用的jar包jacob-1.9jacob运用中,需要将附件内的jacob.dll放到windows/system32下 直接上代码:public static void main(String[] args) { List list = new ArrayList(); String f..._jacob insertfile hebing

iOS-MJRefresh框架-程序员宅基地

文章浏览阅读75次。1.用MJRefresh框架实现上下拉刷新1.1如何使用这个框架,只需要告诉控件的scrollView是谁,就能将框架添加到我们的滚动视图中了//下拉刷新MJRefreshHeaderView*header = [MJRefreshHeaderViewheader];header.scrollView=self.tableView;...

误删除SSIS中的“Maintenance Plans”文件夹的恢复-程序员宅基地

文章浏览阅读115次。在SQL Server 2005/2008中,维护计划的功能通过SSIS包来完成。如果不小心在SSIS管理中删除了"Maintenance Plans"文件夹,则在SQL Server中建立维护计划的时候会出现问题,如下图所示。 在Management studio中创建误删除的"Maintenance Plans"文件夹,如下图所示。 建立好"Maintenance Plan..._package=maintenance plans 路径