后台管理系统项目中的路由需要权限管理,不同的角色登录看到的页面是不一样的,所以路由应该是后端动态返回,然后前端拿到路由表进行处理后调用router.addRoutes([])添加到前端的静态路由表中形成完整路由表。

一、实现完整路由表

1,首先需要在router文件夹里新建一个路由配置项的文件routes.js,在里面定义一个静态无权限的路由,如登录页和总布局页面。

//初始静态路由,无权限

let routes = [

    {

        path: "/login",

        name: "Login",

        component: "Login",

    },

    {

        path: "/",

        redirect: "/index",

        name: "Layout",

        component: "Layout",

        children: [

            {

                path: "password",

                name: "Password",

                component: "my/Password",

                mata: {

                    bread: ["修改密码"],

                },

            },

        ],

    },

    {

        path: "*",

        component: "404",

    },

];

 

2,用户登录之前只能进入登录页,这里设置了一个全局前置守卫,在用户登陆成功之后本地存储个人信息,其中包含token,这里是个重点:给登录组件加一个组件内守卫  beforeRouteLeave在离开登录页之前提交Actions,在vuex中发送请求,根据token拿到路由表(这里试了下vuex的状态管理,像路由表这种可以直接存在本地,因为vuex中的数据在刷新时会丢失)

//vuex中的内容
export default new Vuex.Store({
    state: {
        asyncRoutes: [],
    },
    getters: {},
    mutations: {
        getAsyncRoutes(state, payload) {
            state.asyncRoutes = payload;
            sessionStorage.setItem(
                "asyncRoutes",
                JSON.stringify(state.asyncRoutes)
            );
        },
    },
    actions: {
        setAsyncRoutes({ commit }) {
            get("/menu").then((res) => {
                commit("getAsyncRoutes", res.data);
            });
        },
    },
    modules: {
        all,
    },
});
//login组件的组件内守卫,离开login之前触发,提交异步申请
beforeRouteLeave(to, from, next) {
        this.setAsyncRoutes();
        next();
    },

3,在routes.js文件中把动态返回的路由加在静态路由里,得到完整的路由表,注意:因为这里动态返回的路由都是layout的子路由,所以可以直接用数组的concat方法拼接(因为在vue-router官网中明确提到addRoutes()已废弃,但是新的方法addRoute()兼容性不好,这里可以用数组拼接的方式解决可以避免以上的问题)

//在本地存储里拿到有权限的路由表
let asyncRoutes = JSON.parse(sessionStorage.getItem("asyncRoutes"))
    ? JSON.parse(sessionStorage.getItem("asyncRoutes"))
    : [];
//把动态路由加到静态路由里
routes[1].children = routes[1].children.concat(asyncRoutes);

解释:本地存储中存的是字符串,使用时要转成对象,由于登陆前没有动态路由,所以用三目运算符避免错误,另外concat方法会返回一个新的数组,不会改变原数组,所以需要重新存一下,这时完整的路由表就实现了吗?其实不然,这里的component的值是一个字符串,但是我们实际路由中是懒加载的,是一个箭头函数,这里还需要进行处理,配置好component。 

4,封装函数实现路由的懒加载

//封装路由配置处理函数
let getRoutes = function(routes) {
    createRoute(routes);
    return routes;
};
//核心处理方法
function createRoute(route) {
    route.forEach((item) => {
        //没有component直接结束
        if (!item.component) {
            return;
        }
        //每一个路由懒加载配置
        let componentFn = import(`@/views/${item.component}.vue`);
        item.component = () => componentFn;
        //有子路由递归的进行配置
        if (item.children) {
            createRoute(item.children);
        }
    });
}
export default getRoutes(routes);

解释:第一个getRoutes函数负责把routes传进去,经过第二个函数处理后在返回,默认导出后引入到router文件夹的index.js文件中使用,第二个函数createRoute对routes进行循环处理,把每一个routes的component的值都变成懒加载的形式,如果有子路由用递归处理

