vue 大文件分片上传

<div>
    <el-upload
      action
      :auto-upload="false"
      :show-file-list="false"
      :on-change="handleChange"
      :disabled="loadingStatus == 'uploading'"
    >
      <el-button type="primary" :disabled="loadingStatus == 'uploading'">
        <i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
      </el-button>
    </el-upload>  

    <!-- 进度显示 -->
    <div class="mask"  v-if="loadingStatus == 'uploading'">
      <i class="el-icon-loading"></i>
      <div class="progress-box">
        <span>上传进度:{{Number(percent.toFixed())}}%</span>
        <!-- <el-button type="primary" size="mini" @click="handleClickBtn">{{
          upload | btnTextFilter
        }}</el-button> -->
      </div>
    </div>


    <!-- 展示上传成功 -->
    <div class="file-list">
      <div v-if="uploadFiles.length">
        <div v-for="(item, index) in uploadFiles" :key="index">
          <div class="list-item">
            <div class="item-name">
              <span>{{ item.name }}</span>
            </div>
            <div class="item-size">大小:{{ item.size | transformByte }}</div>
            <div class="item-remove"><i class="el-icon-delete" @click="onRemoveFile(index)"></i></div>
          </div>
        </div>
      </div>
    </div>
     <slot name="tip"></slot>
  </div>

