In Vue, how to design multi-level menus to look professional?

It's a cliché!

Although we are Java apes, the front-end code is not ambiguous in writing! Today I want to talk to you about this front-end dynamic menu, how to design it to look professional! Take our TienChin project as an example, let's take a look.

Let's take a screenshot to see the effect:

So how is such a menu designed?

Today, I don't want to talk too much about the technical details with you. I will talk about how the routing is designed. Once everyone understands how the routing is designed, the rest of the problems are trivial issues.

1. Routing Design

Some friends have done vhr and know how to implement dynamic menu in vhr. Song Ge, like everyone, is also learning and making progress. Today I want to discuss with you the implementation plan of dynamic menu in TienChin project to see if it is a better solution.

1.1 Menu Design

Let's review the solution in vhr with my friends:

In vhr, the control of permissions is only controlled to the second-level menu, that is, the first-level menu has nothing to do with permissions. For example, now there is a first-level menu A and a second-level menu B, B is the menu in A, now suppose:

  • If the current user rights can view the B menu, then the A menu will automatically be displayed.
  • If the current user authority cannot view the B menu, and there are no other submenus to display in the A menu, then the A menu will not be displayed.

In other words, whether menu A is displayed or not depends on whether there are submenus in it that need to be displayed. If there is, menu A will be displayed, if not, menu A will not be displayed.

The idea in vhr is this.

In the TienChin project, this block has some changes:

If there is only one B in A, then it seems that there is no need to make a two-level menu, just show B directly? User operation is also convenient!

This is the first difference.

1.2 Routing data

Based on the first point, it involves a question, that is, how to design the routing interface? The most important thing is what should the data format returned by the interface look like?

First of all, you should know that the routing here is a nested routing, that is, the second-level menu is nested in the first-level menu. Even if this place is displayed, there is no hierarchical relationship, such as the promotion in the above figure, but the underlying data structure should be nested routing.

Okay, let's not talk about it, let's look at a piece of routing JSON:

[{
	"name": "Monitor",
	"path": "/monitor",
	"hidden": false,
	"redirect": "noRedirect",
	"component": "Layout",
	"alwaysShow": true,
	"meta": {
		"title": "系统监控",
		"icon": "monitor",
		"noCache": false,
		"link": null
	},
	"children": [{
		"name": "Online",
		"path": "online",
		"hidden": false,
		"component": "monitor/online/index",
		"meta": {
			"title": "在线用户",
			"icon": "online",
			"noCache": false,
			"link": null
		}
	}, {
		"name": "Job",
		"path": "job",
		"hidden": false,
		"component": "monitor/job/index",
		"meta": {
			"title": "定时任务",
			"icon": "job",
			"noCache": false,
			"link": null
		}
	}]
}, {
	"path": "/",
	"hidden": false,
	"component": "Layout",
	"children": [{
		"name": "Role",
		"path": "role",
		"hidden": false,
		"component": "system/role/index",
		"meta": {
			"title": "角色管理",
			"icon": "peoples",
			"noCache": false,
			"link": null
		}
	}]
}]
复制代码

这里我举了两个菜单的例子,这两个例子比较具有代表性,这个菜单最终显示效果大概类似下面这样:

  • 系统监控
    • 在线用户
    • 定时任务
  • 角色管理

大概显示效果如上图。

接下来我就来说一下这里几个典型属性:

  1. redirect:noRedirect 表示该路由在面包屑导航中不可被点击。
  2. alwaysShow:如果这个属性设置为 false,那么当当前菜单只有一个子菜单的时候,默认情况下就只会显示子菜单,而忽略父菜单(如 1.1 小节所述),但是如果将该属性设置为 true,则无论当前菜单有几个子菜单,都会将当前菜单展示出来(这就类似于 vhr 中的效果了)。
  3. 每一个父菜单都有自己的 path,每一个 children 也有自己的 path,父菜单的 path 加上每一个 children 的 path,共同组成每一个 children 的路径。
  4. 再来看第二个角色管理这个菜单项,由于它的父菜单中只有一个子菜单项,并且父菜单中也没有 alwaysShow 属性,所以这个菜单项在最终展示的时候,就只展示里边的角色管理,父菜单则不会展示出来(正好,生成的 JSON 中也没说父菜单的名字、图标等属性)。

