在“VUE3 后台管理系统「模板构建」”文章中,详细的介绍了我使用 vue3.0 和 vite2.0 构建的后台管理系统,虽然只是简单的一个后台管理系统,其中涉及的技术基本都覆盖了,基于 vue3 的 ”vue-router 和 vuex,以及借助第三方开源插件来实现 vuex 数据持久化。前边只是介绍了 vue 后台管理系统的页面布局,以及一些常用的插件的使用,如:富文本编辑器、视频播放器、页面滚动条美化(前边忘记介绍了,此次文章中将会进行添加和补充)。
本次文章主要介绍的是 vue-router 的动态匹配和动态校验,来实现不同账号不同权限,通过前端来对用户权限进行相应的限制;在一些没有访问权限的路径下访问时给予相应的提示以及后续相应的跳转复原等逻辑操作。用户鉴权,前端可以进行限制,也可以通过后台接口数据进行限制,之前开发过程中遇到过通过后台接口来动态渲染路由的,接下来介绍的是纯前端来做路由访问的限制。
1import Layout from "../layout/Index.vue";
2import RouteView from "../components/RouteView.vue";
3
4const layoutMap = [
5 {
6 path: "/",
7 name: "Index",
8 meta: { title: "控制台", icon: "home" },
9 component: () => import("../views/Index.vue")
10 },
11 {
12 path: "/data",
13 meta: { title: "数据管理", icon: "database" },
14 component: RouteView,
15 children: [
16 {
17 path: "/data/list",
18 name: "DataList",
19 meta: { title: "数据列表", roles: ["admin"] },
20 component: () => import("../views/data/List.vue")
21 },
22 {
23 path: "/data/table",
24 name: "DataTable",
25 meta: { title: "数据表格" },
26 component: () => import("../views/data/Table.vue")
27 }
28 ]
29 },
30 {
31 path: "/admin",
32 meta: { title: "用户管理", icon: "user" },
33 component: RouteView,
34 children: [
35 {
36 path: "/admin/user",
37 name: "AdminAuth",
38 meta: { title: "用户列表", roles: ["admin"] },
39 component: () => import("../views/admin/AuthList.vue")
40 },
41 {
42 path: "/admin/role",
43 name: "AdminRole",
44 meta: { title: "角色列表" },
45 component: () => import("../views/admin/RoleList.vue")
46 }
47 ]
48 },
49 {
50 path: "user",
51 name: "User",
52 hidden: true /* 不在侧边导航展示 */,
53 meta: { title: "个人中心" },
54 component: () => import("../views/admin/User.vue")
55 },
56 {
57 path: "/error",
58 name: "NotFound",
59 hidden: true,
60 meta: { title: "Not Found" },
61 component: () => import("../components/NotFound.vue")
62 }
63];
64
65const routes = [
66 {
67 path: "/login",
68 name: "Login",
69 meta: { title: "用户登录" },
70 component: () => import("../views/Login.vue")
71 },
72 {
73 path: "/",
74 component: Layout,
75 children: [...layoutMap]
76 },
77 { path: "/*", redirect: { name: "NotFound" } }
78];
79
80export { routes, layoutMap };
1// vue-router4.0版写法
2import { createRouter, createWebHistory } from "vue-router";
3import { decode } from "js-base64";
4import { routes } from "./router";
5import NProgress from "nprogress";
6import "nprogress/nprogress.css";
7
8NProgress.configure({ showSpinner: false });
9
10const router = createRouter({
11 history: createWebHistory(),
12 routes: [...routes],
13 scrollBehavior(to, from, savedPosition) {
14 if (savedPosition) {
15 return savedPosition;
16 } else {
17 return { top: 0 };
18 }
19 }
20});
21
22// 路由拦截与下方vue-router3.x写法相同
1// vue-router3.x版写法
2import Vue from "vue";
3import VueRouter from "vue-router";
4import { decode } from "js-base64";
5import { routes } from "./router";
6import NProgress from "nprogress";
7import "nprogress/nprogress.css";
8
9NProgress.configure({ showSpinner: false });
10
11Vue.use(VueRouter);
12
13const router = new VueRouter({
14 mode: "history",
15 base: process.env.BASE_URL,
16 routes: [...routes],
17 scrollBehavior(to, from, savedPosition) {
18 if (savedPosition) {
19 return savedPosition;
20 } else {
21 return { top: 0 };
22 }
23 }
24});
25
26router.beforeEach((to, from, next) => {
27 NProgress.start();
28 const jwt = sessionStorage.getItem("jwt") || "";
29
30 document.title = jwt ? (to.meta.title ? to.meta.title + " - 管理应用" : "管理系统") : "系统登录";
31 if (to.path === "/login") {
32 !!jwt ? next("/") : next();
33 } else {
34 if (from.path === "/login" && !jwt) {
35 NProgress.done(true);
36 next(false);
37 return;
38 }
39 if (!!jwt) {
40 if (to.meta.hasOwnProperty("roles")) {
41 let roles = to.meta.roles || [],
42 { role } = jwt && JSON.parse(decode(jwt));
43 roles.includes(role) ? next() : next("/error");
44 return;
45 }
46 next();
47 } else {
48 next("/login");
49 }
50 }
51});
52
53router.afterEach(() => {
54 NProgress.done();
55});
56
57export default router;
1/* 处理权限 */
2export const hasPermission = (route, role) => {
3 if (route["meta"] && route.meta.hasOwnProperty("roles")) {
4 return route.meta.roles.includes(role);
5 }
6 return true;
7};
8
9/* 过滤数组 */
10export const filterAsyncRouter = (routers, role) => {
11 let tmp = [];
12 tmp = routers.filter(el => {
13 if (hasPermission(el, role)) {
14 if (el["children"] && el.children.length) {
15 el.children = filterAsyncRouter(el.children, role);
16 }
17 return true;
18 }
19 return false;
20 });
21 return tmp;
22};
此两函数为封装的过滤指定权限的路由数据,返回过滤后的数据(即当前账号有权访问的页面);
1import Vue from "vue";
2import Vuex from "vuex";
3import { layoutMap } from "../router/router";
4import { filterAsyncRouter } from "../utils/tool";
5import createPersistedState from "vuex-persistedstate";
6import SecureLS from "secure-ls";
7import { CLEAR_USER, SET_USER, SET_ROUTES } from "./mutation-types";
8
9Vue.use(Vuex);
10
11const state = {
12 users: null,
13 routers: []
14};
15
16const getters = {};
17
18const mutations = {
19 [CLEAR_USER](state) {
20 state.users = null;
21 state.routers.length = 0;
22 },
23 [SET_USER](state, payload) {
24 state.users = payload;
25 },
26 [SET_ROUTES](state, payload) {
27 state.routers = payload;
28 }
29};
30
31const ls = new SecureLS({
32 encodingType: "aes" /* 加密方式 */,
33 isCompression: false /* 压缩数据 */,
34 encryptionSecret: "vue" /* 加密密钥 */
35});
36
37const actions = {
38 clearUser({ commit }) {
39 commit(CLEAR_USER);
40 },
41 setUser({ commit }, payload) {
42 let deepCopy = JSON.parse(JSON.stringify(layoutMap)),
43 accessedRouters = filterAsyncRouter(deepCopy, payload.role);
44 commit(SET_USER, payload);
45 commit(SET_ROUTES, accessedRouters);
46 }
47};
48
49const myPersistedState = createPersistedState({
50 key: "store",
51 storage: window.sessionStorage,
52 // storage: {
53 // getItem: state => ls.get(state),
54 // setItem: (state, value) => ls.set(state, value),
55 // removeItem: state => ls.remove(state)
56 // } /* 永久存储 */
57 reducer(state) {
58 return { ...state };
59 }
60});
61
62export default new Vuex.Store({
63 state,
64 getters,
65 mutations,
66 actions
67 // plugins: [myPersistedState]
68});
1<template>
2 <a-layout-sider class="sider" v-model="collapsed" collapsible :collapsedWidth="56">
3 <div class="logo">
4 <a-icon type="ant-design" />
5 </div>
6 <a-menu
7 class="menu"
8 theme="dark"
9 mode="inline"
10 :defaultOpenKeys="[defaultOpenKeys]"
11 :selectedKeys="[$route.path]"
12 :inlineIndent="16"
13 >
14 <template v-for="route in routers">
15 <template v-if="!route['hidden']">
16 <a-sub-menu v-if="route.children && route.children.length" :key="route.path">
17 <span slot="title">
18 <a-icon :type="route.meta['icon']" />
19 <span>{{ route.meta.title }}</span>
20 </span>
21 <a-menu-item v-for="sub in route.children" :key="sub.path">
22 <router-link :to="{ path: sub.path }">
23 <a-icon v-if="sub.meta['icon']" :type="sub.meta['icon']" />
24 <span>{{ sub.meta.title }}</span>
25 </router-link>
26 </a-menu-item>
27 </a-sub-menu>
28 <a-menu-item v-else :key="route.path">
29 <router-link :to="{ path: route.path }">
30 <a-icon :type="route.meta['icon']" />
31 <span>{{ route.meta.title }}</span>
32 </router-link>
33 </a-menu-item>
34 </template>
35 </template>
36 </a-menu>
37 </a-layout-sider>
38</template>
39
40<script>
41 import { mapState } from "vuex";
42
43 export default {
44 name: "Sider",
45 data() {
46 return {
47 collapsed: false,
48 defaultOpenKeys: ""
49 };
50 },
51 computed: {
52 ...mapState(["routers"])
53 },
54 created() {
55 this.defaultOpenKeys = "/" + this.$route.path.split("/")[1];
56 }
57 };
58</script>
59
60<style lang="less" scoped>
61 .sider {
62 height: 100vh;
63 overflow: hidden;
64 overflow-y: scroll;
65 &::-webkit-scrollbar {
66 display: none;
67 }
68
69 .logo {
70 height: 56px;
71 line-height: 56px;
72 font-size: 30px;
73 color: #fff;
74 text-align: center;
75 background-color: #002140;
76 }
77
78 .menu {
79 width: auto;
80 }
81 }
82</style>
83
84<style>
85 ul.ant-menu-inline-collapsed > li.ant-menu-item,
86 ul.ant-menu-inline-collapsed > li.ant-menu-submenu > div.ant-menu-submenu-title {
87 padding: 0 16px !important;
88 text-align: center;
89 }
90</style>
该菜单渲染是基于 Vue2.x 和Ant Design Vue来编辑实现的。
1<template>
2 <el-aside :width="isCollapse ? `64px` : `200px`">
3 <div class="logo">
4 <img src="@/assets/img/avatar.png" alt="logo" draggable="false" />
5 <p>Vite2 Admin</p>
6 </div>
7 <el-menu
8 background-color="#001529"
9 text-color="#eee"
10 active-text-color="#fff"
11 router
12 unique-opened
13 :default-active="route.path"
14 :collapse="isCollapse"
15 >
16 <template v-for="item in routers" :key="item.name">
17 <template v-if="!item['hidden']">
18 <el-submenu v-if="item.children && item.children.length" :index="concatPath(item.path)">
19 <template #title>
20 <i :class="item.meta.icon"></i>
21 <span>{{ item.meta.title }}</span>
22 </template>
23 <template v-for="sub in item.children" :key="sub.name">
24 <el-menu-item :index="concatPath(item.path, sub.path)">
25 <i :class="sub.meta['icon']"></i>
26 <template #title>{{ sub.meta.title }}</template>
27 </el-menu-item>
28 </template>
29 </el-submenu>
30 <el-menu-item v-else :index="concatPath(item.path)">
31 <i :class="item.meta['icon']"></i>
32 <template #title>{{ item.meta.title }}</template>
33 </el-menu-item>
34 </template>
35 </template>
36 </el-menu>
37 <div class="fold" @click="changeCollapse">
38 <i v-show="!isCollapse" class="el-icon-d-arrow-left"></i>
39 <i v-show="isCollapse" class="el-icon-d-arrow-right"></i>
40 </div>
41 </el-aside>
42</template>
43
44<script>
45 import { computed, reactive, toRefs } from "vue";
46 import { useRoute } from "vue-router";
47 import { useStore } from "vuex";
48
49 export default {
50 setup() {
51 const route = useRoute();
52 const store = useStore();
53 const state = reactive({ isCollapse: false });
54 const routers = computed(() => store.state.routers);
55
56 const changeCollapse = () => {
57 state.isCollapse = !state.isCollapse;
58 };
59
60 const concatPath = (p_path, c_path = "") => {
61 return `${p_path !== "" ? "/" + p_path : "/"}${c_path !== "" ? "/" + c_path : ""}`;
62 };
63
64 return {
65 route,
66 routers,
67 concatPath,
68 changeCollapse,
69 ...toRefs(state)
70 };
71 }
72 };
73</script>
结合之前的模板代码,就可以完整的搭建出一个带有前端权限校验的 vue 后台管理系统,主要是梳理清路由数据和过滤后的路由鉴权后的路由数据信息。主要代码就是上述封装的过滤和权限校验函数。后续将放开后台模板代码,模板代码完善中......🍎🍎🍎