说起菜单的如何生成的,这个会与路由和权限的定义有关。因为路由涉及页面的跳转以及当前菜单项高亮选中等,可能后面还会涉及面包屑、标签页等功能的制作。目前不考虑权限,我们根据约定路由的配置,来生成动态菜单。
一、布局
对于后台管理系统,通常由 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 | 配合 | 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)
}