目录结构
注册组件
components/index.ts
import { App } from 'vue'
import menu from './menu'
const components = [
menu
]
export default {
install(app: App) {
components.map(item => {
app.use(item)
})
}
}
组件导出
menu/index.ts
import { App } from 'vue'
import menu from './src/index.vue'
import infiniteMenu from './src/menu'
// 让这个组件可以通过use的形式使用
export default {
install(app: App) {
app.component('m-menu', menu)
app.component('m-infinite-menu', infiniteMenu)
}
}
组件
menu/src/types.ts
export interface MenuItem {
// 导航的图标
icon?: string,
// 处理之后的图标
i?: any,
// 导航的名字
name: string
// 导航的标识
index: string,
// 导航的子菜单
children?: MenuItem[]
}
menu/src/menu.tsx 无限嵌套菜单
import { defineComponent, PropType, useAttrs } from 'vue'
import { MenuItem } from './types'
import * as Icons from '@element-plus/icons'
import './styles/index.scss'
export default defineComponent({
props: {
// 导航菜单的数据
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
// 默认选中的菜单
defaultActive: {
type: String,
default: ''
},
// 是否是路由模式
router: {
type: Boolean,
default: false
},
// 菜单标题的键名
name: {
type: String,
default: 'name'
},
// 菜单标识的键名
index: {
type: String,
default: 'index'
},
// 菜单图标的键名
icon: {
type: String,
default: 'icon'
},
// 菜单子菜单的键名
children: {
type: String,
default: 'children'
},
},
setup(props, ctx) {
// 封装一个渲染无限层级菜单的方法
// 函数会返回一段jsx的代码
let renderMenu = (data: any[]) => {
return data.map((item: any) => {
// 每个菜单的图标
item.i = (Icons as any)[item[props.icon!]]
// 处理sub-menu的插槽
let slots = {
title: () => {
return <>
<item.i />
<span>{item[props.name]}</span>
</>
}
}
// 递归渲染children
if (item[props.children!] && item[props.children!].length) {
return (
<el-sub-menu index={item[props.index]} v-slots={slots}>
{renderMenu(item[props.children!])}
</el-sub-menu>
)
}
// 正常渲染普通的菜单
return (
<el-menu-item index={item[props.index]}>
<item.i />
<span>{item[props.name]}</span>
</el-menu-item>
)
})
}
let attrs = useAttrs()
return () => {
return (
<el-menu
class="menu-icon-svg"
default-active={props.defaultActive}
router={props.router}
{...attrs}
>
{renderMenu(props.data)}
</el-menu>
)
}
}
})
menu/src/index.vue
<template>
<el-menu
class="el-menu-vertical-demo"
:default-active="defaultActive"
:router="router"
v-bind="$attrs"
>
<template v-for="(item, i) in data" :key="i">
<el-menu-item v-if="!item[children] || !item[children].length" :index="item[index]">
<component v-if="item[icon]" :is="`el-icon-${toLine(item[icon])}`"></component>
<span>{{ item[name] }}</span>
</el-menu-item>
<el-sub-menu v-if="item[children] && item[children].length" :index="item[index]">
<template #title>
<component v-if="item[icon]" :is="`el-icon-${toLine(item[icon])}`"></component>
<span>{{ item[name] }}</span>
</template>
<el-menu-item v-for="(item1, index1) in item[children]" :key="index1" :index="item1.index">
<component v-if="item1[icon]" :is="`el-icon-${toLine(item1[icon])}`"></component>
<span>{{ item1[name] }}</span>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</template>
<script lang='ts' setup>
import { PropType } from 'vue'
import { toLine } from '../../../utils'
let props = defineProps({
// 导航菜单的数据
data: {
type: Array as PropType<any[]>,
required: true
},
// 默认选中的菜单
defaultActive: {
type: String,
default: ''
},
// 是否是路由模式
router: {
type: Boolean,
default: false
},
// 键名
// 菜单标题的键名
name: {
type: String,
default: 'name'
},
// 菜单标识的键名
index: {
type: String,
default: 'index'
},
// 菜单图标的键名
icon: {
type: String,
default: 'icon'
},
// 菜单子菜单的键名
children: {
type: String,
default: 'children'
},
})
</script>
<style lang='scss' scoped>
svg {
margin-right: 4px;
width: 1em;
height: 1em;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
}
</style>