BCVP.VUE3系列第七课:基于布局模式实现动态菜单渲染

艺帆风顺 发布于 2025-04-03 37 次阅读


BCVP 开发者社区出品BCVP V3开发数字化服务化绿色化

放假不停歇,趁着假期学习下VUE3相关的内容,一方面是自己保持活力,另一方面也是工作需要,本系列是我的自学教程,如果有从0开始学习VUE3的,可以跟着一起练习下,毕竟前端我也是泥腿子出身,这一系列会使用Vite、TS、Pinia、Element-Plus等新知识点,既是查漏补缺,也是知识分享。

代码地址:

https://github.com/anjoy8/bcvp.vue3.git

这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore。

系列文章:

第一课:项目初始化与核心知识点说明

第二课:基于泛型基类封装Axios请求

第三课:封装Axios拦截器

第四课:登录页设计

第五课:获取用户信息

第六课:获取动态菜单接口

0、本文介绍

本文参考的是开源项目

https://gitee.com/HalseySpicy/Geeker-Admin/tree/template

分步骤讲解授权逻辑,今天就开始正式实现动态菜单的页面渲染,并且支持多种自定义布局模式,效果图:

1、定义layout模板

本文的设计思路,简单来说是这样的,可以先心里有个谱,再往下看会更容易些:

故事还得回到上篇文章中,我们说到了动态菜单数据,里边有一个逻辑就是把数据添加到一个叫layout的路由中,其实这是一个父级路由,所有的子页面子路由都是基于这个母版页面做的渲染:srcroutermodulesdynamicRouter.ts文件中:

router.addRoute("layout", item as unknown as RouteRecordRaw);

所以就需要一个这样的路由,大概页面逻辑是这样的:

