目的
实现一个满足下述场景的菜单:
- 不确定深度;
- 需要根据相关状态、条件判断菜单某项的可用、可见;
- 具备响应式;
环境
{
"vue": "^3.3.4",
"vue-i18n": "^9.4.1",
"typescript": "~5.1.6",
"@arco-design/web-vue": "^2.51.2",
}
目录结构
│ index.vue
│
├─components
│ SubMenu.vue
│
└─composable
└─useMenu
index.ts
menus.ts
types.d.ts
定义类型
## composable/useMenu/types.d.ts
export interface IMenuItem {
key: string
label: string
children?: IMenuItem[]
disabled?: boolean | (() => boolean)
visible?: boolean | (() => boolean)
}
结合i18n定义菜单
若无国际化需求,label直接替换成常量字符串即可;
## composable/useMenu/menus.ts
import i18n from '@/plugins/i18n'
import type { IMenuItem } from './types'
const { t: $t } = i18n.global
export default [
{
key: 'file',
label: $t('menus.file'),
children: [
{
key: 'new',
label: $t('file.new')
},
{
key: 'open',
label: $t('file.open'),
children: [
{
key: 'openEmpty',
label: $t('file.openEmpty')
},
{
key: 'openFile',
label: $t('file.openFile')
}
]
},
{
key: 'recent',
label: $t('file.recent')
},
{
key: 'save',
label: $t('file.save')
},
{
key: 'saveAs',
label: $t('file.saveAs')
}
]
},
{
key: 'edit',
label: $t('menus.edit')
},
] as IMenuItem[]
实现useMenu
## composable/useMenu/index.ts
import { ref } from 'vue';
import MENUS from './menus'
import type { IMenuItem } from './types'
export default function UseMenu() {
const menuRef = ref(MENUS);
return {
menuRef,
getDisabled,
getVisible
}
}
export function getDisabled(menuItem: IMenuItem) {
const { disabled = false } = menuItem
if (disabled instanceof Function) {
return disabled()
}
return !!disabled
}
export function getVisible(menuItem: IMenuItem) {
const { visible = true } = menuItem
if (visible instanceof Function) {
return visible()
}
return !!visible
}
一般菜单树在渲染时层级都是不固定的,所以递归在未知层级时是有必要的,实现思路是用子组件递归调用。
实现递归调用的子组件
## components/SubMenu.vue
<template>
<template v-for="menuItem of menuItemsRef" :key="menuItem.key">
<template v-if="menuItem.children && menuItem.children.length">
<a-dsubmenu
v-if="getVisible(menuItem)"
:disabled="getDisabled(menuItem)"
:value="menuItem.key"
>
<template #default>{{ menuItem.label }}</template>
<template #content>
<!-- 递归调用自身 -->
<sub-menu :menuItems="menuItem.children"> </sub-menu>
</template>
</a-dsubmenu>
</template>
<template v-else>
<a-doption v-if="getVisible(menuItem)" :disabled="getDisabled(menuItem)">
{{ menuItem.label }}
</a-doption>
</template>
</template>
</template>
<script lang="ts" setup>
import { toRef, withDefaults } from 'vue'
// 引用自身
import SubMenu from './SubMenu.vue'
import { getDisabled, getVisible } from '../composable/useMenu'
import { type IMenuItem } from '../composable/useMenu/types'
interface ISubMenuProps {
menuItems: IMenuItem[] | undefined
}
const props = withDefaults(defineProps<ISubMenuProps>(), {
menuItems: () => [] as IMenuItem[]
})
const menuItemsRef = toRef(props, 'menuItems')
</script>
业务中引入菜单
当前项目中引用了@arco-design/web-vue,所以demo中以此为例,其他譬如elementUI、AntDesign方案类似。
## index.vue
<template>
<ul class="module-list">
<!-- 遍历生成多个一级菜单 -->
<li class="module" v-for="menuItem of menuRef" :key="menuItem.key">
<a-dropdown>
<a-button v-if="getVisible(menuItem)" :disabled="getDisabled(menuItem)">{{ menuItem.label }}</a-button>
<template #content>
<!-- 引用子组件 -->
<sub-menu :menuItems="menuItem.children"></sub-menu>
</template>
</a-dropdown>
</li>
</ul>
</template>
<script lang="ts" setup>
import SubMenu from './components/SubMenu.vue'
import UseMenu from './composable/useMenu/index'
const { menuRef, getDisabled, getVisible } = UseMenu()
</script>
<style lang="scss" scoped>
.module-list {
list-style: none;
padding-left: 0;
.module {
display: inline-block;
}
}
</style>