vue2+element文件上传断点续传

文章详细描述了如何在Vue项目中使用ElementUI组件实现文件上传的断点续传功能,包括组件结构、请求处理、MD5生成和文件格式验证等关键部分。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

element文件上传断点续传

文件首次上传
在这里插入图片描述
断点续传内容展示
在这里插入图片描述
在这里插入图片描述
根据自己公司应用场景实现功能,断点续传组件代码如下

<template>
  <div class="upload">
    <el-upload class="upload-demo" action="" :on-exceed="handleExceed" :before-remove="beforeRemove" 
      :before-upload="handlePreview" :accept="parameterObj.accept" :file-list="fileList" :limit="parameterObj.limit"
      :list-type="parameterObj.type" :show-file-list="parameterObj.showFile" 
      :http-request="handleFile" ref="uploadRef">
      <slot name="button"></slot>
    </el-upload>
    <el-progress :percentage="percentage" style="width:120px;"
      :style="[{ opacity: percentage != 0 ? 1 : 0 }]"></el-progress>
  </div>
</template>
<script>
import Axios from 'axios'
// 这是生成md5字符串的文件
// 需要 npm install crypto 
import crypto from 'crypto'
// inquireFileMd5 后端校验文件是否需要断点续传
// FileFinish  上传成功需要调用接口
import { inquireFileMd5, FileFinish } from '@/api/common/index'
export default {
  name: "componentsUpload",
  data() {
    return {
    // 获取本地Storage当前系统
      systemText: sessionStorage.getItem('systemCode'),
      // 上传文件内容
      paths: [],
      // 当前续传分段
      sublevel: 0,
      // 需要续传的总数
      total: 0,
      // 进度条
      percentage: 0,
      // 1 M 分段续传
      chunkSize: 1024 * 1024 * 1,
      // 后端返回的文件续传内容
      sequelArr: [],
    }
  },
  // vue2 model方法配置
  model: {
    prop: 'dialogVisible',
    // 自定义的方法
    event: 'change'
  },
  methods: {
    /***
     * 文件续传上传请求
     * @param { FormData } formData
     * @param { file } file
     * ***/
    UrlUpload(formData, file, isUlr = true) {
      this.sublevel++
      return new Promise((resolve, reject) => {
        Axios.post(process.env.VUE_APP_IMG + '/public/basic/breakpointResumeUpload', formData)
          .then((response) => {
            if (response.data.success) {
              --this.total
              if (isUlr) {
                this.breakpointUpload(file)
                const { chunkSize } = this
                var size = file.size; //总大小
                //计算每一篇的起始位置和结束位置
                var start = this.sequelArr.length === 0 ? this.sublevel * chunkSize : this.sequelArr[this.sublevel] * chunkSize
                // var start = this.sublevel * chunkSize;
                var end = Math.min(size, start + chunkSize);
                var process = ((end / size) * 100);
                this.percentage = Math.ceil(process) === 0 ? 0 : Math.ceil(process) - 1
              }
              resolve(response.data)
            } else {
              this.$message({ message: "分片上传失败", type: 'fail' })
              reject(err)
            }
          })
          .catch((err) => {
            console.log(err)
          })
      })
    },
    // 处理切片函数
    breakpointUpload(file) {
      const { chunkSize } = this
      var size = file.size; //总大小
      var totalChunks = Math.ceil(size / chunkSize);
      //计算每一篇的起始位置和结束位置
      var start = this.sequelArr.length === 0 ? this.sublevel * chunkSize : this.sequelArr[this.sublevel] * chunkSize
      // var start = this.sublevel * chunkSize;
      var end = Math.min(size, start + chunkSize);
      // console.log(end/size);
      var fileData = file.slice(start, end)
      //文件分块上传携带参数
      let formData = new FormData();
      formData.append('chunkSize', chunkSize);
      formData.append('name', file.name);
      formData.append('size', size) //总的大小
      formData.append('chunks', totalChunks)
      // 判断是断点续传还是第一次上传
      this.sequelArr.length === 0 ? formData.append('chunk', this.sublevel) : formData.append('chunk', this.sequelArr[this.sublevel])
      formData.append('md5', this.createMd5(file.name))
      formData.append('file', fileData) //分段文件
      formData.append('system', this.system) //分段文件
      if (this.sequelArr.length === 0) {
        if (this.sublevel === totalChunks) {
          // 当请求最后一条切片上传完毕
          if (this.total === 0) {
            FileFinish({ name: file.name, system: this.system }).then(res => {
              this.$message({ message: "上传成功", type: 'success' })
              res.raw = URL.createObjectURL(file)
              this.paths.push(res)
              this.percentage = 0
            }).catch((err) => {
              console.log(err);
              this.$message.error({ message: "上传失败" })
            })
          }
          return
        }
      } else {
        if (!this.sequelArr[this.sublevel]) {
          if (this.total === 0) {
            FileFinish({ name: file.name, system: this.system }).then(res => {
              this.$message({ message: "上传成功", type: 'success' })
              res.raw = URL.createObjectURL(file)
              this.paths.push(res)
              this.percentage = 0
            }).catch((err) => {
              console.log(err);
              this.$message.error({ message: "上传失败" })
            })
          }
          return
        }
      }
      // 发送切片上传请求
      this.UrlUpload(formData, file)
    },
    // 同时发送三次请求提高文件上传速度
    uploadBySplit(file) {
      // 三条请求提供文件上传
      for (let index = 0; index < 3; index++) {
        this.breakpointUpload(file)
      }
    },
    // 根据文件名称生成md5字符串
    createMd5(content) {
      let md5 = crypto.createHash('md5')
      md5.update(content)
      return md5.digest('hex')
    },
    /***文件上传触发 
     * @param { file } file
     * ***/
    handleFile(file, fileList) {
      this.percentage = 0
      if (this.total) {
        this.$message({
          message: '文件上传中请勿再次上传',
          type: 'warning'
        });
        return
      }
      let content = file ? file.file : ""
      // 文件小于15M 不分片
      if (file.file.size < 1024 * 1024 * 15) {
        // 生成FormData格式
        let formData = new FormData();
        // 添加参数
        formData.append('file', content)
        formData.append('system', this.system)
        
        // 发送请求
        Axios.post(this.urlData, formData)
          .then((response) => {
            if (response.data.code === 200) {
              this.$message({
                message: "上传成功",
                type: "success"
              });
              this.percentage = 0
              // 页面回显
              this.paths.push(response.data.data)
              response.data.data.raw = URL.createObjectURL(content)
            } else {
              this.$message({
                message: response.data.msg,
                type: "warning"
              });
            }
          })
          .catch((err) => {
            console.log(err)
          })
        return
      } else {
        // 大于15M 
        // 根据名称字符串生成md5字符串
        const md5Name = this.createMd5(content.name)
        // 查询是否需要文件续传
        inquireFileMd5({ md5: md5Name, fileName: content.name,system:this.system }).then(res => {
          // 文件不存在使用切片上传
          this.sublevel = 0
          if (!res.continue) {
            this.sequelArr = []
            var size = content.size; //总大小
            // 计算出总条数
            this.total = Math.ceil(size / this.chunkSize);
            this.uploadBySplit(content)
          } else {
            if (res.list) {
              // 需要文件续传
              this.sequelArr = res.list
              this.total = res.list.length;
              this.uploadBySplit(content)
            } else {
              FileFinish({ name: content.name, system: this.system }).then(succeed => {
                this.$message({ message: "上传成功", type: 'success' })
                succeed.raw = URL.createObjectURL(content)
                this.paths.push(succeed)
                this.percentage = 0
              }).catch((err) => {
                console.log(err);
                this.$message.error({ message: "上传失败" })
              })
            }
          }
        })
      }
    },

    // 限制文件格式
    handlePreview(file) {
      let documentData = file.name.split('.')
      let caudal = documentData[documentData.length - 1]
      let accept = this.parameterObj.accept.split(',')
      let isJPGTY = accept.filter(item => {
        return item.includes(caudal);
      })
      const isJPG = (file.name.length < 90);
      const isLt2M = (file.size < this.parameterObj.fileSize)
      if (!isJPG) {
        this.$message({
          message: "文件名称过长",
          type: "warning"
        });
        return false
      }
      if (!isLt2M) {
        this.$message({
          message: this.parameterObj.message,
          type: "warning"
        });
        return false
      }
      if (isJPGTY.length === 0) {
        this.$message({
          message: '请上传正确的文件',
          type: "warning"
        });
        return false
      }
      return isJPG && isLt2M && isJPGTY.length > 0;
    },
    // 文件超出个数限制时的钩子
    handleExceed(files, fileList) {
      this.$message.warning(
        `当前限制选择 ${this.parameterObj.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`
      );
    },
    // 文件删除
    beforeRemove(file, fileList) {
      this.paths.map((item, index) => {
        if (file.response && item.name == file.response.data.name) {
          this.paths.splice(index, 1)
        }
        if (item.name == file.name) {
          this.paths.splice(index, 1)
        }
      })
    },
    // 文件返回给父组件
    uploadData() {
      this.$emit("uploadClick", this.paths)
    },

    fileShow() {
      if (this.fileList.length > 0) {
        this.paths = JSON.parse(JSON.stringify(this.fileList));
      }
    },
  },
  props: {
    // 文件回显
    fileList: {
      type: Array,
      default() {
        return []
      }
    },
    parameterObj: {
      type: Object,
      default() {
        return {
          type: 'picture-card',
          accept: '.png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.mp4,.MP4,.avi,.AVI,.wmv,.WMV,.mov,.MOV,.rmvb,.RMVB,.flv,.FLV,.f4v,.F4V,.3gp,3GP',
          limit: 100,
          fileSize: 1024 * 1024 * 100,
          message: '上传文件不得大于100M',
          showFile: false
        }
      }
    },
    // 需要携带的参数
    paramsData: {
      type: Object
    },
    // 请求接口
    urlData: {
      type: String,
      default: process.env.VUE_APP_IMG + '/public/basic/upload'
    },

  },
  watch: {
    fileList: {
      handler(newName, oldName) {
        this.fileShow()
      },
      deep: true
    },
    paths: {
      handler(newName, oldName) {
        this.$emit('change', newName)
      },
      deep: true
    }
  },
  computed: {
    system() {
      let text = ''
      switch (this.systemText) {
        case '/basic':
          text = 'basic_system'
          break;
        case '/operation':
          text = 'oam_system'
          break;
        case '/emergency':
          text = 'eme_system'
          break;
        default:
          text = 'system'
          break;
      }
      return text
    }
  },
  mounted() {
    this.fileShow()
  }
}
</script>

