项目背景:登录的时候,后端在返回token的同时还一并返回用户的登录权限,且我司返回的是一串数组,里面的内容对应每个要显示的路由,没有admin之类的权限。

实现流程(具体看代码,超级详细):

  1. 改变路由结构,分为constantRoutes(静态路由) 和 asyncRoutes(动态路由)
  2. 给每个路由赋予角色
  3. 开始实现动态加载啦

实现过程遇到的bug:

  • 登录后刷新,页面变成空白。(已解决,将拿到的角色存储在本地,在使用的时候也从本地拿,即可解决)
  • 切换用户,左侧路由权限不变,要刷新一下才可以(已解决,两个方法。①退出时刷新(比较简便,在store/modules/user.js中);②给予标注,是否第一次登陆,代码中的init就是相关方法。)
  • 登录,退出后,显示的路由指向路径不变。(已解决,在Navbar.vue中修改指向路径)
  • 登录刷新后显示404页面(已解决,两个方案 ①在src/permission.js中;②方案二(比较简单),放在动态路由的最后)
  • 将加载的路由放在中间(此方法还有bug,不推荐使用先,如果你司强烈要求此功能就做个思考参考吧, 在 src/store/modules/permission.js中)


代码

  • src/router/index.js
  • src/store/modules/user.js
  • src/store/getters.js
  • src/permission.js
  • src/store/modules/permission.js
  • src/layout/components/Sidebar/index.js
  • src/layout/components/Navbar.vue
  • 登录刷新后显示404页面,方案二


src/router/index.js

修改路由结构:
把需要动态加载的菜单放在asyncRoutes里面,并给每个路由加个角色(父路由也需要的哦),放在meta里哦,【分为constantRoutes(静态路由) 和 asyncRoutes(动态路由)】

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

/* Layout */
import Layout from "@/layout";

export const constantRoutes = [
  {
    path: "/login",
    component: () => import("@/views/login/index"),
    hidden: true
  },
  {
    path: "/",
    component: Layout,
    redirect: "/dashboard",
    children: [
      {
        path: "dashboard",
        name: "主页",
        component: () => import("@/views/dashboard/index"),
        meta: { title: "主页", icon: "dashboard" }
      }
    ]
  },
  {
    path: "/external-link",
    component: Layout,
    children: [
      {
        path: "/",
        meta: { title: "关于我们", icon: "link" }
      }
    ]
  },
  {
    path: "/404",
    component: () => import("@/views/404"),
    hidden: true
  },
  { path: "*", redirect: "/404", hidden: true },
];

export const asyncRoutes = [
  {
    path: "/example",
    component: Layout,
    redirect: "/example/table",
    name: "Example",
    meta: { title: "个人管理", icon: "el-icon-user-solid", roles: ['Personal'] },
    children: [
      {
        path: "table",
        name: "Table3",
        component: () => import("@/views/table/index"),
        meta: { title: "单", roles: ['Personal'] }
      },
      {
        path: "tree",
        name: "Tree3",
        component: () => import("@/views/table/rate"),
        // meta: { title: "进度", icon: "tree", roles: ['Personal'] }
      }
    ]
  },
  {
    path: "/proxy",
    component: Layout,
    redirect: "proxy/manage",
    meta: { title: "服务商管理", icon: "el-icon-office-building", roles: ['maintainCompanyPolicy', 'maintainCompanyEmployee'] },
    children: [
      {
        path: "manage",
        name: "Proxy",
        component: () => import("@/views/manage/index"),
        meta: { title: "单", roles: ['maintainCompanyPolicy'] }
      },
      {
        path: "employee",
        name: "Employee",
        component: () => import("@/views/manage/employee"),
        // meta: { title: "管理", roles: ['maintainCompanyEmployee'] }
      }
    ]
  },
  {
    path: "/company",
    component: Layout,
    redirect: "company/maintain/table",
    name: "Admin",
    meta: { title: "后台管理", icon: "el-icon-s-help", roles: ['maintainAdminCompany'] },
    children: [
      {
        path: "maintain",
        component: () => import("@/views/company/index"), // Parent router-view
        name: "Maintain",
        redirect: "maintain/table",
        meta: { title: "公司", roles: ['maintainAdminCompany'] },
        children: [
          {
            path: "table",
            name: "Table2",
            component: () => import("@/views/company/table"),
            meta: { title: "公司", roles: ['maintainAdminCompany'] }
          }
        ]
      },
      {
        path: "manage",
        component: () => import("@/views/company/maintain/index"),
        name: "Manage",
        meta: {
          title: "维保单", roles: ['maintainAdminCompany']
        }
      },
      {
        path: "recycle",
        component: () => import("@/views/company/recycle/index"),
        name: "Unusual",
        meta: { title: "回收站", roles: ['maintainAdminCompany'] }
      }
    ]
  },
]

