vue动态路由

vue动态路由

之前一直被动态路由这个问题所困扰, 现在终于实现了, 姑且写篇文章记录下来。

先看实现

在这里插入图片描述
主要是左侧菜单,废话不多说 直接上代码

数据库数据格式

在这里插入图片描述

数据

在这里插入图片描述
在这里插入图片描述

后端代码实现

组装菜单格式数据

组装菜单格式数据这一步的操作在登录之后, 关于登录功能就不必讲了吧
在这里插入图片描述
菜单实体类

@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("sys_menu")
public class SysMenuEntity extends BaseEntity<SysMenuEntity> {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.UUID)
    private String id;

    @ApiModelProperty(value = "菜单名称")
    @TableField("menu_name")
    private String menuName;

    @ApiModelProperty(value = "上级菜单id")
    @TableField("parent_id")
    private String parentId;

    @ApiModelProperty(value = "上级菜单名称")
    @TableField(value = "parentMenuName", exist = false)
    private String parentMenuName;

    @ApiModelProperty(value = "图标")
    @TableField("icon")
    private String icon;

    @ApiModelProperty(value = "图标颜色")
    @TableField("icon_color")
    private String iconColor;

    @ApiModelProperty(value = "路径")
    @TableField("path")
    private String path;

    @ApiModelProperty(value = "重定向")
    @TableField("redirect")
    private String redirect;

    @ApiModelProperty(value = "引用组件")
    @TableField("component")
    private String component;

    @ApiModelProperty(value = "跳转方式")
    @TableField("target")
    private String target;

    @ApiModelProperty(value = "是否展开(0否 1是)")
    @TableField("is_open")
    private Integer isOpen;

    @ApiModelProperty(value = "路由名称")
    @TableField("route_name")
    private String routeName;

    @ApiModelProperty(value = "序号")
    @TableField("order_num")
    private Integer orderNum;

    @ApiModelProperty(value = "创建时间")
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @ApiModelProperty(value = "创建人")
    @TableField(value = "create_user", fill = FieldFill.INSERT)
    private String createUser;

    @ApiModelProperty(value = "修改时间")
    @TableField(value = "update_time", fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;

    @ApiModelProperty(value = "修改人")
    @TableField(value = "update_user", fill = FieldFill.UPDATE)
    private String updateUser;

    @ApiModelProperty(value = "状态(1有效 0无效 99已删除)")
    @TableField("status")
    private Integer status;
    /**
     * 子菜单
     */
    @TableField(value = "children", exist = false)
    private List<SysMenuEntity> children = new ArrayList<>();

    @Override
    protected Serializable pkVal() {
        return this.id;
    }

}

我的一级菜单pid是0 这里根据你们自己的数据来

/**
     * 根据用户ID查询菜单列表
     *
     * @param id
     * @return
     */
    @Override
    public List<SysMenuEntity> selectMenusByUserId(String id) {
        if (StringUtils.isEmpty(id)) {
            throw new BusinessException(ResultEnum.PARAM_IS_EMPTY);
        }
        //List<SysMenuEntity> menuLists = this.menuMapper.selectMenusByUserId(id);
        //TODO 暂时修改成查询所有的
        List<SysMenuEntity> menuLists = this.menuMapper.selectList(null);

        //组装成带层级的数据
        List<SysMenuEntity> nodes = new ArrayList<>();
        menuLists.forEach(e -> {
            //先挑出一级菜单
            if (StatusEnum.INVALID_STRING.getStringCode().equals(e.getParentId())) {
                nodes.add(e);
            }
            //筛选二级菜单
            menuLists.forEach(p -> {
                try {
                    //过滤pid
                    if (e.getId().equals(p.getParentId()) && !StatusEnum.INVALID_STRING.getStringCode().equals(p.getParentId())) {
                        e.getChildren().add(p);
                    }
                } catch (Exception e1) {
                    throw new BusinessException(e1);
                }
            });
        });

        //排序父节点
        nodes.sort(Comparator.comparingInt(SysMenuEntity::getOrderNum));

        //排序子节点
        nodes.forEach(e -> {
            e.getChildren().sort(Comparator.comparingInt(SysMenuEntity::getOrderNum));
        });
        return nodes;
    }

后端只负责根据用户的不同角色组装个数据

前端代码实现

前端目录结构

在这里插入图片描述

紧接着登录成功后

//设置token    
setToken(res.data.userToken)
//设置菜单              
setRouter(res.data.menus)            
this.$router.push({path: '/'})

export function setRouter(route) {
    return window.sessionStorage.setItem(routeKey, JSON.stringify(route))
}

这里是把数据存到了session中

/**
 User: gubingxu
 Date: 2020/5/28 16:28
 Description:  路由权限处理
 */
import router from './router'
import store from './store'
import {Message} from 'element-ui'
import user from './store/modules/user'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import {getToken, getRouter, removeToken} from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
import {resolveRouter} from './router/resolveRouter'
import ca from 'element-ui/src/locale/lang/ca'

