Vue3互斥下拉菜单实现:打开一个关闭另一个(无插件)
Vue3 实现下拉菜单互斥关闭并修复点击外部关闭功能
我在Vue3项目的NavShop组件中嵌入了两个下拉菜单组件(DropDownBrand和DropDownAccessory),需要实现两个需求:
- 打开其中一个下拉菜单时,自动关闭另一个
- 点击下拉菜单外部区域时,关闭当前打开的下拉
目前遇到两个问题:
- 两个下拉菜单状态独立,无法实现互斥关闭
- 自定义composable中的
closed函数无法正常工作,点击外部无法关闭下拉
现有代码的核心问题
- 状态分散,无互斥逻辑:每个下拉组件自己维护
close状态,父组件没有统一管理,所以打开一个时无法通知另一个关闭。 - composable中的
this绑定错误:Vue3的setup函数中不存在this,原代码里closed、mounted函数中使用this.$el和this.close会导致undefined,事件监听也绑错了目标函数。 - 事件监听逻辑错误:
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
相关产品推荐
相关产品推荐

