vue 压缩图片+上传多张图

421 阅读2分钟

功能:本组件支持同时上传多张图(最多4张图片),并且对每张图都压缩之后上传,也支持图片回显。

基本思路:

1、获取上传 Input 中的图片对象 File

2、将图片转换成 base64 格式

3、base64 编码的图片通过 Canvas 转换压缩,这里会用到的 Canvas 的 drawImage 以及 toDataURL 这两个 Api,一个调节图片的分辨率的,一个是调节图片压缩质量并且输出的

4、转换后的图片生成对应的新图片,然后输出

注意:

图片尺寸超过大小的限制要进行按比例缩小

ios上画布绘制图片时会被旋转,要将其旋转回来

选择图片,Input 中的图片对象 File 图片要小于20M

    /**
     * 选择图片
     */
    selectImgs (e) {
      var files = e.target.files
      // 取消选择文件,不做任何操作
      if (files.length === 0) {
        return
      }
      // 如果将要上传的图片加上已上传的图片的个数大于4,进行提示
      if (this.localPicArr.length + files.length > 4) {
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_pic_num_limit')
        })
        return
      }
      // 如果上传的图片列表里有不是图片的,进行提示
      const notAllImage = Object.keys(files).some((file) => files[file].type.indexOf("image") !== 0)
      if (notAllImage) {
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_pic_limit')
        })
        return
      }
      // 如果上传的图片列表里有大于20MB的,进行提示
      const isTooLarge = Object.keys(files).some((file) => files[file].size > 1024 * 1024 * 20)
      if (isTooLarge) {
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_pic_size_limit')
        })
        return
      }
      // 如果上传的文件全部符合条件,开始加载图片
      Object.keys(files).some((file, index) => this.loadImages(files[file], index))
    },
    
    

开始读取文件,获取照片方向角属性,如果被旋转了就转回来

    /**
     * 加载图片
     */
    loadImages (file, index) {
      let orientation = null
      //获取照片方向角属性,用户旋转控制
          //获取照片方向角属性,用户旋转控制
      // Orientation:照片拍攝角度
      // DateTime:照片拍攝時間
      // ImageWidth:照片寬度
      // ImageHeight:照片高度
      // 获取图像的数据
      EXIF.getData(file, function() {
      // 获取图像的全部数据,值以对象的方式返回
        EXIF.getAllTags(this)
          // 获取图像的某个数据
        orientation = EXIF.getTag(this, 'Orientation')
      })
      var that = this
      var reader = null
      var picIndex = this.localPicArr.length + index
      reader = new window.FileReader()
      reader.readAsDataURL(file)
      // 开始读取文件,打开loading
      reader.onloadstart = function () {
        that.localPicArr.push('loading')
      }
      // 读取文件结束,无论成功还是失败,关闭loading
      reader.onloadend  = function () {
        // 清空input
        that.$refs.input.value = ''
      }
      // 读取文件失败
      reader.onerror = function () {
        // 删除掉读取失败的图片
        that.localPicArr.splice(picIndex, 1)
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_error')
        })
      }
      // 读取成功
      reader.onload = function (e) {
        that.tempImageArr[picIndex] = new Image()
        that.tempImageArr[picIndex].onload = function () {
          setTimeout(() => that.compressImages(file, that.tempImageArr[picIndex].width, that.tempImageArr[picIndex].height, picIndex, orientation), 1000)
        }
        that.tempImageArr[picIndex].src= e.target.result
      }
    },

