Skip to content

在前几篇内容中已完成菜单、角色及菜单权限等相关开发,若要在左侧菜单根据用户角色动态展示菜单,需对 Sidebar 中的相关数据进行修改。鉴于其他相关方法及类型已在前文实现,本文不再重复阐述。

修改 Sidebar 组件

在 src/layout/components/Sidebar/index.vue 中修改路由数据来源,更新为动态生成的菜单,代码如下:

html
//src/layout/components/Sidebar/index.vue
<template>
  <div>
    <!-- 侧边栏Logo组件,根据配置决定是否显示,并响应侧边栏折叠状态 -->
    <logov-if="sidebarLogo":collapse="sidebar.opened"></logo>
    <!-- Element UI菜单组件,配置了背景色、文本颜色和激活项颜色 -->
    <el-menu
      border-none
      class="sidebar-container-menu"
      :default-active="defaultActive"
      :background-color="variables.menuBg"
      :text-color="variables.menuText"
      :active-text-color="theme"
      :collapse="sidebar.opened"
    >
      <!-- 动态生成菜单项,menuRoutes包含了所有需要显示的菜单项 -->
      <sidebar-item
        v-for="route in menuRoutes"
        :key="route.path"
        :item="route"
      />
      <!-- 增加父路径,用于el-menu-item渲染的时候拼接 -->
    </el-menu>
  </div>
</template>
<scriptlang="ts"setup>
// 导入应用状态管理相关模块
import { useAppStore } from "@/stores/app";
// 导入样式变量
import variables from "@/style/variables.module.scss";
// 导入设置状态管理
import { useSettingStore } from "@/stores/settings";
// 导入菜单状态管理
import { useMenuStore } from "@/stores/menu";
// 获取菜单状态
const meuStore = useMenuStore();
// 计算属性:响应式菜单数据,来自状态管理中的权限菜单树
const menuRoutes = computed(() => meuStore.state.authMenuTreeData);
// 获取当前路由信息
const route = useRoute();
// 获取侧边栏状态(展开/折叠)
const { sidebar } = useAppStore();
// 计算属性:当前激活菜单项,默认使用当前路由路径
const defaultActive = computed(() => {
  // .....
  return route.path;
});
// 获取设置状态
const settingsStore = useSettingStore();
// 计算属性:主题颜色,响应式更新
const theme = computed(() => settingsStore.settings.theme);
// 计算属性:侧边栏Logo显示控制,响应式更新
const sidebarLogo = computed(() => settingsStore.settings.sidebarLogo);
</script>
<stylescoped></style>

修改 SidebarItem 组件

修改 src/layout/components/Sidebar/SidebarItem.vue 中的路径解析方法,代码如下:

html
//src/layout/components/Sidebar/SidebarItem.vue
<template>
  <!-- 当菜单项没有设置hidden为true时显示 -->
  <templatev-if="!item.meta?.hidden">
    <!-- 情况1:如果只有一个子菜单且没有设置alwaysShow,直接渲染子菜单项 -->
    <sidebar-item-link
      v-if="filteredChildren.length <= 1 && !item.meta?.alwaysShow"
      :to="resolvePath(singleChildRoute.path)"
    >
      <el-menu-item:index="resolvePath(singleChildRoute.path)">
        <el-iconv-if="iconName">
          <!-- 显示菜单项图标 -->
          <svg-icon:icon-name="iconName" />
        </el-icon>
        <!-- 菜单项标题 -->
        <template #title>{{ singleChildRoute.meta.title }}</template>
      </el-menu-item>
    </sidebar-item-link>
    <!-- 情况2:有多个子菜单或设置了alwaysShow,渲染为下拉菜单 -->
    <el-sub-menuv-else:index="item.path">
      <template #title>
        <el-iconv-if="iconName">
          <!-- 父菜单项图标 -->
          <svg-icon:icon-name="iconName" />
        </el-icon>
        <!-- 父菜单项标题 -->
        <span>{{ item.meta?.title }}</span>
      </template>
      <!-- 递归渲染子菜单项 -->
      <sidebar-item
        v-for="child of filteredChildren"
        :key="child.path"
        :item="child"
      ></sidebar-item>
    </el-sub-menu>
  </template>
</template>
<scriptlang="ts"setup>
// 导入路径校验工具和菜单项类型定义
import { isExternal } from "@/utils/validate";
import type { ITreeItemDataWithMenuData } from "@/utils/generateTree";
// 定义组件props,接收菜单项数据
const { item } = defineProps<{
  item: ITreeItemDataWithMenuData;
}>();
// 计算属性:过滤掉隐藏的子菜单项
const filteredChildren = computed(() =>
  (item.children || []).filter((child) => !child.meta?.hidden)
);
// 计算属性:确定当前要渲染的菜单项
// 如果只有一个子菜单,渲染子菜单;否则渲染当前菜单项
const singleChildRoute = computed(() =>
  filteredChildren.value.length === 1 ? filteredChildren.value[0] : { ...item }
);
// 计算属性:获取菜单项图标
const iconName = computed(() => singleChildRoute.value.meta?.icon);
// 路径解析函数:处理菜单项路径
// 外部链接直接返回,内部链接可能需要拼接父路径
const resolvePath = (childPath: string) => {
  if (isExternal(childPath)) {
    return childPath;
  }
  return childPath;
};
</script>