vue实现大文件切片上传

使用插件 vue-simple-uploader

  1. 封装Uploader组件

<template>
  <div id="global-uploader" :class="{'global-uploader-single': !global}">
    <uploader
        ref="uploader"
        :options="initOptions"
        :fileStatusText="fileStatusText"
        :autoStart="false"
        @file-added="onFileAdded"
        @file-success="onUploadSuccess"
        @file-progress="onFileProgress"
        @file-error="onFileError"
        class="uploader-app">

      <uploader-unsupport></uploader-unsupport>
      <div @click="clickUploader" :style="{width}">
        <uploader-drop
            class="custom_uploader_drop"
            :style="{width}"
            v-if="!isUpload"
            v-loading="isMd5Upload"
            element-loading-text="正在读取中">
          <slot name="customContent"></slot>
          <uploader-btn ref="uploadBtn" style="display: none" />
        </uploader-drop>
        <div v-if="isUpload" class="upload_process_box">
          <div>文件名:{{fileName}}</div>

            <el-progress  :percentage="uploadProcessNum"></el-progress>
<!--            <el-progress v-else :percentage="syncUploadProcessNum"></el-progress>-->
          <div v-if="isMd5Upload">
            正在读取文件中 - {{md5ProgressText}}
          </div>
          <div v-if="!isSyncUpload&&!isMd5Upload">
            正在上传至服务器 - <span>{{uploadSpeed}} M/s</span>
          </div>
          <div v-if="isSyncUpload">
            正在上传至华为云,稍等会儿 (*^<i class="el-icon-loading" />^*)
          </div>
        </div>
      </div>


<!--      <uploader-list v-show="panelShow">-->
<!--        <div class="file-panel" slot-scope="props" :class="{ collapse: collapse }">-->
<!--          <div class="file-title">-->
<!--            <div class="title">文件列表</div>-->
<!--            <div class="operate">-->
<!--              <el-button @click="collapse = !collapse" type="text" :title="collapse ? '展开' : '折叠'">-->
<!--                <i class="iconfont" :class="collapse ? 'el-icon-full-screen' : 'el-icon-minus'"></i>-->
<!--              </el-button>-->
<!--              <el-button @click="close" type="text" title="关闭">-->
<!--                <i class="el-icon-close"></i>-->
<!--              </el-button>-->
<!--            </div>-->
<!--          </div>-->

<!--          <ul class="file-list">-->
<!--            <li-->
<!--                class="file-item"-->
<!--                v-for="file in props.fileList"-->
<!--                :key="file.id">-->
<!--              <uploader-file-->
<!--                  :class="['file_' + file.id, customStatus]"-->
<!--                  ref="files"-->
<!--                  :file="file"-->
<!--                  :list="true"-->
<!--              ></uploader-file>-->
<!--            </li>-->
<!--            <div class="no-file" v-if="!props.fileList.length">-->
<!--              <i class="iconfont icon-empty-file"></i> 暂无待上传文件-->
<!--            </div>-->
<!--          </ul>-->
<!--        </div>-->
<!--      </uploader-list>-->
    </uploader>
  </div>
</template>

<script>
import { ACCEPT_CONFIG } from './js/config'
import Bus from './js/bus'
import SparkMD5 from 'spark-md5'
// import { mergeSimpleUpload } from '@/api'
// 封装的网络请求promise
import { uploadFile,startOriginUploadFile, queryUploadFileProgress,startMergeFile } from './js/service.js'

import { getToken } from "@/utils/auth";

// let urll = process.env.VUE_APP_BASE_API + '/wk-upload/upload/upload';

// let urll = 'http://192.168.1.111:9999/wk-upload/upload/upload';

