说起菜单的如何生成的,这个会与路由和权限的定义有关。因为路由涉及页面的跳转以及当前菜单项高亮选中等,可能后面还会涉及面包屑、标签页等功能的制作。目前不考虑权限,我们根据约定路由的配置,来生成动态菜单。

一、布局

对于后台管理系统,通常由 sider 菜单栏、header、footer 和 content 的内容组成。

<a-layout>
  <a-layout-sider>Sider</a-layout-sider>
  <a-layout>
    <a-layout-header>Header</a-layout-header>
    <a-layout-content>
       <route-view />
    </a-layout-content>
    <a-layout-footer>Footer</a-layout-footer>
    <!-- 右侧抽屉 -->
    <setting-drawer /> 
  </a-layout>
  <!-- 回到顶部 -->
  <back-top />
</a-layout>

二、路由

{ Route } 对象

参数

说明

类型

默认值

hidden

控制路由是否显示在 sidebar

boolean

false

redirect

重定向地址, 访问这个路由时,自定进行重定向

string

-

name

路由名称, 必须设置,且不能重名

string

-

meta

路由元信息(路由附带扩展信息)

object

{}

hideChildrenInMenu

强制菜单显示为Item而不是SubItem(配合 meta.hidden)

boolean

-

{ Meta } 路由元信息对象

参数

说明

类型

默认值

title

路由标题, 用于显示面包屑, 页面标题 *推荐设置

string

-

icon

路由在 menu 上显示的图标

[string,svg]

-

keepAlive

缓存该路由

boolean

false

target

菜单链接跳转目标(参考 html a 标记)

string

-

hidden

配合hideChildrenInMenu使用,用于隐藏菜单时,提供递归到父菜单显示 选中菜单项_(可参考 个人页 配置方式)_

boolean

false

hiddenHeaderContent

*特殊 隐藏 PageHeader 组件中的页面带的 面包屑和页面标题栏

boolean

false

permission

与项目提供的权限拦截匹配的权限,如果不匹配,则会被禁止访问该路由页面

array

[]

路由自定义 Icon 请引入自定义 svg Icon 文件,然后传递给路由的 meta.icon 参数即可

更多路由与菜单配置,请参考:ant-design-pro 之路由和菜单

三、菜单栏

mode 菜单类型,支持垂直、水平、和内嵌模式三种 vertical/horizontal/inline

theme 主题颜色  dark/light

inlineCollapsed inline 时菜单是否收起状态

<a-menu
  :default-selected-keys="['1']"
  :default-open-keys="['2']"
  mode="inline"
  theme="dark"
  :inline-collapsed="collapsed"
>
  <template v-for="item in list">
    <a-menu-item v-if="!item.children" :key="item.key">
      <a-icon type="pie-chart" />
      <span>{{ item.title }}</span>
    </a-menu-item>
    <sub-menu v-else :key="item.key" :menu-info="item" />
  </template>
</a-menu>

菜单可能由一级菜单,二级菜单,三级菜单,甚至四级菜单组成,因此推出 Menu.Submenu 子菜单,使用递归生成多级菜单。

Menu.Item 和 Menu.SubMenu 中 key 都是为了作为唯一标志的。

<template functional>
  <a-sub-menu :key="props.menuInfo.key">
    <span slot="title">
      <a-icon type="mail" /><span>{{ props.menuInfo.title }}</span>
    </span>
    <template v-for="item in props.menuInfo.children">
      <a-menu-item v-if="!item.children" :key="item.key">
        <a-icon type="pie-chart" />
        <span>{{ item.title }}</span>
      </a-menu-item>
      <sub-menu v-else :key="item.key" :menu-info="item" />
    </template>
  </a-sub-menu>
</template>
export default {
  props: ['menuInfo'],
};

通常我们都会将菜单栏封装成一个独立的组件,代码如下:

我们可以配置菜单的类型,主题和菜单的数据等参数来生成菜单栏。当然我们还会传入一个参数 collapsed 来决定菜单栏是否展开显示。

import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
const { Item, SubMenu } = Menu
export default {
  props: {
    mode: {
      type: String,
      required: false,
      default: 'inline',
    },
    theme: {
      type: String,
      required: false,
      default: 'dark',
    },
    menu: {
      type: Array,
      required: true,
    }
  }
  render () {
    const { mode, theme, menu } = this
    const props = {
      mode,
      theme,
      openKey: this.openKeys,
    }
    const on = {
      select: this.onSelect,
      openChange: this.onOpenChange,
    }
    const menuTree = menu.map(item => {
      if (item.hidden) {
        return null
      }
      return this.renderItem(item)
    })
    return (
      <Menu
        v-model={this.selectedKeys}
        {...{ props, on }}
      >
        {menuTree}
      </Menu>
    )
  }
}

