Bootstrap

vue中使用动态路由,自定义指令做权限模块

简介

动态路由可做模块层面的权限控制,但要想把权限做的更细,可以使用vue自定义指令,控制到具体的标签中,当然 使用递归解决权限管理表单也是本文的一大亮点

动态路由

vue路由配置 并且在跳转路由时获取后台存储的路由信息

动态路由配置代码,当然仅仅有这段代码还不能够支持使用,因为它还需要其他的代码如发送请求,向session中存储appToken等。但您可以在此获得动态路由相关的业务,其核心部分在于 fetchGetRouterMenu()请求获取路由动态部分,再通过二次转换得到elementUI导航栏菜单需要的导航栏数据与静态路由部分合并成的整个路由。该部分需要的插件如下:

  1. 进度条插件,来自于 https://github.com/rstacruz/nprogress
    npm install --save nprogress
  2. vue官方的路由,用于路由用户设定访问路径,将路径和组件映射起来
    npm install vue-router --save
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from "@/store"
import fetchLogin, {getRouterMenu} from "@/api/users"; //本项目将所有的请求,统一到api目录,这里是引入自己的url请求方法
import {routes} from "./routes.js"; //前端存储的一些固定的路由 不需要动态路由存储的,比如“login”
import NProgress from "nprogress"; //进度条插件,用官网的话来讲: 涓涓细流动画告诉你的用户,一些事情正在发生!
import StringUtils from "@/js/StringUtils"; //自己的stringUtils

/**
 * 通过vue.use 安装路由插件
 */
const router = new VueRouter({
    mode: 'hash',
    base: process.env.BASE_URL,
    routes
})
Vue.use(VueRouter)

/**
 * 重写路由的push方法,防止跳转相同路由时报错
 */
const routerPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
    return routerPush.call(this, location).catch(error => error)
};

NProgress.configure({ showSpinner: false }) //进度条配置关闭加载微调器
const whiteList = ["/login", "/auth-redirect"]; //没有appToken时允许跳转这些路由
/**
 * 路由拦截器,所有路由跳转时都会进入该方法
 */
router.beforeEach(async (to, come, next) => {
    NProgress.start(); //跳转路由时 开启进度条
    if (Vue.prototype.$ViewConfig.appToken.get()) { //通过校验存储在session中的appToken, 来确定用户是否登录
        if (to.path === "/login") { //单独校验当存在appToken,又需要跳转到login的情况(退出登录)
            next({ path: "/" })
            NProgress.done()
        } else {
            if (store.state.roles.length === 0) { //通过校验登陆时存的roles,进一步校验是否已经登录
                try {
                    await GetUserInfo(); // 向后端认证是否登录
                    await getRouterMenu().then((res) => { //获取路由动态部分
                        let routesConcat = []
                        /* 核心部分,将通过请求获取到的路由动态部分加入到路由中*/
                        routesConcat = routes.concat(filterAsyncRoutes(res));
                        store.state.menuList = filterMenuList(routesConcat);
                        router.addRoutes(routesConcat)
                        next({ ...to, replace: true })
                    })
                } catch (err) {
                	/* 认证登录报错,或者获取动态路由报错,重定向到登录页 */
                    Vue.prototype.$ViewConfig.outSystem()
                    Vue.prototype.$message.error(err || "Has Error")
                    next(`/login?redirect=${to.path}`)
                    NProgress.done()
                }
            } else {
                next()
            }
        }
    } else {
        if (whiteList.indexOf(to.path) !== -1) {
            next()
        } else {
            next(`/login?redirect=${to.path}`)
            NProgress.done()
        }
    }
})
router.afterEach((to) => {
    NProgress.done()
})

/**
 * 二次校验登录信息
 */
const GetUserInfo = async () => {
    if (Vue.prototype.$ViewConfig.appToken === "") {
        throw Error("GetUserInfo: token is undefined!")
    }
    const { data } = await fetchLogin.fetchGetUserInfo()
    if (!data) {
        throw Error("认证失败,请先登录!!!")
    }
    // 存储一些与本文无关的认证属性(如appToken) 略....
}

