<div>
<el-upload
action
:auto-upload="false"
:show-file-list="false"
:on-change="handleChange"
:disabled="loadingStatus == 'uploading'"
>
<el-button type="primary" :disabled="loadingStatus == 'uploading'">
<i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
</el-button>
</el-upload>
<!-- 进度显示 -->
<div class="mask" v-if="loadingStatus == 'uploading'">
<i class="el-icon-loading"></i>
<div class="progress-box">
<span>上传进度:{{Number(percent.toFixed())}}%</span>
<!-- <el-button type="primary" size="mini" @click="handleClickBtn">{{
upload | btnTextFilter
}}</el-button> -->
</div>
</div>
<!-- 展示上传成功 -->
<div class="file-list">
<div v-if="uploadFiles.length">
<div v-for="(item, index) in uploadFiles" :key="index">
<div class="list-item">
<div class="item-name">
<span>{{ item.name }}</span>
</div>
<div class="item-size">大小:{{ item.size | transformByte }}</div>
<div class="item-remove"><i class="el-icon-delete" @click="onRemoveFile(index)"></i></div>
</div>
</div>
</div>
</div>
<slot name="tip"></slot>
</div>
import * as api from "@/api/common-api";
import SparkMD5 from "./spark-md5.min.js";
const defaultChunkSize = 10 * 1024 * 1024;
export default {
props:{
defaultProps: {
type:Array,
default: () => []
}
},
data() {
return {
percent: 0,
upload: true,
percentCount: 0,
uploadFiles:[],
loadingStatus: ''
};
},
watch:{
defaultProps(val) {
console.log(val);
this.uploadFiles = val;
}
},
filters: {
btnTextFilter(val) {
return val ? '暂停' : '继续'
},
transformByte(size) {
if (!size) {
return "0B";
}
var num = 1024.0; // byte
if (size < num) {
return size + "B";
}
if (size < Math.pow(num, 2)) {
return (size / num).toFixed(2) + "K";
} // kb
if (size < Math.pow(num, 3)) {
return (size / Math.pow(num, 2)).toFixed(2) + "M";
} // M
if (size < Math.pow(num, 4)) {
return (size / Math.pow(num, 3)).toFixed(2) + "G";
} // G
return (size / Math.pow(num, 4)).toFixed(2) + "T"; // T
},
},
created() {
this.fileId = new Date().getTime();
},
mounted(){
},
methods: {
async handleChange(file) {
if (!file) return;
this.percent = 0;
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw;
let buffer;
try {
buffer = await this.fileToBuffer(fileObj);
} catch (e) {
console.log(e);
}
// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
// const chunkSize = 2097152,
const chunkSize = defaultChunkSize, // 切片大小
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名
// 根据文件内容生成 hash 值
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
const hash = spark.end();
const verifyRes = await this.verifyUpload(
fileObj.name,
fileObj.size,
hash
)
console.log(verifyRes)
// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0; // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
file: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${hash}_${i}.${suffix}`, // 文件名规则按照 hash_1.jpg 命名
blockNum: i + 1,
id: this.fileId,
totalSize: fileObj.size,
uploadId: this.createData.uploadId,
};
curChunk += chunkSize;
chunkList.push(item);
}
this.chunkList = chunkList; // sendRequest 要用到
this.hash = hash; // sendRequest 要用到
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("转换文件格式发生错误"));
};
});
},
// 发送请求
sendRequest() {
this.loadingStatus = 'uploading'
const requestList = []; // 请求集合
this.chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData();
formData.append("file", item.file);
formData.append("size", item.file.size); // 文件名使用切片的下标
formData.append("blockNum", item.blockNum); // 文件名使用切片的下标
formData.append("id", item.id); // 文件名使用切片的下标
formData.append("relationId", ''); // 文件名使用切片的下标
formData.append("totalSize", item.totalSize); // 文件名使用切片的下标
formData.append("uploadId", item.uploadId); // 文件名使用切片的下标
return api.uploadBurst(formData).then((res) => {
if (this.percentCount === 0) {
// 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
this.percentCount = 100 / this.chunkList.length;
}
this.percent += this.percentCount; // 改变进度
this.chunkList.splice(index, 1); // 一旦上传成功就删除这一个 chunk,方便断点续传
});
};
requestList.push(fn);
});
let i = 0; // 记录发送的请求个数
// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
const complete = () => {
api.completeUploadBurst(this.fileId).then((res) => {
this.loadingStatus = 'done'
this.uploadFiles.push(res);
this.$emit('on-success', this.uploadFiles)
}).catch(err=>{
this.resetProgress();
})
};
const send = async () => {
if (!this.upload) return;
if (i >= requestList.length) {
// 发送完毕
complete();
return;
}
await requestList[i]();
i++;
send();
};
send(); // 发送请求
},
// 按下暂停按钮
handleClickBtn() {
this.upload = !this.upload
// 如果不暂停则继续上传
if (this.upload) this.sendRequest()
},
// 文件上传之前的校验: 校验文件是否已存在
verifyUpload(fileName, fileSize, fileHash) {
return new Promise((resolve) => {
let params = {
md5: fileHash,
fileName: fileName,
id: this.fileId,
totalSize: fileSize,
};
api
.createUploadBurst(params)
.then((res) => {
console.log("verifyUpload -> res", res);
this.createData = res;
resolve(res);
})
.catch((err) => {
console.log("verifyUpload -> err", err);
});
});
},
onRemoveFile(index){
this.uploadFiles.splice(index, 1);
this.$emit('on-success', this.uploadFiles)
},
resetProgress(){
this.percent = 0;
this.loadingStatus = ''
},
},
};
</script>
<style lang="scss" scoped>
.file-list {
.list-item {
padding: 8px 10px;
display: flex;
align-items: center;
line-height: 25px;
position: relative;
&:hover .item-chunk-box {
display: block;
}
.item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 6px;
.svg-icon {
font-size: 22px;
vertical-align: sub;
}
}
.item-size {
padding-left: 10px;
}
.item-remove {
margin-left: 10px;
color: #999;
cursor: pointer;
}
}
}
.progress-box {
display: flex;
color: #fff;
align-items: center;
font-size: 16px;
}
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.3);
display:flex;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 999;
.el-icon-loading {
font-size: 30px;
color: #fff;
}
}
</style>