手把手教你前后分离架构(七) 动态路由与菜单实现

前面章节我们实现的菜单功能都是展示的固定数据,跳转路由也是配置的固定路由,生产实践中非常不便,所以要实现菜单通过业务功能实现维护。菜单信息存储到数据库中,和传统应用差别不大,前端路由通过菜单信息预先生成动态路由,实现菜单跳转。

系统菜单展示后台实现

创建菜单表

CREATE TABLE `sys_menu` (
  `id` BIGINT NOT NULL,
  `parent_id` BIGINT DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  `name` VARCHAR(50)  DEFAULT NULL COMMENT '菜单名称',
  `url` VARCHAR(200)  DEFAULT NULL COMMENT '菜单URL',
  `perms` VARCHAR(500) DEFAULT NULL COMMENT '授权(多个用逗号分隔)',
  `type` CHAR(6)  DEFAULT NULL COMMENT '类型 [0:目录、1:菜单、2:按钮]',
  `icon` VARCHAR(50) DEFAULT NULL COMMENT '菜单图标',
  `order_num` INT DEFAULT NULL COMMENT '排序',
  `status` CHAR(6)  DEFAULT '0'  DEFAULT NULL COMMENT '启用状态:[0:启用,1:停用]',
  `create_by` BIGINT DEFAULT NULL COMMENT '创建人ID',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `update_by` BIGINT DEFAULT NULL COMMENT '修改人ID',
  `update_time` DATETIME DEFAULT NULL COMMENT '修改时间',
  `deleted` INT DEFAULT '0' COMMENT '是否被删除(0:未删除;1:已删除)',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='菜单管理'

代码生成拷贝至项目工程。

自定义sql

xml文件种添加自定义sql。遍历菜单资源树,前端展现菜单树。

 <select id="queryListByUserId" resultType="com.example.sys.vo.SysMenuVO">
        SELECT *
        FROM sys_menu u
        WHERE status = #{status}
        <if test="delField != null and delField != ''">
            AND #{delField} = #{delVal}
        </if>
        order by ORDER_NUM
    </select>

这里暂时查询全部菜单数据,后续添加权限,会很根据权限展示相应菜单。

delField、delVal为逻辑删除字段和字段值,Mapper接口使用mybaties-plus内置变量传入,最好不要手动在sql种手动编写容易出错。这里查询显示所有菜单只是单表查询,用mybaties-plus自由接口即可实现,完全不需要自定义sql。这里之所以使用自定义sql因为后续这个sql会比较复杂,会关联查询用户权限内的菜单。

Mapper接口

List<SysMenuVO> queryListByUserId(String status, String delField, String delVal);
service层接口
 private GlobalConfig gc = GlobalConfigUtils.defaults();
 
    @Override
    public List<SysMenuVO> getCurrentUserMenus() {
        List<SysMenuVO> list = sysMenuMapper.queryListByUserId( "0", gc.getDbConfig().getLogicDeleteField(), gc.getDbConfig().getLogicNotDeleteValue());
 
        List<SysMenuVO> r_list = list.stream()
                .filter(item -> item.getParentId() == 0L)
                .collect(Collectors.toList());
        r_list.forEach(item -> getChildMenu(item, list));
        return r_list;
    }
 
    private void getChildMenu(SysMenuVO parentMenu, List<SysMenuVO> menuVOs) {
        List<SysMenuVO> childList = menuVOs.stream().filter(item -> item.getParentId().equals(parentMenu.getId())).collect(Collectors.toList());
        if (childList!=null&&childList.size()>0) {
            parentMenu.setList(childList);
            childList.forEach(item -> getChildMenu(item, menuVOs));
        }
    }

controller接口

    @ApiOperation(value = "导航菜单")
    @GetMapping("/myMenu")
    public R<List<SysMenuVO>> myMenu() throws Exception {
        List<SysMenuVO> menuList = sysMenuService.getCurrentUserMenus();
        return R.ok().info(menuList);
    }

 使用swagger测试可以看到该接口

 但是此时测试此接口会提示报错,后台报错提示找不到咱们的xml文件,所以我们需要配置xml路径。

POM文件配置

        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>

再次测试成功。

前端导航菜单实现

动态路由实现

路由加载前读取数据库中的菜单树信息,将菜单树信息保存到store中,并根据菜单信息初始化相关路由信息。

路由index.js

const router = new Router({
  routes: routes.concat(mainRoutes)
})
 
router.beforeEach((to, from, next) => {
  JSON.parse(sessionStorage.getItem('dynamicMenuRoutes') || '[]')
  if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, routers) === 'global') {
    next()
  } else {
    http({
      url: http.adornUrl("/sys/menu/myMenu"),
      method: 'get'
    }).then(({data}) => {
      if (data && data.success == true) {
        //取出动态路由传入
        let routArr = JSON.parse(sessionStorage.getItem('dynamicRoutes')) || [];
        routArr.forEach((item => {
          let url = item.name.replace('-', '/');
          item.component=_import(`${url}`) || null
        }));

        fnAddDynamicMenuRoutes(data.info, routArr);
        router.options.isAddDynamicMenuRoutes = true
        sessionStorage.setItem('menuList', JSON.stringify(data.info || '[]'))
        next({ ...to, replace: true })
      } else {
        sessionStorage.setItem('menuList', '[]')
        next()
      }
    }).catch((e) => {
      console.log('路由异常',e)
      Message.error(tips.error);
      router.push({ name: 'login'},()=>{},()=>{})
    })

  }
})
 
