vue 版本 v3.2.26
fileSpliceUpload.js
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import {
isExistAssignmentFile,
initFileData,
uploadMaxFile,
mergeSliceFile,
} from '@/api/common.js'
import lodash from 'lodash'
import { ElLoading } from 'element-plus'
// 文件名称
let fileName = ''
// 分片总数
let total = ''
// 总文件md5值
let contentHash = ''
let worker = ''
let hashPercentage = ''
// 记录当前已成功上传的数量
let successNum = 0
// 分片的大小
const size = 1024 * 1024 * 5
// 用来记录当前上传的进度(父级传过来的)
let progressCofig = ''
// 是否需要切片
let isSlice = false
// 文件上传的参数
let fileParam = {}
// 切片上传失败的请求集合
const faildRequestList = []
// 文件切片的数据
const uploadOption = ref([])
// 获取响应数据
function getResData(res) {
if (!res) return
return lodash.get(res, 'data.data')
}
// 分片上传后,发送分片合并请求
async function mergeRequest() {
const { businessId, md5, totalIndex } = fileParam
return new Promise((res, rej) => {
return mergeSliceFile({
businessId,
md5,
totalIndex,
originalFilename: fileName,
})
.catch(() => {
rej()
progressCofig.show = false
// ElMessage.error('上传失败!')
})
.then(() => {
res()
})
})
}
/**
* 生成文件切片
* @param {*} selectedFile
*/
function createFileChunk(selectedFile) {
successNum = 0
const fileChunkList = []
let cur = 0
while (cur < selectedFile.size) {
fileChunkList.push({
file: selectedFile.slice(cur, cur + size),
})
cur += size
}
return fileChunkList
}
// 计算md5
function calculateHash(fileChunkList) {
return new Promise(resolve => {
// 添加 worker 属性
worker = new Worker('/hash.js')
worker.postMessage({ fileChunkList })
worker.onmessage = e => {
const { percentage, hash } = e.data
hashPercentage = percentage
progressCofig.setAnProcentage(parseInt(hashPercentage))
// console.log('hashPercentage', hashPercentage)
if (hash) {
resolve(hash)
}
}
})
}
/**
* 网络请求并发控制
* @param {*} requestList 请求列表
* @param {*} max 同时请求最大数
* @returns
*/
function sendRequest(requestList, max = 6) {
return new Promise(resolve => {
const len = requestList.length
let idx = 0
let counter = 0
const start = async () => {
// 有请求,有通道
while (idx < len && max > 0) {
max-- // 占用通道
// console.log(idx, 'start')
const formData = requestList[idx].formData
// const index = requestList[idx].index
const cur = idx
idx++
uploadMaxFile(formData)
.then(res => {
// 页面上的进度条处理
successNum++
progressCofig.setPercentage(
parseInt((successNum / requestList.length).toFixed(2) * 100),
)
})
.catch(err => {
// progressCofig.show = false
// idx = len
// ElMessage.error('上传失败!')
const curObj = requestList[cur]
// 这是第一次上传的,肯定是没有的,就直接push了
faildRequestList.push({
formData: curObj.formData,
...curObj.param,
num: 0,
})
})
.finally(() => {
// 网络请求并发控制
// 释放通道
max++
counter++
if (counter === len) {
resolve()
} else {
start()
}
})
}
}
start()
})
}
/**
* 判断当前文件是否已上传
* @param {*} contentHash md5的值
* @returns
*/
async function isExist(contentHash) {
return getResData(await isExistAssignmentFile(contentHash))
}
/**
* 处理上传的参数
* @param {*} uploadParam 参数
*/
async function handleUploadParam(selectedFile, uploadParam, loading) {
const fileChunkList = createFileChunk(selectedFile, uploadParam)
contentHash = await calculateHash(fileChunkList)
fileParam.md5 = contentHash
fileParam.totalIndex = fileChunkList.length
const isUploaded = await isExist({
md5: contentHash,
businessId: uploadParam.businessId,
fileType: uploadParam.fileType,
module: uploadParam.module,
})
// 如果已经上传了,直接返回
if (isUploaded) {
ElMessage.warning('当前文件已存在!')
progressCofig.show = false
return {
flag: false,
requestList: [],
}
}
try {
// 初始化
await initFileData({
md5: contentHash,
totalSlice: fileChunkList.length, // 总分片个数
isSlice: isSlice ? '1' : '0', //是否分片 0:不分片 1:分片
originalFileName: fileName,
fileSize: selectedFile.size,
businessId: uploadParam.businessId,
recordingDuration: uploadParam.recordingDuration,
fileType: uploadParam.fileType,
module: uploadParam.module,
})
} catch (error) {
loading && loading.close()
return {
flag: false,
requestList: [],
}
}
// 文件切片的数据
uploadOption.value = fileChunkList.map(({ file }, index) => ({
chunk: file,
hash: fileName + '-' + index,
percentage: 0,
index,
size: '',
}))
const requestList = fileChunkList.map(({ file }, index) => {
const formData = new FormData()
formData.append('md5', contentHash)
formData.append('sliceIndex', index)
formData.append('totalIndex', total)
formData.append('file', file)
formData.append('originalFilename', fileName)
formData.append('businessId', uploadParam.businessId)
formData.append('isSlice', isSlice ? '1' : '0')
return {
formData,
param: {
md5: contentHash,
sliceIndex: index,
totalIndex: total,
originalFilename: fileName,
businessId: uploadParam.businessId,
file,
isSlice: isSlice ? '1' : '0',
},
}
})
return {
flag: true,
requestList,
}
}
/**
* 循环递归失败的请求
* @param {*} faildList 失败请求的list
* @returns
*/
async function recursionFaild(faildList) {
const requestFaildList = faildList.map((item, index) => {
return uploadMaxFile(item.formData, 'retryRequest')
.then(res => {
faildList = faildList.filter(it => it.sliceIndex !== item.sliceIndex)
// 页面上的进度条处理
successNum++
progressCofig.setPercentage(
parseInt((successNum / requestList.length).toFixed(2) * 100),
)
})
.catch(err => {
const curObj = faildList[index] || { num: 0 }
// console.log('curObjcurObjcurObjcurObj', curObj)
// const obj = faildList.find(it => it.sliceIndex === curObj.sliceIndex)
curObj.num++
// 如果有接口调用了两次都不成功,就直接失败了
if (curObj.num >= 2) {
progressCofig.show = false
faildList.length = 0
ElMessage.error('上传失败!')
}
})
})
console.log('recursionFaild', requestFaildList)
await Promise.all(requestFaildList)
if (faildList.length) {
recursionFaild(faildList)
} else {
return true
}
}
// 小于5M直接上传,不用现实弹框等信息
async function uploadSingleFile(selectedFile, param) {
const loading = ElLoading.service({
lock: true,
text: '',
background: 'rgba(0, 0, 0, 0.7)',
})
const { flag, requestList } = await handleUploadParam(
selectedFile,
param,
loading,
)
if (!flag) return
const formData = requestList[0].formData
await uploadMaxFile(formData)
.then(() => {
ElMessage.success('上传成功!')
})
.catch(() => {
ElMessage.error('上传失败!')
})
.finally(() => {
loading.close()
})
}
/**
* 上传附件
* @param {*} selectedFile 选中的文件
* @param {*} param 上传参数
* @param {*} cofig 上传进度的配置
* @returns
*/
export async function handleFileSpliceUpload(selectedFile, param, cofig) {
if (selectedFile) {
progressCofig = cofig
fileParam = param
fileName = param.fileName || selectedFile.name
total = Math.ceil(selectedFile.size / size)
isSlice = selectedFile.size > 1024 * 1024 * 5
if (selectedFile.size > 1024 * 1024 * 1024 * 1.7) {
ElMessage.warning('最大仅可上传1.7G文件!')
return false
}
// 小于5M直接上传,不用现实弹框等信息
if (selectedFile.size <= 1024 * 1024 * 5) {
await uploadSingleFile(selectedFile, param)
return false
}
progressCofig.setPercentage(0)
progressCofig.setAnProcentage(0)
progressCofig.show = true
const { flag, requestList } = await handleUploadParam(selectedFile, param)
if (!flag) return false
// 并发请求
// await Promise.all(requestList)
await sendRequest(requestList)
let faildFlag = faildRequestList.length === 0
if (faildRequestList.length) {
faildFlag = await recursionFaild(faildRequestList)
}
if (faildFlag) {
// 合并切片
isSlice && (await mergeRequest())
progressCofig.show = false
ElMessage.success('上传成功!')
return true
}
}
}
export default handleFileSpliceUpload
hash.js
// 导入脚本
self.importScripts('/spark-md5.min.js')
// 生成文件 hash
self.onmessage = e => {
const { fileChunkList } = e.data
const spark = new self.SparkMD5.ArrayBuffer()
let percentage = 0
let count = 0
const loadNext = index => {
const reader = new FileReader()
reader.readAsArrayBuffer(fileChunkList[index].file)
reader.onload = e => {
count++
spark.append(e.target.result)
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end(),
})
self.close()
} else {
percentage += 100 / fileChunkList.length
self.postMessage({
percentage,
})
// calculate recursively
loadNext(count)
}
}
}
loadNext(0)
}