HcFileUploadLarge.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <template>
  2. <el-upload ref="uploadRef" class="hc-file-upload-box" :headers="getTokenHeader()" :data="uploadData" :disabled="uploadDisabled" multiple :show-file-list="false" :http-request="uploadFileHandle"
  3. :on-success="uploadSuccess" :on-exceed="uploadExceed" :on-error="uploadError" :before-upload="beforeUpload" :on-progress="uploadprogress">
  4. <slot></slot>
  5. </el-upload>
  6. </template>
  7. <script setup>
  8. import {ref,watch,onMounted} from "vue";
  9. import {getTokenHeader} from '~src/api/request/header';
  10. import {isSize, deepClone, getObjValue} from "vue-utils-plus"
  11. import md5 from 'js-md5' //引入MD5加密
  12. import ossApi from "~api/oss";
  13. const props = defineProps({
  14. datas: {
  15. type: Object,
  16. default: () => ({})
  17. },
  18. api: {
  19. type: String,
  20. default: "/api/blade-resource/oss/endpoint/"
  21. },
  22. action: {
  23. type: String,
  24. default: "upload-file2"
  25. },
  26. accept: {
  27. type: String,
  28. default: "image/png,image/jpg,image/jpeg,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/pdf,.doc,.docx,application/msword"
  29. },
  30. size: {
  31. type: Number,
  32. default: 60
  33. }
  34. })
  35. //变量
  36. const uploadRef = ref(null)
  37. const uploadData = ref(props.datas)
  38. const uploadDisabled = ref(false)
  39. //监听
  40. watch(() => [
  41. props.datas,
  42. ], ([datas]) => {
  43. uploadData.value = datas
  44. })
  45. //渲染完成
  46. onMounted(()=> {
  47. beforeFileNum.value = 0
  48. finishFileNum.value = 0
  49. errorFileNum.value = 0
  50. })
  51. //事件
  52. const emit = defineEmits(['change', 'progress'])
  53. //上传前
  54. const beforeFileNum = ref(0)
  55. const beforeUpload = async (file) => {
  56. beforeFileNum.value ++;
  57. return true;
  58. }
  59. //超出限制时
  60. const uploadExceed = () => {
  61. window?.$message?.warning(`请上传 ${props.accept} 格式的文件,文件大小不超过${props.size}M`);
  62. }
  63. //上传中
  64. const uploadprogress = () => {
  65. uploadDisabled.value = true
  66. emit('progress', true)
  67. }
  68. //上传完成
  69. const finishFileNum = ref(0)
  70. const uploadSuccess = (response, uploadFile, uploadFiles) => {
  71. //console.log(response)
  72. //console.log(uploadFile)
  73. //console.log(uploadFiles)
  74. finishFileNum.value ++;
  75. if (beforeFileNum.value === finishFileNum.value) {
  76. const fileList = getUploadFile(deepClone(uploadFiles))
  77. uploadClearFiles()
  78. emit('change', {type: 'success', fileList})
  79. emit('progress', false)
  80. }
  81. }
  82. //上传失败
  83. const errorFileNum = ref(0)
  84. const uploadError = (error,uploadFile,uploadFiles) => {
  85. errorFileNum.value ++;
  86. window?.$message?.error('上传失败');
  87. console.log(error)
  88. const num = finishFileNum.value + errorFileNum.value;
  89. if (beforeFileNum.value === num) {
  90. const fileList = getUploadFile(deepClone(uploadFiles))
  91. uploadClearFiles()
  92. emit('change', {type: 'error', fileList})
  93. emit('progress', false)
  94. }
  95. }
  96. const uploadClearFiles = () => {
  97. finishFileNum.value = 0
  98. beforeFileNum.value = 0
  99. errorFileNum.value = 0
  100. uploadDisabled.value = false
  101. uploadRef.value?.clearFiles()
  102. }
  103. //获取文件
  104. const getUploadFile = (fileList) => {
  105. let fileArr = [];
  106. for (let i = 0; i < fileList.length; i++) {
  107. const item = getObjValue(fileList[i]?.response?.data)
  108. fileArr.push(item)
  109. }
  110. return fileArr
  111. }
  112. const uploadFileHandle = (options) =>{
  113. console.log(options)
  114. uploadByPieces(options)
  115. }
  116. /**
  117. * 文件分片上传
  118. * @params file {File} 文件
  119. * @params pieceSize {Number} 分片大小 默认3MB
  120. * @params concurrent {Number} 并发数量 默认2
  121. * @params process {Function} 进度回调函数
  122. * @params success {Function} 成功回调函数
  123. * @params error {Function} 失败回调函数
  124. */
  125. const uploadByPieces = ({
  126. file,
  127. pieceSize = 5,
  128. concurrent = 3,
  129. onSuccess:success,
  130. onProgress:process,
  131. onError:error
  132. }) => {
  133. // 如果文件传入为空直接 return 返回
  134. if (!file || file.length < 1) {
  135. return error('文件不能为空')
  136. }
  137. let fileMD5 = '' // 总文件列表
  138. const chunkSize = pieceSize * 1024 * 1024 // 1MB一片
  139. const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
  140. const chunkList = [] // 分片列表
  141. let uploaded = [] // 已经上传的
  142. let fileType = '' // 文件类型
  143. // 获取md5
  144. /***
  145. * 获取md5
  146. **/
  147. const readFileMD5 = () => {
  148. // 读取视频文件的md5
  149. fileType = file.name.substring(file.name.lastIndexOf('.') + 1, file.name.length)
  150. console.log('获取文件的MD5值')
  151. let fileRederInstance = new FileReader()
  152. console.log('file', file)
  153. fileRederInstance.readAsBinaryString(file)
  154. fileRederInstance.addEventListener('load', e => {
  155. let fileBolb = e.target.result
  156. fileMD5 = md5(fileBolb)
  157. var index = file.name.lastIndexOf('.')
  158. var tp = file.name.substring(index + 1, file.name.length)
  159. let form = new FormData()
  160. form.append('filename', file.name)
  161. //form.append('file', new File([], 'filename'))
  162. form.append('identifier', fileMD5)
  163. form.append('objectType', fileType)
  164. form.append('chunkNumber', 1)
  165. ossApi.uploadChunk(form).then(res => {
  166. if (res.skipUpload) {
  167. console.log('文件已被上传')
  168. success && success(res)
  169. } else {
  170. // 判断是否是断点续传
  171. if (res.uploaded && res.uploaded.length != 0) {
  172. uploaded = [].concat(res.uploaded)
  173. }
  174. console.log('已上传的分片:' + uploaded)
  175. // 判断是并发上传或顺序上传
  176. if (concurrent == 1 || chunkCount == 1) {
  177. console.log('顺序上传')
  178. sequentialUplode(0)
  179. } else {
  180. console.log('并发上传')
  181. concurrentUpload()
  182. }
  183. }
  184. }).catch((e) => {
  185. console.log('文件合并错误')
  186. console.log(e)
  187. })
  188. })
  189. }
  190. /***
  191. * 获取每一个分片的详情
  192. **/
  193. const getChunkInfo = (file, currentChunk, chunkSize) => {
  194. let start = currentChunk * chunkSize
  195. let end = Math.min(file.size, start + chunkSize)
  196. let chunk = file.slice(start, end)
  197. return {
  198. start,
  199. end,
  200. chunk
  201. }
  202. }
  203. /***
  204. * 针对每个文件进行chunk处理
  205. **/
  206. const readChunkMD5 = () => {
  207. // 针对单个文件进行chunk上传
  208. for (var i = 0; i < chunkCount; i++) {
  209. const {
  210. chunk
  211. } = getChunkInfo(file, i, chunkSize)
  212. // 判断已经上传的分片中是否包含当前分片
  213. if (uploaded.indexOf(i + '') == -1) {
  214. uploadChunk({
  215. chunk,
  216. currentChunk: i,
  217. chunkCount
  218. })
  219. }
  220. }
  221. }
  222. /***
  223. * 原始上传
  224. **/
  225. const uploadChunk = (chunkInfo) => {
  226. var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
  227. console.log(sd, '进度')
  228. process(sd)
  229. console.log(chunkInfo, '分片大小')
  230. let inde = chunkInfo.currentChunk + 1
  231. if (uploaded.indexOf(inde + '') > -1) {
  232. const {
  233. chunk
  234. } = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
  235. uploadChunk({
  236. chunk,
  237. currentChunk: inde,
  238. chunkCount
  239. })
  240. } else {
  241. var index = file.name.lastIndexOf('.')
  242. var tp = file.name.substring(index + 1, file.name.length)
  243. // 构建上传文件的formData
  244. let fetchForm = new FormData()
  245. fetchForm.append('identifier', fileMD5)
  246. fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
  247. fetchForm.append('chunkSize', chunkSize)
  248. fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
  249. const chunkfile = new File([chunkInfo.chunk], file.name)
  250. fetchForm.append('file', chunkfile)
  251. // fetchForm.append('file', chunkInfo.chunk)
  252. fetchForm.append('filename', file.name)
  253. fetchForm.append('relativePath', file.name)
  254. fetchForm.append('totalChunks', chunkInfo.chunkCount)
  255. fetchForm.append('totalSize', file.size)
  256. fetchForm.append('objectType', tp)
  257. // 执行分片上传
  258. let config = {
  259. headers: {
  260. 'Content-Type': 'application/json',
  261. 'Accept': '*/*'
  262. }
  263. }
  264. ossApi.uploadChunk(fetchForm, config).then(res => {
  265. if (res.code == 200) {
  266. console.log('分片上传成功')
  267. uploaded.push(chunkInfo.currentChunk + 1)
  268. // 判断是否全部上传完
  269. if (uploaded.length == chunkInfo.chunkCount) {
  270. console.log('全部完成')
  271. success(res)
  272. process(100)
  273. } else {
  274. const {
  275. chunk
  276. } = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
  277. uploadChunk({
  278. chunk,
  279. currentChunk: chunkInfo.currentChunk + 1,
  280. chunkCount
  281. })
  282. }
  283. } else {
  284. console.log(res.msg)
  285. }
  286. }).catch((e) => {
  287. error && error(e)
  288. })
  289. // if (chunkInfo.currentChunk < chunkInfo.chunkCount) {
  290. //   setTimeout(() => {
  291. //
  292. //   }, 1000)
  293. // }
  294. }
  295. }
  296. /***
  297. * 顺序上传
  298. **/
  299. const sequentialUplode = (currentChunk) => {
  300. const {
  301. chunk
  302. } = getChunkInfo(file, currentChunk, chunkSize)
  303. let chunkInfo = {
  304. chunk,
  305. currentChunk,
  306. chunkCount
  307. }
  308. var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
  309. process(sd)
  310. console.log('当前上传分片:' + currentChunk)
  311. let inde = chunkInfo.currentChunk + 1
  312. if (uploaded.indexOf(inde + '') > -1) {
  313. console.log('分片【' + currentChunk + '】已上传')
  314. sequentialUplode(currentChunk + 1)
  315. } else {
  316. let uploadData = createUploadData(chunkInfo)
  317. let config = {
  318. headers: {
  319. 'Content-Type': 'application/json',
  320. 'Accept': '*/*'
  321. }
  322. }
  323. // 执行分片上传
  324. ossApi.uploadChunk(uploadData, config).then(res => {
  325. if (res.code == 200) {
  326. console.log('分片【' + currentChunk + '】上传成功')
  327. uploaded.push(chunkInfo.currentChunk + 1)
  328. // 判断是否全部上传完
  329. if (uploaded.length == chunkInfo.chunkCount) {
  330. console.log('全部完成')
  331. success(res)
  332. process(100)
  333. } else {
  334. sequentialUplode(currentChunk + 1)
  335. }
  336. } else {
  337. console.log(res.msg)
  338. }
  339. }).catch((e) => {
  340. error && error(e)
  341. })
  342. }
  343. }
  344. /***
  345. * 并发上传
  346. **/
  347. const concurrentUpload = () => {
  348. for (var i = 0; i < chunkCount; i++) {
  349. chunkList.push(Number(i))
  350. }
  351. console.log('需要上传的分片列表:' + chunkList)
  352. concurrentExecution(chunkList, concurrent, (curItem) => {
  353. return new Promise((resolve, reject) => {
  354. const {
  355. chunk
  356. } = getChunkInfo(file, curItem, chunkSize)
  357. let chunkInfo = {
  358. chunk,
  359. currentChunk: curItem,
  360. chunkCount
  361. }
  362. var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
  363. process(sd)
  364. console.log('当前上传分片:' + curItem)
  365. let inde = chunkInfo.currentChunk + 1
  366. if (uploaded.indexOf(inde + '') == -1) {
  367. // 构建上传文件的formData
  368. let uploadData = createUploadData(chunkInfo)
  369. // 请求头
  370. let config = {
  371. headers: {
  372. 'Content-Type': 'application/json',
  373. 'Accept': '*/*'
  374. }
  375. }
  376. ossApi.uploadChunk(uploadData, config).then(res => {
  377. if (res.code == 200) {
  378. uploaded.push(chunkInfo.currentChunk + 1)
  379. console.log('已经上传完成的分片:' + uploaded)
  380. // 判断是否全部上传完
  381. // if (uploaded.length == chunkInfo.chunkCount) {
  382. // success(res)
  383. // process(100)
  384. // }
  385. if(typeof res.data == 'object'){
  386. success(res)
  387. process(100)
  388. }
  389. resolve()
  390. } else {
  391. reject(res)
  392. console.log(res.msg)
  393. }
  394. }).catch((e) => {
  395. reject(res)
  396. error && error(e)
  397. })
  398. } else {
  399. console.log('分片【' + chunkInfo.currentChunk + '】已上传')
  400. resolve()
  401. }
  402. })
  403. }).then(res => {
  404. console.log('finish', res)
  405. }).catch((e)=>{
  406. error && error(e)
  407. })
  408. }
  409. /***
  410. * 创建文件上传参数
  411. **/
  412. const createUploadData = (chunkInfo) => {
  413. let fetchForm = new FormData()
  414. fetchForm.append('identifier', fileMD5)
  415. fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
  416. fetchForm.append('chunkSize', chunkSize)
  417. fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
  418. const chunkfile = new File([chunkInfo.chunk], file.name)
  419. fetchForm.append('file', chunkfile)
  420. // fetchForm.append('file', chunkInfo.chunk)
  421. fetchForm.append('filename', file.name)
  422. fetchForm.append('relativePath', file.name)
  423. fetchForm.append('totalChunks', chunkInfo.chunkCount)
  424. fetchForm.append('totalSize', file.size)
  425. fetchForm.append('objectType', fileType)
  426. return fetchForm
  427. }
  428. readFileMD5() // 开始执行代码
  429. }
  430. /**
  431. * 并发执行
  432. * @params list {Array} - 要迭代的数组
  433. * @params limit {Number} - 并发数量控制数,最好小于3
  434. * @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
  435. * @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
  436. */
  437. const concurrentExecution = (list, limit, asyncHandle)=>{
  438. // 递归执行
  439. let recursion = (arr) => {
  440. // 执行方法 arr.shift() 取出并移除第一个数据
  441. return asyncHandle(arr.shift()).then(() => {
  442. // 数组还未迭代完,递归继续进行迭代
  443. if (arr.length !== 0) {
  444. return recursion(arr)
  445. } else {
  446. return 'finish'
  447. }
  448. })
  449. }
  450. // 创建新的并发数组
  451. let listCopy = [].concat(list)
  452. // 正在进行的所有并发异步操作
  453. let asyncList = []
  454. limit = limit > listCopy.length ? listCopy.length : limit
  455. console.log(limit)
  456. while (limit--) {
  457. asyncList.push(recursion(listCopy))
  458. }
  459. // 所有并发异步操作都完成后,本次并发控制迭代完成
  460. return Promise.all(asyncList)
  461. }
  462. </script>
  463. <style lang="scss">
  464. .hc-file-upload-box .el-upload-list .el-upload-list__item {
  465. .el-upload-list__item-status-label, .el-icon--close-tip {
  466. display: none;
  467. }
  468. }
  469. </style>