背景:vue3 + typescript + elementplus 开发的后台管理系统,由于多个页面需要上传图片/视频/音频,所以将上传逻辑写成一个公共组件,如果上传文件超过5M,则需要分片上传,从0开始分片,前端传递给后台接口参数chunk 0表示第一片,1表示第二片,2表示第三片。。。依次类推,如果文件没有超过5M,则不分片。
️效果:页面点击上传按钮(可自定义传入上传样式)调起电脑的文件选择,选中文件后直接上传后台服务器,页面展示上传进度条。
代码实现:
<template>
<div class="upload">
<el-upload
ref="uploadRefs"
:action="action"
:multiple="multiple"
:limit="limit"
:auto-upload="false"
:show-file-list="false"
:on-change="onUpload"
:accept="getAccept"
>
<slot>slot>
el-upload>
<el-dialog
v-model="showProgress"
title="上传进度"
:close-on-click-modal="false"
width="500px"
:modal="false"
:show-close="false"
>
<div class="file-list p-4">
<div class="mb-5">
<div>{{ fileName }}div>
<div class="flex-1">
<el-progress
:percentage="uploadPercentage"
:format="format"
max="100"
>el-progress>
div>
div>
div>
el-dialog>
div>
template>
<script lang="ts">
// 使用axios进行上传接口
import axios from 'axios'
import { computed, defineComponent, ref, shallowRef } from 'vue'
// 反馈提示(根据实际项目进行修改)
import feedback from '@/utils/feedback'
import type { ElUpload } from 'element-plus'
// 获取上传图片、视频、音频token接口(根据实际项目情况进行修改)
import { getImgToken, getVodToken, getAudioToken } from '@/api/mediaResources'
export default defineComponent({
props: {
// 上传文件类型,不传默认图片类型,传递'video'只选择视频文件,传递'audio'只选择音频文件
type: {
type: String,
default: 'image'
},
// 是否支持多选
multiple: {
type: Boolean,
default: true
},
// 多选时最多选择几条
limit: {
type: Number,
default: 10
},
// 是否分片上传
isChunk: {
type: Boolean,
default: true
}
},
emits: ['change', 'error'],
setup(props, { emit }) {
// 计算文件总分片数
// 两个入参,file:文件流,chunkSize:分片大小
function sliceFile(file: File, chunkSize: number): Blob[] {
const chunks: Blob[] = []
let start = 0
while (start < file.size) {
const chunk = file.slice(start, start + chunkSize)
chunks.push(chunk)
start += chunkSize
}
return chunks
}
// 文件名称,渲染进度弹窗时使用
const fileName = ref('')
// 格式化进度,使用百分比进行展示
const format = (percentage: any) => `${percentage}%`
const uploadRefs = shallowRef<InstanceType<typeof ElUpload>>()
// 上传接口
const action = 'https://up.rmt.omtech.cn/api/upload'
// 上传百分比
const uploadPercentage = ref(0)
// 分片大小,默认5M(可根据实际项目分片大小进行修改)
const chunkSize = 5 * 1024 * 1024
// 控制进度条弹窗显示与隐藏
const showProgress = ref(false)
// 上传事件
const onUpload = async (File: any) => {
// 不分片文件提示(根据实际项目进行修改)
if (!props.isChunk && File.size > 5 * 1024 * 1024) {
feedback.msgError('文件大小不能超过5MB!')
return
}
const chunks = sliceFile(File.raw, chunkSize)
// 总分片数
const totalChunks = chunks.length
// 文件从0开始分片
let uploadedChunks = 0
// 打开进度条弹窗
showProgress.value = true
// 设置文件名称
fileName.value = File.name
// 获取上传token(根据实际项目进行修改/删除)
const keyAndtoken = await (File.raw.type.includes('image')
? getImgToken
: File.raw.type.includes('video')
? getVodToken
: getAudioToken)({
name: File.name,
timestamp: File.raw.lastModified,
size: File.size,
chunk: 0,
chunks: totalChunks
})
// 定义上传完成后的文件数据,传递给父组件的数据
let responseFile = {}
// 循坏上传,一片上传完成后再进行下一片的上传
for (let i = 0; i < totalChunks; i++) {
try {
// 传递给后台上传接口的参数,form-data格式(根据实际项目进行修改)
const formData = new FormData()
// key
formData.append('key', keyAndtoken.key)
// token
formData.append('token', keyAndtoken.token)
// 文件名称
formData.append('name', File.name)
// 时间戳
formData.append('timestamp', File.raw.lastModified)
// 分片文件流
formData.append('file', chunks[i])
// 当前分片数
formData.append('chunk', i.toString())
// 总分片数
formData.append('chunks', totalChunks.toString())
const response = await axios.post(action, formData)
// 上传接口响应成功
if (response.status === 200) {
if (response.data.code === 0) {
// 上传成功后,进行下一片的上传
uploadedChunks++
// 上传进度条更新
uploadPercentage.value = Math.round(
(uploadedChunks / totalChunks) * 100
)
// 获取上传成功后接口返回的文件数据
responseFile = response.data.data
} else {
// 上传失败,页面提示
feedback.msgError(response.data.message)
// 上传失败,停止上传
break
}
} else {
// 上传失败,停止上传
console.error(`Failed to upload chunk ${i}`)
feedback.msgError(`上传第${i}片失败`)
break
}
} catch (error) {
// 上传失败,停止上传
console.error('Error uploading chunk:', error)
feedback.msgError('上传失败')
break
}
}
// 设置进度条百分比
uploadPercentage.value = 100
setTimeout(() => {
// 关闭进度条弹窗
showProgress.value = false
uploadPercentage.value = 0
// 将上传成功后的文件信息传递给父组件(根据实际项目需要的数据进行修改,博主这里传递了接口数据和文件大小)
emit('change', { responseFile: responseFile, size: File.size })
}, 500)
}
// 设置上传文件的格式
const getAccept = computed(() => {
switch (props.type) {
case 'image':
return '.jpg,.png,.gif,.jpeg,.ico,.bmp'
case 'video':
return '.wmv,.avi,.mov,.mp4,.flv,.rmvb'
case 'audio':
return '.mp3,.wav,.aac,.flac,.ogg'
default:
return '*'
}
})
return {
uploadRefs,
format,
fileName,
uploadPercentage,
action,
showProgress,
getAccept,
onUpload
}
}
})
</script>
<upload
type="image"
:show-progress="true"
@change="handleCommon"
><div class="upload-btn">图片div>
upload>
<upload
type="video"
:show-progress="true"
@change="handleCommon"
><div class="upload-btn">视频div>
upload>
<upload
type="audio"
:show-progress="true"
@change="handleCommon"
>
<div class="upload-btn">音频div>
upload>
// 获取文件上传后的接口数据
const handleCommon = (data) => {
console.log(data, '子组件传递过来的数据')
...
...
//进行页面渲染等操作
}
结语:整体分片上传组件是比较简单的,重点就在计算分片的函数,祝大家永不宕机,永无bug