iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 19
0
Modern Web

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

day19 AdminModule

  • 分享至 

  • xImage
  •  

簡述

開始實作權限管理的功能模塊。

功能

adminlist

  • 管理者列表(換頁搜尋)
  • 列表中更新管理者
  • Dialog新增管理者
  • 子組件查看管理者

檔案結構

-src
  |-app
    |-cms
       |-admin
           |-list
               |-insert
                   |-dialog-insert.component.html
                   |-dialog-insert.component.css
                   |-dialog-insert.component.ts
               |-power
                   |-power.component.html
                   |-power.component.css
                   |-power.component.ts
               |-power-main
                   |-power-main.component.html
                   |-power-main.component.css
                   |-power-main.component.ts
               |-list.component.html
               |-list.component.css
               |-list.component.ts
           |-admin-routing.module.ts
           |-admin.component.ts
           |-admin.module.ts

因Demo的規模較小,Menu只有大選單沒有次選單。
但路由上的設計基本上還是會預設未來有可能會增加次選單的方向。

跟UserModule不同的是,最外層會有一個admin.component.ts
因為內層可能有多個次選單,
但是Demo沒有次選單,所以我們內層只有一個(list)。

//admin.component.ts
@Component({
  selector: "app-admin",
  template: `
    <router-outlet></router-outlet>
  `
})
export class AdminComponent implements OnInit {
  constructor() {}
  ngOnInit() {}
}

實作

(一) 管理者列表

adminlistview

list/list.component.ts

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

  /*更新管理者的相關Function */
  ...

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

  /*換分頁 */
  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("admins");
    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 = <IAdmin[]>data.res;
          this.setLoadingDatas(isDataInit)
        }
      }
    });
  }

  /*儲存storage */
  setLoadingDatas(isDataInit = false) {
    if (!isDataInit) {
      this.tabService.nextTabMain(<ITabMain>{
        request: 'update',
        content: this.tab
      });
    }
  }

  /*新增Admin的相關 Dialog Function */
  ...

 
  /*查看Admin的相關 子組件 Function */
  ...
  

  /*打開 alert-dialog 提示視窗 */
  openStatusDialog(errorcode: number) {
    let dialogRef = this.dialog.open(DialogAlertComponent, {
      width: "250px",
      data: {
        errorcode: errorcode
      }
    });
    dialogRef.afterClosed().subscribe(() => {
      this.setDatas();
    });
  }
}
  • 通常幾個時機點要重新裝畫面資料
  1. 初始化
  2. 換頁
  3. 搜尋某些條件
  4. 新增一筆資料
  5. 更新某筆資料
  • 新增/更新完資料通常會出現 dialog 提示視窗
    所以重裝畫面的時機就會放在openStatusDialog()裡。

  • 每次重新裝畫面資料時,代表頁面元素會有變動,
    所以最後一定要送storage暫存,
    這樣重新整理後才會長得一模一樣。

storagetab

幾乎所有功能模塊的list流程一模一樣,
所以後續不會再贅述了。


(二) 列表中更新管理者

adminlistupdate

其實是在列表中點兩下即可修改,
點空白處後完成修改。

能修改的值只有名字跟狀態。

list/list.component.ts

@Component({
  selector: "app-admin-list",
  templateUrl: "./list.component.html",
  styleUrls: ["./list.component.css"]
})
export class AdminListComponent implements OnInit, AfterViewInit, OnDestroy {
  ...
  subscriptionClick: Subscription;
  clickToggle = new ClickToggle(
      0, 
      "", 
      ["change-status", "change-name"]
    );
  inputVal = new FormControl("", [Validators.required]);

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

  ngOnInit() {
    ...
  }

  ngOnDestroy() {
    if (!!this.subscriptionClick) {
      this.subscriptionClick.unsubscribe();
    }
  }

  ngAfterViewInit() {
    this.subscriptionClick = merge(
      observableFromEvent(document, "click"),
      observableFromEvent(document, "touchstart")
    ).subscribe((e: any) => {
      let isClick = this.clickToggle.atrTag.find(item => {
        if (!!e.srcElement.attributes.getNamedItem(item)) {
          let v = e.srcElement.attributes.getNamedItem(item);
          this.clickToggle.atrId = parseInt(v.nodeValue);
          this.clickToggle.atrTagSel = item;
          return true;
        }
      });
      if (!isClick) {
        if (!this.inputVal || !this.inputVal.valid) {
          this.clickReset();
          return;
        }
        //inputName exist
        if (!!this.inputVal && this.inputVal.valid) {
          this.saveClick(this.inputVal.value);
          this.clickReset();
        }
      }
    });
  }