/**
 * 判断当前路由类型, global: 全局路由, main: 主入口路由
 * @param {*} route 当前路由
 */
function fnCurrentRouteType (route, globalRoutes = []) {
  var temp = []
  for (var i = 0; i < globalRoutes.length; i++) {
    if (route.path === globalRoutes[i].path) {
      return 'global'
    } else if (globalRoutes[i].children && globalRoutes[i].children.length >= 1) {
      temp = temp.concat(globalRoutes[i].children)
    }
  }
  return temp.length >= 1 ? fnCurrentRouteType(route, temp) : 'main'
}
 
/**
 * 添加动态(菜单)路由
 * @param {*} menuList 菜单列表
 * @param {*} routes 递归创建的动态(菜单)路由
 */
function fnAddDynamicMenuRoutes (menuList = [], routes = []) {
  var temp = []
  for (var i = 0; i < menuList.length; i++) {
    if (menuList[i].list && menuList[i].list.length >= 1) {
      temp = temp.concat(menuList[i].list)
    } else if (menuList[i].url && /\S/.test(menuList[i].url)) {
      menuList[i].url = menuList[i].url.replace(/^\//, '')
      var route = {
        path: menuList[i].url.replace('/', '-'),
        component: null,
        name: menuList[i].url.replace('/', '-'),
        meta: {
          menuId: menuList[i].id,
          title: menuList[i].name,
          isDynamic: true,
          isTab: true,
          iframeUrl: ''
        }
      }
      // url以http[s]://开头, 通过iframe展示
      if (isURL(menuList[i].url)) {
        route['path'] = `i-${menuList[i].id}`
        route['name'] = `i-${menuList[i].id}`
        route['meta']['iframeUrl'] = menuList[i].url
      } else {
        try {
          route['component'] = _import(`${menuList[i].url}`) || null
        } catch (e) {
 
        }
      }
      routes.push(route)
    }
  }
  if (temp.length >= 1) {
    fnAddDynamicMenuRoutes(temp, routes)
  } else {
    mainRoutes.name = 'main-dynamic'
    mainRoutes.children = routes
    router.addRoutes([
      mainRoutes,
      { path: '*', redirect: { name: '404' } }
    ])
    sessionStorage.setItem('dynamicMenuRoutes', JSON.stringify(mainRoutes.children || '[]'))
  }
}
 
export default router

菜单树展现

读取store中缓存的菜单信息展示导航菜单

子组件sub-menu

<template>
  <el-submenu
    v-if="menu.list && menu.list.length >= 1"
    :index="menu.id + ''"
    :popper-class="'site-sidebar--' + sidebarLayoutSkin + '-popper'">
    <template slot="title">
      <i :class="menu.icon"></i>
      <span>{{ menu.name }}</span>
    </template>
    <sub-menu
      v-for="item in menu.list"
      :key="item.id"
      :menu="item"
      :dynamicMenuRoutes="dynamicMenuRoutes">
    </sub-menu>
  </el-submenu>
  <el-menu-item v-else :index="menu.id + ''" @click="gotoRouteHandle(menu)">
    <i :class="menu.icon"></i>
    <span>{{ menu.name }}</span>
  </el-menu-item>
</template>

<script>
  import SubMenu from './main-sidebar-sub-menu'
  import { isURL } from '@/utils/validate'
  export default {
    name: 'sub-menu',
    props: {
      menu: {
        type: Object,
        required: true
      },
      dynamicMenuRoutes: {
        type: Array,
        required: true
      }
    },
    components: {
      SubMenu
    },
    computed: {
      sidebarLayoutSkin: {
        get () { return this.$store.state.common.sidebarLayoutSkin }
      }
    },
    methods: {
      // 通过menuId与动态(菜单)路由进行匹配跳转至指定路由
      gotoRouteHandle (menu) {
        if(menu.isJump==1&&isURL(menu.url)) {//跳出新窗口标识,并且以http、https的url
          //跳出新窗口
          window.open(menu.url)
        }else{
          var route = this.dynamicMenuRoutes.filter(item => item.meta.menuId === menu.id)
          if (route.length >= 1) {
            this.$router.push({name: route[0].name})
          }
        }
        if (this.$store.state.common.clientType=='phone')this.$store.commit('common/updateSidebarFold', true);// 手机模式点击完菜单自动关闭
      }
    }
  }
</script>

菜单树数据保存在$store.state.common.menuList 中,NavMenu导航菜单使用$store.state.common.menuList数据进行展示。

<sub-menu
          v-show="showSidebar"
          v-for="menu in menuList"
          :key="menu.id"
          :menu="menu"
          :dynamicMenuRoutes="dynamicMenuRoutes">
</sub-menu>

添加测试数据

insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('1','0','系统管理',NULL,NULL,'0','el-icon-setting','0','0','1','2022-07-14 14:05:11',NULL,NULL,'0');
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('2','1','菜单管理','sys/menu-list',NULL,'1',NULL,'1','0','1','2022-07-14 14:05:16','1','2022-07-14 15:06:07','0');
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('3','1','用户管理','sys/user-list',NULL,'1',NULL,'0','0','1','2022-07-14 14:05:13','1','2022-07-14 15:06:01','0');
insert into `sys_menu` (`id`, `parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) values('1547513127237103617','1','角色管理','sys/role-list',NULL,'1','','3','0','1','2022-07-14 17:27:36',NULL,NULL,'0');

效果展示

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值