实现动态侧边菜单,包括多级菜单方案:
方案一、使用 VueRouter 的动态路由,遍历路由信息动态构建菜单。
优势:熟悉 Vue 路由的情况下,这种方式可能构建起来会省部分代码。
劣势:路由信息不可见,对于刚接手的开发者没有后端服务的情况下,无法摸索精确找 url 对应的页面,构建多级菜单时需要增加一个空页面作为子路由的载体,页面结构变复杂。
根据后端构建动态路由有点难度,如构建 children: [{ path: '', component: () => import('pages/IndexPage.vue') }]
类似这样的代码。
方案二、单独构建菜单树,使用单独的一个菜单树结构来构建菜单。
单独构建菜单树,使用单独的一个菜单树结构来构建菜单,使用 url 或者 name 来匹配对应的路由信息。
优势:原始路由清晰,易于调试,多级菜单不需要空页面作为路由载体,等等
劣势:维护菜单树和路由树的对应关系时代码可能有点复杂,
这里笔者是用的第二种方式。
思路分析
第一,我们需要构建一个菜单树,怎么构建树呢,我们需要子节点包含父节点信息的平级结构,然后把它转成树结构,只有子节点包含父节点的信息我们才能构建树结构,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| { id: 'main2', pid: '0', name: '主页2', path: '/index', icon: 'home', auth: '23', }, { id: '1', pid: '0', name: '系统管理', path: '', icon: 'settings_applications', auth: '23', }, { id: '1-1', pid: '1', name: '菜单管理', path: '/sys-menu', icon: 'menu', auth: '23', },
|
上面这样的平级数据组成树结构应该是这样的
了解菜单数据的组成过后,开始编写处理菜单树的代码
在stores
文件夹下新建layout-store.ts
文件,编写如下代码,需要熟悉pinia
,
1 2 3 4 5 6 7
| import { defineStore } from "pinia";
export const useLayoutStore = defineStore("layout", { state: () => ({}), getters: {}, actions: {}, });
|
定义菜单类型,在src
下新建types
目录,之后新建base.d.ts
文件,编写如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| declare namespace WJ { interface MenuItem { id: string; pid: string; name: string; path: string; icon: string; auth: string; sort: number; children?: Array<WJ.MenuItem>; } }
|
在 router 文件夹下新建my-router.ts
文件,编写如下内容,此文件内容就是一会需要展示的菜单原始数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export default [ { id: "1", pid: "0", name: "系统管理", path: "", icon: "settings_applications", sort: 0, }, { id: "1-1", pid: "1", name: "菜单管理", path: "/sys-menu", icon: "menu", sort: -1, }, { id: "1-2", pid: "1", name: "用户管理", path: "/sys-user", icon: "person", sort: -2, }, ];
|
修改layout-store.ts
文件,编写平级菜单结构转树结构的方法
编写过后完整代码如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import { store } from "quasar/wrappers"; import { defineStore } from "pinia";
interface LayoutType { menuList: Array<WJ.MenuItem>; } export const useLayoutStore = defineStore("layout", { state: (): LayoutType => ({ menuList: [], }), getters: { menuIdMap: (state): Map<string, WJ.MenuItem> => { const map = new Map<string, WJ.MenuItem>(); state.menuList.forEach((item) => { map.set(item.id, item); }); return map; }, menuTree(state): Array<WJ.MenuItem> { const res: Array<WJ.MenuItem> = []; state.menuList.forEach((item) => { const parent = this.menuIdMap.get(item.pid); if (parent) { const pc = parent.children || (parent.children = []); pc.indexOf(item) === -1 ? pc.push(item) : ""; pc.sort((a, b) => a.sort - b.sort); } else { res.push(item); } }); res.sort((a, b) => a.sort - b.sort); return res; }, }, actions: {}, });
|
这里菜单数据的代码就写好了,接下来编写页面展示。
在layouts
文件夹下新建目录comp
,在comp
目录新建文件LeftDrawer.vue
作为左边侧边栏内容组件,
LeftDrawer.vue
组件代码如下
1 2 3 4 5 6 7 8 9
| <template> <div> <h3>这是侧边菜单</h3> </div> </template>
<script setup lang="ts"></script>
<style scoped></style>
|
在MainLayout.vue
文件中引入LeftDrawer.vue
组件,并替换q-drawer
的标签中的q-list
标签,然后精简代码,编辑过后MainLayout.vue
代码如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <q-layout view="lHh Lpr lFf"> <q-header elevated> <q-toolbar> <q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
<q-toolbar-title> Quasar App </q-toolbar-title>
<div>Quasar v{{ $q.version }}</div> </q-toolbar> </q-header>
<q-drawer v-model="leftDrawerOpen" show-if-above bordered> <LeftDrawer></LeftDrawer> </q-drawer>
<q-page-container> <router-view /> </q-page-container> </q-layout> </template>
<script setup lang="ts"> import { ref } from "vue"; import LeftDrawer from "./comp/LeftDrawer.vue";
const leftDrawerOpen = ref(false);
function toggleLeftDrawer() { leftDrawerOpen.value = !leftDrawerOpen.value; } </script>
|
运行项目,如下图所示
说明组件引入成功,然后在comp
文件夹下新建MenuItem.vue
文件,编写如下内容代码,此代码有点复杂,用到了q-list
,q-expansion-item
组件,如果不熟悉组件使用方法,可查看官方文档学习使用
MenuItem.vue
代码如下,我尽可能的多写注释,便于理解,如果没看懂,可以把代码拉到本地自己琢磨琢磨。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <template v-for="item in menuTree" :key="item.id"> <q-item v-if="!item.children" @click="handleClick(item)" clickable> <q-item-section avatar> <q-icon :name="item.icon" /> </q-item-section> <q-item-section> <q-item-label>{{ item.name }}</q-item-label> </q-item-section> </q-item> <q-expansion-item :icon="item.icon" :label="item.name" :content-inset-level="0.2" v-else > <!-- 如果包含子集。则递归调用此组件 --> <MenuItem :menu-tree="item.children"></MenuItem> </q-expansion-item> </template> </template>
<script setup lang="ts"> import { useRouter } from "vue-router";
interface Props { // 菜单树 menuTree: Array<WJ.MenuItem>; } withDefaults(defineProps<Props>(), {}); const router = useRouter();
/* 处理点击事件 */ const handleClick = (item: WJ.MenuItem) => { router.push(item.path); }; </script>
<style scoped></style>
|
编写完成过后,修改LeftDrawer.vue
文件,要在此文件中引入MenuItem.vue
组件,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <q-scroll-area class="fit"> <q-list bordered> <!-- 主页 --> <q-item clickable :active="route.fullPath === layout.mainPage.path" @click="router.push({ path: layout.mainPage.path })" > <q-item-section avatar> <q-icon :name="layout.mainPage.icon" /> </q-item-section> <q-item-section> <q-item-label>{{ layout.mainPage.name }}</q-item-label> </q-item-section> </q-item> <!-- 菜单组件 --> <menu-item :menu-tree="layout.menuTree"></menu-item> </q-list> </q-scroll-area> </template>
<script setup lang="ts"> import { defineAsyncComponent } from "vue"; import { useLayoutStore } from "src/stores/layout-store"; import { useRoute, useRouter } from "vue-router"; import myRoute from "src/router/my-router"; const MenuItem = defineAsyncComponent(() => import("./MenuItem.vue")); const route = useRoute(); const router = useRouter(); const layout = useLayoutStore(); // 注意这里,赋值菜单数据 layout.menuList = myRoute as Array<WJ.MenuItem>; </script>
<style scoped></style>
|
这个时候不出意外的话,LeftDrawer.vue
内代码会有报错,提示没有mainPage
属性,我们给它加上即可,修改layout-store.ts
内代码,在LayoutType
中加入代码 mainPage: WJ.MenuItem
,加入过后,如下
1 2 3 4 5
| interface LayoutType { menuList: Array<WJ.MenuItem>; mainPage: WJ.MenuItem; }
|
加入过后,代码提示报错,这时候我们再编写代码在state
中添加mainPage
属性,修改过后完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import { defineStore } from "pinia";
interface LayoutType { menuList: Array<WJ.MenuItem>; mainPage: WJ.MenuItem; } export const useLayoutStore = defineStore("layout", { state: (): LayoutType => ({ menuList: [], mainPage: { id: "0", pid: "0", name: "首页", sort: 0, icon: "home", auth: "", path: "/", }, }), getters: { menuIdMap: (state): Map<string, WJ.MenuItem> => { const map = new Map<string, WJ.MenuItem>(); state.menuList.forEach((item) => { map.set(item.id, item); }); return map; }, menuTree(state): Array<WJ.MenuItem> { const res: Array<WJ.MenuItem> = []; state.menuList.forEach((item) => { const parent = this.menuIdMap.get(item.pid); if (parent) { const pc = parent.children || (parent.children = []); pc.indexOf(item) === -1 ? pc.push(item) : ""; pc.sort((a, b) => a.sort - b.sort); } else { res.push(item); } }); res.sort((a, b) => a.sort - b.sort); return res; }, }, actions: {}, });
|
重启项目,可以看到,我们编写的动态菜单已经加载出来了,
至此我们的动态菜单的展示已经编写完成,不过点击你会发现会跳转到 404 页面,因为我们并没有编写对应的路由配置,接下来我们将优化这个动态菜单功能,如,菜单高亮,支持外部链接,支持内部打开外部链接等等……..
动态菜单到此编写完成。
封