十、Vue3+Ts+Vite+AntdUI构建后台基础模板——动态生成菜单栏

420 阅读2分钟

一、新增页面

  1. 新增三级菜单页
  2. @/views/Menu/Menu2目录下新增Menu2-1目录
  3. @/views/Menu/Menu2/Menu2-1目录新增index.vue
  4. @/views/Menu/Menu2/Menu2-1/index.vue
<template>
    <h1>Menu2-1页面!</h1>
</template>

<script lang="ts" setup></script>
  1. 修改Menu2页面
  2. @/views/Menu/Menu2/index.vue
<template>
    <router-view />
</template>

二、修改Mock动态路由数据

  1. mock/data/routes.ts
interface routeConfig {
    path: string;
    name: string;
    component?: string;
    redirect?: string;
    meta: {
        title: string;
        layout: boolean;
        menu: boolean;
        icon: string;
        sort?: number;
    };
    children?: routeConfig[];
}

const rootRoutes: routeConfig[] = [
    {
        path: '/root',
        name: 'root',
        component: 'views/Root/index.vue',
        meta: {
            title: 'Root权限页',
            layout: true,
            menu: true,
            icon: 'KeyOutlined',
            sort: 2,
        },
    },
    {
        path: '/menu',
        name: 'menu',
        component: 'views/Menu/index.vue',
        redirect: '/menu/menu1',
        meta: {
            title: '多级菜单页',
            layout: true,
            menu: true,
            icon: 'MenuOutlined',
            sort: 3,
        },
        children: [
            {
                path: 'menu1',
                name: 'menu1',
                component: 'views/Menu/Menu1/index.vue',
                meta: {
                    title: 'Menu1页',
                    layout: true,
                    menu: true,
                    icon: 'MenuOutlined',
                    sort: 1,
                },
            },
            {
                path: 'menu2',
                name: 'menu2',
                component: 'views/Menu/Menu2/index.vue',
                redirect: 'menu2/menu2-1',
                meta: {
                    title: 'Menu2页',
                    layout: true,
                    menu: true,
                    icon: 'MenuOutlined',
                    sort: 2,
                },
                children: [
                    {
                        path: 'menu2-1',
                        name: 'menu2-1',
                        component: 'views/Menu/Menu2/Menu2-1/index.vue',
                        meta: {
                            title: 'Menu2-1页',
                            layout: true,
                            menu: true,
                            icon: 'MenuOutlined',
                            sort: 3,
                        },
                    },
                ],
            },
        ],
    },
];

const adminRoutes: routeConfig[] = [
    {
        path: '/admin',
        name: 'admin',
        component: 'views/Admin/index.vue',
        meta: {
            title: 'Admin权限页',
            layout: true,
            menu: true,
            icon: 'KeyOutlined',
            sort: 2,
        },
    },
];

const userRoutes: routeConfig[] = [];

export { rootRoutes, adminRoutes, userRoutes, routeConfig };

三、修改静态路由数据

  1. @/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import { useUserStore } from '@/store';
import { addRoutes, clerRoutes } from '@/util/anyncRoutes';

const routes = [
    {
        path: '/',
        name: 'home',
        component: () => import('@/views/Home/index.vue'),
        meta: {
            title: '首页',
            layout: true,
            menu: true,
            icon: 'DashboardOutlined',
            sort: 1,
        },
    },
    {
        path: '/login',
        name: 'Login',
        component: () => import('@/views/Login/index.vue'),
        meta: {
            title: '登录',
            layout: false,
            menu: false,
            icon: 'LoginOutlined',
        },
    },
];

const router = createRouter({
    history: createWebHashHistory(),
    routes,
});

let registerRouteFresh = true;
router.beforeEach((to, from, next) => {
    const userStore = useUserStore();
    if (to.meta.title) {
        document.title = to.meta.title as string;
    }
    if (to.name !== 'Login' && !userStore.token) {
        next({ name: 'Login' });
    } else {
        if (to.name === 'Login') {
            userStore.clearToken();
            userStore.clearUser();
            clerRoutes(userStore, router);
        }
        if (!from.name && registerRouteFresh) {
            addRoutes(userStore, router);
            next({ ...to, replace: true });
            registerRouteFresh = false;
        } else {
            next();
        }
    }
});

export default router;
  1. 修改@/util/anyncRoutes.ts
......
// 添加动态路由
const addRoutes = (userStore, router) => {
    ......
    router.addRoute({
        path: '/:pathMatch(.*)*',
        name: '404',
        component: () => import('@/views/404/index.vue'),
        meta: {
            title: '无法找到该页面',
            layout: false,
            menu: false,
        },
    });
};
......

四、修改网站标题

  1. index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>后台基础模板</title>
</head>

<body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
</body>

</html>