//NProgress.configure({showSpinner: false}) // NProgress Configuration

//路由白名单
const whiteList = ['/login'] // no redirect whitelist

//路由守卫(路由过滤)
router.beforeEach((to, from, next) => {
  // 开启进度条
  NProgress.start()

  // 设置页面标题
  document.title = getPageTitle(to.meta.title)

  // 用户token
  const hasToken = getToken()

  // 是否已登录
  if (hasToken) {
    if (to.path === '/login') {
      Message({message: '你已经登录', type: 'info'})
      // 如果已经登录, 则定向到首页
      next({path: '/'})
    } else {
      //用户token存在直接放行
      if (user.state.init) {
        next()
      } else {
        //跳转到获取动态路由的方法
        gotoRouter(to, next)
      }
    }
    if (user.state.init) {
      next()
    } else {
      //跳转到获取动态路由的方法
      gotoRouter(to, next)
    }
    //没有登录
  } else {
    //路由白名单直接进入
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      if (to.path !== '/login') {
        // 重定向到登录页面 不能这么写 因为假如之前的角色是 管理员页面 后又登陆了非管理员 重定向的页面就可能不存在,就会导致404
        // next(`/login?redirect=${to.path}`)
        next('/login')
      } else {
        next()
      }
    }
  }
  NProgress.done()
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})


function gotoRouter(to, next) {

  //获取后端传递的路由
  let routeData = JSON.parse(getRouter())

  //解析路由
  let finalRoute = resolveRouter(routeData)

  //将路由添加到vuex中 渲染菜单
  store.commit('user/set_router_list', finalRoute)

  //设置用户 初始化为完成
  store.commit('user/set_init', true)

  try {
    // 后置添加404页面,防止刷新404
    finalRoute.push({
      path: '*',
      redirect: '/404',
      hidden: true
    })

    //将路由添加到vue中
    router.addRoutes(finalRoute) // vue-router提供的addRouter方法进行路由拼接

    //跳转
    next({...to, replace: true}) // hack方法 确保addRoutes已完成
  } catch (e) {
    console.error(e)
    removeToken()
  }

}

这里全局统一处理路由 permission.js

import Vue from 'vue'
import Router from 'vue-router'

import {constantRoutes} from './static_router'

Vue.use(Router)

const createRouter = () =>
    new Router({
    // mode: 'history', // require service support
    // 解决vue框架页面跳转有白色不可追踪色块的bug
    scrollBehavior: () => ({y: 0}),
    routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
    const newRouter = createRouter()
    router.matcher = newRouter.matcher // reset router
}

export default router
 

这里是 router/index.js


/* layout */
import layout from '@/layout'

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true,
    meta: {title: '登录', icon: 'login'}
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true,
    meta: {title: '404', icon: '404'}
  },

  {
    path: '/',
    component: layout,
    redirect: '/main',
    name: 'Main',
    meta: {title: '主页', icon: 'fa fa-home'},
    // 菜单序号
    menuOrder: 1,
    // 是否单独菜单 --> 即只有一级
    isSingleMenu: true,
    children: [
      {
        path: 'main',
        component: () => import('@/views/main/index')
      },
    ]
  },

  //当使用了动态路由时  就不需要在手动添加了  不然会出现刷新页面404的错误
  /*{path: '*', redirect: '/404', hidden: true, meta: {title: '404', icon: 'main'}}*/
]

静态路由表, 一个固定的路由,例如首页 404等

/**
 User: gubingxu
 Date: 2020/5/28 16:28
 Description:  解析后端返回的路由表  返回vue接受的路由格式
 */

import {Message} from 'element-ui'

/* layout */
import layout from '@/layout'

//解析组件的方法 (线上环境为路由懒加载模式)
const _import = require('../router/_import_' + process.env.VUE_APP_BASE_EV)  //获取组件的方法

export function resolveRouter(routeData) {
  //最终返回的路由格式
  const route = []
  try {
    //遍历路由数据
    routeData.forEach(e => {
      let newRoute = {
        //路径
        path: e.path,
        //名称
        name: e.routeName,
        //解析component(由于后端传递的component 为字符串,则需要解析为vue接受的component组件)
        component: _resolveComponent(e.component)
      }

      //如果存在子级
      if (e.children) {
        //递归解析
        const children = resolveRouter(e.children)
        //保存子级
        newRoute = {...newRoute, children: children}
      }

      //redirect
      if (e.redirect) {
        //递归解析
        //保存子级
        newRoute = {...newRoute, redirect: e.redirect}
      }

      //图标和标题
      if (e.icon && e.menuName) {
        newRoute = {...newRoute, meta: {title: e.menuName, icon: e.icon}}
      }

      //图标颜色
      if (e.iconColor) {
        newRoute = {...newRoute, iconColor: e.iconColor}
      }

      //图标为空 名字不为空(子级菜单)
      if (e.menuName && !e.icon) {
        newRoute = {...newRoute, meta: {title: e.menuName, icon: ''}}
      }

      //将组装的数据放进route中
      route.push(newRoute)
    })
  } catch (e) {
    Message.error({
      message: `解析路由数据失败!${e.message}`,
      showClose: true,
      type: 'warning'
    })
    console.error(e)
    return []
  }
  //返回组装好的路由
  return route
}