当然,不是说你的 JSON 这么写就自动这么显示,JSON 中的东西只是一个标记,最终怎么显示,还要看渲染:

<div v-if="!item.hidden">
  <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
    <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
      <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
        <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
      </el-menu-item>
    </app-link>
  </template>
  <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
    <template slot="title">
      <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
    </template>
    <sidebar-item
      v-for="child in item.children"
      :key="child.path"
      :is-nest="true"
      :item="child"
      :base-path="resolvePath(child.path)"
      class="nest-menu"
    />
  </el-submenu>
</div>
复制代码

还有一个函数我就没有列出来了,反正我们看名字也大概知道每一个函数的含义。

大家看,这个 div 中实际上分为了两部分,上面 template 专门用来处理 children 中只有一项的情况(角色管理),具体处理方式就是把 children 拿出来显示,其他的则不考虑,具体执行的时候不一定是只有一个 children,也有可能压根就没有 children,此时直接显示 parent 即可(参考 1.3 小节)。

下面的 el-submenu 则处理 children 有多个的情况(系统监控)。

另外这里涉及到了一个 resolvePath,也是特别关键的一个方法,我们来大致看下:

resolvePath(routePath, routeQuery) {
  if (isExternal(routePath)) {
    return routePath
  }
  if (isExternal(this.basePath)) {
    return this.basePath
  }
  if (routeQuery) {
    let query = JSON.parse(routeQuery);
    return { path: path.resolve(this.basePath, routePath), query: query }
  }
  return path.resolve(this.basePath, routePath)
}
复制代码

这个函数的主要左右,就是处理菜单的路径问题。

我们来看下这个具体的判断逻辑:

  1. 如果这个菜单的路径是一个外链(判断逻辑是查看这个 path 是否有 http 或者 https 等前缀),即 isExternal 返回 true,就把这个路径原封不动返回。
  2. 如果这个菜单的父菜单的路径是一个外链,则将父菜单的 path 原封不懂返回。
  3. 如果有查询参数,就把参数加上。
  4. 最后通过 path.resolve 对路径进行一个简单运算。

有的小伙伴可能对 path.resolve 不熟悉,我简单说下:

path.resolve() 方法可以将多个路径解析为一个规范化的绝对路径,它的处理方式类似于对这些路径逐一进行 cd 操作,然而与 cd 操作不同的是,这些路径可以是文件,并且可不必实际存在(resolve() 方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作)。例如:

path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
复制代码

相当于:

cd foo/bar
cd /tmp/file/
cd ..
cd a/../subfile
pwd
复制代码

举个简单的例子:

path.resolve('/foo/bar', './baz') 
// 输出结果为 
'/foo/bar/baz' 

path.resolve('/foo/bar', '/tmp/file/') 
// 输出结果为 
'/tmp/file' 

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') 
// 当前的工作路径是 /home/javaboy/node,则输出结果为 
'/home/javaboy/node/wwwroot/static_files/gif/image.gif'
复制代码

现在大家知道菜单跳转的路径是怎么来的了吧!

1.3 外链问题

在 TienChin 项目中,菜单还存在一个外链的问题。

这个外链有两种不同的显示思路:

  1. 点击外链,直接打开一个新的选项卡,在新的选项卡中展示新的页面。
  2. 点击外链,在当前项目中打开一个新的选项卡,选项卡中展示新的内容。

对于第一种情况我就不和大家演示了,对于第二种情况,我截个图给大家看下:

就是在当前项目的选项卡中,展示一个外部链接的内容。

我们先来看第一种情况。即点击菜单之后,就在一个新的选项卡中打开网页,这种菜单的 JSON 格式如下:

{
    "name": "Http://www.javaboy.org",
    "path": "http://www.javaboy.org",
    "hidden": false,
    "component": "Layout",
    "meta": {
        "title": "TienChin健身官网",
        "icon": "guide",
        "noCache": false,
        "link": "http://www.javaboy.org"
    }
}
复制代码