至此,我们的路由全部配置完毕,实现纯后台维护的权限管理, 将来如果用户的权限变了只需要在后台修改存到数据库就行,前台完全不需要动了,但是如果是路径名或者组件路径修改的话还是需要前后端配合修改的。

二、菜单的动态渲染

获取到完整路由表后菜单渲染就比较简单了,我们来轻松的实现一下吧

1,先看看我们需要的接口数据格式,这点一定要和后端商量好,这个路由表完全由后端维护,格式正确可以事半功倍哦。

const menuList = [
    {
        path: "/index",
        name: "Index",
        component: "index/Index",
        meta: {
            role: ["everybody"],
            keepAlive: false,
            menuName: "首页",
            icon: "el-icon-s-home",
        },
    },
    {
        path: "/account",
        redirect: "/account/all",
        component: "account/Index",
        name: "Account",
        meta: {
            menuName: "账户管理",
            icon: "el-icon-coin",
        },
        children: [
            {
                path: "/account/all",
                name: "AccountAll",
                component: "account/All",
                meta: {
                    role: ["boss", "manager"],
                    bread: ["账户管理", "所有人员"],
                    keepAlive: true,
                    menuName: "所有人员",
                    icon: "el-icon-user",
                },
            },
            {
                path: "/account/business",
                name: "AccountBusiness",
                meta: {
                    menuName: "业务人员",
                    icon: "el-icon-phone-outline",
                },
            },
            {
                path: "/account/audit",
                meta: {
                    menuName: "审核人员",
                    icon: "el-icon-s-check",
                },
            },
            {
                path: "/account/risk",
                meta: {
                    menuName: "风控经理",
                    icon: "el-icon-s-finance",
                },
            },
            {
                path: "/account/admin",
                meta: {
                    menuName: "管理员",
                    icon: "el-icon-s-custom",
                },
            },
        ],
    },
    {
        path: "/product",
        redirect: "product/all",
        component: "product/Index",
        meta: {
            menuName: "产品管理",
            icon: "el-icon-menu",
        },
        children: [
            {
                path: "/product/all",
                name: "ProductAll",
                component: "product/All",
                meta: {
                    role: ["boss", "manager"],
                    bread: ["产品管理", "所有产品"],
                    keepAlive: true,
                    menuName: "全部产品",
                    icon: "el-icon-notebook-2",
                },
            },
            {
                path: "/product/carConsumption",
                meta: {
                    menuName: "汽车消费",
                    icon: "el-icon-truck",
                },
            },
            {
                path: "/product/estate",
                meta: {
                    menuName: "房产消费",
                    icon: "el-icon-office-building",
                },
            },
            {
                path: "/product/mortgage",
                meta: {
                    menuName: "抵押贷款",
                    icon: "el-icon-money",
                },
            },
        ],
    },
    {
        path: "/orders",
        redirect: "orders/all",
        component: "orders/Index",
        meta: {
            menuName: "订单管理",
            icon: "el-icon-s-order",
        },
        children: [
            {
                path: "/orders/all",
                name: "OrdersAll",
                component: "orders/All",
                meta: {
                    bread: ["订单管理", "所有订单"],
                    menuName: "所有订单",
                    icon: "el-icon-tickets",
                },
            },
            {
                path: "/orders/create",
                meta: {
                    menuName: "新建订单",
                    icon: "el-icon-document",
                },
            },
        ],
    },
    {
        path: "/customer",
        redirect: "/customer/info",
        component: "customer/Index",
        meta: {
            menuName: "客户管理",
            icon: "el-icon-user",
        },
        children: [
            {
                path: "/customer/info",
                name: "Info",
                component: "customer/Info",
                meta: {
                    bread: ["客户管理", "资金记录"],
                    menuName: "基本信息",
                    icon: "el-icon-chat-square",
                },
            },
            {
                path: "/record",
                name: "Record",
                component: "customer/Record",
                meta: {
                    bread: ["客户管理", "资金记录"],
                    menuName: "资金记录",
                    icon: "el-icon-bank-card",
                },
            },
        ],
    },
    {
        path: "/todo",
        name: "Todo",
        component: "todo/Todo",
        meta: {
            bread: ["待办事项"],
            menuName: "待办事项",
            icon: "el-icon-chat-dot-square",
        },
    },
    {
        path: "/my",
        name: "My",
        component: "my/My",
        meta: {
            bread: ["个人中心"],
            menuName: "个人中心",
            icon: "el-icon-user",
        },
    },
];
//左侧菜单接口
Mock.mock("localhost:8080/menu", "get", () => {
    //根据token查找权限,返回相应的路由表,此处省略这一步
    return {
        code: 200,
        success: true,
        message: "成功",
        data: menuList,
    };
});

