Vue动态路由, 动态左侧菜单, 文中含C#后台获取代码
业务需求
不知道各位的需求是什么样的, 因为我本人项目用的是Vue + element ui开发的后台管理系统, 因业务需要, 需要将前端配置的菜单改为动态菜单, 我觉得很多项目可能都会有这个需求, 算是比较重要的一个模块, 废话不多说, 直接上代码。
初顾茅庐(router/index.js配置)
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
var constantRouterMap = [];
/**
* 注:子菜单仅在路由children.length >= 1时才会出现。
*
* hidden: true 如果设置为true,项目将不显示在侧边栏(默认为false)
* alwaysShow: true 如果设置为true,将始终显示根菜单。
* 如果不设置总是显示,当项目有一个以上的子项目时,如果不设置总是显示,则该项目有一个以上的子项目路径。
* 将成为嵌套模式,否则不显示根菜单。
* redirect: noRedirect 如果设置为 noRedirect,则面包屑中不会有重定向。
* name:'路由器名称' 该名称是由<keep-alive>使用的(必须设置!!!!)
* meta : {
title: 'title'名称显示在侧边栏和面包屑中(推荐设置)
icon:'svg-name'图标显示在侧边栏的图标
breadcrumb: false 如果设置为false,则该项目将隐藏在面包屑的页面中(默认为true)
activeMenu: '/example/list',如果设置路径,侧边栏会突出显示你设置的路径
}
*/
export default new Router({
routes: constantRouterMap
});
登堂入室(/router/_import*配置)
分别为两个环境的动态生成组件情况
在router中创建两个文件, 如图所示:
_import_development.js
module.exports = file => require(`@/views/${file}.vue`).default; // vue-loader at least v13.0.0+
_import_production.js
module.exports = file => () => import('@/views/' + file + '.vue');
拨开云雾(/permission.js配置)
其中包含网页进度条及element-ui部分样式引入, 如不需要的同学可以去掉, 也包含了登录页是否登录判断, 新项目可以直接用, 如不需要则在代码块中去除登录判断即可.
import router from './router';
import store from './store';
import { Message } from 'element-ui';
import NProgress from 'nprogress'; // 进度条
import 'nprogress/nprogress.css'; // 进度条样式
import { getToken } from '@/utils/auth'; // 获取验证token
import getPageTitle from '@/utils/get-page-title'; //页面标题
const _import = require('./router/_import_' + process.env.NODE_ENV); //获取组件的方法
import Layout from '@/layout'; //Layout 是架构组件,不在后台返回,在文件里单独引入
var getRouter; //用来获取后台拿到的路由
NProgress.configure({ showSpinner: false }); // 进图条配置
// 模拟从后台获取的路由
const objs = [
{
'path': '/redirect',
'name': null,
'component': 'Layout',
'hidden': true,
'alwaysShow': false,
'redirect': null,
'meta': null,
'children': [
{
'path': '/redirect/:path(.*)',
'name': null,
'component': 'redirect/index',
'hidden': true,
'alwaysShow': false,
'redirect': null,
'meta': null,
'children': []
}
]
},
{
'path': '/login',
'name': 'Login',
'component': 'login/index',
'hidden': false,
'alwaysShow': false,
'redirect': null,
'meta': null,
'children': []
},
{
'path': '/',
'name': null,
'component': 'Layout',
'hidden': false,
'alwaysShow': false,
'redirect': 'home',
'meta': null,
'children': [
{
'path': 'home',
'name': 'Home',
'component': 'home/index',
'meta': {
'title': '主页',
'icon': 'home',
'breadcrumb': true,
'affix': true
},
'children': []
}
]
},
{
'path': '/cd1',
'name': 'cd1',
'component': 'Layout',
'hidden': false,
'alwaysShow': false,
'redirect': null,
'meta': {
'title': '菜单1',
'icon': 'blank',
'breadcrumb': true,
'affix': false
},
'children': [
{
'path': '/cd1/cd1-1',
'name': 'cd1-1',
'component': 'cd1/cd1-1/index',
'hidden': false,
'alwaysShow': false,
'redirect': null,
'meta': {
'title': '菜单1-1',
'icon': 'blank',
'breadcrumb': true,
'affix': false
},
'children': [
{
'path': '/cd1/cd1-1/cd1-1-1',
'name': 'cd1-1-1',
'component': 'cd1/cd1-1/cd1-1-1/index',
'hidden': false,
'alwaysShow': false,
'redirect': null,
'meta': {
'title': '菜单1-1-1',
'icon': 'blank',
'breadcrumb': true,
'affix': false
},
'children': []
}
]
},
{
'path': '/cd1-2',
'name': 'cd1-2',
'component': 'cd1/cd1-1/cd1-1-1/index',
'hidden': false,
'alwaysShow': false,
'redirect': null,
'meta': {
'title': '菜单1-2',
'icon': 'blank',
'breadcrumb': true,
'affix': false
},
'children': []
}
]
}
];
// 异步执行 async
router.beforeEach(async(to, from, next) => {
// 进度条开启
NProgress.start();
// 设置页面标题
document.title = getPageTitle(to.meta.title);
// 解决页面主动使用localStorage.removeItem('router');会导致该变量没有初始化从而陷入死循环
if (!getObjArr('router')) {
getRouter = undefined;
}
if (!getRouter) { //不加这个判断,路由会陷入死循环
if (!getObjArr('router')) {
store.dispatch('user/getVueRoutes').then(res => {
getRouter = res; //假装模拟后台请求得到的路由数据
saveObjArr('router', getRouter); //存储路由到localStorage
routerGo(to, next); //执行路由跳转方法
});
} else { //从localStorage拿到了路由
getRouter = getObjArr('router'); //拿到路由
routerGo(to, next);
}
}
// 判断用户是否登录
const hasToken = getToken();
if (hasToken) {
if (to.path === '/') {
// 如果已登录,则重定向到主页
next({ path: '/' });
NProgress.done(); // 完成进度条
} else {
const hasGetUserInfo = store.getters.name;
if (hasGetUserInfo) {
next();
} else {
try {
// 获取用户信息, await为该异步方法执行完毕后执行这个方法
await store.dispatch('user/getInfo');
next();
} catch (error) {
// 移除token,进入登录页面重新登录
await store.dispatch('user/resetToken');
Message.error(error || 'Has Error');
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
}
} else {
/* 没有token */
if (whiteList.indexOf(to.path) !== -1) {
// 进入的页面在免费登录白名单中,直接进入
next();
} else {
// 其他没有权限访问的页面被重定向到登录页面, redirect为登录成功定向的页面
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
});
function routerGo(to, next) {
getRouter = filterAsyncRouter(getRouter); //过滤路由
console.log(getRouter);
router.addRoutes(getRouter); //动态添加路由
global.antRouter = getRouter; //将路由数据传递给全局变量,做侧边栏菜单渲染工作
console.log(to);
next({ ...to, replace: true });
}
function saveObjArr(name, data) { //localStorage 存储数组对象的方法
localStorage.setItem(name, JSON.stringify(data));
}
function getObjArr(name) { //localStorage 获取数组对象的方法
return JSON.parse(window.localStorage.getItem(name));
}
function filterAsyncRouter(asyncRouterMap) { //遍历后台传来的路由字符串,转换为组件对象
const accessedRouters = asyncRouterMap.filter(route => {
if (route.component) {
if (route.component === 'Layout') { //Layout组件特殊处理
route.component = Layout;
} else {
route.component = _import(route.component);
}
}
// 优先删除路由子项为空的属性, 否则会报错
if (route.children == null) {
delete route.children;
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children);
}
return true;
});
return accessedRouters;
}
router.afterEach(() => {
// 完成进度条
NProgress.done();
});
游刃有余(C#后台获取路由)
该调用方式为API调用方式, 如果需要其他调用方式, 改变其返回结果就可以了, IHttpActionResult可以改为AcionResult、JsonResult等
注: 以下GetObjects方法为我使用框架中自带的数据库访问方法, 各位将该方法替换为对应系统中的方法即可, 第一个GetObjects是获取最顶级的路由, 也就是ParentId = 0的, AddChildren中的GetObjects是获取递归中路由的子级路由.
/// <summary>
/// 获取所有路由(拥有树状结构)
/// </summary>
/// <returns>返回Vue层级菜单, 可进行重写</returns>
[HttpGet]
public virtual IHttpActionResult GetVueRoutes()
{
// 先获取最顶级菜单
IList<VueRoute> routes = VueRoute.GetObjects<VueRoute>(new QueryCondition("Where ParentId = 0", ""));
List<object> result = new List<object>();// 最终过滤返回的结果
// 遍历顶级菜单
foreach (var route in routes)
{
// 获取每个顶级菜单下的子菜单
route.children = new List<VueRoute>();
if (route.IsMeta)
{
route.meta = new meta();
route.meta.title = route.Title;
route.meta.icon = route.Icon;
route.meta.affix = route.IsAffix;
route.meta.breadcrumb = route.IsBreadcrumb;
}
AddChildren(route);
FilterVueRoute(route);
}
return Json(new ApiResultModel(200, "获取层级菜单成功!", routes));
}
public void AddChildren(VueRoute route)
{
route.children = new List<VueRoute>();
// 查询出当前这个路由的所有子节点
IList<VueRoute> routes = VueRoute.GetObjects<VueRoute>(new QueryCondition($"Where ParentId = { route.RouteId }", ""));
foreach (VueRoute item in routes)
{
if (item.IsMeta)
{
item.meta = new meta();
item.meta.title = item.Title;
item.meta.icon = item.Icon;
item.meta.affix = item.IsAffix;
item.meta.breadcrumb = item.IsBreadcrumb;
}
route.children.Add(item);
AddChildren(item); //递归调用自己,直到父节点添加所有子节点结束
}
}
/// <summary>
/// 过滤vue路由, 将没有children的路由置位null, 方便前端进行删除
/// </summary>
/// <param name="route">vue路由</param>
public void FilterVueRoute(VueRoute route)
{
if(route.children.Count <= 0)
{
route.children = null;
return;
}
foreach (var item in route.children)
{
FilterVueRoute(item);
}
}
对应的模型:
需引入:
using Newtonsoft.Json;
using System.Collections.Generic;
属性说明:
JsonIgnore: 不序列化这一属性
/// <summary>
/// Vue路由(菜单), 注意这里返回只是Json对象, 需要在前端对component进行转换为组件, 此模型中开头大写为C#属性, 小写为vue路由属性
/// </summary>
public class VueRoute
{
/// <summary>
/// 缺省构造函数
/// </summary>
public VueRoute()
{
}
/// <summary>
/// 路由Id, 主键
/// </summary>
public int RouteId { get; set; }
/// <summary>
/// 父级路由Id, 用于匹配多级菜单
/// </summary>
[JsonIgnore]
public int ParentId { get; set; }
/// <summary>
/// 标签页是否固定
/// </summary>
[JsonIgnore]
public bool IsAffix { get; set; }
/// <summary>
/// Svg菜单图标名称
/// </summary>
[JsonIgnore]
public string Icon { get; set; }
/// <summary>
/// 是否需要meta项
/// </summary>
[JsonIgnore]
public bool IsMeta { get; set; }
/// <summary>
/// 如果设置为false,则该项目将隐藏在面包屑的页面中
/// </summary>
[JsonIgnore]
public bool IsBreadcrumb { get; set; }
/// <summary>
/// 路由器名称, 必须设置, keep-alive使用, 非侧边栏及面包屑名称
/// </summary>
[JsonIgnore]
public string Title { get; set; }
/// <summary>
/// 路径
/// </summary>
public string path { get; set; }
/// <summary>
/// 路由器名称, 必须设置, keep-alive使用, 非侧边栏及面包屑名称
/// </summary>
public string name { get; set; }
/// <summary>
/// 组件路径, 如果是/views/home/index.vue, 则设置该值为: home/index即可
/// </summary>
public string component { get; set; }
/// <summary>
/// 如果设置为true,项目将不显示在侧边栏(默认为false)
/// </summary>
public bool hidden { get; set; }
/// <summary>
/// 如果设置为true,将始终显示根菜单, 简单来说有下级菜单则为false, 没有下级菜单则为true, 主页/登录页等通用页建议为false(根据实际情况来)
/// </summary>
public bool alwaysShow { get; set; }
/// <summary>
/// 重定向, 如果设置为 noRedirect,则面包屑中不会有重定向。
/// </summary>
public string redirect { get; set; }
#region meta数据
public meta meta { get; set; }
#endregion
/// <summary>
/// 子菜单
/// </summary>
public List<VueRoute> children { get; set; }
}
public class meta
{
/// <summary>
/// 显示在侧边栏和面包屑中(推荐设置)
/// </summary>
public string title { get; set; }
/// <summary>
/// svg图标名称, 例如: user
/// </summary>
public string icon { get; set; }
/// <summary>
/// 如果设置为false,则该项目将隐藏在面包屑的页面中
/// </summary>
public bool breadcrumb { get; set; }
/// <summary>
/// 是否为固定的标签页
/// </summary>
public bool affix { get; set; }
}
最终效果(图)
总结
以上为全部内容, 如不需要C#版本则只需要看至"拨开云雾"即可, 文中有不理解的话或者代码欢迎留言讨论.
注释都写的比较详尽, 包括Model对象.