NavBar 顶部菜单按钮点击显示自定义的弹窗,【点击页面空白区域关闭弹窗】,类似 el-Popover 弹出框的效果。点击区域外自动关闭并且联动其他弹框

template

<template>
  <div class="dropdown-with-icons dpitem" @click="openDropDialog">
    <div class="selected-item dpitem">
      <img :src="selectedItem.icon" alt="" class="icon dpitem">
      {{ selectedItem.label }}
    </div>
    <transition name="fade">
      <ul v-if="isOpen" class="dropdown-menu">
        <li v-for="item in items" :key="item.value" @click.stop="selectItem(item)">
          <img :src="item.icon" alt="" class="icon">
          {{ item.label }}
        </li>
      </ul>
    </transition>
  </div>
</template>

vue3 script

<script>
import { ref } from 'vue';
export default {
  name: 'DropdownWithIcons',
  props: {
    items: {
      type: Array,
      required: true,
    },
    initialValue: {
      type: [String, Number],
      required: false,
      default: null,
    },
  },
  setup(props,{ emit }) {

    const isOpen = ref(false);
    const selectedItem = ref(props.items.find(item => item.value === props.initialValue) || props.items[0]);

    const openDropDialog = () => {
      isOpen.value = true
      show()
    };
    const selectItem = (item) => {
      selectedItem.value = item;
      isOpen.value = false;
      emit('change', item); // 在选项式 API 中使用
    };

    const show = ()=> {
        document.addEventListener('click', hidePanel, false)
    };
    const close = ()=> {
        document.removeEventListener('click', hidePanel, false)
        isOpen.value = false
    };
    // 点击空白区域隐藏面板事件
    const hidePanel = (e)=> {
        const isOtherArea = !e.target.parentElement.className.includes('dpitem')
        if (isOtherArea){
            close()
        }
    };

    // 使用组合式 API 时,需要在 setup 函数返回需要暴露给模板的响应式数据和方法
    return {
      isOpen,
      selectedItem,
      openDropDialog,
      selectItem,
      show,
      close,
      hidePanel
    };
  },
};

多个,示例:

<template>
  <div :class="classObj" class="app-wrapper">
    <div
      v-if="device === 'mobile' && sidebar.opened"
      class="drawer-bg"
      @click="handleClickOutside"
    />
    <div class="main-container">
      <navbar
        @handleAvatarClick="handleAvatarClick"
        @handleCompanySeclect="handleCompanySeclect"
        @handleSearchInputChange="handleSearchInputChange"
        @handleMessageClick="handleMessageClick"
        @handleShareClick="handleShareClick"
      />
      <app-main />
    </div>
    <!--弹窗部分 -->
    <AvatarModal
      class="avtar-modal modal-zIndex"
      ref="Avatar"
      v-if="showModalType === 'Avatar'"
      @closeModal="handleCloseModal"
      @removeEventPageClick="removeEventPageClick"
      @addEventPageClick="addEventPageClick"
    />
    <ShareModal
      class="share-modal modal-zIndex"
      ref="Share"
      v-if="showModalType === 'Share'"
      @closeModal="handleCloseModal"
      @removeEventPageClick="removeEventPageClick"
      @addEventPageClick="addEventPageClick"
    />
    <CompanySelectModal
      class="company-select-modal"
      ref="Company"
      v-if="showModalType === 'Company'"
    />
    <SearchModal
      class="search-modal modal-zIndex"
      ref="Search"
      v-if="showModalType === 'Search'"
    />
    <MessageModal
      class="message-modal modal-zIndex"
      ref="Message"
      v-if="showModalType === 'Message'"
      @closeModal="handleCloseModal"
    />
  </div>
</template>

<script>
import { Navbar, AppMain, TagsView, HeadNavbar } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import AvatarModal from './components/ModalComponents/AvatarModal.vue'
import CompanySelectModal from './components/ModalComponents/CompanySelectModal.vue'
import SearchModal from './components/ModalComponents/SearchModal.vue'
import MessageModal from './components/ModalComponents/MessageModal.vue'
import ShareModal from './components/ModalComponents/ShareModal.vue'

export default {
  name: 'Layout',
  components: {
    Navbar,
    AppMain,
    TagsView,
    HeadNavbar,
    AvatarModal,
    CompanySelectModal,
    SearchModal,
    MessageModal,
    ShareModal,
  },
  mixins: [ResizeMixin],
  data() {
    return {
      showModalType: '', // 控制弹框的显示类型
    }
  },
  computed: {
    sidebar() {
      return this.$store.state.app.sidebar
    },
    device() {
      return this.$store.state.app.device
    },
    classObj() {
      return {
        hideSidebar: !this.sidebar.opened,
        openSidebar: this.sidebar.opened,
        withoutAnimation: this.sidebar.withoutAnimation,
        mobile: this.device === 'mobile',
      }
    },
  },
  beforeDestroy() {
    document.removeEventListener('click', this.bodyCloseModal)
  },
  methods: {
    handleClickOutside() {
      this.$store.dispatch('closeSideBar', {
        withoutAnimation: false,
      })
    },
    handleAvatarClick() {
      this.isShowModal('Avatar')
    },
    handleCompanySeclect() {
      this.isShowModal('Company')
    },
    handleSearchInputChange() {
      this.isShowModal('Search')
    },
    handleMessageClick() {
      this.isShowModal('Message')
    },
    handleShareClick() {
      this.isShowModal('Share')
    },
    // 控制弹窗显隐
    isShowModal(type) {
      // 移除页面监听事件(防止用户通过点击 btn 产生 bug)
      this.removeEventPageClick()
      if (this.showModalType === type) {
        this.showModalType = ''
      } else {
        this.showModalType = type
      }
      // 触发监听
      this.addEventPageClick()
    },
    // 触发页面监听
    addEventPageClick() {
      setTimeout(() => {
        document.addEventListener('click', this.bodyCloseModal)
      }, 100)
    },
    // 移除页面监听
    removeEventPageClick() {
      document.removeEventListener('click', this.bodyCloseModal)
    },
    // 点击外部区域关闭
    bodyCloseModal(e) {
      let self = this
      if (
        this.showModalType &&
        this.$refs[this.showModalType] &&
        !this.$refs[this.showModalType].$el.contains(e.target)
      ) {
        if (self.showModalType) {
          self.showModalType = ''
          self.removeEventPageClick()
        }
      }
    },
    // 主动关闭
    handleCloseModal() {
      this.showModalType = ''
    },
  },
}
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
  @import '~@/styles/mixin.scss';

  .app-wrapper {
    @include clearfix;
    position: relative;
    height: 100%;
    width: 100%;

    &.mobile.openSidebar {
      position: fixed;
      top: 0;
    }
  }

  .drawer-bg {
    background: #000;
    opacity: 0.3;
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
  }

  .modal-zIndex {
    z-index: 2000;
  }

  .avtar-modal {
    position: fixed;
    right: 5PX;
    top: 60PX;
  }

  .company-select-modal {
    position: fixed;
    right: 196PX;
    top: 60PX;
  }

  .search-modal {
    position: fixed;
    right: 406PX;
    top: 60PX;
  }

  .message-modal {
    position: fixed;
    right: 75PX;
    top: 60PX;
  }

  .share-modal {
    position: fixed;
    right: 126PX;
    top: 60PX;
  }
</style>