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

科技   科技   2024-10-03 08:11   河南  
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的路由中,其实这是一个父级路由,所有的子页面子路由都是基于这个母版页面做的渲染:
src\router\modules\dynamicRouter.ts文件中:
router.addRoute("layout", item as unknown as RouteRecordRaw);

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

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

<!-- 💥 这里是一次性加载 LayoutComponents --><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<LayoutType, Component> = { 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组件。

新建文件src\layouts\LayoutVertical\index.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>ToolBarLeft</div>
<div>ToolBarRight</div>
</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<GlobalState>) { this.$patch({ [args[0]]: args[1] }); } }});


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


3、Main组件渲染页面内容



新建文件src\layouts\components\Main\index.vue,增加内容

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

<template>  <div>tabs</div>  <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>footer</div>  </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菜单列表传给自定义菜单组件中,新建组件src\layouts\components\Menu\SubMenu.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";
defineProps<{ menuList: 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和面包屑,敬请期待。

BCVP代码创新社
专注于 NetCore 相关技术栈的推广,致力于前后端之间的完全分离,从壹开始,让每一个程序员都能从这里学有所成。
 最新文章