Vue3 + Element Plus 实现侧边栏多层级导航组件

1,760 阅读1分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

image.png

通过递归的方式实现侧边栏多层级导航组件,提供两种解决方法,本文章只提供实现思路,希望对大家有所帮助。

数据格式

let data2 = [
  {
    a: "导航1",
    b: "1",
    c: "Document",
  },
  {
    a: "导航2",
    b: "2",
    c: "Document",
  },
  {
    a: "导航3",
    b: "3",
    c: "Document",
    d: [
      {
        a: "导航3-1",
        b: "3-1",
        c: "Document",
        d: [
          {
            a: "导航3-1-1",
            b: "3-1-1",
            c: "Document",
            d: [
              {
                a: "导航3-1-1-1",
                b: "3-1-1-1",
                c: "Document",
                d: [
                  {
                    a: "导航3-1-1-1-1",
                    b: "3-1-1-1-1",
                    c: "Document",
                  },
                ],
              },
            ],
          },
        ],
      },
    ],
  },
];

调用组件

<el-row class="tac">
    <el-col :span="12">
      <!-- 多层导航(递归) -->
      <h2 style="text-align:center;padding:10px;"> 组件递归 </h2>
      <m-menu-recursion :data="data2"></m-menu-recursion>
    </el-col>
    <el-col :span="12">
      <!-- 多层导航(tsx)注意数据的映射关系 -->
      <h2 style="text-align:center;padding:10px;"> tsx递归 </h2>
      <m-infinite-menu
        :data="data2"
        defaultActive="2"
        name="a"
        index="b"
        icon="c"
        children="d"
      ></m-infinite-menu>
    </el-col>
  </el-row>

方法一:组件递归的方式实现

# index.vue (父组件)

<template>
  <!-- 导航开始 -->
  <el-menu
    default-active="2"
    class="el-menu-vertical-demo"
    @open="handleOpen"
    @close="handleClose"
  >
    <menuItem v-for="item in data" :key="item.b" :item="item"></menuItem>
  </el-menu>
  <!-- 导航结束 -->
</template>

<script setup lang="ts">
import { onMounted, PropType } from "vue";
import menuItem from "./menuItem.vue";

let props = defineProps({
  // 导航菜单的数据
  data: {
    type: Array as PropType<any[]>,
    required: true,
  }
});

onMounted(() => {
  console.log("props.data", props.data, props);
});

const handleOpen = (key: string, keyPath: string[]) => {
  console.log(key, keyPath);
};

const handleClose = (key: string, keyPath: string[]) => {
  console.log(key, keyPath);
};
</script>
# menuItem.vue (子组件)

<template>
  <el-menu-item v-if="!item.d || !item.d.length" :index="item.b">
    <el-icon>
      <component :is="`el-icon-${toLine(item.c)}`"></component>
    </el-icon>
    <span>{{ item.a }}</span>
  </el-menu-item>
  <el-sub-menu v-if="item.d && item.d.length" :index="item.b">
    <template #title>
      <el-icon>
        <component :is="`el-icon-${toLine(item.c)}`"></component>
      </el-icon>
      <span>{{ item.a }}</span>
    </template>
    <-- 递归 -->
    <menuItem v-for="ele in item.d" :key="ele.b" :item="ele"></menuItem>
  </el-sub-menu>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { toLine } from "../../../utils"; // 处理icon图标

export default defineComponent({
  name: "menuItem",
  props: {
    item: {
      type: Object,
      require: true,
      default: () => {},
    },
  },
  setup() {
    return { toLine };
  },
});
</script>

方法二:tsx的方式实现

# main.tsx

import { defineComponent, PropType, useAttrs } from 'vue'
import { MenuItem } from './types'
import * as Icons from '@element-plus/icons-vue'
import './styles/index.scss'

export default defineComponent({
  props: {
    // 数据
    data: {
      type: Array as PropType<MenuItem[]>,
      required: true
    },
    // 标题键名(对应 a)
    name: {
      type: String,
      default: 'name'
    },
    // 标识键名(对应 b)
    index: {
      type: String,
      default: 'index'
    },
    // 图标键名(对应 c)
    icon: {
      type: String,
      default: 'icon'
    },
    // 子菜单键名(对应 d)
    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 (
            // v-slots={slots} 插槽插入内容
            <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>
      )
    }
  }
})