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