Vue直傳文件到騰訊云COS對象存儲

需求: 在開發(fā)過程中,公司有購買阿里云、騰訊云等云存儲服務(wù)且為了解決后端服務(wù)器的壓力,可采用前端直接傳輸文件到對象存儲服務(wù)器(怎么開通對象存儲我這里就不贅述了,直接上代碼~~~)
傳輸流程應(yīng)該是:選取文件---> 向后臺請求接口獲取臨時(shí)密鑰---> 上傳文件

1、在項(xiàng)目中安裝對象存儲的相關(guān)依賴

我這里用的是騰訊云對象存儲
cnpm install cos-js-sdk-v5

2、在utils目錄下創(chuàng)建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 文件上傳過程中返回上傳速度、進(jìn)度以及文件名的方法,
* callback 文件上傳成功后的回調(diào)方法,
* setBigFile 大文件上傳初始化設(shè)置回調(diào)方法
*
* */
export function uploadObject (file, fileCallback, setBigFile, callback) {
    /*
     1.獲取臨時(shí)秘鑰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)
    // 獲取當(dāng)前時(shí)間戳 與文件類型拼接 為cos.putObject里的參數(shù)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 => { // 后臺接口返回 密鑰相關(guān)信息
        const data = response.data
        var credentials = data && data.credentials
        if (!data || !credentials) return console.error('未獲取到參數(shù)')
        // 初始化
        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('校驗(yàn)中', JSON.stringify(progressData))
                    // },
                    onProgress: (progressData) => {
                        const percent = parseInt(progressData.percent * 10000) / 100;
                        const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
                        // console.log('進(jìn)度:' + 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) // 返回文件鏈接地址和視頻的原始名稱 上傳完成后的回調(diào)
                    }
                }
            )
        } 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('進(jìn)度:' + 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) // 返回文件鏈接地址和視頻的原始名稱 上傳完成后的回調(diào)
                    }
                }
            )
        }
    })
}

export default {
    uploadObject
}

3、在components目錄下創(chuàng)建上傳文件的公共組件以及上傳完成后顯示文件列表的組件

文件上傳組件

<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">點(diǎn)擊上傳</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: 這里是根據(jù)文件名來刪除的,因?yàn)榭赡艹霈F(xiàn)沒有上傳完的大文件臨時(shí)刪除,這時(shí)候沒有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}已存在,請勿重復(fù)上傳`,
          offset: 300
        })
      }
    },
    // 文件上傳更新進(jìn)度和單文件上傳初始化
    fileProgressCallback(progress,speed,name){
      /*
      * progress 進(jìn)度
      * 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)
      }
    },
    // 大文件上傳需要記錄文件所對應(yīng)的taskId和cos,這里我是直接給進(jìn)度條 uploadProgress 做了一個(gè)初始化
    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圖標(biāo)隨文件變化
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 預(yù)覽,健壯性待完善
      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 // 預(yù)留最小點(diǎn)擊位置
        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" />
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容