五、新建图标动态引入的方法

  1. @/util目录下新建icon.ts文件
  2. @/util/icon.ts
import { createVNode } from 'vue';
import { DashboardOutlined, MenuOutlined, KeyOutlined } from '@ant-design/icons-vue';
// import  * as $Icon from '@ant-design/icons-vue';
const $Icon = {
    DashboardOutlined,
    MenuOutlined,
    KeyOutlined,
};

const Icon = (props: { icon: string }) => {
    const { icon } = props;
    const antIcon: { [key: string]: any } = $Icon;
    return createVNode(antIcon[icon]);
};

export default Icon;

六、抽离菜单模块

  1. @/layout/modules目录下新建文件Menu.vue
<template>
    <a-menu theme="dark" mode="inline" v-model:selectedKeys="routeKey" @select="menuSelect">
        <template v-for="item in routesData">
            <template v-if="item.meta.menu">
                <template v-if="item.children && item.children.length > 0">
                    <sub-menu :routes-sub-data="item" :key="item.name" />
                </template>
                <template v-else>
                    <a-menu-item :key="item.name">
                        <template #icon v-if="item.meta.icon">
                            <Icon :icon="item.meta.icon" />
                        </template>
                        <span>{{ item.meta.title }}</span>
                    </a-menu-item>
                </template>
            </template>
        </template>
    </a-menu>
</template>

<script lang="ts" setup>
import { ref, onMounted, watch } from 'vue';
import { useRouter, useRoute, RouteRecordName } from 'vue-router';
import Icon from '@/util/icon';
import SubMenu from './SubMenu.vue';

const route = useRoute();
const router = useRouter();

const routeKey = ref<(RouteRecordName | null | undefined)[]>([]);
const routesData = ref<any[]>([]);

const menuSelect = (item: { key: string }) => {
    router.push({ name: item.key });
};

const setSortRouter = (tree: any[]) => {
    tree.sort((aItem, bItem) => {
        return aItem.meta.sort - bItem.meta.sort;
    });
    return tree.map((node) => {
        const tempNode = node;
        if (tempNode.children && tempNode.children.length > 0) {
            setSortRouter(tempNode.children);
        }
        return tempNode;
    });
};

const getMenu = () => {
    let menuRouters: any[] = router.getRoutes();
    menuRouters = menuRouters.filter((item: any) => {
        return item.meta.menu && item.path.split('/').length === 2;
    });
    setSortRouter(menuRouters);
    routesData.value = menuRouters;
    routeKey.value = [route.name];
};

onMounted(() => {
    getMenu();
});

watch(
    () => route.name,
    (val) => {
        if (val) {
            routeKey.value = [route.name];
        }
    }
);
</script>

<style lang="less" scoped></style>

  1. @/layout/modules目录下新建文件SubMenu.vue
<template>
    <a-sub-menu :key="routesSubData.name">
        <template #icon v-if="routesSubData.meta.icon">
            <Icon :icon="routesSubData.meta.icon" />
        </template>
        <template #title>{{ routesSubData.meta.title }}</template>
        <template v-for="item in routesSubData.children">
            <template v-if="item.children && item.children.length > 0">
                <sub-menu :routes-sub-data="item" :key="item.name" />
            </template>
            <template v-else>
                <a-menu-item :key="item.name">
                    <template #icon v-if="item.meta.icon">
                        <Icon :icon="item.meta.icon" />
                    </template>
                    <span>{{ item.meta.title }}</span>
                </a-menu-item>
            </template>
        </template>
    </a-sub-menu>
</template>

<script lang="ts" setup>
import { defineProps } from 'vue';
import Icon from '@/util/icon';

defineProps({
    routesSubData: {
        type: Object,
        default: () => {},
    },
});
</script>

<style lang="less" scoped></style>
  1. @/layout/index.vue
<template>
    <a-layout class="layout">
        <a-layout-sider v-model:collapsed="layoutStore.menuCollapsed" :trigger="null" collapsible>
            <div class="logo" />
            <Menu />
        </a-layout-sider>
        <a-layout>
            <Header />
            <a-layout-content style="margin: 24px 16px; padding: 24px; background: #fff; min-height: 280px">
                <a-spin :spinning="loadingStore.loadingState" :delay="300" size="large">
                    <router-view />
                </a-spin>
            </a-layout-content>
        </a-layout>
    </a-layout>
</template>

<script lang="ts" setup>
import { useLoadingStore, useLayoutStore } from '@/store';
import Menu from './modules/Menu.vue';
import Header from './modules/Header.vue';

const loadingStore = useLoadingStore();
const layoutStore = useLayoutStore();
</script>

<style lang="less" scoped>
......
</style>

七、源代码地址

https://github.com/jiangzetian/vue3-admin-template

八、视频演示及源码

本文演示视频:点击浏览

更多前端内容欢迎关注公众号:天小天个人网