vue +element-ui 实现的oss web直传文件

2021-07-12  本文已影响0人  好名字都让你们用了
简述: 后台系统基本的上传功能,oss 的web直传, 因oss 获取音视频时长需要收费,所以采用的前端本地获取上传音视频资源的时长,并配了上传成功音视频预览功能, oss 直传官网文档

1:oss上传流程,需要拿到后端返回的oss 签名信息

// 获取后端返回的签名信息
export function client(data) {
  //data后端提供数据
  return new OSS({
    region: data.region,
    accessKeyId: data.Credentials.AccessKeyId,
    accessKeySecret: data.Credentials.AccessKeySecret,
    stsToken: data.Credentials.SecurityToken,
    bucket: data.bucket
  })
}

2:主要上传组件ossUpload代码,用element 的自定义上传方法http-request ,

<template>
  <div class="ossUpload">
    <el-upload
      ref="upload"
      action
      :class="{ has: hideUploadBtn, hidden: ModifyStyle }"
      :http-request="Upload"
      :before-upload="beforeAvatarUpload"
      :on-preview="handlePreview"
      :before-remove="beforeRemove"
      :on-remove="handleRemove"
      :on-exceed="handleExceed"
      :on-change="uploadChange"
      :list-type="listType"
      :limit="limit"
      :file-list="fileList"
      :accept="accept"
      :on-error="uploadFail"
      :on-success="uploadSuccess"
      :multiple="multiple"
    >
      <template #trigger>
        <i v-if="listType == 'picture-card'" class="el-icon-plus"></i>
        <el-button v-else type="primary">{{ text }}</el-button>
      </template>
      <el-button
        v-if="(type == 'audio' || type == 'video') && value != ''"
        type="primary"
        class="preview"
        @click="preview"
        >预览</el-button
      >
    </el-upload>
    <!-- 上传进度条 -->
    <el-progress
      v-show="showProgress"
      :text-inside="false"
      :stroke-width="5"
      :percentage="progress"
    ></el-progress>
    <!-- 音频预览 -->
    <el-dialog v-model="dialogAudioVisible" width="560px">
      <div v-if="dialogAudioVisible" class="audio-con">
        <audio-node :node="audio_info" image-height="250px"></audio-node>
      </div>
    </el-dialog>
    <!-- 视频预览 -->
    <el-dialog v-model="dialogVideoVisible" width="800px">
      <div v-if="dialogVideoVisible" class="video-con">
        <video class="videoPrview" controls controlsList="nodownload" preload="load">
          <source :src="video_url" type="video/mp4" />
        </video>
      </div>
    </el-dialog>
    <!-- 视频获取时长 -->
    <video id="myVideo" class="video" controls preload="load" :src="getVideoInfo"> </video>
    <!-- 音频获取时长 -->
    <audio id="myAudio" class="audio" preload="load" :src="getAudioInfo"> </audio>
  </div>