export default {
  props: {
    global: {
      type: Boolean,
      default: false
    },
    // 发送给服务器的额外参数
    params: {
      type: Object
    },
    options: {
      type: Object
    },
    // 宽度
    width: {
      type: [Number,String],
      default: 100,
      validator(value) {
        return typeof value === 'String' ? value : (value + 'px')
      }
    }
  },

  data() {
    return {
      // actionResourceUrl: process.env.VUE_APP_BASE_API + '/upload/upload',
      initOptions: {
        target: this.$api.actionBigFileUrl,
        headers: {
          Authorization: "Bearer " + getToken(),
          // 'Content-Type': 'application/json;charset=UTF-8',
        },
        chunkSize: 1024*1024*10,//10485760 //10000000, //1024 * 1024 * 3,  //3MB 10000000
        // chunkSize: '2048000',
        fileParameterName: 'file', //上传文件时文件的参数名,默认file
        singleFile: true, // 启用单个文件上传。上传一个文件后,第二个文件将超过现有文件,第一个文件将被取消。
        maxChunkRetries: 3,  //最大自动失败重试上传次数
        testChunks: false,     //是否开启服务器分片校验
        // simultaneousUploads: 3, //并发上传数
        // 服务器分片校验函数,秒传及断点续传基础
        // checkChunkUploadedByResponse: (chunk, message) => {
        //   let skip = false
        //   //
        //   // try {
        //   //   let objMessage = JSON.parse(message)
        //   //   if (objMessage.skipUpload) {
        //   //     skip = true
        //   //   } else {
        //   //     skip = (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
        //   //   }
        //   // } catch (e) {}
        //
        //   return skip
        // },
        query: (file, chunk) => {
          return {
            ...file.params
          }
        }
      },
      fileStatusText: {
        success: '上传成功',
        error: '上传失败',
        uploading: '上传中',
        paused: '已暂停',
        waiting: '等待上传'
      },
      panelShow: false, //选择文件后,展示上传panel
      collapse: false,
      customParams: {},
      customStatus: '',

      isUploadOk: false,
      isUploadErr: false,
      isStartUpload: false, // 开始上传
      md5ProgressText: 0,
      isMd5Upload: false, // 计算md5状态
      isUpload: false, // 正在上传
      uploadProcessNum: 0, // 上传进度
      uploadSpeed: 0, // 上传速度
      fileName: '', // 文件名
      isSyncUpload: false, // 是否在同步远程数据
      syncUploadProcessNum: 0, // 同步远程数据
      response: null, // 上传成功
      queryTimer: null, // 轮询计时器

      socket: null,
    }
  },
  computed: {
    // Uploader实例
    uploader() {
      return this.$refs.uploader.uploader
    }
  },

  methods: {
    // 自定义options
    customizeOptions(opts) {
      // 自定义上传url
      if (opts.target) {
        this.uploader.opts.target = opts.target
      }

      // 是否可以秒传、断点续传
      if (opts.testChunks !== undefined) {
        this.uploader.opts.testChunks = opts.testChunks
      }

      // merge 的方法,类型为Function,返回Promise
      this.mergeFn = opts.mergeFn || uploadFile

      // 自定义文件上传类型
      let input = document.querySelector('#global-uploader-btn input')
      let accept = opts.accept || ACCEPT_CONFIG.getAll()
      input.setAttribute('accept', accept.join())
    },
    clickUploader(e) {
      // console.log(e)
      this.$refs.uploadBtn.$el.click()
    },
    // 上传前
    onFileAdded(file) {
      // this.panelShow = true
      // this.emit('fileAdded')

      // 将额外的参数赋值到每个文件上,以不同文件使用不同params的需求
      // file.params = this.customParams

      // 计算MD5
      this.computeMD5(file).then((result) => this.startUpload(result))
    },
    /**
     * 计算md5值,以实现断点续传及秒传
     * @param file
     * @returns Promise
     */
    computeMD5(file) {
      let fileReader = new FileReader()
      let time = new Date().getTime()
      let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
      let currentChunk = 0
      const chunkSize = this.initOptions.chunkSize;

      let chunks = Math.ceil(file.size / chunkSize)
      let spark = new SparkMD5.ArrayBuffer()
      // 获取文件名
      const fileInfo = file.uploader.fileList[0]
      this.fileName = fileInfo.name

      // 文件状态设为"计算MD5"
      // this.statusSet(file.id, 'md5')
      this.isMd5Upload = true;
      this.isUpload = true;
      file.pause()
      loadNext()

      return new Promise((resolve, reject) => {
        fileReader.onload = (e) => {
          spark.append(e.target.result)
          if (currentChunk < chunks) {
            currentChunk++
            loadNext()

            // 实时展示MD5的计算进度
            this.$nextTick(() => {
              this.md5ProgressText = ((currentChunk/chunks)*100).toFixed(0)+'%'
            })

          } else {
            let md5 = spark.end()

            // md5计算完毕
            resolve({md5, file})
            // console.log(file);
            // console.log(
            //     `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${
            //         new Date().getTime() - time
            //     } ms`
            // )
          }
        }

        fileReader.onerror = function () {
          this.error(`文件${file.name}读取出错,请检查该文件`)
          file.cancel()
          reject()
        }
      })

      function loadNext() {
        let start = currentChunk * chunkSize
        let end = start + chunkSize >= file.size ? file.size : start + chunkSize

        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
      }
    },

    // md5计算完毕,开始上传
    startUpload({md5, file}) {
      file.uniqueIdentifier = md5
      file.resume();
      this.isMd5Upload = false;
      this.isStartUpload = true;
      // this.statusRemove(file.id)
    },
    // 上传中
    onFileProgress(rootFile, file, chunk) {
      // console.log(
      //     `上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${
      //         chunk.endByte / 1024 / 1024
      //     }`
      // )
      // let index = this.findFileById(file.uniqueIdentifier),//通过index来获取对应的文件progress
      //     p = Math.round(file.progress()*100);
      // if(index > -1){
      //   if(p < 100){
      //     console.log(p)
      //   }
        // this.fileList[index].status = file.status;
      // }
      // console.log(rootFile)
      // let p = this.$refs.uploader.progress()
      // console.log(p)
      let uploader = this.$refs.uploader.uploader;
      this.uploadProcessNum = Math.floor(uploader.progress() * 100)
      this.emit('onUploadProcess',uploader.progress());


      // let averageSpeed = uploader.averageSpeed
      let averageSpeed = uploader.averageSpeed
      // let timeRemaining = uploader.timeRemaining()
      // let uploadedSize = uploader.sizeUploaded()
      let speed = averageSpeed / 1000 / 10;
      this.uploadSpeed = speed.toFixed(2);
      // console.log(speed.toFixed(2) + 'M/s')
    },
    // 上传中转站成功
    onUploadSuccess(rootFile, file, response, chunk) {
      let res1 = JSON.parse(response);
      if(res1.code===200) {
       // 开始merge
        console.log(rootFile.uniqueIdentifier);
        const body = {
          totalChunks: rootFile.chunks.length,
          md5File: rootFile.uniqueIdentifier,
          fileName: rootFile.name
        }
        console.log(file)
        startMergeFile(body).then(res=>{
          // 上传到华为云
          this.onUploadSuccessFinally({rootFile, file, res, chunk})
        })
      }else{
        this.error('上传失败')
        this.$emit('onUploadError',res1)
      }

      // 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
      // if (!res.result) {
      //   this.error(res.message)
      //   // 文件状态设为“失败”
      //   this.statusSet(file.id, 'failed')
      //   return
      // }
      // return;
      // 如果服务端返回了需要合并的参数
      // if(res.needMerge) {
      //   // 文件状态设为“合并中”
      //   this.statusSet(file.id, 'merging')
      //
      //   this.mergeFn({
      //     tempName: res.tempName,
      //     fileName: file.name,
      //     ...file.params
      //   })
      //       .then((res) => {
      //         // 文件合并成功
      //         this.emit('fileSuccess')
      //
      //         this.statusRemove(file.id)
      //       })
      //       .catch((e) => {})
      //
      //   // 不需要合并
      // } else {
      //   this.emit('fileSuccess')
      //   console.log('上传成功')
      // }
    },
    // 开始上传至华为云
    onUploadSuccessFinally(data) {
     // console.log(data)
      const {rootFile, file, res, chunk} = data
      let body = {
        id: res.data.id
      }
      // try {
        // 请求上传远程开始
        this.isSyncUpload = true;
         startOriginUploadFile(body).then(res1=>{
           this.isUpload = false;
           this.isSyncUpload = false;
           this.$emit('onUploadSuccess',res1)
         }).catch(err=>{
             this.isUpload = false;
             this.$emit('onUploadError',e)
             this.error('上传华为云失败')
             clearInterval(this.queryTimer)
         })
    },
    // 获取上传华为云进度
    queryProcess(body) {

    },
    // 上传失败
    onFileError(rootFile, file, response, chunk) {
      this.error('上传失败');
      this.$emit('onUploadError',response);
      this.isUpload = false;
    },
    // 取消
    close() {
      this.uploader.cancel()
      this.panelShow = false
    },
    /**
     * 新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
     * @param id
     * @param status
     */
    statusSet(id, status) {

      let statusMap = {
        md5: {
          text: '读取文件中',
          bgc: '#fff'
        },
        merging: {
          text: '合并中',
          bgc: '#e2eeff'
        },
        transcoding: {
          text: '转码中',
          bgc: '#e2eeff'
        },
        failed: {
          text: '上传失败',
          bgc: '#e2eeff'
        }
      }

      this.customStatus = status
    },
    // 移除状态
    statusRemove(id) {
      this.customStatus = ''
    },

    emit(e) {
    },

    error(msg) {
      this.$notify({
        title: '错误',
        message: msg,
        type: 'error',
        duration: 2000
      })
    }
  },
  beforeDestroy() {
    clearInterval(this.queryTimer)
  }
}
</script>

