• Vue管理系统前端系列六动态路由-权限管理实现



    为什么要使用动态路由?

    一般系统中,都会有不同权限的操作人员,这时候他们看到的页面也将会不同,因此都需要根据他们的权限来生成对应的菜单,这个就得通过动态路由来实现。

    主流的两种实现方式

    控制一般都是由前端的路由中设置。后端返回路由表动态生成两种。

    本文主要记录由数据库维护的动态路由实现,和相关注意点。即 退出 和 刷新 相关点导致的路由问题。

    其它的可参考文章:动态路由

    前端控制

    • 不用后端控制,只用返回角色、
    • 根据可能有的角色,在对应路由上维护相关角色
    • 在登录后,判断路由中维护的角色是否吻合来动态添加生成

    后端控制

    • 路由存在数据库中,可动态维护。且相对安全。
    • 登录后,获取动态路由信息。
    • 得到路由后,匹配文件,生成路由,添加

    后端控制路由 实现

    由于我这里是用 fastmock 模拟的数据,实际中还请自行生成。

    mock 数据如下:

    点击查看折叠代码块
    {
        "code": 200,
        "success": true,
        "data": [
            {
                "menuId": 2,
                "menuName": "一级菜单",
                "parentMenuId": 0,
                "url": "menu/singleMenu/index",
                "type": 1,
                "icon": "el-icon-wind-power",
                "orderIndex": 1,
                "children": [
                ]
            },
            {
                "menuId": 3,
                "menuName": "二级菜单",
                "parentMenuId": 0,
                "url": "",
                "type": 1,
                "icon": "el-icon-ice-cream-round",
                "orderIndex": 1,
                "children": [
                    {
                        "menuId": 301,
                        "menuName": "二级1-1",
                        "parentMenuId": 3,
                        "url": "menu/secondMenu/second1-1",
                        "type": 2,
                        "icon": "el-icon-milk-tea",
                        "orderIndex": 1,
                        "children": [
                        ]
                    },
                    {
                        "menuId": 302,
                        "menuName": "二级1-2",
                        "parentMenuId": 3,
                        "url": "menu/secondMenu/second1-2",
                        "type": 2,
                        "icon": "el-icon-potato-strips",
                        "orderIndex": 2,
                        "children": [
                        ]
                    },
                    {
                        "menuId": 303,
                        "menuName": "二级1-3",
                        "parentMenuId": 3,
                        "url": "menu/secondMenu/second1-3",
                        "type": 2,
                        "icon": "el-icon-lollipop",
                        "orderIndex": 3,
                        "children": [
                        ]
                    }
                ]
            },
            {
                "menuId": 4,
                "menuName": "三级多级菜单",
                "parentMenuId": 0,
                "url": "",
                "type": 1,
                "icon": "el-icon-ice-cream-round",
                "orderIndex": 1,
                "children": [
                    {
                        "menuId": 401,
                        "menuName": "三级1-1",
                        "parentMenuId": 4,
                        "url": "menu/threeMenu/three1-1",
                        "type": 2,
                        "icon": "el-icon-milk-tea",
                        "orderIndex": 1,
                        "children": [
                        ]
                    },
                    {
                        "menuId": 402,
                        "menuName": "二级1-2",
                        "parentMenuId": 4,
                        "url": "",
                        "type": 2,
                        "icon": "el-icon-potato-strips",
                        "orderIndex": 2,
                        "children": [
                            {
                                "menuId": 40201,
                                "menuName": "三级1-2-1",
                                "parentMenuId": 402,
                                "url": "menu/threeMenu/nextMenu/three1-2-1",
                                "type": 2,
                                "icon": "el-icon-milk-tea",
                                "orderIndex": 1,
                                "children": [
                                ]
                            },
                            {
                                "menuId": 40202,
                                "menuName": "三级1-2-2",
                                "parentMenuId": 402,
                                "url": "menu/threeMenu/nextMenu/three1-2-2",
                                "type": 2,
                                "icon": "el-icon-potato-strips",
                                "orderIndex": 2,
                                "children": [
                                ]
                            }
                        ]
                    }
                ]
            }
        ],
        "message": "成功"
    }
    

    添加菜单接口 及 菜单状态管理

    由于这里是 mock 的。所以就判断了下登录用户名。代码如下:

    export const getMenu = (username) => {
        if (username == 'user') {
            return axios.Get('api/usermenu')
        } else {
            return axios.Get('api/menu')
        }
    }
    

    状态管理用于保存当前理由加载状态,和菜单值。

    再在 actions 中添加一个获取菜单的方法,完整代码如下:

    //引入接口
    import { getMenu } from '@/api/modules/system'
    
    export default {
        state: {
            menuTree: [],
            menuLoad: false, //菜单是否已加载状态避免重复加载,刷新又将变为false。
        },
        getters: {
            menuLoad: (state) => {
                return state.menuLoad
            },
        },
        mutations: {
            setMenuLoad(state, menuLoad) {
                state.menuLoad = menuLoad
            },
            setMenuTree(state, menuTree) {
                state.menuTree = menuTree
            },
        },
        actions: {
            getMenuTree({ commit }, username) {
                return new Promise((resolve, reject) => {
                    getMenu(username)
                        .then((res) => {
                            if (res.code === 200) {
                                if (res.success) {
                                    commit('setMenuTree', res.data)
                                } else {
                                    // TODO 处理错误消息
                                }
                                resolve(res.data)
                            }
                        })
                        .catch((error) => {
                            reject(error)
                        })
                })
            },
        },
    }
    

    根据得到的菜单生成动态路由

    在这里由于退出时,会导致路由和加载状态不会更新,也不会重置路由的原因,完整代码中包含相关处理。

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import login from '@/views/login'
    import store from '@/store'
    import { getMenu } from '@/api/modules/system'
    
    Vue.use(VueRouter)
    const originalPush = VueRouter.prototype.push
    VueRouter.prototype.push = function push(location) {
        return originalPush.call(this, location).catch((err) => err)
    }
    const routes = [
        {
            path: '/',
            name: 'home',
            component: () => import('@/layout'),
            children: [
                {
                    path: '',
                    name: 'index',
                    component: () => import('@/views/home/defaultPage'),
                    meta: {
                        title: '首页',
                        index: 0,
                    },
                },
            ],
        },
        {
            path: '/login',
            name: 'login',
            component: login,
            meta: {
                title: '登录',
            },
        },
        {
            path: '/notfound',
            name: 'notfound',
            component: () => import('@/views/notfound'),
            meta: {
                title: '未找到',
            },
        },
    ]
    
    const defultRouter = () => {
        return new VueRouter({
            routes: routes,
        })
    }
    //每次使用默认路由
    const router = defultRouter()
    
    // 解决addRoute不能删除动态路由问题
    export function resetRouter() {
        const reset = defultRouter()
        router.matcher = reset.matcher
    }
    
    const WhiteListRouter = ['/login', '/notfound'] // 路由白名单
    //导航守卫  路由开始前
    router.beforeEach(async (to, from, next) => {
        let user = store.getters.userInfo
        let token = store.getters.token
        var hasAuth = user !== null && token !== null && user !== undefined && token !== undefined
        if (to.path == '/login') {
            if (hasAuth) {
                next({ path: '/' })
            } else {
                next()
            }
        } else {
            if (!hasAuth) {
                //没登录的情况下  访问的是否是白名单
                if (WhiteListRouter.indexOf(to.path) !== -1) {
                    next()
                } else {
                    next({
                        path: '/login',
                        query: {
                            redirect: to.fullPath,
                        },
                    })
                }
            } else {
                if (store.state.app.menuLoad) {
                    // 已经加载过路由了
                    next()
                    return
                } else {
                    console.log(user.username)
                    // 使用 await 进行同步处理
                    const menu = await store.dispatch('getMenuTree', user.username)
                    console.log(menu)
                    // 加载动态菜单和路由
                    addDynamicMenuRoute(menu)
                    //next({ ...to, replace: true }); // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
                    next()
                }
            }
        }
    })
    
    //刷新 加载完后 加载未找到路由  此方法只会在刷新后加载一次
    router.onReady(() => {
        var notfund = [
            {
                path: '*',
                redirect: '/notfound',
                name: 'notfound',
                component: () => import('@/views/notfound'),
                meta: {
                    title: '未找到',
                },
            },
        ]
        router.options.routes = router.options.routes.concat(notfund)
        router.addRoutes(router.options.routes)
    })
    
    /**
     * 加载动态菜单和路由
     */
    function addDynamicMenuRoute(menuData) {
        if (store.state.app.menuRouteLoaded) {
            console.log('已加载菜单和路由.')
            return
        }
        // 根据返回的菜单 拼装路由模块
        let dynamicRoutes = addDynamicRoutes(menuData)
        // 处理静态组件绑定路由
        router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes)
    
        //添加路由
        router.addRoutes(router.options.routes)
    
        // 保存加载状态
        store.commit('setMenuLoad', true)
    }
    /**
     * 添加动态(菜单)路由
     * @param {*} menuList 菜单列表
     * @param {*} routes 递归创建的动态(菜单)路由
     */
    function addDynamicRoutes(menuList = [], routes = []) {
        var temp = []
        for (var i = 0; i < menuList.length; i++) {
            if (menuList[i].children && menuList[i].children.length >= 1) {
                temp = temp.concat(menuList[i].children)
            } else if (menuList[i].url && /S/.test(menuList[i].url)) {
                //将第一个斜杠去掉
                menuList[i].url = menuList[i].url.replace(/^//, '')
                // 创建路由配置
                var route = {
                    path: menuList[i].url,
                    component: null,
                    name: menuList[i].menuName,
                    meta: {
                        title: menuList[i].menuName,
                        icon: menuList[i].icon,
                        index: menuList[i].menuId,
                    },
                }
                try {
                    // 根据菜单URL动态加载vue组件,这里要求vue组件须按照url路径存储
                    // 如url="menu/singleMenu/index",则组件路径应是"@/views/menu/singleMenu/index".vue",否则将找不到改组件
                    let url = menuList[i].url
                    route['component'] = (resolve) => require([`@/views/${url}`], resolve)
                } catch (e) {}
    
                routes.push(route)
            }
        }
        if (temp.length >= 1) {
            addDynamicRoutes(temp, routes)
        }
        return routes
    }
    
    export default router
    

    根据 vuex 中的暂存的菜单生成侧边菜单栏

    新建菜单组件 递归生成菜单,新建 menuTree/index.vue,代码如下:

    <!-- 动态菜单组成 -->
    <template>
        <div>
            <!-- :popper-append-to-body="false" 解决 el-menu垂直布局递归生成菜单及菜单折叠后hover报错Maximum call stack size exceeded -->
            <!-- 但是会导致最上面的菜单嵌入页面,无法显示出来  因此使用外层嵌套div加样式的解决方案-->
            <el-submenu v-if="menu.children && menu.children.length >= 1"
                        :index="'' + menu.menuId"
                        :popper-append-to-body="true">
                <template slot="title">
                    <i :class="menu.icon"></i>
                    <span slot="title">{{ menu.menuName }}
                    </span>
                </template>
                <menu-tree v-for="item in menu.children"
                           :key="item.menuId"
                           :menu="item">
                </menu-tree>
            </el-submenu>
            <el-menu-item v-else-if="menu.url == ''"
                          :index="'' + menu.menuId"
                          disabled>
                <i class="el-icon-magic-stick"></i>
                <span slot="title">{{ menu.menuName }}
                </span>
            </el-menu-item>
            <el-menu-item v-else
                          :index="'' + menu.menuId"
                          @click="$router.push('/' + menu.url)">
                <i :class="menu.icon"></i>
                <span slot="title">{{ menu.menuName }}
                </span>
            </el-menu-item>
        </div>
    </template>
    
    <script>
    export default {
        name: "MenuTree",
        props: {
            menu: {
                type: Object,
                required: true
            }
        },
        data () {
            return {}
        },
    
    }
    </script>
    <style lang="scss" scoped>
    .el-menu--collapse > div > .el-submenu > .el-submenu__title span {
        height: 0;
         0;
        overflow: hidden;
        visibility: hidden;
        display: inline-block;
    }
    .el-menu--collapse
        > div
        > .el-submenu
        > .el-submenu__title
        .el-submenu__icon-arrow {
        display: none;
    }
    </style>
    

    在侧边栏中,从 state 中得到菜单,生成侧边栏菜单,完整代码如下:

    <!-- aside -->
    <template>
        <div class="aside-container"
             :class="isCollapse ? 'aside-collapse-width' : 'aside-width'">
            <!--导航菜单  default-active="1-1"  :background-color="themeColor" -->
            <el-menu class="el-menu-vertical-demo"
                     :class="isCollapse ? 'aside-collapse-width' : 'aside-width'"
                     :collapse-transition="false"
                     :unique-opened="true"
                     :collapse="isCollapse"
                     ref="menuTreeRef"
                     :background-color="themeColor"
                     text-color="#fff"
                     active-text-color="#ffd04b">
                <menu-tree v-for="menu in menuTree"
                           :key="menu.menuId"
                           :menu="menu">
                </menu-tree>
            </el-menu>
        </div>
    </template>
    
    <script>
    import { mapState } from 'vuex'
    import MenuTree from './menuTree'
    export default {
        data () {
            return {}
        },
        components: {
            MenuTree,
        },
        computed: {
            ...mapState({
                isCollapse: (state) => state.app.isCollapse,
                themeColor: (state) => state.app.themeColor,
                menuTree: (state) => state.app.menuTree,
            }),
            mainTabs: {
                get () {
                    return this.$store.state.app.mainTabs
                },
                set (val) {
                    this.$store.commit('updateMainTabs', val)
                },
            },
            mainTabsActiveName: {
                get () {
                    return this.$store.state.app.mainTabsActiveName
                },
                set (val) {
                    this.$store.commit('updateMainTabsActiveName', val)
                },
            },
        },
        watch: {
            $route: 'handleRoute',
        },
        created () {
            console.log(this.$route)
            this.handleRoute(this.$route)
        },
        methods: {
            // 路由操作处理
            handleRoute (route) {
                // tab标签页选中, 如果不存在则先添加
                var tab = this.mainTabs.filter((item) => item.name === route.name)[0]
                if (!tab) {
                    tab = {
                        name: route.name,
                        title: route.meta.title,
                        icon: route.meta.icon,
                    }
                    this.mainTabs = this.mainTabs.concat(tab)
                }
                this.mainTabsActiveName = tab.name
                //解决刷新后,无法定位到当前打开菜单栏
                this.$nextTick(() => {
                    // 切换标签页时同步更新高亮菜单
                    if (this.$refs.menuTreeRef != null) {
                        this.$refs.menuTreeRef.activeIndex = '' + route.meta.index
                        this.$refs.menuTreeRef.initOpenedMenu()
                    }
                })
            },
        },
    }
    </script>
    <style lang="scss" scoped>
    .aside-container {
        position: fixed;
        top: 0px;
        left: 0;
        bottom: 0;
        z-index: 1020;
        .el-menu {
            position: absolute;
            top: $header-height;
            bottom: 0px;
            text-align: left;
        }
    }
    .aside-width {
         $aside-width;
    }
    .aside-collapse-width {
         $aside-collapse-width;
    }
    </style>
    

    退出后重置 vuex

    因为只要登录过,那么当前状态中的 活动窗体 肯定是有值的,那么只需要判断该值是否有,有就刷新一下界面。

    这里使用的是 reload 来刷新页面。

        created() {
            //若是使用状态退出 则刷新一下 重置vuex
            if (this.$store.state.app.mainTabsActiveName != '') {
                window.location.reload()
            }
        },
    

    刷新的方式可以参考:vue 刷新当前页的三种方法

    最终效果

    原文地址:http://book.levy.net.cn/doc/frontend/uiframe/dynamic_router.html

  • 相关阅读:
    8.8总结
    8.4总结
    8.3总结
    题目分享H 二代目
    题目分享G 二代目
    题目分享E 二代目
    题目分享F 二代目
    题目分享D 二代目
    题目分享C 二代目
    题目分享Y
  • 原文地址:https://www.cnblogs.com/levywang/p/13543759.html
Copyright © 2020-2023  润新知