</template>
<script>
  import {
    client,
    isArrayFn,
    formatTime,
    formatMin,
    randomString,
    loadPreviewPlugin,
    previewImage
  } from '@/utils'
  import AudioNode from '@/components/viewComponent/audio.vue'
  import { createAjax } from '@/utils/ajax'
  import { deleteParame } from '@/utils'
  // import SparkMD5 from 'spark-md5'
  const Ajax = createAjax('common', 'v1', 'restapi')
  export default {
    name: 'Upload',
    components: {
      AudioNode
    },
    props: {
      limit: {
        type: Number,
        default: 1
      },
      text: {
        type: String,
        default: '选择图片'
      },
      listType: {
        type: String,
        default: 'picture-card'
      },
      // 上传文件大小
      measure: {
        type: String,
        default: '500'
      },
      type: {
        type: String,
        default: 'image'
      },
      accept: {
        type: String,
        default: 'image/*'
      },
      value: {
        type: [String, Object],
        default: () => {}
      },
      oss: {
        type: String,
        default: 'oss'
      },
      address: {
        type: String,
        default: 'admin/images'
      },
      // 音视频时长
      duration: {
        type: [String, Number],
        default: () => {}
      },
      name: {
        type: [String, Number, Object],
        default: () => ({})
      },
      unClick: {
        type: Boolean,
        default: false
      },
      // 上传是否为歌词
      isLyc: {
        type: Boolean,
        default: false
      },
      // 业务名
      business: {
        type: String,
        default: 'lesson'
      },
      // 图片尺寸
      imageSize: {
        type: [String, Object],
        default: () => {}
      },
      modality: {
        type: String,
        default: 'form'
      },
      isShow: {
        type: Boolean,
        default: false
      },
      multiple: {
        type: Boolean,
        default: false
      },
      check: {
        type: Boolean,
        default: false
      },
      imageShow: {
        type: Boolean,
        default: true
      }
    },
    emits: [
      'update:value',
      'update:size',
      'update:duration',
      'update:name',
      'removeChange',
      'uploadChange',
      'update:num'
    ],
    data() {
      return {
        // reviewShow: this.isShow,
        urlClone: this.value,
        videoName: this.name,
        fileList: [], //文件列
        showProgress: false, //进度条的显示
        dataObj: {}, //存签名信息
        progress: 0, //进度条数据
        id: this.$route.params.id,
        code: 0,
        message: '',
        fileCurrentLen: 0,
        getVideoInfo: '',
        getAudioInfo: '',
        dialogAudioVisible: false,
        dialogVideoVisible: false,
        uploadInfo: {},
        audio_info: {},
        err: '',
        isDelete: false,
        video_url: '',
        isSuccess: false
      }
    },
    computed: {
      isShowHasClass() {
        // 多张不去掉上传按钮
        if (this.multiple) return false
        // 单张判断是否已生成url
        if (!this.value) return false
        return this.value.length || this.urlClone.length
      },
      hideUploadBtn() {
        return this.fileCurrentLen >= this.limit && this.type === 'image' && this.imageShow
      },
      // 控制表格  表单 上传样式
      ModifyStyle() {
        return this.type === 'image' && this.modality == 'table'
      }
    },
    watch: {
      value(nVal) {
        // 监听父组件清空url的时候清空文件
        if (nVal) return
        this.clearFiles()
      }
    },
    beforeMount() {
      loadPreviewPlugin()
      this.initFieldList()
    },

    methods: {
      preview() {
        this.type == 'video' ? (this.dialogVideoVisible = true) : (this.dialogAudioVisible = true)
      },
      getImageList(fileList) {
        return fileList.map((item) => {
          if (item.status != 'success') return ''
          if (item.response) {
            // 新上传的图
            return deleteParame(item.response)
          } else {
            // 以前上传的图
            return deleteParame(item.url)
          }
        })
      },
      // 清空已上传的文件列表
      clearFiles() {
        this.$refs.upload.clearFiles()
      },
      // 文件超出个数限制时的钩子
      handleExceed(files, fileList) {
        this.$message.warning(`每次只能上传 ${this.limit} 个文件`)
      },
      // 点击文件列表中已上传的文件时的钩子
      handlePreview(file) {
        // 图片预览
        if (this.unClick || this.type != 'image') return
        previewImage(file.path || file.url)
        return false
      },
      uploadChange(file, fileList) {
        this.fileCurrentLen = fileList.length
        this.$emit('uploadChange', file)
      },
      uploadFail(err) {
        this.$message.error(`${JSON.parse(err.message).message},请重新上传`)
      },

      // 删除文件之前的钩子
      beforeRemove(file, fileList) {
        this.fileCurrentLen = fileList.length
        // this.showProgress = false
        const that = this
        if (
          (!this.isDelete && this.type == 'audio') ||
          this.type == 'video' ||
          this.type == 'text'
        ) {
          async function abortMultipartUpload() {
            const name = that.uploadInfo.name // Object所在Bucket的完整路径。
            const uploadId = that.uploadInfo.uploadId // 分片上传uploadId。
            client(that.dataObj)
              .abortMultipartUpload(name, uploadId)
              .then((result) => {
                this.progress = 0
                this.showProgress = false
              })
          }

          abortMultipartUpload()
        }
      },
      // 文件列表移除文件时的钩子
      handleRemove(file, fileList) {
        this.progress = 0
        this.fileCurrentLen = fileList.length
        let imagePathList = []
        fileList.map((item, index) => {
          if (item.uid == file.uid) {
            fileList.splice(index, 1)
          }
        })
        imagePathList = this.getImageList(fileList)

        // 空数组 将imagePathList 转为为字符串
        if (imagePathList.length == 0) {
          imagePathList = ''
        } else if (imagePathList.length == 1) {
          imagePathList = imagePathList[0]
        }

        this.urlClone = []

        this.$emit('update:value', imagePathList)
        // 嵌套多层的情况手动更新数据
        this.$emit('removeChange', imagePathList)
      },
      // 上传成功
      uploadSuccess(response, file, fileList) {
        if (this.type == 'image' && this.isSuccess) {
          // 图片类
          let imageList = this.getImageList(fileList)
          let imagePath
          if (this.limit == 1) {
            // 单张直接返回图片地址
            imagePath = imageList[0]
          } else {
            // 多张返回图片地址列表
            imagePath = imageList
          }
          this.$emit('update:value', imagePath)
        }
      },
      //文件上传前的校验
      beforeAvatarUpload(file) {
        // 获取文件的md5 ,
        // var fileReader = new FileReader()
        // var spark = new SparkMD5() // 创建md5对象(基于SparkMD5)
        // fileReader.readAsBinaryString(file) // file 对应上传的文件

        // // 文件读取完毕之后的处理
        // fileReader.onload = (e) => {
        //   console.log('获取文件的md5')
        //   spark.appendBinary(e.target.result)
        //   const md5 = spark.end()
        //   console.log(md5)
        // }
        const that = this
        // 校验文件类型
        if (this.type && !file.type.startsWith(this.type) && !this.isLyc) {
          this.$message.error('格式不正确,请重新上传!')
          return false
        }
        if (this.type == 'audio') {
          this.showProgress = true
          //音频类型限制上传大小
          if (Number((file.size / 1024 / 1024).toFixed(3)) > this.measure) {
            this.$message.error(`音频不能超过${this.measure}M`)
            return false
          }
          this.getAudioInfo = URL.createObjectURL(file)
          document.getElementById('myAudio').addEventListener('canplaythrough', function (e) {
            const audioTime = formatMin(e.target.duration)
            that.$emit('update:duration', audioTime)
          })
        }
        if (this.type == 'image') {
          if (Number((file.size / 1024 / 1024).toFixed(3)) > this.measure) {
            that.$message.error(`${file.name}大小不对,请重新上传!`)
            return false
          }
        }
        // 上传本地获取视频时长
        if (this.type == 'video') {
          this.showProgress = true
          // 视频类限制上传大小
          if (Number((file.size / 1024 / 1024).toFixed(3)) > this.measure) {
            this.$message.error(`视频不能超过${this.measure}M`)
            return false
          }
          this.getVideoInfo = URL.createObjectURL(file)
          document.getElementById('myVideo').addEventListener('canplaythrough', function (e) {
            const time = formatTime(e.target.duration)
            that.$emit('update:duration', time)
          })
        }
        // 歌词 格式校验

        if (this.isLyc) {
          this.showProgress = true
          if (file.name.split('.')[1].toLowerCase() != 'lrc') {
            this.$message.error('请上传格式正确的歌词文件')
            return false
          }
        }
        if (this.code) {
          this.$message.error(this.message)
          return false
        }
        if (!that.imageSize) {
          return true
        }
        return new Promise(function (resolve, reject) {
          let reader = new FileReader()
          let size = JSON.parse(that.imageSize)
          reader.readAsDataURL(file)
          reader.onload = function (theFile) {
            let image = new Image()
            image.src = theFile.target.result
            image.onload = function () {
              if (size.width && size.height) {
                const noSizeLimit = !this.height || !this.width
                const rightSize = size.width == this.width && size.height == this.height
                if (noSizeLimit || rightSize) {
                  file.width = size.width
                  file.height = size.height
                  resolve(file)
                  return
                } else {
                  that.$message.error(`${file.name}尺寸不对,请重新上传!`)
                  reject('图片尺寸不对')
                  return
                }
              }
              if (!size.width || !size.height) {
                if (size.width) {
                  if (size.width == this.width) {
                    file.width = size.width
                    resolve(file)
                  } else {
                    that.$message.error(`${file.name}尺寸不对,请重新上传!`)
                    reject('图片尺寸不对')
                  }
                } else {
                  if (size.height == this.height) {
                    file.height = size.height
                    resolve(file)
                  } else {
                    that.$message.error(`${file.name}尺寸不对,请重新上传!`)
                    reject('图片尺寸不对')
                  }
                }
              }
            }
          }
        })
      },
      // http-request属性来覆盖默认的上传行为,自定义上传的实现
      async Upload(file) {
        await Ajax.get('/aliyun/oss', {
          params: {
            oss: this.oss
          }
        })
          .then((res) => {
            if (res.code == 500) {
              ;(this.code = 1), this.$message.error(res.message)
              return false
            }
            this.dataObj = res
          })
          .catch((err) => {
            this.$message.error(err.message)
          })
        let fileName = '.' + file.file.name.substring(file.file.name.lastIndexOf('.') + 1)
        const that = this
        that.$emit('update:size', (file.file.size / 1024 / 1024).toFixed(2))
        async function multipartUpload() {
          //增加时间戳和随机数 防止文件覆盖
          let temporary = new Date().getTime() + randomString(6) + fileName
          let date = new Date()
          let year = date.getFullYear()
          let month = date.getMonth() + 1
          let day = date.getDate()
          let fileAddress =
            that.type == 'audio'
              ? 'admin/audio'
              : that.type == 'video'
              ? 'admin/video'
              : that.type == 'image'
              ? 'admin/images'
              : 'admin/lyrics'
          client(that.dataObj)
            .multipartUpload(
              `${fileAddress}/${that.business}/${year}${month}${day}/${temporary}`,
              file.file,
              {
                progress: function (p, _checkpoint) {
                  Object.assign(that.uploadInfo, _checkpoint)
                  if (that.type != 'image') {
                    that.showProgress = true
                    that.progress = Math.floor(p * 100)
                  }
                }
              }
            )
            .then((result) => {
              let url = `${that.dataObj.domain}/${fileAddress}/${that.business}/${year}${month}${day}/${temporary}`
              if (result) {
                that.isSuccess = true
              //回调,可以使用element 的上传成功钩子函数
                file.onSuccess(url)
              }
              // 上传成功 赋值 音视频预览
              that.type == 'video' ? (that.video_url = url) : (that.audio_info.resource_url = url)
              // 上传成功 隐藏进度条
              if (Number(that.progress) == 100) that.showProgress = false
              that.isDelete = true
              if (!that.multiple) that.$emit('update:value', url)
              that.$emit('update:name', temporary)
            })
            .catch((err) => {
              that.showProgress = false
              // 捕获超时异常。
              if (err.code === 'ConnectionTimeoutError') {
                that.$message.error('TimeoutError')
              }
            })
        }
        multipartUpload()
      },
      initFieldList() {
        this.urlClone = isArrayFn(this.value) || !this.value ? this.value : [this.value]
        // 初始化
        if (this.urlClone && this.urlClone.length) {
          this.fileList = this.urlClone.map((item) => {
            // 初始化预览音视频赋值
            this.type == 'video'
              ? (this.video_url = deleteParame(item))
              : (this.audio_info.resource_url = deleteParame(item))
            // 返回音视频地址显示处理
            let name = ''
            const audioPathArr = deleteParame(item).split('/')
            name = audioPathArr[audioPathArr.length - 1]
            return {
              name:
                this.type == 'audio' || this.type == 'video' || this.isLyc
                  ? name || item.name || item.audio_name || item
                  : item.name || item.audio_name || item,
              url: item
            }
          })
          this.fileCurrentLen = this.fileList.length
        }
      },
      // getFieldName(item) {
      //   if (this.videoName && Object.getOwnPropertyNames(this.videoName).length !== 0) {
      //     return this.videoName
      //   }
      //   if (this.name && Object.getOwnPropertyNames(this.name).length !== 0) {
      //     return this.name
      //   }
      //   if (this.audio_name && Object.getOwnPropertyNames(this.audio_name).length !== 0) {
      //     return this.audio_name
      //   }
      //   return item
      // },
      beforeDestroy() {
        document.getElementById('myVideo').addEventListener('canplaythrough', true),
          document.getElementById('myAudio').addEventListener('canplaythrough', true)
        // 组件销毁 释放对象url
        URL.revokeObjectURL(this.getVideoInfo)
        URL.revokeObjectURL(this.getAudioInfo)
      }
    }
  }
