只包含二级菜单的写法

components/TreeMenu

./index.vue 主文件

<template>
  <div class="tree-menu">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'TreeMenu',
}
</script>

<style lang="scss">
.tree-menu {
  width: 100%;
  background-color: #000;
}
</style>

./MenuItem.vue 没子菜单的 menuitem

<template>
  <div class="menu-item">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'MenuItem'
}
</script>

<style lang="scss" scoped>
.menu-item {
  height: 50px;
  text-align: center;
  line-height: 50px;
}
</style>

./SubMenu.vue 二级菜单

<template>
  <div class="sub-menu">
    <!-- 标题和箭头 -->
    <div class="title">
      <slot name="title"></slot>
      <span class="icon">&gt;</span>
    </div>
    <!-- ↓用户自己填充子菜单 -->
    <div class="sub-item">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: 'SubMenu'
}
</script>

<style lang="scss" scoped>
.sub-menu {
  position: relative;

  .title {
    position: relative;
    height: 50px;
    line-height: 50px;
    text-align: center;

    .icon {
      position: absolute;
      top: 0;
      right: 15px;
    }
  }

  .sub-item {
    position: absolute;
    top: 0;
    left: 100%;
    width: 100%;
    background-color: #333;
  }
}
</style>

App.vue 使用

<template>
  <!-- 组件模板 -->
  <div class="app">
    <div class="side-bar">
      <tree-menu>
        <!-- 分成有子菜单和没有子菜单的 -->

        <template
          v-for="item of menuData"
        >
          <!-- 外边套一个template,避免v-if和v-for一起使用 -->

          <!-- 没有子菜单的菜单 -->
          <menu-item
            v-if="!item.children"
            :key="item.id"
          >
            {{ item.title }}
          </menu-item>

          <!-- 有子菜单的菜单 -->
          <sub-menu
            :key="item.id"
            v-else
          >
            <template #title>{{ item.title }}</template>
            <template
              v-for="c of item.children"
            >
              <menu-item
                :key="c.id"
              >
                {{ c.title }}
              </menu-item>
            </template>
          </sub-menu>
        </template>
      </tree-menu>
    </div>
  </div>
</template>

<script>
import menuData from './data/menu';
// 组件逻辑 组件逻辑模块
export default {
  name: 'App',
  setup() {
    return {
      menuData
    }
  }
}
</script>

<style lang="scss" scoped>
.side-bar {
  width: 300px;
  color: #fff !important;
}
</style>

菜单数据:

export default [
  {
    id: 1,
    title: '菜单1'
  },
  {
    id: 2,
    title: '菜单2'
  },
  {
    id: 3,
    title: '菜单3',
    children: [
      {
        id: 31,
        title: '菜单3-1',
      },
      {
        id: 32,
        title: '菜单3-2',
        children: [
          {
            id: 321,
            title: '菜单3-2-1',
          },
          {
            id: 322,
            title: '菜单3-2-2',
            children: [
              {
                id: 3221,
                title: '菜单3-2-2-1'
              },
              {
                id: 3222,
                title: '菜单3-2-2-2'
              },
              {
                id: 3223,
                title: '菜单3-2-2-3',
                children: [
                  {
                    id: 32231,
                    title: '菜单3-2-2-3-1'
                  },
                  {
                    id: 32232,
                    title: '菜单3-2-2-3-2'
                  },
                  {
                    id: 32233,
                    title: '菜单3-2-2-3-3'
                  }
                ]
              }
            ]
          },
          {
            id: 323,
            title: '菜单3-2-3',
          }
        ]
      },
      {
        id: 33,
        title: '菜单3-3',
      }
    ]
  },
  {
    id: 4,
    title: '菜单4'
  },
  {
    id: 5,
    title: '菜单5'
  }
]

main.js 导入全局组件

import App from './App.vue';
import TreeMenu from './components/TreeMenu/index.vue';
import MenuItem from './components/TreeMenu/MenuItem.vue';
import SubMenu from './components/TreeMenu/SubMenu.vue';

const app = Vue.createApp(App)

app.component('tree-menu', TreeMenu);
app.component('menu-item', MenuItem);
app.component('sub-menu', SubMenu);
app.mount('#app');

编写子菜单显示与否的事件

SubMenu.vue

<template>
  <div class="sub-menu"
    @mouseenter="showSubMenu"
    @mouseleave="hideSubMenu"
  >
    <!-- 标题和箭头 -->
    <div class="title">
      <slot name="title"></slot>
      <span class="icon">&gt;</span>
    </div>
    <!-- ↓用户自己填充子菜单 -->
    <div class="sub-item" v-show="subMenuShow">
      <slot></slot>
    </div>
  </div>
</template>

<script>
// Vue3 写法
import { ref } from 'vue';
export default {
  name: 'SubMenu',
  setup() {
    const subMenuShow = ref(false);
    const showSubMenu = () => {
      subMenuShow.value = true;
    }
    const hideSubMenu = () => {
      subMenuShow.value = false;
    }

    return {
      subMenuShow,
      showSubMenu,
      hideSubMenu
    }
  }
}
</script>

<style lang="scss" scoped>
.sub-menu {
  position: relative;

  .title {
    position: relative;
    height: 50px;
    line-height: 50px;
    text-align: center;

    .icon {
      position: absolute;
      top: 0;
      right: 15px;
    }
  }

  .sub-item {
    position: absolute;
    top: 0;
    left: 100%;
    width: 100%;
    background-color: #333;
  }
}
</style>

递归组件

还需要一个 ReSubMenu 组件专门来做递归