这个大家看,也没有 children,因为不需要,这个显示的时候,就当成了只有一个 children 来处理,然后菜单项的 path 是一个 http 路径,一点击,自然就跳到新的选项卡了。

对于第二种情况,即点击外链,在当前项目中打开一个新的选项卡,选项卡中展示链接的内容,它的 JSON 结构类似下面这样:

{
    "name": "Http://www.javaboy.org",
    "path": "/",
    "hidden": false,
    "component": "Layout",
    "meta": {
        "title": "TienChin健身官网",
        "icon": "guide",
        "noCache": false,
        "link": null
    },
    "children": [
        {
            "name": "Www.javaboy.org",
            "path": "www.javaboy.org",
            "hidden": false,
            "component": "InnerLink",
            "meta": {
                "title": "TienChin健身官网",
                "icon": "guide",
                "noCache": false,
                "link": "http://www.javaboy.org"
            }
        }
    ]
}
复制代码

这个其实也没啥好说的,类似于上面系统监控的那种情况,但是只有一个子菜单,在菜单渲染的时候,也是只渲染一个子菜单。由于父子菜单的 path 都不是以 http 或者 https 之类的地址开头,所以这个链接最终生成的 path 是 /www.javaboy.org,然后这个路径的内容将展示在 InnerLink 组件上,最终就是大家上图中所看到的效果了。

好啦,这就是前端菜单的各种情况,后端菜单如何按照需要返回数据,咱们继续~

2. 菜单表

首先我们来看看菜单表的定义,也就是 sys_menu

