【10分钟系列(进阶版)】手把手带你封装一个可删除可放大预览的上传头像组件

563 阅读12分钟

组件说明、预览、功能

在Element UI的上传组件基础上,根据业务特点进行二次封装;使用腾讯云COS作为图片服务器的平替

  1. 添加本地图片,并上传到图片服务器
  2. 本地预览上传到图片服务器的图片,并可以放大预览
  3. 删除本地预览的图片
  4. 自定义限制图片的格式和大小
  5. 预览旧头像

11.png

12.png

13.png

一、新建组件

使用“照片墙”模式的el-upload,并设定limit的属性值为1

原因:利用该组件自带的预览放大图片和删除图片的功能;虽然是照片墙意味着要上传很多图片,但是可以给< el-upload >设定limit属性,限制上传数量为1,这样既限制了上传数量,又可以使用照片墙模式的预览放大图片和删除图片的功能

<template>
  <div>
    <el-upload
      action="#"
      list-type="picture-card"
      :limit="1"
      :file-list="fileListArr"
      :on-preview="avatarPreview"
      :on-remove="removeHandler"
      :class="{disabled:uploadedOnePic}"
      :on-change="fileStateChangeHandler"
      :before-upload="beforeUploadHandler"
      :http-request="upload"
    >
      <!-- 虚线框中间的加号 -->
      <i class="el-icon-plus" />
    </el-upload>

    <!-- 放大图片查看时的对话框 -->
    <el-dialog :visible="showDialog" @close="closeDialog">
      <img width="100%" :src="avatarUrl" alt="">
    </el-dialog>

    <!-- el-progress 进度条, 与'选择图片的虚线框'同宽 -->
    <el-progress v-if="showProgress" :percentage="percent" style="width:148px" />
  </div>
</template>

属性说明:

  1. action 属性:必须项,默认上传接口
  2. list-type 属性 :文件列表的类型(字符串),可以是:text / picture / picture-card
  3. limit 属性:控制上传文件的数量
  4. file-list 属性:它是已经上传完成的文件的列表,属性值是一个数组,成员是对象。象必须有url属性,即完成上传的图片的在线地址;还可以添加自定义属性,例如文件名name等。完成上传的图片,如何在页面中显示? 将fileListArr数组绑定给 file-list 属性
  5. on-preview 属性:点击“ 放大镜 ”时能触发它的回调函数
  6. on-remove 属性:点击“ 垃圾桶图标 ”时能触发它的回调函数
  7. class 属性:vue的动态类名
  8. on-change 属性:添加文件、上传成功和上传失败时都会触发它的回调函数
  9. before-upload 属性:上传操作执行前,回调被自动触发
  10. http-request 属性:属性值是函数,函数的参数是事件对象e,事件对象e有很对属性, 包括原生JS的File文件对象属性

二、配置el-upload组件的核心属性

file-list 属性

  • 它是已经上传完成的文件的列表,属性值是数组fileListArr,成员是对象,对象必须有url属性,即上传完成的图片的在线地址
  • 将fileListArr数组绑定给 file-list 属性,已经上传完成的图片就能在页面中显示
  • fileListArr数组中先放入一个图片在线地址,方便调试。此时,左侧删除完成图片 和 右侧'选择图片的虚线框'都显示在页面上
data() {
    return {
      /*  已经完成上传的文件的列表, 是一个数组
      例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] */

      // fileListArr: [{ url: 'https://tse2-mm.cn.bing.net/th/id/OIP-C.myc8QUm7MwMHeC2FLR4snAAAAA?pid=ImgDet&rs=1' }],
      fileListArr: [],

      // 对话框中的<img>元素的src属性值
      avatarUrl: '',

      showDialog: false,

      // 当前选择的确定上传的图片的uid,即图片的唯一标识
      currentFileUid: null,

      showProgress: false,

      // 上传进度条的百分比,属性值来自于COS
      percent: 0
    }
  }

