iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
0
Modern Web

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

day20 CustomerModule

簡述

開始實作會員管理的功能模塊。

功能

customerlist

  • 會員列表
  • 會員新增修改
  • 會員等級新增修改

檔案結構

-src
  |-app
    |-cms
       |-customer
            |-dialog-level
                   |-dialog-level.component.html
                   |-dialog-level.component.css
                   |-dialog-level.component.ts
            |-list
               |-dialog-detail
                   |-dialog-detail.component.html
                   |-dialog-detail.component.css
                   |-dialog-detail.component.ts
               |-list.component.html
               |-list.component.css
               |-list.component.ts
           |-customer-routing.module.ts
           |-customer.component.ts
           |-customer.module.ts

實作

(一) 管理者列表

customerlistview

list/list.component.ts

export class CustomerListComponent implements OnInit {
  ACCOUNTSTATUS = ACCOUNTSTATUS;
  isLoadingToggle = true;   //讀取list的特效
  tab: ITabBase;            //此頁面元素資料
  result: ICustomer[] = []; //顯示在畫面的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.setLevelDatas();
  }

  /*換分頁 */
  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(
      "customers", [{ key: "_expand", val: "level" }]
    );

    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 = <ICustomer[]>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('level')">
          <mat-icon>add</mat-icon>
          <span>{{ "level_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 class="sticky-col">
                <div><span>{{ "account" | translate }}</span></div>
              </th>
              <th>
                <div><span>{{ "name" | translate }}</span></div>
              </th>
              <th>
                <div><span>{{ "insert_date" | translate }}</span></div>
              </th>
              <th>
                <div><span>{{ "level_type" | 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]="'account' | translate" 
                class="sticky-col">
                  <span>{{ r.account }}</span>
                </td>
                <td [attr.data-title]="'name' | translate">
                  <span>{{ r.name }}</span>
                </td>
                <td [attr.data-title]="'insert_date' | translate">
                  <span>{{ r.inserted | pipetime }}</span>
                </td>
                <td [attr.data-title]="'level_type' | translate">
                  <span>{{ r.level.name }}</span>
                </td>
                <td [attr.data-title]="'status' | translate">
                  <span>
                    {{ r.status | pipetag: ACCOUNTSTATUS | 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>

(二) 會員新增修改

customerupdateview

dialog-detail/dialog-detail.component.ts

export interface IDetailCustomer {
  customer: ICustomer;
  levels: ILevel[];
}

@Component({
  ...
})
export class DialogCustomerDetailComponent implements OnInit {
  ACCOUNTSTATUS = ACCOUNTSTATUS;
  form: FormGroup;

  constructor(
    public dialogRef: MatDialogRef<DialogCustomerDetailComponent>,
    @Inject(MAT_DIALOG_DATA) public data: IDetailCustomer,
    private fb: FormBuilder
  ) {}

  ngOnInit() {
    if (!!this.data && !!this.data.levels) {
      this.createForm();
      if (!!this.data.customer) {
        this.editForm();
      }
    }
  }

  createForm() {
    let obj = {
      name: ["", Validators.required],
      levelId: ["", Validators.required],
      status: ["", Validators.required]
    };
    if (!this.data.customer) {
      //insert
      obj["account"] = [
        "",
        [
          Validators.required, 
          Validators.minLength(6), 
          ValidationService.userValidator
        ]
      ];
      obj["password"] = [
        "",
        [
          Validators.required, 
          Validators.minLength(6), 
          ValidationService.userValidator
        ]
      ];
    }
    this.form = this.fb.group(obj);
  }

  editForm() {
    let customer = this.data.customer;
    this.form.setValue({
      name: customer.name || "",
      levelId: customer.levelId.toString() || "",
      status: customer.status.toString() || ""
    });
  }

  getDetailData(): ICustomer {
    return <ICustomer>{
      account: this.form.value.account,
      password: this.form.value.password,
      name: this.form.value.name,
      levelId: +this.form.value.levelId,
      status: +this.form.value.status
    };
  }

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

  onEnter() {
    this.dialogRef.close(this.getDetailData());
  }
}

--

dialog-detail/dialog-detail.component.html

<form [formGroup]="form">
  <div mat-dialog-title class="flex center">
    <mat-icon svgIcon="alert"></mat-icon>
    <span *ngIf="!data.customer"> 
      {{ "alert_customer_insert" | translate }} 
    </span>
    <span *ngIf="!!data.customer"> 
      {{ "alert_customer_update" | translate }} 
    </span>
  </div>
  <div mat-dialog-content>
    <div class="item-wrapper two pink" *ngIf="!data.customer">
      <div>
        <span>*{{ "account" | translate }}</span>
      </div>
      <div>
        <input
          type="text"
          formControlName="account"
          [placeholder]="'import_account' | translate"
          required
        />
      </div>
      <validation-messages [control]="form.controls.account">
      </validation-messages>
    </div>
    <div class="item-wrapper two pink" *ngIf="!data.customer">
      <div>
        <span> *{{ "password" | translate }} </span>
      </div>
      <div>
        <input
          type="password"
          formControlName="password"
          [placeholder]="'import_password' | translate"
          required
        />
      </div>
      <validation-messages [control]="form.controls.password">
      </validation-messages>
    </div>

    <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>*{{ "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 ACCOUNTSTATUS" 
          [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>*{{ "level_type" | translate }}</span>
      </div>
      <div>
        <select name="se_level" id="se_level" 
        formControlName="levelId">

          <option value="" disabled>{{ "select" | translate }}</option>
          <option *ngFor="let level of data.levels" [value]="level.id">
            {{ level.name | translate }}
          </option>
          
        </select>
      </div>
      <validation-messages [control]="form.controls.levelId">
      </validation-messages>
    </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()"
      [disabled]="!form.valid"
      [ngClass]="{ disable: !form.valid }"
      class="button pb pink radius-5"
    >
      {{ "enter" | translate }}
    </button>
  </div>
</form>

--

list/list.component.ts

export class CustomerListComponent implements OnInit {
  ...
  levels: ILevel[] = [];  //所有等級類別

  constructor(...) {}

  /*初始 */
  init() {
    ...
    this.setLevelDatas();
  }

  ...

  /*裝所有levels資料 */
  setLevelDatas() {
    let url = this.dataService.setUrl("levels");
    this.dataService.getData(url).subscribe((data: IData) => {
      if (!!data.errorcode) {
        this.openStatusDialog(data.errorcode);
      } else {
        if (!!data.res) {
          this.levels = <ILevel[]>data.res;
        }
      }
    });
  }

  /*開Dialog */
  openDialog(action: string, select?: ICustomer) {
    switch (action) {
      case "insert":
        this.openDetailDialog();
        break;
      case "update":
        this.openDetailDialog(select);
        break;
      case "level":
        this.openLevelDialog();
        break;
    }
  }

  /*開啟新增/更新的dialog */
  openDetailDialog(select?: ICustomer) {
    if (!!this.levels && !!this.levels.length) {
      let obj = <IDetailCustomer>{
        customer: null,
        levels: this.levels
      };
      if (!!select) {
        obj.customer = select;
      }
      let dialogRef = this.dialog.open(DialogCustomerDetailComponent, {
        width: "600px",
        data: obj
      });
      dialogRef.afterClosed().subscribe((o: ICustomer) => {
        if (!!o) {
          if (!select) {
            this.insertCustomer(o);
          } else {
            this.updateCustomer(o, select);
          }
        }
      });
    }
  }

  /*新增會員 */
  insertCustomer(o: ICustomer) {
    let url = this.dataService.setUrl("customers");

    o = <ICustomer>this.dataService
    .checkData(o, this.userService.getUser().id);

    this.dataService.insertOne(url, o)
    .subscribe((data: IData) => {
      this.openStatusDialog(data.errorcode);
    });
  }

  /*修改會員 */
  updateCustomer(o: ICustomer, select: ICustomer) {
    let url = this.dataService.setUrl("customers", null, select.id);

    o = <ICustomer>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();
    });
  }
}

(三) 會員等級新增修改

customerupdatlevel

dialog-level/dialog-level.component.ts

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

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

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

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

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

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

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

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

--

dialog-level/dialog-level.component.html

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

<div mat-dialog-content>
  <div class="flex center">
    <div style="margin-right:10px;">
      <input
        [formControl]="ipContent"
        [placeholder]="'import_level_name' | 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.levelNames && !!data.levelNames.length">
      <tbody>
        <tr *ngFor="let name of data.levelNames; 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 CustomerListComponent implements OnInit {
  ...
  levels: ILevel[] = [];  //所有等級類別

  constructor(...) {}

  ...

  /*開Dialog */
  openDialog(action: string, select?: ICustomer) {
    switch (action) {
      case "insert":
        this.openDetailDialog();
        break;
      case "update":
        this.openDetailDialog(select);
        break;
      case "level":
        this.openLevelDialog();
        break;
    }
  }

 /*開啟新增等級類型的dialog */
  openLevelDialog() {
    if (!!this.levels && !!this.levels.length) {
      let obj = {
        levelNames: this.levels.map((level: ILevel) => {
          return level.name;
        })
      };
      let dialogRef = this.dialog
      .open(DialogCustomerLevelComponent, {
        width: "600px",
        data: obj
      });

      dialogRef.afterClosed().subscribe((levelNames: string[]) => {
        if (!!levelNames && !!levelNames.length) {
          levelNames.forEach((name: string, index: number) => {
            let length = this.levels.length;
            if (index < length) {
              //update level name
              let url = this.dataService
              .setUrl("levels", null, index + 1);

              this.dataService.updateOne(url, { name: name })
              .subscribe();
            } else {
              //insert level
              let url = this.dataService.setUrl("levels", null);
              this.dataService.insertOne(url, <ILevel>{ name: name })
              .subscribe();
            }
            if (index === levelNames.length - 1) {
              this.setLevelDatas();
              this.openStatusDialog(0);
            }
          });
        }
      });
    }
  }

  /*打開 alert-dialog 提示視窗 */
  openStatusDialog(errorcode: number) {
    let dialogRef = this.dialog.open(DialogAlertComponent, {
      width: "250px",
      data: {
        errorcode: errorcode
      }
    });
    dialogRef.afterClosed().subscribe(() => {
      this.setDatas();
    });
  }
}

更新等級功能是不能刪除的。
這是因為會員列表本身跟等級有關聯性levelId
如果刪除的話,連結到的levelId就失效了。

特別要注意的是更新 等級類別array 的流程,
openLevelDialog()裡面,會先判斷舊有的陣列長度,
再去判斷dialog-levels送來的 等級類別array 的長度。

假設舊有長度是3,新長度是5,
根據新舊長度判斷來做,
前3個更新舊有的值,後2個新增新的值。


範例碼

此為完整專案範例碼,連線方式為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


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

尚未有邦友留言

立即登入留言