// 切片上传相关配置
const chunkSize = ref(2 * 1024 * 1024) // 切片大小 2MB
const uploadProgress = ref<Record<string, number>>({}) // 上传进度
const uploadAbort = ref<Record<string, AbortController>>({}) // 取消控制器
// 生成文件唯一标识
const generateFileId = (file: File): string => {
return `${file.name}-${file.size}-${file.lastModified}`
}
// 计算文件哈希(简化版,使用文件名+大小+时间戳)
const getFileHash = (file: File): string => {
return `${file.name}-${file.size}-${file.lastModified}`
}
// 切片上传主函数
/**
* 视频切片上传主函数
* @param file - 要上传的视频文件对象
*/
const uploadVideoChunk = async (file: File) => {
// 文件唯一标识(用于追踪上传进度)
const fileId = generateFileId(file)
// 文件哈希值(用于断点续传,判断哪些切片已上传)
const fileHash = getFileHash(file)
// 总切片数 = 向上取整(文件大小 / 切片大小)
const totalChunks = Math.ceil(file.size / chunkSize.value)
// 存储所有切片上传的Promise,用于并行上传
const chunkPromises: Promise<void>[] = []
// 初始化上传进度为0%
uploadProgress.value[fileId] = 0
// 获取视频时长(秒)
const duration = (await getVideoLengthAcsyn(file)) as number
// 格式化时长为 HH:MM:SS 格式
const durationStr = formatDuration(duration)
// 预检查阶段:询问后端哪些切片已上传(用于断点续传)
try {
// 调用预检查接口
const checkRes = await fetch(`${PATH_URL}/admin/file/chunk/check`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + yourToken, // 身份认证token
'Content-Type': 'application/json' // 请求体格式
},
body: JSON.stringify({
fileHash, // 文件哈希(用于标识文件)
fileName: file.name, // 原始文件名
totalChunks, // 总切片数
group_id: fileGroup.value.id, // 文件分组ID
module: 'video', // 文件类型标记
length: duration, // 视频时长(秒)
duration: durationStr // 视频时长(时分秒格式)
})
})
// 解析预检查响应
const checkData = await checkRes.json()
// 已上传的切片索引数组(后端返回),默认为空数组
const uploadedChunks = checkData.data?.uploadedChunks || []
// 遍历所有切片,只上传未完成的切片
for (let i = 0; i < totalChunks; i++) {
// 如果该切片已上传,则跳过
if (uploadedChunks.includes(i)) continue
// 计算当前切片的起始位置
const start = i * chunkSize.value
// 计算当前切片的结束位置(最后一个切片可能小于chunkSize)
const end = Math.min(start + chunkSize.value, file.size)
// 从文件中切出当前切片
const chunk = file.slice(start, end)
// 添加到并行上传队列
chunkPromises.push(uploadChunk(chunk, i, totalChunks, fileHash, file.name, fileId))
}
// 等待所有切片上传完成
await Promise.all(chunkPromises)
// 切片上传完成后,调用合并接口
await mergeChunks(fileHash, file.name, totalChunks, duration, durationStr)
// 清理上传进度记录
delete uploadProgress.value[fileId]
// 提示上传成功
ElMessage.success(`${file.name} 上传成功`)
// 刷新文件列表
fileMangeRightListData(fileGroup.value.id)
} catch (error) {
// 上传失败时清理进度记录
delete uploadProgress.value[fileId]
// 提示上传失败
ElMessage.error(`${file.name} 上传失败: ${(error as Error).message}`)
}
}
// 上传单个切片
const uploadChunk = async (chunk: Blob, chunkIndex: number, totalChunks: number, fileHash: string, fileName: string, fileId: string) => {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', chunkIndex.toString())
formData.append('totalChunks', totalChunks.toString())
formData.append('fileHash', fileHash)
formData.append('fileName', fileName)
const controller = new AbortController()
uploadAbort.value[fileId] = controller
const res = await fetch(`${PATH_URL}/admin/file/chunk/upload`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + yourToken
},
body: formData,
signal: controller.signal
})
const result = await res.json()
if (result.code !== 0) {
throw new Error(result.msg || '切片上传失败')
}
// 更新进度
uploadProgress.value[fileId] = ((chunkIndex + 1) / totalChunks) * 100
}
// 合并切片
const mergeChunks = async (fileHash: string, fileName: string, totalChunks: number, duration: number, durationStr: string) => {
const res = await fetch(`${PATH_URL}/admin/file/chunk/merge`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + yourToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileHash,
fileName,
totalChunks,
group_id: fileGroup.value.id,
module: 'video',
length: duration,
duration: durationStr
})
})
const result = await res.json()
if (result.code !== 0) {
throw new Error(result.msg || '切片合并失败')
}
}
// 取消上传
const cancelUpload = (fileId: string) => {
uploadAbort.value[fileId]?.abort()
delete uploadProgress.value[fileId]
delete uploadAbort.value[fileId]
}
// 自定义上传请求(视频切片上传)
const handleVideoUpload = async (options: { file: File }) => {
const { file } = options
// 验证文件类型
const videoTypes = ['video/mp4', 'video/mpeg', 'video/avi', 'video/flv', 'video/wmv', 'video/mov']
if (!videoTypes.includes(file.type)) {
ElMessage.error('只支持 mp4、mpeg、avi、flv、wmv、mov 格式的视频')
return
}
// 验证文件大小(可选,根据需求添加)
const maxSize = 100 * 1024 * 1024 // 100MB
if (file.size > maxSize) {
ElMessage.error('视频文件大小不能超过 100MB')
return
}
loading.value = true
await uploadVideoChunk(file)
loading.value = false
}