iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 21
0
Modern Web

用Angular打造完整後台系列 第 21

day21 ProductModule(一)

  • 分享至 

  • xImage
  •  

簡述

開始實作商品管理的功能模塊。

功能

productlist

  • 商品列表
  • 商品類別新增修改

檔案結構

-src
  |-app
    |-cms
       |-product
            |-dialog-type
                   |-dialog-type.component.html
                   |-dialog-type.component.css
                   |-dialog-type.component.ts
            |-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

實作

(一) 商品列表

productlistview

list/list.component.ts

export class ProductListComponent implements OnInit {
  PRODUCTSTATUS = PRODUCTSTATUS;
  isLoadingToggle = true; //讀取list的特效
  tab: ITabBase;          //此頁面元素資料
  result: IProduct[] = [];//顯示在畫面的list array
  ...

  constructor(
    public dialog: MatDialog,
    private route: ActivatedRoute,
    private dataService: DataService,
    private userService: UserService,
    private tabService: TabService
  ) {}

 /*get resolve */
  ngOnInit() {
    this.route.data.subscribe(resolversData => {
      this.tab = resolversData.listTab;
      if (!!this.tab) {
        this.init();
      }
    });
  }

  /*初始 */
  init() {
    if (!this.tab.pageObj) {
      this.tab.pageObj = <IPage>{
        pageIndex: 0,
        pageSize: PAGESIZE,
        length: 0
      };
    }
    this.setDatas(true);
    this.setTypes();
  }

  /*換分頁 */
  onSetPage(pageObj: IPage) {
    this.tab.pageObj.pageIndex = pageObj.pageIndex;
    this.tab.pageObj.pageSize = pageObj.pageSize;
    this.setDatas();
  }

 /*裝畫面資料 */
  setDatas(isDataInit = false) {
    this.isLoadingToggle = true;
    let url = this.dataService.setUrl(
                "products", 
                [{ key: "_expand", val: "type" }]
              );

    this.dataService.getData(url, this.tab.pageObj)
    .subscribe((data: IData) => {
      this.isLoadingToggle = false;
      if (!!data.errorcode) {
        this.openStatusDialog(data.errorcode);
      } else {
        if (!!data.res) {
          this.result = <IProduct[]>data.res;
          this.setLoadingDatas(isDataInit);
        }
      }
    });
  }

 /*儲存storage */
  setLoadingDatas(isDataInit = false) {
    if (!isDataInit) {
      this.tabService.nextTabMain(<ITabMain>{
        request: "update",
        content: this.tab
      });
    }
  }
  ...
}
  • 幾個時機點要重新裝畫面資料與其他功能模塊皆相同。

--

list/list.component.html

