浙里办 文件实现分片上传

前言:

浙里办项目中涉及到上传图片或者视频、文件的地方,当我们保存时,如果文件稍大点,一般都会失败。

我们做了测试,现只知道3M以内的没问题,决定将文件切片分批次上传。

环境是vue2 + vant2,上传使用的 input type="file" 的方式。分片上传要看后端逻辑怎么写,这里只给出我这边配合后端实现的部分,可自行调整。此上传是单张分别上传,并不是提交时一起上传,所以删除最好调接口删除。

实现:

data.js

// 文件分片 
// file:文件   pieceSize:分片大小  uploadId:后台返回的参数值,用于分片方法的传参  success:成功的回调  error:失败的回调  uploading:上传状态(进度)处理
export const uploadByPieces = ({ file, pieceSize = 2, uploadId, success, error, uploading }) => {
    if (!file) return false // 如果文件传入为空直接 return 返回
    let fileMD5 = ''// 总文件列表
    let base64 = '' // 每片转成的base64数据
    let entity = null // 定义参数对象
    const chunkSize = pieceSize * 1024 * 1024 // 每片的大小(M)
    const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
    // 获取md5
    const readFileMD5 = () => {
        // 读取视频文件的md5
        let fileRederInstance = new FileReader()
        fileRederInstance.readAsBinaryString(file)
        fileRederInstance.addEventListener('load', e => {
            let fileBolb = e.target.result
            fileMD5 = md5(fileBolb)
            readChunkMD5()
        })
    }
    // 获取分片数据(分片)
    const getChunkInfo = async (file, currentChunk, chunkSize) => {
        let start = currentChunk * chunkSize
        let end = Math.min(file.size, start + chunkSize)
        let chunk = file.slice(start, end)
        // 文件转成base64格式字符串
        base64 = await store.dispatch('getFileToBase64', chunk)
        return { start, end, chunk }
    }
    // 获取文件数据(不分片)
    const singleInfo = async (file) => {
        // 文件转成base64格式字符串
        base64 = await store.dispatch('getFileToBase64', file)
        return base64
    }
    // 针对每个文件进行chunk处理
    const readChunkMD5 = async () => {
        if (chunkCount != 1) {
            // 大于1片的
            console.log('分片');
            for (var i = 0; i < chunkCount; i++) {
                const { chunk } = await getChunkInfo(file, i, chunkSize)
                await uploadChunk({ chunk, currentChunk: i, chunkCount })
            }
        } else {
            // 只有1片
            console.log('不分片');
            const result = await singleInfo(file)
            const data = { Data: result }
            http.mgopPOST('/xxxx/xxxx/addfile', data, {}, true).then(res => {
                return new Promise((resolver, reject) => {
                    if (res.ErrorType == 200) {
                        uploading(1, 1)
                        resolver(true)
                        success({ skipUpload: true, url: res.Data })
                    } else {
                        error(res.Message)
                    }
                }).catch((e) => {
                    console.log(e);
                    error && error(e)
                })
            })
        }
    }
    // 上传分片
    const uploadChunk = (chunkInfo) => {
        return new Promise((resolver, reject) => {
            entity = {
                Data: {
                    base64: base64,
                    chunkNumber: chunkInfo.currentChunk + 1,
                    chunkSize: chunkSize,
                    currentChunkSize: chunkInfo.chunk.size,
                    filename: file.name,
                    totalChunks: chunkInfo.chunkCount,
                    identifier: fileMD5,
                    uploadId
                }
            }
            http.mgopPOST('/xxx/xxx/uploadChunk', entity, {}, true).then(res => {
                if (res.ErrorType == 200) {
                    uploading(chunkInfo.currentChunk + 1, chunkInfo.chunkCount)
                    resolver(true)
                    success(res.Data)
                } else {
                    error(res.Message)
                }
            }).catch((e) => {
                error && error(e)
            })
        })
    }
    readFileMD5() // 开始执行代码
}

引入:md5加密(npm install js-md5)、store、http(封装的请求)

    // 文件转成base64格式字符串
    getFileToBase64({ commit }, file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = () => resolve(reader.result)
        reader.onerror = (error) => reject(error)
      })
    }