/**
 * 处理动态路由部分,主要是为了将表示路由地址的字符串转化为路由地址
 */
export const filterAsyncRoutes = (routers) => {
    return routers.filter(router => {
        if (router.component) {
            if (router.component === "Layout") { // Layout组件特殊处理
                router.component = (resolve) => require([`@/views/index.vue`],resolve)
            } else {
                const component = router.component
                router.component = loadView(component, StringUtils.isNotEmpty(router.children))
            }
        }
        if (router.children && router.children.length) {
            router.children = filterAsyncRoutes(router.children)
        }
        return true
    })
}
export const loadView = (view, state) => {
    if (state) {
        return (resolve) => require([`@/views/templateIndex.vue`], resolve) //路由途径文件,单独处理
    } else {
        return (resolve) => require([`@/views/${view}.vue`], resolve)
    }
}

/**
 * 轮询解析路由动态部分,生成elementUI导航菜单能使用的菜单数据,因为接收到的格式不能直接放到elementUI中使用,需要修改属性名
 */
export const filterMenuList = (routers) => {
    let menuList = [];
    routers.forEach(router => {
        let menu = {};
        if (router.menuName) {
            if (!router.meta.hidden) { // 是否可见
                menu.route = router.name;
                menu.title = router.menuName;
                menu.icon = "";
                if (router.children && router.children.length) {
                    menu.children = filterMenuList(router.children)
                }
                menuList.push(menu)
            }
        }
    })
    return menuList

}

export default router

静态路由部分

在刚进入系统时需要一个路由从默认路径 / 重定向到 /login,而此时动态路由的请求还未发送,因此需要初始化写死一部分,这部分 本文中统一称为 静态路由部分

export const routes = [
    {
        path: '/',
        component: () => import("@/views"),
        meta: {
            requireAuth: true
        },
        children: [
    	    {
                path: "/",
                name: "home",
                component: () => import("@/views/home"),
                meta: {roles: []}
            }
        ]
    },
        {
        path: "/404",
        name: "notFound",
        component: () => import("@/views/404"),
        meta: {roles: []},
    },
    {
        path: "/login",
        name: "login",
        component: () => import("@/views/login"),
        meta: {roles: []},
    },
]

权限控制自定义指令

vue自定义指令用于权限控制算是一个很经典的用法了,网上一搜就可以搜到很多相关的介绍文章,但都是使用了 vue-directive: https://cn.vuejs.org/v2/api/#Vue-directive。若只有这段代码store.state.roles是不能被识别的,因为他是项目初始化时从后台获取到的一些,代表该角色拥有的权限的权限码 组成的数组

创建自定义指令

此自定义指令中主要业务是判断store.state.roles中是否有传入的权限码

/**
 * 检验元素权限属性,若有权限保留,无权限则删除
 */
Vue.directive('permission', {
    inserted: function (el, binding, vnode) {
        const {value} = binding
        let boolean = true;
        const roles = store.state.roles; //获取session中存储的权限
        if(!roles) {
            return false;
        }
        if (value && value instanceof Array && value.length > 0) {
            //该层if是为了排除多个权限控制一个按钮的情况,正常情况下不会进入该if
            const permissionRoles = value
            const hasPermission = permissionRoles.some(permissionRole =>{
                return roles.includes(permissionRole+'')
            })
            if (!hasPermission) {
                boolean = false
            }
        }else if(value) {
            if (!roles.includes(value)) {
                boolean = false
            }
        }
        //没有权限则删除标签
        if (!boolean){
            el.parentNode && el.parentNode.removeChild(el)
        }
    },
})

使用自定义指令

使用自定义指令只需要在任意标签上加上v-permission的属性就可以校验登录的角色是否有允许查看该标签的权限了。其中 110 代表该新增按钮所需的权限码

<el-button type="primary" :sizse="$ViewConfig.size" @click="handleAdd()" v-permission="110">新增</el-button>

