
1. Dependencies
- @angular/cdk
- @angular/material
2. file-upload组件
- file-upload.component.html - 1- <div class="upload-container"- 2- #uploadzone- 3- dropzone- 4- [class.hovering]="uploadzone.isHovering"- 5- (hovered)="uploadzone.isHovering = $event"- 6- (dropped)="onDrop($event)"- 7- (click)="onClick()">- 8- <p class="icon"><mat-icon>cloud_upload</mat-icon></p>- 9- <p class="title">Click or drag file to this area to upload</p>- 10- <p class="desc">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>- 11- </div>- 12- 13- <ul>- 14- <li *ngFor="let file of files" [@fadeInOut]="file.state">- 15- <mat-progress-bar [value]="file.progress"></mat-progress-bar>- 16- <span class="file-label">- 17- {{file.data.name}}- 18- <a title="Retry" (click)="retryFile(file)" *ngIf="file.canRetry">- 19- <mat-icon>refresh</mat-icon></a>- 20- <a title="Cancel" (click)="cancelFile(file)" *ngIf="file.canCancel">- 21- <mat-icon>cancel</mat-icon></a>- 22- </span>- 23- </li>- 24- </ul>- 25- 26- <input- 27- type="file"- 28- id="fileUpload"- 29- name="fileUpload"- 30- multiple="multiple"- 31- accept="{{accept}}"- 32- style="display:none;"/>
- file-upload.component.scss 
| 1 | @import '~@angular/material/theming'; | 
| 2 | |
| 3 | .upload-container { | 
| 4 |   display: flex; | 
| 5 |   flex-direction: column; | 
| 6 |   align-content: center; | 
| 7 |   justify-content: center; | 
| 8 |   text-align: center; | 
| 9 |   background: #fafafa; | 
| 10 |   border: 1px dashed #d9d9d9; | 
| 11 |   padding: 16px 0; | 
| 12 |   margin: 20px; | 
| 13 |   cursor: pointer; | 
| 14 |   p { | 
| 15 |     margin: 0; | 
| 16 |     padding: 0; | 
| 17 |   } | 
| 18 |   .icon { | 
| 19 |     margin-bottom: 20px; | 
| 20 |   } | 
| 21 |   .title { | 
| 22 |     margin: 0 0 4px; | 
| 23 |     color: rgba(0,0,0,.85); | 
| 24 |     font-size: 16px; | 
| 25 |   } | 
| 26 |   .desc { | 
| 27 |     color: rgba(0,0,0,.45); | 
| 28 |     font-size: 14px; | 
| 29 |   } | 
| 30 | } | 
| 31 | |
| 32 | .hovering { | 
| 33 |   border: 1px dashed tomato; | 
| 34 | } | 
| 35 | |
| 36 | ul, | 
| 37 | li { | 
| 38 |     margin: 0 20px; | 
| 39 |     padding: 0; | 
| 40 |     list-style: none; | 
| 41 |     mat-progress-bar { | 
| 42 |         border-radius: 10px; | 
| 43 |     } | 
| 44 |     .file-label { | 
| 45 |         display: inline-flex; | 
| 46 |         vertical-align: middle; | 
| 47 |         font-size: 12px; | 
| 48 |         line-height: 18px; | 
| 49 |         justify-content: flex-end; | 
| 50 |         width: 100%; | 
| 51 |         padding-top: 5px; | 
| 52 |         mat-icon { | 
| 53 |             font-size: 18px; | 
| 54 |             text-align: center; | 
| 55 |         } | 
| 56 |         mat-icon:hover { | 
| 57 |             color: map-get($mat-red, 500); | 
| 58 |         } | 
| 59 |         a { | 
| 60 |             margin-left: 4px; | 
| 61 |             cursor: pointer; | 
| 62 |         } | 
| 63 |     } | 
| 64 | } | 
- file-upload.component.ts
| 1 | import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; | 
| 2 | import { | 
| 3 |   trigger, | 
| 4 |   state, | 
| 5 |   style, | 
| 6 |   animate, | 
| 7 |   transition | 
| 8 | } from "@angular/animations"; | 
| 9 | import { | 
| 10 |   HttpClient, | 
| 11 |   HttpResponse, | 
| 12 |   HttpRequest, | 
| 13 |   HttpEventType, | 
| 14 |   HttpErrorResponse | 
| 15 | } from "@angular/common/http"; | 
| 16 | import { of } from "rxjs"; | 
| 17 | import { catchError, last, map, tap } from "rxjs/operators"; | 
| 18 | import { FileUploadModel } from './file-upload.model'; | 
| 19 | |
| 20 | @Component({ | 
| 21 |   selector: "app-file-upload", | 
| 22 |   templateUrl: "./file-upload.component.html", | 
| 23 |   styleUrls: ["./file-upload.component.scss"], | 
| 24 |   animations: [ | 
| 25 |     trigger("fadeInOut", [ | 
| 26 |       state("in", style({ opacity: 100 })), | 
| 27 |       transition("* => void", [animate(300, style({ opacity: 0 }))]) | 
| 28 |     ]) | 
| 29 |   ] | 
| 30 | }) | 
| 31 | export class FileUploadComponent implements OnInit { | 
| 32 |   /** Link text */ | 
| 33 |   @Input() text = "Upload"; | 
| 34 |   /** Name used in form which will be sent in HTTP request. */ | 
| 35 |   @Input() param = "file"; | 
| 36 |   /** Target URL for file uploading. */ | 
| 37 |   @Input() target = "https://file.io"; | 
| 38 |   /** File extension that accepted, same as 'accept' of <input type="file" />. By the default, it's set to 'image/*'. */ | 
| 39 |   @Input() accept = "image/*"; | 
| 40 |   /** Allow you to add handler after its completion. Bubble up response text from remote. */ | 
| 41 |   @Output() complete = new EventEmitter<string>(); | 
| 42 | |
| 43 |   private files: Array<FileUploadModel> = []; | 
| 44 | |
| 45 |   constructor(private _http: HttpClient) {} | 
| 46 | |
| 47 |   ngOnInit() {} | 
| 48 | |
| 49 |   onDrop(files: FileList) { | 
| 50 |     for (let index = 0; index < files.length; index++) { | 
| 51 |       const file = files[index]; | 
| 52 |       this.files.push({ | 
| 53 |         data: file, | 
| 54 |         state: "in", | 
| 55 |         inProgress: false, | 
| 56 |         progress: 0, | 
| 57 |         canRetry: false, | 
| 58 |         canCancel: true | 
| 59 |       }); | 
| 60 |       this.uploadFiles(); | 
| 61 |     } | 
| 62 |   } | 
| 63 | |
| 64 |   onClick() { | 
| 65 |     const fileUpload = document.getElementById( | 
| 66 |       "fileUpload" | 
| 67 |     ) as HTMLInputElement; | 
| 68 |     fileUpload.onchange = () => { | 
| 69 |       for (let index = 0; index < fileUpload.files.length; index++) { | 
| 70 |         const file = fileUpload.files[index]; | 
| 71 |         this.files.push({ | 
| 72 |           data: file, | 
| 73 |           state: "in", | 
| 74 |           inProgress: false, | 
| 75 |           progress: 0, | 
| 76 |           canRetry: false, | 
| 77 |           canCancel: true | 
| 78 |         }); | 
| 79 |       } | 
| 80 |       this.uploadFiles(); | 
| 81 |     }; | 
| 82 |     fileUpload.click(); | 
| 83 |   } | 
| 84 | |
| 85 |   cancelFile(file: FileUploadModel) { | 
| 86 |     file.sub.unsubscribe(); | 
| 87 |     this.removeFileFromArray(file); | 
| 88 |   } | 
| 89 | |
| 90 |   retryFile(file: FileUploadModel) { | 
| 91 |     this.uploadFile(file); | 
| 92 |     file.canRetry = false; | 
| 93 |   } | 
| 94 | |
| 95 |   private uploadFile(file: FileUploadModel) { | 
| 96 |     const fd = new FormData(); | 
| 97 |     fd.append(this.param, file.data); | 
| 98 | |
| 99 |     const req = new HttpRequest("POST", this.target, fd, { | 
| 100 |       reportProgress: true | 
| 101 |     }); | 
| 102 | |
| 103 |     file.inProgress = true; | 
| 104 |     file.sub = this._http | 
| 105 |       .request(req) | 
| 106 |       .pipe( | 
| 107 |         map(event => { | 
| 108 |           switch (event.type) { | 
| 109 |             case HttpEventType.UploadProgress: | 
| 110 |               file.progress = Math.round((event.loaded * 100) / event.total); | 
| 111 |               break; | 
| 112 |             case HttpEventType.Response: | 
| 113 |               return event; | 
| 114 |           } | 
| 115 |         }), | 
| 116 |         tap(message => {}), | 
| 117 |         last(), | 
| 118 |         catchError((error: HttpErrorResponse) => { | 
| 119 |           file.inProgress = false; | 
| 120 |           file.canRetry = true; | 
| 121 |           return of(`${file.data.name} upload failed.`); | 
| 122 |         }) | 
| 123 |       ) | 
| 124 |       .subscribe((event: any) => { | 
| 125 |         if (typeof event === "object") { | 
| 126 |           this.removeFileFromArray(file); | 
| 127 |           this.complete.emit(event.body); | 
| 128 |         } | 
| 129 |       }); | 
| 130 |   } | 
| 131 | |
| 132 |   private uploadFiles() { | 
| 133 |     const fileUpload = document.getElementById( | 
| 134 |       "fileUpload" | 
| 135 |     ) as HTMLInputElement; | 
| 136 |     fileUpload.value = ""; | 
| 137 | |
| 138 |     this.files.forEach(file => { | 
| 139 |       this.uploadFile(file); | 
| 140 |     }); | 
| 141 |   } | 
| 142 | |
| 143 |   private removeFileFromArray(file: FileUploadModel) { | 
| 144 |     const index = this.files.indexOf(file); | 
| 145 |     if (index > -1) { | 
| 146 |       this.files.splice(index, 1); | 
| 147 |     } | 
| 148 |   } | 
| 149 | |
| 150 | } | 
- file-upload.model.ts
| 1 | import { Subscription } from "rxjs"; | 
| 2 | |
| 3 | export class FileUploadModel { | 
| 4 |   data: File; | 
| 5 |   state: string; | 
| 6 |   inProgress: boolean; | 
| 7 |   progress: number; | 
| 8 |   canRetry: boolean; | 
| 9 |   canCancel: boolean; | 
| 10 |   sub?: Subscription; | 
| 11 | } | 
3. drop指令
- dropzone.directive.ts
| 1 | import { Directive, HostListener, Output, EventEmitter } from "@angular/core"; | 
| 2 | |
| 3 | @Directive({ | 
| 4 |   selector: "[dropzone]" | 
| 5 | }) | 
| 6 | export class DropzoneDirective { | 
| 7 |   @Output() dropped = new EventEmitter<FileList>(); | 
| 8 |   @Output() hovered = new EventEmitter<boolean>(); | 
| 9 | |
| 10 |   @HostListener("drop", ["$event"]) | 
| 11 |   onDrop($event) { | 
| 12 |     $event.preventDefault(); | 
| 13 |     this.dropped.emit($event.dataTransfer.files); | 
| 14 |     this.hovered.emit(false); | 
| 15 |   } | 
| 16 | |
| 17 |   @HostListener("dragover", ["$event"]) | 
| 18 |   onDragOver($event) { | 
| 19 |     $event.preventDefault(); | 
| 20 |     this.hovered.emit(true); | 
| 21 |   } | 
| 22 | |
| 23 |   @HostListener("dragleave", ["$event"]) | 
| 24 |   onDragLeave($event) { | 
| 25 |     $event.preventDefault(); | 
| 26 |     this.hovered.emit(false); | 
| 27 |   } | 
| 28 | } |