  clickReset() {
    this.clickToggle.reset();
    this.inputVal.patchValue("");
  }

  saveClick(val: string) {
    switch (this.clickToggle.atrTagSel) {
      case "change-status":
        this.saveStatus(this.clickToggle.atrId, val);
        break;
      case "change-name":
        this.saveName(this.clickToggle.atrId, val);
        break;
    }
  }

  saveStatus(id: number, val: string) {
    let url = this.dataService.setUrl("admins", null, id);
    this.dataService.updateOne(url, <IAdmin>{ status: +val })
    .subscribe((data: IData) => {
      if (!!data.errorcode) {
        this.openStatusDialog(data.errorcode);
      } else {
        this.setDatas();
      }
    });
  }

  saveName(id: number, val: string) {
    let url = this.dataService.setUrl("admins", null, id);
    this.dataService.updateOne(url, { name: val })
    .subscribe((data: IData) => {
      if (!!data.errorcode) {
        this.openStatusDialog(data.errorcode);
      } else {
        this.setDatas();
      }
    });
  }

  ...
}

上述過程簡單來說

  1. 當點擊的時候 => 判斷點擊的位置,
    如果點擊是點在需要修改的欄位,
    則會自動變成 input

  2. 當點擊不是需要修改的地方,
    則會判斷input有沒有值。
    有值就驗證 => 驗證成功後更新資料,
    沒值就reset。

--

list/list.component.html

 <td [attr.data-title]="'name' | translate">
  <span
    *ngIf="!(clickToggle.atrId == r.id && clickToggle.atrTagSel == 'change-name')"
    [attr.change-name]="r.id"
    >
    {{ r.name }}
  </span>
  <div *ngIf="clickToggle.atrId == r.id && clickToggle.atrTagSel == 'change-name'">
    <div class="flex center">
      <input [attr.change-name]="r.id" [formControl]="inputVal" 
      cusAutofocus />
      <mat-icon
        *ngIf="!inputVal.valid"
        (click)="clickReset()"
        svgIcon="cancel"
        style="margin-left:2px;"
      ></mat-icon>
    </div>
    <validation-messages [control]="inputVal"></validation-messages>
  </div>
</td>
  <td [attr.data-title]="'status' | translate">
    <span
      *ngIf="!(clickToggle.atrId == r.id && clickToggle.atrTagSel == 'change-status')"
      [attr.change-status]="r.id"
    >
      {{ r.status | pipetag: ACCOUNTSTATUS | translate }}
    </span>
    <div
      *ngIf="clickToggle.atrId == r.id && clickToggle.atrTagSel == 'change-status'"
    >
      <div class="flex center">
        <select
          name="se_status"
          id="se_status"
          [formControl]="inputVal"
          [attr.change-status]="r.id"
        >
          <option value="" disabled>{{ "select" | translate }}</option>
          <option
            *ngFor="let status of ACCOUNTSTATUS"
            [ngValue]="status.id"
            [selected]="status.id == r.status"
          >
            {{ status.name | translate }}
          </option>
        </select>
        <mat-icon
          *ngIf="!inputVal.valid"
          (click)="clickReset()"
          svgIcon="cancel"
          style="margin-left:2px;"
        ></mat-icon>
      </div>
      <validation-messages [control]="inputVal"></validation-messages>
    </div>
  </td>
  • [attr.change-name]="r.id"[attr.change-status]="r.id"
    就是做記號,代表是某筆資料被點到姓名/狀態。

(三) 複用的子組件:權限區

在我們要開始做新增跟查看權限之前,
我們要有個思維:就是能夠複用的功能有哪些。

比如說這是新增的畫面:
admindialog

這是查看權限畫面
adminchild

有沒有發現這兩者有一塊地方長得相同,
就是權限區是相同的。
adminpower

因此我們應該判定最小的零件複用就是權限區
然後在新增的時候用一個 dialog Component 把權限區包起來,
在查看的時候用一個component把權限區包起來,
並做一個可以修改的開關。

檔案結構

