系列文章

一、先说点什么

从这一章开始,文章描述的重点,将会从实际代码转移到思路讲述,贴出的代码只是关键部分。俗话说授之以鱼不如授之以渔,要想学会一样东西,打通思路是最重要的。就和学会武功,要打通任督二脉是一个道理。好的,那么我们就进入侧边栏的实现吧。

二、从问题开始,侧边栏是干啥的?

这个问题问得好!我们在做一个东西之前,首先要了解这个东西有啥用,你光图好看有逼格那属于浪费精力。所以,我们必须要知道我们为什么要去做侧边栏。那么侧边栏是干啥的呢?它的核心功能只有一个,那就是路由跳转。用户可以通过这样一个侧边导航,很轻松的进入一个又一个页面,这就是它最本质的功能。

所以,有了这个思路,我们就知道,我们需要将路由配置与这个组件联系起来,每当我们改动路由配置的时候,它就会自动生成菜单项。好的,既然说到了菜单项,那我们就先来研究一下,每个菜单项应该包括一些什么东西:

  • key:每个菜单项的唯一标识,最好是其名称的英文翻译
  • type:菜单项的类别(可包含子级菜单项的下拉菜单、可包含子级菜单项的分组菜单和普通子级菜单项)
  • label:菜单项名称
  • icon:svg图标名
  • hidden:该路由是否显示到菜单中
  • roles:能够访问该路由的账号角色,可以是字符串或字符串数组
  • disabled:是否已禁用状态展示该菜单项
  • children:其下包含的子级菜单项
  • onClick:点击当前路由会发生什么(一般情况下是在系统内跳转路由,但有时候也需要干点别的)

这样就分析好了每个菜单项的属性了,现在让我们来与路由配置项对应一下:

{
  key: name,               // 菜单项的唯一标识对应路由名
  type: meta.type,         // 除了children之外的都可以放进meta中
  label: meta.label,
  icon: meta.icon,
  hidden: meta.hidden,
  roles: meta.roles,
  disabled: meta.disabled,
  onClick: meta.onClick
  children                 // 子级菜单项对应子路由
}

由此可见,二者可以非常完美的对应起来,那么根据这个思路,我们就可以开始封装组件了。

三、封装组件之思路分析

初步分析一下,整个组件里面都有些啥:

  • 一个整体:AsideBar
  • 头部下面的菜单:ModuleMenu
  • 菜单里的菜单项:MenuItem
  • 头部Logo与标题:SystemLogo

总结一下:

  1. 创建一个MenuItem.vue作为菜单项,将路由配置项转换为菜单配置项,并且要分三种不同的类别,其中submenu和group两类中需要递归调用自身
  2. 创建一个ModuleMenu.vue作为所有菜单项的容器,负责将路由配置传递给MenuItem.vue
  3. 创建一个SystemLogo.vue作为头部的容器,展示图标和系统名
  4. 创建一个AsideBar.vue作为一个整体的容器,读取路由配置信息并传递给ModuleMenu.vue,同时需要接收外部传来的collapse(控制自身的折叠与展开)、iconName(logo图标名)、iconColor(logo颜色)、systemName(系统名)

好的,整体分析完成。下面我们来一个一个的去实现吧。

四、封装菜单部分

1. MenuItem

我们先来看一下MenuItem的template部分:

<template>
  <!-- menuItems是经过转换的菜单项列表 -->
  <template v-for="item in menuItems" :key="item.key">
    <!-- 通过hidden和roles字段判断是否显示当前菜单项 -->
    <template v-if="getItemShow(item)">
      <!-- v-if submenu -->
      <el-submenu v-if="item.type === 'submenu'" :index="item.key" :disabled="item.disabled">
        <template #title>
          <svg-icon v-if="item.icon" :name="item.icon" size="xl" class="item-icon" />
          <span>{{ item.label }}</span>
        </template>
        <!-- 递归调用自身,渲染子级菜单项,下面的group也是一样 -->
        <template v-if="item.children">
          <menu-item :route-list="item.children" />
        </template>
      </el-submenu>

      <!-- v-if group -->
      <el-menu-item-group v-if="item.type === 'group'">
        <template #title>
          <span>{{ item.label }}</span>
        </template>
        <template v-if="item.children">
          <menu-item :route-list="item.children" />
        </template>
      </el-menu-item-group>

      <!-- v-if item -->
      <el-menu-item
        v-if="item.type === 'item'"
        :index="item.key"
        @click="onMenuItemClick($event, item)"
        :disabled="item.disabled"
      >
        <svg-icon v-if="item.icon" :name="item.icon" size="xl" class="item-icon" />
        <span>{{ item.label }}</span>
      </el-menu-item>
    </template>
  </template>
