iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 14
0

簡述

傳統常見的導航做法多半是麵包屑breadcrumb
但是在系統組織很多分層的結構下,
光要多方對帳,可能會查證的比較辛苦,
所以現在越來越多採 Tab 導航模式,
很像在使用 Ghrome 分頁呢 XD。

流程

tab 定義為頁面資訊。
tabs 將裝載目前已打開的頁面。
tab.component.ts則是對 tabs 做新增修改的操作。

舉例來說:

  • Menu 的頁面大標題點擊後,例如點選商品管理

  • insert tab 到 tabs 裡面,
    但如果 tabs 裡已經有商品管理,tabs 就不會再新增,
    並且把舊有的商品管理的頁面資訊送去功能頁。

  • 功能頁開啟之前一定要先拿到頁面資訊tab
    才會渲染畫面。

  • tabs 每次操作的動作(如新增刪除修改),
    操作完都會把目前的 tabs 紀錄在storage

檔案結構

首先建立一個全域型的功能模塊,在 tab 資料夾底下。

-src
  |-app
    ...
    |-app.module.ts
    |-core
    |-shared
    ...
    |-model
        |-data
            |-...
        |-base.ts
        |-tabs.ts
    |-module
       |-tab
          |-tab.component.css
          |-tab.component.html
          |-tab.component.ts
          |-tab.module.ts
          |-tab.service.ts

實作

首先在model資料夾底下新增 tabs.ts

(一) tabs.ts:

import { IPage } from "./base";
import { Search } from "../modules/search/search";

export interface ITabMain {
  request: string;
  content: ITabBase;
}

export interface ITabBase {
  tag: string;
  path: string;
  pageObj: IPage;
  unix: string;
  searchObj: Search;
}
  • ITabBase就是一個頁面資訊元素
    tag:標籤名稱,之後會做 i18n 翻譯。
    path:路由路徑。
    pageObj:第幾頁、一頁幾筆、總長度。
    unix:時間戳,何時新增此 tab。
    searchObjSearch物件 後續章節會提起。

  • ITabMain就是告訴 TabComponent,
    現在是要哪種動作 (新增/修改)。

request: string; //insert update
content: ITabBase;

(二) tab.service.ts

接下來定義兩種傳送資料的管道。

@Injectable({
  providedIn: "root"
})
export class TabService {
  private tabMainSubject = new BehaviorSubject<ITabMain>(null);
  private tabSubject = new BehaviorSubject<ITabBase>(null);

  constructor() {}

  nextTab(listTab: ITabBase) {
    this.tabSubject.next(listTab);
  }

  isTabIn(): Observable<ITabBase> {
    if (!!this.tabSubject) {
      return this.tabSubject.asObservable();
    }
    return null;
  }

  nextTabMain(listTabMain: ITabMain) {
    this.tabMainSubject.next(listTabMain);
  }

  isTabMainIn(): Observable<ITabMain> {
    if (!!this.tabMainSubject) {
      return this.tabMainSubject.asObservable();
    }
    return null;
  }
}

tabMainSubject:

  • 基本上 Menu 的任何頁面控制項點擊,
    都會觸發this.tabMainSubject.next(listTabMain)
//menu.component.ts
setListTab(menuInfo: IMenu) {
    this.coreService.nextHamburger("next");
    let tab = <ITabBase>{
      tag: `${menuInfo.name}_list`,
      path: "cms" + menuInfo.path
    };
    let tabMain = <ITabMain>{
      request: "insert",
      content: tab
    };
    this.tabService.nextTabMain(tabMain);//**
}

--

  • TabComponent接請求並開始過濾 tabs。
ngOnInit() {
  ...

  if (!!this.tabService.isTabMainIn()) {
    this.subscription = this.tabService.isTabMainIn()
    .subscribe((tabMain: ITabMain) => {
      if (!!tabMain && !!tabMain.request) {
        switch (tabMain.request) {
          case "insert":
            if (this.searchListTabIndex(tabMain.content) != -1) {
              //如果存在就更新
              this.selected = 
              this.searchListTabIndex(tabMain.content);

              this.listTabs[this.selected] = tabMain.content;
                this.goResolve(this.listTabs[this.selected]);
                this.goUrl(this.listTabs[this.selected]);
                this.saveTabs();
                return;
              }

              //如果為首頁 將清空所有tab
              if (tabMain.content.tag === "index_list") {
                this.listTabs = [];
                this.saveTabs();
                this.goHome();
                return;
              }

              //不存在開始進行新增
              let dateTime = Date.now();
              let timestamp = Math.floor(dateTime / 1000);
              tabMain.content.unix = `${timestamp}`;

              this.listTabs.push(tabMain.content);
              this.selected = this.listTabs.length - 1;
              this.goResolve(this.listTabs[this.selected]);
              this.goUrl(this.listTabs[this.selected]);
              this.saveTabs();

              break;
            case "update":
              let i = this.searchListTabIndex(tabMain.content);
              this.listTabs[i] = tabMain.content;
              this.saveTabs();
              break;
          }
        }
      });
    }
  }

