HcFileUploadLarge.vue 15 KB

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