CREATE TABLE `sys_menu` (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
  `order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
  `path` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '组件路径',
  `query` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路由参数',
  `is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)',
  `is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)',
  `menu_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '#' COMMENT '菜单图标',
  `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3054 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';
复制代码

其实这里很多字段都和我们 vhr 项目项目很相似,我也就不重复啰嗦了,我这里主要和小伙伴们说一个字段,那就是 menu_type

menu_type 表示一个菜单字段的类型,一个菜单有三种类型,分别是目录(M)、菜单(C)以及按钮(F)。这里所说的目录,相当于我们在 vhr 中所说的一级菜单,菜单相当于我们在 vhr 中所说的二级菜单。

当用户从前端登录成功后,要去动态加载的菜单的时候,就查询 M 和 C 类型的数据即可,F 类型的数据不是菜单项,查询的时候直接过滤掉即可,通过 menu_type 这个字段可以轻松的过滤掉 F 类型的数据。小伙伴们想想,F 类型的数据过滤掉之后,剩下的数据不就是一级菜单和二级菜单了,那不就和 vhr 又一样了么!

在 vhr 中,考虑到菜单就是只有两级:一级菜单和二级菜单,一级菜单是目录,二级菜单是则是具体的菜单项,没有三级菜单!所以在 vhr 中,查询菜单的时候我直接用了一个一对多的查询,将一级菜单做一的一方,二级菜单做多的一方,这样比较省事。当然灵活度差一点,所以在 TienChin 项目中,这块还是用上了递归。

3. 前端菜单展示

接下来,前端菜单展示分为了几种情况?这个松哥在之前的文章中已经和大家聊过了,具体可以参考Vue 里,多级菜单要如何设计才显得专业?一文,这里不再赘述。

4. 菜单接口

当用户登录成功之后,会自动请求 /getRouters 接口来获取菜单信息,我们一起来看下:

/**
 * 获取路由信息
 *
 * @return 路由信息
 */
@GetMapping("getRouters")
public AjaxResult getRouters() {
    Long userId = SecurityUtils.getUserId();
    List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
    return AjaxResult.success(menuService.buildMenus(menus));
}
复制代码

这里的查询实际上分为两个步骤:

  1. 根据用户 id 查询到所有的菜单信息,这一步的查询实际上是比较容易的,就单纯的多张表联合在一起,然后过滤出和当前用户相关并且菜单类型为 M 或者 C 的菜单(类型为 F 的表示按钮,就不要了),查询到菜单信息之后,然后进行一个递归操作,将菜单数据的层级排列出来。
  2. menuService.buildMenus 这一步则是将菜单数据专为前端所需要的路由数据。

一共就这两个步骤,我们来逐一进行分析。

先来看查询菜单数据。

/**
 * 根据用户ID查询菜单
 *
 * @param userId 用户名称
 * @return 菜单列表
 */
@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
    List<SysMenu> menus = null;
    if (SecurityUtils.isAdmin(userId)) {
        menus = menuMapper.selectMenuTreeAll();
    } else {
        menus = menuMapper.selectMenuTreeByUserId(userId);
    }
    return getChildPerms(menus, 0);
}
/**
 * 根据父节点的ID获取所有子节点
 *
 * @param list     分类表
 * @param parentId 传入的父节点ID
 * @return String
 */
public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) {
    List<SysMenu> returnList = new ArrayList<SysMenu>();
    for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext(); ) {
        SysMenu t = (SysMenu) iterator.next();
        // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
        if (t.getParentId() == parentId) {
            recursionFn(list, t);
            returnList.add(t);
        }
    }
    return returnList;
}
/**
 * 递归列表
 *
 * @param list
 * @param t
 */
private void recursionFn(List<SysMenu> list, SysMenu t) {
    // 得到子节点列表
    List<SysMenu> childList = getChildList(list, t);
    t.setChildren(childList);
    for (SysMenu tChild : childList) {
        if (hasChild(list, tChild)) {
            recursionFn(list, tChild);
        }
    }
}
/**
 * 得到子节点列表
 */
private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t) {
    List<SysMenu> tlist = new ArrayList<SysMenu>();
    Iterator<SysMenu> it = list.iterator();
    while (it.hasNext()) {
        SysMenu n = (SysMenu) it.next();
        if (n.getParentId().longValue() == t.getMenuId().longValue()) {
            tlist.add(n);
        }
    }
    return tlist;
}
/**
 * 判断是否有子节点
 */
private boolean hasChild(List<SysMenu> list, SysMenu t) {
    return getChildList(list, t).size() > 0;
}
复制代码

这里一共涉及到五个关键方法,我们来逐一进行分析:

  1. selectMenuTreeByUserId:这个方法的执行比较容易,如果当前用户是管理员,那就不用加过滤条件了,直接查询出所有的类型为 M 和 C 的菜单项即可。
  2. getChildPerms:这个方法主要是将前面查询出来的菜单数据进行重组,本来都是一个集合中的数据,现在在该方法中处理成树状,处理的核心逻辑就是调用 recursionFn 方法将之进行递归。
  3. recursionFn:这是最为关键的递归方法了,首先调用 getChildList 获取当前菜单项的 children,然后将获取到的 children 设置给当前菜单项,最后还要遍历获取到的 children,如果这个 children 也是有子菜单的,则继续调用 recursionFn 方法进行处理。
  4. getChildList:这个是查询某一个菜单的子菜单,这个很容易,如果某一个菜单的 parentId 是当前菜单的 id,那么这个菜单就是当前菜单的子菜单。
  5. hasChild:这个是判断给定的菜单是否有子菜单,这个逻辑就比较简单了。

好啦,这个就是整个的查询逻辑,整体上来说是比较容易的,就是查询 M 和 C 类型的菜单,然后再做一个递归操作,将菜单数据变成一个树状数据。

但是因为 SysMenu 和前后端所需要的路由数据的字段名称对不上,并且格式参数等都不符合前端的要求,所以还需要再做一个转换,这就是 menuService.buildMenus 所做的事情了,在分析 menuService.buildMenus 方法之前,我觉得大家有必要先来回顾一下Vue 里,多级菜单要如何设计才显得专业?一文,再来捋一捋菜单的四种情况,我们先来回顾下四种菜单格式:

[{
	"name": "Monitor",
	"path": "/monitor",
	"hidden": false,
	"redirect": "noRedirect",
	"component": "Layout",
	"alwaysShow": true,
	"meta": {
		"title": "系统监控",
		"icon": "monitor",
		"noCache": false,
		"link": null
	},
	"children": [{
		"name": "Online",
		"path": "online",
		"hidden": false,
		"component": "monitor/online/index",
		"meta": {
			"title": "在线用户",
			"icon": "online",
			"noCache": false,
			"link": null
		}
	}, {
		"name": "Job",
		"path": "job",
		"hidden": false,
		"component": "monitor/job/index",
		"meta": {
			"title": "定时任务",
			"icon": "job",
			"noCache": false,
			"link": null
		}
	}]
}, {
	"path": "/",
	"hidden": false,
	"component": "Layout",
	"children": [{
		"name": "Role",
		"path": "role",
		"hidden": false,
		"component": "system/role/index",
		"meta": {
			"title": "角色管理",
			"icon": "peoples",
			"noCache": false,
			"link": null
		}
	}]
},{
    "name": "Http://www.javaboy.org",
    "path": "http://www.javaboy.org",
    "hidden": false,
    "component": "Layout",
    "meta": {
        "title": "TienChin健身官网",
        "icon": "guide",
        "noCache": false,
        "link": "http://www.javaboy.org"
    }
},{
    "name": "Http://www.javaboy.org",
    "path": "/",
    "hidden": false,
    "component": "Layout",
    "meta": {
        "title": "TienChin健身官网",
        "icon": "guide",
        "noCache": false,
        "link": null
    },
    "children": [
        {
            "name": "Www.javaboy.org",
            "path": "www.javaboy.org",
            "hidden": false,
            "component": "InnerLink",
            "meta": {
                "title": "TienChin健身官网",
                "icon": "guide",
                "noCache": false,
                "link": "http://www.javaboy.org"
            }
        }
    ]
}]
复制代码

这四种菜单 JSON,从上往下显示效果依次是:

  1. 一级菜单中有二级菜单,一级菜单不可点击,二级菜单点击后在右边打开相应的页面。
  2. 只有一个一级菜单,点击之后,右边打开相应的页面。
  3. 一个外链(只有一级菜单),点击之后,在新的选项卡中打开新的页面。
  4. 一个外链(只有一级菜单),点击之后,在当前系统中打开新的页面(第三方页面通过 iframe 标签出现在当前系统中)。

牢记这四种不同的菜单情况,再来看 buildMenus 方法,就会容易很多了(下文我说菜单 1、2、3、4 分别对应上面的四种情况):

/**
 * 构建前端路由所需要的菜单
 *
 * @param menus 菜单列表
 * @return 路由列表
 */
@Override
public List<RouterVo> buildMenus(List<SysMenu> menus) {
    List<RouterVo> routers = new LinkedList<RouterVo>();
    for (SysMenu menu : menus) {
        RouterVo router = new RouterVo();
        router.setHidden("1".equals(menu.getVisible()));
        router.setName(getRouteName(menu));
        router.setPath(getRouterPath(menu));
        router.setComponent(getComponent(menu));
        router.setQuery(menu.getQuery());
        router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
        List<SysMenu> cMenus = menu.getChildren();
        if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
            router.setAlwaysShow(true);
            router.setRedirect("noRedirect");
            router.setChildren(buildMenus(cMenus));
        } else if (isMenuFrame(menu)) {
            router.setMeta(null);
            List<RouterVo> childrenList = new ArrayList<RouterVo>();
            RouterVo children = new RouterVo();
            children.setPath(menu.getPath());
            children.setComponent(menu.getComponent());
            children.setName(StringUtils.capitalize(menu.getPath()));
            children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
            children.setQuery(menu.getQuery());
            childrenList.add(children);
            router.setChildren(childrenList);
        } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
            router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
            router.setPath("/");
            List<RouterVo> childrenList = new ArrayList<RouterVo>();
            RouterVo children = new RouterVo();
            String routerPath = innerLinkReplaceEach(menu.getPath());
            children.setPath(routerPath);
            children.setComponent(UserConstants.INNER_LINK);
            children.setName(StringUtils.capitalize(routerPath));
            children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
            childrenList.add(children);
            router.setChildren(childrenList);
        }
        routers.add(router);
    }
    return routers;
}
复制代码

这个方法一个核心思想就是格式转换,其他的都没啥,不过看似简单的逻辑里边,其实也隐藏了很多实现细节。

这个方法细看的话,会有很多地方感觉比较绕。但是,小伙伴们仔细回顾一下Vue 里,多级菜单要如何设计才显得专业?一文,在该文章中,松哥将前端展示出来的菜单分为了四种情况,根据那四种显示的情况,再来看这里的数据组装逻辑,就很好懂了。

首先我们来看 router 基本属性的设置:

  1. 首先是可见性 hidden,这个没啥好说的。
  2. 接下来是菜单的 name 属性,name 属性分为了两种情况:路由的 name 属性是菜单表中的 path 字段值且首字母大写(菜单 1、3、4);如果在一级菜单中,出现了一个菜单 C(本来这一级别只有 M),并且还不是外链,那么就设置菜单的 name 为空字符串(相当于此时不需要 name 属性了,对应菜单 2 的情况)。
  3. 接下来是路由的 path,设置 path 的时候也分好种情况,松哥对照着代码来和大家说一下:
/**
 * 获取路由地址
 *
 * @param menu 菜单信息
 * @return 路由地址
 */
public String getRouterPath(SysMenu menu) {
    String routerPath = menu.getPath();
    // 内链打开外网方式
    if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
        routerPath = innerLinkReplaceEach(routerPath);
    }
    // 非外链并且是一级目录(类型为目录)
    if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
            && UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
        routerPath = "/" + menu.getPath();
    }
    // 非外链并且是一级目录(类型为菜单)
    else if (isMenuFrame(menu)) {
        routerPath = "/";
    }
    return routerPath;
}
复制代码

a. 首先获取从数据库中查询到的 path 属性。 b. 如果当前组件不是一级菜单,并且是在内部组件中展示,那么除去这个 path 里边的 http 或者 https(对应菜单 4 的 children 的情况)。 c. 如果当前组件是一级菜单并且是 M 型并且不是外链,那么就在原有的 path 上加上 / 前缀(对应菜单 1 的一级菜单的 path 情况)。 d. 如果当前组件是一级菜单,且是 C 型菜单,那么设置 path 为 /(对应菜单 2、4 中一级菜单的 path 情况)。 e. 其他情况,菜单都是从数据库查到什么返回什么。

  1. 接下来是设置前端 component,这个菜单项用哪个 component 组件显示出来。
/**
 * 获取组件信息
 *
 * @param menu 菜单信息
 * @return 组件信息
 */
public String getComponent(SysMenu menu) {
    String component = UserConstants.LAYOUT;
    if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
        component = menu.getComponent();
    } else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
        component = UserConstants.INNER_LINK;
    } else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) {
        component = UserConstants.PARENT_VIEW;
    }
    return component;
}
复制代码

a. 首先默认的组件是 Layout(菜单1、2、3、4 的一级菜单)。 b. 如果配置的时候就有 component,并且当前菜单项也不是外链,那么就使用配置的 component(菜单 1、2 的子菜单情况)。 c. 如果不是一级菜单(是一个子菜单),并且是一个在当前系统展示的外链,那么就使用 InnerLink 这个组件(这个组件中有一个 iframe 标签可以把外链展示出来,如菜单 4 的子菜单情况)。 d. 如果配置的时候没有设置组件并且菜单类型是 M(二级菜单中还有三级菜单的情况),那么就设置显示组件为 ParentView。

component 就分为这几种情况。

  1. 接下来就是 query 和 meta 这两个参数就没啥好说的。

接下来就是三个分支的情况了。

  1. 首先第一个 if,处理的就是常规情况,一级菜单中有二级菜单的情况(对应菜单 1 的一级菜单情况)。
  2. 第二个分支处理一级 C 型菜单是非外链的情况(对应菜单 2 的情况),此时自动给该菜单项加上一个 children。
  3. 第三个分支是处理一级 M 型菜单是外链的情况(对应菜单 4 的情况),此时自动给该菜单加上一个 children。
  4. 如果三个分支都不进去,实际上就是菜单 3 的情况了。

好啦,这就是菜单接口分析的全部内容了,有点绕,后面松哥再整几集视频和大家详细分析,对视频感兴趣的小伙伴戳这里:TienChin 项目配套视频来啦

Guess you like

Origin juejin.im/post/7119083323556626446