Skip to content

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 核心功能

  1. 动态菜单渲染
  2. 权限控制
  3. 响应式布局
  4. 菜单状态持久化

2.2 实现特点

  1. 支持多级菜单
  2. 基于角色的权限控制
  3. 自适应布局(PC/移动端)
  4. 状态持久化存储

2.3 性能优化

  1. 使用计算属性缓存过滤后的菜单
  2. 组件卸载时清理事件监听
  3. 菜单状态本地存储

3. 使用说明

3.1 配置要求

  1. 需要配置 roleRoutes 权限映射
  2. 需要在路由 meta 中配置对应的 key
  3. 需要在 localStorage 中存储用户信息

3.2 注意事项

  1. 菜单配置需要符合特定格式
  2. 权限控制需要与后端配合
  3. 响应式布局断点可根据需求调整