父组件应用

<template>
  <div class="fileList-img">
          <div v-for="(item, index) in fileData" :key="index" class="el-upload el-upload--picture-card picture">
            <div class="img-icon">
              <i class="el-icon-upload-success el-icon-check"></i>
            </div>
            <i class="el-icon-close" @click="fileClick(item, index)"></i>
            // 后端给前端返回1为image 2为video 
            <el-image  v-if="item.type === '1'"  :src="item.url || item.raw" :preview-src-list="srcList" class="image" />
            <template v-else>
             <video :src="item.url || item.raw" style="height: 100%" controls="controls" preload="auto"
                  class="image"></video>
            </template >
            
          </div>
          <staff-upload ref="uploadComponents" v-model="fileData"  :file-list="fileList"
            @uploadClick="uploadClick">
            <template v-slot:button>
              <i class="el-icon-plus avatar-uploader-icon"></i>
            </template>
          </staff-upload>

        </div>
</template>
<script>
import StaffUpload from './upload'
export default {
  components: {
    StaffUpload,
  },
  data(){
    return {
     // 绑定文件上传组件的model
     fileData: [],
     // 文件需要回显的内容
     fileList: [],
    }
  },
    methods: {
     // 获取文件内容
    uploadClick(content) {
      this.form.fileImg = content
    },
    // 删除文件
    fileClick({ name, type }, key) {
      this.fileData.splice(key, 1)
      this.$refs.uploadComponents.$refs.uploadRef.handleRemove({ name, type })
    },
  }
 }