|-admin
  |-list
      |-insert
          |-dialog-insert.component.html
          |-dialog-insert.component.css
          |-dialog-insert.component.ts
      |-power
          |-power.component.html
          |-power.component.css
          |-power.component.ts
      |-power-main
          |-power-main.component.html
          |-power-main.component.css
          |-power-main.component.ts
  • power:權限區子組,會提供給新增跟查看Component去做使用
  • insert:新增管理者的Dialog
  • power-main:查看管理者權限的子組件

--

power.component.ts

export class AdminPowerComponent implements OnInit {
  @Input() toggleUpdate: string; //view, update, close-update, insert
  @Input() holds: IHold[] = [];
  powers: IPower[] = [];

  constructor(private dataService: DataService) {}

  ngOnInit() {
    if (!!this.holds) {
      this.initPowers();
    }
  }

  initPowers() {
    let url = this.dataService.setUrl(`powers`);
    this.dataService.getData(url).subscribe((data: IData) => {
      if (!data.errorcode && !!data.res) {
        this.powers = <IPower[]>data.res;
        this.powers.forEach((power: IPower) => {
          power.check = false;
        });
        this.setPowers();
      }
    });
  }

  setPowers() {
    for (let i = 0; i < this.powers.length; i++) {
      for (let y = 0; y < this.holds.length; y++) {
        if (this.holds[y].powerId === this.powers[i].id) {
          this.powers[i].check = true;
        }
      }
    }
  }

  getPowers(): IPower[] {
    return this.powers;
  }
}

--

power.component.html

<mat-card *ngIf="!!powers">
  <div class="flex straight-flex slides">
    <div class="title">
      {{ "power_setting" | translate }}
    </div>
    <ng-container *ngFor="let p of powers">
      <mat-slide-toggle
        [(ngModel)]="p.check"
        [disabled]="toggleUpdate === 'view' || toggleUpdate === 'close-update'"
      >
        {{ p.name + "_manage" | translate }}
      </mat-slide-toggle>
    </ng-container>
  </div>
</mat-card>

(四)) 用Dialog新增管理者

dialog-insert.component.ts

export interface IDetailAdmin {
  admin: IAdmin;
  powers: IPower[];
}

@Component({
  selector: "dialog-admin-insert",
  templateUrl: "./dialog-insert.component.html",
  styleUrls: ["./dialog-insert.component.css"]
})
export class DialogAdminInsertComponent implements OnInit {
  @ViewChild(AdminPowerComponent,
         { static: false }) powerComp: AdminPowerComponent;
  form: FormGroup;

  constructor(
    public dialogRef: MatDialogRef<DialogAdminInsertComponent>,
    private fb: FormBuilder
  ) {}

  ngOnInit() {
    this.createForm();
  }

  createForm() {
    let obj = {
      account: [
        "",
        [
          Validators.required, 
          Validators.minLength(6), 
          ValidationService.userValidator
        ]
      ],
      password: [
        "",
        [
          Validators.required, 
          Validators.minLength(6), 
          ValidationService.userValidator
        ]
      ],
      name: ["", Validators.required]
    };
    this.form = this.fb.group(obj);
  }

  getDetailData(): IDetailAdmin {
    let powers: IPower[] = [];
    this.powerComp.getPowers().forEach((power: IPower) => {
      if (power.check) {
        powers.push(power);
      }
    });
    let admin = <IAdmin>{
      account: this.form.value.account,
      password: this.form.value.password,
      name: this.form.value.name,
      status: 1
    };
    return <IDetailAdmin>{
      admin: admin,
      powers: powers
    };
  }

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

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

--

dialog-insert.component.html

<form [formGroup]="form">
  <div mat-dialog-title class="flex center">
    <mat-icon svgIcon="alert"></mat-icon>
    <span> {{ "alert_admin_insert" | translate }} </span>
  </div>
  <div mat-dialog-content>
    <div class="item-wrapper two pink">
      <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">
      <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>
      <app-admin-power [toggleUpdate]="'insert'"></app-admin-power>
    </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>

子組件查看管理者

listComponent.ts 打開子組件寫法請參照day17 List常見問題(二)

list.component.html

...
<tbody *ngIf="!!result && !!result.length">
  <ng-container *ngFor="let r of result; let i = index">
  ...
    <tr>
      <td>
        <button
          (click)="onOpenChild('power', r)"
          class="button blue pb"
          [ngClass]="{
            active:
              childToggle.selectTag == 'power' &&
              childToggle.selectId == r[childToggle.selectMarkID]
          }"
        >
          {{ "view_power" | translate }}
        </button>
      </td>
    </tr>
    <tr
      *ngIf="
        childToggle.selectTag == 'power' &&
        childToggle.selectId == r[childToggle.selectMarkID]
      "
    >
      <td colspan="6" class="skin">
        <app-admin-power-main [adminId]="r.id"></app-admin-power-main>
      </td>
    </tr>
  </ng-container>
