likes
comments
collection
share

实现文件切片上传

作者站长头像
站长
· 阅读数 19

1. 预期效果

实现文件切片上传

实现文件切片上传

实现文件切片上传

2. 过程实现

vue3

<div class="info">
   <el-input v-model="currentFileName" clearable class="info-content" @clear="resetFile" />
      <input
        id="file-upload"
        ref="fileUpload"
        type="file"
        accept=".xls, .csv"
        @input="uploadFile"
      />
      <el-button size="small" class="file-btn" @click="uploadClick">选择</el-button>
      <div class="info-tips">仅支持XLS、CSV格式文件,小于等于500M</div>
    </div>

    <div v-if="fileObj.state" class="uploadChunksPanel">
      <div class="progressWrap">
        <el-progress text-inside :stroke-width="14" :format="format" :percentage="uploadProgress" />
      </div>
      <div class="tipsWrap">
        <div class="leftBox">
          <span :class="[fileObj.state === 3 ? 'blue' : '']">
            <i v-if="fileObj.state === 2" class="el-icon-loading"></i>
            <i v-if="fileObj.state === 3" class="el-icon-success"></i>
            {{ fileObj.tips }}
          </span>
        </div>
        <div v-if="fileObj.name" class="rightBox">
          {{ fileObj.name }}<span class="line">/</span>
          {{ fileObj.size }}<span v-if="speedStr" class="line">/</span>{{ speedStr }}
        </div>
      </div>
    </div>

这里是用的input来做文件上传,也可以改成element的上传组件el-upload

  1. 变量定义
const uploadProgress = ref(0); // 上传进度
const fileObj = ref({
  name: '',
  size: '',
  state: 0,
  tips: ''
});
const speedStr = ref('');
  1. 值计算
import BMF from 'browser-md5-file';

// 计算MD5
const toHandleMd5 = (file) => {
  return new Promise((resolve, reject) => {
    const bmf = new BMF();
    bmf.md5(file, (err, md5) => {
      err ? reject(err) : resolve(md5);
    },
    progress => {
      fileObj.value.tips = `玩命计算中 ${parseInt(progress * 100)}%,即将开始上传`;
    }
    );
  });
};

