VUE3+vite+Ant Design+TS-后台管理系统-动态菜单问题

本文详细介绍了在Vue后台管理系统中如何配置动态菜单,包括使用vue-router处理路由,通过mock数据获取后端路由,以及如何在权限控制下动态生成和隐藏菜单项。
摘要由CSDN通过智能技术生成

我们在写后台管理系统动态菜单的时候经常会碰到一些问题,动态菜单具体实现步骤以及实现时经常碰到的一系列问题,在这里记录并分享。

首先是router,router包含两个部分,router/index.vue,和后端或者mock获取的json.

//router/index.vue
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
export const Layout = () => import("@/layout/index.vue");
export const constantRoutes: Array<RouteRecordRaw> = [
    {
        path: '/',
        name: '/',
        component: Layout,
        meta: {
            title: "首页",
            icon: "system",
            hidden: false,
            roles: ["ADMIN"],
        },
        children:[
            {
                path:"dashboard",
                component: () => import('@/view/Index.vue'),
                name: "Dashboard",
                meta: {
                    title:'dashboard',
                    hidden: false,
                },
            },
            {
                path: "401",
                component: () => import("@/views/error-page/401.vue"),
                meta: { hidden: true },//在动态菜单中隐藏当前页面
            },
        ],
    },
]
export const router = createRouter({//main.js中添加router后就可以使用了
    history: createWebHistory(),//或者createWebHashHistory:url使用#号
    routes: constantRoutes as RouteRecordRaw[],
})
//menu.mock.ts
export default [
    {
        url: "/api/v1/menus/routes", 
        method: "get",
        response: () => {
            return {
                code: 0,
                message: "ok",
                data: [
                    {
                        path: "/renmindemingyi",
                        component: "Layout",
                        name: "/renmindemingyi",
                        meta: {
                            title: "人民的名义"
                            icon: "menu",
                            hidden: false,//菜单中是否隐藏
                            roles: ["ADMIN"],
                        },
                        children: [
                            {
                                path: "roleList",
                                component: "renmindemingyi/roleList/index",
                                name: "RoleList",
                                meta: {
                                    title: "角色列表",
                                    hidden: false,
                                },
                            },

                            {
                                path: "roleList",
                                component: "renmindemingyi/introduce/index",
                                name: "RoleList",
                                meta: {
                                    title: "剧情简介",
                                    hidden: false,
                                },
                            },
                        ],
                    },
                    {
                        path: "/fumuaiqing",
                        component: "Layout",
                        name: "/renmindemingyi",
                        meta: {
                            title: "父母爱情"
                            icon: "menu",
                            hidden: false,//菜单中是否隐藏
                            roles: ["ADMIN"],
                        },
                    },
                    //继续添加加。。。。。。
                ],
            }
        }
    },
    {
        //继续添加加。。。。。。
    }
]
//main.js  入口文件
import "@/permission";
import {router} from '@/router'
app.use(router).mount('#app')
//permission.js
import {router} from "@/router";
import { usePermissionStoreHook } from "@/store/modules/permission";
const permissionStore = usePermissionStoreHook();
let isShow = true // 自定义变量去控制两次进入路由守卫
router.beforeEach(async (to, from, next) => {
    try {
        const roles = ['ADMIN']
        const accessRoutes = await permissionStore.generateRoutes(roles);
        accessRoutes.forEach((route) => {
            router.addRoute(route);
        });
        // 判断是否第一次进入路由守卫
        if (isShow) { 
            next({ ...to, replace: true })
            isShow = false // 第一次进入后修改,使第二次跳转
        } else {
            next()
        }
    } catch (error) {
        
    }
    return false
})

判断是否第一次进入路由守卫特别重要,如果只使用 next({ ...to, replace: true }),就会陷入无限循环。但是如果去掉参数使用next(),它会警告: No match found for location with path "*****"

