Vue3 动态侧边栏组件实现
1. 组件模板结构
vue
<template>
<div class="submenu">
<!-- 小屏幕下的菜单切换按钮 -->
<a-button v-if="isSmallScreen" @click="toggleSidebar" v-model="isSidebarVisible">亖</a-button>
<!-- 大屏幕下的固定侧边栏 -->
<a-layout-sider v-if="!isSmallScreen" :style="{ siderStyle }">
<!-- 主菜单组件 -->
<a-menu @select="onSelect"
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
mode="inline"
:style="{ height: '100%' }">
<!-- 遍历过滤后的菜单数据 -->
<template v-for="menu in filteredMenu" :key="menu.key">
<!-- 处理带有子菜单的项目 -->
<template v-if="menu.children">
<a-sub-menu :key="menu.key">
<template #title>
<span>
<component :is="menu.icon" />
{{ menu.title }}
</span>
</template>
<!-- 递归处理子菜单 -->
<template v-for="child in menu.children" :key="child.key">
<!-- 处理二级子菜单 -->
<template v-if="child.children">
<a-sub-menu :key="child.key">
<template #title>
<span>
<component :is="child.icon" />
{{ child.title }}
</span>
</template>
<!-- 渲染三级菜单项 -->
<a-menu-item v-for="subChild in child.children" :key="subChild.key">
{{ subChild.title }}
</a-menu-item>
</a-sub-menu>
</template>
<!-- 渲染二级菜单项 -->
<a-menu-item v-else :key="child.key">
<component :is="child.icon" v-if="child.icon" />
{{ child.title }}
</a-menu-item>
</template>
</a-sub-menu>
</template>
<!-- 渲染一级菜单项 -->
<a-menu-item v-else :key="menu.key">
<component :is="menu.icon" />
{{ menu.title }}
</a-menu-item>
</template>
</a-menu>
</a-layout-sider>
<!-- 小屏幕下的抽屉式菜单 -->
<a-drawer v-if="isSmallScreen"
v-model:open="isSidebarVisible"
placement="left"
:closable="false"
:maskClosable="true"
:width="256">
<!-- 抽屉内的菜单内容与主菜单相同 -->
<!-- ... 与上面的菜单结构相同 ... -->
</a-drawer>
</div>
</template>
<script setup>
// 导入必要的依赖
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import { useRouter } from 'vue-router';
import { UserOutlined, LaptopOutlined, NotificationOutlined } from '@ant-design/icons-vue';
import { roleRoutes } from '@/router/permission';
// 侧边栏样式配置
const siderStyle = {
textAlign: 'center',
lineHeight: '120px',
color: '#fff',
backgroundColor: '#3ba0e9',
};
// 状态管理相关变量
const selectedKeys2 = ref(JSON.parse(localStorage.getItem('selectedKeys')) || ['1']); // 当前选中的菜单项,从本地存储获取或默认为['1']
const openKeys = ref(JSON.parse(localStorage.getItem('openKeys')) || ['sub1']); // 当前展开的子菜单,从本地存储获取或默认为['sub1']
const router = useRouter(); // 路由实例
const isSidebarVisible = ref(false); // 控制侧边栏在小屏幕下的显示状态
const isSmallScreen = ref(false); // 是否为小屏幕设备
// 菜单数据配置
const menuData = [
{
key: 'sub1', // 菜单项唯一标识
title: '公司信息', // 菜单项标题
icon: UserOutlined, // 菜单项图标组件
children: [ // 子菜单配置
{ key: '1', title: '公司证照', path: '/CompanyLicense' },
{ key: '2', title: '修改公司信息', path: '/ModifyCompany' },
{ key: '3', title: '用户管理', path: '/UserManagement' }
]
},
// ... 其他菜单配置 ...
];
// 获取并处理用户角色信息
const userRole = ref((() => {
const userInfo = localStorage.getItem('userInfo');
if (!userInfo) return [];
const parsed = JSON.parse(userInfo);
// 处理不同格式的用户信息,支持字符串、数组等格式
if (typeof parsed === 'string') return parsed.split(',');
if (Array.isArray(parsed)) return parsed;
// 如果解析结果不是字符串或数组,则返回一个包含解析结果的数组
return [parsed];
})());
// 根据用户角色过滤菜单的计算属性
const filteredMenu = computed(() => {
// 确保角色信息为数组格式
const roles = Array.isArray(userRole.value) ? userRole.value : [userRole.value];
// 获取所有角色的访问权限路径
const accessPaths = roles.reduce((acc, role) => {
const paths = roleRoutes[role];
// 如果是全部权限或使用排除法,则返回'all'
if (paths === 'all' || paths?.exclude) return 'all';
// 将当前角色的可访问路径合并到累加器中
return acc.concat(paths || []);
}, []);
// 如果可访问所有路径,直接返回完整菜单
if (accessPaths === 'all') {
console.log('所有菜单:', menuData);
return menuData;
}
// 递归过滤菜单项
const filterMenuItems = (items) => {
return items.map(menu => {
if (menu.children) {
// 处理包含子菜单的情况
const filteredChildren = filterMenuItems(menu.children);
// 只保留有权限访问的子菜单,如果没有可访问的子菜单则过滤掉整个父菜单
return filteredChildren.length > 0 ? { ...menu, children: filteredChildren } : null;
}
// 如果是叶子节点,检查路径是否在可访问路径中
return accessPaths.includes(menu.path) ? menu : null;
}).filter(Boolean); // 移除空值
};
// 过滤整个菜单树
const filteredRoutes = filterMenuItems(menuData);
console.log('过滤后的路由:', filteredRoutes);
return filteredRoutes;
});
// 响应式布局处理函数
const handleResize = () => {
isSmallScreen.value = window.innerWidth <= 768; // 根据窗口宽度判断是否为小屏幕
if (!isSmallScreen.value) {
isSidebarVisible.value = false; // 大屏幕时关闭抽屉式菜单
}
};
// 生命周期钩子
onMounted(() => {
handleResize(); // 初始化屏幕尺寸判断
window.addEventListener('resize', handleResize); // 添加窗口大小变化监听
// 根据当前路由设置选中的菜单项
const currentRoute = router.currentRoute.value;
if (currentRoute.meta?.key) {
// 设置当前选中的菜单项
selectedKeys2.value = [currentRoute.meta.key];
}
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize); // 组件卸载时移除事件监听
});
// 监听器设置
watch(selectedKeys2, (newVal) => {
// 将选中的菜单项保存到本地存储
localStorage.setItem('selectedKeys', JSON.stringify(newVal));
}, { deep: true });
watch(openKeys, (newVal) => {
// 将展开的子菜单保存到本地存储
localStorage.setItem('openKeys', JSON.stringify(newVal));
}, { deep: true });
watch(() => router.currentRoute.value, (newRoute) => {
// 路由变化时更新选中的菜单项
if (newRoute.meta?.key) {
selectedKeys2.value = [newRoute.meta.key];
}
}, { immediate: true });
// 事件处理函数
const onSelect = ({ key }) => {
// 菜单项选中时的处理
const route = router.getRoutes().find(route => route.meta?.key === key);
if (route) {
router.push(route.path); // 跳转到对应路由
selectedKeys2.value = [key]; // 更新选中状态
}
};
const toggleSidebar = () => {
// 切换侧边栏显示状态
isSidebarVisible.value = !isSidebarVisible.value;
};
</script>
2. 功能说明
2.1 核心功能
- 动态菜单渲染
- 权限控制
- 响应式布局
- 菜单状态持久化
2.2 实现特点
- 支持多级菜单
- 基于角色的权限控制
- 自适应布局(PC/移动端)
- 状态持久化存储
2.3 性能优化
- 使用计算属性缓存过滤后的菜单
- 组件卸载时清理事件监听
- 菜单状态本地存储
3. 使用说明
3.1 配置要求
- 需要配置 roleRoutes 权限映射
- 需要在路由 meta 中配置对应的 key
- 需要在 localStorage 中存储用户信息
3.2 注意事项
- 菜单配置需要符合特定格式
- 权限控制需要与后端配合
- 响应式布局断点可根据需求调整