Vue直传文件到腾讯云COS对象存储

需求: 在开发过程中,公司有购买阿里云、腾讯云等云存储服务且为了解决后端服务器的压力,可采用前端直接传输文件到对象存储服务器(怎么开通对象存储我这里就不赘述了,直接上代码~~~)
传输流程应该是:选取文件---> 向后台请求接口获取临时密钥---> 上传文件

1、在项目中安装对象存储的相关依赖

我这里用的是腾讯云对象存储
cnpm install cos-js-sdk-v5

2、在utils目录下创建upload.js

import COS from 'cos-js-sdk-v5'
import { Message } from 'element-ui'
import voteApi from "@/api/vote";
const config = {
    Bucket: 'xxx', 
    Region: 'xxx' 
}
// 上传到腾讯云cos
/*
* file 选取的文件,
* fileCallback 文件上传过程中返回上传速度、进度以及文件名的方法,
* callback 文件上传成功后的回调方法,
* setBigFile 大文件上传初始化设置回调方法
*
* */
export function uploadObject (file, fileCallback, setBigFile, callback) {
    /*
     1.获取临时秘钥data
     2.初始化
     3.判断上传文件的类型
     4.判断文件大小 是否需要分片上传
     */
    let fileName = file.name || ""
    const origin_file_name = fileName.split(".").slice(0, fileName.split(".").length - 1).join('.') // 获取文件名称
    // console.log('origin_file_name', origin_file_name)
    // 获取当前时间戳 与文件类型拼接 为cos.putObject里的参数Key
    const upload_file_name = new Date().getTime() + '.' + fileName.split(".")[fileName.split(".").length - 1];
    let userId = file.userId
    let date =  new Date()
    let year = date.getFullYear()
    let month = date.getMonth() + 1
    let strDate = date.getDate()
    let uploadDay = `${year}${month}${strDate}`
    // 获取密钥
    voteApi.fileUploadGetAuth({filePath: `/zhjyone/${userId}`}).then(response => { // 后台接口返回 密钥相关信息
        const data = response.data
        var credentials = data && data.credentials
        if (!data || !credentials) return console.error('未获取到参数')
        // 初始化
        var cos = new COS({
            getAuthorization: (options, callback) => {
                callback({
                    TmpSecretId: credentials.tmpSecretId,
                    TmpSecretKey: credentials.tmpSecretKey,
                    XCosSecurityToken: credentials.sessionToken,
                    StartTime: data.startTime,
                    ExpiredTime: data.expiredTime,
                    expiration: data.expiration,
                    requestId: data.requestId,
                })
            },
        })
        // 获取上传文件大小
        let size = file.size
        let key = `/zhjyone/${uploadDay}/${userId}/${upload_file_name}`
        // console.log('size', size)
            // console.log(size / (1024 * 2024))
        if (size / (1024 * 1024) < 5) { // 文件小于5M走普通上传
            console.log('文件普通上传')
            cos.putObject(
                {
                    Bucket: config.Bucket, // 存储桶名称
                    Region: config.Region, // 存储桶所在地域,必须字段
                    Key: key, // 文件名称
                    StorageClass: 'STANDARD',
                    Body: file, // 上传文件对象
                    // onHashProgress: (progressData) => {
                    //   console.log('校验中', JSON.stringify(progressData))
                    // },
                    onProgress: (progressData) => {
                        const percent = parseInt(progressData.percent * 10000) / 100;
                        const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
                        // console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;');
                        fileCallback(percent,speed,origin_file_name)
                    },
                },
                (err, data) => {
                    if (err) {
                        console.log('err', err)
                        Message({ message: '文件上传失败,请重新上传', type: 'error' })
                        let fileUrl = null
                        callback(fileUrl, origin_file_name)
                    } else {
                        let fileUrl = 'https://' + data.Location
                        callback(fileUrl, origin_file_name) // 返回文件链接地址和视频的原始名称 上传完成后的回调
                    }
                }
            )
        } else {
            console.log('文件分块上传')
            // 上传分块
            cos.sliceUploadFile(
                {
                    Bucket: config.Bucket, // 存储桶名称
                    Region: config.Region, // 存储桶所在地域,必须字段
                    Key: key /* 必须 */,
                    Body: file,
                    onTaskReady: (taskId) => {
                        /* 非必须 */
                        setBigFile && setBigFile(cos, taskId, origin_file_name)
                    },
                    // onHashProgress: (progressData) => {
                    //     /* 非必须 */
                    //     // console.log(JSON.stringify(progressData))
                    // },
                    onProgress: function (progressData) {
                        const percent = parseInt(progressData.percent * 10000) / 100;
                        const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
                        // console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;');
                        fileCallback(percent, speed, origin_file_name)
                    },
                },
                (err, data) => {
                    if (err) {
                        // console.log(err)
                        Message({ message: '文件上传失败,请重新上传', type: 'error' })
                        let fileUrl = null
                        callback(fileUrl, origin_file_name)
                    } else {
                        let fileUrl = 'https://' + data.Location
                        callback(fileUrl, origin_file_name) // 返回文件链接地址和视频的原始名称 上传完成后的回调
                    }
                }
            )
        }
    })
}

