Vue动态路由, 动态左侧菜单, 文中含C#后台获取代码

4 篇文章 0 订阅

业务需求

不知道各位的需求是什么样的, 因为我本人项目用的是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对象.

部分内容参照这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值