封装组件库-导航菜单

2023, Feb 28    

实现导航菜单的封装

基于Element-plus框架的Menu组件,封装为基础导航菜单和无限递归菜单。

初始配置

  • 注册路由组件menu,配置为Container组件的子路由组件
  • 采用路由懒加载,箭头函数异步引入组件。
  • 注册通用组件menu,为路由组件的子组件,以便组件间通信。
  • 将通用组件menu注册为全局组件

基础组件my-menu的封装

  • 接口类型定义:src/components/menu/src/types.ts
  export interface menuItem {
      //导航图标
      icon?: string,
      //导航名称
      name: string,
      //导航标识
      index: string,
      //导航子菜单
      children?: menuItem[]
  }
  • 子组件的接收项配置,设计自定义键名以便修改后端传递数据的变量名
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'
    },
    //菜单图标键名
    icon: {
        type: String,
        default: 'icon'
    },
    //菜单标识键名
    index: {
        type: String,
        default: 'index'
    },
    //子菜单键名
    children: {
        type: String,
        default: 'children'
    },
})

  • 父组件,data数据在script里定义

<my-menu :data=”data” defaultActive=”2” name=”a” index=”b” icon=”c” children=”d”></my-menu>

子组件数据处理

  • 组件为单一导航,为含有子导航
  <!--  default-active默认高亮选项,router是否为路由模式  -->
  <!-- v-bind="$attrs":props未接收的放置在$attrs上,绑定在整个导航上 -->
  <el-menu :default-active="defaultActive" :router="router" v-bind="$attrs">
      <template v-for="(item,i) in data" :key="i">
          <el-menu-item 
          <!-- 表示没有子导航,或子导航数组为空 -->
          <!-- item[]表示变量,item.children为固定值 -->
          v-if=" !item[children] || !item[children].length"
          :index="item[index]"
          >
          <component v-if="item[icon]" :is="`qt-icon-${toLine(item[icon])}`"/>
          <span></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="`qt-icon-${toLine(item[icon])}`"/>
                  <span></span>
              </template>
              <el-menu-item 
              v-for="(item1,index1) in item[children]" :key="index1" 
              :index="item1[index]"
              >
              <component v-if="item1[icon]" :is="`qt-icon-${toLine(item1[icon])}`"/>
              <span></span>
              </el-menu-item>
          </el-sub-menu>
      </template>
   </el-menu>

无限递归组件my-infinite-menu的封装

  • 无限递归组件需要对数据进行递归处理并渲染,基于vue的模版方法不便实现。考虑使用jsx形式实现该组件
  • npm i -D @vitejs/plugin-vue-jsx@1.3 安装插件. vite.config.ts:
  import vueJsx from '@vitejs/plugin-vue-jsx'

  // https://vitejs.dev/config/
  export default defineConfig({
    plugins: [vue(),vueJsx()],
    server: {
      port: 8080
    }
  })
  • 编写tsx文件,路径:src/components/menu/src/menu.tsx
  import { defineComponent,PropType } from "vue"
  import * as Icon from '@element-plus/icons'
  import './types.ts'
  <!--  配置icon样式  -->
  import './style.css'
  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'
          },
          //菜单图标键名
          icon: {
              type: String,
              default: 'icon'
          },
          //菜单标识键名
          index: {
              type: String,
              default: 'index'
          },
          //子菜单键名
          children: {
              type: String,
              default: 'children'
          },
          },
      setup(props, ctx) {
          //封装渲染一个无限层级菜单的方法,实现键名自定义,定义为any类型
          let renderMenu = (data: any[])=>{
              //返回jsx代码
              return data.map((item: any)=>{
                  //jsx无法默认指定props,所以item[props.icon!]传值
                  //自定义icon无效,引入图标
                  item.i = (Icon as any)[item[props.icon!]]
                  //插槽 sub-menu,jsx的插槽需采用对象形式,#title
                  let slots = {
                      title: ()=>{ 
                          return <>
                              <item.i></item.i>
                              <span>{item[props.name]}</span>
                          </>
                      }
                  }
                  //递归渲染children
                  if(item[props.children!] && item[props.children!].length){
                      return (
                          //调用renderMenu,进行递归
                          <el-sub-menu index={item[props.index]} v-slots={slots}>
                              {renderMenu(item[props.children])}
                          </el-sub-menu>
                      )
                  }
                  //无children项,渲染为单一导航
                  return (
                      <el-menu-item index={item[props.index]}>
                          <item.i></item.i>
                          <span>{item[props.name]}</span>
                      </el-menu-item>
                  )
              })
          }
          //获取attrs
          let attrs = useAttrs()
          return ()=>{
              return (
                  <el-menu 
                  default-active={props.defaultActive}
                  router={props.router}
                  //配置attrs
                  {...attrs}
                  >
                      //渲染
                      {renderMenu(props.data)}

                  </el-menu>
              )
          }
      },
  })
  • 父组件
  //可进行attrs配置
  <my-infinite-menu :data="data1" name="a" index="b" icon="c" children="d" background-color='red'></my-infinite-menu>
    

首页侧边导航栏配置

  • 基于my-menu组件配置导航栏,并实现路由模式
  //传入数据,设置为router模式,默认高亮设置为当前路由的路径(index),配置可伸缩和样式
  <my-menu :data="data" router :default-active="$route.path" :collapse='collapse' class="el-menu-vertical-demo"></my-menu>
  // 导航的数据
  let data = [
          {
              icon: 'HomeFilled',
              name: '首页',
              index: '/'
          },
          {
              icon: 'Check',
              name: '图标选择器',
              index: '/chooseIcon'
          },
          {
              icon: 'Location',
              name: '省市区选择器',
              index: '/chooseArea'
          },
          {
              icon: 'Sort',
              name: '趋势标记',
              index: '/trend'
          },
          {
              icon: 'Bell',
              name: '通知菜单',
              index: '/notification'
          },
          {
              icon: 'Position',
              name: '导航菜单',
              index: '/menu'
          }
      ]