新建一个页面/src/layouts/index.vue:

    template> component :is="LayoutComponents['vertical']" />template>
    script setup lang="ts" name="layout">import { type Component } from "vue";import type { LayoutType } from "@/stores/interface";import LayoutVertical from "./LayoutVertical/index.vue";
    const LayoutComponents: Record = { vertical: LayoutVertical};script>
    style scoped lang="scss">.layout { min-width: 600px;}style>

    这个模板目前只实现了一种布局类型,普通的纵向布局,可以支持自定义扩展,可以实现多种类型,比如横向布局,传统布局和其他各种各样的布局,

    然后在router的index.ts中,将这个页面添加到路由里

    这里顺便把路由过滤器也加上

      /** * @description 路由拦截 beforeEach * */router.beforeEach(async (to, from, next) => { const userStore = useAuthStore(); const authStore = useAuthMenuStore();
      // 2.动态设置标题 const title = 'blogvue3'; document.title = to.meta.title ? `${to.meta.title} - ${title}` : title;
      // 3.判断是访问登陆页,有 Token 就在当前页面,没有 Token 重置路由到登陆页 if (to.path.toLocaleLowerCase() === '/login') { if (userStore.token) return next(from.fullPath); resetRouter(); return next(); }
      // 4.判断访问页面是否在路由白名单地址(静态路由)中,如果存在直接放行 if (["/500"].includes(to.path)) return next();
      // 5.判断是否有 Token,没有重定向到 login 页面 if (!userStore.token) return next({ path: '/login', replace: true });
      // 6.如果没有菜单列表,就重新请求菜单列表并添加动态路由 if (!authStore.authMenuListGet.length) { const userInfoStore = useUserInfoStore(); const menuReq: Menu.MenuRequest = { uid: userInfoStore.user?.uID || '12'}; await initDynamicRouter(menuReq); return next({ ...to, replace: true }); }
      // 7.存储 routerName 做按钮权限筛选 authStore.setRouteName(to.name as string);
      // 8.正常访问页面 next();});
      /** * @description 重置路由 * */export const resetRouter = () => { const authStore = useAuthMenuStore(); authStore.flatMenuListGet.forEach(route => { const { name } = route; if (name && router.hasRoute(name)) router.removeRoute(name); });};

      到这里,基本骨架已经搭建完成,开始实现对布局页面的设计。

      2、设计页面布局

      从文章开头的效果截图中,可以看处理,页面布局主要是7个部分,左侧是1:logo、2:动态菜单,右侧从上往下是3:左侧的bar菜单+面包屑,4:右侧的bar配置菜单+用户信息,下边的5:tabs标签,6:正文内容,以及7:页脚,其中567抽离出main组件。

      新建文件srclayoutsLayoutVerticalindex.vue

        template> el-container class="layout"> el-aside> div class="aside-box" :style="{ width: isCollapse ? '65px' : '210px' }"> div class="logo flx-center"> img class="logo-img" src="@/assets/images/logo.svg" alt="logo" /> span v-show="!isCollapse" class="logo-text">{{ title }}span> div> el-scrollbar> el-menu :router="false" :default-active="activeMenu" :collapse="isCollapse" :unique-opened="accordion" :collapse-transition="false"> SubMenu :menu-list="menuList" /> el-menu> el-scrollbar> div> el-aside> el-container> el-header> div>ToolBarLeftdiv>
        div>ToolBarRightdiv>
        el-header> Main /> el-container> el-container>template>
        script setup lang="ts" name="layoutVertical">import { computed } from "vue";import { useRoute } from "vue-router";import { useAuthMenuStore } from "@/stores/modules/authMenu";import Main from "@/layouts/components/Main/index.vue";import SubMenu from "@/layouts/components/Menu/SubMenu.vue";import { useGlobalStore } from "@/stores/modules/global";
        const title = 'BlogVue3';
        const route = useRoute();const authStore = useAuthMenuStore();const globalStore = useGlobalStore();const accordion = computed(() => globalStore.accordion);const isCollapse = computed(() => globalStore.isCollapse);const menuList = computed(() => authStore.showMenuListGet);const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);script>
        style scoped lang="scss">@import "./index.scss";style>

        本文重点说Main组件,其他的内容,后边的文章会一一介绍

        引入了一个/stores/modules/global.ts的状态管理,主要是提供参数配置

          import { defineStore } from "pinia";import type { GlobalState } from "@/stores/interface";
          export const useGlobalStore = defineStore({ id: "blogvue3-global", // 修改默认值之后,需清除 localStorage 数据 state: (): GlobalState => ({ // 布局模式 (纵向:vertical | 经典:classic | 横向:transverse | 分栏:columns) layout: "vertical", // element 组件大小 assemblySize: "default", // 当前页面是否全屏 maximize: false, // 主题颜色 primary: '#009688', // 深色模式 isDark: false, // 灰色模式 isGrey: false, // 色弱模式 isWeak: false, // 侧边栏反转 asideInverted: false, // 头部反转 headerInverted: false, // 折叠菜单 isCollapse: false, // 菜单手风琴 accordion: true, // 面包屑导航 breadcrumb: true, // 面包屑导航图标 breadcrumbIcon: true, // 标签页 tabs: true, // 标签页图标 tabsIcon: true, // 页脚 footer: true }), getters: {}, actions: { // Set GlobalState setGlobalState(...args: ObjToKeyValArray) { this.$patch({ [args[0]]: args[1] }); } }});

          还有一个keepAlive.ts的状态管理器,实现对keepalive的数据存储响应式管理,这里就不再赘述了,可以参考第七课的代码。

          3、Main组件渲染页面内容

          新建文件srclayoutscomponentsMainindex.vue,增加内容:

          这里只是粘贴了核心逻辑,主要通过 vue的router-view组件,将当前路由页面的内容给视图化出来的作用。

            template> div>tabsdiv> el-main> router-view v-slot="{ Component, route }"> transition appear name="fade-transform" mode="out-in"> keep-alive :include="keepAliveName"> component :is="Component" v-if="isRouterShow" :key="route.fullPath" /> keep-alive> transition> router-view> el-main> el-footer v-if="footer"> div>footerdiv> el-footer>template>
            script setup lang="ts">import { ref, onBeforeUnmount, provide, watch } from "vue";import { storeToRefs } from "pinia";import { useDebounceFn } from "@vueuse/core";import { useKeepAliveStore } from "@/stores/modules/keepAlive";import { useGlobalStore } from "@/stores/modules/global";
            const globalStore = useGlobalStore();const { maximize, isCollapse, layout, tabs, footer } = storeToRefs(globalStore);
            const keepAliveStore = useKeepAliveStore();const { keepAliveName } = storeToRefs(keepAliveStore);
            // 注入刷新页面方法const isRouterShow = ref(true);const refreshCurrentPage = (val: boolean) => (isRouterShow.value = val);provide("refresh", refreshCurrentPage);script>

            其他的逻辑,比如标签tabs和页脚footer等其他逻辑,后边的文章会解释的到。

            4、重点:渲染左侧菜单子组件

            终于到了动态菜单权限的重点,在LayoutVertical中,将menuList菜单列表传给自定义菜单组件中,新建组件srclayoutscomponentsMenuSubMenu.vue,内容如下:

              template> template v-for="subItem in menuList" :key="subItem.name"> el-sub-menu v-if="subItem.children?.length" :index="subItem.name"> template #title> el-icon> component :is="'HomeFilled'">component> el-icon> span class="sle">{{ subItem.meta.title }}span> template> SubMenu :menu-list="subItem.children" /> el-sub-menu> el-menu-item v-else :index="subItem.path" @click="handleClickMenu(subItem)"> el-icon> component :is="'Menu'">component> el-icon> template #title> span class="sle">{{ subItem.meta.title }}span> template> el-menu-item> template>template>
              script setup lang="ts">import { useRouter } from "vue-router";
              definePropsmenuList: Menu.MenuOptions[] }>();
              const router = useRouter();const handleClickMenu = (subItem: Menu.MenuOptions) => { if (subItem.meta.isLink) return window.open(subItem.meta.isLink, "_blank"); router.push(subItem.path);};script>

              其中CSS样式就不粘贴出来了,核心逻辑就是定义了一个组件,实现内部递归,最终效果就出来了,写一个home/index.vue页面,再来个部门管理的空页面,试试路由跳转和页面刷新都没问题

              下篇文章我们继续对页面优化,增加右侧顶部的功能ToolBar和面包屑,敬请期待。