</script>
断点续传是指在文件上传过程中,如果上传中断,可以从上传中断的地方开始继续上传,避免重新上传整个文件。在使用 VueElement、Axios 和 Qs 实现断点续传时,可以按照以下步骤进行操作: 1. 前端页面部分 在前端页面中,可以使用 Element 组件库来实现上传文件的功能。例如可以使用 el-upload 组件来上传文件,代码如下: ``` <template> <el-upload class="upload-demo" action="yourUploadUrl" :auto-upload="false" :on-change="handleChange" :data="uploadData" :file-list="fileList" :http-request="uploadFile" > <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" :disabled="!fileList.length" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </template> ``` 在模板中,el-upload 组件的属性值中,我们需要设置以下内容: - `action` 属性:设置上传文件的地址。 - `auto-upload` 属性:设置是否自动上传。 - `on-change` 属性:设置文件上传状态改变时的回调函数。 - `data` 属性:设置上传文件时需要携带的参数。 - `file-list` 属性:设置已经上传的文件列表。 - `http-request` 属性:设置文件上传的函数,我们在这里定义上传文件的逻辑。 接下来,我们需要在 data 函数中定义 fileList 和 uploadData 对象,代码如下: ``` data() { return { fileList: [], uploadData: { chunkSize: 1024 * 1024, // 文件切片大小 chunks: 0, // 切片总数 chunkIndex: 0, // 当前切片编号 fileSize: 0, // 文件大小 fileName: '', // 文件名 fileMd5: '', // 文件md5值 uploadUrl: '', // 上传接口地址 }, } }, ``` 在这里,我们需要定义上传文件时需要携带的参数。其中,chunkSize 表示每个切片的大小,chunks 表示总共需要切片的数量,chunkIndex 表示当前上传的切片编号,fileSize 表示文件的大小,fileName 表示文件的名称,fileMd5 表示文件的 md5 值,uploadUrl 表示上传接口的地址。 然后,我们需要在 methods 函数中定义 handleChange、uploadFile 和 submitUpload 函数。其中,handleChange 函数用来监听文件上传状态的改变,uploadFile 函数用来上传文件,submitUpload 函数用来提交上传请求。代码如下: ``` methods: { handleChange(file) { this.fileList = [file] this.uploadData.fileSize = file.size this.uploadData.fileName = file.name this.uploadData.fileMd5 = 'md5' this.uploadData.uploadUrl = 'uploadUrl' this.uploadData.chunks = Math.ceil(file.size / this.uploadData.chunkSize) }, async uploadFile(file) { const index = this.uploadData.chunkIndex++ const startByte = index * this.uploadData.chunkSize const endByte = Math.min((index + 1) * this.uploadData.chunkSize, this.uploadData.fileSize) const chunkFile = file.slice(startByte, endByte) const formData = new FormData() formData.append('file', chunkFile) formData.append('chunkIndex', index) formData.append('fileName', this.uploadData.fileName) formData.append('fileMd5', this.uploadData.fileMd5) const { data } = await this.$axios.post(this.uploadData.uploadUrl, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) if (index === this.uploadData.chunks - 1) { console.log('upload success') } else { this.uploadFile(file) } }, async submitUpload() { if (!this.fileList.length) return this.uploadData.chunkIndex = 0 await this.uploadFile(this.fileList[0].raw) } }, ``` 在这里,handleChange 函数将上传文件的基本信息保存到 uploadData 对象中,uploadFile 函数用来上传文件,submitUpload 函数用来提交上传请求。其中,uploadFile 函数是核心部分,它通过循环上传每个切片,如果上传成功,则继续上传下一个切片,否则重新上传当前切片。 2. 后端接口部分 在后端接口中,需要实现文件上传的逻辑。由于是断点续传,所以需要实现上传切片、合并切片的功能。例如可以使用 Node.js 和 Express 框架来实现上传文件的功能,代码如下: ``` const express = require('express') const multer = require('multer') const path = require('path') const fs = require('fs') const app = express() app.use(express.static('public')) app.post('/upload', multer({ dest: 'uploads/' }).single('file'), (req, res) => { const { fileName, fileMd5, chunkIndex } = req.body const chunkFile = req.file const chunkDir = path.join(__dirname, `./uploads/${fileMd5}`) const filePath = path.join(chunkDir, `${fileName}-${chunkIndex}`) if (!fs.existsSync(chunkDir)) { fs.mkdirSync(chunkDir) } fs.renameSync(chunkFile.path, filePath) res.json({ code: 0, message: '上传成功', }) }) app.post('/merge', (req, res) => { const { fileName, fileMd5, chunks } = req.body const chunkDir = path.join(__dirname, `./uploads/${fileMd5}`) const filePath = path.join(chunkDir, fileName) const chunkPaths = [] for (let i = 0; i < chunks; i++) { chunkPaths.push(path.join(chunkDir, `${fileName}-${i}`)) } let ws = fs.createWriteStream(filePath) chunkPaths.forEach((chunkPath) => { let rs = fs.createReadStream(chunkPath) rs.on('end', () => { fs.unlinkSync(chunkPath) }) rs.pipe(ws, { end: false }) }) ws.on('close', () => { fs.rmdirSync(chunkDir) res.json({ code: 0, message: '上传成功', }) }) }) app.listen(3000, () => { console.log('server is running at http://localhost:3000') }) ``` 在这里,我们实现了两个接口,一个是上传切片的接口 `/upload`,一个是合并切片的接口 `/merge`。其中,上传切片的接口会将上传的切片保存到指定的目录下,并返回上传成功的消息;合并切片的接口会将上传的所有切片合并成一个完整的文件,并删除上传的切片。注意,在合并切片的过程中,需要使用 fs.createWriteStream 和 fs.createReadStream 将切片合并成一个完整的文件。 以上就是使用 VueElement、Axios 和 Qs 实现文件上传断点续传的完整流程。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值