2026-04-07 20:01:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 工具类集合
|
|
|
|
|
|
* 包含防抖等通用工具函数(无显式any类型)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const Utils = {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 防抖函数
|
|
|
|
|
|
* @param func - 需要防抖的函数
|
|
|
|
|
|
* @param delay - 延迟时间(毫秒),默认500ms
|
|
|
|
|
|
* @param immediate - 是否立即执行,默认false
|
|
|
|
|
|
* @returns 防抖处理后的函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
debounce: function <This, T extends unknown[], R = void>(
|
|
|
|
|
|
func: (this: This, ...args: T) => R,
|
|
|
|
|
|
delay: number = 500,
|
2026-04-13 10:30:03 +08:00
|
|
|
|
immediate: boolean = false
|
2026-04-07 20:01:06 +08:00
|
|
|
|
) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
let timer: number | null = null;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 用泛型This指定this类型
|
|
|
|
|
|
return function (this: This, ...args: T): void {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
if (timer) clearTimeout(timer);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 立即执行逻辑
|
|
|
|
|
|
if (immediate && !timer) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
func.apply(this, args);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重新设置定时器
|
|
|
|
|
|
timer = window.setTimeout(() => {
|
|
|
|
|
|
if (!immediate) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
func.apply(this, args);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
2026-04-13 10:30:03 +08:00
|
|
|
|
timer = null;
|
|
|
|
|
|
}, delay);
|
|
|
|
|
|
};
|
2026-04-07 20:01:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-13 22:28:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 日期格式化函数
|
|
|
|
|
|
* @param format - 格式化字符串(支持YYYY、MM、DD、HH、mm、ss等)
|
|
|
|
|
|
* @param date - 日期对象,默认为当前时间
|
|
|
|
|
|
* @returns 格式化后的日期字符串
|
|
|
|
|
|
*/
|
2026-04-07 20:01:06 +08:00
|
|
|
|
formatDate: (format: string, date: Date = new Date()): string => {
|
|
|
|
|
|
// 基础时间数据
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = date.getMonth() + 1; // 月份0-11,需+1
|
|
|
|
|
|
const day = date.getDate();
|
|
|
|
|
|
const hours24 = date.getHours();
|
|
|
|
|
|
const hours12 = hours24 % 12 || 12; // 12小时制处理(0→12)
|
|
|
|
|
|
const minutes = date.getMinutes();
|
|
|
|
|
|
const seconds = date.getSeconds();
|
|
|
|
|
|
const weekNum = date.getDay(); // 星期0-6(0=周日)
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 星期映射配置
|
|
|
|
|
|
const weekMaps = {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
ddd: ['日', '一', '二', '三', '四', '五', '六'].map(
|
|
|
|
|
|
(day) => `星期${day}`
|
|
|
|
|
|
),
|
2026-04-07 20:01:06 +08:00
|
|
|
|
dd: ['日', '一', '二', '三', '四', '五', '六'].map((day) => `周${day}`),
|
|
|
|
|
|
d: [0, 1, 2, 3, 4, 5, 6],
|
2026-04-13 10:30:03 +08:00
|
|
|
|
};
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 占位符替换规则(顺序:长占位符优先,避免冲突)
|
|
|
|
|
|
const replaceRules = [
|
|
|
|
|
|
{ regex: /YYYY/g, value: year.toString() },
|
|
|
|
|
|
{ regex: /YY/g, value: year.toString().slice(-2) },
|
|
|
|
|
|
{ regex: /MM/g, value: month.toString().padStart(2, '0') },
|
|
|
|
|
|
{ regex: /M/g, value: month.toString() },
|
|
|
|
|
|
{ regex: /DD/g, value: day.toString().padStart(2, '0') },
|
|
|
|
|
|
{ regex: /D/g, value: day.toString() },
|
|
|
|
|
|
{ regex: /HH/g, value: hours24.toString().padStart(2, '0') },
|
|
|
|
|
|
{ regex: /H/g, value: hours24.toString() },
|
|
|
|
|
|
{ regex: /hh/g, value: hours12.toString().padStart(2, '0') },
|
|
|
|
|
|
{ regex: /h/g, value: hours12.toString() },
|
|
|
|
|
|
{ regex: /mm/g, value: minutes.toString().padStart(2, '0') },
|
|
|
|
|
|
{ regex: /m/g, value: minutes.toString() },
|
|
|
|
|
|
{ regex: /ss/g, value: seconds.toString().padStart(2, '0') },
|
|
|
|
|
|
{ regex: /s/g, value: seconds.toString() },
|
|
|
|
|
|
{ regex: /ddd/g, value: weekMaps.ddd[weekNum] },
|
|
|
|
|
|
{ regex: /dd/g, value: weekMaps.dd[weekNum] },
|
|
|
|
|
|
{ regex: /d/g, value: weekMaps.d[weekNum] },
|
2026-04-13 10:30:03 +08:00
|
|
|
|
];
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 执行替换
|
|
|
|
|
|
return replaceRules.reduce((result, { regex, value }) => {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return result.replace(regex, String(value ?? ''));
|
|
|
|
|
|
}, format);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
},
|
2026-04-11 10:09:40 +08:00
|
|
|
|
|
2026-04-07 20:01:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 深拷贝函数
|
|
|
|
|
|
* 支持类型:原始类型、数组、对象、Date、RegExp、Map、Set、ArrayBuffer等
|
|
|
|
|
|
* @param source 要拷贝的数据源
|
|
|
|
|
|
* @param hash 用于解决循环引用的哈希表,内部使用
|
|
|
|
|
|
* @returns 深拷贝后的新数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
deepClone: <T>(source: T, hash = new WeakMap<object, unknown>()): T => {
|
|
|
|
|
|
// 处理 null 或 undefined
|
|
|
|
|
|
if (source === null || source === undefined) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return source;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理原始类型(string, number, boolean, symbol, bigint, function)
|
|
|
|
|
|
if (typeof source !== 'object' && typeof source !== 'function') {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return source;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理函数 - 直接返回原函数引用(通常不需要克隆函数)
|
|
|
|
|
|
if (typeof source === 'function') {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return source;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解决循环引用
|
|
|
|
|
|
if (hash.has(source)) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return hash.get(source) as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 Date 对象
|
|
|
|
|
|
if (source instanceof Date) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned = new Date(source.getTime()) as T;
|
|
|
|
|
|
hash.set(source, cloned);
|
|
|
|
|
|
return cloned;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 RegExp 对象
|
|
|
|
|
|
if (source instanceof RegExp) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned = new RegExp(source.source, source.flags);
|
|
|
|
|
|
cloned.lastIndex = source.lastIndex; // 安全访问
|
|
|
|
|
|
return cloned as unknown as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 处理 Map 对象
|
|
|
|
|
|
if (source instanceof Map) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned = new Map();
|
|
|
|
|
|
hash.set(source, cloned);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
source.forEach((value, key) => {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
cloned.set(Utils.deepClone(key, hash), Utils.deepClone(value, hash));
|
|
|
|
|
|
});
|
|
|
|
|
|
return cloned as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 Set 对象
|
|
|
|
|
|
if (source instanceof Set) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned = new Set<unknown>();
|
|
|
|
|
|
hash.set(source, cloned);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
for (const value of source.values()) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
cloned.add(Utils.deepClone(value, hash));
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return cloned as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 ArrayBuffer
|
|
|
|
|
|
if (source instanceof ArrayBuffer) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned = source.slice(0) as T;
|
|
|
|
|
|
hash.set(source, cloned);
|
|
|
|
|
|
return cloned;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理数组
|
|
|
|
|
|
if (Array.isArray(source)) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned: T[] = [];
|
|
|
|
|
|
hash.set(source, cloned);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
for (let i = 0; i < source.length; i++) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
cloned[i] = Utils.deepClone(source[i], hash);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return cloned as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理普通对象
|
|
|
|
|
|
if (typeof source === 'object') {
|
|
|
|
|
|
// 处理 Error 对象
|
|
|
|
|
|
if (source instanceof Error) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned = new Error(source.message);
|
|
|
|
|
|
cloned.stack = source.stack;
|
|
|
|
|
|
cloned.name = source.name;
|
|
|
|
|
|
hash.set(source, cloned);
|
|
|
|
|
|
return cloned as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理其他对象
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const cloned: { [key: string | symbol]: unknown } = {};
|
|
|
|
|
|
hash.set(source, cloned);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取对象的所有属性(包括不可枚举的属性和 Symbol)
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const keys = [
|
|
|
|
|
|
...Object.getOwnPropertyNames(source),
|
|
|
|
|
|
...Object.getOwnPropertySymbols(source),
|
|
|
|
|
|
];
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
for (const key of keys) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果是访问器属性
|
|
|
|
|
|
if (descriptor && descriptor.get) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
Object.defineProperty(cloned, key, descriptor);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果是数据属性
|
2026-04-13 10:30:03 +08:00
|
|
|
|
cloned[key] = Utils.deepClone(
|
|
|
|
|
|
(source as { [key: string | symbol]: unknown })[key],
|
|
|
|
|
|
hash
|
|
|
|
|
|
);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理原型链
|
2026-04-13 10:30:03 +08:00
|
|
|
|
const proto = Object.getPrototypeOf(source);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
if (proto && proto !== Object.prototype) {
|
2026-04-13 10:30:03 +08:00
|
|
|
|
Object.setPrototypeOf(cloned, proto);
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return cloned as T;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对于其他无法处理的情况,返回原值
|
2026-04-13 10:30:03 +08:00
|
|
|
|
return source;
|
2026-04-07 20:01:06 +08:00
|
|
|
|
},
|
2026-04-11 10:09:40 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 调整元素位置,确保其不超出屏幕可视区域边界
|
|
|
|
|
|
* @param {number} offsetX - 元素左上角原 X 坐标(相对于视口)
|
|
|
|
|
|
* @param {number} offsetY - 元素左上角原 Y 坐标(相对于视口)
|
|
|
|
|
|
* @param {number} width - 元素的宽度
|
|
|
|
|
|
* @param {number} height - 元素的高度
|
|
|
|
|
|
* @returns {[number, number]} 调整后的 [newX, newY]
|
2026-04-13 10:30:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
keepWithinScreen: (
|
|
|
|
|
|
offsetX: number,
|
|
|
|
|
|
offsetY: number,
|
|
|
|
|
|
width: number,
|
|
|
|
|
|
height: number
|
|
|
|
|
|
) => {
|
2026-04-11 10:09:40 +08:00
|
|
|
|
const viewportW = window.innerWidth;
|
|
|
|
|
|
const viewportH = window.innerHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算允许的 X 范围:最小 0,最大 (视口宽度 - 元素宽度)
|
|
|
|
|
|
// 如果元素宽度大于视口宽度,允许范围为 [0, 0](即只能左对齐)
|
|
|
|
|
|
const maxX = Math.max(0, viewportW - width);
|
|
|
|
|
|
const newX = Math.min(Math.max(offsetX, 0), maxX);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算允许的 Y 范围
|
|
|
|
|
|
const maxY = Math.max(0, viewportH - height);
|
|
|
|
|
|
const newY = Math.min(Math.max(offsetY, 0), maxY);
|
|
|
|
|
|
|
|
|
|
|
|
return [newX, newY];
|
2026-04-13 10:30:03 +08:00
|
|
|
|
},
|
2026-04-13 20:55:32 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将数组拆分为指定大小的子数组
|
|
|
|
|
|
* @param {T[]} arr - 要拆分的数组
|
|
|
|
|
|
* @param {number} [size=2] - 子数组的大小
|
|
|
|
|
|
* @returns {T[][]} 拆分后的子数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
chunkArray: <T>(arr: T[], size: number = 2): T[][] => {
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
for (let i = 0; i < arr.length; i += size) {
|
|
|
|
|
|
result.push(arr.slice(i, i + size));
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
},
|
2026-04-13 10:30:03 +08:00
|
|
|
|
};
|