diff --git a/src/components/Form/src/jeecg/components/JUpload/JUpload.vue b/src/components/Form/src/jeecg/components/JUpload/JUpload.vue index f01f3ca..db9957e 100644 --- a/src/components/Form/src/jeecg/components/JUpload/JUpload.vue +++ b/src/components/Form/src/jeecg/components/JUpload/JUpload.vue @@ -1,9 +1,8 @@ - + {{ text }} + @@ -29,7 +29,7 @@ import { ref, reactive, computed, watch, nextTick, createApp, unref } from 'vue'; import { Icon } from '/@/components/Icon'; import { getToken } from '/@/utils/auth'; - import { uploadUrl } from '/@/api/common/api'; + import { uploadUrl, baseUploadUrl } from '/@/api/common/api'; import { propTypes } from '/@/utils/propTypes'; import { useMessage } from '/@/hooks/web/useMessage'; import { createImgPreview } from '/@/components/Preview/index'; @@ -38,6 +38,9 @@ import { UploadTypeEnum } from './upload.data'; import { getFileAccessHttpUrl } from '/@/utils/common/compUtils'; import UploadItemActions from './components/UploadItemActions.vue'; + import { defHttp } from '/@/utils/http/axios'; + import { buildUUID } from '/@/utils/uuid'; + import CryptoJS from 'crypto-js'; const { createMessage, createConfirm } = useMessage(); const { prefixCls } = useDesign('j-upload'); @@ -70,9 +73,16 @@ forceBeforeUploadFn: propTypes.bool.def(true), //后加的,强制验证accept,目前仅支持*.jpg,*.gif之类的扩展名验证,video/*,audio/*,image/jpeg之类的暂不支持 forceAcceptVerify: propTypes.bool.def(false), + //后加的,是否查询biz + isGetBiz: propTypes.bool.def(false), + //后加的,查询biz参数 + getBizParam: propTypes.object.def({}), disabled: propTypes.bool.def(false), }); + const otherData = ref({}); + const otherClass = ref({}); + const headers = reactive({ 'X-Access-Token': getToken(), }); @@ -89,8 +99,8 @@ const bind: any = Object.assign({}, props, unref(attrs)); bind.name = 'file'; bind.listType = isImageMode.value ? 'picture-card' : 'text'; - bind.class = [bind.class, { 'upload-disabled': props.disabled }]; - bind.data = { biz: props.bizPath, ...bind.data }; + bind.class = [bind.class, { 'upload-disabled': props.disabled, ...unref(otherClass) }]; + bind.data = { biz: props.bizPath, ...bind.data, ...unref(otherData) }; //update-begin-author:taoyan date:20220407 for: 自定义beforeUpload return false,并不能中断上传过程 if (!bind.beforeUpload) { bind.beforeUpload = onBeforeUpload; @@ -124,6 +134,41 @@ watch(fileList, () => nextTick(() => addActionsListener()), { immediate: true }); + const loadBizFn = (params) => defHttp.get({ url: '/ktgl/kcKetangbiao/getBizPath', params }, { isTransformResponse: false }) + + function loadBiz(param){ + otherData.value.loading = true; + // otherClass.value = { 'upload-disabled': true, } + loadBizFn(param).then(res => { + //otherData.value + if(res.success){ + otherData.value.biz = res.result; + }else{ + delete otherData.value.biz; + } + // otherClass.value = {} + delete otherData.value.loading; + }); + } + + + watch(() => props.getBizParam, + (param, oldParam) => { + if(JSON.stringify(param) != JSON.stringify(oldParam)){ + nextTick(() => { + console.log(`🚀 ~ props.isGetBiz:`, props.isGetBiz, bindProps.value.disabled, loadBiz); + if(props.isGetBiz && !bindProps.value.disabled){ + //非被禁用状态,查询 + nextTick(() => { + loadBiz(param); + }); + } + }); + } + }, + { immediate: true } + ); + const antUploadItemCls = 'ant-upload-list-item'; // Listener @@ -299,7 +344,7 @@ }); } } else if (info.file.status === 'error') { - createMessage.error(`${info.file.name} 上传失败.`); + createMessage.error(`${info.file.name} 上传失败. ${info.file.response.message}`); } fileList.value = fileListTemp; if (info.file.status === 'done' || info.file.status === 'removed') { @@ -372,6 +417,296 @@ return path.substring(path.lastIndexOf('/') + 1); } + // //获取文件md5(仅支持小文件,,,弃用) + // function calculateFileMd5(file) { + // return new Promise((resolve, reject) => { + // const reader = new FileReader(); + // // 当文件被成功读取时调用onload事件 + // reader.onload = function (event) { + // const arrayBufferView = event.target.result; + // // 将ArrayBuffer转换为WordArray对象 + // const wordArray = CryptoJS.lib.WordArray.create(arrayBufferView); + // // 计算MD5哈希值 + // const md5Hash = CryptoJS.MD5(wordArray).toString(); + // resolve(md5Hash); + // }; + // // 当发生错误时调用onerror事件 + // reader.onerror = function () { + // reject('Failed to read file'); + // }; + // // 开始读取文件内容 + // reader.readAsArrayBuffer(file); + // }); + // } + + /** + * 计算文件md5(分批次计算,挺慢的,支持大文件) + */ + function calFileMd5(file){ + return new Promise((resolve, reject) => { + let md5 = CryptoJS.algo.MD5.create(); + //获取文件对象 + let reader = new FileReader(); + let step = 1024* 1024; + let total = file.size; + let cuLoaded = 0; + let time=1; + console.info("文件大小:" + file.size); + //读取一段成功 + reader.onload = (e) => { + //console.log("开始读取第"+time+"段"); + //处理读取的结果 + let wordArray = CryptoJS.lib.WordArray.create(reader.result); + md5.update(wordArray); + cuLoaded += e.loaded; + //如果没有读完,继续 + if (cuLoaded < total) { + readBlob(cuLoaded); + } else { + cuLoaded = total; + let hash = md5.finalize().toString(); + //document.getElementById("result").innerText=hash; + resolve(hash + buildUUID()); + } + time++; + } + reader.onerror = (e) => { + console.error('读取文件MD5出现错误 =>',e); + reject(e); + } + //开始读取 + readBlob(0); + //指定开始位置,分块读取文件 + function readBlob(start) { + //指定开始位置和结束位置读取文件 + let blob = file.slice(start, start + step); + reader.readAsArrayBuffer(blob); + } + }); + } + + function calFileUid(file){ + return new Promise((resolve, reject) => { + resolve(buildUUID()); + }); + } + + + + + //请求接口的数据返回 +// function upSearch(data){ +// return new Promise((resolve,reject)=>{ +// setTimeout(()=>{ +// console.log(data) +// resolve(data) +// },2000) +// }) +// } + +// function limitPromise(promiseList, limit){ + +// let init = Math.min(limit, promiseList.length) +// let promiseList = [] +// for(let i=0; i{ +// console.log('全部完成') +// }) +// } + +//limitUp([1,2,3,4,5,6,7,8], 3) + + + async function limitPromise(promises, limit) { + const results = [] + + for(let i=0; i<=promises.length; i+=limit) { + const tasks = [] + // 切片 Promise.all 的参数 + for(const promise of promises.slice(i, i + limit)) { + tasks.push(promise) + } + // 用 await 以确保后面的任务不会在执行当前任务时执行 + const result = await Promise.all(tasks) + results.push(...result) + } + + return results + } + + //分片大小 5m + const chunkSize = 5 * 1024 * 1024; + + const runBatchNum = 20; + + const uploadAxiosHttpConfig = { + //超时时间,分钟 * 秒 * 毫秒 60分钟 + timeout: 60 * 60 * 1000, + //不自动拼接前缀 + joinPrefix: false, + //自定义前缀 + apiUrl: baseUploadUrl, + //不自动处理 + isTransformResponse: false + } + + const defUploadFn = (params) => defHttp.post({ url: '/sys/common/upload', params }, uploadAxiosHttpConfig ); + + const bigFileUploadInit = (params) => defHttp.post({ url: '/sys/common/sectionUpload/init', params }, uploadAxiosHttpConfig ); + + const bigFileUploadUpload = (params) => defHttp.post({ url: '/sys/common/sectionUpload/upload', params }, uploadAxiosHttpConfig ); + + const bigFileUploadEnd = (params) => defHttp.post({ url: '/sys/common/sectionUpload/end', params }, uploadAxiosHttpConfig) + + /** + * 自定义文件上传方法,小于5m的走默认上传,大于5m的走切片上传 + */ + function uploadFn(customRequestData){ + const { data, file, onProgress, onSuccess, onError } = customRequestData; // 解构出a-upload的内置的方法(进度条,成功失败等) + + let fileSize = file.size; + let uploadPromiseList = []; + + if(fileSize <= chunkSize){ + let formData = new FormData(); + Object.keys(data).forEach(key => { + formData.append(key, data[key]); + }); + formData.append('file', file); + //传统直传 + defUploadFn(formData).then(res => { + console.log(`🚀 ~ 传统直传 - defUploadFn ~ res:`, res); + if(res.success){ + onProgress({ percent: 100 }, file) // 进度条 + onSuccess(res, file) // 上传文件的状态 + } else { + onError(res, res, file); + } + }).catch(err => { + console.log(`🚀 ~ 传统直传 - defUploadFn ~ err:`, err); + onError(err, err, file); + }); + } else { + //计算当前选择文件需要的分片数量 + const chunkCount = Math.ceil(fileSize / chunkSize); + console.log("文件大小:",(file.size / 1024 / 1024) + "Mb","分片数:",chunkCount); + //获取文件md5 + console.log('第一步,计算文件的md5',calFileMd5, file); + calFileMd5(file).then(fileMd5 => { + console.log(`🚀 ~ 文件的md5是:`, fileMd5); + const initUploadParams = { chunkCount, fileMd5 , ...data, }; + let successNum = 0; + + let taskAllList = []; + //切片上传(开始) + console.log('第二步,根据md5创建文件夹', initUploadParams); + bigFileUploadInit(initUploadParams).then(res => { + console.log('第三步,准备切片文件', res); + if(res.success){ + onProgress({ percent: 0 }, file); // 进度条 + //后台创建成功,下一步,分批次上传 + for (let i = 1; i <= chunkCount; i++) { + let uploadParams = { + partNumber: i, + ...initUploadParams, + file: null, + } + //分片开始位置 + let start = (i - 1) * chunkSize + //分片结束位置 + let end = Math.min(fileSize, start + chunkSize) + //取文件指定范围内的byte,从而得到分片数据 + let _chunkFile = file.slice(start, end) + console.log("第四步,开始准备文件第" + i + "个分片" + ",切片范围从" + start + "到" + end) + + uploadParams.file = _chunkFile; + let uploadFormData = new FormData(); + Object.keys(uploadParams).forEach(key => { + uploadFormData.append(key, uploadParams[key]); + }); + + console.log('第五步,准备切片文件上传参数', uploadFormData); + taskAllList.push(new Promise((resolve, reject) => { + console.log('第六步,准备切片文件上传前,', uploadFormData); + bigFileUploadUpload(uploadFormData).then(res => { + console.log('第七步,切片文件上传后,', uploadFormData, res); + if(res.success) { + successNum++; + let percent = Number(Number(successNum/chunkCount*100).toFixed(0)) + onProgress({ percent }, file) // 进度条 + resolve(res); + }else{ + reject(res); + } + }).catch(err => { + reject(err) + }); + })); + } + //任务分片,一次20片 + console.log('第八步,全部切片,根据指定线程数执行,', limitPromise, taskAllList); + limitPromise(taskAllList, runBatchNum).then(resList => { + console.log('第九步,全部切片,根据指定线程数执行完成后的结果,', resList); + //完成后再执行一个合并 + let endUploadParams = { + ...initUploadParams, + fileName: file.name, + } + console.log('第十步,全部切片,发生合并指令,', endUploadParams); + bigFileUploadEnd(endUploadParams).then(res => { + console.log(`第十一步 ~ 合并返回值 defHttp.post ~ res:`, res); + //返回跟单个上传一样的返回值, + if(res.success){ + onProgress({ percent: 100 }, file) // 进度条 + onSuccess(res, file) // 上传文件的状态 + } else { + onError(res, res, file); + } + + }).catch(err => { + console.error(`🚀 ~ 合并出现错误! defHttp.post ~ err:`, err); + onError(err, err, file); + }) + }).catch(err => { + console.error(`🚀 ~ 全部完成后,其中有错误limitPromise ~ err:`, err); + //有错误,, + onError(err, err, file); + }); + + } + //return + }) + + }) + + + } + + + // Promise.all(uploadPromiseList).then(resList => { + + // }); + } + defineExpose({ addActionsListener, }); diff --git a/src/router/routes/modules/zy/zy.ts b/src/router/routes/modules/zy/zy.ts index d6158dc..a454f52 100644 --- a/src/router/routes/modules/zy/zy.ts +++ b/src/router/routes/modules/zy/zy.ts @@ -65,7 +65,7 @@ const zuoye: AppRouteModule = { { path: 'jiaoXueDanYuanNeiRong', name: 'jiaoXueDanYuanNeiRong', - component: () => import('/@/views/zy/jiaoXueDanYuanNeiRong/index2.vue'), + component: () => import('/@/views/zy/jiaoXueDanYuanNeiRong/index.vue'), meta: { title: '教学单元内容', }, diff --git a/src/views/zy/jiaoXueDanYuanNeiRong/index.vue b/src/views/zy/jiaoXueDanYuanNeiRong/index.vue index 442729b..29dccf2 100644 --- a/src/views/zy/jiaoXueDanYuanNeiRong/index.vue +++ b/src/views/zy/jiaoXueDanYuanNeiRong/index.vue @@ -1,90 +1,108 @@