傳統常見的導航做法多半是麵包屑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。
searchObj:Search物件 後續章節會提起。
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;
}
}
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,<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>
--
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();
}
}
那 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();
}
});
}
每次 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 也會先給預設值[]
。
//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
一開始會跳出提示視窗顯示fail為正常,
請先從範例專案裡下載或是複製db.json
到本地端,
並下指令:
json-server db.json
json-server開啟成功後請連結此網址:
https://ngcms-corecomp.stackblitz.io/cms?token=bc6e113d26ce620066237d5e43f14690