395 lines
9.6 KiB
TypeScript
395 lines
9.6 KiB
TypeScript
/**
|
|
* @fileoverview 工具函数集合
|
|
* @description 提供项目中常用的工具函数
|
|
*/
|
|
|
|
import { clsx, type ClassValue } from 'clsx';
|
|
|
|
/**
|
|
* 合并 CSS 类名
|
|
* @param inputs - CSS 类名数组
|
|
* @returns 合并后的 CSS 类名字符串
|
|
*/
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return clsx(inputs);
|
|
}
|
|
|
|
/**
|
|
* 格式化日期
|
|
* @param date - 日期对象或字符串
|
|
* @param locale - 语言代码,默认 'zh-CN'
|
|
* @returns 格式化后的日期字符串
|
|
*/
|
|
export function formatDate(date: Date | string, locale: string = 'zh-CN'): string {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return dateObj.toLocaleDateString(locale, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 格式化时间
|
|
* @param date - 日期对象或字符串
|
|
* @param locale - 语言代码,默认 'zh-CN'
|
|
* @returns 格式化后的时间字符串
|
|
*/
|
|
export function formatTime(date: Date | string, locale: string = 'zh-CN'): string {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return dateObj.toLocaleTimeString(locale, {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 格式化日期时间
|
|
* @param date - 日期对象或字符串
|
|
* @param locale - 语言代码,默认 'zh-CN'
|
|
* @returns 格式化后的日期时间字符串
|
|
*/
|
|
export function formatDateTime(
|
|
date: Date | string,
|
|
locale: string = 'zh-CN'
|
|
): string {
|
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
return `${formatDate(dateObj, locale)} ${formatTime(dateObj, locale)}`;
|
|
}
|
|
|
|
/**
|
|
* 生成随机 ID
|
|
* @param length - ID 长度,默认 8
|
|
* @returns 随机 ID 字符串
|
|
*/
|
|
export function generateId(length: number = 8): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 防抖函数
|
|
* @param func - 要防抖的函数
|
|
* @param delay - 延迟时间(毫秒)
|
|
* @returns 防抖后的函数
|
|
*/
|
|
export function debounce<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
delay: number
|
|
): (...args: Parameters<T>) => void {
|
|
let timeoutId: NodeJS.Timeout;
|
|
return (...args: Parameters<T>) => {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(() => func.apply(null, args), delay);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 节流函数
|
|
* @param func - 要节流的函数
|
|
* @param delay - 延迟时间(毫秒)
|
|
* @returns 节流后的函数
|
|
*/
|
|
export function throttle<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
delay: number
|
|
): (...args: Parameters<T>) => void {
|
|
let lastCall = 0;
|
|
return (...args: Parameters<T>) => {
|
|
const now = new Date().getTime();
|
|
if (now - lastCall < delay) {
|
|
return;
|
|
}
|
|
lastCall = now;
|
|
return func.apply(null, args);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 深拷贝对象
|
|
* @param obj - 要拷贝的对象
|
|
* @returns 深拷贝后的对象
|
|
*/
|
|
export function deepClone<T>(obj: T): T {
|
|
if (obj === null || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
|
|
if (obj instanceof Date) {
|
|
return new Date(obj.getTime()) as unknown as T;
|
|
}
|
|
|
|
if (obj instanceof Array) {
|
|
return obj.map(item => deepClone(item)) as unknown as T;
|
|
}
|
|
|
|
if (typeof obj === 'object') {
|
|
const clonedObj = {} as T;
|
|
for (const key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
clonedObj[key] = deepClone(obj[key]);
|
|
}
|
|
}
|
|
return clonedObj;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* 检查是否为移动设备
|
|
* @returns 是否为移动设备
|
|
*/
|
|
export function isMobile(): boolean {
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
navigator.userAgent
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 检查是否为平板设备
|
|
* @returns 是否为平板设备
|
|
*/
|
|
export function isTablet(): boolean {
|
|
return /iPad|Android(?=.*\bTablet\b)|Windows(?=.*\bTouch\b)/i.test(
|
|
navigator.userAgent
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 检查是否支持触摸
|
|
* @returns 是否支持触摸
|
|
*/
|
|
export function isTouchDevice(): boolean {
|
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
}
|
|
|
|
/**
|
|
* 获取文件扩展名
|
|
* @param filename - 文件名
|
|
* @returns 文件扩展名
|
|
*/
|
|
export function getFileExtension(filename: string): string {
|
|
return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2);
|
|
}
|
|
|
|
/**
|
|
* 格式化文件大小
|
|
* @param bytes - 字节数
|
|
* @param decimals - 小数位数,默认 2
|
|
* @returns 格式化后的文件大小字符串
|
|
*/
|
|
export function formatFileSize(bytes: number, decimals: number = 2): string {
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* 复制文本到剪贴板
|
|
* @param text - 要复制的文本
|
|
* @returns Promise<boolean> 是否复制成功
|
|
*/
|
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
try {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} else {
|
|
// 降级方案
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
const result = document.execCommand('copy');
|
|
textArea.remove();
|
|
return result;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to copy text:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 下载文件
|
|
* @param content - 文件内容
|
|
* @param filename - 文件名
|
|
* @param mimeType - MIME 类型
|
|
*/
|
|
export function downloadFile(
|
|
content: string,
|
|
filename: string,
|
|
mimeType: string = 'text/plain'
|
|
): void {
|
|
const blob = new Blob([content], { type: mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* 获取 URL 参数
|
|
* @param key - 参数名
|
|
* @returns 参数值或 null
|
|
*/
|
|
export function getUrlParam(key: string): string | null {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get(key);
|
|
}
|
|
|
|
/**
|
|
* 设置 URL 参数
|
|
* @param key - 参数名
|
|
* @param value - 参数值
|
|
*/
|
|
export function setUrlParam(key: string, value: string): void {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set(key, value);
|
|
window.history.replaceState({}, '', url.toString());
|
|
}
|
|
|
|
/**
|
|
* 移除 URL 参数
|
|
* @param key - 参数名
|
|
*/
|
|
export function removeUrlParam(key: string): void {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete(key);
|
|
window.history.replaceState({}, '', url.toString());
|
|
}
|
|
|
|
/**
|
|
* 检查对象是否为空
|
|
* @param obj - 要检查的对象
|
|
* @returns 是否为空
|
|
*/
|
|
export function isEmpty(obj: any): boolean {
|
|
if (obj == null) return true;
|
|
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
|
|
if (obj instanceof Date) return false;
|
|
return Object.keys(obj).length === 0;
|
|
}
|
|
|
|
/**
|
|
* 唯一数组
|
|
* @param array - 原数组
|
|
* @returns 去重后的数组
|
|
*/
|
|
export function uniqueArray<T>(array: T[]): T[] {
|
|
return [...new Set(array)];
|
|
}
|
|
|
|
/**
|
|
* 数组分组
|
|
* @param array - 原数组
|
|
* @param size - 每组大小
|
|
* @returns 分组后的二维数组
|
|
*/
|
|
export function chunkArray<T>(array: T[], size: number): T[][] {
|
|
const chunks: T[][] = [];
|
|
for (let i = 0; i < array.length; i += size) {
|
|
chunks.push(array.slice(i, i + size));
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* 对象数组根据某个属性分组
|
|
* @param array - 对象数组
|
|
* @param key - 分组键
|
|
* @returns 分组后的对象
|
|
*/
|
|
export function groupBy<T extends Record<string, any>>(
|
|
array: T[],
|
|
key: keyof T
|
|
): Record<string, T[]> {
|
|
return array.reduce((groups, item) => {
|
|
const group = String(item[key]);
|
|
groups[group] = groups[group] || [];
|
|
groups[group].push(item);
|
|
return groups;
|
|
}, {} as Record<string, T[]>);
|
|
}
|
|
|
|
/**
|
|
* 等待指定时间
|
|
* @param ms - 等待时间(毫秒)
|
|
* @returns Promise
|
|
*/
|
|
export function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* 重试函数
|
|
* @param fn - 要重试的函数
|
|
* @param retries - 重试次数
|
|
* @param delay - 重试间隔(毫秒)
|
|
* @returns Promise
|
|
*/
|
|
export async function retry<T>(
|
|
fn: () => Promise<T>,
|
|
retries: number = 3,
|
|
delay: number = 1000
|
|
): Promise<T> {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
if (retries > 0) {
|
|
await sleep(delay);
|
|
return retry(fn, retries - 1, delay);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取随机颜色
|
|
* @returns 随机颜色十六进制字符串
|
|
*/
|
|
export function getRandomColor(): string {
|
|
return '#' + Math.floor(Math.random() * 16777215).toString(16);
|
|
}
|
|
|
|
/**
|
|
* 计算相对时间
|
|
* @param date - 日期
|
|
* @returns 相对时间字符串
|
|
*/
|
|
export function getRelativeTime(date: Date | string): string {
|
|
const now = new Date();
|
|
const target = typeof date === 'string' ? new Date(date) : date;
|
|
const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
|
|
|
|
if (diffInSeconds < 60) {
|
|
return '刚刚';
|
|
} else if (diffInSeconds < 3600) {
|
|
return `${Math.floor(diffInSeconds / 60)} 分钟前`;
|
|
} else if (diffInSeconds < 86400) {
|
|
return `${Math.floor(diffInSeconds / 3600)} 小时前`;
|
|
} else if (diffInSeconds < 2592000) {
|
|
return `${Math.floor(diffInSeconds / 86400)} 天前`;
|
|
} else {
|
|
return formatDate(target);
|
|
}
|
|
} |