Vue+el-upload配置minIO实现大文件的切片并发上传、上传进度展示、失败重试功能

news2025/3/7 1:05:56

vue3+el-upload实现切片上传

效果图

初始界面
在这里插入图片描述
上传中的界面
在这里插入图片描述
上传完成的界面
在这里插入图片描述
上传失败的界面
在这里插入图片描述

<template>
  <div>
    <el-upload
      class="BigFileUpload"
      ref="uploadRef"
      action="#"
      drag
      :show-file-list="false"
      :on-change="handleFileChange"
      :on-exceed="handleExceed"
      :on-error="handleError"
      :http-request="handleUpload"
      :limit="fileLimit"
      :file-list="fileList"
    >
      <div
        :class="[
          'BigFileUpload-text',
          fileList.length > 0 ? 'BigFileUpload-text-hasFileList' : 'BigFileUpload-text-noFileList'
        ]"
      >
        <img class="el-upload__img" src="@/assets/newUI/icon-upload.png" />
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      </div>
      <template #tip>
        <div class="el-upload__tip">导入规则:只允许上传大小不超过{{ fileSize / 1024 }}GB的文件。</div>
      </template>
    </el-upload>
    <div
      :class="[
        'BigFileUpload-list',
        fileList.length > 0 ? 'BigFileUpload-list-hasFileList' : 'BigFileUpload-list-noFileList'
      ]"
    >
      <div class="fileList" v-for="(file, index) in fileList" :key="index">
        <el-image fit="scale-down" :src="computedFileIcon(file)" class="image file-image"></el-image>
        <div class="file-name">{{ file.name }}</div>
        <div class="file-progress">
          <div v-if="fileStatus === 'fail'" class="fail">
            <span class="text">上传失败!</span>
            <span class="btn" @click="handleReTry(file)">点击重试</span>
          </div>
          <div v-if="fileStatus === 'success'" class="success">
            <span>100%上传完成</span>
            <el-icon @click="handleRemove(file)">
              <Delete />
            </el-icon>
          </div>
          <div v-if="fileStatus === 'loading'" class="uploading">
            <span>{{ percentage }}%上传中</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, getCurrentInstance } from 'vue'
