2/22 实现一个图片上传的 Vue 组件( 完善版本 )

写在前面

  之前有一个关于这个的 随笔

  但是没有 优化 过

  这个是简单优化过的版本 

  暂时只有代码 


  1 <template>
  2   <div id="app">
  3     <div class="container">
  4       <div class="title-container">
  5         <span>上传文件</span>
  6       </div>
  7 
  8       <div class="upload-box">
  9         <!--  上传的那个框框 -->
 10         <div
 11           class="drag-box"
 12           ref="dragBox"
 13           :class="isDrag ? 'draging-drag-box' : ''"
 14         >
 15           <label>
 16             <div class="icon-container">
 17               <i
 18                 class="fa fa-file"
 19                 :class="isDrag ? 'draging-fa-file' : ''"
 20               ></i>
 21             </div>
 22             <div class="label-text-container">
 23               <span :class="isDrag ? 'draging-label-text-container' : ''"
 24                 >请将文件拖动至此</span
 25               >
 26               <br />
 27               <span :class="isDrag ? 'draging-label-text-container' : ''"
 28                 >或者单击上传</span
 29               >
 30             </div>
 31             <input
 32               type="file"
 33               accept="image/jepg, image/png"
 34               name="pictures"
 35               @change="storepreFiles($event.target)"
 36               multiple
 37             />
 38           </label>
 39         </div>
 40 
 41         <div class="show-file-box" v-if="preFiles.length == 0 ? false : true">
 42           <div class="text-container">
 43             <span>上传列表</span>
 44             <span>{{ updateState }} / {{ preFilesLength }}</span>
 45           </div>
 46           <!-- 渲染出来的情况 有一个进度条 -->
 47           <div class="pre-upload-bar">
 48             <div>
 49               <span
 50                 >列表加载进度
 51                 {{ (updateState / preFilesLength).toFixed(1) * 100 }} %</span
 52               >
 53             </div>
 54             <div
 55               class="pre-upload-bar-done"
 56               :style="{  (updateState / preFilesLength) * 100 + '%' }"
 57             ></div>
 58           </div>
 59           <!-- <div v-if="updateState === preFilesLength ? true : false"> -->
 60 
 61           <ul class="pre-upload-list">
 62             <transition-group tag="li" name="list">
 63               <li
 64                 class="pre-upload-file"
 65                 v-for="file in preFiles"
 66                 :key="file.name"
 67               >
 68                 <div class="review">
 69                   <!-- 图片预览 -->
 70                   <div class="review-image-container">
 71                     <img class="review-image" :src="file.src" />
 72                   </div>
 73 
 74                   <div class="review-file-name-container">
 75                     <span class="file-name"> {{ file.name }}</span>
 76                   </div>
 77                   <div class="progress">
 78                     <div
 79                       class="progress-done"
 80                       :style="{  file.index + '%' }"
 81                     ></div>
 82                   </div>
 83                   <span class="percent">{{ file.index }}%</span>
 84                   <input
 85                     v-if="uploadStatu === 0 ? true : false"
 86                     class="select-box"
 87                     check
 88                     type="checkbox"
 89                     name="select"
 90                     :value="file.name"
 91                     :checked="file.isChecked"
 92                     @change="doSelect(file.name)"
 93                     ref="select"
 94                     :id="file.name"
 95                   />
 96                   <label :for="file.name" style="margin-left:40px"></label>
 97                 </div>
 98               </li>
 99             </transition-group>
