|
@@ -0,0 +1,495 @@
|
|
|
+<template>
|
|
|
+ <el-upload ref="uploadRef" class="hc-file-upload-box" :headers="getTokenHeader()" :data="uploadData" :disabled="uploadDisabled" multiple :show-file-list="false" :http-request="uploadFileHandle"
|
|
|
+ :on-success="uploadSuccess" :on-exceed="uploadExceed" :on-error="uploadError" :before-upload="beforeUpload" :on-progress="uploadprogress">
|
|
|
+ <slot></slot>
|
|
|
+ </el-upload>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import {ref,watch,onMounted} from "vue";
|
|
|
+import {getTokenHeader} from '~src/api/request/header';
|
|
|
+import {isSize, deepClone, getObjValue} from "vue-utils-plus"
|
|
|
+import md5 from 'js-md5' //引入MD5加密
|
|
|
+import ossApi from "~api/oss";
|
|
|
+const props = defineProps({
|
|
|
+ datas: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ },
|
|
|
+ api: {
|
|
|
+ type: String,
|
|
|
+ default: "/api/blade-resource/oss/endpoint/"
|
|
|
+ },
|
|
|
+ action: {
|
|
|
+ type: String,
|
|
|
+ default: "upload-file2"
|
|
|
+ },
|
|
|
+ accept: {
|
|
|
+ type: String,
|
|
|
+ default: "image/png,image/jpg,image/jpeg,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/pdf,.doc,.docx,application/msword"
|
|
|
+ },
|
|
|
+ size: {
|
|
|
+ type: Number,
|
|
|
+ default: 60
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+//变量
|
|
|
+const uploadRef = ref(null)
|
|
|
+const uploadData = ref(props.datas)
|
|
|
+const uploadDisabled = ref(false)
|
|
|
+
|
|
|
+//监听
|
|
|
+watch(() => [
|
|
|
+ props.datas,
|
|
|
+], ([datas]) => {
|
|
|
+ uploadData.value = datas
|
|
|
+})
|
|
|
+
|
|
|
+//渲染完成
|
|
|
+onMounted(()=> {
|
|
|
+ beforeFileNum.value = 0
|
|
|
+ finishFileNum.value = 0
|
|
|
+ errorFileNum.value = 0
|
|
|
+})
|
|
|
+
|
|
|
+//事件
|
|
|
+const emit = defineEmits(['change', 'progress'])
|
|
|
+
|
|
|
+//上传前
|
|
|
+const beforeFileNum = ref(0)
|
|
|
+const beforeUpload = async (file) => {
|
|
|
+ beforeFileNum.value ++;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+//超出限制时
|
|
|
+const uploadExceed = () => {
|
|
|
+ window?.$message?.warning(`请上传 ${props.accept} 格式的文件,文件大小不超过${props.size}M`);
|
|
|
+}
|
|
|
+
|
|
|
+//上传中
|
|
|
+const uploadprogress = () => {
|
|
|
+ uploadDisabled.value = true
|
|
|
+ emit('progress', true)
|
|
|
+}
|
|
|
+
|
|
|
+//上传完成
|
|
|
+const finishFileNum = ref(0)
|
|
|
+const uploadSuccess = (response, uploadFile, uploadFiles) => {
|
|
|
+ //console.log(response)
|
|
|
+ //console.log(uploadFile)
|
|
|
+ //console.log(uploadFiles)
|
|
|
+ finishFileNum.value ++;
|
|
|
+ if (beforeFileNum.value === finishFileNum.value) {
|
|
|
+ const fileList = getUploadFile(deepClone(uploadFiles))
|
|
|
+ uploadClearFiles()
|
|
|
+ emit('change', {type: 'success', fileList})
|
|
|
+ emit('progress', false)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+//上传失败
|
|
|
+const errorFileNum = ref(0)
|
|
|
+const uploadError = (error,uploadFile,uploadFiles) => {
|
|
|
+ errorFileNum.value ++;
|
|
|
+ window?.$message?.error('上传失败');
|
|
|
+ console.log(error)
|
|
|
+ const num = finishFileNum.value + errorFileNum.value;
|
|
|
+ if (beforeFileNum.value === num) {
|
|
|
+ const fileList = getUploadFile(deepClone(uploadFiles))
|
|
|
+ uploadClearFiles()
|
|
|
+ emit('change', {type: 'error', fileList})
|
|
|
+ emit('progress', false)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const uploadClearFiles = () => {
|
|
|
+ finishFileNum.value = 0
|
|
|
+ beforeFileNum.value = 0
|
|
|
+ errorFileNum.value = 0
|
|
|
+ uploadDisabled.value = false
|
|
|
+ uploadRef.value?.clearFiles()
|
|
|
+}
|
|
|
+
|
|
|
+//获取文件
|
|
|
+const getUploadFile = (fileList) => {
|
|
|
+ let fileArr = [];
|
|
|
+ for (let i = 0; i < fileList.length; i++) {
|
|
|
+ const item = getObjValue(fileList[i]?.response?.data)
|
|
|
+ fileArr.push(item)
|
|
|
+ }
|
|
|
+ return fileArr
|
|
|
+}
|
|
|
+
|
|
|
+const uploadFileHandle = (options) =>{
|
|
|
+ console.log(options)
|
|
|
+ uploadByPieces(options)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 文件分片上传
|
|
|
+ * @params file {File} 文件
|
|
|
+ * @params pieceSize {Number} 分片大小 默认3MB
|
|
|
+ * @params concurrent {Number} 并发数量 默认2
|
|
|
+ * @params process {Function} 进度回调函数
|
|
|
+ * @params success {Function} 成功回调函数
|
|
|
+ * @params error {Function} 失败回调函数
|
|
|
+ */
|
|
|
+ const uploadByPieces = ({
|
|
|
+ file,
|
|
|
+ pieceSize = 3,
|
|
|
+ concurrent = 3,
|
|
|
+ onSuccess:success,
|
|
|
+ onProgress:process,
|
|
|
+ onError:error
|
|
|
+}) => {
|
|
|
+ // 如果文件传入为空直接 return 返回
|
|
|
+ if (!file || file.length < 1) {
|
|
|
+ return error('文件不能为空')
|
|
|
+ }
|
|
|
+ let fileMD5 = '' // 总文件列表
|
|
|
+ const chunkSize = pieceSize * 1024 * 1024 // 1MB一片
|
|
|
+ const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
|
|
|
+ const chunkList = [] // 分片列表
|
|
|
+ let uploaded = [] // 已经上传的
|
|
|
+ let fileType = '' // 文件类型
|
|
|
+ // 获取md5
|
|
|
+ /***
|
|
|
+ * 获取md5
|
|
|
+ **/
|
|
|
+ const readFileMD5 = () => {
|
|
|
+ // 读取视频文件的md5
|
|
|
+ fileType = file.name.substring(file.name.lastIndexOf('.') + 1, file.name.length)
|
|
|
+ console.log('获取文件的MD5值')
|
|
|
+ let fileRederInstance = new FileReader()
|
|
|
+ console.log('file', file)
|
|
|
+ fileRederInstance.readAsBinaryString(file)
|
|
|
+ fileRederInstance.addEventListener('load', e => {
|
|
|
+ let fileBolb = e.target.result
|
|
|
+ fileMD5 = md5(fileBolb)
|
|
|
+ var index = file.name.lastIndexOf('.')
|
|
|
+ var tp = file.name.substring(index + 1, file.name.length)
|
|
|
+ let form = new FormData()
|
|
|
+ form.append('filename', file.name)
|
|
|
+ //form.append('file', new File([], 'filename'))
|
|
|
+ form.append('identifier', fileMD5)
|
|
|
+ form.append('objectType', fileType)
|
|
|
+ form.append('chunkNumber', 1)
|
|
|
+ ossApi.uploadChunk(form).then(res => {
|
|
|
+ if (res.skipUpload) {
|
|
|
+ console.log('文件已被上传')
|
|
|
+ success && success(res)
|
|
|
+ } else {
|
|
|
+ // 判断是否是断点续传
|
|
|
+ if (res.uploaded && res.uploaded.length != 0) {
|
|
|
+ uploaded = [].concat(res.uploaded)
|
|
|
+ }
|
|
|
+ console.log('已上传的分片:' + uploaded)
|
|
|
+ // 判断是并发上传或顺序上传
|
|
|
+ if (concurrent == 1 || chunkCount == 1) {
|
|
|
+ console.log('顺序上传')
|
|
|
+ sequentialUplode(0)
|
|
|
+ } else {
|
|
|
+ console.log('并发上传')
|
|
|
+ concurrentUpload()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).catch((e) => {
|
|
|
+ console.log('文件合并错误')
|
|
|
+ console.log(e)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ /***
|
|
|
+ * 获取每一个分片的详情
|
|
|
+ **/
|
|
|
+ const getChunkInfo = (file, currentChunk, chunkSize) => {
|
|
|
+ let start = currentChunk * chunkSize
|
|
|
+ let end = Math.min(file.size, start + chunkSize)
|
|
|
+ let chunk = file.slice(start, end)
|
|
|
+ return {
|
|
|
+ start,
|
|
|
+ end,
|
|
|
+ chunk
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /***
|
|
|
+ * 针对每个文件进行chunk处理
|
|
|
+ **/
|
|
|
+ const readChunkMD5 = () => {
|
|
|
+ // 针对单个文件进行chunk上传
|
|
|
+ for (var i = 0; i < chunkCount; i++) {
|
|
|
+ const {
|
|
|
+ chunk
|
|
|
+ } = getChunkInfo(file, i, chunkSize)
|
|
|
+
|
|
|
+ // 判断已经上传的分片中是否包含当前分片
|
|
|
+ if (uploaded.indexOf(i + '') == -1) {
|
|
|
+ uploadChunk({
|
|
|
+ chunk,
|
|
|
+ currentChunk: i,
|
|
|
+ chunkCount
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /***
|
|
|
+ * 原始上传
|
|
|
+ **/
|
|
|
+ const uploadChunk = (chunkInfo) => {
|
|
|
+ var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
|
|
|
+ console.log(sd, '进度')
|
|
|
+ process(sd)
|
|
|
+ console.log(chunkInfo, '分片大小')
|
|
|
+ let inde = chunkInfo.currentChunk + 1
|
|
|
+ if (uploaded.indexOf(inde + '') > -1) {
|
|
|
+ const {
|
|
|
+ chunk
|
|
|
+ } = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
|
|
|
+ uploadChunk({
|
|
|
+ chunk,
|
|
|
+ currentChunk: inde,
|
|
|
+ chunkCount
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ var index = file.name.lastIndexOf('.')
|
|
|
+ var tp = file.name.substring(index + 1, file.name.length)
|
|
|
+ // 构建上传文件的formData
|
|
|
+ let fetchForm = new FormData()
|
|
|
+ fetchForm.append('identifier', fileMD5)
|
|
|
+ fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
|
|
|
+ fetchForm.append('chunkSize', chunkSize)
|
|
|
+ fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
|
|
|
+ const chunkfile = new File([chunkInfo.chunk], file.name)
|
|
|
+ fetchForm.append('file', chunkfile)
|
|
|
+ // fetchForm.append('file', chunkInfo.chunk)
|
|
|
+ fetchForm.append('filename', file.name)
|
|
|
+ fetchForm.append('relativePath', file.name)
|
|
|
+ fetchForm.append('totalChunks', chunkInfo.chunkCount)
|
|
|
+ fetchForm.append('totalSize', file.size)
|
|
|
+ fetchForm.append('objectType', tp)
|
|
|
+ // 执行分片上传
|
|
|
+ let config = {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Accept': '*/*'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ossApi.uploadChunk(fetchForm, config).then(res => {
|
|
|
+
|
|
|
+ if (res.code == 200) {
|
|
|
+ console.log('分片上传成功')
|
|
|
+ uploaded.push(chunkInfo.currentChunk + 1)
|
|
|
+ // 判断是否全部上传完
|
|
|
+ if (uploaded.length == chunkInfo.chunkCount) {
|
|
|
+ console.log('全部完成')
|
|
|
+ success(res)
|
|
|
+ process(100)
|
|
|
+ } else {
|
|
|
+ const {
|
|
|
+ chunk
|
|
|
+ } = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
|
|
|
+ uploadChunk({
|
|
|
+ chunk,
|
|
|
+ currentChunk: chunkInfo.currentChunk + 1,
|
|
|
+ chunkCount
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ console.log(res.msg)
|
|
|
+ }
|
|
|
+
|
|
|
+ }).catch((e) => {
|
|
|
+ error && error(e)
|
|
|
+ })
|
|
|
+ // if (chunkInfo.currentChunk < chunkInfo.chunkCount) {
|
|
|
+ // setTimeout(() => {
|
|
|
+ //
|
|
|
+ // }, 1000)
|
|
|
+ // }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /***
|
|
|
+ * 顺序上传
|
|
|
+ **/
|
|
|
+ const sequentialUplode = (currentChunk) => {
|
|
|
+ const {
|
|
|
+ chunk
|
|
|
+ } = getChunkInfo(file, currentChunk, chunkSize)
|
|
|
+ let chunkInfo = {
|
|
|
+ chunk,
|
|
|
+ currentChunk,
|
|
|
+ chunkCount
|
|
|
+ }
|
|
|
+ var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
|
|
|
+ process(sd)
|
|
|
+ console.log('当前上传分片:' + currentChunk)
|
|
|
+ let inde = chunkInfo.currentChunk + 1
|
|
|
+ if (uploaded.indexOf(inde + '') > -1) {
|
|
|
+ console.log('分片【' + currentChunk + '】已上传')
|
|
|
+ sequentialUplode(currentChunk + 1)
|
|
|
+ } else {
|
|
|
+ let uploadData = createUploadData(chunkInfo)
|
|
|
+ let config = {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Accept': '*/*'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 执行分片上传
|
|
|
+ ossApi.uploadChunk(uploadData, config).then(res => {
|
|
|
+ if (res.code == 200) {
|
|
|
+ console.log('分片【' + currentChunk + '】上传成功')
|
|
|
+ uploaded.push(chunkInfo.currentChunk + 1)
|
|
|
+ // 判断是否全部上传完
|
|
|
+ if (uploaded.length == chunkInfo.chunkCount) {
|
|
|
+ console.log('全部完成')
|
|
|
+ success(res)
|
|
|
+ process(100)
|
|
|
+ } else {
|
|
|
+ sequentialUplode(currentChunk + 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ console.log(res.msg)
|
|
|
+ }
|
|
|
+
|
|
|
+ }).catch((e) => {
|
|
|
+ error && error(e)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /***
|
|
|
+ * 并发上传
|
|
|
+ **/
|
|
|
+ const concurrentUpload = () => {
|
|
|
+ for (var i = 0; i < chunkCount; i++) {
|
|
|
+ chunkList.push(Number(i))
|
|
|
+ }
|
|
|
+ console.log('需要上传的分片列表:' + chunkList)
|
|
|
+ concurrentExecution(chunkList, concurrent, (curItem) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const {
|
|
|
+ chunk
|
|
|
+ } = getChunkInfo(file, curItem, chunkSize)
|
|
|
+ let chunkInfo = {
|
|
|
+ chunk,
|
|
|
+ currentChunk: curItem,
|
|
|
+ chunkCount
|
|
|
+ }
|
|
|
+ var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
|
|
|
+ process(sd)
|
|
|
+ console.log('当前上传分片:' + curItem)
|
|
|
+ let inde = chunkInfo.currentChunk + 1
|
|
|
+ if (uploaded.indexOf(inde + '') == -1) {
|
|
|
+ // 构建上传文件的formData
|
|
|
+ let uploadData = createUploadData(chunkInfo)
|
|
|
+ // 请求头
|
|
|
+ let config = {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Accept': '*/*'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ossApi.uploadChunk(uploadData, config).then(res => {
|
|
|
+ if (res.code == 200) {
|
|
|
+ uploaded.push(chunkInfo.currentChunk + 1)
|
|
|
+ console.log('已经上传完成的分片:' + uploaded)
|
|
|
+ // 判断是否全部上传完
|
|
|
+ // if (uploaded.length == chunkInfo.chunkCount) {
|
|
|
+ // success(res)
|
|
|
+ // process(100)
|
|
|
+ // }
|
|
|
+ if(typeof res.data == 'object'){
|
|
|
+ success(res)
|
|
|
+ process(100)
|
|
|
+ }
|
|
|
+ resolve()
|
|
|
+ } else {
|
|
|
+ reject(res)
|
|
|
+ console.log(res.msg)
|
|
|
+ }
|
|
|
+
|
|
|
+ }).catch((e) => {
|
|
|
+ reject(res)
|
|
|
+ error && error(e)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ console.log('分片【' + chunkInfo.currentChunk + '】已上传')
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }).then(res => {
|
|
|
+ console.log('finish', res)
|
|
|
+ }).catch((e)=>{
|
|
|
+ error && error(e)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ /***
|
|
|
+ * 创建文件上传参数
|
|
|
+ **/
|
|
|
+ const createUploadData = (chunkInfo) => {
|
|
|
+ let fetchForm = new FormData()
|
|
|
+ fetchForm.append('identifier', fileMD5)
|
|
|
+ fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
|
|
|
+ fetchForm.append('chunkSize', chunkSize)
|
|
|
+ fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
|
|
|
+ const chunkfile = new File([chunkInfo.chunk], file.name)
|
|
|
+ fetchForm.append('file', chunkfile)
|
|
|
+ // fetchForm.append('file', chunkInfo.chunk)
|
|
|
+ fetchForm.append('filename', file.name)
|
|
|
+ fetchForm.append('relativePath', file.name)
|
|
|
+ fetchForm.append('totalChunks', chunkInfo.chunkCount)
|
|
|
+ fetchForm.append('totalSize', file.size)
|
|
|
+ fetchForm.append('objectType', fileType)
|
|
|
+ return fetchForm
|
|
|
+ }
|
|
|
+
|
|
|
+ readFileMD5() // 开始执行代码
|
|
|
+
|
|
|
+}
|
|
|
+/**
|
|
|
+ * 并发执行
|
|
|
+ * @params list {Array} - 要迭代的数组
|
|
|
+ * @params limit {Number} - 并发数量控制数,最好小于3
|
|
|
+ * @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
|
|
|
+ * @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
|
|
|
+ */
|
|
|
+const concurrentExecution = (list, limit, asyncHandle)=>{
|
|
|
+ // 递归执行
|
|
|
+ let recursion = (arr) => {
|
|
|
+ // 执行方法 arr.shift() 取出并移除第一个数据
|
|
|
+ return asyncHandle(arr.shift()).then(() => {
|
|
|
+ // 数组还未迭代完,递归继续进行迭代
|
|
|
+ if (arr.length !== 0) {
|
|
|
+ return recursion(arr)
|
|
|
+ } else {
|
|
|
+ return 'finish'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 创建新的并发数组
|
|
|
+ let listCopy = [].concat(list)
|
|
|
+ // 正在进行的所有并发异步操作
|
|
|
+ let asyncList = []
|
|
|
+ limit = limit > listCopy.length ? listCopy.length : limit
|
|
|
+ console.log(limit)
|
|
|
+ while (limit--) {
|
|
|
+ asyncList.push(recursion(listCopy))
|
|
|
+ }
|
|
|
+ // 所有并发异步操作都完成后,本次并发控制迭代完成
|
|
|
+ return Promise.all(asyncList)
|
|
|
+}
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+.hc-file-upload-box .el-upload-list .el-upload-list__item {
|
|
|
+ .el-upload-list__item-status-label, .el-icon--close-tip {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|