uploadFile.vue
<!-- 文件上传 图片视频文件等 -->
<template>
  <div class="flex-ali-cen content">
    <span
      class="file-item"
      style="width: 80px; height: 80px;"
      v-for="(item, index) in fileBox"
      :key="index"
    >
      <!-- 图片 -->
      <img
        v-if="item.data.type.indexOf('image/') !== -1"
        :src="item.flowUrl"
        alt="图片"
        @click="preview(item)"
      >
      <!-- 视频 img-视频封面图片 -->
      <div
        v-else-if="item.data.type.indexOf('video/') !== -1"
        class="other-file"
        style="width: 80px; height: 80px;"
        @click="preview(item)">
        <img src="../assets/img/video.png" alt="" width="80" height="80">
      </div>
      <!-- 其他文件 -->
      <div
        v-else
        class="other-file"
        @click="preview(item)"
        style="width: 80px; height: 80px;"
      >
        <van-icon
          name="description"
          size="20px"
        />
        <span>{{item.data.name}}</span>
      </div>
      <!-- 删除按钮 -->
      <span
        class="close-icon"
        @click.stop="closeImg(item)"
      >
        <van-icon
          name="cross"
          color="#ffffff"
          size="24px"
        />
      </span>
    </span>
    <!-- 上传 -->
    <div
      v-if="fileBox.length < maxCount"
      class="upload-box"
      style="width: 80px; height: 80px;"
    >
      <van-icon
        name="photograph"
        size="24"
        color="#dcdee0"
      />
      <input
        id="fileUpload"
        type="file"
        @change="upload"
      >
    </div>
    <!-- 进度条展示 -->
    <van-overlay :show="overlayShow">
      <p class="upload-text">文件上传中...</p>
      <van-progress
        v-if="processflag"
        :percentage="percentage"
        stroke-width="8"
      />
    </van-overlay>
  </div>
</template>

<script>
import { hasStr, uploadByPieces } from '@/utils/data'
import { mapGetters } from 'vuex'
import md5 from 'js-md5'

export default {
  props: {
    // 是否可以上传文件(多项 包括图片和视频等) true可以 false不可以
    hasFile: {
      type: Boolean,
      default: () => {
        return false
      }
    },
    // 文件个数限制
    maxCount: {
      type: Number,
      default: () => {
        return 9
      }
    },
    // 是否只上传图片
    isImg: {
      type: Boolean,
      default: () => {
        return false
      }
    }
  },
  data() {
    return {
      fileBox: [],
      xlclBase: [],
      percentage: 0,
      processflag: false,
      overlayShow: false,
      flowFileUrl: window.g.READ_FILE_URL
    }
  },
  computed: {
    ...mapGetters(['getFileUrl'])
  },
  mounted() {},
  methods: {
    // 删除方法
    closeImg(data) {
      this.fileBox.forEach((item, index, array) => {
        if (item.resultUrl === data.resultUrl) {
          array.splice(index, 1)
          this.xlclBase.splice(index, 1)
        }
      })
      this.$http.mgopGET('/xxx/xxxx/deleteFile', { lllj: data.resultUrl }).then(res => {
        if (res.ErrorType == 200) {
          this.$notify.success('删除成功')
        }
      })
    },
    // 获取上传后的数据
    getUrls() {
      return this.xlclBase
    },
    // 上传方法
    upload() {
      const file = document.getElementById('fileUpload'), fileObj = file.files[0], that = this
      const size = fileObj.size / (1024 * 1024) // 文件大小
      // 上传条件判断
      if (this.isImg && !hasStr(fileObj.type, 'image/')) {
        this.$_toast('请上传照片')
        return false
      }
      if (!this.hasFile && !this.isImg && !hasStr(fileObj.type, 'image/') && !hasStr(fileObj.type, 'video/')) {
        this.$_toast('请上传照片或视频')
        return false
      }
      if (size > 50) {
        // 文件大小大于 50兆 先提醒
        this.$dialog.confirm({
          title: '提醒',
          message: '上传的文件超过50M,要继续上传吗?',
          cancelButtonText: '取消',
          confirmButtonText: '确定'
        })
          .then(() => {
            that.uploadChunk(fileObj, that)
          })
          .catch(() => { });
      } else {
        that.uploadChunk(fileObj, that)
      }
    },
    // 处理上传逻辑
    uploadChunk(fileObj, that) {
      // 重置数据
      const reset = (that) => {
        setTimeout(() => {
          that.overlayShow = false
          that.processflag = false;
          that.percentage = 0
        }, 2000);
      }
      // 存返回的文件信息
      const setFileData = (result, file, that) => {
        that.$notify.success('文件上传成功')
        that.xlclBase.push(result.url)
        that.fileBox.push({
          url: this.getFileUrl + result.url, // 地址前缀拼接展示
          flowUrl: this.flowFileUrl + result.url, // 文件流拼接展示
          resultUrl: result.url,
          data: file
        })
        console.log('fileBox', that.fileBox);
      }
      let fileReder = new FileReader()
      fileReder.readAsBinaryString(fileObj)
      fileReder.addEventListener('load', e => {
        const result = e.target.result, fileMD5 = md5(result), req = {
          Data: {
            filename: fileObj.name,
            identifier: fileMD5
          }
        }
        that.$http.mgopPOST('/xxx/xxxx/uploadChunk', req, {}, true).then(res => {
          if (res.ErrorType == 200) {
            if (res.Data.skipUpload) {
              setFileData(res.Data, fileObj, that)
              reset(that)
            } else {
              that.overlayShow = true
              uploadByPieces({
                file: fileObj,
                pieceSize: 3,
                uploadId: res.Data.uploadId,
                success: (result) => {
                  if (result.skipUpload) {
                    setFileData(result, fileObj, that)
                    reset(that)
                  }
                },
                error: (e) => {
                  that.$_toast(e);  // 打印报错信息
                  reset(that)
                },
                uploading: (chunk, allChunk) => {
                  that.processflag = true;   // 上传时进度条展示 根据需要添加
                  let st = Math.floor((chunk / allChunk) * 100); // 这里是用上传的第几片除以总片数进行百分比计算
                  that.percentage = st;
                },
              })
            }
          }
        })
      })
    },
    // 处理上传后的回显
    preview(data) {}
  }
}
</script>