三、点击放大镜图标,放大查看图片

使用的钩子:on-preview

实现方式:将当前图片的在线地址赋值给对话框中的图片,再显示对话框

注意:没有预览本地图片的功能,预览的只是上传到图片服务器后,拿到的图片在线地址,点击“保存更新”后,才将在线地址存到我们自己的服务器;或者预览后端传来的旧头像

/* 函数来源:on-preview 属性的回调,组件的钩子函数 */
    /* 函数作用:放大预览已经完成上传的头像 */
    /* 触发方式:点击“ 放大镜 ”时触发该函数 */
    avatarPreview(file) {
      // console.log(file)
      /*
      打印 file 参数, 它是一个对象,有三个属性:status uid url
        status 属性:上传的完成的状态;
        url 属性:图片完成上传后的在线地址, 属性值来自 fileListArr 数组中的url属性;
        uid 属性:文件的唯一标识
      */

      // 1. 将file.url(图片的在线地址)存入变量avatarUrl,作为对话框中<img>标签的src属性值,实现大图预览
      this.avatarUrl = file.url
      // 2. 显示对话框
      this.showDialog = true
    },

    closeDialog() {
      this.showDialog = false
    },

四、控制'选择图片的虚线框'的显示或隐藏

实现方式:'选择图片的虚线框'就是有加号的大上传按钮,通过判断数组fileListArr的长度是否为1,再结合计算属性计算出布尔值,最后使用vue的动态类名实现这个盒子的显示或隐藏

// 计算属性控制'选择图片的虚线框'的显示或隐藏
  computed: {
    /*  判断 已经完成上传的文件的列表fileListArr数组中是否存在图片

      1. 判断的目的:如果已经上传,则不应该显示'选择图片的虚线框', 表示不允许继续上传;如果没有上传,则显示'选择图片的虚线框',表示允许上传

      2. 如何显示'选择图片的虚线框'?
        2.1 打开调试面板,观察发现'选择图片的虚线框'的类名是'el-upload--picture-card',在该类名下添加display:none 可隐藏盒子

        2.2 但是,该盒子是编译出来的,在<template>中看不到。怎么办?给<el-upload>添加类名disabled,通过叠加类选择器的形式,可以给'el-upload--picture-card'盒子添加属性' display:none '。注意,必须使用全局的CSS,<style>上不能加scoped

        2.3 所以, 根据计算属性uploadedOnePic,给<el-upload>动态添加或移除类名disabled,disabled类名控制 '选择图片的虚线框' 的显示或隐藏

        2.4 vue中,给元素动态添加/移除类名的语法: v-on:class="{ 类名 : 布尔值 }"
    */
    uploadedOnePic() {
      if (this.fileListArr.length === 1) {
        return true
      } else {
        return false
      }
    }
  },
<style>
/* //! 错误点/BUG/注意:不能加scoped,否则样式失效。因为编译后,要保证该样式在全局有效 */
.disabled .el-upload--picture-card {
  display: none;
}
</style>

五、点击垃圾桶图标,删除当前图片

使用的钩子:on-remove 实现方式:清空数组fileListArr

注意:

  1. 删除当前图片后,'选择图片的虚线框'自动显示
  2. on-remove的钩子函数只是删除了页面上能看到的上传成功的图片(前端视图层与数据层), 后端数据库中的图片并未删除(后端数据库层)。如果需要删除数据库,必须在该函数中调用删除用户头像的接口