这是我内容随便写的一个mock模拟接口,其实就是一个路由配置项,里面的path,name,component,children,meta都是关键字不能改,格式一定要完全一样,其中meta里可以带上我们需要的信息,比如面包屑,菜单渲染的名字,图标,是否需要缓存,角色限制等,项目中需要用到的都可以存在meta中。

2,接下来借助element-ui的el-menu组件进行菜单渲染, 思路:路由菜单根据返回的动态路由表实现,一般的项目都会有好几级菜单,具体的数量我们不清楚,用递归的思想完成

//这是父组件的内容,整个菜单
<template>
    <div>
        <!-- 菜单渲染 -->
        <el-menu
            :default-active="$route.path"
            class="el-menu-vertical-demo"
            unique-opened
            background-color="#333"
            text-color="white"
            router
        >
            //菜单的每一项,循环路由配置项
            <menu-item
                v-for="item in menus"
                :key="item.path"
                :menu="item"
            ></menu-item>
        </el-menu>
    </div>
</template>

<script>
import menuItem from "./menuItem.vue";
export default {
    components: { menuItem },
    data() {
        return {
            menus: "",
        };
    },
    methods: {
        loadData() {
            this.menus = JSON.parse(sessionStorage.getItem("asyncRoutes"));
        },
    },
    created() {
        this.loadData();
    },
};
</script>

 

//这是子组件的内容,渲染菜单的每一项
<template>
    <div>
        //路由的配置项中有children时,代表有二级菜单,渲染el-submenu
        <el-submenu :index="menu.path" v-if="menu.children">
            <template slot="title">
                <i :class="menu.meta.icon"></i>
                <span>{{ menu.meta.menuName }}</span>
            </template>
            //在组件中调用组件自身,递归的判断是否有子级,知道最后没有子级为止
            <menu-item
                v-for="item in menu.children"
                :key="item.path"
                :menu="item"
            ></menu-item>
        </el-submenu>
        //没有子级直接渲染这个
        <el-menu-item :index="menu.path" v-else>
            <i :class="menu.meta.icon"></i>
            <span slot="title">{{ menu.meta.menuName }}</span>
        </el-menu-item>
    </div>
</template>

<script>
export default {
    props: {
        menu: {
            type: Object,
            required: true,
        },
    },
    //组件调用自身时必须加name属性
    name: "menuItem",
};
</script>

 注意:菜单的真正渲染是在这个组件中,组件name的作用在组件内部调用自身时使用组件的name作为组件名,此外,keepAlive对动态组件进行缓存时用include时可以用name来决定哪个组件会被缓存。

这里menu是子组件自定义的属性,用于把父组件的数据传到子组件中,目的是把routes的每一项传进来获取每一个路由配置项的meta,拿到菜单名称和图标逐层渲染到菜单中。

 

 总结:vue的这种完全后端返回路由表的权限管理方式可以实现真正的权限管理,基本做到前后端分离,主要注意路由表的格式问题,路由表里需要后端返回的其他信息都放在meta里,比如菜单渲染,面包屑,角色限制,缓存需求等

Logo

前往低代码交流专区

更多推荐