element plus中el-upload分片上传文件
el-upload分片上传文件
这里前端使用的是vue3+ts+element plus,后端是springboot2.5.6+jdk1.8
这个是element Plus的官网
Upload 上传 | Element Plus (gitee.io)
这里主要使用的了el-upload中的:on-change="handleChange"就可以实现了,如果需要对文件进行校验的话,看官网的:before-upload,官网给出的例子里面有如校验上传的文件是否是图片的方法和校验图片大小是否符合的方法。这里需要注意的是:auto-upload="false"设置为false的时候,上传成功的回调,on-success方法就不执行了,其实这里也不需要用到这个。详情看官网吧
上传过程
看一下请求的过程
前端代码
el-upload中自定义的change方法的参数,具体看下面的代码的handleChange方法,el-upload中,会为每个上传的文件设置一个唯一的uid,还有一个percentage属性,完整代码在截图的后面
前端代码如下所示
<template>
<!-- 注意这里的 action="#",和:auto-upload="false",如果:auto-upload="true",由于el-upload的change事件,上传和上传成功都会回调,这里
将其设置为false,不自动上传,不然会重复请求。 :show-file-list="true"是否展示文件列表,如果不需要就设置为false就可以了,
v-model:file-list="fileList"文件列表数组,具体看官网就行了-->
<el-upload class="upload-demo" action="#" drag multiple :auto-upload="false" :show-file-list="true" v-model:file-list="fileList"
:on-change="handleChange" :on-remove="handleRemove">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽文件到这里或者<em>点击上传</em>
</div>
<template #tip>
<!-- <div class="el-upload__tip">
jpg/png files with a size less than 500kb
</div> -->
</template>
</el-upload>
<el-row>
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column prop="name" label="名字"/>
<el-table-column prop="percentage" label="进度">
<template #default="scope">
<el-progress :text-inside="true" :stroke-width="26" :percentage="scope.row.percentage" />
</template>
</el-table-column>
<el-table-column prop="percentage" label="操作">
<template #default="scope">
<el-button type="primary" @click="handleRemoveUploadFileList(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row>
<!-- 这个要设置宽度,否则有可能显示不出来 -->
<!-- <el-progress style="width: 100%" :text-inside="true" :stroke-width="26" :percentage="percentage" /> -->
</el-row>
</template>
<script setup lang="ts">
import axios from 'axios'
import { UploadFilled } from '@element-plus/icons-vue'
import { type UploadProps, UploadFile, UploadFiles } from 'element-plus'
import { ref } from 'vue';
const tableData = ref<UploadFiles>([])
// 文件列表
const fileList = ref<UploadFiles>([])
// 分块大小8MB
const chunkSize = 1024*1024*8
// const percentage = ref(0)
/**
* 将分块文件上传至服务器
* @param file 上传的分块文件
* @param chunkNumber 当前是第几块
* @param chunkTotal 文件分块的总数
* @param fileName 文件名称
*/
const uploadFileToServer = async (file: any, chunkNumber: any, chunkTotal: any, fileName: any) => {
const form = new FormData();
// 这里的data是文件
form.append("file", file);
form.append("chunkNumber", chunkNumber);
form.append("chunkTotal", chunkTotal);
form.append("fileName", fileName)
const result = await axios.post("http://localhost:9090/uploadVideo/uploadVideo", form)
return result
}
/**
* 合并文件
* @param chunkTotal 文件分块的总数量
* @param fileName 文件名称
*/
const mergeFiles = async (chunkTotal: any, fileName: any) => {
// const form = new FormData();
// form.append("chunkTotal", chunkTotal);
// form.append("fileName", fileName)
// await axios.post("http://localhost:9090/uploadVideo/merge", form)
// 这两种方式都可以,毕竟后台是@RequestMapping
axios.get(`http://localhost:9090/uploadVideo/merge?chunkTotal=${chunkTotal}&fileName=${fileName}`)
}
/**
* 根据文件名删除文件
* @param fileName 文件名
*/
const deleteFileByFileName = async (fileName: any) => {
const result = await axios.get(`http://localhost:9090/uploadVideo/deleteByFileName?fileName=${fileName}`)
// 这个result.data这个不多说了,axios相关的,不懂的console.log(result)就知道了
console.log(result.data)
}
/**
* el-upload内置的change函数,文件上传或者上传成功时的回调,不过这里因为
* :auto-upload="false"的缘故,上床成功的回调不会执行
* @param uploadFile el-upload当前上传的文件对象
* @param uploadFiles el-upload上传的文件列表
*/
const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => {
// v-model:file-list="fileList" 就是这里的uploadFiles。这两个文件列表是一样的,多以在删除的时候,从fileList清除某一项,uploadFiles这里也会清除的
// 这个el-upload 已经实现了的
// console.log(uploadFile)
tableData.value.push({...uploadFile})
// tableData.value.push(uploadFile)
const index = tableData.value.findIndex(item => item.uid === uploadFile.uid)
let fileName = uploadFile.name
// if (uploadFile.name.lastIndexOf('.') !== -1) {
// fileName = uploadFile.name.substring(0, uploadFile.name.lastIndexOf('.'))
// }
const fileSize = uploadFile.size || 0
// 因为后面的 let chunkTotals = Math.ceil(fileSize / chunkSize); 使用了Math.ceil() 向上取整,所以不要这个分支也是可以的。
// if (fileSize <= chunkSize) {
// uploadFile.raw这个是element plus 中 el-upload的自己上传的文件就放在这个raw里面,可以console.log(uploadFile)看一下
// const result = await uploadFileToServer(uploadFile.raw, 1, 1, fileName)
// console.log(result)
// 合并文件
// await mergeFiles(1, fileName)
}
let chunkTotals = Math.ceil(fileSize / chunkSize);
if (chunkTotals > 0) {
for (let chunkNumber = 0, start = 0; chunkNumber < chunkTotals; chunkNumber++, start += chunkSize) {
let end = Math.min(fileSize, start + chunkSize);
// uploadFile.raw这个是element plus 中 el-upload的自己上传的文件就放在这个raw里面,可以console.log(uploadFile)看一下
// 加 ?是因为ts语法提示“uploadFile.raw”可能为“未定义”,加了这个就不过有报错了
const files = uploadFile.raw?.slice(start, end)
const result = await uploadFileToServer(files, chunkNumber+1, chunkTotals, fileName)
const percents = parseFloat(result.data.replace("%",''))
uploadFile.percentage = percents
tableData.value[index].percentage = percents
// percentage.value = percents
console.log(result.data)
}
await mergeFiles(chunkTotals, fileName)
}
console.log(uploadFiles)
}
/**
* 从el-upload封装的文件列表中删除文件
* // el-upload的(内置方法),点击el-upload默认的文件展示以列表的 X 那个图标时,会移除文件,并从文件列表中去掉
* @param uploadFile el-upload当前删除点击的文件
* @param uploadFiles el-upload的文件列表
*/
const handleRemove: UploadProps['onRemove'] = async (uploadFile, uploadFiles) => {
// 这个删除表格tableData中的列表数据
const index2 = tableData.value.findIndex((item2: UploadFile) => item2.uid === uploadFile.uid)
if (index2 !== -1) {
tableData.value.splice(index2,1)
}
await deleteFileByFileName(uploadFile.name)
// handleRemove内置的删除文件方法,
// 这里的uploadFiles跟绑定的v-model:file-list="fileList"可以看成是同一个数组,这两个数组的数据是一样的,从这两个数组中删除或者移除任何一个数据,
// 另外一个数据也会跟着变化的,这个element plus已经实现了
console.log(uploadFiles)
}
/**
* 自定义的表格删除的方法
* @param file 表格某一行的数据
*/
const handleRemoveUploadFileList = async (file: UploadFile) => {
const index = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid)
if (index !== -1) {
fileList.value.splice(index,1)
}
const index2 = tableData.value.findIndex((item2: UploadFile) => item2.uid === file.uid)
if (index2 !== -1) {
tableData.value.splice(index2,1)
}
// 删除磁盘中的文件
await deleteFileByFileName(file.name)
}
</script>
<style scoped>
</style>
后端Java代码
springboot中的application.yml配置,这样设置的原因是,springboot中默认允许上传的文件大小是1MB,上面的前端代码中分块的大小是8MB,所以这里需要设置比8MB大,这里设置了10MB
server:
port: 9090
spring:
servlet:
multipart:
max-file-size: 10MB
controller
@RestController
@RequestMapping("/uploadVideo")
// 允许跨域
@CrossOrigin
public class uploadVideo {
/**
* 文件分块上传
* @param chunkNumber 当前分块数
* @param chunkTotal 总分块数
* @param file 文件
* @param fileName 文件名
* @return
* @throws IOException
*/
@RequestMapping("/uploadVideo")
public String uploadVideo(int chunkNumber, int chunkTotal, MultipartFile file, String fileName) throws IOException {
file.transferTo(Paths.get("D:\\testuplosd\\" + fileName + ".part" + chunkNumber));
Double process = (chunkNumber * 1.0) / (chunkTotal * 1.0) * 100;
DecimalFormat df = new DecimalFormat("0.00");
String format = df.format(process) + "%";
return format;
}
/**
* 合并分块
* @param chunkTotal 总的分块数
* @param fileName 文件名称
*/
@RequestMapping("/merge")
public void mergeVideo(int chunkTotal, String fileName) {
// 这里注意了for循环在try里面
try (FileOutputStream os = new FileOutputStream("D:\\testuplosd\\" + fileName)) {
for (int i = 1; i <= chunkTotal; i++) {
// D:\testuplosd\这个文件路径,在程序中可以配置在springboot的配置文件yml中。这里就不配置了
Path part = Paths.get("D:\\testuplosd\\" + fileName + ".part" + i);
// 这个会自动关闭流的
Files.copy(part, os);
part.toFile().delete(); // 删除part文件
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 删除文件,防止不需要的文件还存在服务器中
* @param fileName 文件名称
* @return
*/
@RequestMapping("/deleteByFileName")
public String deletyByFileName(String fileName) {
// 这里不严谨,这个应该是通过文件的md5值和文件名一起判断来删除,会好一点,这里只是试着删除
Path part = Paths.get("D:\\testuplosd\\" + fileName);
part.toFile().delete();
return "删除文件成功";
}
}
如果只是分片上传视频
代码是在上面代码的基础上改的
完整代码在截图后面:
我们可以通过设置属性,让el-upload只能接受视频文件,如只能接受.mp4的文件,具体看官网Upload 上传 | Element Plus (gitee.io) 这里可以填多种类型如:accept=".mp4, .mp3, .jpg, .png"等,以逗号分隔开来,注意.mp4、.mp3、.jpg、.png这些前面都有一个英文输入状态下的(.)点。
这时我们就可以改造我们后端的合并方法,后端通过设置静态资源映射的方式,合并成功后返回可以访问视频的地址,然后赋值给对应的表格数组中的某一行的UploadFile对象的url,让其具有播放视频的路径。
这里是先把遇到的问题截图出来,完整代码在截图后面
ts报如下问题的时候,
可以在值后面给一个默认值,若
不能将类型“HTMLElement | null”分配给类型“HTMLVideoElement”。
不能将类型“null”分配给类型“HTMLVideoElement”。
解决办法如下
前端完整代码如下
<template>
<!-- 注意这里的 action="#",和:auto-upload="false",如果:auto-upload="true",由于el-upload的change事件,上传和上传成功都会回调,这里
将其设置为false,不自动上传,不然会重复请求。 :show-file-list="true"是否展示文件列表,如果不需要就设置为false就可以了,
v-model:file-list="fileList"文件列表数组,具体看官网就行了, accept=".mp4"接收的文件类型,这里只接受mp4,可以填多个如:accept=".mp4, mp3, .jpg" -->
<el-upload class="upload-demo" action="#" accept=".mp4" drag multiple :auto-upload="false" :show-file-list="true"
v-model:file-list="fileList" :on-change="handleChange" :on-remove="handleRemove">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽文件到这里或者<em>点击上传</em>
</div>
<template #tip>
<!-- <div class="el-upload__tip">
jpg/png files with a size less than 500kb
</div> -->
</template>
</el-upload>
<el-row>
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column prop="name" label="名字" />
<el-table-column prop="percentage" label="进度">
<template #default="scope">
<el-progress :text-inside="true" :stroke-width="26" :percentage="scope.row.percentage" />
</template>
</el-table-column>
<el-table-column prop="percentage" label="操作">
<template #default="scope">
<el-button type="primary" @click="handleRemoveUploadFileList(scope.row)">删除</el-button>
<el-button type="primary" @click="handlePreviewUploadFileList(scope.row)">预览</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row>
<!-- 这个要设置宽度,否则有可能显示不出来 -->
<!-- <el-progress style="width: 100%" :text-inside="true" :stroke-width="26" :percentage="percentage" /> -->
</el-row>
<el-dialog v-model="dialogTableVisible" title="预览视频" @close="handleClose">
<video v-if="dialogTableVisible" width="800" height="600" controls loop id="video-play">
<source :src="videoPath" type="video/mp4">
您的浏览器不支持 video 标签。
</video>
</el-dialog>
</template>
<script setup lang="ts">
import axios from 'axios'
import { UploadFilled } from '@element-plus/icons-vue'
import { type UploadProps, UploadFile, UploadFiles } from 'element-plus'
import { ref } from 'vue';
const tableData = ref<UploadFiles>([])
// 文件列表
const fileList = ref<UploadFiles>([])
// 分块大小8MB
const chunkSize = 1024 * 1024 * 8
const dialogTableVisible = ref(false)
const videoPath = ref('')
// const percentage = ref(0)
/**
* 将分块文件上传至服务器
* @param file 上传的分块文件
* @param chunkNumber 当前是第几块
* @param chunkTotal 文件分块的总数
* @param fileName 文件名称
*/
const uploadFileToServer = async (file: any, chunkNumber: any, chunkTotal: any, fileName: any) => {
const form = new FormData();
// 这里的data是文件
form.append("file", file);
form.append("chunkNumber", chunkNumber);
form.append("chunkTotal", chunkTotal);
form.append("fileName", fileName)
const result = await axios.post("http://localhost:9090/uploadVideo/uploadVideo", form)
return result
}
/**
* 合并文件
* @param chunkTotal 文件分块的总数量
* @param fileName 文件名称
*/
const mergeFiles = async (chunkTotal: any, fileName: any) => {
// const form = new FormData();
// form.append("chunkTotal", chunkTotal);
// form.append("fileName", fileName)
// await axios.post("http://localhost:9090/uploadVideo/merge", form)
// 这两种方式都可以,毕竟后台是@RequestMapping
const result = await axios.get(`http://localhost:9090/uploadVideo/merge?chunkTotal=${chunkTotal}&fileName=${fileName}`)
// 这个result.data是axios相关的了,这里就不说了
return result.data
}
/**
* 根据文件名删除文件
* @param fileName 文件名
*/
const deleteFileByFileName = async (fileName: any) => {
const result = await axios.get(`http://localhost:9090/uploadVideo/deleteByFileName?fileName=${fileName}`)
// 这个result.data这个不多说了,axios相关的,不懂的console.log(result)就知道了
console.log(result.data)
}
/**
* el-upload内置的change函数,文件上传或者上传成功时的回调,不过这里因为
* :auto-upload="false"的缘故,上床成功的回调不会执行
* @param uploadFile el-upload当前上传的文件对象
* @param uploadFiles el-upload上传的文件列表
*/
const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => {
// v-model:file-list="fileList" 就是这里的uploadFiles。这两个文件列表是一样的,多以在删除的时候,从fileList清除某一项,uploadFiles这里也会清除的
// 这个el-upload 已经实现了的
// console.log(uploadFile)
tableData.value.push({ ...uploadFile })
// tableData.value.push(uploadFile)
const index = tableData.value.findIndex(item => item.uid === uploadFile.uid)
let fileName = uploadFile.name
// if (uploadFile.name.lastIndexOf('.') !== -1) {
// fileName = uploadFile.name.substring(0, uploadFile.name.lastIndexOf('.'))
// }
const fileSize = uploadFile.size || 0
// 因为后面的 let chunkTotals = Math.ceil(fileSize / chunkSize); 使用了Math.ceil() 向上取整,所以不要这个分支也是可以的。
// if (fileSize <= chunkSize) {
// // uploadFile.raw这个是element plus 中 el-upload的自己上传的文件就放在这个raw里面,可以console.log(uploadFile)看一下
// const result = await uploadFileToServer(uploadFile.raw, 1, 1, fileName)
// console.log(result)
// // 合并文件
// await mergeFiles(1, fileName)
// }
let chunkTotals = Math.ceil(fileSize / chunkSize);
if (chunkTotals > 0) {
for (let chunkNumber = 0, start = 0; chunkNumber < chunkTotals; chunkNumber++, start += chunkSize) {
let end = Math.min(fileSize, start + chunkSize);
// uploadFile.raw这个是element plus 中 el-upload的自己上传的文件就放在这个raw里面,可以console.log(uploadFile)看一下
// 加 ?是因为ts语法提示“uploadFile.raw”可能为“未定义”,加了这个就不过有报错了
const files = uploadFile.raw?.slice(start, end)
const result = await uploadFileToServer(files, chunkNumber + 1, chunkTotals, fileName)
const percents = parseFloat(result.data.replace("%", ''))
uploadFile.percentage = percents
tableData.value[index].percentage = percents
// percentage.value = percents
console.log(result.data)
}
// 这里可以tableData.value[index].percentage先判断一下进度是否100%了,然后再合并,防止只上传了一半中途出现问题了也合并
const videoUrl = await mergeFiles(chunkTotals, fileName)
tableData.value[index].url = videoUrl
}
console.log(uploadFiles)
}
/**
* 从el-upload封装的文件列表中删除文件
* // el-upload的(内置方法),点击el-upload默认的文件展示以列表的 X 那个图标时,会移除文件,并从文件列表中去掉
* @param uploadFile el-upload当前删除点击的文件
* @param uploadFiles el-upload的文件列表
*/
const handleRemove: UploadProps['onRemove'] = async (uploadFile, uploadFiles) => {
// 这个删除表格tableData中的列表数据
const index2 = tableData.value.findIndex((item2: UploadFile) => item2.uid === uploadFile.uid)
if (index2 !== -1) {
tableData.value.splice(index2, 1)
}
await deleteFileByFileName(uploadFile.name)
// handleRemove内置的删除文件方法,
// 这里的uploadFiles跟绑定的v-model:file-list="fileList"可以看成是同一个数组,这两个数组的数据是一样的,从这两个数组中删除或者移除任何一个数据,
// 另外一个数据也会跟着变化的,这个element plus已经实现了
console.log(uploadFiles)
}
/**
* 自定义的表格删除的方法
* @param file 表格某一行的数据
*/
const handleRemoveUploadFileList = async (file: UploadFile) => {
const index = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid)
if (index !== -1) {
fileList.value.splice(index, 1)
}
const index2 = tableData.value.findIndex((item2: UploadFile) => item2.uid === file.uid)
if (index2 !== -1) {
tableData.value.splice(index2, 1)
}
// 删除磁盘中的文件
await deleteFileByFileName(file.name)
}
/**
* 预览视频
* @param row 表格传入的行数据
*/
const handlePreviewUploadFileList = (row: UploadFile) => {
dialogTableVisible.value = true
videoPath.value = row.url || ''
}
/**
* el-dialog的回调
*/
const handleClose = () => {
// 下面这两中都可以,any方便。HTMLVideoElement这个也行,有提示。两种方法写都可以不会报错
// let myVideo:any = document.getElementById('video-play') //对应video标签的ID
let myVideo:HTMLVideoElement = document.getElementById('video-play') as HTMLVideoElement//对应video标签的ID
myVideo.pause()
dialogTableVisible.value = false
videoPath.value = ''
}
</script>
<style scoped></style>
后端代码如下所示
配置静态资源映射
springboot静态资源映射配置_m0_62317155的博客-CSDN博客
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置静态资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// registry.addResourceHandler("/img/**").addResourceLocations("file:D:\\testuplosd\\" );
// 这里的D:\testuplosd\可以考虑从application.yml配置文件中配置读取,这里为了方便就不配置了
registry.addResourceHandler("/play/**").addResourceLocations("file:D:\\testuplosd\\");
}
}
springboot中的application.yml配置,这样设置的原因是,springboot中默认允许上传的文件大小是1MB,上面的前端代码中分块的大小是8MB,所以这里需要设置比8MB大,这里设置了10MB
server:
port: 9090
spring:
servlet:
multipart:
max-file-size: 10MB
controller
@RestController
@RequestMapping("/uploadVideo")
// 允许跨域
@CrossOrigin
public class uploadVideo {
/**
* 文件分块上传
* @param chunkNumber 当前分块数
* @param chunkTotal 总分块数
* @param file 文件
* @param fileName 文件名
* @return
* @throws IOException
*/
@RequestMapping("/uploadVideo")
public String uploadVideo(int chunkNumber, int chunkTotal, MultipartFile file, String fileName) throws IOException {
file.transferTo(Paths.get("D:\\testuplosd\\" + fileName + ".part" + chunkNumber));
Double process = (chunkNumber * 1.0) / (chunkTotal * 1.0) * 100;
DecimalFormat df = new DecimalFormat("0.00");
String format = df.format(process) + "%";
return format;
}
/**
* 合并分块
* @param chunkTotal 总的分块数
* @param fileName 文件名称
*/
@RequestMapping("/merge")
public String mergeVideo(int chunkTotal, String fileName) {
// 这里注意了for循环在try里面
try (FileOutputStream os = new FileOutputStream("D:\\testuplosd\\" + fileName)) {
for (int i = 1; i <= chunkTotal; i++) {
// D:\testuplosd\这个文件路径,在程序中可以配置在springboot的配置文件yml中。这里就配置了
Path part = Paths.get("D:\\testuplosd\\" + fileName + ".part" + i);
// 这个会自动关闭流的
Files.copy(part, os);
part.toFile().delete(); // 删除part文件
}
// 这里是配置了静态资源映射之后可以直接通过这个url路径访问视频文件,除了这种还可以专门写一个访问视频的方法,这里就不写了,通过
// 静态资源映射这个比较简单。
return "http://localhost:9090/play/" + fileName;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 删除文件,防止不需要的文件还存在服务器中
* @param fileName 文件名称
* @return
*/
@RequestMapping("/deleteByFileName")
public String deletyByFileName(String fileName) {
// 这里不严谨,这个应该是通过文件的md5值和文件名一起判断来删除,会好一点,这里只是试着删除
Path part = Paths.get("D:\\testuplosd\\" + fileName);
part.toFile().delete();
return "删除文件成功";
}
}
遇到的问题
在handleChange这个方法中,为刚开始的时候是tableData.value.push(uploadFile)直接将当前文件push到表格数组中的,导致进度没有成功展示,结果如下所示
然后我把,handleChange中的tableData.value.push(uploadFile)改成,tableData.value.push({…uploadFile})就可以了
更多推荐
所有评论(0)