基于Angular2+的Tab頁(yè)+側(cè)邊菜單比較完整的解決方案

一、具體需求:

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

  1. 先說(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ù)中referenceisLast是要穿給自定義路由策略組件的,自定義路由策略組件在截獲路由參數(shù)時(shí)观腊,將reference鍵所對(duì)應(yīng)的存儲(chǔ)刪掉邑闲,然后判斷isLast的值,如果為真梧油,則將緩存中的所有內(nèi)容都刪除苫耸。
  2. 左側(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)存泣侮,具體功能這里先不講即彪。

  3. 輔助菜單界面部分:

    <aside class="aside-menu">
      <router-outlet name="sub"></router-outlet>
    </aside>
    
  4. 全局路由定義:

    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ù)得也不讓人滿意。

  5. 主路由與輔助路由組件之間數(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ù)着饥。
  1. 主路由某具體組件的業(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)的界面元素,這么做是為了能夠直觀地操作界面元素的背景赁濒。其他部分的代碼很清楚轨奄,就不解釋了。

  2. 輔助路由界面代碼部分:
    因?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ù)表或是其重加載。

  3. 最后講如何通過(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)閉镇匀,所以我們將此處緩存的所有信息全部清空照藻。

感想:

  1. 關(guān)于路由部分,我想我可能試了好多語(yǔ)法汗侵,通常情況下會(huì)報(bào)各種我們作為普通開(kāi)發(fā)者看不懂的錯(cuò)幸缕,總之按照我上面的寫法應(yīng)該沒(méi)問(wèn)題群发。

參考資料:

  1. 路由:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市发乔,隨后出現(xiàn)的幾起案子熟妓,更是在濱河造成了極大的恐慌,老刑警劉巖栏尚,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件起愈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡抵栈,警方通過(guò)查閱死者的電腦和手機(jī)告材,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)古劲,“玉大人斥赋,你說(shuō)我怎么就攤上這事〔” “怎么了疤剑?”我有些...
    開(kāi)封第一講書人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)闷堡。 經(jīng)常有香客問(wèn)我隘膘,道長(zhǎng),這世上最難降的妖魔是什么杠览? 我笑而不...
    開(kāi)封第一講書人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任弯菊,我火速辦了婚禮,結(jié)果婚禮上踱阿,老公的妹妹穿的比我還像新娘管钳。我一直安慰自己,他們只是感情好软舌,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布才漆。 她就那樣靜靜地躺著,像睡著了一般佛点。 火紅的嫁衣襯著肌膚如雪醇滥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,268評(píng)論 1 309
  • 那天超营,我揣著相機(jī)與錄音鸳玩,去河邊找鬼。 笑死演闭,一個(gè)胖子當(dāng)著我的面吹牛不跟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播船响,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼躬拢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了见间?” 一聲冷哼從身側(cè)響起聊闯,我...
    開(kāi)封第一講書人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎米诉,沒(méi)想到半個(gè)月后菱蔬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡史侣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年拴泌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惊橱。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蚪腐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出税朴,到底是詐尸還是另有隱情回季,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布正林,位于F島的核電站泡一,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏觅廓。R本人自食惡果不足惜鼻忠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望杈绸。 院中可真熱鬧帖蔓,春花似錦、人聲如沸蝇棉。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)篡殷。三九已至钝吮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間板辽,已是汗流浹背奇瘦。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劲弦,地道東北人耳标。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像邑跪,于是被迫代替她去往敵國(guó)和親次坡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呼猪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)砸琅,斷路器宋距,智...
    卡卡羅2017閱讀 134,702評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,288評(píng)論 25 707
  • 一.課程簡(jiǎn)介 (注意:這里的AngularJS指的是2.0以下的版本) AngularJS的優(yōu)點(diǎn): 模板功能強(qiáng)大豐...
    壹點(diǎn)微塵閱讀 923評(píng)論 0 0
  • 這是我第96篇原創(chuàng)日記谚赎,我是日記星球226號(hào)星寶寶。 9月10日 深圳晴 今天是生命密碼最后一天的課程诱篷。...
    天鳴老師閱讀 870評(píng)論 0 4
  • 北方有南音閱讀 184評(píng)論 4 2