2025-06-02 11:31:50 +08:00

206 lines
4.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { arrow, autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue';
import { onClickOutside } from '@vueuse/core';
export type PopoverPlacement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
| 'right'
| 'right-start'
| 'right-end';
export type Offset = [number, number];
export interface PopoverProps {
placement?: PopoverPlacement;
offset?: Offset;
popoverStyle?: CSSProperties;
popoverClass?: string;
trigger?: 'hover' | 'click' | 'clickTarget';
triggerStyle?: CSSProperties;
triggerClass?: string;
hoverDelay?: number; // 悬停延迟关闭时间ms
}
const props = withDefaults(defineProps<PopoverProps>(), {
placement: 'bottom',
offset: () => [0, 0],
trigger: 'hover',
hoverDelay: 0, // 默认300ms延迟关闭
});
const emits = defineEmits<{
(e: 'show'): void;
(e: 'hide'): void;
}>();
const triggerRef = ref<HTMLElement | null>(null);
const popoverRef = ref<HTMLElement | null>(null);
const floatingArrow = ref<HTMLElement | null>(null);
const showPopover = ref(false);
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
// 新增:记录鼠标是否在触发元素或内容区域内
const isHovering = ref(false);
const { floatingStyles } = useFloating(triggerRef, popoverRef, {
placement: props.placement,
transform: false,
whileElementsMounted: autoUpdate,
middleware: [
shift(),
flip(),
arrow({ element: floatingArrow }),
offset({
mainAxis: props.offset[0],
crossAxis: props.offset[1],
}),
],
});
function show() {
if (!showPopover.value) {
showPopover.value = true;
emits('show');
}
// 显示时强制清除定时器(无论是否在悬停)
if (hideTimeout)
clearTimeout(hideTimeout);
hideTimeout = null;
}
function hide() {
if (showPopover.value) {
showPopover.value = false;
emits('hide');
}
hideTimeout = null;
}
defineExpose({ show, hide });
watch(showPopover, (newValue) => {
if (newValue && props.trigger !== 'hover') {
onClickOutside(popoverRef, () => hide(), {
ignore: [triggerRef] as any[],
});
}
});
// 触发元素鼠标事件调整同步isHovering状态
function handleTriggerMouseEnter() {
if (props.trigger === 'hover') {
isHovering.value = true; // 进入触发元素
show();
}
}
function handleTriggerMouseLeave() {
if (props.trigger === 'hover') {
isHovering.value = false; // 离开触发元素
// 仅当鼠标不在内容区域时,才设置延迟关闭
scheduleHideIfNeeded();
}
}
// 内容区域鼠标事件调整同步isHovering状态
function handlePopoverMouseEnter() {
if (props.trigger === 'hover') {
isHovering.value = true; // 进入内容区域
if (hideTimeout)
clearTimeout(hideTimeout); // 取消关闭
}
}
function handlePopoverMouseLeave() {
if (props.trigger === 'hover') {
isHovering.value = false; // 离开内容区域
// 仅当鼠标不在触发元素时,才设置延迟关闭
scheduleHideIfNeeded();
}
}
// 新增:统一延迟关闭逻辑(仅当完全离开两个区域时触发)
function scheduleHideIfNeeded() {
// 如果鼠标仍在任一区域isHovering为true不关闭
if (isHovering.value)
return;
// 否则设置延迟关闭
hideTimeout = setTimeout(() => {
if (!isHovering.value) {
// 再次确认是否仍离开
hide();
}
}, props.hoverDelay);
}
function handleClick() {
if (props.trigger === 'click') {
showPopover.value ? hide() : show();
}
else if (props.trigger === 'clickTarget' && !showPopover.value) {
show();
}
}
</script>
<template>
<div
ref="triggerRef"
class="popover-trigger"
:class="[props.triggerClass]"
:style="[props.triggerStyle]"
@mouseenter="handleTriggerMouseEnter"
@mouseleave="handleTriggerMouseLeave"
@click="handleClick"
>
<slot name="trigger" />
</div>
<Teleport to="body">
<Transition name="popover-fade">
<div
v-if="showPopover"
ref="popoverRef"
:style="[floatingStyles, props.popoverStyle]"
class="popover-content-box"
:class="[props.popoverClass]"
@mouseenter="handlePopoverMouseEnter"
@mouseleave="handlePopoverMouseLeave"
>
<slot />
</div>
</Transition>
</Teleport>
</template>
<style scoped lang="scss">
.popover-fade-enter-active,
.popover-fade-leave-active {
transition:
opacity 0.2s ease,
transform 0.2s ease;
will-change: transform, opacity;
}
.popover-fade-enter-from,
.popover-fade-leave-to {
opacity: 0;
transform: scale(0.95);
}
.popover-fade-enter-to,
.popover-fade-leave-from {
opacity: 1;
transform: scale(1);
}
</style>