使用vue+elementui实现
<template>
<el-upload
class="upload-demo"
ref="upload"
:action="uploadUrl" // 文件上传接口地址
:headers="uploadHeaders" // 上传请求头部信息
:with-credentials="true"
:on-change="handleChange"
:file-list="fileList"
:data="uploadData"
multiple
:on-progress="handleUploadProgress"
:on-success="handleUploadSuccess"
:before-remove="handleFileRemove"
@dragover.native.prevent // 防止浏览器默认行为
@drop.native="handleDrop" // 文件拖拽事件
>
<slot>
<div v-if="!showProgress">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</div>
<el-progress :percentage="uploadPercentage" v-if="showProgress"></el-progress>
</slot>
</el-upload>
</template>
<script>
export default {
name: 'BigFileUpload',
props: {
uploadUrl: String, // 文件上传接口地址
uploadHeaders: Object, // 上传请求头部信息
uploadData: Object, // 上传附带数据
chunkSize: Number, // 分块大小(字节)
maxRetryTimes: Number, // 最大重试次数
retryInterval: Number, // 重试间隔时间(毫秒)
showSlot: Boolean // 是否显示插槽
},
data() {
return {
fileList: [], // 待上传的文件列表
showProgress: false, // 是否显示上传进度条
uploadPercentage: 0, // 上传进度百分比
uploadInterrupted: false, // 上传是否被中断
uploadedChunks: [] // 已经成功上传的块列表
}
},
methods: {
handleChange(file, fileList) {
if (file.raw) { // 判断是否通过拖拽上传
const totalSize = file.size
const chunks = Math.ceil(totalSize / this.chunkSize) // 计算文件需要分成多少个块上传
let currentChunk = 0 // 当前正在上传的块编号
let start = 0
let end = 0
const reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = () => { // 当文件读取完成后开始逐个上传文件块
const buffer = reader.result
this.showProgress = true
this.uploadInterrupted = false // 初始化上传状态
const uploadNextChunk = () => {
if (currentChunk === chunks) { // 如果所有块都已上传完成,清除进度条并重置已上传块列表
this.showProgress = false
this.uploadedChunks = []
return
}
if (this.uploadInterrupted) { // 如果上传被中断,停止上传
return
}
if (this.uploadedChunks.includes(currentChunk)) { // 如果当前块已上传过,跳过该块并继续上传下一个块
currentChunk++
this.uploadPercentage = Math.round(currentChunk * 100 / chunks)
uploadNextChunk()
return
}
start = currentChunk * this.chunkSize // 计算当前块在文件中的起始和结束位置
end = Math.min(start + this.chunkSize, totalSize)
const formData = new FormData() // 创建表单对象,添加文件块和相关信息
formData.append('chunkNumber', currentChunk)
formData.append('totalChunks', chunks)
formData.append('file', new Blob([buffer.slice(start, end)], { type: file.type }))
this.$axios.post(this.uploadUrl, formData, { headers: this.uploadHeaders }) // 发送文件块到服务器
.then(() => { // 如果上传成功,更新进度条并记录已上传块号
currentChunk++
this.uploadPercentage = Math.round(currentChunk * 100 / chunks)
this.uploadedChunks.push(currentChunk - 1)
uploadNextChunk()
})
.catch(error => { // 如果上传失败,记录错误并尝试重新上传该块
console.error(error)
setTimeout(() => { this.retryUpload(formData, currentChunk, chunks)
}, this.retryInterval)
})
}
uploadNextChunk()
}
} else { // 通过点击上传按钮上传,直接将文件添加到待上传列表并等待用户触发上传操作
this.fileList = fileList
}
},
handleDrop(event) {
const fileList = event.dataTransfer.files // 获取拖拽文件列表
for (let i = 0; i < fileList.length; i++) { // 将文件添加到待上传列表中
const file = fileList[i]
if (!this.fileList.find(item => item.name === file.name)) {
this.fileList.push(file)
}
}
},
handleUploadProgress(event, file, fileList) { // 更新上传进度条
this.uploadPercentage = Math.round(event.percent)
},
handleUploadSuccess(response, file, fileList) { // 处理上传成功事件
// 可以在此处进行一些操作,例如显示上传成功提示等
},
handleFileRemove(file, fileList) { // 处理文件移除事件
if (this.showProgress && !this.uploadInterrupted) { // 如果上传正在进行中且未完成,将 uploadInterrupted 状态设为 true 阻止文件移除
this.uploadInterrupted = true
return false
}
return true
},
retryUpload(formData, currentChunk, chunks) { // 重试上传方法
if (this.maxRetryTimes > 0) {
this.maxRetryTimes--
this.$axios.post(this.uploadUrl, formData, { headers: this.uploadHeaders }) // 发送文件块到服务器
.then(() => { // 如果上传成功,更新进度条并记录已上传块号,并重置最大重试次数
currentChunk++
this.uploadPercentage = Math.round(currentChunk * 100 / chunks)
this.uploadedChunks.push(currentChunk - 1)
this.maxRetryTimes = this.props.maxRetryTimes
this.retryUpload(formData, currentChunk, chunks)
})
.catch(error => { // 如果上传失败,继续尝试重新上传该块
console.error(error)
setTimeout(() => {
this.retryUpload(formData, currentChunk, chunks)
}, this.retryInterval)
})
}
}
},
render() {
return (
<el-upload
class="upload-demo"
ref="upload"
action={this.uploadUrl}
headers={this.uploadHeaders}
with-credentials
onChange={this.handleChange}
fileList={this.fileList}
data={this.uploadData}
multiple
on-progress={this.handleUploadProgress}
on-success={this.handleUploadSuccess}
before-remove={this.handleFileRemove}
onDragover={e => e.preventDefault()}
onDrop={this.handleDrop}
>
{ this.showSlot ? this.$slots.default : (
<div v-if={!this.showProgress}>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</div>
<el-progress :percentage="uploadPercentage" v-if="showProgress"></el-progress>
)}
</el-upload>
)
}
}
</script>
在父组件中使用
<template>
<div>
<big-file-upload
upload-url="/api/upload"
:chunk-size="1024 * 1024 * 2"
:max-retry-times="5"
:retry-interval="3000"
:upload-headers="{ Authorization: 'Bearer ' + token }"
:upload-data="{ userId: userId }"
@upload-success="handleUploadSuccess"
>
<div class="upload-slot">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</div>
</big-file-upload>
</div>
</template>
<script>
import BigFileUpload from '@/components/BigFileUpload'
export default {
components: { BigFileUpload },
data() {
return {
token: 'xxxxx',
userId: '123',
uploadedFiles: []
}
},
methods: {
handleUploadSuccess(response, file, fileList) {
// 处理上传成功事件,将已上传文件添加到 uploadedFiles 数组中
this.uploadedFiles.push(file)
}
}
}
</script>