// @/store/modules/permission
import { ref } from 'vue';
import { RouteRecordRaw } from "vue-router";
import { defineStore } from "pinia";
import { constantRoutes } from "@/router";
import { store } from "@/store";
import { listRoutes } from "@/api/menu";
//后端获取的路由中component是字符串,想要导入component中的文件需要使用vue3+vite中的import.meta.glob动态导入, 获取view文件下的所有vue文件
const modules = import.meta.glob("../../view/**/**.vue");
const Layout = () => import("@/layout/index.vue");
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
    const asyncRoutes: RouteRecordRaw[] = [];
    routes.forEach((route) => {
        const tmpRoute = { ...route }; 
        if (!route.name) {
            tmpRoute.name = route.path;
        }
        if (tmpRoute.component?.toString() == "Layout") {
            //跳转layout/index.vue文件
            tmpRoute.component = Layout;
        } else {
            const component = modules[`../../view/${tmpRoute.component}.vue`];
            if (component) {
              tmpRoute.component = component;
            } else {
              tmpRoute.component = modules[`../../view/error-page/404.vue`];
            }
        }
        //递归children
        if (tmpRoute.children) {
            tmpRoute.children = filterAsyncRoutes(tmpRoute.children, roles);
        }
        //把转换好的数据push到route中
        asyncRoutes.push(tmpRoute);
    })
    return asyncRoutes;
}
export const usePermissionStore = defineStore("permission", () => {
    const routes = ref<RouteRecordRaw[]>([]);
      // actions
    function setRoutes(newRoutes: RouteRecordRaw[]) {
        //获取静态路由并设置新路由
        routes.value = constantRoutes.concat(newRoutes);
    }
    /**
     * 生成动态路由
     */
    function generateRoutes(roles: string[]) {
        return new Promise<RouteRecordRaw[]>((resolve, reject) => {
            // 接口获取所有路由
            listRoutes().then((asyncRoutes) => {
                    const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
                    setRoutes(accessedRoutes);
                    resolve(accessedRoutes);
                })
                .catch((error) => {
                    reject(error);
                });
        });
    }
    return { routes, generateRoutes }
})
export function usePermissionStoreHook() {
    return usePermissionStore(store);
}
//layout/index.vue
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { Layout, LayoutSider, LayoutContent, LayoutFooter } from 'ant-design-vue';
import Sidebar from "./Sidebar/index.vue";
import Main from "./main.vue";
const contentStyle: CSSProperties = {
  textAlign: 'center',
  minHeight: 120,
  lineHeight: '120px',
  color: '#fff',
  backgroundColor: '#fff',
};

const siderStyle: CSSProperties = {
  textAlign: 'center',
  lineHeight: '120px',
  color: '#fff',
  backgroundColor: '#888',
};

const footerStyle: CSSProperties = {
  textAlign: 'center',
  color: '#fff',
  backgroundColor: '#999',
};
</script>
<template>
    <Layout>
        <Layout>
            <LayoutSider :style="siderStyle">
                <!--左侧动态路由-->
                <Sidebar class="sidebar-container" />
            </LayoutSider>
            <LayoutContent :style="contentStyle">
                <!--右侧内容-->
                <Main />
            </LayoutContent>
        </Layout>
        <LayoutFooter :style="footerStyle">Footer</LayoutFooter>
    </Layout>