读取文件成功加载,压缩图片之后绘制图片, 上传

    /**
     * 压缩图片
     */
    compressImages (file, originWidth, originHeight, index, orientation) {
      var that = this
      // 最大尺寸限制
      var maxWidth = 800, maxHeight = 800
      // 目标尺寸
      var targetWidth = originWidth, targetHeight = originHeight
      // 图片尺寸超过大小的限制
      if (originWidth > maxWidth || originHeight > maxHeight) {
        if (originWidth / originHeight > maxWidth / maxHeight) {
          // 更宽,按照宽度限定尺寸
          targetWidth = maxWidth
          targetHeight = Math.round(maxWidth * (originHeight / originWidth))
        } else {
          targetHeight = maxHeight
          targetWidth = Math.round(maxHeight * (originWidth / originHeight))
        }
      }
      // 实例化
      this.cavasArr[index] = document.getElementById(`cavas${index}`)
      this.contextArr[index] = this.cavasArr[index].getContext('2d')
      // canvas对图片进行缩放
      this.cavasArr[index].width = targetWidth
      this.cavasArr[index].height = targetHeight
      // 清除画布
      this.contextArr[index].clearRect(0, 0, targetWidth, targetHeight)
      this.contextArr[index].drawImage(this.tempImageArr[index], 0, 0, targetWidth, targetHeight)
      // 如果上传的图片有被旋转,将其旋转回来,一般IOS手机上面才会出现这种情况
      if (orientation !== undefined && orientation != 1) {
        switch (orientation) {
          case 6:
            this.contextArr[index].rotate(Math.PI / 2)
            this.contextArr[index].drawImage(this.cavasArr[index], 0, -targetHeight, targetWidth, targetHeight)
            break
          case 3:
            this.contextArr[index].rotate(Math.PI)
            this.contextArr[index].drawImage(this.cavasArr[index], -targetWidth, -targetHeight, targetWidth, targetHeight)
          case 8:
            this.contextArr[index].rotate(3 * Math.PI / 2)
            this.contextArr[index].drawImage(this.cavasArr[index], -targetWidth, 0, targetWidth, targetHeight)
            break
        }
      }
      // canvas转为blob并上传                                                    
      this.cavasArr[index].toBlob(function (blob) {
        // 存储blob对象至数组
        that.blobArr.push(blob)
        // 使用压缩后的图片,防止缓存过大
        that.localPicArr.splice(index, 1, that.cavasArr[index].toDataURL())
        const params = {
          blobArr: that.blobArr,
          localPics: that.localPicArr
        }
        // 将数据传递到父组件
        that.$emit('setPic', params)
      }, file.type || 'image/png')
    }

组件全部代码:

<template>
  <div class="cmpress-container">
    <label>
      <input ref="input" id="file" multiple class="upload-input" type="file" name="image" accept="image/*" @change="selectImgs($event)">
      <img :class="{ oc: usingType === 'oc', bp: usingType === 'bp' }" src="../../assets/img/original_collection/register/upload_icon.png">
    </label>
    <!-- 显示图片 -->
    <div
      v-for="(item, index) in localPicArr"
      :key="index"
      :class="{ oc: usingType === 'oc', bp: usingType === 'bp' }"
      class="pic-view"
    >
      <div v-if="item === 'loading'" class="loading"><div class="xz"></div></div>
      <div v-else class="img-wrapper" :class="{ oc: usingType === 'oc', bp: usingType === 'bp' }">
        <img :src="item.img_url ? item.img_url : item" class="pic-item">
        <img @click="delPic(index)" src="../../assets/img/original_collection/register/del_icon.png" class="del-icon">
      </div>
    </div>
    <!-- 绘制图片 隐藏 canvas作用:drawImage 绘制图片之后 利用toDataURL转为base64编码 也转成blob传至后端  使用formData请求-->
    <canvas
      v-for="(item, index) in cavasArr"
      :key="index + 100"
      :id="'cavas' + index"
      class="cp-cavas"
      ></canvas>
  </div>
</template>

<script>
import EXIF from 'exif-js'