import { watch } from 'vue-demi'
import axios from 'axios'
import SparkMD5 from 'spark-md5'
import { getToken } from '@/utils/auth'
import { getUserAgentInfo } from '@/api/login'
import box from '@/assets/images/knowdge/box.png'
import jpg from '@/assets/images/knowdge/image.png'
import word from '@/assets/images/knowdge/DOC.png'
import xslx from '@/assets/images/knowdge/XLS.png'
import pdf from '@/assets/images/knowdge/PDF.png'
import ppt from '@/assets/images/knowdge/PPT.png'
import video from '@/assets/images/knowdge/video.png'
import mp3 from '@/assets/images/knowdge/video.png'
import zip from '@/assets/images/knowdge/zip.png'
import other from '@/assets/images/knowdge/file.png'
import txt from '@/assets/images/knowdge/TXT.png'
const imgList = {
  ppt: ppt,
  doc: word,
  docx: word,
  xlsx: xslx,
  xls: xslx,
  pdf: pdf,
  pdfx: pdf,
  zip: zip,
  rar: zip,
  jpg: jpg,
  jpeg: jpg,
  png: jpg,
  webp: jpg,
  mp3: mp3,
  mp4: video,
  txt: txt
}
let timeStamp = 0
const props = defineProps({
  chunkSize: {
    type: Number,
    default: 10 // 默认分片大小 10MB
  },
  actionURL: {
    type: String,
    required: true // 文件上传接口地址
  },
  fileLimit: {
    type: Number,
    default: 1
  },
  MAX_REQUEST: {
    // 最大并发数
    type: Number,
    default: 5
  },
  maxRetries: {
    // 重试次数
    type: Number,
    default: 2
  },
  // 大小限制(MB)
  fileSize: {
    type: Number,
    default: 200
  },
  // 大小限制(MB)
  fileListArr: {
    type: Array,
    default: []
  }
})
const { proxy } = getCurrentInstance()
const emit = defineEmits(['upload-success', 'upload-remove'])
const fileList = ref([])
const file = ref(null) // 当前上传的文件
const fileMd5 = ref('') // 文件的 MD5 值
const fileName = ref('') // 文件名称
const fileType = ref('') // 文件类型,带".",例如 :  .mp3
const totalChunks = ref(0) // 文件的分片总数量
const uploadedChunks = ref([]) // 已上传的分片索引
const requestPool = ref([]) // 文件请求池
let chunkCountList = ref([]) // 选择的文件切片总数数组
let MAX_REQUEST = props.MAX_REQUEST // 最大请求数
let httpRequestParams = {}
const percentage = ref(0) // 进度
const fileStatus = ref('') // 当前上传的文件状态loading上传中,success上传成功,fail上传失败
let isAbortRequest = false // 是否放弃请求
// 计算文件类型
const computedFileIcon = fileItem => {
  const index = fileItem.name.lastIndexOf('.')
  const ext = fileItem.name.substr(index + 1)
  return imgList[ext] || other
}
// 计算文件的 MD5
const calculateFileMd5 = file => {
  return new Promise(resolve => {
    const reader = new FileReader()
    const spark = new SparkMD5.ArrayBuffer()
    reader.onload = e => {
      spark.append(e.target.result)
      resolve(spark.end())
    }
    reader.readAsArrayBuffer(file)
  })
}
// 文件选择回调
const handleFileChange = async uploadFile => {
  console.log(123)
  // 校验网络状态
  // if (!navigator.onLine) {
  //   this.$message.error('请检查网络连接');
  //   return false; // 阻止上传
  // }
  // 校验文件大小
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 > props.fileSize
    if (isLt) {
      console.log('上传文件已超过4G!')
      return false
    }
  }
  const index = uploadFile.name.lastIndexOf('.')
  const ext = uploadFile.name.substr(index + 1)
  // 文件
  file.value = uploadFile.raw
  // 文件名称
  fileName.value = uploadFile.name
  // 文件类型
  fileType.value = ext
}
// 开始上传
const startUpload = async () => {
  if (!file.value) {
    alert('请先选择文件!')
    return
  }
  uploadedChunks.value = []
  isAbortRequest = false
  percentage.value = 0
  fileStatus.value = 'loading'
  // 分片上传
  totalChunks.value = Math.ceil(file.value.size / (props.chunkSize * 1024 * 1024))
  // 计算文件的 MD5
  fileMd5.value = await calculateFileMd5(file.value)
  sliceFile()
}
// 将文件切片
const sliceFile = () => {
  // 用一个数组保存,一个文件切出来的总数
  chunkCountList.value.push(totalChunks.value)
  for (let i = 0; i < totalChunks.value; i++) {
    const size = props.chunkSize * 1024 * 1024
    const chunk = file.value.slice(i * size, (i + 1) * size) // 获取切片
    requestPool.value.push({ chunk, index: i }) // 加入请求池
  }
  timeStamp = new Date().getTime()
  processPool() // 处理请求池
}
// 处理请求池中的切片上传
const processPool = () => {
  while (requestPool.value.length > 0 && MAX_REQUEST > 0 && !isAbortRequest) {
    // 取出一个切片
    const { chunk, index } = requestPool.value.shift()
    // 接口切片
    uploadChunk(chunk, index)
      .then(() => {
        if (requestPool.value.length > 0) {
          processPool() // 继续处理请求池
        }
      })
      .finally(() => {
        MAX_REQUEST++ // 释放一个请求槽
      })
    MAX_REQUEST-- // 占用一个请求槽
  }
}
// 上传分片
const uploadChunk = async (chunk, chunkIndex, retries = 0) => {
  const formData = new FormData()
  formData.append('fileName', fileName.value)
  formData.append('fileMD5', fileMd5.value)
  formData.append('chunkIndex', chunkIndex)
  formData.append('totalIndex', totalChunks.value)
  formData.append('file', chunk)
  formData.append('type', `.${fileType.value}`)
  formData.append('timeStamp', timeStamp)
  try {
    await axios
      .post(props.actionURL, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: 'Bearer ' + getToken(),
          Clientid: getUserAgentInfo()
        }
      })
      .then(res => {
        uploadedChunks.value.push(chunkIndex)
        if (res.data.data.status === 3) {
          console.log('上传完成:')
          httpRequestParams.onSuccess(res.data)
          percentage.value = 100
          fileStatus.value = 'success'
          emit('upload-success', res.data.data) // 通知父组件上传成功
        }
        if (res.data.data.status === 2) {
          const percent = (uploadedChunks.value.length / totalChunks.value) * 100
          percentage.value = Math.round(percent)
          fileStatus.value = 'loading'
        }
        if (res.data.data.status === -2) {
          // 接口出现错误-2
          console.log('后端合并失败-2:', res.data)
          isAbortRequest = true
          requestPool.value.length = 0
          MAX_REQUEST = props.MAX_REQUEST
          fileStatus.value = 'fail'
          proxy.$refs.uploadRef.onError(new Error(`后端合并失败-2!`))
          httpRequestParams.onError(new Error(`后端合并失败-2!`))
          proxy.$refs.uploadRef.abort()
          throw new Error(`后端合并失败-2!`)
        }
        if (res.data.code === 510) {
          // 后端限流
          console.log('后端限流:', res.data)
          isAbortRequest = true
          requestPool.value.length = 0
          props.MAX_REQUEST
          fileStatus.value = 'fail'
          httpRequestParams.onError()
          proxy.$refs.uploadRef.abort()
          throw new Error(`后端限流!`)
        }
      })
  } catch (error) {
    // 失败重试
    // console.log('失败重试:', error)
    // if (retries < props.maxRetries) {
    //   console.warn(`分片 ${chunkIndex} 上传失败,正在重试 (${retries + 1}/${props.maxRetries})`)
    //   await uploadChunk(chunk, chunkIndex, retries + 1) // 重试
    // } else {
    isAbortRequest = true
    requestPool.value.length = 0
    MAX_REQUEST = props.MAX_REQUEST
    fileStatus.value = 'fail'
    proxy.$refs.uploadRef.abort()
    httpRequestParams.onError()
    throw new Error(`分片 ${chunkIndex} 上传失败,重试次数用尽!`)
    // }
  }
}
// 文件列表移除文件
const handleRemove = file => {
  if (props.fileLimit === 1) {
    proxy.$refs['uploadRef'].clearFiles()
    fileList.value = []
    emit('upload-remove')
  }
}
// 上传失败,重试
const handleReTry = file => {
  percentage.value = 0
  uploadedChunks.value = []
  // 自动触发上传
  startUpload()
}
// 文件超出个数限制
const handleExceed = () => {
  proxy.$modal.msgError(`上传文件数量不能超过 ${props.fileLimit} 个!`)
}
// 文件上传失败
const handleError = () => {
  percentage.value = 0
  fileStatus.value = 'fail'
}
// 文件上传进度
const handleUpload = async parms => {
  console.log(parms)
  httpRequestParams = parms
  // 文件列表
  fileList.value = [
    {
      name: fileName.value
    }
  ]
  console.log(fileList.value, "fileList.value");
  // 自动触发上传
  startUpload()
}
watch(
  () => props.fileListArr,
  newVal => {
    if (newVal && newVal.length > 0) {
      fileList.value = newVal
      fileStatus.value = 'success'
    }
  },
  { immediate: true }
)
</script>