递归解决权限管理页面

了解了自定义指令如何在标签层面解决权限问题后,有个重要的问题,就是这些权限码如何而来呢?当然此处就不介绍上传权限码,存到数据库,读取这些crud操作了,主要讲讲如何上传这些权限码

权限管理页面的效果

先看看权限管理页面的效果
**表单效果
**

递归实现

由于此处子节点不确定数量,此处使用组件递归处理,当然由后端统计个数,然后用for循环解决也是可行的。

表单部分

表单部分为上图的表单,为了更能直观的看到递归实现的逻辑,此处代码只有上图中"权限"部分

  1. 该弹窗需要外部调用 this.$refs.configForm.addRole(); 开启弹窗
  2. 该段代码主要可分为三个部分:使用elementUI的多选框实现权限选中,获取权限列表及提交表单,子权限和父权限之间的联动
<style scoped>
.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner::after{
    border-color: #151516 !important;
}
</style>
<template>
    <div>
        <el-dialog title="角色" :visible.sync="isShow" width="800px" :close-on-click-modal="false" :close-on-press-escape="false"  :destroy-on-close="true" top="5vh" @closed="resetForm">
            <el-form ref="dataForm" :model="formData" :rules="rules" label-width="100px" size="mini"  v-loading="loading">
                <el-form-item label="权限:">
                    <template slot-scope="scope">
                        <el-checkbox-group v-model="checkList">
                            <template v-for="(root, rootIndex) in permissionList">
                                <div style="background-color: #63b8ce;padding: 5px 10px 0px;">
                                    <el-checkbox :label="root.code" @change="changeRootCheck(root)"><span style="color: #FFF;">{{root.desc}}</span></el-checkbox>
                                </div>

                                <role-recursion :ref="'recursion_'+root.code"
                                                :root="root"
                                                :thisObj="thisObj"
                                                :disabledArray=notSelectableList
                                                @push-in-check-list-super="pushInCheckList(root.code)"
                                                @remove-by-father-super="removeByCheckListSuper(root)"></role-recursion>

                            </template>
                        </el-checkbox-group>
                    </template>
                </el-form-item>

                <el-form-item>
                    <div style="float: right" v-if="flag !== 'detail'">
                        <el-button :size="$ViewConfig.size">取 消</el-button>
                        <el-button :size="$ViewConfig.size" type="primary" @click="submitForm">确 定</el-button>
                    </div>
                </el-form-item>
            </el-form>
        </el-dialog>
    </div>
</template>

