最近一直在处理文件的分批、分片、自动上传,分批、分片上传还好处理,说起自动上传这个功能,各种权限的判断,各种情况的考虑,搞得头大如斗,今天主要说一下分批、分片上传:
先看效果图:
一、基础属性
data() {
return {
totalSizeImage: 0, //所有文件size
uploadSizeImage: 0, //上传size
dataFileListImage: [], //文件列表
eachSize: 2 * 1024 * 1024, //文件切片尺寸
dialogLoading: false, //
dialogLoadText: '加载中···',
page: {
size: 10,
current: 1,
total: 0,
},
dataTable: [], //飞行图片分页显示数据
maxUploadCount: 3, //最大上传文件数量
}
},
二、文件选择
accept:支持文件类型;multiple:是否支持多选;webkitdirectory:是否选择文件夹,true 是选择文件夹,false或者去掉是选择文件;
onchange触发后,要清理掉添加的input节点;
md5的加密处理,这个根据项目自身情况来定,不一定非要用md5
文件分片:我这里是按照2MB进行分片处理的,把大文件分成多个小文件进行上传,记录下当前上传的片数,可以做到断点续传
handleChoiceFile() {
let fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'image/*'
fileInput.multiple = true
fileInput.webkitdirectory = true
fileInput.click()
fileInput.onchange = () => {
let temp_files = Array.from(fileInput.files)
this.uploadInputChange(temp_files)
fileInput.remove()
}
},
async uploadInputChange(files) {
try {
let tempFiles = files
//判断选择文件是否超过30G
let totalSize = 0
tempFiles.forEach((item) => {
totalSize += parseInt(item.fileSize)
})
if (totalSize > 30 * 1024 * 1024 * 1024) {
return this.$message.warning(
'图片总大小超过30GB,可以考虑分割!'
)
}
let temp_file_list = []
this.dialogLoading = true
this.dialogLoadText = '文件读取中···'
for (var i = 0; i < tempFiles.length; i++) {
let item = tempFiles[i]
// 这里可以对文件进行类型判断
this.dialogLoadText = `读取文件${i + 1}/${tempFiles.length}`
let chunks = Math.ceil(item.size / this.eachSize)
let params = {
file: item,
fileName: item.name, //文件名称
fileSize: item.size, //文件大小
status: '1', //上传状态
statusName: '待上传',
totalChunk: chunks, // 切片个数
chunkFile: [], // 切片文件
currentChunk: 0, //默认上传的分片索引
fileMd5: '', // md5,这里是文件第一片和最后一片相加的字符串,用于验证文件
relativePath: '', //文件夹内的相对路径
ratio: 0,
}
// 处理分片数据
for (let chunk = 0; chunks > 0; chunks--) {
let chunkFile = item.slice(chunk, chunk + this.eachSize)
params.chunkFile.push(chunkFile)
chunk += this.eachSize
}
// 计算md5
let first = await params.chunkFile[0].text()
let last = await params.chunkFile[params.chunkFile.length - 1].text()
params.fileMd5 = md5(first + last)
// 判断文件是否已经存在,如果存在则替换 chunkFile 和 file
if (this.dataFileListImage.length > 0) {
let temp_index = this.dataFileListImage.findIndex(
(o) => o.fileMd5 == params.fileMd5
)
if (temp_index > -1) {
this.dataFileListImage[temp_index].chunkFile = params.chunkFile
this.dataFileListImage[temp_index].file = params.file
// 4,5是暂停和失败状态,这里替换成待上传状态
if (
['4', '5'].includes(this.dataFileListImage[temp_index].status)
) {
this.dataFileListImage[temp_index].status = params.status
this.dataFileListImage[temp_index].statusName =
params.statusName
}
} else {
//未找到对应文件的,把文件添加到数组中
this.dataFileListImage.push(params)
//添加的临时数组中,用于初始化
temp_file_list.push(params)
}
} else {
this.dataFileListImage.push(params)
//添加的临时数组中,用于初始化
temp_file_list.push(params)
}
// 最后一个文件读取结束后,开始初始化
if (i == tempFiles.length - 1) {
if (temp_file_list.length > 0) {
this.dialogLoadText = '文件初始化中···'
this.dataFileListImage = temp_file_list
//文件选择后,直接初始化
let obj = {
files: temp_file_list.map((v) => {
return {
fileMd5: v.fileMd5,
fileName: v.fileName,
fileSize: v.fileSize,
relativePath: v.relativePath,
totalChunk: v.totalChunk,
}
}),
}
// 文件初始化方法,只初始化新增的文件
const { data } = await fileInit(obj).finally(() => {
this.dialogLoading = false
this.dialogLoadText = '加载中···'
})
if (data.code == 0) {
// 初始化完成,重新获取文件列表
await this.getFileListImage()
// 计算文件size
this.computeImageTotalSize()
// 对文件进行前端分页,文件太多,分页更合适
this.setTableData()
}
} else {
// 没有需要初始化的文件,这里重新分页
this.setTableData()
}
this.dialogLoading = false
this.dialogLoadText = '加载中···'
}
}
} catch (error) {
console.log(error)
this.dialogLoading = false
this.dialogLoadText = '加载中···'
}
},
计算进度这里说一下:当文件上传片数和总片数相等时,使用文件的原始尺寸,这里不能通过计算,否则结果是偏大的
获取文件列表:主要判断一下文件是否已经存在,已经存在的要替换一下file 和 chunkFile,file对应的是当前文件,chunkFile对应的是切片后的文件数组,在初始化的时候这两个字段是不进行初始化的
// 计算上传进度
computeImageTotalSize() {
let image_size = 0,
image_upload = 0
this.dataFileListImage.forEach((item) => {
image_size += parseInt(item.fileSize)
if (item.currentChunk >= item.totalChunk) {
image_upload += parseInt(item.fileSize)
} else {
image_upload += parseInt(item.currentChunk) * this.eachSize
}
})
this.totalSizeImage = image_size
this.uploadSizeImage = image_upload
},
// 获取文件列表
async getFileListImage() {
const { data } = await uploadList()
if (data.code == 0 && data.data) {
if (this.dataFileListImage.length == 0) {
this.dataFileListImage = data.data.map((item) => {
item.file = null
item.chunkFile = []
return item
})
} else {
this.dataFileListImage = data.data.map((item) => {
let find = this.dataFileListImage.find(
(o) => o.fileMd5 == item.fileMd5
)
if (find) {
item.file = find.file
item.chunkFile = find.chunkFile
}
return item
})
}
this.setTableData()
}
},
//计算分页
setTableData() {
this.page.total = this.dataFileListImage.length
this.dataTable = []
this.dataTable = this.dataFileListImage.slice(
this.page.size * (this.page.current - 1),
this.page.size * this.page.current
)
},
开始上传:判断文件是否存在,因为是多文件同时上传,判断当前上传中的文件数,以及需要上传的文件数,根据最大上传文件数计算得到可以上传的文件数,然后循环上传即可
切片上传:我这里对列表的处理都是在前端修改的,没有调用接口,调用接口重新获取列表会增加复杂度,也不好控制,所以这里直接在前端控制即可;每次上传一片后就要计算一下总进度,做到实时更新
//开始上传
async handleStartUpload() {
if (this.dataTable.length == 0)
return this.$message.warning('请选择文件')
//当前上传中的文件
let uploading_list = this.dataTable.filter((o) => o.status == '2')
//还未上传的文件
let notUpload_list = this.dataTable.filter((o) => o.status == '1')
//获取剩余可以上传的文件数
let left_upload_count = this.maxUploadCount - uploading_list.length
//需要上传的文件数
let need_upload_count = 0
// 未上传的文件大于0,继续上传
if (notUpload_list.length > 0) {
if (notUpload_list.length >= left_upload_count) {
need_upload_count = left_upload_count
} else {
need_upload_count = notUpload_list.length
}
for (var i = 0; i < need_upload_count; i++) {
let item = this.dataTable.find((o) => o.status == '1')
this.uploadChunkFile(item)
}
} else {
//判断是否还有正在上传中的文件,如果没有,则进行翻页
if (uploading_list.length == 0) {
//判断是否需要翻页
if (this.page.size * this.page.current < this.page.total) {
this.page.current += 1
this.setTableData()
this.handleStartUpload()
} else {
// 不需要翻页
//判断文件总数组是否全部上传完成
let index = this.dataFileListImage.findIndex(
(o) => o.status == '1'
)
// 发现未上传的文件,则继续上传
if (index > -1) {
this.page.current = Math.ceil(index / this.page.size)
this.setTableData()
this.handleStartUpload()
}
}
}
}
},
//切片上传
async uploadChunkFile(row, type) {
let chunkFile = row.chunkFile
row.status = '2'
row.statusName = '上传中'
try {
for (var i = 0; i < chunkFile.length; i++) {
if (i == row.currentChunk) {
let param = {
chunk: i,
chunks: row.totalChunk,
chunkSize: this.eachSize,
file: chunkFile[i],
fileName: row.fileName,
fullSize: row.fileSize,
md5: row.fileMd5,
}
let formData = new FormData()
for (let p in param) {
formData.append(p, param[p])
}
const { data } = await chunkUpload(formData).catch(() => {
row.currentChunk = row.currentChunk + 1
this.computeImageTotalSize()
row.status = '5'
row.statusName = '已失败'
})
if (data.code == 0) {
row.currentChunk = row.currentChunk + 1
}
this.computeImageTotalSize()
//如果已暂停,则直接终止上传
if (row.status == '4') {
break
}
}
if (row.currentChunk == row.totalChunk) {
row.status = '3'
row.statusName = '已完成'
this.computeImageTotalSize()
this.handleStartUpload()
}
}
} catch (err) {
console.log(err, 'err')
}
},