feat: 前端搭建

This commit is contained in:
Gsh
2025-06-17 22:37:37 +08:00
parent 4830be6388
commit 0cd795f57a
1228 changed files with 23627 additions and 1 deletions

View 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>