--

  • 同時tap.component.html將會根據 tabs,
    來呈現目前已經點開的 tab。
<div class="nav-container" *ngIf="!!listTabs && !!listTabs.length">
  <mat-tab-group [selectedIndex]="selected" 
  (selectedTabChange)="selectTab($event.index)">

    <ng-template ngFor let-tab [ngForOf]="listTabs" let-index="index">
      <mat-tab (click)="selectTab(tab)">
        <ng-template mat-tab-label class="active">
          <span>{{ getTagName(tab.tag) | translate }}</span>
          <mat-icon
            (click)="delTab(index)"
            svgIcon="cancel"
            style="margin-left:10px;font-size: small"
          ></mat-icon>
        </ng-template>
      </mat-tab>
    </ng-template>

  </mat-tab-group>
</div>

--

  • 如果直接點擊 tab,就會直接搜索此 tab 的頁面資訊。

tabsel

selectTab(index: number) {
    this.selected = index;
    let t = this.listTabs[index];
    this.goResolve(t);
    this.goUrl(t);
}

delTab(index: number) {
    this.listTabs.splice(index, 1);
    this.saveTabs();
    if (!!this.listTabs.length) {
      this.selected = this.listTabs.length - 1;
      this.goResolve(this.listTabs[this.selected]);
      this.goUrl(this.listTabs[this.selected]);
    } else {
    //如果tabs長度為0 則打開首頁
      this.goHome();
    }
}

tabSubject:

那 Menu 觸發對 tabs 的操作後,
TabComponent 會返回被新增或是被修改的頁面資訊,
並且開啟此功能頁。

  • tab.component.ts
goResolve(listTab: ITabBase) {
    this.tabService.nextTab(listTab);
}

goUrl(listTab: ITabBase) {
    if (!!listTab && !!listTab.path) {
      this.router.navigate([listTab.path, { unix: listTab.unix }], {
        skipLocationChange: true,
        queryParamsHandling: "merge"
      });
    }
}
  • 此時 TabComponent 扮演推值,推要打開的頁面資訊物件ITabBase回去。

  • 相關的功能頁必須要先接到自己的ITabBase,才能開始抓取數據並渲染畫面。

檔案結構::

-src
  |-app
    ...
    |-module
       |-tab
          |-tab.component.ts
          ...
    |-cms
       |-admin
            |-admin-routing.module.ts
            |-list
                |-list.component.ts
                ...
            ...
       ...
       |-cms.resolve.ts

--

admin-routing.module.ts

const routes: Routes = [
  {
    path: "",
    component: AdminComponent,
    children: [
      {
        path: "list",
        component: AdminListComponent,
        resolve: { listTab: CmsResolver }
      },
      { path: "", redirectTo: "list", pathMatch: "full" }
    ]
  }
];

--

cms.resolve.ts

@Injectable()
export class CmsResolver implements Resolve<Observable<ITabBase>> {
  constructor(
    private service: TabService, 
    private logger: LoggerService
  ) {}
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    if (!!this.service.isTabIn()) {
      return this.service.isTabIn().pipe(
        take(1),
        map(e => {
          if (!!e) {
            this.logger.print("ListTab", e);
            return e;
          }
        })
      );
    }
  }
}

--

list.component.ts

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

(三) StorageService

每次 tabs 的變動,最後一步就是儲存在 StorageService

檔案結構:

-src
  |-app
    ...
    |-module
       |-tab
          |-tab.component.ts
          ...
    |-service
       |-storage.service.ts

--

tab.component.ts

saveTabs() {
    this.storageService.setStorage(this.listTabs);
}

--

storage.service.ts

...
setKey(account: string) {
    this.key = JSON.stringify(Md5.hashStr(account + UserKey));
    this.getStorage();
}

setStorage(listTabs: ITabBase[]) {
    sessionStorage.setItem(this.key, JSON.stringify(listTabs));
}

getStorage(): ITabBase[] {
    if (!sessionStorage.getItem(this.key)) {
      this.setStorage([]);
    }
    return JSON.parse(sessionStorage.getItem(this.key));
}
...

為什麼要儲存 storage
因為我們還要考慮到一種情況,就是當按下重新整理時,
其實 tabs 已經是有資料的。

所以第一步初始化 tabs,應該要先抓 storage 裡紀錄的 tabs
若沒有 storage 也會先給預設值[]

storagetab

//tab.component.ts
ngOnInit() {
    this.listTabs = this.storageService.getStorage();
    if (!!this.listTabs.length) {
      this.selected = this.listTabs.length - 1;
      this.goResolve(this.listTabs[this.selected]);
      this.goUrl(this.listTabs[this.selected]);
    }
    ...
}

範例碼

https://stackblitz.com/edit/ngcms-corecomp

Start

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

json-server db.json

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


上一篇
day13 Menu
下一篇
day15 Cms Routing
系列文
用Angular打造完整後台30

尚未有邦友留言

立即登入留言