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>