<script>
// 引入递归部分
import role_recursion from "./role_recursion";
export default {
    name: "role-list",
    components:{
        "role-recursion": role_recursion
    },
    data() {
        return {
            loading: true,
            isShow: false,
            formData: {
                name: null,
                description: null,
                auths: null,
            },
            permissionList: [],// 可选权限
            notSelectableList: [],// 不可选权限
            checkList: [],// 选中权限
            rules: {
                name: [
                    { required: true, message: '请输入角色名称', trigger: 'blur' }
                ],
            },
            thisObj: this
        }
    },
    methods: {
        /**
         * 新增
         */
        addRole() {
            this.getPermissionList();
            this.getModelList();
            this.isShow = true;
            this.formData = {
                name: null,
                description: null,
                auths: null,
            }
            this.loading = false;
        },
        /**
         *  获取权限列表
         **/
        getPermissionList(id) {
            this.$Http.get("role/getShowRole",{id: id}).then(data => {
                this.permissionList = data.permissionList;
                this.notSelectableList = data.notSelectableList;
            }).catch(err =>{
                this.$message.error("获取权限列表失败")
            })
        },
        /**
         * 重置表单
         **/
        resetForm() {
            this.modelOptions = [];
            this.modelRole = null;
            this.checkList = [];
            this.$refs.dataForm.resetFields();
             //permissionList为空数组时 不会渲染 role_recursion,此处防止新的notSelectableList未获取到就渲染了新的权限复选框
            this.permissionList = [];
        },
        /**
         * 提交表单
         */
        submitForm() {
            this.$refs.dataForm.validate((valid) => {
                if(valid) {
                    this.formData.auths = this.checkList;
                    let url = "role/add";
                    this.loading = true;
                    this.$Http.post(url,this.formData,true).then(data => {
                        this.$parent.getTableList();
                        this.isShow = false;
                    }).finally(()=> {
                        this.loading = false;
                    });
                }
            })
        },
        /**
         * 修改根权限事件
         */
        changeRootCheck(menu) {
            if (this.$StringUtils.isEmpty(menu.subList)) {
                return
            }

            if (this.thisObj.checkList.includes(menu.code)) {
                for (const check of menu.subList) {//循环添加子节点
                    this.pushInCheckList(check.code);
                    this.$refs['recursion_'+menu.code][0].changeChain(check);
                }
            } else {
                for (const check of menu.subList) {//循环删除子节点
                    this.removeByCheckList(check.code);
                    this.changeRootCheck(check);
                }
            }
        },
        /**
         * 移除顶级
         */
        removeByCheckListSuper(menu) {
            for (const check of menu.subList) {//校验子节点是否存在
                if (this.thisObj.checkList.includes(check.code)) {
                    return
                }
            }

            this.removeByCheckList(menu.code);
        },
        /**
         * 如果不存在,则加入checkList
         * 如果存在
         */
        pushInCheckList(item) {
            if(this.checkList.indexOf(item) === -1) {
                this.checkList.push(item);
            }
        },
        /**
         * 移除checkList的元素
         */
        removeByCheckList(item) {
            let index = this.checkList.indexOf(item);
            if(index !== -1) {
                this.checkList.splice(index, 1);
            }
        },
    }
}
</script>

递归部分

为了方便递归,将递归部分单独提出来,这段代码较另外几段代码理解起来有一定的难度,这里做一些解释吧

dom部分
  1. 这里一次递归表示上图中横着一行权限
  2. 它的v-for循环的是这样一段数据 1这里为了节约篇幅没有格式化,若需要格式化可以搜一下格式化json的在线网址
  3. dom部分的重点在于,在role-recursion中引用role-recursion 达到递归的效果,role-recursion是该递归组件的文件名
js部分
  1. props.thisObj:为了方便对父级的选中项做处理,这里传入父级的this
  2. props.disabledArray: role-recursion会将这个数组中的code禁用,当然,该层递归用过后会将该数组继承到下一次递归,以便下一次递归能正常禁用其中的code
  3. 因为是递归实现dom,子级和父级之间的多选框不能自动实现联动,需要手动实现联动效果
<template>
    <div>
        <div v-for="(menu, menuIndex) in root.subList" style="padding-left: 5px;display: inline">
            <template v-if="menu.subList === null">
                <el-checkbox
                    :label="menu.code"
                    @change="changeChain(menu)"
                    :disabled="disabled_[menu.code]">
                    <span style="display:-moz-inline-box; display:inline-block;">
                        {{ menu.desc }}
                    </span>
                </el-checkbox>
            </template>

            <template v-else>
                <el-checkbox
                    :label="menu.code" v-model="thisObj.checkList"
                    @change="changeChain(menu)"
                    :disabled="disabled_[menu.code]">
                    <span style="display:-moz-inline-box; display:inline-block;width: 100px;color: #428bca;">
                        {{ menu.desc }}
                    </span>
                </el-checkbox>
                <role-recursion :ref="'recursion_' + menu.code"
                                :root="menu"
                                :thisObj="thisObj"
                                :disabled="disabled_[menu.code]"
                                :disabledArray="disabledArray"
                                @push-in-check-list-super="pushInCheckListSuper(menu)"
                                @remove-by-father-super="removeByCheckListSuper(menu)"
                                class="recursion-portion" ></role-recursion>
            </template>
        </div>
    </div>

</template>

