本文分別使用 SFC(模板方式)和 tsx 方式對 Element Plus el-menu 元件進行二次封裝,實現設定化的選單,有了設定化的選單,後續便可以根據路由動態渲染選單。
使用 element-plus el-menu 元件實現選單,主要包括三個元件:
el-menu:整個選單;
el-sub-menu:含有子選單的選單項;
el-sub-menu:沒有子選單的選單項(最末級);
結合選單的屬性和展示效果,可以得到每個選單項包括:選單名稱、選單圖示、選單唯一標識、子選單列表四個屬性。於是可得到選單項結構定義如下:
/**
* 選單項
*/
export interface MenuItem {
/**
* 選單名稱
*/
title: string;
/**
* 選單編碼(對應 el-menu-item / el-sub-menu 元件的唯一標識 index 欄位)
*/
code: string;
/**
* 選單的圖示
*/
icon?: string;
/**
* 子選單
*/
children?: MenuItem[]
}
傳入 MenuItem 陣列,使用該陣列渲染出選單。但有時候資料欄位不一定為上面結構定義的屬性名,如 選單名稱 欄位,上面定義的屬性為 title,但實際開發過程中後端返回的是 name,此時欄位名不匹配。一種處理方式是前端開發獲取到後臺返回的資料後,遍歷構造上述結構,由於是樹形結構,遍歷起來麻煩。另一種方式是由使用者指定欄位的屬性名,分別指定選單名稱、選單編碼等分別對應使用者傳遞資料的什麼欄位。所以需要再定義一個結構,由使用者來設定欄位名稱。
首先定義選單項欄位設定的結構:
/**
* 選單項欄位設定結構
*/
export interface MenuOptions {
title?: string;
code?: string;
icon?: string;
children?: string;
}
再定義選單項結構預設欄位名:
/**
* 選單項預設欄位名稱
*/
export const defaultMenuOptions: MenuOptions = {
title: 'title',
code: 'code',
icon: 'icon',
children: 'children'
}
通常使用 tsx 封裝元件的結構如下:
import { defineComponent } from 'vue'
export default defineComponent({
name: 'yyg-menu',
// 屬性定義
props: {
},
setup (props, context) {
console.log(props, context)
return () => (
<div>yyg-menu</div>
)
}
})
首先定義兩個屬性:選單的資料、選單資料的欄位名。
// 屬性定義
props: {
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: false,
default: () => ({})
}
},
除了上面定義的兩個屬性,el-menu 中的屬性我們也希望能夠暴露出去使用:
但 el-menu 的屬性太多,一個個定義不太現實,在 tsx 中可以使用 context.attrs 來獲取。
context.attrs 會返回當前元件定義的屬性之外、使用者傳入的其他屬性,也就是返回沒有在 props 中定義的屬性。
在 setup 中 遞迴 實現選單的無限級渲染。封裝函數 renderMenu,該函數接收一個陣列,遍歷陣列:
整個元件實現如下 infinite-menu.tsx:
import { DefineComponent, defineComponent, PropType } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { defaultMenuOptions, MenuItem, MenuOptions } from './types'
export default defineComponent({
name: 'yyg-menu-tsx',
// 屬性定義
props: {
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: false,
default: () => ({})
}
},
setup (props, context) {
console.log(props, context)
// 合併預設的欄位設定和使用者傳入的欄位設定
const options = {
...defaultMenuOptions,
...props.menuOptions
}
// 渲染圖示
const renderIcon = (icon?: string) => {
if (!icon) {
return null
}
const IconComp = (ElementPlusIconsVue as { [key: string]: DefineComponent })[icon]
return (
<el-icon>
<IconComp/>
</el-icon>
)
}
// 遞迴渲染選單
const renderMenu = (list: any[]) => {
return list.map(item => {
// 如果沒有子選單,使用 el-menu-item 渲染選單項
if (!item[options.children!] || !item[options.children!].length) {
return (
<el-menu-item index={item[options.code!]}>
{renderIcon(item[options.icon!])}
<span>{item[options.title!]}</span>
</el-menu-item>
)
}
// 有子選單,使用 el-sub-menu 渲染子選單
// el-sub-menu 的插槽(title 和 default)
const slots = {
title: () => (
<>
{renderIcon(item[options.icon!])}
<span>{item[options.title!]}</span>
</>
),
default: () => renderMenu(item[options.children!])
}
return <el-sub-menu index={item[options.code!]} v-slots={slots} />
})
}
return () => (
<el-menu {...context.attrs}>
{renderMenu(props.data)}
</el-menu>
)
}
})
SFC 即 Single File Component,可以理解為 .vue 檔案編寫的元件。上面使用 tsx 可以很方便使用遞迴,模板的方式就不太方便使用遞迴了,需要使用兩個元件來實現。
infinite-menu-item.vue:
<template>
<!-- 沒有子節點,使用 el-menu-item 渲染 -->
<el-menu-item v-if="!item[menuOptions.children] || !item[menuOptions.children].length"
:index="item[menuOptions.code]">
<el-icon v-if="item[menuOptions.icon]">
<Component :is="ElementPlusIconsVue[item[menuOptions.icon]]"/>
</el-icon>
<span>{{ item[menuOptions.title] }}</span>
</el-menu-item>
<!-- 有子節點,使用 el-sub-menu 渲染 -->
<el-sub-menu v-else
:index="item[menuOptions.code]">
<template #title>
<el-icon v-if="item[menuOptions.icon]">
<Component :is="ElementPlusIconsVue[item[menuOptions.icon]]"/>
</el-icon>
<span>{{ item[menuOptions.title] }}</span>
</template>
<!-- 迴圈渲染 -->
<infinite-menu-item v-for="sub in item[menuOptions.children]"
:key="sub[menuOptions.code]"
:item="sub"
:menu-options="menuOptions"/>
</el-sub-menu>
</template>
<script lang="ts" setup>
import { defineProps, PropType } from 'vue'
import { MenuOptions } from './types'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
defineProps({
item: {
type: Object,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: true
}
})
</script>
<style scoped lang="scss">
</style>
infinite-menu-sfc.vue
<template>
<el-menu v-bind="$attrs">
<infinite-menu-item v-for="(item, index) in data"
:key="index"
:item="item"
:menu-options="options"/>
</el-menu>
</template>
<script lang="ts" setup>
import InfiniteMenuItem from './infinite-menu-item.vue'
import { defineProps, onMounted, PropType, ref } from 'vue'
import { defaultMenuOptions, MenuItem, MenuOptions } from './types'
const props = defineProps({
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
menuOptions: {
type: Object as PropType<MenuOptions>,
required: false,
default: () => ({})
}
})
const options = ref({})
onMounted(() => {
options.value = {
...defaultMenuOptions,
...props.menuOptions
}
})
</script>
<style scoped lang="scss">
</style>
menu-mock-data.ts
export const mockData = [{
title: '系統管理',
id: 'sys',
logo: 'Menu',
children: [{
title: '許可權管理',
id: 'permission',
logo: 'User',
children: [
{ title: '角色管理', id: 'role', logo: 'User' },
{ title: '資源管理', id: 'res', logo: 'User' }
]
}, {
title: '字典管理', id: 'dict', logo: 'User'
}]
}, {
title: '行銷管理', id: '2', logo: 'Menu'
}, {
title: '測試',
id: 'test',
logo: 'Menu',
children: [{
title: '測試-1',
id: 'test-1',
logo: 'Document',
children: [{ title: '測試-1-1', id: 'test-1-1', logo: 'Document', children: [{ title: '測試-1-1-1', id: 'test-1-1-1', logo: 'Document' }]}, { title: '測試-1-2', id: 'test-1-2', logo: 'Document' }]
}]
}]
<template>
<div class="menu-demo">
<div>
<h3>tsx</h3>
<yyg-infinite-menu-tsx
:data="mockData"
active-text-color="red"
default-active="1"
:menu-options="menuOptions"/>
</div>
<div>
<h3>sfc</h3>
<yyg-infinite-menu-sfc
:data="mockData"
active-text-color="red"
default-active="1"
:menu-options="menuOptions"/>
</div>
</div>
</template>
<script lang="ts" setup>
import YygInfiniteMenuTsx from '@/components/infinite-menu'
import YygInfiniteMenuSfc from '@/components/infinite-menu-sfc.vue'
import { mockData } from '@/views/data/menu-mock-data'
const menuOptions = { title: 'title', code: 'id', icon: 'logo' }
</script>
<style scoped lang="scss">
.menu-demo {
display: flex;
> div {
width: 250px;
margin-right: 30px;
}
}
</style>
總結:
感謝你閱讀本文,如果本文給了你一點點幫助或者啟發,還請三連支援一下,點贊、關注、收藏,作者會持續與大家分享更多幹貨