/* 函数来源:on-remove 属性的回调,组件的钩子函数 */
    /* 函数作用:删除当前页面正显示的上传成功的图片,同时让'选择图片的虚线框'显示 */
    /* 触发方式:点击“ 垃圾桶图标 ”时触发 */
    //! 注意:该函数只是删除了页面上能看到的上传成功的图片(前端视图层与数据层), 后端数据库中的图片并未删除(后端数据库层)。如果需要删除数据库,必须在该函数中调用删除用户头像的接口
    /* 实现原理:
      1. 点击“ 垃圾桶图标 ”时,<el-upload>会自动移除页面上的图片,但这只是视图层的变化,数据层也要同步。所以将回调的fileList参数(空数组)赋值给fileListArr数组,清空该数组
      2.因为fileListArr数组为空,所以计算属性uploadedOnePic能让'选择图片的虚线框'显示,它显示后,用户,可上传其他图片,或不再上传(如果调用了删除后端头像结果的话,相当于删除了当前的头像)
     */
    removeHandler(file, fileList) {
      // 打印查看
      // console.log(file) // 打印 file 参数, 它是一个对象,有三个属性:status uid url
      // console.log(fileList) // 打印 fileList 参数,它是一个删除当前文件后的空数组
      this.fileListArr = fileList
    },

六、删除当前图片后,可添加其他图片

使用的钩子:on-change

实现方式:清空数组fileListArr

/* 函数来源:on-change 属性的回调,组件的钩子函数 */
    /* 函数作用:删除当前头像,再点击'选择图片的虚线框'的加号选择图片后,显示已经完成上传的图片,同时让'选择图片的虚线框'隐藏 */
    /* 触发方式:添加文件、上传成功和上传失败时都会触发。因此添加图片时,不能向fileListArr数组push成员,因为会被重复添加多次*/
    fileStateChangeHandler(file, fileList) {
      // 打印 file 参数, 它是一个对象,有多个属性(name percentage raw size uid url等),但它可不是原生JS的文件对象File
      // console.log(file)
      // 打印 fileList 参数, 它是一个数组,成员就是上一行的对象
      // console.log(fileList)
      // 将fileList数组赋值给fileListArr数组,就能显示已经完成上传的图片
      this.fileListArr = fileList
    },

七、校验上传图片的格式和大小

使用的钩子:before-upload

实现方式:准备图片格式的数组,some方法遍历,不符合时返回false,终止上传操作;准备图片大小,if判断,不符合时返回false,终止上传操作;

/* 函数来源:before-upload 属性的回调,组件的钩子函数 */
    /* 函数作用:上传前校验图片的文件类型和大小 */
    /* 触发方式:在上传这一操作发生前 */
    /*  实现原理:
        1. beforeUploadHandler函数的file参数为原生JS的文件对象File,有很多属性,例如文件大小size、文件类型type、文件的唯一标识uid
        2. 如果beforeUploadHandler函数 返回 false ,则停止上传
        3. 如果beforeUploadHandler函数 返回 true ,则可以上传
    */
    beforeUploadHandler(file) {
      console.log(file)
      /* 1. 图片格式校验 */
      // 允许的上传的图片格式
      const types = ['image/jpeg', 'image/gif', 'image/bmp', 'image/png']
      // 根据根据当前选择文件的类型,在允许的图片格式中查找,如果找到说明格式支持;如果找不到说明格式不支持
      const isAllow = types.some((item) => {
        return item === file.type
      })
      if (isAllow === false) {
        this.$message.error('图片格式只支持jpeg gif bmp png')
        return false // 函数返回false,终止上传操作
      }

      /* 2. 图片大小校验 */
      // 限制为3M
      const maxFileSize = 3 * 1024 * 1024
      if (file.size > maxFileSize) {
        this.$message.error('图片大小不能超过3M')
        return false // 函数返回false,终止上传操作
      }

      /* 3. 校验全部通过 */
      // 提取当前文件的标识,存入变量currentFileUid,表示当前确定的要上传的图片就是此uid的图片
      this.currentFileUid = file.uid
      // 如果校验全部通过,一定要返回true,上传操作才能进行
      return true
    },

八、上传到腾讯云COS

  • 利用http-request 属性,自定义上传的实现
  • 配置el-progress,显示和隐藏上传进度条
  • 将腾讯云COS响应回的在线地址保存到数组fileListArr中,实现图片的回显,然后将在线地址传给父组件
  • 最后,在父组件中发请求,将在线地址存到我们的服务器中