如何高亮显示当前选中菜单项和展开菜单栏?

menu 提供 select 和 openChange 事件,被选中时调用和 SubMenu 展开/关闭的回调。

onSelect (obj) {
  this.selectedKeys = obj.selectedKeys
  this.$emit('select', obj)
}
onOpenChange (openKeys) {
  ......
}

如何生成菜单树呢?

对于生成菜单树,我们需要判断是渲染的菜单项还是子菜单?路由需要 hidden 来隐藏渲染该菜单。如果想禁止渲染子菜单,则通过设置 hideChildrenInMenu 来设置。

renderItem (menu) {
  if (!menu.hidden) {
    return menu.children && !menu.hideChildrenInMenu ? this.renderSubMenu(menu) : this.renderMenuItem(menu)
  } 
  return null
}

渲染菜单项

renderMenuItem (menu) {
  const target = menu.meta.target || null
  const tag = (target && 'a') || 'router-link'
  const props = { to: { name: menu.name } }
  const attrs = { href: menu.path, target: menu.meta.target }
  <!-- 有子菜单,且需要隐藏子菜单的添加 hidden 属性 -->
  if (menu.children && menu.hideChildrenInMenu) {
    menu.children.forEach(item = > {
      item.meta = Object.assign(item.meta, { hidden: true })
    })
  }
  return (
    <Item {...{ key: menu.path }}>
      <tag {...{ props, attrs }}>
        {this.renderIcon(menu.meta.icon)}
        <span>{menu.meta.title}</span>
      </tag>
    </Item>
  )
}

渲染子菜单

renderSubMenu (menu) {
  const itemArr = []
  if (!menu.hideChildrenInMenu) {
    menu.children.forEach(item => itemArr.push(this.renderItem(item)))
  }
  return (
    <SubMenu {...{ key: menu.path }}>
      <span slot="title">
        {this.renderIcon(menu.meta.icon)}
        <span>{menu.meta.title}<span>
      </span>
    </SubMenu>
  )
}

title 子菜单项值, 使用 slot 插入 Icon 和 title。这里使用 path 作为 Menu.Item 和 Menu.SubMenu 的 key, 即唯一标识。

渲染 ICON

renderIcon (icon) {
  if (icon === 'none' || icon === undefined) return null
  const props = {}
  type (icon) === 'object' ? props.component = icon : props.type = icon
  return (
    <Icon {...{ props }} />
  )
}

type  图标类型.

component  控制如何渲染图标,通常是一个渲染根标签为 <svg> 的 Vue 组件,会使 type 属性失效.

import MessageSvg from 'path/to/message.svg'; // path to your '*.svg' file.
new Vue({
  el: '#app',
  data() {
    return {
      MessageSvg,
    };
  },
  template: '<a-icon :component="MessageSvg" />',
});

如何更新菜单?

在 vue 动态路由中,我们可以看到 this.$route.matched.concat() 来用于菜单列表的更新。这是 Vue  Router 提供的 API:$route.matched 一个数组,包含当前路由的所有嵌套路径片段的路由记录。路由记录就是 routes 配置数组中的对象副本(还有在 children 数组)。对于  concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

const router = new VueRouter({
  routes: [
    ...
  ]
})

 路由需要配合菜单栏高亮显示当前选中的菜单栏。当我们更新菜单时,主要是更新高亮选中菜单和菜单展开功能。我们可以根据 router.meta 中的 hidden 来决定菜单栏是否显示, selectedKeys 属性用来获取当前选中的 router 的 path 路径,同时使用 openKeys 来存储当前的所有路径。

updateMenu () {
  const routes = this.$route.matched.concat()
  const { hidden } = this.$route.meta
  if (route.lenth >= 3 && hidden) {
    routes.pop()
    this.selectedKeys = [routes[routes.length - 1].path]
  } else {
    this.selectedKeys = [routes.pop().path]
  }
  ......
}

$route.path:字符串,对应当前路由的路径,总是解析绝对路径,如 '/user/login'。 

当然有时候我们设置菜单栏收缩,因此可以不用展开菜单栏,因此我们需要将 openKeys 设置为空数组。

watch: {
  collapsed (val) {
    if (val) {
      this.cachedOpenKeys = this.openKeys.concat()
      this.openKeys = []
    } else {
      this.openKeys = this.cachedOpenKeys
    }
  }
}

因此更新菜单栏时,我们判断菜单栏是否展开/收缩。

updateMenu () {
  ......
  const openKeys = []
  if (this.mode === 'inline') {
    routes.forEach(item => {
      openKeys.push(item.path)
    })
  }
  this.collapsed ? (this.cacheOpenKeys = openKeys) : (this.openKeys = openKeys)
}