一、具體需求:
1. 頁(yè)面布局
左側(cè)導(dǎo)航篡九,右側(cè)側(cè)邊輔助菜單乙各,中間主業(yè)務(wù)界面墨礁;左側(cè)導(dǎo)航是根據(jù)用戶權(quán)限動(dòng)態(tài)生成的。
2. 支持Tab頁(yè)
中間的主業(yè)務(wù)界面是通過(guò)Tab頁(yè)的方式實(shí)現(xiàn)耳峦,可以根據(jù)左側(cè)導(dǎo)航欄的點(diǎn)擊行為動(dòng)態(tài)打開(kāi)或切換到相應(yīng)的Tab頁(yè)恩静,當(dāng)然,這里的Tab頁(yè)要支持Tab頁(yè)基本應(yīng)該具有的功能特點(diǎn)蹲坷,包括打開(kāi)蜕企、切換和關(guān)閉(拖換Tab頁(yè)次序功能后期可以試試加上)。
3. Tab頁(yè)特性
Tab頁(yè)內(nèi)的內(nèi)容是一個(gè)Angular的組件冠句,在Tab頁(yè)切換的時(shí)候要求組件狀態(tài)保持(不重新加載,即組件數(shù)據(jù)不丟失)幸乒。
4. 輔助菜單功能
輔助菜單完成的功能是對(duì)主業(yè)務(wù)界面的具體數(shù)據(jù)進(jìn)行操作懦底,比如主業(yè)務(wù)是“用戶管理”,顯示的是用戶數(shù)據(jù)表罕扎,輔助菜單顯示的就是用戶數(shù)據(jù)表的具體某一行的內(nèi)容(如姓名聚唐、工號(hào)、電話……)腔召。
5. 輔助菜單特性
輔助菜單根據(jù)主業(yè)務(wù)表顯示的組件動(dòng)態(tài)切換內(nèi)容杆查,也要求狀態(tài)保持。
二臀蛛、技術(shù)路線:
1. 界面布局
分為左中右三部分亲桦,左側(cè)導(dǎo)航和右側(cè)輔助菜單分別是一個(gè)組件。
2. 支持Tab頁(yè)
使用mat-tab-group
標(biāo)簽實(shí)現(xiàn)浊仆,Tab頁(yè)內(nèi)加載組件通過(guò)正常的前端路由實(shí)現(xiàn)客峭。
3. Tab頁(yè)特性
狀態(tài)保持通過(guò)自定義路由策略(RouteReuseStrategy
)實(shí)現(xiàn)。
4. 輔助菜單功能
通過(guò)輔助路由實(shí)現(xiàn)抡柿,難點(diǎn)在于主業(yè)務(wù)部分與輔助菜單部分傳值方式舔琅。
5. 輔助菜單特性
根據(jù)具體業(yè)務(wù)邏輯,加載不同的主路由與輔助路由洲劣,輔助路由部分同樣采用自定義路由策略來(lái)實(shí)現(xiàn)狀態(tài)保持备蚓。
三课蔬、具體實(shí)現(xiàn):
1. 頁(yè)面布局:
code:
<app-header></app-header>
<div class="app-body">
<app-sidebar></app-sidebar>
<!-- Main content -->
<main class="main">
<!-- Breadcrumb -->
<!--<ol class="breadcrumb"><app-breadcrumbs></app-breadcrumbs></ol>-->
<div>
<mat-tab-group [(selectedIndex)]="tabIndex">
<mat-tab *ngFor="let link of navLinks">
<ng-template mat-tab-label>
<span (click)="tabChanged(link.code)">{{link.label}}
<i class="fa fa-close fa-lg" style="margin: 2px 0px 0px 5px" (click)="closeTab(link.code)"></i>
</span>
</ng-template>
</mat-tab>
</mat-tab-group>
<router-outlet></router-outlet>
</div>
</main>
<app-aside></app-aside>
</div>
<app-footer></app-footer>
布局解釋:
-
<app-sidebar></app-sidebar>
導(dǎo)航部分。 -
<app-aside></app-aside>
輔助菜單部分郊尝。 - 兩者中間是主業(yè)務(wù)界面部分二跋。
Note:
-
先說(shuō)主業(yè)務(wù)界面部分,Angular Material天然提供了一種可以通過(guò)Tab頁(yè)的方式加載組件的解決方案如下:
<nav mat-tab-nav-bar> <a mat-tab-link *ngFor="let link of navLinks" [routerLink]="link.path" routerLinkActive #rla="routerLinkActive" [active]="rla.isActive"> {{link.label}} </a> </nav> <router-outlet></router-outlet>
這種解決方案有以下缺陷:
- 當(dāng)Tab頁(yè)標(biāo)簽多于界面一行可容納的數(shù)量時(shí)虚循,導(dǎo)致出現(xiàn)第二行Tab頁(yè)標(biāo)簽同欠,而不是在一行Tab頁(yè)標(biāo)簽首尾出現(xiàn)左右可移動(dòng)的箭頭,來(lái)使用戶可以左右點(diǎn)擊實(shí)現(xiàn)Tab頁(yè)標(biāo)簽可見(jiàn)窗口的滑動(dòng)横缔。
- 不支持組件的狀態(tài)保持铺遂,Tab頁(yè)的切換會(huì)導(dǎo)致非當(dāng)前Tab頁(yè)相應(yīng)組件的銷毀(揣測(cè)Angular或許提供防止組件銷毀的接口,但是沒(méi)仔細(xì)去調(diào)研)茎刚。
主界面Tab頁(yè)邏輯代碼部分:
public tabChanged(code) { for (let count = 0; count < this.navLinks.length; count++) { if (this.navLinks[count].code === code) { this.tabIndex = count; this.router.navigateByUrl(this.navLinks[count].path + '?paramsExtras=' + this.paramsExtras).then(); this.toggleWorkShop(this.params[this.navLinks[count].moduleName].isOpenedWorkshop); break; } } }; public closeTab(code) { for (let count = 0; count < this.navLinks.length; count++) { if (this.navLinks[count].code === code) { const paramsExtras = { reference: this.navLinks[count]['path'], isLast: false }; if (count !== 0) { this.tabIndex = count - 1; this.router.navigateByUrl(this.navLinks[count - 1].path + '?paramsExtras=' + JSON.stringify(paramsExtras)).then(); this.toggleWorkShop(this.params[this.navLinks[count - 1].moduleName].isOpenedWorkshop); } else { if (this.navLinks.length !== 1) { this.tabIndex = count; this.router.navigateByUrl(this.navLinks[count + 1].path + '?paramsExtras=' + JSON.stringify(paramsExtras)).then(); this.toggleWorkShop(this.params[this.navLinks[count + 1].moduleName].isOpenedWorkshop); } else { this.tabIndex = count; paramsExtras.isLast = true; this.router.navigateByUrl('tasks/(manage-board//sub:user-workshop)' + '?paramsExtras=' + JSON.stringify(paramsExtras)).then(); this.toggleWorkShop(this.params['manageboardModule'].isOpenedWorkshop); } } this.navLinks.splice(count, 1); break; } } } public updateArrays(linkInfoStringify) { const linkInfo = JSON.parse(linkInfoStringify); let updateFlag = true; for (let count = 0; count < this.navLinks.length; count++) { if (linkInfo.code === this.navLinks[count].code) { this.tabIndex = count; this.router.navigateByUrl(linkInfo.path + '?paramsExtras=' + this.paramsExtras).then(); updateFlag = false; break; } } if (updateFlag) { this.navLinks.push(linkInfo); this.tabIndex = this.navLinks.length - 1; this.router.navigateByUrl(linkInfo.path + '?paramsExtras=' + this.paramsExtras).then(); this.toggleWorkShop(this.params[linkInfo.moduleName].isOpenedWorkshop); } } public toggleWorkShop(newTabIsOpenedWorkshop) { if (!document.querySelector('body').classList.contains('aside-menu-hidden') || newTabIsOpenedWorkshop) { document.querySelector('body').classList.toggle('aside-menu-hidden'); } }
代碼解釋:
-
navLinks
數(shù)組元素結(jié)構(gòu)形如{path:"url",label:"labelName",code:"privilegeCode",moduleName:"moduleName"}
襟锐,是由導(dǎo)航項(xiàng)點(diǎn)擊傳過(guò)來(lái)的。 - Tab頁(yè)的切換觸發(fā)方式有兩種膛锭,一種是點(diǎn)擊Tab頁(yè)標(biāo)簽粮坞,一種是點(diǎn)擊左側(cè)導(dǎo)航菜單。
- 在點(diǎn)擊Tab頁(yè)標(biāo)簽時(shí)初狰,會(huì)調(diào)用
tabChange
函數(shù)莫杈,并傳入模塊所對(duì)應(yīng)的權(quán)限代碼code
,然后遍歷navLinks
數(shù)組奢入,如果找到對(duì)應(yīng)code
的項(xiàng)筝闹,則讀出其中的前端路由,將當(dāng)前索引值記下腥光,然后跳轉(zhuǎn)同時(shí)終止循環(huán)关顷,并判斷此刻輔助菜單的打開(kāi)狀態(tài)。 - 在點(diǎn)擊左側(cè)導(dǎo)航菜單項(xiàng)時(shí)武福,在導(dǎo)航類中觸發(fā)本類中的
updateArrays
函數(shù)议双,將一個(gè)navLinks
數(shù)組元素傳入,判斷該元素內(nèi)的code
值是否已經(jīng)存在捉片,即是否已經(jīng)在Tab頁(yè)的label欄中存在(激活狀態(tài)或者未激活狀態(tài))平痰,如果已經(jīng)存在,則將當(dāng)前組件所對(duì)應(yīng)的Tab頁(yè)激活界睁,記錄下索引并跳轉(zhuǎn)觉增,且終止循環(huán);如果當(dāng)前code
并不存在翻斟,則將當(dāng)前code
所對(duì)應(yīng)的內(nèi)容壓入navLinks
數(shù)組逾礁,并把當(dāng)前激活狀態(tài)的Tab設(shè)為數(shù)組最后一項(xiàng),并判斷此刻輔助菜單的打開(kāi)狀態(tài)。 - 在點(diǎn)擊Tab頁(yè)標(biāo)簽上的“×”時(shí)嘹履,會(huì)調(diào)用
closeTab
函數(shù)腻扇,遍歷navLinks
數(shù)組找到相應(yīng)的code
,判斷要關(guān)閉的Tab是否位于第一個(gè)砾嫉,如果不是幼苛,將Tab索引置為關(guān)閉頁(yè)的前一個(gè),然后通過(guò)前端路由跳轉(zhuǎn)到那個(gè)組件焕刮;如果是第一個(gè)舶沿,則先判斷當(dāng)前是否只剩唯一的一個(gè)Tab,如果不是配并,則將Tab索引置為關(guān)閉頁(yè)的后一個(gè)括荡,跳轉(zhuǎn)。如果只剩一個(gè)溉旋,則跳轉(zhuǎn)到一個(gè)固定的前端路由地址畸冲。 - 傳的參數(shù)中
reference
和isLast
是要穿給自定義路由策略組件的,自定義路由策略組件在截獲路由參數(shù)時(shí)观腊,將reference
鍵所對(duì)應(yīng)的存儲(chǔ)刪掉邑闲,然后判斷isLast
的值,如果為真梧油,則將緩存中的所有內(nèi)容都刪除苫耸。
-
左側(cè)導(dǎo)航部分,界面代碼如下:
<nav class="sidebar-nav"> <ul class="nav"> <ng-template ngFor let-navitem [ngForOf]="navObj"> <li *ngIf="isDivider(navitem)" class="nav-divider"></li> <ng-template [ngIf]="isTitle(navitem)"> <app-sidebar-nav-title [title]='navitem'></app-sidebar-nav-title> </ng-template> <ng-template [ngIf]="!isDivider(navitem)&&!isTitle(navitem)"> <app-sidebar-nav-item [item]='navitem'></app-sidebar-nav-item> </ng-template> </ng-template> </ul> </nav>
邏輯代碼如下:
const srcArray = navigation; const distArray = []; const privilegeCodeObj = this.ls.getObject('privilegeCode'); for (let priviCodeLvlCount_1 = 0; priviCodeLvlCount_1 < srcArray.length; priviCodeLvlCount_1++) { if (srcArray[priviCodeLvlCount_1].privilegeCode === 'static') { distArray.push(srcArray[priviCodeLvlCount_1]); } else { if (privilegeCodeObj[srcArray[priviCodeLvlCount_1].privilegeCode]) { distArray.push(srcArray[priviCodeLvlCount_1]); const srcChildrenArray = srcArray[priviCodeLvlCount_1]['children']; const distChildrenArray = []; for (let priviCodeLvlCount_2 = 0; priviCodeLvlCount_2 < srcChildrenArray.length; priviCodeLvlCount_2++) { if (privilegeCodeObj[srcChildrenArray[priviCodeLvlCount_2].privilegeCode]) { distChildrenArray.push(srcChildrenArray[priviCodeLvlCount_2]); this.full.params[srcChildrenArray[priviCodeLvlCount_2].moduleName] = srcChildrenArray[priviCodeLvlCount_2].moduleParams; } } distArray[distArray.length - 1]['children'] = distChildrenArray; } } } this.navObj = distArray;
代碼說(shuō)明:
全導(dǎo)航列表內(nèi)容存在于一個(gè)ts文件中儡陨,ts文件結(jié)構(gòu)如下:export const navigation = [ { title: true, name: '吉大醫(yī)療云平臺(tái)', privilegeCode: 'static', wrapper: { element: 'span', attributes: {} }, class: 'text-center' }, { name: '醫(yī)院管理系統(tǒng)', privilegeCode: 'static', url: 'tasks/manage-board', icon: 'icon-speedometer', moduleName: 'manageboardModule', moduleParams: { canOpenWorkshop: false, isOpenedWorkshop: false } }, { title: true, name: '功能列表', privilegeCode: 'static', wrapper: { element: 'span', attributes: {} }, class: 'text-center' }, { name: '系統(tǒng)管理', privilegeCode: 'AAAB0000', url: 'tasks/(system-manage/user-manage//sub:user-workshop)' icon: 'icon-note', children: [ { name: '用戶管理', privilegeCode: 'AAABAA00', url: 'tasks/system-manage/user-manage', icon: 'icon-note', moduleName: 'userModule', moduleParams: { workTabSelected: 0, canClickModifyTab: false, isModifyTabChanged: false, // 工作區(qū)是否有修改 modifyParams: {}, primaryTableData: [], primaryTableTotalRows: 0, isOpenedWorkshop: true, indexSelected: 0 } }] } ];
因?yàn)閷?dǎo)航欄是在用戶登錄后加載的鲸阔,而在用戶登錄時(shí)已將用戶權(quán)限代碼列表存于LocalStorage中,只需在此處從LocalStorage中讀出迄委,并根據(jù)用戶權(quán)限列表內(nèi)容生成用于生成導(dǎo)航欄的數(shù)組。此處LocalStorage中的權(quán)限內(nèi)容的數(shù)據(jù)結(jié)構(gòu)是一個(gè)元素形為{"privilegeCode":true/false}的JSON數(shù)組类少,此處沒(méi)有選擇使用Set這種數(shù)據(jù)類型,是因?yàn)長(zhǎng)ocalStorage的存儲(chǔ)需要序列化,而Set這種數(shù)據(jù)結(jié)構(gòu)在序列化與反序列化時(shí)的具體操作有難度健田。需要說(shuō)明的是泽西,ts結(jié)構(gòu)中除了用于生成導(dǎo)航所包含的信息外,還有用于定義輔助菜單的一些數(shù)據(jù)結(jié)構(gòu)残吩,雖然跟導(dǎo)航關(guān)系不大财忽,但是從數(shù)據(jù)結(jié)構(gòu)上來(lái)講放在這里管理是比較合理的,可以同導(dǎo)航數(shù)據(jù)一并載入內(nèi)存泣侮,具體功能這里先不講即彪。
-
輔助菜單界面部分:
<aside class="aside-menu"> <router-outlet name="sub"></router-outlet> </aside>
-
全局路由定義:
export const routes: Routes = [ { path: '', redirectTo: '/login', pathMatch: 'full', }, { path: 'tasks', component: FullLayoutComponent, children: [ { path: 'manage-board', component: ManageboardComponent }, { path: 'user-workshop', outlet: 'sub', component: UserWorkshopComponent }, { path: 'system-manage', loadChildren: './views/system-manage/system-manage.module#SystemManageModule' } ] }, { path: 'login', component: SimpleLayoutComponent, children: [ { path: '', loadChildren: './views/login/login.module#LoginModule', } ] } ];
user-workshop
路由定義為出口在sub
的一個(gè)輔助路由,此處僅為聲明活尊,具體打開(kāi)途徑已內(nèi)建到主界面路由中隶校。另外漏益,案例來(lái)講,應(yīng)該把輔助路由寫到一個(gè)模塊或者寫到所屬的主界面路由的模塊中深胳,但是在實(shí)現(xiàn)過(guò)程中會(huì)有技術(shù)問(wèn)題绰疤,比如在試圖將輔助路由寫到一個(gè)組件中時(shí),并不能支持帶命名的outlet舞终。另外值得一提的是轻庆,如果在路由中想要加入主路由與輔助路由,則主路由前必須不能是blank敛劝,并且既然用到了主路由與輔助路由余爆,那個(gè)在跳轉(zhuǎn)的時(shí)候就一定要將主路由與輔助路由寫全(如果沒(méi)有業(yè)務(wù)中沒(méi)有輔助路由則寫一個(gè)統(tǒng)一的空白輔助路由),不然會(huì)報(bào)各種奇葩錯(cuò)攘蔽,個(gè)人感覺(jué)Angular在路由模塊的處理上不是很完美龙屉,Angular的Github上也答復(fù)得也不讓人滿意。 -
主路由與輔助路由組件之間數(shù)據(jù)傳遞:
采取的策略是將需要傳的數(shù)據(jù)規(guī)范化满俗,寫到主布局文件中转捕,通過(guò)主路由與輔助路由組件到可以訪問(wèn)到主布局文件的特點(diǎn),實(shí)現(xiàn)數(shù)據(jù)的傳遞唆垃。值得一提的是五芝,由于主路由組件與輔助路由組件都可以利用到Angular的雙向綁定的特點(diǎn),主布局文件中的變量應(yīng)該都是位于主路由組件與輔助路由組件檢測(cè)的scope中辕万,所以該特性使得我們業(yè)務(wù)中的一些邏輯不需要自己再另寫代碼枢步。主布局文件中應(yīng)定義的數(shù)據(jù)結(jié)構(gòu)在前邊提到過(guò),也是從本地讀入渐尿,具體結(jié)果如下:moduleName: 'userModule', moduleParams: { workTabSelected: 0, canClickModifyTab: false, isModifyTabChanged: false, // 工作區(qū)是否有修改 modifyParams: {}, primaryTableData: [], primaryTableTotalRows: 0, isOpenedWorkshop: true, indexSelected: 0 }
moduleName
定義的是這個(gè)總的JSON對(duì)象中模塊所對(duì)應(yīng)的鍵名醉途,moduleParams
是具體存數(shù)據(jù)的結(jié)構(gòu)。下面說(shuō)明一下每個(gè)存儲(chǔ)結(jié)構(gòu)具體的含義:
- workTabSelected:右側(cè)也是一個(gè)Tab頁(yè)的結(jié)構(gòu)砖茸,可能包含增加隘擎、修改和幫助等等,這個(gè)字段標(biāo)識(shí)了當(dāng)前所對(duì)應(yīng)的Tab頁(yè)凉夯。
- canClickModifyTab:這個(gè)字段根據(jù)業(yè)務(wù)需要货葬,決定修改Tab是否可以點(diǎn)擊。
- isModifyTabChanged:記錄了修改Tab中的數(shù)據(jù)是否已經(jīng)修改過(guò)劲够。
- modifyParams:主從路由直接具體業(yè)務(wù)數(shù)據(jù)交換處震桶。
- primaryTableData:主路由中的數(shù)據(jù)表數(shù)據(jù)存在這里,主要完成的事情就是征绎,如果在輔助路由中完成了數(shù)據(jù)變更蹲姐,在輔助路由中操作這個(gè)數(shù)據(jù)域,可以利用Angular的雙向綁定特性,完成主路由中界面的自動(dòng)更新淤堵,而無(wú)需手動(dòng)去做寝衫。
- primaryTableTotalRows:記錄了主路由數(shù)據(jù)表的數(shù)據(jù)總行數(shù)。這個(gè)字段跟分頁(yè)有關(guān)拐邪,關(guān)于分頁(yè)的實(shí)現(xiàn)慰毅,會(huì)具體寫一篇來(lái)介紹。
- isOpenedWorkshop:記錄了輔助路由所對(duì)應(yīng)的區(qū)域界面是否是打開(kāi)狀態(tài)扎阶。
- indexSelected:這個(gè)字段記錄了我們修改時(shí)汹胃,修改的那條記錄在主界面中所對(duì)應(yīng)的數(shù)組的索引,通過(guò)這個(gè)數(shù)字东臀,可以直接操作primaryTableData的具體索引處的數(shù)據(jù)着饥。
-
主路由某具體組件的業(yè)務(wù)邏輯代碼:
核心部分在于主數(shù)據(jù)表中對(duì)checkbox的點(diǎn)擊行為的處理,處理函數(shù)如下:onCheckBoxClick(index, event) { if (this.full.params['userModule'].workTabSelected === 2) { if (this.full.params['userModule'].isModifyTabChanged) { this.showToastInfo(0); event.preventDefault(); } else { if (this.checkBoxSelected[index]) { delete this.checkBoxSelected[index]; event.target.parentElement.parentElement.setAttribute('style', 'background-color:'); } else { const count = Object.keys(this.checkBoxSelected).toString(); this.checkBoxSelected[count].parentElement.parentElement.setAttribute('style', 'background-color:'); delete this.checkBoxSelected[count]; this.checkBoxSelected[index] = event.target; event.target.parentElement.parentElement.setAttribute('style', 'background-color: #e5f1fb'); } this.full.params['userModule'].workTabSelected = Object.keys(this.checkBoxSelected).length === 1 ? 2 : 1; } } else { if (this.checkBoxSelected[index]) { delete this.checkBoxSelected[index]; event.target.parentElement.parentElement.setAttribute('style', 'background-color:'); } else { this.checkBoxSelected[index] = event.target; event.target.parentElement.parentElement.setAttribute('style', 'background-color: #e5f1fb'); } } const checkBoxSelectedLength = Object.keys(this.checkBoxSelected).length; this.canDelFlag = checkBoxSelectedLength > 0; this.full.params['userModule'].canClickModifyTab = checkBoxSelectedLength === 1; if (checkBoxSelectedLength === 1) { const count = Object.keys(this.checkBoxSelected)[0]; this.full.params['userModule'].modifyParams = { staffId: this.full.params['userModule'].primaryTableData[count].staffId, userName: this.full.params['userModule'].primaryTableData[count].userName, idNum: this.full.params['userModule'].primaryTableData[count].idNum, phoneNum: this.full.params['userModule'].primaryTableData[count].phoneNum, staffType: this.full.params['userModule'].primaryTableData[count].staffType, staffTitle: this.full.params['userModule'].primaryTableData[count].staffTitle, staffPosition: this.full.params['userModule'].primaryTableData[count].staffPosition, staffRank: this.full.params['userModule'].primaryTableData[count].staffRank, staffDept: this.full.params['userModule'].primaryTableData[count].staffDept, userAddress: this.full.params['userModule'].primaryTableData[count].userAddress, }; this.full.params['userModule'].indexSelected = +count; } }
checkBoxSelected
是一個(gè)JSON數(shù)組惰赋,鍵是數(shù)據(jù)表中數(shù)組的索引宰掉,值存儲(chǔ)的是相應(yīng)的界面元素,這么做是為了能夠直觀地操作界面元素的背景赁濒。其他部分的代碼很清楚轨奄,就不解釋了。 -
輔助路由界面代碼部分:
因?yàn)檫@個(gè)部分對(duì)Angular表單的運(yùn)用有一點(diǎn)難度拒炎,所以還是詳細(xì)說(shuō)明一下挪拟。<tab heading="修改" (select)="full.params.userModule.workTabSelected=2" [disabled]="!full.params.userModule.canClickModifyTab" [active]="full.params.userModule.workTabSelected===2"> <div class="animated fadeIn" *ngIf="this.full.params.userModule.workTabSelected===2&&initModifyForm()" style="padding:28px 5px"> <form [formGroup]="formModelModify"> <div class="input-group mb-1"> <div style="text-align:center; width:100%"> <button type="button" class="btn btn-success1 px-3" style="margin-right:10%" [disabled]="formModelModify.invalid" (click)="modifyUser()">修改</button> <button type="button" class="btn btn-primary px-3" (click)="formModelModify.reset(full.params.userModule.modifyParams)">重置</button> </div> </div> <div class="input-group mb-2" style="margin-top:28px"> <span class="input-group-addonnew">姓名</span> <input #usernameModify type="text" class="form-control col-md-9" formControlName="userName" required /> </div> <label class="input-group mb-2" style="margin-left:5%" [hidden]="formModelModify.get('userName').valid||formModelModify.get('userName').pristine">姓名應(yīng)為2~10位的中英文</label> <div class="input-group mb-2"> <span class="input-group-addonnew">工號(hào)</span> <input #staffIdModify type="text" class="form-control col-md-9" formControlName="staffId" readonly /> </div> <label class="input-group mb-2" style="margin-left:5%" [hidden]="formModelModify.get('staffId').valid||formModelModify.get('staffId').pristine">工號(hào)為數(shù)字形式</label> <div class="input-group mb-2"> <span class="input-group-addonnew">電話號(hào)碼</span> <input #mobileModify type="text" class="form-control col-md-9" formControlName="phoneNum" [value]=full.params.userModule.modifyParams.phoneNum required /> </div> <label class="input-group mb-2" style="margin-left:5%" [hidden]="formModelModify.get('phoneNum').valid||formModelModify.get('phoneNum').pristine">手機(jī)號(hào)不正確,請(qǐng)輸入正確號(hào)碼</label> <div class="input-group mb-2"> <span class="input-group-addonnew">人事科室</span> <input #departmentModify list="staffDepartmentModify" class="form-control col-md-9" formControlName="staffDept" required /> <datalist id="staffDepartmentModify"> <option class="form-control" *ngFor="let staffDepartment of this.categoryResponse.staffDeptList" [value]="staffDepartment.deptname">{{staffDepartment.deptpycode}}</option> </datalist> </div> </form> </div> </tab>
界面代碼相對(duì)容易理解击你,果然在于以下業(yè)務(wù)邏輯處理部分:
在構(gòu)造器中:this.formControlsConfig = { staffId: ['', numberValidator], userName: ['', truenameValidator], phoneNum: ['', mobileValidator], staffDept: [''], }; this.formModelModify = fb.group(this.formControlsConfig);
因?yàn)檎鎸?shí)業(yè)務(wù)中有兩套表單驗(yàn)證器玉组,所以把表單配置信息單獨(dú)寫出來(lái),然后讓兩個(gè)表單控制器分別讀入丁侄。因?yàn)?code>staffDept的驗(yàn)證器是通過(guò)網(wǎng)絡(luò)請(qǐng)求過(guò)來(lái)的數(shù)組動(dòng)態(tài)加載的惯雳,所以在這里初始化時(shí)候,先配置固定的
validator
鸿摇,比如姓名吨凑、工號(hào)和身份證等,然后在網(wǎng)絡(luò)請(qǐng)求回來(lái)以后户辱,再動(dòng)態(tài)將這個(gè)驗(yàn)證器加上,代碼如下:this.formModelAdd.get('staffRank').setValidators(listValidator(this.staffRankList));
另外糙臼,關(guān)于驗(yàn)證器部分庐镐,跟分頁(yè)一樣后面將單獨(dú)寫一篇來(lái)介紹。
public modifyUser() { let respData = ''; const loginUrl = '/user-manage/change-user-info'; const params = {}; Object.keys(this.formModelModify.value).forEach((item) => { params[item] = this.formModelModify.get(item).value; }); this.submitService.postSubmitWithRelativeUrl(loginUrl, JSON.stringify(params)) .then( responseData => respData = responseData, error => this.errorMessage = <any>error) .then(() => { const responseData = JSON.parse(respData).responseCode; if (responseData === '100') { alert('您尚未登錄或會(huì)話過(guò)期变逃,請(qǐng)重新登錄必逆!'); this.router.navigate(['']).then(); } else if (responseData === '1') { // 修改成功 this.toasterService.pop('success', '后臺(tái)管理', '修改成功!'); Object.keys(this.formModelModify.value).forEach((item) => { this.full.params['userModule'].modifyParams[item] = this.formModelModify.get(item).value; }); const currentYear = new Date().getFullYear(); const idNum = this.formModelModify.get('idNum').value; Object.keys(this.formModelModify.value).forEach((item) => { this.full.params['userModule'].primaryTableData[this.full.params['userModule'].indexSelected][item] = this.formModelModify.get(item).value; }); this.full.params['userModule'].primaryTableData[this.full.params['userModule'].indexSelected][ 'age'] = currentYear - parseInt(idNum.substring(6, idNum.length === 18 ? 10 : 8), 0); this.full.params['userModule'].primaryTableData[this.full.params['userModule'].indexSelected]['userGender'] = parseInt(idNum.substr(idNum.length === 18 ? 16 : 14, 1), 0) % 2; this.formModelModify.reset(this.full.params['userModule'].modifyParams); this.full.params['userModule'].isModifyTabChanged = false; } else { this.toasterService.pop('info', '后臺(tái)管理', '修改失敗名眉!'); } }); } public initModifyForm() { if (this.localIndex !== this.full.params['userModule'].indexSelected) { this.formModelModify.reset(this.full.params['userModule'].modifyParams); this.localIndex = this.full.params['userModule'].indexSelected; } this.full.params['userModule'].isModifyTabChanged = !this.formModelModify.pristine; return true; }
先說(shuō)
initModifyForm
函數(shù)粟矿,以為輔助路由中的表單數(shù)據(jù)是通過(guò)this.formModelModify.reset()
函數(shù)初始化的,所以沒(méi)有辦法通過(guò)數(shù)據(jù)的雙向綁定來(lái)讓左邊主路由的動(dòng)作觸發(fā)右邊的動(dòng)態(tài)加載损拢,所以利用了Angular的監(jiān)視scope域陌粹,把處理表單初始化的函數(shù)寫在右邊組件的*ngIf
里,不斷監(jiān)視右邊往主界面組件中傳的值福压,然后再處理這個(gè)業(yè)務(wù)掏秩。
再說(shuō)modifyUser
函數(shù),我們通過(guò)遍歷表單控制器里的所有控制域來(lái)減少生成網(wǎng)絡(luò)請(qǐng)求參數(shù)的生成過(guò)程荆姆,然后通過(guò)直接設(shè)置主界面組件中相應(yīng)的需要修改的數(shù)組索引處的值來(lái)直接觸發(fā)主路由組件的數(shù)據(jù)表更新蒙幻,從而省去了手動(dòng)操作主路由數(shù)據(jù)表或是其重加載。 -
最后講如何通過(guò)自定義路由策略來(lái)自由控制通過(guò)路由方式加載的組件的銷毀邏輯:
首先app.module
中的providers
數(shù)組中要說(shuō)明:{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
然后再看具體的
CustomRouteReuseStrategy
這個(gè)類:export class CustomRouteReuseStrategy implements RouteReuseStrategy { _cacheRouters: { [key: string]: any } = {}; private storeFlag: boolean; private clearFlag: boolean; constructor() { this.storeFlag = true; this.clearFlag = false; } shouldDetach(route: ActivatedRouteSnapshot): boolean { // 對(duì)所有路由允許復(fù)用 return true; } store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { // 按path作為key存儲(chǔ)路由快照&組件當(dāng)前實(shí)例對(duì)象 // path等同RouterModule.forRoot中的配置 if (this.clearFlag) { delete this._cacheRouters; this._cacheRouters = {}; this.clearFlag = false; } else { if (this.storeFlag) { this._cacheRouters[this.interceptString(route['_routerState']['url'])] = { snapshot: route, handle: handle }; } else { this.storeFlag = true; } } } shouldAttach(route: ActivatedRouteSnapshot): boolean { // 在緩存中有的都認(rèn)為允許還原路由 return !!route.routeConfig && !!this._cacheRouters[this.interceptString(route['_routerState']['url'])] && !!this._cacheRouters[this.interceptString(route['_routerState']['url'])].handle; } retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { // 從緩存中獲取快照胆筒,若無(wú)則返回null if (!route.routeConfig || !this._cacheRouters[this.interceptString(route['_routerState']['url'])] || !this._cacheRouters[this.interceptString(route['_routerState']['url'])].handle) { return null; } // 當(dāng)前緩存對(duì)象里的handle與當(dāng)前請(qǐng)求的路由所對(duì)應(yīng)的handle不同邮破,以至于angular路由內(nèi)部策略通不過(guò),會(huì)拋出錯(cuò)誤仆救,終止當(dāng)前邏輯抒和。 return this._cacheRouters[this.interceptString(route['_routerState']['url'])].handle; } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { // 同一路由時(shí)復(fù)用路由 if (curr.queryParams['paramsExtras']) { const paramsExtras = JSON.parse(curr.queryParams['paramsExtras']); if (paramsExtras['reference'] !== '') { this.storeFlag = false; delete this._cacheRouters[paramsExtras['reference']]; } this.clearFlag = paramsExtras['isLast'] === 'true'; } return future.routeConfig === curr.routeConfig; } private interceptString(route) { const index = route.indexOf('?'); return index === -1 ? route : route.substring(0, index); } }
其中,判斷
paramsExtras['reference'] !== ''
這一句派桩,是因?yàn)槲覀冊(cè)谧雒恳淮温酚商D(zhuǎn)的時(shí)候都會(huì)傳一個(gè)paramsExtras
值构诚,然后我們定義,當(dāng)Tab頁(yè)關(guān)閉的時(shí)候铆惑,會(huì)把要關(guān)閉的Tab頁(yè)所應(yīng)的路由名帶上范嘱,在路由策略類中截獲到路由跳轉(zhuǎn)傳遞的參數(shù)時(shí),如果其中帶了reference
员魏,也就是我們正在關(guān)閉的路由名丑蛤,就將緩存中相應(yīng)的信息給刪掉。關(guān)于怎樣定義存儲(chǔ)邏輯撕阎,我們測(cè)試到shouldReuseRoute
接口會(huì)在store
接口之前調(diào)用受裹,所以在shouldReuseRoute
中寫好判斷邏輯,然后執(zhí)行到store
時(shí)再判斷存儲(chǔ)與否虏束。另外棉饶,如果傳遞過(guò)來(lái)的參數(shù)中的clearFlag
為真,則我們確定用戶執(zhí)行的操作狀態(tài)是所以Tab頁(yè)均已關(guān)閉镇匀,所以我們將此處緩存的所有信息全部清空照藻。
感想:
- 關(guān)于路由部分,我想我可能試了好多語(yǔ)法汗侵,通常情況下會(huì)報(bào)各種我們作為普通開(kāi)發(fā)者看不懂的錯(cuò)幸缕,總之按照我上面的寫法應(yīng)該沒(méi)問(wèn)題群发。
參考資料:
- 路由:
- Angular Router Introduction - Child Routes, Auxiliary Routes, Avoid Common Pitfalls
- Auxiliary route from a lazy loaded module
- Angular 2: Working with auxiliary routes
- Angular 2 - Routing - Aux Routes
- How do aux routes work in Angular2 RC5/Router3 RC1?
- Angular2 router - Auxiliary route
- Angular2 : Router and auxiliary routes
- Angular2 RC6 Route學(xué)習(xí)
- [Angular2 Router] Configure Auxiliary Routes in the Angular 2 Router - What is the Difference Towards a Primary Route?
- Error: Cannot reattach ActivatedRouteSnapshot created from a different route