import type {Ref} from 'vue'; import { computed, getCurrentInstance, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, toValue, useSlots, watch } from 'vue'; /** * 组合式函数 * 监听给定名称或名称数组的插槽是否存在,支持监听单个插槽或一组插槽的存在 * * @param slotsName - 插槽的名称或名称数组,默认为 'default' * @returns 如果是单个插槽名称,则返回一个计算属性,表示该插槽是否存在 * 如果是插槽名称数组,则返回一个 reactive 对象,其中的每个属性对应该插槽是否存在 */ export function useSlotsExist(slotsName: string | string[] = 'default') { const slots = useSlots(); // 获取当前组件的所有插槽 // 检查特定名称的插槽是否存在且不为空 const checkSlotsExist = (slotsName: string): boolean => { const slotsContent = slots[slotsName]?.(); const checkExist = (slotContent: any) => { if (typeof slotContent.children === 'string') { // 排除 v-if="false" 的插槽内容 if (slotContent.children === 'v-if') { return false; } return slotContent.children.trim() !== ''; } else { if (slotContent.children === null) { if (slotContent.type === 'img' || typeof slotContent.type !== 'string') { return true; } } else { return Boolean(slotContent.children); } } }; if (slotsContent && slotsContent?.length) { const result = slotsContent.some((slotContent) => { return checkExist(slotContent); }); return result; } return false; }; if (Array.isArray(slotsName)) { const slotsExist = reactive({}); slotsName.forEach((item) => { const exist = computed(() => checkSlotsExist(item)); slotsExist[item] = exist; // 将一个 ref 赋值给一个 reactive 属性时,该 ref 会自动解包 }); return slotsExist; } else { return computed(() => checkSlotsExist(slotsName)); } } /** * 组合式函数 * 使用 Vue 的生命周期钩子添加和移除事件监听器 * * 该函数旨在提供一种优雅的方式来管理事件监听器,避免在组件卸载后仍保留事件监听器, * 从而可能导致内存泄漏的问题;通过结合 Vue 的 onMounted 和 onUnmounted 钩子, * 在组件挂载时添加事件监听器,并在组件卸载时移除它 * * @param target 目标元素或对象;可以是 DOM 元素或其他支持 addEventListener 的对象 * @param event 要监听的事件名称 * @param callback 事件被触发时执行的回调函数 */ export function useEventListener(target: HTMLElement | Window | Document, event: string, callback: EventListenerOrEventListenerObject): void { // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素 onMounted(() => target.addEventListener(event, callback as EventListenerOrEventListenerObject)); onUnmounted(() => target.removeEventListener(event, callback as EventListenerOrEventListenerObject)); } /** * 使用 requestAnimationFrame 实现的延迟 setTimeout 或间隔 setInterval 调用函数 * * @param fn 要执行的函数 * @param delay 延迟的时间,单位为 ms,默认为 0,表示不延迟立即执行 * @param interval 是否间隔执行,如果为 true,则在首次执行后,以 delay 为间隔持续执行 * @returns 返回一个对象,包含一个 id 属性,该 id 为 requestAnimationFrame 的调用 ID,可用于取消动画帧 */ export function rafTimeout(fn: () => void, delay: number = 0, interval: boolean = false): object { let start: number | null = null; // 记录动画开始的时间戳 function timeElapse(timestamp: number) { // 定义动画帧回调函数 /* timestamp参数:与 performance.now() 的返回值相同,它表示 requestAnimationFrame() 开始去执行回调函数的时刻 */ if (!start) { // 如果还没有开始时间,则以当前时间为开始时间 start = timestamp; } const elapsed = timestamp - start; if (elapsed >= delay) { try { fn(); // 执行目标函数 } catch (error) { console.error('Error executing rafTimeout function:', error); } if (interval) { // 如果需要间隔执行,则重置开始时间并继续安排下一次动画帧 start = timestamp; raf.id = requestAnimationFrame(timeElapse); } } else { raf.id = requestAnimationFrame(timeElapse); } } interface AnimationFrameID { id: number } // 创建一个对象用于存储动画帧的 ID,并初始化动画帧 const raf: AnimationFrameID = { id: requestAnimationFrame(timeElapse) }; return raf; } /** * 用于取消 rafTimeout 函数 * * @param raf - 包含请求动画帧 ID 的对象;该 ID 是由 requestAnimationFrame 返回的 * 该函数旨在取消之前通过 requestAnimationFrame 请求的动画帧 * 如果传入的 raf 对象或其 id 无效,则会打印警告 */ export function cancelRaf(raf: { id: number }): void { if (raf && raf.id && typeof raf.id === 'number') { cancelAnimationFrame(raf.id); } else { console.warn('cancelRaf received an invalid id:', raf); } } /** * 组合式函数 * 使用 ResizeObserver 观察 DOM 元素尺寸变化 * * 该函数提供了一种方便的方式来观察一个或多个元素的尺寸变化,并在变化时执行指定的回调函数 * * @param target 要观察的目标,可以是 Ref 对象、Ref 数组、HTMLElement 或 HTMLElement 数组 * @param callback 当元素尺寸变化时调用的回调函数 * @param options ResizeObserver 选项,用于定制观察行为 * @returns 返回一个对象,包含停止和开始观察的方法,使用者可以调用 start 方法开始观察,调用 stop 方法停止观察 */ export function useResizeObserver( target: Ref | Ref[] | HTMLElement | HTMLElement[], callback: ResizeObserverCallback, options: object = {} ) { const isSupported = useSupported(() => window && 'ResizeObserver' in window); let observer: ResizeObserver | undefined; const stopObservation = ref(false); const targets = computed(() => { const targetsValue = toValue(target); if (targetsValue) { if (Array.isArray(targetsValue)) { return targetsValue.map((el: any) => toValue(el)).filter((el: any) => el); } else { return [targetsValue]; } } return []; }); // 定义清理函数,用于断开 ResizeObserver 的连接 const cleanup = () => { if (observer) { observer.disconnect(); observer = undefined; } }; // 初始化 ResizeObserver,开始观察目标元素 const observeElements = () => { if (isSupported.value && targets.value.length && !stopObservation.value) { observer = new ResizeObserver(callback); targets.value.forEach((element: HTMLElement) => observer!.observe(element, options)); } }; // 监听 targets 的变化,当 targets 变化时,重新建立 ResizeObserver 观察 watch( () => targets.value, () => { cleanup(); observeElements(); }, { immediate: true, // 立即触发回调,以便初始状态也被观察 flush: 'post' } ); const stop = () => { stopObservation.value = true; cleanup(); }; const start = () => { stopObservation.value = false; observeElements(); }; // 在组件卸载前清理 ResizeObserver onBeforeUnmount(() => cleanup()); return { stop, start }; } // 辅助函数 export function useSupported(callback: () => unknown) { const isMounted = useMounted(); return computed(() => { // to trigger the ref if (isMounted.value) { // no-op } ; return Boolean(callback()); }); } export function useMounted() { const isMounted = ref(false); // 获取当前组件的实例 const instance = getCurrentInstance(); if (instance) { onMounted(() => { isMounted.value = true; }, instance); } return isMounted; } /** * 防抖函数 debounce * * 主要用于限制函数调用的频率,当频繁触发某个函数时,实际上只需要在最后一次触发后的一段时间内执行一次即可 * 这对于诸如输入事件处理函数、窗口大小调整事件处理函数等可能会频繁触发的函数非常有用 * * @param fn 要执行的函数 * @param delay 防抖的时间期限,单位 ms,默认为 300ms * @returns 返回一个新的防抖的函数 */ export function debounce(fn: (...args: any[]) => any, delay: number = 300): any { let timer: any = null; // 使用闭包保存定时器的引用 return function (...args: any[]) { // 返回一个包装函数 if (timer) { // 如果定时器存在,则清除之前的定时器 clearTimeout(timer); } // 设置新的定时器,延迟执行原函数 timer = setTimeout(() => { fn(...args); }, delay); }; } /** * 组合式函数 * 使用 MutationObserver 观察 DOM 元素的变化 * * 该函数提供了一个便捷的方式来订阅 DOM 元素的变动,当元素发生指定的变化时,调用提供的回调函数 * 使用者可以指定要观察的一个或多个 DOM 元素,以及观察的选项和回调函数 * * @param target 要观察的目标,可以是 Ref 对象、Ref 数组、HTMLElement 或 HTMLElement 数组 * @param callback 当观察到变化时调用的回调函数 * @param options MutationObserver 的观察选项,默认为空对象;例如: * subtree: 是否监听以 target 为根节点的整个子树,包括子树中所有节点的属性 * childList: 是否监听 target 节点中发生的节点的新增与删除 * attributes: 是否观察所有监听的节点属性值的变化 * attributeFilter: 声明哪些属性名会被监听的数组;如果不声明该属性,所有属性的变化都将触发通知 * @returns 返回一个对象,包含停止和开始观察的方法,使用者可以调用 start 方法开始观察,调用 stop 方法停止观察 */ export function useMutationObserver( target: Ref | Ref[] | HTMLElement | HTMLElement[], callback: MutationCallback, options = {} ) { const isSupported = useSupported(() => window && 'MutationObserver' in window); const stopObservation = ref(false); let observer: MutationObserver | undefined; const targets = computed(() => { const targetsValue = toValue(target); if (targetsValue) { if (Array.isArray(targetsValue)) { return targetsValue.map((el: any) => toValue(el)).filter((el: any) => el); } else { return [targetsValue]; } } return []; }); // 定义清理函数,用于断开 MutationObserver 的连接 const cleanup = () => { if (observer) { observer.disconnect(); observer = undefined; } }; // 初始化 MutationObserver,开始观察目标元素 const observeElements = () => { if (isSupported.value && targets.value.length && !stopObservation.value) { observer = new MutationObserver(callback); targets.value.forEach((element: HTMLElement) => observer!.observe(element, options)); } }; // 监听 targets 的变化,当 targets 变化时,重新建立 MutationObserver 观察 watch( () => targets.value, () => { cleanup(); observeElements(); }, { immediate: true, // 立即触发回调,以便初始状态也被观察 flush: 'post' } ); const stop = () => { stopObservation.value = true; cleanup(); }; const start = () => { stopObservation.value = false; observeElements(); }; // 在组件卸载前清理 MutationObserver onBeforeUnmount(() => cleanup()); return { stop, start }; } /** * 消除 js 加减精度问题的加法函数 * * 该函数旨在添加两个数字,考虑到它们可能是整数或小数;对于整数,直接返回它们的和 * 对于小数,为了确保精确计算,将小数转换为相同长度的字符串进行处理,然后将结果转换回小数 * * @param num1 第一个数字 * @param num2 第二个数字 * @returns 返回两个数字的和 */ export function add(num1: number, num2: number): number { // 验证输入是否为有效的数字 // Number.isNaN() 不会尝试将参数转换为数字;全局 isNaN() 函数会将参数强制转换为数字 if (Number.isNaN(num1) || Number.isNaN(num2)) { throw new Error('Both num1 and num2 must be valid numbers.'); } // 检查输入是否为小数 const isDecimalNum1 = num1 % 1 !== 0; const isDecimalNum2 = num2 % 1 !== 0; if (!isDecimalNum1 && !isDecimalNum2) { return num1 + num2; // 如果两个数字都是整数,则直接返回它们的和 } const num1DeciStr = String(num1).split('.')[1] ?? ''; const num2DeciStr = String(num2).split('.')[1] ?? ''; const maxLen = Math.max(num1DeciStr.length, num2DeciStr.length); const factor = Math.pow(10, maxLen); const num1Str = num1.toFixed(maxLen); const num2Str = num2.toFixed(maxLen); // 将小数点移除并转换为整数相加 const result = (+num1Str.replace('.', '') + +num2Str.replace('.', '')) / factor; return result; } /** * 数字格式化函数 * * 该函数提供了一种灵活的方式将数字格式化为字符串,包括设置精度、千位分隔符、小数点字符、前缀和后缀 * * @param value 要格式化的数字或数字字符串 * @param precision 小数点后的位数,默认为 2 * @param separator 千分位分隔符,默认为 ',' * @param decimal 小数点字符,默认为 '.' * @param prefix 数字前的字符串,默认为 undefined * @param suffix 数字后的字符串,默认为 undefined * @returns 格式化后的字符串;如果输入值不是数字或字符串,则抛出类型错误 */ export function formatNumber( value: number | string, precision: number = 2, separator: string = ',', decimal: string = '.', prefix?: string, suffix?: string ): string { // 类型检查 if (typeof value !== 'number' && typeof value !== 'string') { console.warn('Expected value to be of type number or string'); } if (typeof precision !== 'number') { console.warn('Expected precision to be of type number'); } // 处理非数值或NaN的情况 const numValue = Number(value); if (isNaN(numValue) || !isFinite(numValue)) { return ''; } if (numValue === 0) { return numValue.toFixed(precision); } let formatValue = numValue.toFixed(precision); // 如果 separator 是数值而非字符串,会导致错误,此处进行检查 if (typeof separator === 'string' && separator !== '') { const [integerPart, decimalPart] = formatValue.split('.'); formatValue = integerPart.replace(/(\d)(?=(\d{3})+$)/g, '$1' + separator) + (decimalPart ? decimal + decimalPart : ''); } return (prefix || '') + formatValue + (suffix || ''); }