Skip to content

Menu导航菜单

Menu导航菜单用于为页面和功能提供导航。导航菜单一般分为顶部导航和侧边导航,顶部导航提供全局性类目和功能,侧边导航通过多级结构收纳和排列网站架构。更多布局和导航使用可参考通用布局 。

开发者需注意,Menu元素为ul,仅支持li以及script-supporting子元素,子节点元素应在Menu.Item内使用。Menu需计算节点结构,其子元素仅支持Menu.*及相关HOC组件,且必须为SubMenu设置唯一key。

代码演示

  1. 顶部导航:水平的顶部导航菜单,示例代码如下:
vue
<template>
  <a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" />
</template>
<script lang="ts" setup>
import { h, ref } from 'vue'
import { MailOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue'
import { MenuProps } from '@design-middle-center/fuse-design-vue'
const current = ref<string[]>(['mail'])
const items = ref<MenuProps['items']>([
  {
    key: 'mail',
    icon: () => h(MailOutlined),
    label: 'Navigation One',
    title: 'Navigation One'
  },
  {
    key: 'app',
    icon: () => h(AppstoreOutlined),
    label: 'Navigation Two',
    title: 'Navigation Two'
  },
  {
    key: 'sub1',
    icon: () => h(SettingOutlined),
    label: 'Navigation Three - Submenu',
    title: 'Navigation Three - Submenu',
    children: [
      {
        type: 'group',
        label: 'Item 1',
        children: [
          {
            label: 'Option 1',
            key: 'setting:1'
          },
          {
            label: 'Option 2',
            key: 'setting:2'
          }
        ]
      },
      {
        type: 'group',
        label: 'Item 2',
        children: [
          {
            label: 'Option 3',
            key: 'setting:3'
          },
          {
            label: 'Option 4',
            key: 'setting:4'
          }
        ]
      }
    ]
  },
  {
    key: 'alipay',
    label: h('a', { href: 'https://antdv.com', target: '_blank' }, 'Navigation Four - Link'),
    title: 'Navigation Four - Link'
  }
])
</script>
  1. 内嵌菜单:垂直菜单,子菜单内嵌在菜单区域。
vue
<template>
  <a-menu
    id="dddddd"
    v-model:openKeys="openKeys"
    v-model:selectedKeys="selectedKeys"
    style="width: 256px"
    mode="inline"
    :items="items"
    @click="handleClick"
  ></a-menu>
</template>
<script lang="ts" setup>
import { reactive, ref, watch, VueElement, h } from 'vue'
import { MailOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue'
import type { MenuProps, ItemType } from '@design-middle-center/fuse-design-vue'

const selectedKeys = ref<string[]>(['1'])
const openKeys = ref<string[]>(['sub1'])

function getItem(
  label: VueElement | string,
  key: string,
  icon?: any,
  children?: ItemType[],
  type?: 'group'
): ItemType {
  return {
    key,
    icon,
    children,
    label,
    type
  } as ItemType
}

const items: ItemType[] = reactive([
  getItem('Navigation One', 'sub1', () => h(MailOutlined), [
    getItem('Item 1', 'g1', null, [getItem('Option 1', '1'), getItem('Option 2', '2')], 'group'),
    getItem('Item 2', 'g2', null, [getItem('Option 3', '3'), getItem('Option 4', '4')], 'group')
  ]),

  getItem('Navigation Two', 'sub2', () => h(AppstoreOutlined), [
    getItem('Option 5', '5'),
    getItem('Option 6', '6'),
    getItem('Submenu', 'sub3', null, [getItem('Option 7', '7'), getItem('Option 8', '8')])
  ]),

  { type: 'divider' },

  getItem('Navigation Three', 'sub4', () => h(SettingOutlined), [
    getItem('Option 9', '9'),
    getItem('Option 10', '10'),
    getItem('Option 11', '11'),
    getItem('Option 12', '12')
  ]),

  getItem('Group', 'grp', null, [getItem('Option 13', '13'), getItem('Option 14', '14')], 'group')
])

const handleClick: MenuProps['onClick'] = (e) => {
  console.log('click', e)
}

watch(openKeys, (val) => {
  console.log('openKeys', val)
})
</script>
  1. 缩起内嵌菜单:内嵌菜单可缩起/展开,可在Layout里查看侧边布局结合的完整示例。
vue
<template>
  <div style="width: 256px">
    <a-button type="primary" style="margin-bottom: 16px" @click="toggleCollapsed">
      <MenuUnfoldOutlined v-if="state.collapsed" />
      <MenuFoldOutlined v-else />
    </a-button>
    <a-menu
      v-model:openKeys="state.openKeys"
      v-model:selectedKeys="state.selectedKeys"
      mode="inline"
      theme="dark"
      :inline-collapsed="state.collapsed"
      :items="items"
    ></a-menu>
  </div>
</template>
<script lang="ts" setup>
import { reactive, watch, h } from 'vue';
import {
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  PieChartOutlined,
  MailOutlined,
  DesktopOutlined,
  InboxOutlined,
  AppstoreOutlined,
} from '@ant-design/icons-vue';
const state = reactive({
  collapsed: false,
  selectedKeys: ['1'],
  openKeys: ['sub1'],
  preOpenKeys: ['sub1'],
});
const items = reactive([
  {
    key: '1',
    icon: () => h(PieChartOutlined),
    label: 'Option 1',
    title: 'Option 1',
  },
  {
    key: '2',
    icon: () => h(DesktopOutlined),
    label: 'Option 2',
    title: 'Option 2',
  },
  {
    key: '3',
    icon: () => h(InboxOutlined),
    label: 'Option 3',
    title: 'Option 3',
  },
  {
    key: 'sub1',
    icon: () => h(MailOutlined),
    label: 'Navigation One',
    title: 'Navigation One',
    children: [
      {
        key: '5',
        label: 'Option 5',
        title: 'Option 5',
      },
      {
        key: '6',
        label: 'Option 6',
        title: 'Option 6',
      },
      {
        key: '7',
        label: 'Option 7',
        title: 'Option 7',
      },
      {
        key: '8',
        label: 'Option 8',
        title: 'Option 8',
      },
    ],
  },
  {
    key: 'sub2',
    icon: () => h(AppstoreOutlined),
    label: 'Navigation Two',
    title: 'Navigation Two',
    children: [
      {
        key: '9',
        label: 'Option 9',
        title: 'Option 9',
      },
      {
        key: '10',
        label: 'Option 10',
        title: 'Option 10',
      },
      {
        key: 'sub3',
        label: 'Submenu',
        title: 'Submenu',
        children: [
          {
            key: '11',
            label: 'Option 11',
            title: 'Option 11',
          },
          {
            key: '12',
            label: 'Option 12',
            title: 'Option 12',
          },
        ],
      },
    ],
  },
]);
watch(
  () => state.openKeys,
  (_val, oldVal) => {
    state.preOpenKeys = oldVal;
  },
);
const toggleCollapsed = () => {
  state.collapsed = !state.collapsed;
  state.openKeys = state.collapsed ? [] : state.preOpenKeys;
};
</script>
  1. 只展开当前父级菜单:点击菜单,收起其他展开的所有菜单,保持菜单聚焦简洁。
vue
<template>
  <div>
    <a-menu
      v-model:selectedKeys="state.selectedKeys"
      style="width: 256px"
      mode="inline"
      :open-keys="state.openKeys"
      :items="items"
      @openChange="onOpenChange"
    ></a-menu>
  </div>
</template>
<script lang="ts" setup>
import { VueElement, h, reactive } from 'vue'
import { MailOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue'
import { ItemType } from '@design-middle-center/fuse-design-vue'

function getItem(
  label: VueElement | string,
  key: string,
  icon?: any,
  children?: ItemType[],
  type?: 'group'
): ItemType {
  return {
    key,
    icon,
    children,
    label,
    type
  } as ItemType
}

const items: ItemType[] = reactive([
  getItem('Navigation One', 'sub1', () => h(MailOutlined), [
    getItem('Option 1', '1'),
    getItem('Option 2', '2'),
    getItem('Option 3', '3'),
    getItem('Option 4', '4')
  ]),
  getItem('Navigation Two', 'sub2', () => h(AppstoreOutlined), [
    getItem('Option 5', '5'),
    getItem('Option 6', '6'),
    getItem('Submenu', 'sub3', null, [getItem('Option 7', '7'), getItem('Option 8', '8')])
  ]),
  getItem('Navigation Three', 'sub4', () => h(SettingOutlined), [
    getItem('Option 9', '9'),
    getItem('Option 10', '10'),
    getItem('Option 11', '11'),
    getItem('Option 12', '12')
  ])
])

const state = reactive({
  rootSubmenuKeys: ['sub1', 'sub2', 'sub4'],
  openKeys: ['sub1'],
  selectedKeys: []
})
const onOpenChange = (openKeys: string[]) => {
  const latestOpenKey = openKeys.find((key) => state.openKeys.indexOf(key) === -1) ?? ''
  if (state.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
    state.openKeys = openKeys
  } else {
    state.openKeys = latestOpenKey ? [latestOpenKey] : []
  }
}
</script>
  1. 垂直菜单:子菜单以弹出形式展示。
vue
<template>
  <a-menu
    v-model:openKeys="openKeys"
    v-model:selectedKeys="selectedKeys"
    style="width: 256px"
    mode="vertical"
    :items="items"
    @click="handleClick"
  />
</template>
<script lang="ts" setup>
import { h, ref } from 'vue'
import {
  MailOutlined,
  CalendarOutlined,
  AppstoreOutlined,
  SettingOutlined
} from '@ant-design/icons-vue'
import type { MenuProps } from '@design-middle-center/fuse-design-vue'

const selectedKeys = ref([])
const openKeys = ref([])
const items = ref([
  {
    key: '1',
    icon: () => h(MailOutlined),
    label: 'Navigation One',
    title: 'Navigation One'
  },
  {
    key: '2',
    icon: () => h(CalendarOutlined),
    label: 'Navigation Two',
    title: 'Navigation Two'
  },
  {
    key: 'sub1',
    icon: () => h(AppstoreOutlined),
    label: 'Navigation Three',
    title: 'Navigation Three',
    children: [
      {
        key: '3',
        label: 'Option 3',
        title: 'Option 3'
      },
      {
        key: '4',
        label: 'Option 4',
        title: 'Option 4'
      },
      {
        key: 'sub1-2',
        label: 'Submenu',
        title: 'Submenu',
        children: [
          {
            key: '5',
            label: 'Option 5',
            title: 'Option 5'
          },
          {
            key: '6',
            label: 'Option 6',
            title: 'Option 6'
          }
        ]
      }
    ]
  },
  {
    key: 'sub2',
    icon: () => h(SettingOutlined),
    label: 'Navigation Four',
    title: 'Navigation Four',
    children: [
      {
        key: '7',
        label: 'Option 7',
        title: 'Option 7'
      },
      {
        key: '8',
        label: 'Option 8',
        title: 'Option 8'
      },
      {
        key: '9',
        label: 'Option 9',
        title: 'Option 9'
      },
      {
        key: '10',
        label: 'Option 10',
        title: 'Option 10'
      }
    ]
  }
])
const handleClick: MenuProps['onClick'] = (menuInfo) => {
  console.log('click ', menuInfo)
}
</script>
  1. 主题:内建lightdark两套主题,默认light


vue
<template>
  <div>
    <a-switch
      :checked="theme === 'dark'"
      checked-children="Dark"
      un-checked-children="Light"
      @change="changeTheme"
    />
    <br />
    <br />
    <a-menu
      v-model:openKeys="openKeys"
      v-model:selectedKeys="selectedKeys"
      style="width: 256px"
      mode="inline"
      :theme="theme"
      :items="items"
    />
  </div>
</template>
<script lang="ts" setup>
import { h, ref } from 'vue'
import {
  MailOutlined,
  CalendarOutlined,
  AppstoreOutlined,
  SettingOutlined
} from '@ant-design/icons-vue'
import type { MenuTheme } from '@design-middle-center/fuse-design-vue'
const theme = ref<MenuTheme>('dark')
const selectedKeys = ref(['1'])
const openKeys = ref(['sub1'])
const items = ref([
  {
    key: '1',
    icon: () => h(MailOutlined),
    label: 'Navigation One',
    title: 'Navigation One'
  },
  {
    key: '2',
    icon: () => h(CalendarOutlined),
    label: 'Navigation Two',
    title: 'Navigation Two'
  },
  {
    key: 'sub1',
    icon: () => h(AppstoreOutlined),
    label: 'Navigation Three',
    title: 'Navigation Three',
    children: [
      {
        key: '3',
        label: 'Option 3',
        title: 'Option 3'
      },
      {
        key: '4',
        label: 'Option 4',
        title: 'Option 4'
      },
      {
        key: 'sub1-2',
        label: 'Submenu',
        title: 'Submenu',
        children: [
          {
            key: '5',
            label: 'Option 5',
            title: 'Option 5'
          },
          {
            key: '6',
            label: 'Option 6',
            title: 'Option 6'
          }
        ]
      }
    ]
  },
  {
    key: 'sub2',
    icon: () => h(SettingOutlined)
  },
  {
    key: 'sub2',
    icon: () => h(SettingOutlined),
    label: 'Navigation Four',
    title: 'Navigation Four',
    children: [
      {
        key: '7',
        label: 'Option 7',
        title: 'Option 7'
      },
      {
        key: '8',
        label: 'Option 8',
        title: 'Option 8'
      },
      {
        key: '9',
        label: 'Option 9',
        title: 'Option 9'
      },
      {
        key: '10',
        label: 'Option 10',
        title: 'Option 10'
      }
    ]
  }
])
const changeTheme = (checked: boolean) => {
  theme.value = checked ? 'dark' : 'light'
}
</script>
  1. 子菜单主题:可通过theme属性设置SubMenu主题,实现不同目录树下不同主题色。以下示例默认根目录深色,子目录浅色。


vue
<template>
  <div>
    <a-switch
      :checked="theme === 'dark'"
      checked-children="dark"
      un-checked-children="light"
      @Change="changeTheme"
    />
    <br />
    <br />
    <a-menu
      :style="{ width: '256px' }"
      :open-keys="openKeys"
      :selected-keys="selectedKeys"
      mode="vertical"
      theme="dark"
      :items="items"
      @click="handleClick"
    />
  </div>
</template>
<script lang="ts" setup>
import { computed, ref, VueElement, ComputedRef, h } from 'vue'
import { MailOutlined } from '@ant-design/icons-vue'
import type { MenuProps } from '@design-middle-center/fuse-design-vue'

const selectedKeys = ref<string[]>(['1'])
const openKeys = ref<string[]>(['sub1'])
const theme = ref<MenuProps['theme']>('light')

function getItem(
  label: VueElement | string,
  key: string,
  icon?: any,
  children?: MenuProps['items'],
  theme?: 'light' | 'dark'
): MenuProps['items'][number] {
  return {
    key,
    icon,
    children,
    label,
    theme
  }
}

const items: ComputedRef<MenuProps['items']> = computed(() => [
  getItem(
    'Navigation One',
    'sub1',
    () => h(MailOutlined),
    [getItem('Option 1', '1'), getItem('Option 2', '2'), getItem('Option 3', '3')],
    theme.value
  ),
  getItem('Option 5', '5'),
  getItem('Option 6', '6')
])

function handleClick(info: any) {
  console.log('click', info)
}

function changeTheme(checked: boolean) {
  theme.value = checked ? 'dark' : 'light'
}
</script>
  1. 切换菜单类型:展示动态切换模式。
Change Mode Change Theme

vue
<template>
  <div>
    <a-switch :checked="state.mode === 'vertical'" @change="changeMode" />
    Change Mode
    <span class="ant-divider" style="margin: 0 1em" />
    <a-switch :checked="state.theme === 'dark'" @change="changeTheme" />
    Change Theme
    <br />
    <br />
    <a-menu
      v-model:openKeys="state.openKeys"
      v-model:selectedKeys="state.selectedKeys"
      style="width: 256px"
      :mode="state.mode"
      :items="items"
      :theme="state.theme"
    ></a-menu>
  </div>
</template>
<script lang="ts" setup>
import { h, reactive } from 'vue'
import {
  MailOutlined,
  CalendarOutlined,
  AppstoreOutlined,
  SettingOutlined
} from '@ant-design/icons-vue'
import type { MenuMode, MenuTheme } from '@design-middle-center/fuse-design-vue'
import { ItemType } from '@design-middle-center/fuse-design-vue'

const state = reactive({
  mode: 'inline' as MenuMode,
  theme: 'light' as MenuTheme,
  selectedKeys: ['1'],
  openKeys: ['sub1']
})

function getItem(
  label: string,
  key: string,
  icon?: any,
  children?: ItemType[],
  type?: 'group'
): ItemType {
  return {
    key,
    icon,
    children,
    label,
    type
  } as ItemType
}

const items: ItemType[] = reactive([
  getItem('Navigation One', '1', h(MailOutlined)),
  getItem('Navigation Two', '2', h(CalendarOutlined)),
  getItem('Navigation Two', 'sub1', h(AppstoreOutlined), [
    getItem('Option 3', '3'),
    getItem('Option 4', '4'),
    getItem('Submenu', 'sub1-2', null, [getItem('Option 5', '5'), getItem('Option 6', '6')])
  ]),
  getItem('Navigation Three', 'sub2', h(SettingOutlined), [
    getItem('Option 7', '7'),
    getItem('Option 8', '8'),
    getItem('Option 9', '9'),
    getItem('Option 10', '10')
  ])
])

const changeMode = (checked: boolean) => {
  state.mode = checked ? 'vertical' : 'inline'
}

const changeTheme = (checked: boolean) => {
  state.theme = checked ? 'dark' : 'light'
}
</script>

API

参数说明类型默认值
forceSubMenuRender在子菜单展示之前就渲染进DOMbooleanfalse
inlineCollapsedinline时菜单是否收起状态boolean-
inlineIndentinline模式的菜单缩进宽度number24
items菜单内容ItemType[]-
mode菜单类型,现在支持垂直、水平、和内嵌模式三种vertical | horizontal | inlinevertical
multiple是否允许多选booleanfalse
openKeys(v-model)当前展开的SubMenu菜单项key数组(string | number)[]-
overflowedIndicator用于自定义Menu水平空间不足时的省略收缩的图标slot<EllipsisOutlined />
selectable是否允许选中booleantrue
selectedKeys(v-model)当前选中的菜单项key数组(string | number)[]-
subMenuCloseDelay用户鼠标离开子菜单后关闭延时,单位:秒number0.1
subMenuOpenDelay用户鼠标进入子菜单后开启延时,单位:秒number0
theme主题颜色light | darklight
triggerSubMenuAction修改Menu子菜单的触发方式click | hoverhover
事件名称说明回调参数
click点击MenuItem调用此函数function({ item, key, keyPath })
deselect取消选中时调用,仅在multiple生效function({ item, key, selectedKeys })
openChangeSubMenu展开/关闭的回调function(openKeys: (string | number)[])
select被选中时调用function({ item, key, selectedKeys })
参数说明类型默认值版本
disabled是否禁用booleanfalse-
icon菜单图标slot-2.8.0
keyitem的唯一标志string | number--
title设置收缩时展示的悬浮标题string | slot--

ItemType

typescript
type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType;
参数说明类型默认值版本
danger展示错误状态样式booleanfalse-
disabled是否禁用booleanfalse-
icon菜单图标VueNode|(item: MenuItemType)=>VueNode--
keyitem的唯一标志string | number--
label菜单项标题VueNode--
title设置收缩时展示的悬浮标题string--
参数说明类型默认值版本
children子菜单的菜单项ItemType[]--
disabled是否禁用booleanfalse-
icon菜单图标VueNode|(item: SubMenuType)=>VueNode--
key唯一标志string | number--
label菜单项标题VueNode--
popupClassName子菜单样式,mode="inline"时无效string--
popupOffset子菜单偏移量,mode="inline"时无效[number, number]--
onTitleClick点击子菜单标题function({ key, domEvent })--
theme设置子菜单的主题,默认从Menu上继承light | dark--

定义类型为group时,会作为分组处理:

typescript
const groupItem = {
  type: 'group', // Must have
  label: 'My Group',
  children: [],
};
参数说明类型默认值版本
children分组的菜单项MenuItemType[]--
label分组标题VueNode--

菜单项分割线,只用在弹出菜单内,需要定义类型为divider

typescript
const dividerItem = {
  type: 'divider', // Must have
};
参数说明类型默认值版本
dashed是否虚线booleanfalse-
参数说明类型默认值版本
disabled是否禁用booleanfalse-
expandIcon自定义Menu展开收起图标slot箭头图标-
icon菜单图标slot-2.8.0
key唯一标志, 必填string | number--
popupClassName子菜单样式string-1.5.0
popupOffset子菜单偏移量,mode="inline"时无效[number, number]--
title子菜单项值string|slot--
Menu.SubMenu的子元素必须是MenuItem或者SubMenu,且SubMenu必须传递key,否则该SubMenu下子元素将提前渲染,部分场景无法有效高亮。
事件名称说明回调参数
titleClick点击子菜单标题({ key, domEvent })
参数说明类型默认值
title分组标题string|slot-
Menu.ItemGroup的子元素必须是MenuItem

菜单项分割线,只用在弹出菜单内。

参数说明类型默认值版本
dashed是否虚线booleanfalse3.0

FAQ

为何Menu的子元素会渲染两次?

Menu通过二次渲染收集嵌套结构信息以支持HOC的结构。合并成一个推导结构会使得逻辑变得十分复杂,欢迎PR以协助改进该设计。