import type { Arrayable, MaybeElementRef } from '@vueuse/core'; import type { Ref } from 'vue'; import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue'; import { isFunction } from '@vben/utils'; import { useElementHover } from '@vueuse/core'; interface HoverDelayOptions { /** 鼠标进入延迟时间 */ enterDelay?: (() => number) | number; /** 鼠标离开延迟时间 */ leaveDelay?: (() => number) | number; } const DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间,默认为 500ms const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0(立即响应) /** * 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false * @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true * @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象 * @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用 */ export function useHoverToggle( refElement: Arrayable | Ref, delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY, ) { // 兼容旧版本API const normalizedOptions: HoverDelayOptions = typeof delay === 'number' || isFunction(delay) ? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay } : { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: DEFAULT_LEAVE_DELAY, ...delay, }; const value = ref(false); const enterTimer = ref | undefined>(); const leaveTimer = ref | undefined>(); const hoverScopes = ref[]>([]); // 使用计算属性包装 refElement,使其响应式变化 const refs = computed(() => { const raw = unref(refElement); if (raw === null) return []; return Array.isArray(raw) ? raw : [raw]; }); // 存储所有 hover 状态 const isHovers = ref>>([]); // 更新 hover 监听的函数 function updateHovers() { // 停止并清理之前的作用域 hoverScopes.value.forEach((scope) => scope.stop()); hoverScopes.value = []; isHovers.value = refs.value.map((refEle) => { if (!refEle) { return ref(false); } const eleRef = computed(() => { const ele = unref(refEle); return ele instanceof Element ? ele : (ele?.$el as Element); }); // 为每个元素创建独立的作用域 const scope = effectScope(); const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false); hoverScopes.value.push(scope); return hoverRef; }); } // 监听元素数量变化,避免过度执行 const elementsCount = computed(() => { const raw = unref(refElement); if (raw === null) return 0; return Array.isArray(raw) ? raw.length : 1; }); // 初始设置 updateHovers(); // 只在元素数量变化时重新设置监听器 const stopWatcher = watch(elementsCount, updateHovers, { deep: false }); const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value)); function clearTimers() { if (enterTimer.value) { clearTimeout(enterTimer.value); enterTimer.value = undefined; } if (leaveTimer.value) { clearTimeout(leaveTimer.value); leaveTimer.value = undefined; } } function setValueDelay(val: boolean) { clearTimers(); if (val) { // 鼠标进入 const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY; const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay; if (delayTime <= 0) { value.value = true; } else { enterTimer.value = setTimeout(() => { value.value = true; enterTimer.value = undefined; }, delayTime); } } else { // 鼠标离开 const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY; const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay; if (delayTime <= 0) { value.value = false; } else { leaveTimer.value = setTimeout(() => { value.value = false; leaveTimer.value = undefined; }, delayTime); } } } const hoverWatcher = watch( isOutsideAll, (val) => { setValueDelay(!val); }, { immediate: true }, ); const controller = { enable() { hoverWatcher.resume(); }, disable() { hoverWatcher.pause(); }, }; onUnmounted(() => { clearTimers(); // 停止监听器 stopWatcher(); // 停止所有剩余的作用域 hoverScopes.value.forEach((scope) => scope.stop()); }); return [value, controller] as [typeof value, typeof controller]; }