从0到一个前端后台管理框架(三)
文章列表:
实现方案:
方案一、使用VueRouter的动态路由,遍历路由信息动态构建菜单。
优势:熟悉Vue路由的情况下,这种方式可能构建起来会省部分代码。
劣势:路由信息不可见,对于刚接手的开发者没有后端服务的情况下,无法摸索精确找url对应的页面,构建多级菜单时需要增加一个空页面作为子路由的载体,页面结构变复杂。
根据后端构建动态路由有点难度,如构建 children: [{ path: '', component: () => import('pages/IndexPage.vue') }]
类似这样的代码。
方案二、单独构建菜单树,使用单独的一个菜单树结构来构建菜单。
单独构建菜单树,使用单独的一个菜单树结构来构建菜单,使用url或者name来匹配对应的路由信息。
优势:原始路由清晰,易于调试,多级菜单不需要空页面作为路由载体,等等
劣势:维护菜单树和路由树的对应关系时代码可能有点复杂,
这里笔者是用的第二种方式。
思路分析
第一,我们需要构建一个菜单树,怎么构建树呢,我们需要子节点包含父节点信息的平级结构,然后把它转成树结构,只有子节点包含父节点的信息我们才能构建树结构,
{
id: 'main2', // 节点的Id信息
pid: '0', // 父节点Id信息,为0表示为根节点
name: '主页2', // 菜单名称
path: '/index', // 菜单url,对应路由信息
icon: 'home', // 菜单图标
auth: '23', // 权限标识,暂定,鉴权也可以使用url作为鉴权标识。
},
{
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
,
import { defineStore } from 'pinia';
export const useLayoutStore = defineStore('layout', {
state: () => ({
}), // 存储的数据
getters: {
}, // 计算属性
actions: {
}, // 一些操作
});
定义菜单类型,在src
下新建types
目录,之后新建base.d.ts
文件,编写如下代码
declare namespace WJ {
/* 菜单项 */
interface MenuItem {
// 菜单ID
id: string;
// 菜单父级Id根节点使用"0"
pid: string;
// 菜单名称
name: string;
// 菜单路径
path: string;
// 菜单图标
icon: string;
// 菜单权限编码
auth: string;
// 排序,数字越大越靠后
sort: number;
// 子级别菜单
children?: Array<WJ.MenuItem>;
}
}
在router文件夹下新建my-router.ts
文件,编写如下内容,此文件内容就是一会需要展示的菜单原始数据。
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
文件,编写平级菜单结构转树结构的方法
编写过后完整代码如下,
import { store } from 'quasar/wrappers';
import { defineStore } from 'pinia';
// 类型定义
interface LayoutType {
// 平级菜单数据
menuList: Array<WJ.MenuItem>
}
export const useLayoutStore = defineStore('layout', {
state: (): LayoutType => ({
menuList: []
}),
getters: {
// ID:WJ.MenuItem,Map 结构,Key为菜单ID
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
组件代码如下
<template>
<div>
<h3>这是侧边菜单</h3>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
在MainLayout.vue
文件中引入LeftDrawer.vue
组件,并替换q-drawer
的标签中的q-list
标签,然后精简代码,编辑过后MainLayout.vue
代码如下所示
<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
代码如下,我尽可能的多写注释,便于理解,如果没看懂,可以把代码拉到本地自己琢磨琢磨。
<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
组件,代码如下
<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
,加入过后,如下
interface LayoutType {
// 平级菜单数据
menuList: Array<WJ.MenuItem>
mainPage: WJ.MenuItem
}
加入过后,代码提示报错,这时候我们再编写代码在state
中添加mainPage
属性,修改过后完整代码如下
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: {
// ID:WJ.MenuItem
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页面,因为我们并没有编写对应的路由配置,接下来我们将优化这个动态菜单功能,如,菜单高亮,支持外部链接,支持内部打开外部链接等等……..
动态菜单到此编写完成。