<style lang="scss">
#global-uploader {
  &:not(.global-uploader-single) {
    position: fixed;
    z-index: 20;
    right: 15px;
    bottom: 15px;
    box-sizing: border-box;
  }

  .uploader-app {
    width: 520px;
  }

  .file-panel {
    background-color: #fff;
    border: 1px solid #e2e2e2;
    border-radius: 7px 7px 0 0;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);

    .file-title {
      display: flex;
      height: 40px;
      line-height: 40px;
      padding: 0 15px;
      border-bottom: 1px solid #ddd;

      .operate {
        flex: 1;
        text-align: right;

        i {
          font-size: 18px;
        }
      }
    }

    .file-list {
      position: relative;
      height: 240px;
      overflow-x: hidden;
      overflow-y: auto;
      background-color: #fff;
      transition: all 0.3s;

      .file-item {
        background-color: #fff;
      }
    }

    &.collapse {
      .file-title {
        background-color: #e7ecf2;
      }
      .file-list {
        height: 0;
      }
    }
  }

  .no-file {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 16px;
  }

  .uploader-file {
    &.md5 {
      .uploader-file-resume {
        display: none;
      }
    }
  }

  .uploader-file-icon {
    &:before {
      content: '' !important;
    }
  }

  .uploader-file-actions > span {
    margin-right: 6px;
  }

  .custom-status {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1;
  }
}