</template>
//./Sidebar/index.vue 左侧动态路由
<script lang="ts" setup>
import { ref } from 'vue';
import { Menu, MenuItem, SubMenu } from 'ant-design-vue';
import type { MenuTheme, MenuProps, ItemType } from 'ant-design-vue';
import { RouteRecordRaw } from "vue-router";
import { usePermissionStore } from "@/store/modules/permission";
import SiderbarItem from './SidebarItem.vue'
const permissionStore = usePermissionStore();
const theme = ref<MenuTheme>('dark');
const selectedKeys = ref(['1']);
const openKeys = ref(['sub1']);
console.log(permissionStore.routes, 40)
const handleClick: MenuProps['onClick'] = e => {
  console.log('click', e);
};
const onlyOneChild = ref(); // 临时变量,唯一子路由
function hasOneShowingChild(
    children: RouteRecordRaw[] = [],
    parent: RouteRecordRaw
) {
    // 子路由集合
    const showingChildren = children.filter((route: RouteRecordRaw) => {
            if (route.meta?.hidden) {
            // 过滤不显示的子路由
            return false;
        } else {
            route.meta!.hidden = false;
            // 临时变量(多个子路由 onlyOneChild 变量是用不上的)
            onlyOneChild.value = route;
            return true;
        }
    });

    // 如果没有子路由,显示父级路由
    if (showingChildren.length === 0) {
        onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
        return true;
    }
    return false;
}
</script>
<template>
    <div class="menu-wrap">
        <Menu
            v-model:openKeys="openKeys"
            v-model:selectedKeys="selectedKeys"
            :theme="theme"
            mode="inline"
            @click="handleClick"
        >
            <template v-for="item in permissionStore.routes" :key="item.name">
                <template v-if="!item.meta || !item.meta.hidden">
                    <!--没有child,子路由-->
                    <template 
                        v-if="
                            hasOneShowingChild(item.children, item as RouteRecordRaw) &&
                            (!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
                            !item.meta?.alwaysShow
                        "
                    > 
                        <MenuItem :key="item.name">
                            <PieChartOutlined />
                            <span>{{ item.meta?.title }}</span>
                        </MenuItem>
                    </template>
                    <template v-else> 
                        <SiderbarItem :menuInfo="item" :key="item.name" />
                    </template>
                </template>
                
            </template>
        </Menu>
    </div>
</template>
//./SidebarItem.vue
<script lang="ts" setup>
import { ref } from 'vue';
import { isExternal } from "@/utils/index";
import { Menu,MenuItem, SubMenu } from 'ant-design-vue';
import { useRouter, RouteRecordRaw } from "vue-router";
import path from "path-browserify";
const props= defineProps({
    ...Menu.SubMenu.props,
    menuInfo: {
        type: Object,
    },
});
const router = useRouter();
function resolvePath(route: RouteRecordRaw) {
    let fullPath
    // 完整路径(/system/user) = 父级路径(/system) + 路由路径(user)
    fullPath = path.resolve(props.menuInfo.path, route.path);
    return fullPath;
}
function hasOneShowingChild(parent: RouteRecordRaw) {
    if (parent.meta?.hidden) {// 过滤不显示的子路由
        return false;
    } else {
        return true;
    }
    return false;
}

</script>
<template>
    <SubMenu :key="menuInfo?.name" v-bind="$attrs" >
        <template #title>
            <span>
                <span>{{ menuInfo?.meta.title }}</span>
            </span>
        </template> 
        <template v-for="(item) in menuInfo.children" :key="item.name">
            <template 
                v-if="
                    hasOneShowingChild(item as RouteRecordRaw)
                "
            >
                <MenuItem :key="item.name" >
                    <routerLink :to="resolvePath(item)">
                        {{ menuInfo.path + '/' + item.path }}
                        {{ item.meta.title }}
                    </routerLink>
                </MenuItem>
            </template>
            <template v-else>
                <!--递归展示children-->
                <SiderbarItem :menuInfo="item" :key="item.name" />
            </template>
        </template> 
    </SubMenu>
</template>
//右侧main.vue
<script setup lang="ts">
import { useTagsViewStore } from "@/store/modules/tagsView";
const tagsViewStore = useTagsViewStore();
</script>
<template>
    <div class="main-container">
        <section class="app-main">
            <router-view>
                <template #default="{ Component, route }">
                        <component :is="Component" :key="route.fullPath" />
                </template>
            </router-view>
        </section>
    </div>
</template>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值