export default {
    uploadObject
}

3、在components目录下创建上传文件的公共组件以及上传完成后显示文件列表的组件

文件上传组件

<template>
  <div class="upload">
    <div class="file-upload">
      <el-upload
        ref="upload"
        :userId = "userId"
        :disabled="disabled"
        :accept="accept"
        action="#"
        :show-file-list="false"
        :http-request="uploadToCos"
        :before-upload="beforeImageUpload"
        :on-change="onChangeHandle">
        <el-button v-if="value.length < limit" :disabled="disabled" size="small" type="primary">点击上传</el-button>
      </el-upload>

      <div class="file-list">
        <file-item
          v-for="file in value"
          :key="file.id || file.fileUrl"
          :file="file"
          :disableDel="disabled"
          showDownBtn
          @remove="removeFile(file)"
        />
      </div>
    </div>
  </div>
</template>

<script>
import { uploadObject } from '@/utils/uploadObject'
import { Message } from 'element-ui'
import FileItem from './FileItem'
export default {
  name: 'MyUploadPlus',
  components: { FileItem },
  data () {
    return {
      userId: this.$route.query.userId,
      imgWidth: 0,
      imgHeight: 0,
      picIndex: -1,
      dialogImageUrl: '',
      dialogVisibleShow: false,
      fileList: [],
      isUpload: true,
      fileName: ''
    }
  },
  props: {
    disabled: {
      type: Boolean,
      default: () => false
    },
    value: {
      type: Array,
      default: () => []
    },
    accept: {
      type: String,
      default: ''
    },
    limit: {
      type: Number,
      default : 100
    }
  },
  created () {
  },
  methods: {
    removeFile (file) {
      if (file) {
        // remark: 这里是根据文件名来删除的,因为可能出现没有上传完的大文件临时删除,这时候没有url就会造成删不掉的情况
        const fileIndex = this.value.findIndex(i => i.name === file.name)
        // remark: 这里是终止上传大文件的操作
        if(file.taskId){
          let taskId = this.value[fileIndex].taskId
          file.cos.cancelTask(taskId)
        }
        this.value.splice(fileIndex, 1)
        this.$emit('input', this.value)
        // console.log(this.value)
        file.cancel && file.cancel()
      } else {
        this.value.length = 0
        this.$emit('input', this.value)
      }
    },
    onChangeHandle (file, fileList) {
      this.fileList = [file]
      console.log('onChangeHandle file, fileList', fileList);
      this.$refs.upload.$refs['upload-inner'].handleClick()
    },
    beforeImageUpload (file) {
      let fileName = this.getFileName(file.name)
      let idx = this.value.findIndex(e => e.name === fileName);
      if(idx != -1){
        this.fileName = this.value[idx].name;
      }
      this.isUpload = idx === -1 ? true : false;
      file.userId = this.userId;
    },
    // 获取选取文件的文件名
    getFileName (name) {
      return name.substring(0, name.lastIndexOf("."))
    },
    // 上传文件
    uploadToCos () {
      if(this.isUpload){
        uploadObject(this.fileList[0].raw, this.fileProgressCallback, this.setBigFile, (url, fileName) => {
          // console.log('files', this.fileList[0].raw)
          let index = this.value.findIndex(e => e.name === fileName)
          if(url){
            this.value[index].url = url
            this.$emit('input', this.value)
            Message.success({
              message:`${fileName}已上传完成`,
              offset: 300
            })
          }else{
            this.value.splice(index, 1); // 文件上传失败删除fileitem列表显示文件
          }
        })
      }else{
        Message.error({
          message:`${this.fileName}已存在,请勿重复上传`,
          offset: 300
        })
      }
    },
    // 文件上传更新进度和单文件上传初始化
    fileProgressCallback(progress,speed,name){
      /*
      * progress 进度
      * speed 传输速度
      * name 文件名称
      * */
      console.log('speed=====>',speed)
      let file = {
        name: name,
        uploadProgress: progress
      }
      if(this.value && this.value.length > 0){
        let index = this.value.findIndex(e => e.name === name)
        if(index >= 0){
          this.value[index].uploadProgress = progress
        } else {
          this.value.push(file)
        }
      } else {
        this.value.push(file)
      }
    },
    // 大文件上传需要记录文件所对应的taskId和cos,这里我是直接给进度条 uploadProgress 做了一个初始化
    setBigFile(cos, taskId, fileName){
      let file = {
        name: fileName,
        taskId: taskId,
        uploadProgress: 0,
        cos: cos
      }
        let index = this.value.findIndex(e => e.name === fileName)
        if(index >= 0){
          this.value[index].taskId = taskId
          this.value[index].cos = cos
          this.value[index].uploadProgress = 0
        } else {
          this.value.push(file)
        }
    }
  }
}
</script>