// 格式化字节
export function formatBytes(value) {
  let bytes = '-';
  if (value < 1024 * 1024) {
    bytes = parseFloat(value / 1024).toFixed(2) + ' KB';
  } else if (value >= 1024 * 1024 && value < 1024 * 1024 * 1024) {
    bytes = parseFloat(value / (1024 * 1024)).toFixed(2) + ' MB';
  } else if (value >= 1024 * 1024 * 1024 && value < 1024 * 1024 * 1024 * 1024) {
    bytes = parseFloat(value / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
  } else if (value >= 1024 * 1024 * 1024 * 1024) {
    bytes = parseFloat(value / (1024 * 1024 * 1024 * 1024)).toFixed(2) + ' TB';
  }
  return bytes;
}
  1. 切片上传
const uploadFile = async(e) => {
  const file = e.target.files[0];
  const { size: fileSize, name: fileName } = file;
  const maxFileSize = 500 * 1024 * 1024;   // 限制文件上传大小 500MB

  if (fileSize <= maxFileSize) {
    fileObj.value.state = 1;
    fileObj.value.name = fileName;
    fileObj.value.size = formatBytes(fileSize);
    const fileMd5 = await toHandleMd5(file);
    fileObj.value.tips = '文件上传中...';
    fileObj.value.state = 2;

    const sliceSize = 5 * 1024 * 1024; // 切片大小 5MB
    const totalChunks = Math.ceil(fileSize / sliceSize); // 总共切片数量  Math.ceil(x) 向上取整

    for (let i = 0; i < totalChunks; i++) {
      const start = i * sliceSize;
      const chunk = file.slice(start, Math.min(fileSize, start + sliceSize)); // Math.min 返回最小的数
      const fileIndex = i + 1;

      const formData = new FormData();
      formData.append('file', chunk);  // 当前切片的文件
      formData.append('file_name', fileName); // 文件名
      formData.append('file_size', fileSize); // 文件总大小
      formData.append('file_index', fileIndex); // 当前上传的切片下标,起始1
      formData.append('total_chunks', totalChunks); // 切片总数
      formData.append('file_md5', fileMd5); // 文件md5值

      // 最后一个切片上传时
      if (fileIndex + 1 === totalChunks) fileObj.value.tips = '文件合并转存中...';

      const sTime = new Date().getTime();
      const result = await toUpload(formData); // 上传事件
      const eTime = new Date().getTime();
      const speed = sliceSize / ((eTime - sTime) / 1000); // 单个切片上传速度
      if (speed < (1024 * 1024)) {
        speedStr.value = parseFloat(speed / 1024).toFixed(2) + ' KB/s';
      } else if (speed >= (1024 * 1024) && speed < (1024 * 1024 * 1024)) {
        speedStr.value = parseFloat(speed / (1024 * 1024)).toFixed(2) + ' MB/s';
      }

      // 正常操作
      if (result?.status) {
        uploadProgress.value = Number(((fileIndex / totalChunks) * 100).toFixed(2));
        if (result?.data?.is_complete === 1) { // 是否完成 0未完成 1完成
          speedStr.value = '';
          fileObj.value.tips = '文件上传完成!';
          fileObj.value.state = 3;

          currentFilePath.value = result?.data?.file_path; // 上传完成时返回的文件路径 用于另一个接口
          currentFileName.value = fileName;
        }
      } else {
        resetParams();
        ElMessage.warning('上传失败,请重新上传!');
        break;
      }
    }
  } else {
    ElMessage.warning('文件大小超出500MB');
    resetParams();
  }
};
// 上传
const toUpload = (fileObj) => {
  return new Promise((resolve, reject) => {
    // uploadChunk 上传接口
    uploadChunk(fileObj).then(res => {
      resolve(res);
    });
  });
};
// 重置
const resetParams = () => {
  uploadProgress.value = 0;
  fileObj.value = {
    name: '',
    size: '',
    state: 0,
    tips: ''
  };
  speedStr.value = '';
};
  1. 输入框清空

实现文件切片上传

const fileUpload = ref(null);

const resetFile = () => {
  currentFileName.value = '';
  currentFilePath.value = '';
  fileUpload.value.value = ''; // 清空input选中文件
  resetParams();
};

在点击小图标时,除了要重置输入框绑定的文件名的值,同时要清空input框选中的文件。

  1. 增加 ref;<input type="file" ref="fileUpload" />
  2. 获取input file;const fileUpload = ref(null)
  3. 执行清空操作。fileUpload.value.value = ''

3. 部分样式

.card-content {
  padding: 25px 56px;
  background-color: #FAFAFA ;
  border-radius: 3px;

  .info {
    margin-top: 20px;

    .info-content {
      margin-right: 8px;
      width: 480px;
      height: 32px;
    }
    #file-upload {
      display: none;
    }
    .file-btn {
      color: #1890FF;
      border-color: #1890FF;
    }

    .info-tips {
      margin-top: 5px;
      font-size: 12px;
      color: #909399;
    }
  }
}

.uploadChunksPanel {
  margin-top: 5px;
  
  .progressWrap{
    width: 480px;
  }
  
  .tipsWrap{
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 12px;
    color: #333;
    height: 28px;
    
    .leftBox{
      margin-right: 160px;
      display: flex;
      align-items: center;
      height: 28px;
      .blue{
        color: #409EFF;
      }
      .green{
        color: #2fab66;
      }
      i{
        font-size: 14px;
      }
    }
    
    .rightBox{
      flex: 1;
      display: flex;
      align-items: center;
      height: 28px;
      .line{
        padding: 0 10px;
      }
    }
  }
}