</template>

上面SvgIcon组件的实现请看同系列文章:如何优雅地使用Svg图标,然后看一下script部分:

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { RouteRecordRaw, useRouter } from 'vue-router'
import { MenuItemProps } from '../../typings'
import Variables from '@/assets/styles/variables.scss'

export default defineComponent({
  name: 'MenuItem',
  props: {
    routeList: {
      type: Array as PropType<RouteRecordRaw[]>,
      required: true
    }
  },
  emits: ['itemClick'],
  setup(props, context) {
    const router = useRouter()
    
    // 处理菜单项的点击事件
    const onMenuItemClick = (event: any, item: MenuItemProps) => {
      // 处理onClick函数
      if (item.onClick) {
        // 把MouseEvent和菜单项本身传回去
        item.onClick(event, item)
        // 触发一个点击事件给父组件,用于再不进行路由跳转时,取消菜单项的激活状态
        context.emit('itemClick', event, item)
        return
      }
      // 没有onClick函数的时候,进行路由跳转
      router.push({ name: item.key })
    }

    // 将路由配置转换成菜单配置
    const menuItems = computed(() => {
      return props.routeList.map((item) => {
        let temp: MenuItemProps = { key: item.name as string }
        if (item.meta) {
          temp = { ...temp, ...item.meta }
        }
        if (item.children) {
          temp.children = item.children
        }
        return temp
      })
    })

    // 通过hidden和roles字段判断是否显示当前菜单项
    const getItemShow = computed(() => (item: MenuItemProps) => {
      /**
       * 这里暂时不写关于用户权限控制的逻辑了,因为我也不知道你们会怎么存储用户的role字段
       * 权限控制思路:
       * 1. 假设你的role字段存在了store.userModule.role中
       * 2. 使用instanceof判断一下menuitem中的roles是数组还是字符串
       * 3. 如果是字符串:return store.userModule.role === item.roles
       * 4. 如果是数组:return item.roles.includes(store.userModule.role)
       * 5. 但是要注意修改一下分支结构,因为item.hidden的优先级在item.roles前面
       */
      return !item.hidden
    })

    return {
      menuItems,
      Variables,
      onMenuItemClick,
      getItemShow
    }
  }
})
</script>

2. ModuleMenu

template部分:

<template>
  <el-menu
    :collapse="collapse"
    :collapse-transition="false"
    :background-color="Variables.ASIDE_BAR_BG_COLOR"
    :text-color="Variables.ASIDE_BAR_COLOR"
    :active-text-color="Variables.ASIDE_BAR_ACTIVE_COLOR"
    :default-active="$route.name"
    :key="menuKey"
    class="asideBar-menu"
  >
    <menu-item :route-list="routeList" @item-click="onItemClick" />
  </el-menu>
</template>

这个组件里只是简单的引用了一下ElMenu组件以及我们刚刚封装好的MenuItem组件。然后看一下script部分:

<script lang="ts">
import { defineComponent, PropType, ref } from 'vue'
import { MenuItemProps } from '../../typings'
import Variables from '@/assets/styles/variables.scss'
import Utils from '@/utils'
import MenuItem from '../MenuItem'

export default defineComponent({
  name: 'ModuleMenu',
  components: {
    MenuItem
  },
  props: {
    collapse: {
      type: Boolean,
      required: true
    },
    routeList: {
      type: Array as PropType<MenuItemProps[]>,
      required: true
    }
  },
  setup() {
    /** 
     * 定义key,并监听菜单项点击事件,也就是每当菜单项的onClick函数触发的时候,
     * 都会强制刷新ElMenu组件,达到取消目标路由激活状态的目的
     */
    const menuKey = ref(Utils.uuid())
    const onItemClick = () => (menuKey.value = Utils.uuid())

    return {
      Variables,
      menuKey,
      onItemClick
    }
  }
})
</script>

至此,前两步的主要内容就讲述完了,后面的内容且听下回分解。

下一篇预告:vue3.0+ts+element-plus多页签应用模板:侧边导航菜单(下)

Logo

前往低代码交流专区

更多推荐