</script>
<style lang="less" scoped>
  .ossUpload {
    /deep/ .el-upload-list__item {
      transition: none !important;
    }
    .el-progress {
      width: 55%;
    }
    .el-progress-bar {
      width: 55%;
    }
    .el-upload-list__item {
      width: 30% !important;
    }
    .video {
      display: none;
    }
    .upload {
      line-height: 1.4;
    }
    .el-upload-list .focusing {
      border: 1px solid #c0ccda;
      outline: none;
    }
    .has {
      /deep/ .el-upload--picture-card:last-child {
        display: none;
      }
      :global(.el-upload--) {
        display: none;
      }
    }
    .unshow {
      :global(.el-upload-list--) {
        display: none;
      }
      :global(.el-upload-list--picture) {
        display: none;
      }
    }
    .audio {
      display: none;
    }
    .preview {
      margin-left: 10px;
    }
    .video-con {
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .videoPrview {
      width: 100%;
      height: auto;
      max-height: 60vh;
    }
    .hidden {
      /deep/ .el-upload-list--picture-card .el-upload-list__item {
        height: 100px;
        width: 100px;
      }
      /deep/ .el-upload-list--picture-card .el-upload-list__item-actions {
        font-size: 14px;
      }
      /deep/.el-upload--picture-card {
        height: 100px;
        width: 100px;
        line-height: 100px;
      }
    }
  }
</style>

3:防止不同文件相同名字上传oss 覆盖, 对文件名处理拼接(看公司需求要求)

// 随机字符串
export function randomString(len) {
  len = len || 32
  const $chars =
    'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
  const maxPos = $chars.length
  let pwd = ''
  for (let i = 0; i < len; i++) {
    pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
  }
  return pwd
}

:有任何问题都可留言 ps: 此组件之前全是vue2 写法, 现用在vue3项目中,所以中有emits, 并没有全部改成3.0的写法,只是兼容了3,0 ,各种可按情况更改

上一篇下一篇

猜你喜欢

热点阅读