feat: 前端搭建
This commit is contained in:
203
Yi.Ai.Vue3/src/components/Popover/index.vue
Normal file
203
Yi.Ai.Vue3/src/components/Popover/index.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user