• 改造delon库的reuse-tab标签使其关联隐藏动态菜单Menu及tab切换时参数不丢失方案


      最近使用了erupt-web项目,非常感谢作者提供了功能强大、使用方便、配置灵活的工具:erupt项目

      在使用到其路由复用标签reuse-tab时,发现tab来回切换时,url上附带的参数会丢失,翻阅官方文档,有明确说明不能使用queryString作为参数:
    ng-alain==>reuse-tab

    不支持 QueryString 查询参数
    复用采用URL来区分是否同一个页面,而 QueryString 查询参数很容易产生重复性误用,因此不支持查询参数,且在复用过程中会强制忽略掉 QueryString 部分。

      这就麻烦了!

      虽然是后台管理平台,但在我们的规划中,各个页面之间是可以互相跳转的,而且菜单一般是进入列表页面,通过点击列表页面上各行、各列的数据进行页面之间、弹出层的交互。

      比如说“用户管理”菜单下“用户列表”这个菜单,点开之后内容区域显示每个用户一行的概括数据,点击列表行末的操作菜单(比如查看详情)即打开新的一个操作内容tab页面,在这个页面里面,展示用户的各种详细信息。

      这种设计就必须要求新打开的tab页要从触发点击事件的页面中传递参数过来,而且tab来回切换、浏览器刷新等操作参数都不能丢失!

      于是就开始研究reuse-tab!

      erupt-web使用的delon库是8.8.0版本,虽然版本有点老,但胜在稳定,参考了博客园大佬的文章:【NG-Alain】组件Reuse-tab的前世今生,其中提到:

    启用reuse-tab后的路由传参
      在任意的单页面web应用程序中,涉及到【路由】的项目都必须严格按照框架推荐的方式进行传参。
      而在ng-alain中,当reuse-tab被启用后(即便在启用之前),URL都不应该继续使用带有【?】的传参方式,无论【?】是在【#】之前还是之后。此规则是强制性要求,不遵守即必然出现错误,表现为参数会在页面切换或刷新后丢失。

      其中提到一种传参方式叫:矩阵参数(matrix parameters),简单来说就是将url中的?/&换成;来传参,于是尝试了一下:

    let menu: Menu = this.menuSrv.getItem(menuCode);
    let queryParams = {id:1,name:"张三"};
    let link = menu.link;
    for (let p in queryParams) {
        link += ';' + (p + '=' + queryParams[p])
    }
    console.log('url:', link);
    this.router.navigateByUrl(link);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

      路由确实跳转成功了,而且tab来回切换参数不会丢失,在跳转过来的页面中读取location.href自行解析参数即可。

      但是新的问题:新开的tab没有名字,使用的url作为tab名称,大佬也提到过:

    在显示多标签式选项卡标题时,优先使用通过ReuseTabService.title设置并存储在ReuseTabService._titleCached数组内的标题,然后使用路由的data对象的title,否则使用菜单的text属性,最后直接显示URL。
    根据此方法最后两行代码,如果设置mode的值为ReuseTabMatchModel.URL时,请务必满足前两个条件之一,否则只会显示URL。

      而我这里为了让所有动态URL都能复用,确实将menu的model值设为了ReuseTabMatchModel.URL。于是只好手动设置ReuseTabService.title = '标题’这种方式来设置tab标题。

      但即使是这样也还不够,一是左侧菜单没有关联打开,二是页面顶部面包屑导航栏是一片空白!而且手动设置tab名称的方法也不靠谱,毕竟erupt-web最终是要打包编译到erupt项目中的,java代码里面可没有手动设置tab名称的地方!

      所以转而就采用最朴素的方法:已知每一个新开的tab都是一个menu,从列表页中跳转的路由只是menu的link上附带了矩阵参数,那么如果我新开tab之前,向menu服务中动态新增一个隐藏的menu,link设置为带了矩阵参数的link,那么新开的tab不是变相的打开了一个menu么,那么关联展开菜单、导航栏不是都有了么?

      于是——首先将各种详情页菜单项作为隐藏菜单进行设置:

    function generateTree(menus, pid): Menu[] {
        let result: Menu[] = [];
        menus.forEach((menu) => {
            if (menu.type === MenuTypeEnum.button || menu.type === MenuTypeEnum.api) {
                return;
            }
            if (menu.pid == pid) {
                let option: Menu = {
                    text: menu.name,
                    key: menu.code, 
                    i18n: menu.name,
                    hide: menu.status === MenuStatusEnum.HIDE,
                    hideInBreadcrumb: false, //隐藏菜单时隐藏面包屑等:false
                    icon: menu.icon || {
                        type: "icon",
                        value: "unordered-list"
                    },
                    link: generateMenuPath(menu.type, menu.value),
                    children: generateTree(menus, menu.id)
                };
                result.push(option);
            }
        });
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

      想法虽好,问题不少!delon-8.8.0这个版本的menu有个bug,当菜单之下的所有子菜单都是hide,页面菜单栏上的父菜单仍然表现为一个菜单组,点击是展开(虽然展开后其下并无任何可视子菜单),再点击是关闭。而我们期望父菜单(实际就是“用户管理列表”菜单)点击之后就直接跳转列表页的。

      后面的高版本官方是有fix了这个bug的:refactor(theme:menu): refactor MenuService #1507

      根据这个PR,是在LayoutDefaultNavComponent初始化时将menu进行了一次hide属性设置及过滤:

     ngOnInit(): void {
    	 menuSrv.change.pipe(takeUntil(destroy$)).subscribe(data => {
    	   menuSrv.visit(data, (i: Nav, _p, depth) => {
    	     ...
    	   });
    	   //如果所有的子菜单都hide,则将父菜单也一并hide
    	   this.fixHide(data);
    	   //过滤隐藏的菜单
    	   this.list = data.filter((w: Nav) => w._hidden !== true);
    	   cdr.detectChanges();
    	 });
    	 
      private fixHide(ls: Nav[]): void {
        const inFn = (list: Nav[]): void => {
          for (const item of list) {
            if (item.children && item.children.length > 0) {
              inFn(item.children);
              if (!item._hidden) {
                item._hidden = item.children.every((v: Nav) => v._hidden);
              }
            }
          }
        };
    
        inFn(ls);
      }
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

      所以,像我这种“用户管理”——“用户列表”下面全部是隐藏菜单的情况,界面直接连“用户列表”都显示不出来,所以升级版本也不是解决这个问题的办法!

      在将delon的8.8.0的源码拉下来尝试修改了数次之后(ng-alain/delon/8.8.0),我注意到这段源码:

    <ng-template #item let-i>
      
      <a *ngIf="i._type <= 2" (click)="to(i)" [attr.data-id]="i.__id" class="sidebar-nav__item-link"
        [ngClass]="{'sidebar-nav__item-disabled': i.disabled}">
        <ng-container *ngIf="i._needIcon">
          <ng-container *ngIf="!collapsed">
            <ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{$implicit: i.icon}">ng-template>
          ng-container>
          <span *ngIf="collapsed" nz-tooltip nzTooltipPlacement="right" [nzTooltipTitle]="i.text">
            <ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{$implicit: i.icon}">ng-template>
          span>
        ng-container>
        <span class="sidebar-nav__item-text" [innerHTML]="i._text">span>
      a>
      
      <a *ngIf="i._type === 3" (click)="toggleOpen(i)" (mouseenter)="showSubMenu($event, i)" class="sidebar-nav__item-link">
        <ng-template [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{$implicit: i.icon}">ng-template>
        <span class="sidebar-nav__item-text" [innerHTML]="i._text">span>
        <i class="sidebar-nav__sub-arrow">i>
      a>
      
      <div *ngIf="i.badge" [attr.title]="i.badge" class="badge badge-{{i.badgeStatus}}" [class.badge-dot]="i.badgeDot">
        <em>{{i.badge}}em>
      div>
    ng-template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

      现在的问题实际上就是进入到i._type === 3 这个分支里面去了,所以我如果直接将源码这里加上是否子菜单全部是隐藏的判断,再重新打包编译,功能肯定是能实现的,只是这手段太勉强了。

      观察源码,发现_type属性的赋值,只在resume方法中,如下:

    resume(callback?: (item: Menu, parentMenum: Menu | null, depth?: number) => void) {
        let i = 1;
        const shortcuts: Menu[] = [];
        this.visit(this.data, (item, parent, depth) => {
          ...
    
          item._type = item.externalLink ? 2 : 1;
          if (item.children && item.children.length > 0) {
            item._type = 3;
          }
    
          ...
    
          if (callback) callback(item, parent, depth);
        });
    	...
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

      resume方法只在add和切换i18n国际化方案的时候会调用,那我就计划在菜单add完成之后,重新遍历一次菜单,将所有子菜单为hide的父菜单的_type强制设置为2,使界面渲染时不进入i._type === 3的分支,代码在erupt-web的default.component.ts中,大致如下:

    ngOnInit() {
            ...
            this.data.getMenu().subscribe(res => {
            	...
            	function generateTree(menus, pid): Menu[] {
            		...
            	}
            	...
            	let rootMenu: Menu[] = [{
                    group: false,
                    hideInBreadcrumb: true, //不生成导航栏
                    text: "~",
                    shortcutRoot: true,
                    children: generateTree(res, null)
                }];
                this.menuSrv.add(rootMenu);
                console.log('菜单初始化完成!', this.menuSrv.menus)
                
                this.menuSrv.menus.forEach((m) => {
                    this.resumeMenuType(m.children)
                });
                console.log('重设type之后的菜单:', this.menuSrv.menus)
            	
    	})
    
    private resumeMenuType(ls: Menu[]): void {
        const inFn = (list: Menu[]): void => {
            for (const item of list) {
                if (item.children && item.children.length > 0) {
                    inFn(item.children);
                    if (!item._hidden) {
                        let h = item.children.every((v: Menu) => v._hidden);
                        if (h === true) {
                        	//所有子菜单都是hidden,但菜单本身link不为空,则设置type=2
                            if (item.link && item.link.trim().length > 0) {
                                item._type = 2;
                            } else {
                                item._type = 3
                            }
                        }
                    }
                }
            }
        };
    
        inFn(ls);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

      以上操作,算是煞费苦心的绕过了delon-8.8.0的bug。项目启动起来,终于看到期望中的菜单了,欣喜若狂!

      带着兴奋的神情,继续计划的第二步:在新开tab之前,向menu服务中动态新增一个隐藏的menu,link设置为带了矩阵参数的link,主要代码如下:

    newTabForMenu(menuCode: string, menuTitle: string, data: any, eruptName: string, primaryKeyName: string, primaryKeyVal: any) {
            let menu: Menu = this.menuSrv.getItem(menuCode);
            if (!menu) {
                menu = this.menuSrv.getHit(this.menuSrv.menus, menuTitle);
            }
            if (!menu) {
                this.msg.warning("无法找到此路由,请检查!");
                return;
            }
    		
    		//根据自身业务,构建矩阵参数
    		// ...
    		let queryParams = {id:1,name:'张三',age:30};
            let link = menu.link;
            for (let p in queryParams) {
                link += ';' + (p + '=' + queryParams[p])
            }
            //在当前menu对象的同级生成一个隐藏菜单,防止router无法找到路由
            const newHideMenuKey = '_hide_' + menu.key + '_' + new Date().getTime();
            let newHideMenu: Menu = {
                text: menu.text,
                key: newHideMenuKey,
                hideInBreadcrumb: false,
                i18n: menu.text,
                hide: true,
                link: link,
                __parent: menu.__parent
            };
            if (menu.__parent) {
                menu.__parent.children.push(newHideMenu);
            }
            this.menuSrv.setItem(newHideMenuKey, newHideMenu);
            console.log('新增隐藏菜单之后:', this.menuSrv.menus);
            
            //跳转到菜单路由
            this.router.navigate([newHideMenu.link]);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

      一气呵成!启动,试验,失败——url中的;=等字符竟然被自动编码了:

    xx%2Fxxx%3Bid%3D1%3Bname%3D%E5%BC%A0%E4%B8%89%3Bage%3D30

      这一串当然与我们设置的原link相距甚远:

    xx/xxx;id=1;name=张三;age=30

      所以reuse-tab自然的404了。

      为了解决这个问题,遍翻各资料站点:  这是说自定义URL序列化规则的方案  这是说使用router.navigate([‘/xxx/xxx’,{k:v,k:v}])的方案

      最后准备使用第二种方案,修改代码:

    newTabForMenu(menuCode: string, menuTitle: string, data: any, eruptName: string, primaryKeyName: string, primaryKeyVal: any) {
    	...
    	let queryParams = {id:1,name:'张三',age:30};
    	...
    	//跳转到菜单路由,参数格式为:['/xxx/xxx',{k:v,k:v}]
        this.router.navigate([menu.link, queryParams]);
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

      这次再试,终于成功了!!!导航栏面包屑很完整,新开tab页菜单栏也关联打开了,终于完整实现了最初的构想!!!

      泪流满面!

      总结这次调整,其实就是使用矩阵参数+动态插入隐藏菜单两个核心技术点,但无论是从reuse-tab的使用、ng-alain的menu的源码实现以及angular的router的使用,方方面面,无一精通,无一不坑!处处全靠锲而不舍的精神慢慢磨出来的效果,所谓念念不忘必有回响,所谓走的人多了也就有了路…

  • 相关阅读:
    带你从0到1开发AI图像分类应用
    解决websocket不定时出现1005错误
    LeetCode //C - 37. Sudoku Solver
    【Python零基础入门篇 · 10】:集合的相关操作
    每日一题——Java编程练习题
    运维学习CentOS 7进行Nightingale二进制部署
    Android Material Design之SwitchMaterial(三)
    【.Net实用方法总结】 整理并总结System.IO中Path类及其方法介绍
    PyQt界面里如何加载本地视频以及调用摄像头实时检测(小白入门必看)
    Flink实时仓库-DWD层(流量域)模板代码
  • 原文地址:https://blog.csdn.net/AJian759447583/article/details/128009374