目前根据业务只进行了切片功能,暂时没有断点续传、秒传。
效果图:
<template>
<el-upload
ref="uploadRef"
v-show="showCollection.showUpload"
class="upload-demo"
action="#"
:auto-upload="false"
:on-change="onChange"
:on-remove="onRemove"
:limit="props.limit"
:on-exceed="handleExceed"
:file-list="props.fileList"
>
<template #trigger>
<div class="uploadBtn">
<slot>
<img src="@/assets/imgs/upload-file.png" alt="">
<span>添加</span>
</slot>
</div>
</template>
</el-upload>
<!-- 进度条 -->
<div v-if="showCollection.showProgress" class="progressBox">
<el-progress
:text-inside="true"
:stroke-width="15"
:status="progress == 100 ? 'success' : ''"
:percentage="progress"
/>
<el-icon>
<SuccessFilled color="#00A058" v-if="showCollection.showSuccess" />
<Loading color="#989BA0" size="20" v-else />
</el-icon>
</div>
<p v-if="showCollection.showLoading" class="tips">
<span>正在切片/加密文件</span> <el-icon><Loading /></el-icon>
</p>
</template>
<script setup>
import { ref, reactive, defineExpose, computed, watch, toRefs } from "vue"
import MD5 from 'js-md5';
import { ElMessage } from 'element-plus'
let isFirst = true;
const props = defineProps({
accept: { //限制上传类型
type: String,
default: ""
},
limit: { //限制上次数量
type: Number,
default: null
},
chunkSize: { // 切片大小, 默认30MB
type: Number,
default: 1024 * 1024 * 30
},
fileList: { //默认显示文件
type: Array,
default: ()=> ([])
},
})
let { fileList } = toRefs(props);
const emits = defineEmits(['onChange'])
let uploadData = {}; //上传的切片数据集合
let allFiles = []; //所有上传的文件(包含默认显示)
let md5Arr = [];
const uploadRef = ref();
const progress = ref(0); // 上传进度
// 集中控制显示
const showCollection = reactive({
showUpload: true,
showProgress: false,
showLoading: false,
showSuccess: false,
})
watch(fileList,(arr) => {
if(isFirst && arr && arr.length) {
allFiles = arr;
isFirst = false;
}
},{ deep: true })
//限制文件数量
const handleExceed = (files) => {
if(props.limit && props.limit == 1) {
uploadRef.value.clearFiles()
uploadData = {};
md5Arr = [];
let file = files[0];
uploadRef.value.handleStart(file);
}
console.log('超出限制');
}
// 上传文件
const onChange = (uploadFile, uploadFiles) => {
if (!uploadFile) return;
// 限制格式
if(props.accept && !(uploadFile.name.toLowerCase().includes(props.accept))) {
uploadRef.value.handleRemove(uploadFile);
ElMessage({
message: `只能上传${props.accept}格式!`,
type: 'warning',
})
return;
}
//移除空文件
if(!uploadFile.size) {
uploadRef.value.handleRemove(uploadFile);
ElMessage({
message: `不能上传空文件!`,
type: 'warning',
})
return;
}
fileMd5(uploadFile.raw);
allFiles = uploadFiles;
}
// 计算整个文件的md5
const fileMd5 = (file) => {
showCollection.showLoading = true;
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (data) => {
let md5 = MD5(new Uint8Array(data.target?.result));
if(md5Arr.includes(md5)) {
ElMessage({
message: `已经存在相同的文件`,
type: 'warning',
})
uploadRef.value.handleRemove(file);
showCollection.showLoading = false;
return;
}
md5Arr.push(md5);
uploadData[file.uid] = [];
fileChunk(file, md5);
emits("onChange",uploadData);
};
reader.onerror = () => {
ElMessage({
message: `md5计算失败`,
type: 'error',
})
showCollection.showLoading = false;
uploadRef.value.handleRemove(file);
};
}
// 文件进行切片,数据结构根据后端接口进行调整
const fileChunk = (file, md5) => {
let cur = 0;
let index = 0;
while (cur < file.size) {
uploadData[file.uid].push({
name: file.name,
file: file.slice(cur, cur + props.chunkSize), //当前切片文件
chunk: index, //当前切片下标
md5, //整个文件的md5标识
size: file.size, //原文件大小
chunks: null
})
cur += props.chunkSize;
index += 1;
}
//设置分片总数量, 当分片数量不为空时分片上传, 为空时全文上传
uploadData[file.uid].forEach(item => {
item.chunks = uploadData[file.uid].length > 1 ?
uploadData[file.uid].length : null;
})
showCollection.showLoading = false;
console.log('切片',uploadData);
}
// 移除
const onRemove = (file, UploadFiles) => {
// 移除md5
let curMd5 = uploadData[file.uid]?.[0]?.md5;
md5Arr = md5Arr.filter(md5 => md5 != curMd5);
delete uploadData[file.uid];
console.log('移除文件',uploadData);
allFiles = UploadFiles;
emits("onChange",uploadData, file);
}
// 上传,外部调用
const handlerUpload = (callback) => {
initProgress();
let allChunk = []; //所有切片
Object.keys(uploadData).forEach((key) => {
uploadData[key].forEach(chunk => {
allChunk.push(chunk);
})
});
if(!allChunk.length) return;
uploadLoop(allChunk, 0, callback);
}
// 循环上传
const uploadLoop = (allChunk, idx, callback, fileIdArr = []) => {
// 调用上传接口
Api(allChunk[idx]).then((res) => {
if(res.fileId) {
fileIdArr.push(res.fileId);
}
let val = parseInt((idx + 1) / allChunk.length * 100);
progress.value = val > 100 ? 100 : val;
if(idx == allChunk.length - 1) {
showCollection.showSuccess = true;
callback && callback(fileIdArr);
return;
}
uploadLoop(allChunk, idx + 1, callback, fileIdArr);
}).catch(() => {
callback && callback(fileIdArr);
recover();
})
}
//初始化进度条
const initProgress = () => {
showCollection.showUpload = false;
showCollection.showProgress = true;
progress.value = 0;
}
// 还原
const recover = () => {
showCollection.showUpload = true;
showCollection.showProgress = false;
progress.value = 0;
}
// 清空
const clear = () => {
uploadRef.value.clearFiles();
isFirst = true;
uploadData = {};
md5Arr = [];
Object.keys(showCollection).forEach(key => {
showCollection[key] = key == 'showUpload' ? true : false;
})
progress.value = 0;
}
// 外部调用,获取文件数据
const getFilesList = () => {
let uploadCount = Object.keys(uploadData).length;
return {
// 包含默认显示的列表信息
allFiles,
allFilesCount: allFiles.length,
// 上传的文件列表信息
uploadData,
uploadCount
};
}
defineExpose({
handlerUpload,
showCollection,
clear,
recover,
getFilesList,
initProgress
})
</script>
<style lang="scss" scoped>
.uploadBtn {
width: 100%;
height: 32px;
border: 1px dashed #DDDFE3;
display: flex;
align-items: center;
justify-content: center;
color: #006EFF;
span {
margin-left: 8px;
}
}
:deep(.el-upload) {
width: 100%;
}
.tips {
color: #989BA0;
display: flex;
align-items: center;
justify-content: center;
span {
margin-right: 5px;
}
}
.progressBox {
display: flex;
align-items: center;
i {
width: 14px;
margin-left: 10px;
}
:deep(.el-progress) {
flex: 1;
}
}
</style>