<style lang="scss" scoped>
.BigFileUpload {
  width: 100%;
  min-width: 440px;
  :deep .el-upload-dragger {
    height: 132px;
    padding: 0;
    border-radius: 4px 4px 4px 4px;
    border: 1px dashed #2e75ff;
  }
  .BigFileUpload-text {
    margin-top: 20px;
  }
  .el-upload__img {
    width: 54px;
    height: 54px;
  }
  .el-upload__tip {
    margin-top: 0;
    font-family:
      PingFang SC,
      PingFang SC;
    font-weight: 400;
    font-size: 12px;
    color: #141d39;
    line-height: 24px;
    text-align: left;
    font-style: normal;
    text-transform: none;
  }
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2310796.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

正则表达式梳理(基于python)

正则表达式&#xff08;regular expression&#xff09;是一种针对字符串匹配查找所定义的规则模式&#xff0c;独立于语言&#xff0c;但不同语言在实现上也会存在一些细微差别&#xff0c;下面基于python对常用的相关内容进行梳理。 文章目录 一、通用常识1.通配符ps.反义 2.…

【仿muduo库one thread one loop式并发服务器实现】

文章目录 一、项目介绍1-1、项目总体简介1-2、项目开发环境1-3、项目核心技术1-4、项目开发流程1-5、项目如何使用 二、框架设计2-1、功能模块划分2-1-1、SERVER模块2-1-2、协议模块 2-2、项目蓝图2-2-1、整体图2-2-2、模块关系图2-2-2-1、Connection 模块关系图2-2-2-2、Accep…

服务流程设计和服务或端口重定向及其websocket等应用示例

服务流程设计和服务或端口重定向及其websocket等应用示例 目录 服务或端口重定向的服务设计和websocket等应用示例 一、通用请求控制流程 1.1、入口 1.2、所有GET请求首先预检控制单元 1.3、http请求会分别自动307重定向 1.4、所有请求首先执行跨源控制单元 1.5、然后…

【数据库】关系代数

关系代数 一、关系代数的概念二、关系代数的运算2.1 并、差、交2.2 投影、选择2.3 笛卡尔积2.4 连接2.5 重命名2.6 优先级 一、关系代数的概念 关系代数是一种抽象的数据查询语言用对关系的运算来表达查询 运算对象&#xff1a;关系运算符&#xff1a;4类运算结果&#xff1a;…

ubuntu20 安装python2

1. 确保启用了 Universe 仓库 在某些情况下&#xff0c;python2-minimal 包可能位于 Universe 仓库中。你可以通过以下命令启用 Universe 仓库并更新软件包列表&#xff1a; bash复制 sudo add-apt-repository universe sudo apt update 然后尝试安装&#xff1a; bash复制…

MySQL无法连接到本地localhost的解决办法2024.11.8

问题描述&#xff1a;我的MySQL可以远程连接服务器&#xff0c;但无法连接自己的localhost。 错误提示&#xff1a; 2003 - Cant connet to MySQL server on localhost(10061 "Unknown error")查找问题原因&#xff1a; 1. 检查环境变量是否正确&#xff1a;发现没…

最新Spring Security实战教程(一)初识Spring Security安全框架

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…

告别GitHub连不上!一分钟快速访问方案

一、当GitHub抽风时&#xff0c;你是否也这样崩溃过&#xff1f; &#x1f621; npm install卡在node-sass半小时不动&#x1f62d; git clone到90%突然fatal: early EOF&#x1f92c; 改了半天hosts文件&#xff0c;第二天又失效了... 根本原因&#xff1a;传统代理需要复杂…

Leetcode 1477. 找两个和为目标值且不重叠的子数组 前缀和+DP

原题链接&#xff1a; Leetcode 1477. 找两个和为目标值且不重叠的子数组 class Solution { public:int minSumOfLengths(vector<int>& arr, int target) {int narr.size();int sum0;int maxnINT_MAX;vector<int> dp(n,maxn);//dp[i]表示以索引i之前的满足要求…

R语言绘图:韦恩图

韦恩分析 韦恩分析&#xff08;Venn Analysis&#xff09;常用于可视化不同数据集之间的交集和并集。维恩图&#xff08;Venn diagram&#xff09;&#xff0c;也叫文氏图、温氏图、韦恩图、范氏图&#xff0c;用于显示元素集合重叠区域的关系型图表&#xff0c;通过图形与图形…

智谱AI-FunctionCall

智谱AI-FunctionCall 编写FuncationCall大模型的函数调用&#xff0c;先直观的感受一下的感受下FunctionCall的魅力 文章目录 智谱AI-FunctionCall[toc]1-参考网址2-思路整理3-代码拆件1-[非核心]两个业务函数2-[非核心]业务函数的JsonSchema定义3-[核心]FunctionCall的调用1-打…

android亮灭屏流程分析

前言 亮灭涉及的东西非常多&#xff0c;因此单独写一个文档&#xff0c;进行详细说明&#xff0c;亮灭屏包括的东西不只是亮灭屏&#xff0c;还包括亮度调节、屏幕状态变化等东西。本文仅作学习使用&#xff0c;不涉及商业&#xff0c;侵权请联系删除。 framework层的学习链接…

Docker Desktop常见问题记录

1.docker pull报错&#xff0c;无法连接https://registry-1.docker.io/v2/ 报错信息如下&#xff1a; Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection(Client.Timeout exceeded …

vscode+vue前端开发环境配置

目录 一、安装Vue二、使用vue新建项目 一、安装Vue 在node.js安装好之后&#xff0c; npm config set registry https://registry.npmmirror.com# 安装vue相关工具&#xff0c;webpack用来项目构建、打包、资源整合等。 npm install webpack -g# 安装vue-cli脚手架 npm insta…

Hive-08之数据仓库之建模、分析

一、目标 掌握数据仓库基本概念熟悉数据仓库的模型建立 二、知识要点 1. 数据仓库基本介绍 英文名称为Data Warehouse&#xff0c;可简写为DW或DWH。数据仓库的目的是构建面向分析的集成化数据环境&#xff0c;为企业提供决策支持&#xff08;Decision Support&#xff09;…

仿12306项目(4)

基本预定车票功能的开发 对于乘客购票来说&#xff0c;需要有每一个车次的余票信息&#xff0c;展示给乘客&#xff0c;供乘客选择&#xff0c;因此首个功能是余票的初始化&#xff0c;之后是余票查询&#xff0c;这两个都是控台端。对于会员端的购票&#xff0c;需要有余票查询…

MySQL零基础教程16—表连接进阶

复习表别名 之前已经学习过&#xff0c;查询的时候可以使用as来对检索的列进行重命名&#xff0c;这样可以让sql更加简介&#xff0c;增强易读性&#xff08;as可以省略&#xff09; 此外&#xff0c;使用表别名还可以支持在一条select语句中&#xff0c;一个表是被多次使用 …

【JavaSE-3】运算符

1、什么是运算符 就是对常量或者变量进行操作的符号&#xff0c;如&#xff1a;&#xff0c;-&#xff0c;*&#xff0c;/ 表达式&#xff1a; 用运算符把常量或者变量连接起来的&#xff0c;符合java语法的式子就是表达式。 2、 算术运算符 2.1、基本四则运算符 - * / % 都…

直接法估计相机位姿

引入 在前面的文章&#xff1a;运动跟踪——Lucas-Kanade光流中&#xff0c;我们了解到特征点法存在一些缺陷&#xff0c;并且用光流法追踪像素点的运动来替代特征点法进行特征点匹配的过程来解决这些缺陷。而这篇文章要介绍的直接法则是通过计算特征点在下一时刻图像中的位置…

VS2022C#windows窗体应用程序调用DeepSeek API

目录 一、创建DeepSeek API Key 二、创建窗体应用程序 三、设计窗体 1、控件拖放布局‌‌ 2、主窗体【Form1】设计 3、多行文本框【tbContent】 4、提交按钮【btnSubmit】 5、单行文字框 四、撰写程序 五、完整代码 六、运行效果 七、其它 一、创建DeepSeek API Ke…