100           </ul>
101 
102           <hr color="lightpink" style="margin: 10px 8px" />
103           <!-- 选择按钮 -->
104           <div class="bottom">
105             <div
106               class="bottom-shelter"
107               v-if="updateState != preFilesLength"
108             ></div>
109             <div class="bottom-content">
110               <div class="bottom-select">
111                 <span
112                   class="bottom-select-button"
113                   @click="seletAll"
114                   v-if="uploadStatu === 0 ? true : false"
115                 >
116                   {{ select }}</span
117                 >
118                 <div class="bottom-select-info">
119                   <div>
120                     <span>已选</span>
121                     <span
122                       class="span-special"
123                       :style="{ color: showSize() > maxSize ? 'red' : 'green' }"
124                       >{{ showSelect() }}
125                     </span>
126                     <span>/{{ preFilesLength }}</span>
127                   </div>
128                   <div>
129                     <span
130                       class="span-special"
131                       :style="{ color: showSize() > maxSize ? 'red' : 'green' }"
132                       >{{ showSize() }}
133                     </span>
134                     <span>/ {{ showAllSize() }} mb </span>
135                   </div>
136                 </div>
137               </div>
138 
139               <div
140                 class="upload-statu-info"
141                 v-if="uploadStatu == 0 ? false : true"
142               >
143                 <span>{{ uploadStatu == 1 ? "上传ing" : "" }}</span>
144                 <span>{{ uploadStatu == 2 ? "上传完成" : "" }}</span>
145               </div>
146 
147               <div
148                 class="bottom-upload"
149                 @click="upload"
150                 v-if="uploadStatu === 0 ? true : false"
151               >
152                 <span>上传</span>
153               </div>
154 
155               <div
156                 class="bottom-upload"
157                 @click="continueUpload"
158                 v-if="uploadStatu === 2 ? true : false"
159               >
160                 <span>清空列表</span>
161               </div>
162             </div>
163           </div>
164         </div>
165       </div>
166     </div>
167   </div>
168 </template>
169 
170 <script>
171 // import { delete } from 'vue/types/umd';
172 export default {
173   name: "upload",
174   props: {
175     userId: {
176       type: String,
177       validator(value) {
178         //  判断String是否为空
179         return value === "" ? false : true;
180       },
181       default: "",
182     },
183 
184     maxSize: {
185       type: Number,
186       validator(value) {
187         return value > 0 ? true : false;
188       },
189       default: 10,
190     },
191   },
192   data() {
193     return {
194       // 预展示的图片对象数组
195       preFiles: [],
196 
197       // 渲染列表
198       preFilesLength: 0,
199 
200       // 这个是渲染list的过程 渲染到第几个li
201       updateState: 0,
202 
203       // 真正要上传的图片文件
204       files: [],
205 
206       // 选择上传的文件 之前想到了更改 但是没有成功 就留下一个响应的变量 可以删除的
207       select: "全选",
208 
209       // 是否进行拖动事件
210       isDrag: false,
211 
212       // 上传进度 0 表示还未开始 1 表示开始上传 2 表示结束上传
213       uploadStatu: 0,
214     };
215   },
216   updated() {},
217   mounted: function () {
218     var dragBox = this.$refs.dragBox;
219     dragBox.addEventListener("dragenter", this.onDrag, false);
220     dragBox.addEventListener("dragover", this.onDrag, false);
221     dragBox.addEventListener(
222       "dragleave",
223       () => {
224         this.isDrag = false;
225       },
226       false
227     );
228     dragBox.addEventListener("drop", this.onDrop, false);
229   },
230   watch: {
231     // 通过对 preFiles 监听 实现简单加载文件进度条
232     preFiles: {
233       handler: "updateS",
234     },
235   },
236   methods: {
237     storepreFiles(obj) {
238       // 如果当前的上传状态是 0-未上传
239       if (this.uploadStatu === 0) {
240         // 获取input里面的文件组
241         var fileList = obj.files;
242         // 先判定有没有重复提交的文件 可以加判断条件
243         for (let i = 0; i < fileList.length; i++) {
244           for (let j = 0; j < this.preFiles.length; j++) {
245             if (this.preFiles[j].name === fileList[i].name) {
246               alert("有文件重复");
247               return;
248             }
249           }
250         }
251 
252         //  这个是处理 显示 有多少个文件在列表的情况 ( 因为可以在未上传的时候 继续添加 )
253         this.preFilesLength = this.preFilesLength + fileList.length;
254 
255         // 进行整合 ( 如果直接用push的话 这样会变成 两个Filelist 对象 组合 )
256         for (var i = 0; i < fileList.length; i++) {
257           this.files = this.files.concat(fileList[i]);
258         }
259 
260         // 下面的作用域会变
261         var vue = this;
262 
263         function uploadFile(i) {
264           return new Promise(function (resolve, reject) {
265             let reader = new FileReader();
266             reader.readAsDataURL(fileList[i]);
267             reader.onload = function () {
268               console.log("readed");
269               resolve(this.result);
270             };
271           });
272         }
273         doUpload();
274         async function doUpload() {
275           for (let i = 0; i < fileList.length; i++) {
276             let result = await uploadFile(i);
277             console.group(i);
278             console.groupEnd();
279             vue.preFiles.push({
280               name: fileList[i].name,
281               size: fileList[i].size,
282               index: 0,
283               src: result,
284               isChecked: true,
285             });
286           }
287         }
288       } else {
289         alert("已经上传完毕!");
290       }
291     },
292 
293     // 渲染列表的参数
294     updateS() {
295       if (this.updateState >= this.preFilesLength) {
296         // this.preFilesLength = this.preFilesLength - this.preFiles.filter(item => item.isChecked == false).length;
297         this.updateState =
298           this.updateState - (this.preFilesLength - this.preFiles.length);
299       } else {
300         this.updateState = this.updateState + 1;
301       }
302     },
303 
304     findPreFile(name) {
305       let that = this;
306       return this.preFiles.find((item, index) => {
307         return item.name === name;
308       });
309     },
310 
311     // 点击 选择框 和 全选
312     doSelect(name) {
313       this.preFiles.forEach((element) => {
314         if (element.name == name) {
315           element.isChecked = !element.isChecked;
316         }
317       });
318     },
319     seletAll() {
320       if (this.$refs.select.find((item) => item.checked == false)) {
321         console.log(this.$refs.select);
322         this.preFiles.forEach((element) => {
323           element.isChecked = true;
324         });
325       }
326     },
327 
328     // 显示 选中个数 和 显示 选中的文件大小
329     showSelect() {
330       let num = 0;
331       this.preFiles.forEach((element) => {
332         if (element.isChecked == true) {
333           num++;
334         }
335       });
336       return num;
337     },
338 
339     showSize() {
340       let size = 0;
341       this.preFiles.forEach((element) => {
342         if (element.isChecked == true) {
343           size = size + element.size;
344         }
345       });
346       return (size / Math.pow(2, 20)).toFixed(2);
347     },
348 
349     showAllSize() {
350       let size = 0;
351       this.preFiles.forEach((element) => {
352         size = size + element.size;
353       });
354       return (size / Math.pow(2, 20)).toFixed(2);
355     },
356 
357     // 拖动文件上传
358     onDrag: function (e) {
359       this.isDrag = true;
360       // 取消默认事件
361       e.stopPropagation();
362       e.preventDefault();
363     },
364 
365     onDragLeave(e) {},
366 
367     onDrop: function (e) {
368       this.isDrag = false;
369       e.stopPropagation();
370       e.preventDefault();
371       var dt = e.dataTransfer;
372       this.storepreFiles(dt);
373     },
374 
375     // 继续文件上传 慎重使用这个方法
376     continueUpload() {
377       this.uploadStatu = 0;
378       this.preFiles = [];
379       this.files = [];
380       this.preFilesLength = this.files.length;
381       this.updateState = 0;
382     },
383     // 文件上传
384     // 确定上传文件 通过  preFiles 的 isChecked 属性 取得每个文件的 name 和 真正要上传的文件的 name 交叉对比
385     checkFiles() {
386       console.log(this.preFiles);
387       let vue = this;
388       this.preFiles.forEach((element) => {
389         console.log(element.isChecked);
390         if (element.isChecked === false) {
391           delete vue.files[vue.preFiles.indexOf(element)];
392           console.log("doing check");
393         }
394       });
395       console.log("doing do");
396       this.files = this.files.filter((item) => item != "undefined");
397       console.log(this.preFiles.filter((item) => item.isChecked == true));
398       // this.preFiles = this.preFiles.filter((item)=>{ item.isChecked == true})
399       this.preFilesLength = this.files.length;
400       this.updateState = this.files.length;
401     },
402     upload() {
403       if (this.showSize() == 0) {
404         alert("您 啥也没选 选个毛");
405       }
406       if (this.maxSize < this.showSize()) {
407         alert("文件太大!请重新选择!");
408       } else {
409         let vue = this;
410         function firstDo() {
411           return new Promise(function (resolve, reject) {
412             if (vue.preFilesLength != 0) {
413               vue.checkFiles();
414               vue.preFilesLength = vue.files.length;
415               console.log("updating " + vue.preFiles);
416             }
417             resolve();
418           });
419         }
420 
421         firstDo().then(() => {
422           vue.files.forEach((element) => {
423             vue.uploadSingleFile(element);
424           });
425           vue.uploadStatu = 2;
426         });
427       }
428     },
429 
430     uploadSingleFile(file) {
431       // 先找到匹配的 预览文件 位置 方便写入 index
432 
433       let position = 0;
434       let vue = this;
435       this.preFiles.forEach((element) => {
436         if (element.name == file.name) {
437           position = vue.preFiles.indexOf(element);
438         }
439       });
440 
441       this.uploadStatu = 1;
442 
443       let param = new FormData(); // 创建form对象
444       param.append("file", file); // 通过append向form对象添加数据
445       console.log(param.get("file")); // FormData私有类对象,访问不到,可以通过get判断值是否传进去
446       vue.axios
447         .post("/demo/upload_test", param, {
448           // 给个请求头,让后端知道应该怎么处理数据
449           headers: {
450             "Content-Type": "multipart/form-data",
451           },
452           onUploadProgress: (progressEvent) => {
453             let processStatu =
454               ((progressEvent.loaded / progressEvent.total) * 100) | 0;
455             vue.preFiles[position].index = processStatu;
456           },
457         })
458         .then((response) => {
459           vue.preFiles = vue.preFiles.filter((item) => item.isChecked == true);
460           console.log(response.data);
461         });
462     },
463   },
464 };
465 </script>
466 
467 <style scoped>
468 span {
469   font-weight: 700;
470   font: bolder;
471 }
472 
473 .container {
474   width: 450px;
475   /* background-color: rgb(195, 209, 228); */
476   box-shadow: 2px 2px 2px 2px rgba(68, 68, 68, 0.2);
477   border-radius: 10px;
478   padding: 20px;
479 }
480 
481 .upload-box {
482   /*  400px; */
483   /* height: 400px; */
484 }
485 
486 .title-container {
487   margin: 10px 8px 20px 8px;
488   font-size: 20px;
489 
490   border-radius: 5px;
491   background: linear-gradient(to right, rgb(112, 158, 242), rgb(255, 255, 255));
492   color: white;
493 }
494 
495 .drag-box {
496   padding: 20px;
497   margin: 10px auto;
498   width: 380px;
499   border-radius: 10px;
500   border: 5px dashed rgba(112, 155, 248, 0.7);
501   text-align: center;
502   transition: all linear 0.1s;
503 }
504 
505 .label-text-container {
506   margin: 20px 8px;
507   font-size: 20px;
508 }
509 
510 .text-container {
511   margin: 20px 8px;
512   font-size: 20px;
513   border-radius: 5px;
514   background: linear-gradient(to right, rgb(112, 158, 242), rgb(255, 255, 255));
515   color: white;
516 }
517 
518 .drag-box .label-text-container {
519   color: rgba(190, 190, 190, 0.8);
520   transition: all linear 0.1s;
521 }
522 
523 .icon-container {
524   margin: 15px;
525 }
526 
527 .fa-file {
528   color: rgba(247, 187, 219, 0.8);
529   font-size: 100px;
530   transition: all linear 0.1s;
531 }
532 
533 /*  当进入文件的时候添加css */
534 .drag-box:hover {
535   border: 5px dashed rgba(112, 155, 248, 1);
536 }
537 
538 .drag-box:hover .fa-file {
539   color: rgb(245, 124, 188);
540 }
541 
542 .drag-box:hover .label-text-container {
543   color: rgb(68, 68, 68);
544 }
545 
546 /*  当拖动文件的时候添加css */
547 .draging-drag-box {
548   border: 5px dashed rgba(112, 155, 248, 1);
549 }
550 
551 .draging-fa-file {
552   color: rgb(245, 124, 188);
553 }
554 
555 .draging-label-text-container {
556   color: rgb(68, 68, 68);
557 }
558 
559 /*  点击上传文件的时候的那个input */
560 label input {
561   display: none;
562 }
563 
564 .review {
565   border: 1px solid transparent;
566   border-radius: 5px;
567   color: #777;
568   display: flex;
569   font-size: 12px;
570   align-items: center;
571   padding: 10px;
572   margin: 5px 8px;
573 }
574 
575 .review:hover {
576   cursor: pointer;
577   /* border: 1px solid #ddd; */
578   box-shadow: 0 3px 10px -5px rgba(0, 0, 0, 0.7);
579 }
580 
581 .pre-upload-bar {
582   margin: 10px 10px;
583   height: 20px;
584 }
585 
586 .pre-upload-bar span {
587   font-size: 12px;
588   font-weight: 20px;
589   color: #777;
590 }
591 
592 .pre-upload-bar-done {
593   background: linear-gradient(to left, rgb(112, 158, 242), rgb(119, 140, 255));
594   box-shadow: 0 3px 3px -5px rgb(100, 115, 143), rgb(134, 138, 165);
595   border-radius: 5px;
596   height: 3px;
597   width: 0;
598   transition: width ease 0.2s;
599 }
600 
601 .review-file-name-container {
602   width: 100px;
603 }
604 
605 .review-image-container {
606   margin: 0 8px 0 0;
607 }
608 
609 .review-image {
610   width: 50px;
611 }
612 
613 .pre-upload-list {
614   list-style: none;
615   /*  取消缩进 */
616   margin: 0px;
617   padding: 0px;
618 }
619 
620 .file-name {
621   width: 100%;
622   float: left;
623   overflow: hidden;
624   text-overflow: ellipsis;
625   white-space: normal;
626 }
627 
628 .progress {
629   background-color: rgba(100, 100, 100, 0.2);
630   border-radius: 5px;
631   position: relative;
632   margin: 0 10px;
633   height: 10px;
634   width: 150px;
635 }
636 
637 .progress-done {
638   background: linear-gradient(to left, rgb(242, 112, 156), rgb(255, 148, 114));
639   box-shadow: 0 3px 3px -5px rgb(242, 112, 156), 0 2px 5px rgb(242, 112, 156);
640   border-radius: 5px;
641   height: 10px;
642   width: 0;
643   transition: width ease 0.1s;
644 }
645 
646 .select-box {
647   width: 20px;
648   height: 20px;
649   margin: 0 0 0 36px;
650 }
651 
652 /* list的transition group */
653 .list-enter,
654 .list-leave-to {
655   opacity: 0;
656 }
657 
658 .list-enter-active {
659   animation: moveIn 1s;
660 }
661 
662 .list-leave-active {
663   animation: moveOut 1s;
664   /* transition: all 1s linear; */
665 }
666 
667 @keyframes moveIn {
668   0% {
669     opacity: 0;
670     transform: translate(30px, 15px);
671   }
672 
673   30% {
674     opacity: 0.5;
675     transform: translate(0px, 15px);
676   }
677 
678   100% {
679     opacity: 1;
680     transform: translate(0, 0px);
681   }
682 }
683 
684 @keyframes moveOut {
685   0% {
686     opacity: 1;
687     transform: translate(0, 0px);
688   }
689 
690   30% {
691     opacity: 0.5;
692     transform: translate(0px, 15px);
693   }
694 
695   100% {
696     opacity: 0;
697     transform: translate(30px, 15px);
698   }
699 }
700 .upload-statu-info {
701   padding: 0 10px;
702   margin: 3px 8px;
703 }
704 .upload-statu-info span {
705   letter-spacing: 5px;
706   font-weight: 300px;
707 }
708 
709 .bottom-select {
710   margin: 5px 8px;
711   padding: 0 10px;
712   height: 40px;
713 }
714 
715 .bottom-select .bottom-select-button {
716   cursor: pointer;
717   color: white;
718   margin: 5px;
719   padding: 7px;
720   float: right;
721   transition: all linear 0.1s;
722   background-color: rgb(247, 159, 172);
723   border-radius: 5px;
724 }
725 
726 .bottom-select-info span {
727   font-size: 15px;
728   font-weight: 200;
729 }
730 
731 .bottom-select-info .span-special {
732   font-weight: 1000;
733 }
734 
735 .bottom-upload {
736   margin: 5% auto;
737   width: 160px;
738   height: 40px;
739   text-align: center;
740   padding: 5px;
741   border-radius: 5px;
742   background-color: lightpink;
743   color: white;
744   cursor: pointer;
745 }
746 
747 .bottom-upload span {
748   letter-spacing: 5px;
749   margin: auto;
750   font-size: 30px;
751 }
752 
753 .bottom {
754   position: relative;
755   width: 450px;
756   height: 150px;
757 }
758 
759 .bottom-shelter {
760   position: absolute;
761   width: 100%;
762   height: 150px;
763   cursor: not-allowed;
764   z-index: 1;
765 }
766 
767 .bottom-content {
768   width: 100%;
769   position: absolute;
770   z-index: 0;
771 }
772 
773 /* 复选框 */
774 input[type="checkbox"] + label::before {
775   content: "a0"; /* non-break space */
776   display: inline-block;
777   width: 20px;
778   height: 20px;
779   border-radius: 3px;
780   border: solid 2px rgba(0, 117, 255, 0.4);
781   background: rgb(255, 255, 255);
782   line-height: 20px;
783   vertical-align: middle;
784 }
785 
786 input[type="checkbox"]:checked + label::before {
787   /* content: "2713"; */
788   content: "✔";
789   text-align: center;
790   top: 20px;
791   font-size: 24px;
792   background: lightpink;
793   display: table-cell;
794   color: aliceblue;
795 }
796 
797 input[type="checkbox"] {
798   position: absolute;
799   clip: rect(0, 0, 0, 0);
800 }
801 </style>
Let it roll
原文地址:https://www.cnblogs.com/WaterMealone/p/14431328.html