<div class="container">
  <div class="search-box">
    <div class="flex">
      <div class="search-action">
        <button class="button small-icons radius-5 pr-10 pink" 
        (click)="openDialog('insert')">
          <mat-icon>add</mat-icon>
          <span>{{ "insert" | translate }}</span>
        </button>
        <button class="button small-icons radius-5 pr-10 pink" 
        (click)="openDialog('type')">
          <mat-icon>add</mat-icon>
          <span>{{ "type_insert" | translate }}</span>
        </button>
      </div>
    </div>
  </div>
  <div class="content">
    <div class="content-container">
      <div class="content-main">
        <table class="table base">
          <thead>
            <tr>
              <th>
              <div><span>{{ "id" | translate }}</span></div>
              </th>
              <th>
              <div><span>{{ "product_img" | translate }}</span></div>
              </th>
              <th>
              <div><span>{{ "product_name" | translate }}</span></div>
              </th>
              <th>
              <div><span>{{ "product_type" | translate }}</span></div>
              </th>
              <th>
              <div><span>{{ "product_price" | translate }}</span></div>
              </th>
              <th>
              <div><span>{{ "status" | translate }}</span></div>
              </th>
              <th>
              <div><span>{{ "action" | translate }}</span></div>
              </th>
            </tr>
          </thead>

          <tbody *ngIf="!!result && !!result.length">
            <ng-container *ngFor="let r of result; let i = index">
              <tr>
                <td [attr.data-title]="'id' | translate">
                  <span>{{ r.id }}</span>
                </td>

                <td [attr.data-title]="'product_img' | translate">
                  <app-preview [imageSrc]="r.file"></app-preview>
                </td>

                <td [attr.data-title]="'product_name' | translate">
                  <span>{{ r.name }}</span>
                </td>

                <td [attr.data-title]="'product_type' | translate">
                  <span>{{ r.type.name }}</span>
                </td>

                <td [attr.data-title]="'product_price' | translate">
                  <span>{{ r.price }}</span>
                </td>

                <td [attr.data-title]="'status' | translate">
                  <span>{
                    { r.status | pipetag: PRODUCTSTATUS | translate 
                  </span>
                </td>

                <td>
                  <button (click)="openDialog('update', r)" 
                  class="button blue pb">
                    {{ "update" | translate }}
                  </button>
                </td>
              </tr>
            </ng-container>
          </tbody>
          <tbody *ngIf="!result || !result.length">
            <ng-container 
            *ngTemplateOutlet="empty; context: { $implicit: 7 }">
            </ng-container>
          </tbody>
        </table>
      </div>
    </div>
  </div>
  <div class="pages">
    <mat-paginator
      *ngIf="!!result && !!result.length"
      [pageIndex]="tab.pageObj.pageIndex"
      [length]="tab.pageObj.length"
      [pageSize]="tab.pageObj.pageSize"
      [pageSizeOptions]="PAGESIZEOPTIONS"
      (page)="onSetPage($event)"
    >
    </mat-paginator>
  </div>
</div>
<ng-template #empty let-col>
  <tr>
    <td [attr.colspan]="col" *ngIf="!isLoadingToggle">
      <span>{{ "nodata" | translate }}</span>
    </td>
    <td [attr.colspan]="col" *ngIf="isLoadingToggle">
      <app-loading></app-loading>
    </td>
  </tr>
</ng-template>

(二) 商品類別新增修改

productupdattype

dialog-type/dialog-type.component.ts

export class DialogProductTypeComponent implements OnInit {
  ipContent = new FormControl("", [Validators.required]);
  isUpdateNameIndex = 0;

  constructor(
    public dialogRef: MatDialogRef<DialogProductTypeComponent>,
    @Inject(MAT_DIALOG_DATA) public data: { typeNames: string[] }
  ) {}

  ngOnInit() {
    if (
      !this.data || 
      !this.data.typeNames || 
      !this.data.typeNames.length
    ) {
      this.data.typeNames = [];
    }
  }

  onDelete(index: number) {
    this.data.typeNames.splice(index, 1);
  }

  onInsert() {
    if (!!this.ipContent.value && this.ipContent.valid) {
      this.data.typeNames.push(this.ipContent.value);
      this.ipContent.setValue("");
    }
  }

  onSave(index: number, value: string) {
    this.data.typeNames[index] = value;
    this.isUpdateNameIndex = 0;
  }

  onNoClick() {
    this.dialogRef.close();
  }

  onEnter() {
    this.dialogRef.close(this.data.typeNames);
  }
}

--

dialog-type/dialog-type.component.html

<div mat-dialog-title class="flex center">
  <mat-icon svgIcon="alert"></mat-icon>
  <span> {{ "alert_product_type" | translate }} </span>
</div>

<div mat-dialog-content>
  <div class="flex center">
    <div style="margin-right:10px;">
      <input
        [formControl]="ipContent"
        [placeholder]="'import_product_type' | translate"
        style="margin-bottom:5px;"
      />
      <validation-messages [control]="ipContent"></validation-messages>
    </div>
    <button
      (click)="onInsert()"
      class="button pb pink radius insert"
      style="height: 35px;"
      [disabled]="!ipContent.valid"
      [ngClass]="{ disable: !ipContent.valid }"
    >
      {{ "insert" | translate }}
    </button>
  </div>
  <div class="flex center">
    <table class="table base" 
    *ngIf="!!data && !!data.typeNames && !!data.typeNames.length">
      <tbody>
        <tr *ngFor="let name of data.typeNames; let i = index">
          <td class="flex center" class="skin">
            <span *ngIf="isUpdateNameIndex != i + 1">
              {{ name }}
            </span>
            <input
              type="text"
              [value]="name"
              [hidden]="isUpdateNameIndex != i + 1"
              style="width:100px"
              #ipname
            />
            <button *ngIf="isUpdateNameIndex != i + 1" 
            (click)="isUpdateNameIndex = i + 1">
              {{ "update" | translate }}
            </button>
            <button *ngIf="isUpdateNameIndex == i + 1" 
            (click)="onSave(i, ipname.value)">
              {{ "save" | translate }}
            </button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</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()" class="button pb pink radius-5">
    {{ "enter" | translate }}
  </button>
</div>

--

list/list.component.ts

export class ProductListComponent implements OnInit {
  ...
  types: IType[] = [];

  constructor(...) {}

  ngOnInit() {...}

  /*初始 */
  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 */
  openTypeDialog() {
    if (!!this.types && !!this.types.length) {
      let obj = {
        typeNames: this.types.map((type: IType) => {
          return type.name;
        })
      };
      let dialogRef = this.dialog.open(DialogProductTypeComponent, {
        width: "600px",
        data: obj
      });

      dialogRef.afterClosed().subscribe((typeNames: string[]) => {
        if (!!typeNames && !!typeNames.length) {
          this.runAction(0, typeNames);
        }
      });
    }
  }

  /*
  遞迴迴圈,有在舊資料範圍內就是更新,
  在舊資料外並在新資料範圍內就是新增,
  超過新資料則遞迴停止
  */
  runAction(index: number, typeNames: string[]) {
    let leng = typeNames.length;
    let oldLeng = this.types.length;
    if (index < oldLeng) {
      this.updateTypeNames(index, typeNames);
    } else {
      if (index < leng) {
        this.insertTypeNames(index, typeNames);
      } else {
        this.setTypes();
        this.openStatusDialog(0);
      }
    }
  }

  updateTypeNames(index: number, typeNames: string[]) {
    this.dataService.updateOne(
      "types", 
      { name: typeNames[index] }, 
      index + 1
    )
    .subscribe(() => {
      this.runAction(++index, typeNames);
    });
  }

  insertTypeNames(index: number, typeNames: string[]) {
    this.dataService.insertOne(
      "types", 
      <IType>{ name: typeNames[index] }
    )
    .subscribe(() => {
      this.runAction(++index, typeNames);
    });
  }

  openStatusDialog(errorcode: number) {
    let dialogRef = this.dialog
    .open(DialogAlertComponent, {
      width: "250px",
      data: {
        errorcode: errorcode
      }
    });
    dialogRef.afterClosed().subscribe(() => {
      this.setDatas();
    });
  }
}

範例碼

此為完整專案範例碼,連線方式為json-server。

https://stackblitz.com/edit/ngcms-json-server

Start

一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json到本地端,
並下指令:

json-server db.json

json-server開啟成功後請連結此網址:
https://ngcms-json-server.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690


上一篇
day20 CustomerModule
下一篇
day22 ProductModule(二):Image
系列文
用Angular打造完整後台30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言