导航菜单往往是管理后台必不可少的一个功能组件。菜单组件的编写重要的还是逻辑,逻辑清晰无论是什么UI库做起来都不难,大同小异罢了。
首先处理好路由数据
// route/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import Layout from '@/layout/index.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'layout',
component: Layout,
redirect: '/home',
children: [
{
path: '/home',
name: 'home',
meta: { title: 'home', icon: 'home' },
component: () => import('@/views/home/index.vue'),
},
{
path: '/form',
name: 'form',
meta: { title: 'form', icon: 'form' },
component: () => import('@/views/form/index.vue'),
},
{
path: '/table',
name: 'table',
meta: { title: 'table', icon: 'table' },
component: () => import('@/views/table/index.vue'),
redirect: '/table/first',
children: [
{
path: '/table/first',
name: 'tableFirst',
meta: { title: 'table', subTitle: 1 },
component: () => import('@/views/table/first.vue'),
},
{
path: '/table/secound',
name: 'tableSecound',
meta: { title: 'table', subTitle: 2, isHidden: true },
component: () => import('@/views/table/secound.vue'),
},
],
},
],
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('@/views/404.vue'),
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
在上面的路由ts文件中,我们需要用来做菜单的只有path为 ‘/’,name为’layout’的路由项中children字段的内容,所以在渲染之前我们要先进行过滤(动态路由的思路也差不多)
// sideBar.vue
<template>
<!-- 这里的class="h-100% w-250px" 为Unocss写法,具体可以自行百度了解下原子化css -->
<a-menu
v-model:openKeys="menuState.openKeys"
v-model:selectedKeys="menuState.selectedKeys"
class="h-100% w-250px"
mode="inline"
@click="handleClickMenu"
>
<MenuItem :routes="routes" v-if="routes" />
</a-menu>
</template>
<script setup lang="ts">
import { MenuProps } from 'ant-design-vue/es';
import MenuItem from './menuItem.vue';
const route = useRoute();
const router = useRouter();
// 过滤出需要添加到菜单栏中的路由
const routes = computed(() => {
// 取出meta中isHidden不为true的路由(isHidden为true时不在导航菜单中渲染)
function _noHidden(_routes: RouteRecordRaw[]) {
const filterRoute: RouteRecordRaw[] = [];
_routes.forEach((_route) => {
if (!_route?.meta?.isHidden) {
if (!_route.children || _route.children.length === 0) {
filterRoute.push(_route);
} else {
filterRoute.push({
..._route,
children: _noHidden(_route.children)! || [],
});
}
}
});
return filterRoute;
}
return _noHidden(
// 这里先取出name为 layout 的children中路由数据
router.getRoutes().find((item) => item.name === 'layout')!.children
);
});
const menuState = reactive<{ selectedKeys: string[]; openKeys: string[] }>({
selectedKeys: [],
openKeys: [],
});
const handleClickMenu: MenuProps['onClick'] = (menuInfo) => {
router.push({ path: menuInfo.key as string });
};
watchEffect(() => {
// 路由菜单改变时 更新 selectedKeys 跟 openKeys 的值 实现菜单同步展开/高亮
menuState.selectedKeys = [route.path];
const keyList: any = route.path.slice(1).split('/');
if (keyList.length === 1) {
menuState.openKeys = [''];
return;
}
for (let index = 0; index < route.path.length; index++) {
if (route.path[index] === '/') {
menuState.openKeys.push(route.path.substr(0, index));
}
}
menuState.openKeys.shift();
});
</script>
<style lang="scss" scoped></style>
// menuItem.vue
<template>
<template v-for="item in routes" :key="item.path">
<a-menu-item
:key="item.path"
v-if="!item.children || item.children.length === 0"
>
<template #icon>
<!-- svg-icon 组件为动态使用svg图标封装的组件。可注释,具体请查看项目源码 -->
<svg-icon
class="text-14px mr-4px"
v-if="item.meta && item.meta.icon"
:name="item.meta.icon as string"
/>
</template>
<span>{{ item.meta?.title }}</span>
</a-menu-item>
<template v-else>
<a-sub-menu :key="item.path">
<template #icon>
<svg-icon
class="text-14px mr-4px"
v-if="item.meta && item.meta.icon"
:name="item.meta.icon as string"
/>
</template>
<template #title>
<span>{{ item.meta?.title }}</span>
</template>
<menuItem :routes="item.children" />
</a-sub-menu>
</template>
</template>
</template>
<script setup lang="ts">
// defineOptions为vue3.3新增特性 用法见vue官网。在这里主要是为了给递归组件命名
defineOptions({
name: 'menuItem',
});
defineProps<{ routes: Array<RouteRecordRaw> }>();
</script>
最后贴上源码地址,有用的话记得给个star哦~ 项目源码在线地址