.custom_uploader_drop {
     background-color: rgba(176, 172, 172, 0.1) !important;
     cursor: pointer;
     border-radius: 5px;
  &:hover {
    border-color: #409eff !important;
  }
}
.upload_process_box {
  border-radius: 5px;
  border: 1px dashed #409eff;
  & > div {
    padding: 5px 0;
  }
}


/* 隐藏上传按钮 */
#global-uploader-btn {
  position: absolute;
  clip: rect(0, 0, 0, 0);
}

.global-uploader-single {
  #global-uploader-btn {
    position: relative;
  }
}

</style>

调用组件给组件传值


<customStardingUploader
   v-if="!form.resourceUrl"
    width="400px"
    @onUploadSuccess="handleAvatarSuccessResource"
    @onUploadError="handleVideoErrorResource">
  <!--   自定义内容   -->
  <template #customContent>
    <div style="margin: 0 auto;width: fit-content;text-align: center">
      <div>将文件拖拽到此处</div>
      <div>或点击上传</div>
    </div>
  </template>
</customStardingUploader>

 // 上传视频成功
 handleAvatarSuccessResource(res, file) {
    this.form.resourceUrl = res.data;
    this.uploadLoading = false;
  },
  // 上传视频失败
  handleVideoErrorResource(err) {
    this.uploadLoading = false;
  },

本来想搞一个手搓的分片的,而且已经写完我都上线了。但是想了下,能用组件为啥手搓,希望大家千万别和我一样。手搓费事还不如插件搞完然后摸鱼,好了再见

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值