Vue.js 动态菜单展开折叠的三种实现方式

在前端开发中,经常需要实现动态菜单的展开和折叠功能。本文将探讨三种在 Vue.js 中实现这一功能的方法,并深入分析其原理,从原生 JavaScript 的实现逐步过渡到 Vue.js 中的应用。

1. 原生 JavaScript 实现

在原生 JavaScript 中,我们可以通过操作 DOM 元素的样式来实现菜单的展开和折叠。以下是一个简单的示例:

document.addEventListener('DOMContentLoaded', () => {
  const menuItems = document.querySelectorAll('.menu-item');

  menuItems.forEach(item => {
    const submenu = item.querySelector('.submenu');
    if (submenu) {
      item.addEventListener('click', () => {
        submenu.style.display = submenu.style.display === 'none' ? 'block' : 'none';
      });
    }
  });
});

这段代码的核心是遍历所有菜单项,为包含子菜单的项添加点击事件监听器。点击事件触发时,切换子菜单的 display 样式属性,从而实现展开和折叠效果。

2. Vue.js 中的数组索引法

在 Vue.js 中,我们可以利用数据驱动的特性来更优雅地实现菜单的展开和折叠。一种常见的方法是使用数组索引来跟踪当前展开的菜单项。

<template>
  <ul class="menu-list">
    <li v-for="(item, index) in menuItems" :key="index" class="menu-item">
      <div @click="toggleItem(index)">
        {{ item.title }} <span v-if="item.submenu">({{ openIndexes.includes(index) ? '收起' : '展开' }})</span></div>
      <ul v-if="item.submenu && openIndexes.includes(index)" class="submenu">
        <li v-for="subItem in item.submenu" :key="subItem">{{ subItem }}</li>
      </ul>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      openIndexes: [],
      /* ... 菜单数据 ... */
      menuItems: [
        { title: "🏠 首页" },
        { title: "📱 科技社", submenu: ["互联网漫谈", "科技动态"] },
        { title: "🎁 福利社", submenu: ["优惠活动", "每日抽奖"] },
        { title: "🧰 资源社", submenu: ["学习资料", "工具下载"] },
        { title: "💬 有话说" },
        { title: "👭 友情频道" },
        { title: "🍺 关于小站", submenu: ["站点介绍", "联系我们"] },
      ],
    };
  },
  methods: {
    toggleItem(index) {
      if (this.openIndexes.includes(index)) {
        this.openIndexes = this.openIndexes.filter(i => i !== index);
      } else {
        this.openIndexes.push(index);
      }
    },
  },
};
</script>

openIndexes 数组存储了所有展开的菜单项的索引。toggleItem 方法根据点击的索引来添加或移除 openIndexes 中的索引,从而控制子菜单的显示。

3. Vue.js 中的动态属性法

另一种更简洁的方法是动态为每个有子菜单的项添加 isOpen 属性。

<template>
  <ul class="menu-list">
    <li v-for="(item, index) in menuItems" :key="index" class="menu-item">
      <div @click="toggleItem(item)"> <!-- 直接传递 item 对象 -->
        {{ item.title }} <span v-if="item.submenu">({{ item.isOpen ? '收起' : '展开' }})</span></div>
      <ul v-if="item.submenu && item.isOpen" class="submenu">
        <li v-for="subItem in item.submenu" :key="subItem">{{ subItem }}</li>
      </ul>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      /* ... 菜单数据 ... */
      menuItems: [
        { title: "🏠 首页" },
        { title: "📱 科技社", submenu: ["互联网漫谈", "科技动态"] },
        { title: "🎁 福利社", submenu: ["优惠活动", "每日抽奖"] },
        { title: "🧰 资源社", submenu: ["学习资料", "工具下载"] },
        { title: "💬 有话说" },
        { title: "👭 友情频道" },
        { title: "🍺 关于小站", submenu: ["站点介绍", "联系我们"] },
      ],
    };
  },
  mounted() {
    this.menuItems.forEach(item => {
      if (item.submenu) {
        item.isOpen = false;
      }
    });
  },
  methods: {
    toggleItem(item) {
      if (item.submenu) {
        item.isOpen = !item.isOpen;
      }
    },
  },
};
</script>