export default {
  props: {
    usingType: {
      type: String,
      default: 'oc'
    },
    imgDatas: {
      type: Array,
      default: []
    }
  },
  data () {
    return {
      // canvas实例
      cavasArr: [undefined, undefined, undefined, undefined],
      // canvas上下文
      contextArr: [undefined, undefined, undefined, undefined],
      // 过度图片对象,在mounted方法里实例化
      tempImageArr:  [undefined, undefined, undefined, undefined],
      // 存储blob对象的数组,长度最大为4
      blobArr: [],
      // 存储本地图片的数组,长度最大为4
      localPicArr: []
    }
  },
  methods: {
    /**
     * 数据回显时,设置图片数据
     */
    setLocalImages () {
      this.localPicArr = this.imgDatas.slice(0)
      this.blobArr = this.imgDatas.slice(0)
    },
    /**
     * 删除图片
     */
    delPic (index) {
      this.localPicArr.splice(index, 1)
      this.blobArr.splice(index, 1)
      this.tempImageArr.splice(index, 1, undefined)
      const params = {
        blobArr: this.blobArr,
        localPics: this.localPicArr
      }
      // 将数据传递到父组件
      this.$emit('setPic', params)
    },
    /**
     * 选择图片
     */
    selectImgs (e) {
      var files = e.target.files
      // 取消选择文件,不做任何操作
      if (files.length === 0) {
        return
      }
      // 如果将要上传的图片加上已上传的图片的个数大于4,进行提示
      if (this.localPicArr.length + files.length > 4) {
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_pic_num_limit')
        })
        return
      }
      // 如果上传的图片列表里有不是图片的,进行提示
      const notAllImage = Object.keys(files).some((file) => files[file].type.indexOf("image") !== 0)
      if (notAllImage) {
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_pic_limit')
        })
        return
      }
      // 如果上传的图片列表里有大于20MB的,进行提示
      const isTooLarge = Object.keys(files).some((file) => files[file].size > 1024 * 1024 * 20)
      if (isTooLarge) {
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_pic_size_limit')
        })
        return
      }
      // 如果上传的文件全部符合条件,开始加载图片
      Object.keys(files).some((file, index) => this.loadImages(files[file], index))
    },
    /**
     * 加载图片
     */
    loadImages (file, index) {
      let orientation = null
      //获取照片方向角属性,用户旋转控制
      EXIF.getData(file, function() {
        EXIF.getAllTags(this)
        orientation = EXIF.getTag(this, 'Orientation')
      })
      var that = this
      var reader = null
      var picIndex = this.localPicArr.length + index
      reader = new window.FileReader()
      reader.readAsDataURL(file)
      // 开始读取文件,打开loading
      reader.onloadstart = function () {
        that.localPicArr.push('loading')
      }
      // 读取文件结束,无论成功还是失败,关闭loading
      reader.onloadend  = function () {
        // 清空input
        that.$refs.input.value = ''
      }
      // 读取文件失败
      reader.onerror = function () {
        // 删除掉读取失败的图片
        that.localPicArr.splice(picIndex, 1)
        this.$store.dispatch('set_popup_datas', {
          type: 'h_tips',
          msg: this.$t('message.upload_error')
        })
      }
      // 读取成功
      reader.onload = function (e) {
        that.tempImageArr[picIndex] = new Image()
        that.tempImageArr[picIndex].onload = function () {
          setTimeout(() => that.compressImages(file, that.tempImageArr[picIndex].width, that.tempImageArr[picIndex].height, picIndex, orientation), 1000)
        }
        that.tempImageArr[picIndex].src= e.target.result
      }
    },
    /**
     * 压缩图片
     */
    compressImages (file, originWidth, originHeight, index, orientation) {
      var that = this
      // 最大尺寸限制
      var maxWidth = 800, maxHeight = 800
      // 目标尺寸
      var targetWidth = originWidth, targetHeight = originHeight
      // 图片尺寸超过大小的限制
      if (originWidth > maxWidth || originHeight > maxHeight) {
        if (originWidth / originHeight > maxWidth / maxHeight) {
          // 更宽,按照宽度限定尺寸
          targetWidth = maxWidth
          targetHeight = Math.round(maxWidth * (originHeight / originWidth))
        } else {
          targetHeight = maxHeight
          targetWidth = Math.round(maxHeight * (originWidth / originHeight))
        }
      }
      // 实例化
      this.cavasArr[index] = document.getElementById(`cavas${index}`)
      this.contextArr[index] = this.cavasArr[index].getContext('2d')
      // canvas对图片进行缩放
      this.cavasArr[index].width = targetWidth
      this.cavasArr[index].height = targetHeight
      // 清除画布
      this.contextArr[index].clearRect(0, 0, targetWidth, targetHeight)
      this.contextArr[index].drawImage(this.tempImageArr[index], 0, 0, targetWidth, targetHeight)
      // 如果上传的图片有被旋转,将其旋转回来,一般IOS手机上面才会出现这种情况
      if (orientation !== undefined && orientation != 1) {
        switch (orientation) {
          case 6:
            this.contextArr[index].rotate(Math.PI / 2)
            this.contextArr[index].drawImage(this.cavasArr[index], 0, -targetHeight, targetWidth, targetHeight)
            break
          case 3:
            this.contextArr[index].rotate(Math.PI)
            this.contextArr[index].drawImage(this.cavasArr[index], -targetWidth, -targetHeight, targetWidth, targetHeight)
          case 8:
            this.contextArr[index].rotate(3 * Math.PI / 2)
            this.contextArr[index].drawImage(this.cavasArr[index], -targetWidth, 0, targetWidth, targetHeight)
            break
        }
      }
      // canvas转为blob并上传                                                    
      this.cavasArr[index].toBlob(function (blob) {
        // 存储blob对象至数组
        that.blobArr.push(blob)
        // 使用压缩后的图片,防止缓存过大
        that.localPicArr.splice(index, 1, that.cavasArr[index].toDataURL())
        const params = {
          blobArr: that.blobArr,
          localPics: that.localPicArr
        }
        // 将数据传递到父组件
        that.$emit('setPic', params)
      }, file.type || 'image/png')
    }
  }
}
</script>

