popover组件(vite5+vue3)

122 阅读1分钟

组件分析

popover组件有两个部分构成,一个是触发弹出层弹出的显示层。一个是弹出层。 一个插槽对应一个插槽。

弹出层绑定鼠标移入和移出事件

代码

<script>
const PROP_TOP_LEFT = 'top-left'
const PROP_TOP_RIGHT = 'top-right'
const PROP_BOTTOM_LEFT = 'bottom-left'
const PROP_BOTTOM_RIGHT = 'bottom-right'

// 定义指定位置的 Enum
const placementEnum = [
  PROP_TOP_LEFT,
  PROP_TOP_RIGHT,
  PROP_BOTTOM_LEFT,
  PROP_BOTTOM_RIGHT
]
</script>

<script setup>
import {
  ref,
  watch,
  nextTick
} from 'vue'

// 控制延迟关闭
let timeout = null

const isVisable = ref(false)
// 鼠标移入移除
const mouseenter = () => {
  isVisable.value = true
  // 再次触发时,清理延时装置
  if (timeout) {
    clearTimeout(timeout)
  }
}
const mouseleave = () => {
  timeout = setTimeout(() => {
    isVisable.value = false
    timeout = null
  }, 300)
}
const props = defineProps({
  placement: {
    type: String,
    default: PROP_TOP_LEFT,
    validator(val) {
      const result =
        placementEnum.includes(val)
      if (!result) {
        throw new Error(
          `placement must be one of ${placementEnum}`
        )
      }
      return result
    }
  }
})
// 展示气泡位置
const contentStyle = ref({
  top: 0,
  left: 0
})

const referenceTarget = ref(null)
const contentTarget = ref(null)
const useElementSize = (target) => {
  if (!target) return {}
  return {
    width: target.offsetWidth,
    height: target.offsetHeight
  }
}
/**
 * 监听展示的变化,在展示时计算气泡位置
 */
watch(isVisable, (val) => {
  if (!val) {
    return
  }
  // 等待渲染成功之后
  nextTick(() => {
    switch (props.placement) {
      // 左上
      case PROP_TOP_LEFT:
        contentStyle.value.top = 0
        contentStyle.value.left =
          -useElementSize(
            contentTarget.value
          ).width + 'px'
        break
      // 右上
      case PROP_TOP_RIGHT:
        contentStyle.value.top = 0
        contentStyle.value.left =
          useElementSize(
            referenceTarget.value
          ).width + 'px'
        break
      // 左下
      case PROP_BOTTOM_LEFT:
        contentStyle.value.top =
          useElementSize(
            referenceTarget.value
          ).height + 'px'
        contentStyle.value.left =
          -useElementSize(
            contentTarget.value
          ).width + 'px'
        break
      // 右下
      case PROP_BOTTOM_RIGHT:
        contentStyle.value.top =
          useElementSize(
            referenceTarget.value
          ).height + 'px'
        contentStyle.value.left =
          useElementSize(
            referenceTarget.value
          ).width + 'px'
        break
    }
  })
})
</script>
<template>
  <div
    class="relative"
    @mouseenter="mouseenter"
    @mouseleave="mouseleave">
    <div ref="referenceTarget">
      <!-- 显示层 -->
      <slot name="reference"></slot>
    </div>

    <transition name="slide">
      <div
        ref="contentTarget"
        v-show="isVisable"
        class="absolute p-1 z-20 bg-white border rounded-md"
        :style="contentStyle">
        <slot></slot>
      </div>
    </transition>
  </div>
</template>

<style scoped>
.slide-enter-active {
  transition: all 0.5s;
}
.slide-leave-active {
  transition: all.5s;
}
.slide-enter-from,
.slide-leave-to {
  opacity: 0;
  transform: translateY(-10px);
}
</style>


后续完善

加入动画、加入控制弹出层显示位置的方法。

备注

鼠标的移入移出需要做延时处理。