const createRouter = () =>
  new Router({
    scrollBehavior: () => ({ y: 0 }),
    routes: constantRoutes
  });

const router = createRouter();
export function resetRouter () {
  const newRouter = createRouter();
  router.matcher = newRouter.matcher; // reset router
}

export default router;

src/store/modules/user.js

把后端传来的角色存储起来:

import { login, logout, getInfo } from "@/api/user";
import { getToken, setToken, removeToken, setUser, removeUser } from "@/utils/auth";
import { resetRouter } from "@/router";

const getDefaultState = () => {
  return {
    token: getToken(), //token
    role: [],  //角色列表
    init: false  //这个是解决后面‘切换用户,左侧路由菜单不变,要刷新一下才可以’的问题,如果在退出时刷新了,就不用写这个
  };
};

const state = getDefaultState();

const mutations = {
  RESET_STATE: state => {
    Object.assign(state, getDefaultState());
  },
  SET_TOKEN: (state, token) => {
    state.token = token;
  }
  SET_ROLE: (state, role) => {
    state.role = role;
  }
  // 判断是否初次登陆,在src/permission.js用到init
  SET_INIT: (state, data) => {
    state.init = data
  }
};

const actions = {
//初次登陆赋予init为true
  changeInit ({ commit }) {
    commit("SET_INIT", true);
  },
  // 账号密码登录
  login ({ commit }, userInfo) {
    const menulist = []
    const { username, password } = userInfo;
    const formdata = new FormData();
    formdata.append('username', username.trim())
    formdata.append('password', password)
    return new Promise((resolve, reject) => {
      login(formdata)
        .then(response => {
          const data = response.data;
          // 权限
          const role = data.userAuth.userAuth.map(roles => roles.moduleCode) //将角色列表格式化
          localStorage.setItem('role', JSON.stringify(role)) // 将角色存储在本地
          commit("SET_ROLE", role);
          setToken(data.token.value);
          resolve();
        })
        .catch(error => {
          reject(error);
        });
    });
  }

  // 退出登录
  logout ({ commit, state, dispatch }) {
    return new Promise((resolve, reject) => {
      logout(state.token)
        .then(() => {
          // location.reload() //退出 刷新页面,如果写了这个就不用写init了,选用init 只是因为个人感觉比较优雅
          localStorage.removeItem('role')
          commit('SET_INIT', false)
          commit('SET_TOKEN', '');
          commit("SET_ROLE", '');
          removeToken(); // must remove  token  first
          removeUser();
          commit("RESET_STATE");
          commit('SET_MENULIST', '');
          dispatch('tagsView/delAllViews', null, { root: true });
          resetRouter();
          resolve();
        })
        .catch(error => {
          reject(error);
        });
    });
  },

  // remove token
  resetToken ({ commit }) {
    return new Promise(resolve => {
      removeToken(); // must remove  token  first
      removeUser();
      commit("RESET_STATE");
      resolve();
    });
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

src/store/getters.js

这个别漏啦

const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  account: state => state.user.account,
  name: state => state.user.name,
  role: state => state.user.role, //加上这个
  permission_routes: state => state.permission.routes, //别漏了这个哦
  init: state => state.user.init //加上这个
}
export default getters

src/permission.js

动态添加路由:

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // 是否有转圈效果

const whiteList = ['/login'] // 没有重定向的白名单

router.beforeEach(async (to, from, next) => {
  // 开始进度条
  NProgress.start()

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

  // 确定页面是否已登录
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // 如果已登录,则重定向到主页
      next({ path: '/' })
      NProgress.done()
    } else {
      // 获取到的静态路由 + 动态动态,如果选择的方案是不使用init,则不要注释此条,把下面的a注释掉
      // const hasGetPermissionRoutes = store.getters.permission_routes && store.getters.permission_routes.length > 0
      // 判断是否第一次登陆
      const a = store.getters.init
      if (a) {
        next()
      } else {
        try {
          // const roles = store.state.user.role,原文档写法,但是这样的话 会导致刷新后,数据丢失
          const roles = JSON.parse(localStorage.getItem('role'))
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          await store.dispatch('user/changeInit')
          router.addRoutes(accessRoutes)
		
		// 在这里动态添加最后的通配路由,确保先有动态路由,再有通配路由,解决动态路由刷新会跳转到404问题
          let lastRou = [{ path: '*', redirect: '/404' }]
          router.addRoutes(lastRou)

          next({ ...to, replace: true })
        } catch (error) {
          // 删除令牌并进入登录页面重新登录
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* has no token*/

    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

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

src/store/modules/permission.js

筛选应该被添加的路由:

import { asyncRoutes, constantRoutes } from '@/router'

/**
 * Use meta.role to determine if the current user has permission
 * @param roles
 * @param route
 */
function hasPermission (roles, route) {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    return true
  }
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes asyncRoutes
 * @param roles
 */
export function filterAsyncRoutes (routes, roles) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })

  return res
}

const state = {
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    // 权限路由放在最后
    // state.routes = constantRoutes.concat(routes)
    
    // 把权限路由放在中间,在constontRoutes
    let rou = constantRoutes
    rou.splice(2, 0, ...routes) //把第二个放在最后
    state.routes = rou
  }
}

const actions = {
  generateRoutes ({ commit }, roles) {
    return new Promise(resolve => {
      const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

src/layout/components/Sidebar/index.js

渲染路由导航:

<template>
  <div :class="{'has-logo':showLogo}">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
      //这里一定要注意修改为 permission_routes
        <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'

export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters([
      'permission_routes', //这里引入permission_routes
      'sidebar'
    ]),
    activeMenu() {
      const route = this.$route
      const { meta, path } = route
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    },
    variables() {
      return variables
    },
    isCollapse() {
      return !this.sidebar.opened
    }
  }
}
</script>

src/layout/components/Navbar.vue

想看‘解决登录,退出后,显示的路由指向路径不变’的问题,可直接拉到后面的代码,修改一行代码就行啦

<template>
  <div class="navbar">
    <hamburger
      :is-active="sidebar.opened"
      class="hamburger-container"
      @toggleClick="toggleSideBar"
    />
    <breadcrumb class="breadcrumb-container" />

    <div class="right-menu">
      <el-dropdown
        class="avatar-container"
        trigger="click"
      >
        <div class="avatar-wrapper">
          <img
            src="./favicon.png"
            class="user-avatar"
          >
          <i class="el-icon-caret-bottom" />
        </div>
        <el-dropdown-menu
          slot="dropdown"
          class="user-dropdown"
        >
          <router-link to="/">
            <el-dropdown-item v-if="user!=null">
              <!-- 用户名:{{user}} -->
              用户:{{ user.nickname }}
            </el-dropdown-item>
          </router-link>
          <a
            target="_blank"
            href="#"
          >
            <!-- <el-dropdown-item v-if="user!=null">账号:{{ user.userId }}</el-dropdown-item> -->
          </a>
          <a href="#">
            <el-dropdown-item>状态:在线</el-dropdown-item>
          </a>
          <el-dropdown-item
            divided
            @click.native="logout"
          >
            <span style="display:block;">退出登录</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import { getUser } from "@/utils/auth";
export default {
  components: {
    Breadcrumb,
    Hamburger
  },
  computed: {
    ...mapGetters(['sidebar', 'avatar', 'name', 'account'])
  },
  data () {
    return {
      user: {}
    }
  },
  created () {
    this.getData()
  },
  methods: {
    toggleSideBar () {
      this.$store.dispatch('app/toggleSideBar')
    },
    async logout () {
      await this.$store.dispatch('user/logout')
      // this.$router.push(`/login?redirect=${this.$route.fullPath}`)
      //退出的时候 直接到登录界面,解决再次登录别的账号出现进去即404
      this.$router.push(`/login`)
    },
    getData () {
      this.user = JSON.parse(getUser());
    }
  }
}
</script>

登录刷新后显示404页面,方案二

在router/index.js中,动态加载路由模块中的最后添加一下这行代码,相对的前面的一样的代码就删除了,还有方案一的代码也不必要存在了

export const asyncRoutes = [
  { path: "*", redirect: "/404", hidden: true } //把通配404页面放在动态路由的最底部,就不用在promission中设置了
]