//将字符串 转换为组件
function _resolveComponent(componentData) {
  //component处理
  if (componentData) {
    if (componentData === 'layout') { //layout组件特殊处理
      componentData = layout
    } else {
      //截取掉第一个 / (数据库中农保存的为带 / 的数据)
      componentData = _import(componentData.slice(1))
    }
  }
  return componentData
}

路由解析页 resolveRouter.js

module.exports = file => require('@/views/' + file + '.vue').default 
//线上环境懒加载
module.exports = file => () => import('@/views/' + file + '.vue')

解析路由的两个方法

菜单页渲染

<template>
  <div class="side-bar-container">
    <side-bar-logo/>
    <hr/>
    <!--
      :collapse 是否展开菜单
      :unique-opened 是否只保持一个菜单为打开状态
      :collapse-transition 是否显示展开动画
    -->
    <el-menu
        :default-active="activeMenu"
        :background-color="variables.sideBarBackgroundColor"
        text-color="#fff"
        :active-text-color="variables.sideBarPickTextColor"
        :collapse="this.sidebar.isExpand"
        :collapse-transition="true"
        mode="vertical"
        router
    >
      <el-menu-item index="/main" key="/main" @click="clickMenu('main','/main')">
        <template slot="title">
          <i class="fa fa-home" :style="{'color':themeColor}"></i>
          <span slot="title">主页</span>
        </template>
      </el-menu-item>
      <template v-for="(item, index) in menuList">
        <!-- 单独菜单 -->
        <!--<el-menu-item :index="item.path" :key="index" v-if="item.isSingleMenu">
          <template slot="title">
            <i :class="item.meta.icon"></i>
            <span slot="title">{{item.meta.title}} + {{index}} ++ {{item.path}}</span>
          </template>
        </el-menu-item>-->

        <!-- 带子菜单 -->
        <el-submenu :index="item.path" :key="index" v-if="!item.isSingleMenu && item.children.length > 0">
          <template slot="title">
            <i :class="'fa ' + 'fa-' + item.meta.icon" :style="{'color':item.iconColor || themeColor}"></i>
            <!--<side-bar-icon :icon="item.meta.icon"/>-->
            <span slot="title">{{item.meta.title}}</span>
          </template>
          <el-menu-item v-for="(children_item, children_index) in item.children" :key="children_index"
                        :index="children_item.path"
                        @click="clickMenu(children_item.meta.title,children_item.path)">
            <!--子菜单图标-->
            <!--<i :class="item.meta.icon"></i>-->
            <span>{{children_item.meta.title}}</span>
          </el-menu-item>
        </el-submenu>
      </template>

    </el-menu>
  </div>
</template>
```js
<script>
  import variables from '@/styles/variables.scss'
  import {mapGetters} from 'vuex'
  import settings from '@/settings'
  import SideBarLogo from '@/layout/components/side-bar/side-bar-logo'
  import NavBar from '@/layout/components/nav-bar/nav-bar'

  export default {
    name: 'side-bar',
    components: {NavBar, SideBarLogo},
    data() {
      return {}
    },

    computed: {
      // 从store的 getters里 获取sidebar
      ...mapGetters(['sidebar']),
      variables() {
        return variables
      },

      //菜单颜色
      themeColor() {
        return settings.themeColor
      },

      /**
       * 计算当前激活菜单是哪个
       */
      activeMenu() {
        const route = this.$route
        const {meta, path} = route
        if (meta.activeMenu) {
          return meta.activeMenu
        }
        return path
      },


      /**
       * 获取菜单数据
       */
      menuList() {

        let ml = this.$store.state.user.routerList

        // TODO 获取静态路由表中需要添加的路由, 将路由渲染到菜单中。
        /*let tempRouter = []
        constantRoutes.forEach(e => {
          if (e && e.menuOrder) {
            tempRouter.push(e)
          }
        })
        //先排序 --> 在插入到路由表中
        tempRouter.sort(this.compare).forEach(e => ml.unshift(e))
        console.log(ml)*/
        //过滤掉404 路由
        return ml.filter(e => {
          if (e.redirect !== '/404') {
            return e
          }
        })
      },
    },

    created() {

    },

    mounted() {
    },

    methods: {
    
      /**
       * 菜单的点击事件
       * @param label 标题
       * @param path 路由
       */
      clickMenu(label, path) {
        let val = {
          label: label,
          path: path
        }
        // 存储到vuex中 方便面包屑组件与tab组件使用
        this.$store.commit('router/select_menu', val)
      }
    }
  }
</script>

大功告成

至此, 已实现开头页面效果。
以梦为马,不负韶华。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值