<style lang='less'>
@small-size: 80px;
.file-upload{
  max-width: 600px;
}
.file-list{
  padding-top: 20px;
  height: auto;
}
</style>

文件列表组件

<template>
  <div class="file-item">
    <div class="item-icon-wrap">
      <yc-svg-icon
          class="item-icon"
          name="video"
          v-if="disablePreview"
      ></yc-svg-icon>
      <i v-else class="el-icon-document-remove el-icon"></i>
    </div>
    <div class="item-message-wrap">
      <div class="item-message">
        <div class="message-name" @click="preview">{{ file.name }}</div>
      </div>
      <el-progress
          class="item-progress-bar"
          :percentage="parseInt(file.uploadProgress)"
          :show-text="true"
      ></el-progress>
    </div>
    <div v-if="showDownBtn" class="item-del" @click="preview">
      <i class="el-icon-download el-icon"></i>
    </div>
    <div v-if="!disableDel" class="item-del" @click="remove">
      <i class="el-icon-delete el-icon"></i>
    </div>
  </div>
</template>

<script>
// todo : icon图标随文件变化
export default {
  props: {
    file: {
      type: Object,
      required: true
    },
    disablePreview: {
      type: Boolean,
      default: false
    },
    disableDel: {
      type: Boolean,
      default: false
    },
    showDownBtn: {
      type: Boolean,
      default: false
    }
  },
  created(){
    // console.log('fileItem.file', this.file)
  },
  methods: {
    remove () {
      this.$emit('remove', this.file)
    },
    preview () {
      // todo 预览,健壮性待完善
      if (!this.disablePreview) {
        window.open(this.file.url)
      }
    }
  }
}
</script>
<style lang="less" scoped>
.file-item{
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: horizontal;
  -webkit-box-direction: normal;
  -ms-flex-flow: row;
  flex-flow: row;
  -ms-flex-wrap: nowrap;
  flex-wrap: nowrap;
  -webkit-box-pack: start;
  -ms-flex-pack: start;
  justify-content: flex-start;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  height: 50px;
  margin-bottom: 10px;
  background: white;
  border-radius: 5px;
  border: 1px solid #eee;
  .el-icon{
    font-size: 20px;
  }
  &:last-child{
    margin-bottom: 0;
  }
  .item-icon-wrap{
    flex: none;
    flex-shrink: 0;
    width: 50px;
    text-align: center;
    border-right: 1px solid #f3f3f3;
  }
  .item-message-wrap{
    flex: 1;
    padding: 0 15px;
    line-height: 1em;
    margin-top: 9px;
    .item-message{
      //@include clearfix
      .message-name {
        display: inline-block;
        text-align: left;
        //width: 180px
        //padding-right: 20px // 预留最小点击位置
        cursor: pointer;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 1;
      }
    }
  //@include text-overflow
      .message-progress-text{
        float: right;
        color: #409eff;
      }
    .item-progress-bar{
      margin-top: 5px;
    }
  }
  .item-del{
    flex: none;
    width: 50px;
    text-align: center;
    cursor: pointer;
    border-left: 1px solid #f3f3f3;
  }
}
</style>

4、页面使用

<MyUploadPlus v-model="form.videoUrls" :userId="userId" :accept="isVideo" :disabled="isDetail" />
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,548评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,069评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,985评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,305评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,324评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,030评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,639评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,552评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,081评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,194评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,327评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,004评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,688评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,188评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,307评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,667评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,337评论 2 358

推荐阅读更多精彩内容