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 | } |