Vue3封装大文件切片上传

 目前根据业务只进行了切片功能,暂时没有断点续传、秒传。

效果图:

<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>

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值