<style lang="less" scoped>
@keyframes loading{
  0%{
    transform: rotate(0deg);
  }
  100%{
    transform: rotate(360deg);
  }
}
.cp-cavas {
  display: none;
}
.cmpress-container {
  width: 100%;
  height: 100%;
  font-size: .16rem;
  margin-top: .3rem;
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  .upload-input {
    display: none;
  }
  .oc {
    width: 1rem;
    height: 1rem;
  }
  .bp {
    width: 1.12rem;
    height: 1.12rem;
  }
  .pic-view {
    position: relative;
    &.oc {
      width: 1rem;
      height: 1rem;
      margin-left: .1rem;
      .del-icon {
        width: .22rem;
        height: .22rem;
      }
    }
    &.bp {
      width: 1.12rem;
      height: 1.12rem;
      margin-left: .2rem;
      .del-icon {
        width: .26rem;
        height: .26rem;
      }
    }
    .loading {
      width: 100%;
      height: 100%;
      border-radius: .1rem;
      background: rgba(0,0,0,.25);
      display: flex;
      justify-content: center;
      align-items: center;
      .xz {
        width: .6rem;
        height: .6rem;
        border: .02rem solid;
        border: .02rem solid;
        border-radius: 50%;
        border-color: #fff #fff transparent;
        animation: loading 1s linear infinite;
      }
    }
    .img-wrapper {
      overflow: hidden;
      display: flex;
      align-items: center;
      border-radius: .1rem;
      &.oc {
        width: 1rem;
        height: 1rem;
      }
      &.bp {
        width: 1.12rem;
        height: 1.12rem;
      }
      .pic-item {
        border-radius: .1rem;
        width: 100%;
        height: auto;
        position: relative;
      }
    }
    
    .del-icon {
      position: absolute;
      top: -.1rem;
      right: -.1rem;
    }
  }
  
}
</style>