放假不停歇,趁着假期学习下VUE3相关的内容,一方面是自己保持活力,另一方面也是工作需要,本系列是我的自学教程,如果有从0开始学习VUE3的,可以跟着一起练习下,毕竟前端我也是泥腿子出身,这一系列会使用Vite、TS、Pinia、Element-Plus等新知识点,既是查漏补缺,也是知识分享。
代码地址:
https://github.com/anjoy8/bcvp.vue3.git
这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore。
系列文章:
本文参考的是开源项目
https://gitee.com/HalseySpicy/Geeker-Admin/tree/template
分步骤讲解授权逻辑,今天就开始正式实现动态菜单的页面渲染,并且支持多种自定义布局模式,效果图:
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);
});
};
到这里,基本骨架已经搭建完成,开始实现对布局页面的设计。
从文章开头的效果截图中,可以看处理,页面布局主要是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的数据存储响应式管理,这里就不再赘述了,可以参考第七课的代码。
新建文件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等其他逻辑,后边的文章会解释的到。
终于到了动态菜单权限的重点,在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和面包屑,敬请期待。