Files
schisandra-cloud-album-front/src/components/MyUI/Utils/index.ts
landaiqing 33d76461f1 refine MyUI
2024-11-07 21:39:16 +08:00

426 lines
16 KiB
TypeScript
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.

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<any>({});
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 || '');
}