import * as api from "@/api/common-api";
import SparkMD5 from "./spark-md5.min.js";
const defaultChunkSize =  10 * 1024 * 1024;
export default {
  props:{
    defaultProps: {
      type:Array,
      default: () => []
    }
  },
  data() {
    return {
      percent: 0,
      upload: true,
      percentCount: 0,
      uploadFiles:[],
      loadingStatus: ''
    };
  },
  watch:{
    defaultProps(val) {
      console.log(val);
      this.uploadFiles = val;
    }
  },
  filters: {
    btnTextFilter(val) {
      return val ? '暂停' : '继续'
    },
    transformByte(size) {
      if (!size) {
        return "0B";
      }

      var num = 1024.0; // byte

      if (size < num) {
        return size + "B";
      }
      if (size < Math.pow(num, 2)) {
        return (size / num).toFixed(2) + "K";
      } // kb
      if (size < Math.pow(num, 3)) {
        return (size / Math.pow(num, 2)).toFixed(2) + "M";
      } // M
      if (size < Math.pow(num, 4)) {
        return (size / Math.pow(num, 3)).toFixed(2) + "G";
      } // G
      return (size / Math.pow(num, 4)).toFixed(2) + "T"; // T
    },
  },
  created() {
    this.fileId = new Date().getTime();
  },
  mounted(){
  },
  methods: {
    async handleChange(file) {
      if (!file) return;
      this.percent = 0;
      // 获取文件并转成 ArrayBuffer 对象
      const fileObj = file.raw;
      let buffer;
      try {
        buffer = await this.fileToBuffer(fileObj);
      } catch (e) {
        console.log(e);
      }

      // 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
      // const chunkSize = 2097152,
      const chunkSize = defaultChunkSize, // 切片大小
        chunkList = [], // 保存所有切片的数组
        chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
        suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名

      // 根据文件内容生成 hash 值
      const spark = new SparkMD5.ArrayBuffer();
      spark.append(buffer);
      const hash = spark.end();


      const verifyRes = await this.verifyUpload(
        fileObj.name,
        fileObj.size,
        hash
      )


      console.log(verifyRes)

      // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
      let curChunk = 0; // 切片时的初始位置
      for (let i = 0; i < chunkListLength; i++) {
        const item = {
          file: fileObj.slice(curChunk, curChunk + chunkSize),
          fileName: `${hash}_${i}.${suffix}`, // 文件名规则按照 hash_1.jpg 命名
          blockNum: i + 1,
          id: this.fileId,
          totalSize: fileObj.size,
          uploadId: this.createData.uploadId,
        };
        curChunk += chunkSize;
        chunkList.push(item);
      }
      this.chunkList = chunkList; // sendRequest 要用到
      this.hash = hash; // sendRequest 要用到
      this.sendRequest()
    },
    // 将 File 对象转为 ArrayBuffer
    fileToBuffer(file) {
      return new Promise((resolve, reject) => {
        const fr = new FileReader();
        fr.onload = (e) => {
          resolve(e.target.result);
        };
        fr.readAsArrayBuffer(file);
        fr.onerror = () => {
          reject(new Error("转换文件格式发生错误"));
        };
      });
    },
    // 发送请求
    sendRequest() {
      this.loadingStatus = 'uploading'
      const requestList = []; // 请求集合
      this.chunkList.forEach((item, index) => {
        const fn = () => {
          const formData = new FormData();
          formData.append("file", item.file);
          formData.append("size", item.file.size); // 文件名使用切片的下标
          formData.append("blockNum", item.blockNum); // 文件名使用切片的下标
          formData.append("id", item.id); // 文件名使用切片的下标
          formData.append("relationId", ''); // 文件名使用切片的下标
          formData.append("totalSize", item.totalSize); // 文件名使用切片的下标
          formData.append("uploadId", item.uploadId); // 文件名使用切片的下标
          return api.uploadBurst(formData).then((res) => {
              if (this.percentCount === 0) {
                // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
                this.percentCount = 100 / this.chunkList.length;
              }
              this.percent += this.percentCount; // 改变进度
              this.chunkList.splice(index, 1); // 一旦上传成功就删除这一个 chunk,方便断点续传
          });
        };
        requestList.push(fn);
      });

      let i = 0; // 记录发送的请求个数
      // 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
      const complete = () => {
        api.completeUploadBurst(this.fileId).then((res) => {
           this.loadingStatus = 'done'
           this.uploadFiles.push(res);
           this.$emit('on-success', this.uploadFiles)
        }).catch(err=>{
          this.resetProgress();
        })
      };
      const send = async () => {
        if (!this.upload) return;
        if (i >= requestList.length) {
          // 发送完毕
          complete();
          return;
        }
        await requestList[i]();
        i++;
        send();
      };
      send(); // 发送请求
    },

    // 按下暂停按钮
    handleClickBtn() {
      this.upload = !this.upload
      // 如果不暂停则继续上传
      if (this.upload) this.sendRequest()
    },
    // 文件上传之前的校验: 校验文件是否已存在
    verifyUpload(fileName, fileSize, fileHash) {
      return new Promise((resolve) => {
        let params = {
          md5: fileHash,
          fileName: fileName,
          id: this.fileId,
          totalSize: fileSize,
        };
        api
          .createUploadBurst(params)
          .then((res) => {
            console.log("verifyUpload -> res", res);
            this.createData = res;
            resolve(res);
          })
          .catch((err) => {
            console.log("verifyUpload -> err", err);
          });
      });
    },

    onRemoveFile(index){
      this.uploadFiles.splice(index, 1);
      this.$emit('on-success', this.uploadFiles)
    },

    resetProgress(){
      this.percent = 0;
      this.loadingStatus = ''
    },
  },
};
</script>
<style lang="scss" scoped>

  .file-list {
    .list-item {
      padding: 8px 10px;
      display: flex;
      align-items: center;
      line-height: 25px;
      position: relative;
      &:hover .item-chunk-box {
        display: block;
      }
      .item-name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        margin-right: 6px;
        .svg-icon {
          font-size: 22px;
          vertical-align: sub;
        }
      }
      .item-size {
        padding-left: 10px;
      }

      .item-remove {
        margin-left: 10px;
        color: #999;
        cursor: pointer;
      }
    }
  }

  .progress-box {
    display: flex;
    color: #fff;
    align-items: center;
    font-size: 16px;
  }


  .mask {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.3);
    display:flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    z-index: 999;
    .el-icon-loading {
      font-size: 30px;
      color: #fff;
    }
  }
</style>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容