開始實作商品管理的上傳圖片、預覽圖片以及相關功能。
在商品新增修改上有出現一個上傳圖片
的功能,
我們會把上傳圖片
的功能拉出來做一個小組件,
在商品修增修改時將會出此小組件。
-src
|-app
|-cms
|-product
...
|-list
|-dialog-detail
|-dialog-detail.component.html
|-dialog-detail.component.css
|-dialog-detail.component.ts
|-file-uploader
|-file-uploader.component.html
|-file-uploader.component.css
|-file-uploader.component.ts
|-list.component.html
|-list.component.css
|-list.component.ts
|-product-routing.module.ts
|-product.component.ts
|-product.module.ts
file-uploader.component.ts
:export class FileUploaderComponent implements OnInit {
@Input() parent: FormGroup;
imageSrc: string = "null";
activeColor: string = "green";
baseColor: string = "#ccc";
overlayColor: string = "rgba(255,255,255,0.5)";
dragging: boolean = false;
loaded: boolean = false;
imageLoaded: boolean = false;
ngOnInit() {
if (!!this.parent) {
this.imageSrc = this.parent.value.file;
}
}
handleDragEnter() {
this.dragging = true;
}
handleDragLeave() {
this.dragging = false;
}
handleDrop(e) {
e.preventDefault();
this.dragging = false;
this.handleInputChange(e);
}
handleImageLoad() {
this.imageLoaded = true;
}
handleInputChange(e) {
let file = e.dataTransfer ?
e.dataTransfer.files[0] : e.target.files[0];
let pattern = /image-*/;
let reader = new FileReader();
if (!file.type.match(pattern)) {
alert("invalid format");
return;
}
this.loaded = false;
reader.onload = this._handleReaderLoaded.bind(this);
reader.readAsDataURL(file);
}
_handleReaderLoaded(e) {
let reader = e.target;
this.imageSrc = reader.result;
this.loaded = true;
this.parent.patchValue({
file: this.imageSrc
});
}
cancel() {
this.imageSrc = "";
}
}
這裡要注意的是,@Input() parent: FormGroup;
,
有來自於父組件的表單綁定跟驗證!
handleInputChange()
,做一些簡單的驗證格式。--
file-uploader.component.html
:<div class="item-wrapper two pink" [formGroup]="parent">
<div>
<span>*{{ "upload_img" | translate }}</span>
</div>
<div class="flex straight-flex uploader-box">
<div>
<label
class="uploader"
ondragover="return false;"
[class.loaded]="loaded"
[style.outlineColor]="dragging ? activeColor : baseColor"
(dragenter)="handleDragEnter()"
(dragleave)="handleDragLeave()"
(drop)="handleDrop($event)"
>
<i
class="icon icon-upload"
[style.color]="
dragging
? imageSrc.length > 0
? overlayColor
: activeColor
: imageSrc.length > 0
? overlayColor
: baseColor
"
></i>
<img [src]="imageSrc" (load)="handleImageLoad()"
[class.loaded]="imageLoaded" />
</label>
</div>
<div>
<input type="file" name="file" accept="image/*"
(change)="handleInputChange($event)" />
</div>
<div>
<button (click)="cancel()">
{{ "clear" | translate }}
</button>
</div>
</div>
</div>
--
file-uploader.component.css
:.uploader input {
display: none;
}
.uploader {
-webkit-align-items: center;
align-items: center;
background-color: #efefef;
background-color: rgba(0, 0, 0, 0.02);
cursor: pointer;
display: -webkit-flex;
display: flex;
height: 150px;
-webkit-justify-content: center;
justify-content: center;
outline: 1px dashed #ccc;
position: relative;
width: 150px;
}
.uploader img,
.uploader .icon {
pointer-events: none;
}
.uploader,
.uploader .icon {
-webkit-transition: all 100ms ease-in;
-moz-transition: all 100ms ease-in;
-ms-transition: all 100ms ease-in;
-o-transition: all 100ms ease-in;
transition: all 100ms ease-in;
}
.uploader .icon {
color: #eee;
color: rgba(0, 0, 0, 0.2);
font-size: 5em;
}
.uploader img {
left: 50%;
opacity: 0;
max-height: 100%;
max-width: 100%;
position: absolute;
top: 50%;
-webkit-transition: all 300ms ease-in;
-moz-transition: all 300ms ease-in;
-ms-transition: all 300ms ease-in;
-o-transition: all 300ms ease-in;
transition: all 300ms ease-in;
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.uploader img.loaded {
opacity: 1;
}
.uploader-box > *:not(:last-child) {
margin-bottom: 5px;
}
參考至 https://stackblitz.com/edit/angular-image-upload-base64?file=app%2Ffile-uploader.component.ts
參考至 https://medium.com/@amcdnl/file-uploads-with-angular-reactive-forms-960fd0b34cb5
dialog-detail.component.ts
:export interface IDetailProduct {
product: IProduct;
types: IType[];
}
@Component(...))
export class DialogProductDetailComponent implements OnInit {
PRODUCTSTATUS = PRODUCTSTATUS;
form: FormGroup;
constructor(
public dialogRef: MatDialogRef<DialogProductDetailComponent>,
@Inject(MAT_DIALOG_DATA) public data: IDetailProduct,
private fb: FormBuilder
) {}
ngOnInit() {
if (!!this.data && !!this.data.types) {
this.createForm();
if (!!this.data.product) {
this.editForm();
}
}
}
createForm() {
let obj = {
name: ["", [Validators.required]],
price: [
"",
[
Validators.required,
ValidationService.integerValidator
]
],
typeId: [
"",
[
Validators.required,
ValidationService.numberOnlyValidator
]
],
status: ["", [Validators.required]],
file: ["", [Validators.required]]
};
this.form = this.fb.group(obj);
}
editForm() {
let product = this.data.product;
this.form.patchValue({
name: product.name || "",
price: product.price.toString() || "",
typeId: product.typeId.toString() || "",
status: product.status.toString() || "",
file: product.file || ""
});
}
getDetailData(): IProduct {
return <IProduct>{
name: this.form.value.name,
price: +this.form.value.price,
typeId: +this.form.value.typeId,
status: +this.form.value.status,
file: this.form.value.file
};
}
onNoClick(): void {
this.dialogRef.close();
}
onEnter() {
this.dialogRef.close(this.getDetailData());
}
}
--
dialog-detail.component.html
:<form [formGroup]="form">
<div mat-dialog-title class="flex center">
<mat-icon svgIcon="alert"></mat-icon>
<span *ngIf="!data.product">
{{ "alert_product_insert" | translate }}
</span>
<span *ngIf="!!data.product">
{{ "alert_product_update" | translate }}
</span>
</div>
<div mat-dialog-content>
<div class="item-wrapper two pink">
<div>
<span> *{{ "name" | translate }} </span>
</div>
<div>
<input
type="text"
formControlName="name"
[placeholder]="'import_name' | translate"
required
/>
</div>
<validation-messages [control]="form.controls.name">
</validation-messages>
</div>
<div class="item-wrapper two pink">
<div>
<span> *{{ "product_price" | translate }} </span>
</div>
<div>
<input
type="text"
formControlName="price"
[placeholder]="'import_coin' | translate"
required
/>
</div>
<validation-messages [control]="form.controls.price">
</validation-messages>
</div>
<div class="item-wrapper two pink">
<div>
<span>*{{ "status" | translate }}</span>
</div>
<div>
<select name="se_status" id="se_status"
formControlName="status">
<option value="" disabled>
{{ "select" | translate }}
</option>
<option *ngFor="let status of PRODUCTSTATUS"
[value]="status.id">
{{status.name | translate}}
</option>
</select>
</div>
<validation-messages [control]="form.controls.status">
</validation-messages>
</div>
<div class="item-wrapper two pink">
<div>
<span>*{{ "product_type" | translate }}</span>
</div>
<div>
<select name="se_type" id="se_type" formControlName="typeId">
<option value="" disabled>{{ "select" | translate }}</option>
<option *ngFor="let type of data.types"
[value]="type.id">
{{type.name | translate}}
</option>
</select>
</div>
<validation-messages [control]="form.controls.typeId">
</validation-messages>
</div>
<file-uploader [parent]="form"></file-uploader>
</div>
<div mat-dialog-actions class="flex center">
<button (click)="onNoClick()" class="button pb radius-5"
style="margin-right:10px;">
{{ "cancel" | translate }}
</button>
<button
(click)="onEnter()"
[disabled]="!form.valid"
[ngClass]="{ disable: !form.valid }"
class="button pb pink radius-5"
>
{{ "enter" | translate }}
</button>
</div>
</form>
--
list.component.ts
:export class ProductListComponent implements OnInit {
...
types: IType[] = [];
constructor(...) {}
/*初始 */
init() {
if (!this.tab.pageObj) {
this.tab.pageObj = <IPage>{
pageIndex: 0,
pageSize: PAGESIZE,
length: 0
};
}
this.setDatas(true);
this.setTypes();
}
/*裝所有types資料 */
setTypes() {
let url = this.dataService.setUrl("types");
this.dataService.getData(url)
.subscribe((data: IData) => {
if (!!data.errorcode) {
this.openStatusDialog(data.errorcode);
} else {
if (!!data.res) {
this.types = <IType[]>data.res;
}
}
});
}
/*開Dialog */
openDialog(action: string, select?: IProduct) {
switch (action) {
case "insert":
this.openDetailDialog();
break;
case "update":
this.openDetailDialog(select);
break;
case "type":
this.openTypeDialog();
break;
}
}
/*開啟新增/更新的dialog */
openDetailDialog(select?: IProduct) {
if (!!this.types && !!this.types.length) {
let obj = <IDetailProduct>{
product: null,
types: this.types
};
if (!!select) {
obj.product = select;
}
let dialogRef = this.dialog
.open(DialogProductDetailComponent, {
width: "600px",
data: obj
});
dialogRef.afterClosed().subscribe((o: IProduct) => {
if (!!o) {
if (!select) {
this.insertProduct(o);
} else {
this.updateProduct(o, select);
}
}
});
}
}
/*新增商品 */
insertProduct(o: IProduct) {
let url = this.dataService.setUrl("products");
o = <IProduct>this.dataService.checkData(
o,
this.userService.getUser().id
);
this.dataService.insertOne(url, o)
.subscribe((data: IData) => {
this.openStatusDialog(data.errorcode);
});
}
/*修改商品 */
updateProduct(o: IProduct, select: IProduct) {
let url = this.dataService.setUrl(
"products",
null,
select.id
);
o = <IProduct>this.dataService.checkData(
o,
this.userService.getUser().id,
false
);
this.dataService.updateOne(url, o)
.subscribe((data: IData) => {
this.openStatusDialog(data.errorcode);
});
}
...
/*打開 alert-dialog 提示視窗 */
openStatusDialog(errorcode: number) {
let dialogRef = this.dialog.open(DialogAlertComponent, {
width: "250px",
data: {
errorcode: errorcode
}
});
dialogRef.afterClosed().subscribe(() => {
this.setDatas();
});
}
}
如下圖中的縮圖
-src
|-app
|-cms
|-modules
|-...
|-preview
|-preview.component.css
|-preview.component.html
|-preview.component.ts
|-preview.module.ts
preview.component.ts
:@Component({
selector: "app-preview",
templateUrl: "preview.component.html",
styleUrls: ["preview.component.css"]
})
export class PreviewComponent {
@Input() imageSrc: string = "";
imageLoaded: boolean = false;
constructor() {}
handleImageLoad() {
this.imageLoaded = true;
}
}
imageLoaded
是指圖片是否已加載完。--
preview.component.html
:<img *ngIf="imageSrc" [src]="imageSrc" (load)="handleImageLoad()" />
img {
width: 25px;
height: auto;
}
--
preview.module.ts
:@NgModule({
providers: [],
imports: [CommonModule],
declarations: [PreviewComponent],
exports: [PreviewComponent]
})
export class PreviewModule {}
實際運用時:
<td [attr.data-title]="'product_img' | translate">
<app-preview [imageSrc]="r.file"></app-preview>
</td>
此為完整專案範例碼,連線方式為json-server。
https://stackblitz.com/edit/ngcms-json-server
一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json
到本地端,
並下指令:
json-server db.json
json-server開啟成功後請連結此網址:
https://ngcms-json-server.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690