基于vue的文件分片上传 demo
inleft
2022-03-19 9cf9fe104510022cc30deb5c194602d17881e77f
commit | author | age
76dbfd 1 <template>
I 2     <div id="global-uploader">
3         <!-- 上传 -->
4         <uploader ref="uploader" :options="options" :autoStart="false" @file-added="onFileAdded"
5             @file-success="onFileSuccess" @file-progress="onFileProgress" @file-error="onFileError"
6             @file-complete="onFileComplete" class="uploader-app">
7             <uploader-unsupport></uploader-unsupport>
8
9             <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
10
11             <uploader-list v-show="panelShow">
12                 <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
13                     <div class="file-title">
14                         <h2>文件列表</h2>
15                         <div class="operate">
16                             <el-button @click="fileListShow" type="text" :title="collapse ? '展开':'折叠' ">
17                                 <i class="iconfont" :class="collapse ? 'inuc-fullscreen': 'inuc-minus-round'"></i>
18                             </el-button>
19                             <el-button @click="close" type="text" title="关闭">
20                                 <i class="iconfont icon-close"></i>
21                             </el-button>
22                         </div>
23                     </div>
24
25                     <ul class="file-list">
26                         <li v-for="file in props.fileList" :key="file.id">
27                             <uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true">
28                             </uploader-file>
29                         </li>
30                         <div class="no-file" v-if="!props.fileList.length"><i class="iconfont icon-empty-file"></i>
31                             暂无待上传文件</div>
32                     </ul>
33                 </div>
34             </uploader-list>
35
36         </uploader>
37
38     </div>
39 </template>
40
41 <script>
42     /**
43      *   全局上传插件
44      *   调用方法:Bus.$emit('openUploader', {}) 打开文件选择框,参数为需要传递的额外参数
45      *   监听函数:Bus.$on('fileAdded', fn); 文件选择后的回调
46      *            Bus.$on('fileSuccess', fn); 文件上传成功的回调
47      */
48
49     import {
50         ACCEPT_CONFIG
51     } from '@/js/config';
52     import Bus from '@/js/bus';
53     import SparkMD5 from 'spark-md5';
54     import $ from 'jquery';
55     import Element from 'element-ui';
56
57     // 这两个是我自己项目中用的,请忽略
58     // import {Ticket} from '@/assets/js/utils';
59     // import api from '@/api';
60
61     export default {
62         data() {
63             return {
9cf9fe 64                 store: {
I 65
66                 },
67                 retrySet: {
68
69                 },
70                 paramSet: {},
76dbfd 71                 options: {
I 72                     //target: "http://192.168.40.222:8089/aa",
73                     chunkSize: 6 * 1024 * 1024,
74                     fileParameterName: 'upFile',
75                     forceChunkSize: true,
76                     maxChunkRetries: 3,
77                     testChunks: true, //是否开启服务器分片校验
78                     // 服务器分片校验函数,秒传及断点续传基础
79                     checkChunkUploadedByResponse: function(chunk, message) {
80                         let resp = JSON.parse(message);
81                         if (!resp.success) {
82                             console.error("校验分片异常")
83                             return false;
84                         }
85
86                         //已经秒传
9cf9fe 87                         let indexArray=resp.result.chunkIndex;
I 88                         if (resp.result.chunkIndex != null && indexArray[chunk.offset] > 0) {
76dbfd 89                             console.log(chunk.offset + 1 + "已经秒传..")
I 90                             return true;
91                         }
92                         console.log(chunk.offset + 1 + "正常上传..")
93
94                         return false;
95                     },
96                     headers: {
97                         // Authorization: Ticket.get() && "Bearer " + Ticket.get().access_token
98                     },
99                     query() {
100                         //aa: "bbb";
101                     }
102                 },
103                 attrs: {
104                     accept: ACCEPT_CONFIG.getAll()
105                 },
106                 panelShow: true, //选择文件后,展示上传panel
107                 collapse: true,
108             }
109         },
110         mounted() {
111             Bus.$on('openUploader', query => {
112                 this.params = query || {};
113
114                 if (this.$refs.uploadBtn) {
115                     $('#global-uploader-btn').click();
116                 }
117             });
118         },
119         computed: {
120             //Uploader实例
121             uploader() {
122                 return this.$refs.uploader.uploader;
123             }
124         },
125         methods: {
126             onFileAdded(file) {
127                 this.panelShow = true;
128                 this.computeMD5(file);
129
130                 Bus.$emit('fileAdded');
131             },
132
133             onFileComplete(rootFile) {
9cf9fe 134                 // console.log(11111)
I 135                 // console.log(rootFile)
76dbfd 136             },
I 137             onFileProgress(rootFile, file, chunk) {
9cf9fe 138
I 139                 // console.log("onFileProgress")
140                 // console.log(rootFile)
141                 // console.log(chunk)
76dbfd 142                 console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
I 143             },
144             onFileSuccess(rootFile, file, response, chunk) {
145                 let res = JSON.parse(response);
146                 console.log("上传完成回调" + response)
147                 // 服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
148                 if (!res.success) {
149                     this.$message({
150                         message: res.errmsg,
151                         type: 'error'
152                     });
153                     // 文件状态设为“失败”
154                     this.statusSet(file.id, 'failed');
155                     return
156                 }
157                 var $this = this;
158
159                 // 如果服务端返回需要合并 todo
160
161
162                 // 文件状态设为“合并中”
9cf9fe 163                 $this.statusRemove(file.id);
76dbfd 164                 $this.statusSet(file.id, 'merging');
I 165
166                 //请求合并
167                 this.$axios.get('http://localhost:8001/commonFileUpload/doMerge?mainMD5=' + res.result.mainMD5)
168                     .then(function(response) {
169
170                         if (response.data.success) {
171                             // 文件合并成功
172                             Bus.$emit('fileSuccess');
173                             $this.statusRemove(file.id);
174
175                             console.log('文件合并成功');
176                             console.log('上传成功');
177                             console.log("结束" + new Date().getTime())
178                         } else {
179                             //请求合并异常 
180                             $this.statusRemove(file.id);
181                             $this.statusSet(file.id, 'mergingExcetion');
182                         }
183
184                     })
185                     .catch(function(error) {
186                         console.log(error);
187                         $this.$message({
188                             message: "合并异常",
189                             type: 'error'
190                         });
191                         $this.statusRemove(file.id);
192                         $this.statusSet(file.id, 'mergingExcetion');
193                     });
194
195
196             },
197             onFileError(rootFile, file, response, chunk) {
198                 this.$message({
199                     message: response,
200                     type: 'error'
201                 })
202             },
203
204             /**
205              * 计算md5,实现断点续传及秒传
206              * @param file
207              */
208             computeMD5(file) {
209                 let fileReader = new FileReader();
210                 let time = new Date().getTime();
211                 console.log("开始" + new Date().getTime())
212                 let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
213
214                 let currentChunk = 0;
215                 const chunkSize = 6 * 1024 * 1024; //md5计算时切片大小
216                 let chunks = Math.ceil(file.size / chunkSize);
217
218                 let spark = new SparkMD5.ArrayBuffer();
219
220                 // 文件状态设为"计算MD5"
221                 this.statusSet(file.id, 'md5');
9cf9fe 222                 // file.pause();
76dbfd 223
I 224                 loadNext();
225
226                 fileReader.onload = (e => {
227                     spark.append(e.target.result);
228
229                     if (currentChunk < chunks) {
230                         currentChunk++;
231                         loadNext();
232
233                         // 实时展示MD5的计算进度
234                         this.$nextTick(() => {
235                             $(`.myStatus_${file.id}`).text('校验MD5 ' + ((currentChunk / chunks) * 100)
236                                 .toFixed(0) + '%')
237                         })
238                     } else {
239                         let md5 = spark.end();
240                         this.computeMD5Success(md5, file);
241                         console.log(
242                             `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`
243                         );
244                     }
245                 });
246
247                 fileReader.onerror = function() {
248                     this.error(`文件${file.name}读取出错,请检查该文件`)
249                     file.cancel();
250                 };
251
252                 function loadNext() {
253                     let start = currentChunk * chunkSize;
254                     let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
255
256                     fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
257                 }
258             },
259
260             computeMD5Success(md5, file) {
261                 //1.初始化分片上传请求,获取分片参数
262
263                 //计算内网ip是否联通,选择上传接口url,如果是
264
265                 let $this = this;
266                 this.$axios.post('http://localhost:8001/commonFileUpload/createShardPost', {
267                         mainMD5: md5,
268                         size: file.size,
269                         fileName: file.name,
270                         ext: file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length), //文件名没有点号会异常
271                         type: file.type,
272                     })
273                     .then(function(response) {
274                         console.log(response);
275                         if (response.data.success) {
276                             let uploadOptions = response.data.result;
277
278                             // 将自定义参数直接加载uploader实例的opts上
279
280                             let opts = $this.uploader.opts;
9cf9fe 281                             opts.target = "http://localhost:8001/commonFileUpload/doUpload"; //上传接口
I 282                             // opts.target = function(file, chunk, isTest, obj) {
283                             //     if (isTest) {
284                             //         return "http://localhost:8001/commonFileUpload/doUpload?mainMD5=" + file
285                             //             .uniqueIdentifier; //上传接口
286                             //     }
287                             //     return uploadOptions.uploadUrlList[chunk.offset];
288
289                             // }
76dbfd 290                             
9cf9fe 291                             // opts.processResponse = function(response, cb, file, chunk) {
I 292                             //     $this.$axios.get(
293                             //             'http://localhost:8001/commonFileUpload/updateProgress?mainMD5=' +
294                             //             file.uniqueIdentifier + "&chunk=" + (chunk.offset + 1) + "&chunkSize=" +
295                             //             chunk.loaded)
296                             //         .then(function(response) {
297                             //             cb(null, response.data);
298                             //         })
299                             //         .catch(function(error) {
300                             //             cb(null, error);
301                             //         });
302                             // }
303
304                             opts.processParams = function(params,file,chunk,isTest) {
305                                 let temp= $this.paramSet[file.uniqueIdentifier];
306                                 // chunkMD5:opts.chunkNumber,
307                                 // chunkNumber:opts.chunkNumber
308                                 temp.chunkMD5=chunk.offset+1;
309                                 temp.chunkNumber=chunk.offset+1;
310                                 return temp;
311                             }
76dbfd 312                             //     console.log("ddd")
I 313
314                             //     if (chunk.offset == 0) {
315                             //         return "http://192.168.40.149:9000/test2//temp/70d52b8181e78089797a1bd0986bbf98/70d52b8181e78089797a1bd0986bbf98-1.chuk?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20211203%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211203T094116Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=7bc2d34e1ea53d7b2c8b2954fe8b72a4715dd54f097c29f02d4f9eba2462ec96"
316                             //     }
317
318                             //     if (chunk.offset == 1) {
319                             //         return "http://192.168.40.149:9000/test2//temp/70d52b8181e78089797a1bd0986bbf98/70d52b8181e78089797a1bd0986bbf98-2.chuk?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20211203%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211203T094128Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=51cffbc815dc80d09b938fd460dfe4b54fbeddde50806ef45bc0f5665edc77c5"
320                             //     }
321
322                             //     return "http://localhost:8001/commonFileUpload/doUpload?off=" + (chunk
323                             //         .offset + 1);
324                             // }
325                             // opts.chunkSize = uploadOptions.chunkSize; //切片大小
326
9cf9fe 327                             // opts.method = "octet";
I 328                             opts.method = "multipart";
76dbfd 329                             opts.testMethod = "GET";
9cf9fe 330                             opts.uploadMethod = "POST";
76dbfd 331
I 332                             $this.params.mainMD5 = md5; //mainMD5参数
9cf9fe 333                             //$this.params.chunkNumber = opts.chunkNumber; //分片索引参数
76dbfd 334                             Object.assign(opts, {
I 335                                 query: {
336                                     ...$this.params,
337                                 }
338                             })
9cf9fe 339                             //文件重新分块
I 340                             file.uploader.opts.chunkSize = uploadOptions.chunkSize;
341                             file.bootstrap();
342                             $this.paramSet[md5] = {
343                                 mainMD5: md5,
344                                 chunkSize: uploadOptions.chunkSize,
345                                 // chunkMD5:opts.chunkNumber,
346                                 // chunkNumber:opts.chunkNumber
347                             }
76dbfd 348
I 349                             file.uniqueIdentifier = md5;
350                             file.resume();
351                             $this.statusRemove(file.id);
352                         } else {
353                             //中断上传
354                             file.cancel();
355                         }
356                     })
357                     .catch(function(error) {
358                         console.log(error);
359                         //中断上传
360                         file.cancel();
361                         return;
362                     });
363
364
365             },
366
367             fileListShow() {
368                 let $list = $('#global-uploader .file-list');
369
370                 if ($list.is(':visible')) {
371                     $list.slideUp();
372                     this.collapse = true;
373                 } else {
374                     $list.slideDown();
375                     this.collapse = false;
376                 }
377             },
378             close() {
379                 this.uploader.cancel();
380
381                 this.panelShow = false;
382             },
383
384             /**
385              * 新增的自定义的状态: 'md5'、'transcoding'、'failed'
386              * @param id
387              * @param status
388              */
389             statusSet(id, status) {
390                 let statusMap = {
391                     md5: {
392                         text: '校验MD5',
393                         bgc: '#fff'
394                     },
395                     merging: {
396                         text: '合并中',
397                         bgc: '#e2eeff'
398                     },
399                     mergingExcetion: {
400                         text: '合并异常',
401                         bgc: '#e2eeff'
402                     },
403                     transcoding: {
404                         text: '转码中',
405                         bgc: '#e2eeff'
406                     },
407                     failed: {
408                         text: '上传失败',
409                         bgc: '#e2eeff'
410                     }
411                 }
412
413                 this.$nextTick(() => {
414                     $(`<p class="myStatus_${id}"></p>`).appendTo(`.file_${id} .uploader-file-status`).css({
415                         'position': 'absolute',
416                         'top': '0',
417                         'left': '0',
418                         'right': '0',
419                         'bottom': '0',
420                         'zIndex': '1',
421                         'backgroundColor': statusMap[status].bgc
422                     }).text(statusMap[status].text);
423                 })
424             },
425             statusRemove(id) {
426                 this.$nextTick(() => {
427                     $(`.myStatus_${id}`).remove();
428                 })
429             },
430
431             error(msg) {
432                 this.$notify({
433                     title: '错误',
434                     message: msg,
435                     type: 'error',
436                     duration: 2000
437                 })
438             }
439         },
440         watch: {},
441         destroyed() {
442             Bus.$off('openUploader');
443         },
444         components: {}
445     }
446 </script>
447
448 <style>
449     #global-uploader {
450         position: fixed;
451         z-index: 20;
452         right: 15px;
453         bottom: 15px;
454     }
455
456     #global-uploader .uploader-app {
457         width: 1820px;
458     }
459
460     #global-uploader .file-panel {
461         background-color: #fff;
462         border: 1px solid #e2e2e2;
463         border-radius: 7px 7px 0 0;
464         box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
465     }
466
467     #global-uploader .file-panel .file-title {
468         display: flex;
469         height: 40px;
470         line-height: 40px;
471         padding: 0 15px;
472         border-bottom: 1px solid #ddd;
473     }
474
475     #global-uploader .file-panel .file-title .operate {
476         flex: 1;
477         text-align: right;
478     }
479
480     #global-uploader .file-panel .file-list {
481         position: relative;
482         height: 240px;
483         overflow-x: hidden;
484         overflow-y: auto;
485         background-color: #fff;
486     }
487
488     #global-uploader .file-panel .file-list>li {
489         background-color: #fff;
490     }
491
492     #global-uploader .file-panel.collapse .file-title {
493         background-color: #E7ECF2;
494     }
495
496     #global-uploader .no-file {
497         position: absolute;
498         top: 50%;
499         left: 50%;
500         transform: translate(-50%, -50%);
501         font-size: 16px;
502     }
503
504     #global-uploader .uploader-file-icon:before {
505         content: "" !important;
506     }
507
508     #global-uploader .uploader-file-actions>span {
509         margin-right: 6px;
510     }
511
512     #global-uploader .uploader-file-icon[icon=image] {
513         background: url(../images/image-icon.png);
514     }
515
516     #global-uploader .uploader-file-icon[icon=video] {
517         background: url(../images/video-icon.png);
518     }
519
520     #global-uploader .uploader-file-icon[icon=document] {
521         background: url(../images/text-icon.png);
522     }
523
524     /* 隐藏上传按钮 */
525     #global-uploader-btn {
526         position: absolute;
527         clip: rect(0, 0, 0, 0);
528     }
529 </style>