<script>
export default {
    //此处name为短横线name,dom递归时引用此name
    name: "role-recursion",
    props: {
        //父级节点
        root: {
            type: Object,
            required: true,
        },
        //调用方的this
        thisObj: {
            type: Object,
            required: true,
        },
        //是否禁用该多选及其子多选
        disabled: {
            type: Boolean,
            required: false,
            default: false
        },
        //禁用这些code
        disabledArray: {
            type: Array,
            required: false,
            default: []
        }
    },
    data() {
        return {
            disabled_: {},
        }
    },

    created() {
        for (let menu of this.root.subList) {
            let includes = this.disabledArray.includes(menu.code);
            this.disabled_[menu.code] = this.disabled || includes;
        }
    },
    methods: {
        //新增父级
        pushInCheckListSuper(menu) {
            this.thisObj.pushInCheckList(menu.code);
            this.$emit('push-in-check-list-super');
        },
        //移除父级
        removeByCheckListSuper(menu) {
            for (const check of menu.subList) {//校验子节点是否存在
                if (this.thisObj.checkList.includes(check.code)) {
                    return
                }
            }

            this.thisObj.removeByCheckList(menu.code);
            this.$emit('remove-by-father-super')
        },
        //勾选连锁
        changeChain(menu) {
            if (this.thisObj.checkList.includes(menu.code)){
                if (this.disabled_[menu.code]) {
                    this.thisObj.removeByCheckList(menu.code);
                    return
                }
                if (menu.subList !== null) {
                    for (const check of menu.subList) {//新增子集
                        this.thisObj.pushInCheckList(check.code);
                        if (this.$refs['recursion_'+menu.code] !== undefined) {//迭代子集
                            this.$refs['recursion_'+menu.code][0].changeChain(check);
                        }
                    }
                }

                this.$emit('push-in-check-list-super')//新增父级
            }else {
                this.thisObj.removeByCheckList(menu.code);

                if (menu.subList !== null) {
                    for (const check of menu.subList) {//移除子集
                        this.thisObj.removeByCheckList(check.code);
                        if (this.$refs['recursion_'+menu.code] !== undefined) {//迭代子集
                            this.$refs['recursion_'+menu.code][0].changeChain(check);
                        }
                    }
                }

                this.$emit('remove-by-father-super');//移除父级
            }
        },
    }
}
</script>

<style scoped>
.recursion-portion{
    margin-left: 9px;
    padding-left: 50px;
    /*box-shadow: inset 5px 0px 0 0 black;*/
}

</style>

  1. [{“code”:100,“desc”:“我的应用”,“subList”:[{“code”:110,“desc”:“系统管理”,“subList”:[{“code”:111,“desc”:“查询”,“subList”:null},{“code”:112,“desc”:“新增”,“subList”:null},{“code”:113,“desc”:“更新”,“subList”:null},{“code”:114,“desc”:“删除”,“subList”:null},{“code”:115,“desc”:“创建”,“subList”:null}]},{“code”:120,“desc”:“权限管理”,“subList”:[{“code”:121,“desc”:“查询”,“subList”:null},{“code”:122,“desc”:“新增”,“subList”:null},{“code”:123,“desc”:“更新”,“subList”:null},{“code”:124,“desc”:“删除”,“subList”:null},{“code”:125,“desc”:“创建”,“subList”:null}]},{“code”:130,“desc”:“账号管理”,“subList”:[{“code”:131,“desc”:“查询”,“subList”:null},{“code”:132,“desc”:“新增”,“subList”:null},{“code”:133,“desc”:“更新”,“subList”:null},{“code”:134,“desc”:“删除”,“subList”:null}]},{“code”:140,“desc”:“SSO配置”,“subList”:[{“code”:141,“desc”:“查询”,“subList”:null},{“code”:142,“desc”:“更新”,“subList”:null}]},{“code”:150,“desc”:LDAP配置",“subList”:[{“code”:151,“desc”:“查询”,“subList”:null},{“code”:152,“desc”:“更新”,“subList”:null}]}]}] ↩︎

;