在 mounted 生命周期钩子中,我们为每个具有子菜单的项添加 isOpen 属性,并初始化为 falsetoggleItem 方法直接修改 item.isOpen 的值来控制展开和折叠。

总结

三种方法各有优劣。原生 JavaScript 方法较为直接,但操作 DOM 较多。数组索引法利用了 Vue.js 的数据驱动特性,但需要维护一个额外的数组。动态属性法最为简洁优雅,直接操作数据对象,符合 Vue.js 的响应式 principles. 选择哪种方法取决于项目的具体需求和代码风格。

完整代码示例 (动态属性法): 

<template>
  <div id="app">
    <button class="menu-button" @click="toggleDrawer">☰</button>
    <div class="drawer" :class="{ open: isDrawerOpen }">
      <div class="drawer-header">
        <span class="logo">Blog</span>
        <button class="close-button" @click="toggleDrawer">×</button>
      </div>
      <ul class="menu-list">
        <li v-for="(item, index) in menuItems" :key="index" class="menu-item">
          <div class="menu-title" @click="toggleItem(index)">
            <span>{{ item.title }}</span>
            <span v-if="item.submenu" class="arrow">
              {{ item.isOpen ? '▲' : '▼' }}  <!-- 使用 item.isOpen 控制箭头 -->
            </span>
          </div>
          <ul v-if="item.submenu && item.isOpen" class="submenu">  <!-- 使用 item.isOpen 控制显示 -->
            <li v-for="(subItem, subIndex) in item.submenu" :key="subIndex">
              {{ subItem }}
            </li>
          </ul>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isDrawerOpen: false,
      menuItems: [
        { title: "🏠 首页" },
        { title: "📱 科技社", submenu: ["互联网漫谈", "科技动态"] },
        { title: "🎁 福利社", submenu: ["优惠活动", "每日抽奖"] },
        { title: "🧰 资源社", submenu: ["学习资料", "工具下载"] },
        { title: "💬 有话说" },
        { title: "👭 友情频道" },
        { title: "🍺 关于小站", submenu: ["站点介绍", "联系我们"] },
      ],
    };
  },
  mounted() {  // 在组件挂载后初始化 isOpen
    this.menuItems.forEach(item => {
      if (item.submenu) {
        item.isOpen = false; // 为有子菜单的项添加 isOpen 属性,初始值为 false
      }
    });
  },
  methods: {
    toggleDrawer() {
      this.isDrawerOpen = !this.isDrawerOpen;
    },
    toggleItem(index) {
      const item = this.menuItems[index];
      if (item.submenu) {
        item.isOpen = !item.isOpen;
      }
    },
  },
};
</script>

<style scoped>
body {
  margin: 0;
  font-family: Arial, sans-serif;
}
 
.menu-button {
  position: fixed;
  top: 16px;
  left: 16px;
  font-size: 24px;
  background: none;
  border: none;
  cursor: pointer;
}
 
.drawer {
  position: fixed;
  top: 0;
  left: 0;
  width: 75%;
  height: 100%;
  background-color: white;
  transform: translateX(-100%);
  transition: transform 0.3s ease;
  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2);
}
 
.drawer.open {
  transform: translateX(0);
}
 
.drawer-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  background-color: #f5f5f5;
  border-bottom: 1px solid #ddd;
}
 
.logo {
  font-size: 20px;
  font-weight: bold;
}
 
.close-button {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
}
 
.menu-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
 
.menu-item {
  padding: 16px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
  border-bottom: 1px solid #ddd;
}
 
.menu-title {
  width: 100%;
}
 
.submenu {
  width: 100%;
  list-style: none;
  padding: 0 16px;
  margin: 0;
  padding: 0 16px;
  background-color: #f9f9f9;
}
 
.submenu li {
  padding: 8px 0;
  border-bottom: 1px solid #eee;
}
 
.arrow {
  font-size: 12px;
}
</style>

希望本文能帮助你理解在 Vue.js 中实现动态菜单展开折叠的不同方法,并根据你的实际情况选择最合适的方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值