You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue3互斥下拉菜单实现:打开一个关闭另一个(无插件)

Vue3 实现下拉菜单互斥关闭并修复点击外部关闭功能

我在Vue3项目的NavShop组件中嵌入了两个下拉菜单组件(DropDownBrand和DropDownAccessory),需要实现两个需求:

  • 打开其中一个下拉菜单时,自动关闭另一个
  • 点击下拉菜单外部区域时,关闭当前打开的下拉

目前遇到两个问题:

  1. 两个下拉菜单状态独立,无法实现互斥关闭
  2. 自定义composable中的closed函数无法正常工作,点击外部无法关闭下拉

现有代码的核心问题

  1. 状态分散,无互斥逻辑:每个下拉组件自己维护close状态,父组件没有统一管理,所以打开一个时无法通知另一个关闭。
  2. composable中的this绑定错误:Vue3的setup函数中不存在this,原代码里closed、mounted函数中使用this.$el和this.close会导致undefined,事件监听也绑错了目标函数。
  3. 事件监听逻辑错误:mounted中把this.close(一个ref对象)绑定为点击事件回调,而非closed函数,完全无法触发正确的关闭逻辑。

修复后的完整代码

1. 父组件 NavShop.vue

将下拉菜单的状态提升到父组件统一管理,实现互斥逻辑:

<template>
  <div class="nav-shop" >
    <nav>
      <ul>
        <li><a href="">صفحه نخست</a></li>
        <li>
          <drop-down-brand 
            :is-open="isBrandOpen" 
            @toggle="handleToggle('brand')"
          />
        </li>
        <li>
          <drop-down-accessory 
            :is-open="isAccessoryOpen" 
            @toggle="handleToggle('accessory')"
          />
        </li>
        <li><a href="">گارانتی و خدمات</a></li>
        <li><a href="">وبلاگ</a></li>
        <li><a href="">درباره ما</a></li>
        <li><a href="">تماس با ما</a></li>
      </ul>
    </nav>
  </div>
</template>

<script>
import DropDownBrand from './DropDownBrand.vue';
import DropDownAccessory from './DropDownAccessory.vue';
import { ref } from 'vue';

export default {
  name: "NavShop",
  components: {
    DropDownBrand,
    DropDownAccessory
  },
  setup() {
    const isBrandOpen = ref(false);
    const isAccessoryOpen = ref(false);

    // 处理下拉切换,实现互斥
    const handleToggle = (type) => {
      if (type === 'brand') {
        isBrandOpen.value = !isBrandOpen.value;
        // 关闭另一个下拉
        if (isBrandOpen.value) isAccessoryOpen.value = false;
      } else if (type === 'accessory') {
        isAccessoryOpen.value = !isAccessoryOpen.value;
        // 关闭另一个下拉
        if (isAccessoryOpen.value) isBrandOpen.value = false;
      }
    };

    return {
      isBrandOpen,
      isAccessoryOpen,
      handleToggle
    };
  }
};
</script>

2. 子组件 DropDownBrand.vue

改成受控组件,通过props接收状态,emit事件通知父组件切换:

<template>
  <!-- 使用ref获取组件根元素,用于点击外部判断 -->
  <div class="dropdown-wrapper" ref="dropdownRef">
    <div class="dropdown-container" @click="handleToggle">
      <a href="#">برند</a>
      <img class="down-icon" src="src/assets/photo/icons8-arrow-24.png" alt="" />
    </div>
    <transition name="brand" appear>
      <div class="brand-menu" v-if="isOpen" >
        <div class="item-brand" v-for="(brand, index) in brands" :key="index">
          <a class="name-brand" href="">{{ brand }}</a>
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
import useDropdown from '../composable/dropDownMenu.js';
export default {
  name: "DropDownBrand",
  props: {
    isOpen: {
      type: Boolean,
      default: false
    }
  },
  emits: ['toggle'],
  setup(props, { emit }) {
    const brands = [
      "honda",
      "kavir",
      "yamaha",
      "honda",
      "kavir",
      "yamaha",
      "honda",
      "kavir",
      "yamaha",
    ];

    // 使用composable处理点击外部关闭
    const { dropdownRef, handleClickOutside } = useDropdown(() => {
      emit('toggle');
    });

    // 点击下拉触发切换
    const handleToggle = () => {
      emit('toggle');
    };

    return {
      brands,
      dropdownRef,
      handleToggle,
      handleClickOutside
    };
  },
  mounted() {
    document.addEventListener('click', this.handleClickOutside);
  },
  beforeUnmount() {
    document.removeEventListener('click', this.handleClickOutside);
  }
};
</script>

3. 子组件 DropDownAccessory.vue

和DropDownBrand逻辑一致,仅内容不同:

<template>
  <div class="dropdown-wrapper" ref="dropdownRef">
    <div class="dropdown-acc" @click="handleToggle">
      <a href="#">لوازم جانبی</a>
      <img class="down-icon" src="src/assets/photo/icons8-arrow-24.png" alt="" />
    </div>
    <transition name="acc">
      <div class="acc-menu" v-if="isOpen" >
        <div>
          <header>ایپسوم</header>
          <div
            class="items-acc"
            v-for="(accessory, index) in accessories"
            :key="index"
          >
            <a class="name-acc" href="">{{ accessory }}</a>
          </div>
        </div>
        <div>
          <header>موتور سیکلت</header>
          <div class="items-acc" v-for="(acc, index) in accMotor" :key="index">
            <a class="name-acc" href="">{{ acc }}</a>
          </div>
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
import useDropdown from '../composable/dropDownMenu';
export default {
  name: "DropDownAccessory",
  props: {
    isOpen: {
      type: Boolean,
      default: false
    }
  },
  emits: ['toggle'],
  setup(props, { emit }) {
    const accessories = [
      "کلاه ایمنی",
      "دستکش",
      "کلاه ایمنی",
      "دستکش",
      "کلاه ایمنی",
      "دستکش",
      "کلاه ایمنی",
      "دستکش",
      "کلاه ایمنی",
      "دستکش",
    ];
    const accMotor = ["کلاه ایمنی", "دستکش", "کلاه ایمنی"];

    const { dropdownRef, handleClickOutside } = useDropdown(() => {
      emit('toggle');
    });

    const handleToggle = () => {
      emit('toggle');
    };

    return {
      accessories,
      accMotor,
      dropdownRef,
      handleToggle,
      handleClickOutside
    };
  },
  mounted() {
    document.addEventListener('click', this.handleClickOutside);
  },
  beforeUnmount() {
    document.removeEventListener('click', this.handleClickOutside);
  }
};
</script>

4. 修复后的 composable:dropDownMenu.js

去掉this依赖,通过闭包和ref实现点击外部关闭逻辑:

import { ref } from "vue";

export default function useDropdown(closeCallback) {
  // 存储下拉组件的根元素引用
  const dropdownRef = ref(null);

  // 点击外部关闭下拉
  function handleClickOutside(e) {
    if (dropdownRef.value && !dropdownRef.value.contains(e.target)) {
      closeCallback();
    }
  }

  return {
    dropdownRef,
    handleClickOutside
  };
}

内容的提问来源于stack exchange,提问作者Fariba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 03:32:03