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>
大功告成
至此, 已实现开头页面效果。
以梦为马,不负韶华。