</tbody>

--

power-main.component.ts

透過父組件list.component.ts送來的 adminId
來跟 Database 要此管理者的權限holds

export class AdminPowerMainComponent implements OnInit {
  @ViewChild(AdminPowerComponent, { static: false }) powerComp: AdminPowerComponent;
  @Input() adminId: number = 0;
  toggleUpdate: string = "close-update";
  admin: IAdmin = null;

  constructor(
    public dialog: MatDialog, 
    private dataService: DataService
  ) {}

  ngOnInit() {
    if (!!this.adminId) {
      this.init();
    }
  }

  init() {
    let url = this.dataService.setUrl(
                `admins`, 
                [{ key: "_embed", val: "holds" }], 
                this.adminId
              );
    this.dataService.getData(url).subscribe((data: IData) => {
      if (!data.errorcode && !!data.res) {
        this.admin = <IAdmin>data.res;
      }
    });
  }

  onSave() {
    this.resetHold();
  }

  /* 新增管理者權限*/
  resetHold() {
    if (!!this.admin.holds && !!this.admin.holds.length) {
      this.runDelHold(0,this.admin.holds,[])
    } else {
      this.runInsertHolds(0, this.powerComp.getPowers(), []);
    }
  }

  /* 
  舊有的管理者權限要先全部刪除,
  此為一筆一筆刪,所以是遞迴迴圈。
  */
  runDelHold(index: number, holds: IHold[], errHolds: IHold[]) {
    let errHoldArr = errHolds;
    let leng = holds.length;
    if (index < leng) {
      this.delHold(index, holds, errHolds);
    } else {
      if (!!errHoldArr.length) {
        this.openStatusDialog(5);
      } else {
        this.runInsertHolds(0, this.powerComp.getPowers(), []);
      }
    }
  }

 /* 
  新增的管理者權限要一筆一筆加,所以是遞迴迴圈。
  */
  runInsertHolds(index: number, powers: IPower[], errHolds: IPower[]) {
    let errHoldArr = errHolds;
    let leng = powers.length;
    if (index < leng) {
      this.insertHolds(index, powers, errHolds);
    } else {
      if (!!errHoldArr.length) {
        this.openStatusDialog(5);
      } else {
        this.toggleUpdate = "close-update";
        this.init();
      }
    }
  }

  insertHolds(index: number, powers: IPower[], errHolds: IPower[]) {
    if (powers[index].check) {
      let obj: IHold = {
        adminId: this.adminId,
        powerId: powers[index].id
      };
      this.dataService.insertOne("holds", obj)
      .subscribe((data: IData) => {
        if (!!data.errorcode) {
          errHolds.push(powers[index]);
        } else {
          this.runInsertHolds(++index, powers, errHolds);
        }
      });
    }else{
      this.runInsertHolds(++index, powers, errHolds);
    }
  }

  openStatusDialog(errorcode: number) {
    this.dialog.open(DialogAlertComponent, {
      width: "250px",
      data: {
        errorcode: errorcode
      }
    });
  }
}
  • 修改此管理者的權限,
    以實務上流程來說,通常會把舊的權限全部刪除,
    再一次新增所有剛剛已選中功能頁。

不然還要判斷哪些已經是原本有的,
哪有原本有的被刪掉,哪些原本沒有要新增,
會太多判斷式!

--

power-main.component.html

<div class="container" *ngIf="!!admin">
  <div class="flex flex-end">
    <button
      class="button green pb"
      *ngIf="toggleUpdate === 'close-update'"
      (click)="toggleUpdate = 'update'"
    >
      {{ "update" | translate }}
    </button>
    <button class="button green pb" *ngIf="toggleUpdate === 'update'" (click)="onSave()">
      {{ "save" | translate }}
    </button>
  </div>
  <div>
    <app-admin-power [holds]="admin.holds" [toggleUpdate]="toggleUpdate">
    </app-admin-power>
  </div>
</div>

範例碼

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


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

尚未有邦友留言

立即登入留言