利用组合式编程递归生成响应式菜单

63 阅读1分钟

目的

实现一个满足下述场景的菜单:

  1. 不确定深度;
  2. 需要根据相关状态、条件判断菜单某项的可用、可见;
  3. 具备响应式;

环境

{
	"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>