/* 函数来源:http-request 属性的回调, 自定义上传的实现 */
    /* 函数作用:将用户选择的图片上传到腾讯云COS,并将响应回的图片在线地址存入fileListArr数组 */
    /* 触发方式:上传条件具备时,自动触发 */
    upload(e) {
      // console.log(e)
      // 如果文件对象不存在,退出函数
      if (e.file === false) return

      this.showProgress = true // 显示上传进度条

      // 在腾讯云控制台一顿配置后,再安装腾讯云COS的SDK依赖: npm i cos-js-sdk-v5 --save
      // 使用node的方式引入
      var COS = require('cos-js-sdk-v5')

      // new一个实例对象
      var cos = new COS({
        SecretId: 'AKIDhEUUDfspVv60dz7yZuxkGVTBcmfIekZ8',
        SecretKey: 'MSHp78sLXS6TZOxFG2XliPpxoOimY4KJ'
      })

      // 调用cos实例的 putObject( {配置对象},上传的回调 ) 方法,实现文件的上传
      cos.putObject({
        Bucket: 'yoyo-1309207666', /* 存储桶名称 */
        Region: 'ap-nanjing', /* 存储桶所在地域,必须字段 */
        Key: e.file.name, /* 上传文件的名字 */
        StorageClass: 'STANDARD',
        Body: e.file, // 上传的文件对象File
        // onProgress 表示上传的进度信息,在上传时,cos支持配置onProgress回调,在这个回调函数中可以拿到当前上传的进度,然后配合el-progress在页面显示上传进度。
        // {"loaded":0,"total":3080,"speed":0,"percent":0}

        onProgress: (progressData) => {
          // 防坑指南:匿名函数统一换成箭头函数,否则“ this.percent ”无法指向data()函数中的percent变量,导致进度条不变化
          // console.log(JSON.stringify(progressData)) // 官网的打印项
          // 转换为整数, el-progress 要求percent的属性值为整数
          this.percent = progressData.percent * 100
        }
      }, (err, data) => {
        // 防坑指南:匿名函数统一换成箭头函数
        this.showProgress = false // 隐藏上传进度条
        this.percent = 0 // 重置进度条的百分比
        console.log(err || data)
        if (data.statusCode === 200) {
        // 上传成功就会得到data对象,访问data对象的Location属性就能拿到图片的非完整在线地址(没有协议名),手动拼接协议名后,得到完整可用的在线地址
          const completeUrl = 'https://' + data.Location
          // 注意:页面显示完成上传的图片的依赖数据是fileListArr数组,数组中能存多个包含url属性的对象,所以不能将图片在线地址简单的push到数组中,也不能将地址直接赋值给fileListArr[0],这样不利于以后上传多张图片。
          // 应该map遍历fileListArr数组,
          this.fileListArr = this.fileListArr.map((item) => {
            if (item.uid === this.currentFileUid) {
              // 如果fileListArr数组的成员中,有uid与当前上传图片的uid相同的(说明此时上传成功的正是刚刚选择的那张图片,而不是别的图片),就将该成员替换为有完整url的新成员,这样才能让完成上传的图片在页面显示
              /*
                同时,要追加一个uploaded属性,这是一个标识,表示这张图片已经完成上传。true表示完成上传,false表示没有完成上传。
                在后期,根据uploaded的属性值来决定是否可以保存用户信息,因为如果没有上传成功不允许保存保存用户信息
              */
              return { url: completeUrl, uploaded: true }
            } else {
              // 如果没有,则不做任何事情
              return item
            }
          })
          /* 将 fileListArr数组 传给父组件。因为保存更新的按钮在父组件中,只有保存更新了,在线图片地址才会存到我们的服务器中*/
          this.$emit('urlFromSon', this.fileListArr)
        }
      })
    }