<template>
<div>
<el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
<i class="el-icon-upload">i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传em>div>
<div class="el-upload__tip" slot="tip">大小不超过 200M 的视频div>
el-upload>
<div class="progress-box">
<span>上传进度:{{ percent.toFixed() }}%span>
<el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}el-button>
div>
div>
template>
<script>
import { getUUID } from '@/utils'
import axios from 'axios'
export default {
name: 'singleUpload',
props: {
value: String
},
filters: {
btnTextFilter(val) {
return val ? '暂停' : '继续'
}
},
data() {
return {
videoUrl: this.value,
percent: 0,
upload: true,
percentCount: 0,
suffix: '',
fileName: '',
preName: ''
}
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
async handleChange(file) {
if (!file) return
this.percent = 0
this.percentCount = 0
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw
let buffer
try {
buffer = await this.fileToBuffer(fileObj)
} catch (e) {
console.log(e)
}
// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 2097152,
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名
this.preName = getUUID() //生成文件名前缀
this.fileName = this.preName+'.'+suffix //文件名
// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0 // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${this.preName}_${i}.${suffix}` // 文件名规则按照 filename_1.jpg 命名
}
curChunk += chunkSize
chunkList.push(item)
}
this.chunkList = chunkList // sendRequest 要用到
this.sendRequest()
},
// 发送请求
sendRequest() {
const requestList = [] // 请求集合
this.chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData()
formData.append('chunk', item.chunk)
formData.append('filename', item.fileName)
return axios({
url: 'http://localhost/api/chunk',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
data: formData
}).then(response => {
if (response.data.errcode === 0) { // 成功
if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
this.percentCount = 100 / this.chunkList.length
}
if (this.percent >= 100) {
this.percent = 100;
}else {
this.percent += this.percentCount // 改变进度
}
if (this.percent >= 100) {
this.percent = 100;
}
this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
}else{
this.$mseeage({
type: "error",
message: response.data.message
})
return
}
})
}
requestList.push(fn)
})
let i = 0 // 记录发送的请求个数
// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件名传递给服务器
const complete = () => {
axios({
url: 'http://localhost/api/merge',
method: 'get',
params: {filename: this.fileName },
timeout: 60000
}).then(response => {
if (response.data.errcode === 0) { // 请求发送成功
// this.videoUrl = res.data.path
console.log(response.data)
}
})
}
const send = async () => {
if (!this.upload) return
if (i >= requestList.length) {
// 发送完毕
complete()
return
}
await requestList[i]()
i++
send()
}
send() // 发送请求
this.emitInput(this.fileName)
},
// 按下暂停按钮
handleClickBtn() {
this.upload = !this.upload
// 如果不暂停则继续上传
if (this.upload) this.sendRequest()
},
// 将 File 对象转为 ArrayBuffer
fileToBuffer(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.onload = e => {
resolve(e.target.result)
}
fr.readAsArrayBuffer(file)
fr.onerror = () => {
reject(new Error('转换文件格式发生错误'))
}
})
}
}
}
script>
<style scoped "">
.progress-box {
box-sizing: border-box;
width: 360px;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding: 8px 10px;
background-color: #ecf5ff;
font-size: 14px;
border-radius: 4px;
}
.videoShow{
width: 100%;
height:600px;
padding: 10px 0 50px;
position: relative;
}
#videoBox{
object-fit:fill;
border-radius: 8px;
display: inline-block;
vertical-align: baseline;
}
.video-img{
position: absolute;
top: 0;
bottom: 0;
width: 100%;
z-index: 999;
background-size:100%;
cursor:pointer;
}
.video-img img {
display:block;
width: 60px;
height: 60px;
position: relative;
top:260px;
left: 48%;
}
video:focus {
outline: -webkit-focus-ring-color auto 0px;
}
style>
String dirPath = "D:\\video\\train"
@PostMapping("/chunk")
public Result upLoadChunk(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("filename") String fileName) {
// 用于存储文件分片的文件夹
File folder = new File(dirPath);
if (!folder.exists() && !folder.isDirectory())
folder.mkdirs();
// 文件分片的路径
String filePath = dirPath + fileName;
try {
File saveFile = new File(filePath);
// 写入文件中
//FileOutputStream fileOutputStream = new FileOutputStream(saveFile);
//fileOutputStream.write(chunk.getBytes());
//fileOutputStream.close();
chunk.transferTo(saveFile);
return new Result();
} catch (Exception e) {
e.printStackTrace();
}
return new Result();
}
@GetMapping("/merge")
public Result MergeChunk(@RequestParam("filename") String filename) {
String preName = filename.substring(0,filename.lastIndexOf("."));
// 文件分片所在的文件夹
File chunkFileFolder = new File(dirPath);
// 合并后的文件的路径
File mergeFile = new File(dirPath + filename);
// 得到文件分片所在的文件夹下的所有文件
File[] chunks = chunkFileFolder.listFiles();
System.out.println(chunks.length);
assert chunks != null;
// 排序
File[] files = Arrays.stream(chunks)
.filter(file -> file.getName().startsWith(preName))
.sorted(Comparator.comparing(o -> Integer.valueOf(o.getName().split("\\.")[0].split("_")[1])))
.toArray(File[]::new);
try {
// 合并文件
RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
byte[] bytes = new byte[1024];
for (File chunk : files) {
RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
int len;
while ((len = randomAccessFileReader.read(bytes)) != -1) {
randomAccessFileWriter.write(bytes, 0, len);
}
randomAccessFileReader.close();
System.out.println(chunk.getName());
chunk.delete(); // 删除已经合并的文件
}
randomAccessFileWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
return new Result();
}
vue组件中导入的utils/index.js
/**
* 获取uuid
*/
export function getUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}