<style lang="scss" scoped>
.content {
  flex-wrap: wrap;
  .other-file {
    background-color: #f7f8fa;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    span {
      display: inline-block;
      width: 100%;
      margin-top: 8px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
  }
  .van-progress {
    top: 48%;
  }
}
.upload-text {
  position: absolute;
  top: 40%;
  left: 50%;
  transform: translate(-50%, 0);
  color: var(--global);
  font-size: var(--font16);
  background: #1989fa;
  padding: 3px 10px;
  border-radius: 5px;
}
</style>

部分方法、文件的预览等可自行实现,这里没给出。主要的上传部分都已展示。

当做回显时,如果后台返回的文件流形式,一般采用直接拼接方式(实际也是接口调取)后台需配置网关忽略来展示。浙里办不支持responseType: 'blob'类的直接请求(并未明文指出,经过我们大量实践总结)。

如:READ_FILE_URL: 'https://xxxxxxxx/xxxx/xxxx/xxx/ossShow?fileUrl=',

使用:

<template>
    <div class="container">
        <van-field class="file-field" label-width="5rem" name="uploader" label="文件">
            <template #input>
                <UploadFile ref="uploadFile" :has-file="true" />
            </template>
        </van-field>
        <van-button class="submit" @click="submit">提交</van-button>
    </div>
</template>

<script>
import UploadFile from '@/components/uploadFile'
export default {
    components: { UploadFile },
    methods: {
        async submit() {
            const arr = await this.$refs.uploadFile.getUrls()
            console.log('arr', arr);
        }
    }
}
</script>

<style lang="scss">
.file-field {
    .van-field__control {
        flex-wrap: wrap;
    }
    .van-uploader {
        display: inline-block;
    }
    .file-item {
        display: inline-block;
        position: relative;
        margin: 0 5px 5px 0;
        box-sizing: border-box;

        img,
        video {
            width: 100%;
            height: 100%;
        }
    }
    .upload-box {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin: 0 5px 5px 0;
        background-color: #f7f8fa;
        position: relative;
        box-sizing: border-box;

        #fileUpload {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            overflow: hidden;
            cursor: pointer;
            opacity: 0;
        }
    }
}

.submit {
    width: 90%;
    margin-left: 5%;
    margin-top: 20px;
}
</style>

获取到的最终数据:

 

 

posted @ 2023-02-27 15:15  拾忆-iiii  阅读(451)  评论(0编辑  收藏  举报