/** * @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 any>( func: T, delay: number ): (...args: Parameters) => void { let timeoutId: NodeJS.Timeout; return (...args: Parameters) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(null, args), delay); }; } /** * 节流函数 * @param func - 要节流的函数 * @param delay - 延迟时间(毫秒) * @returns 节流后的函数 */ export function throttle any>( func: T, delay: number ): (...args: Parameters) => void { let lastCall = 0; return (...args: Parameters) => { const now = new Date().getTime(); if (now - lastCall < delay) { return; } lastCall = now; return func.apply(null, args); }; } /** * 深拷贝对象 * @param obj - 要拷贝的对象 * @returns 深拷贝后的对象 */ export function deepClone(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 是否复制成功 */ export async function copyToClipboard(text: string): Promise { 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(array: T[]): T[] { return [...new Set(array)]; } /** * 数组分组 * @param array - 原数组 * @param size - 每组大小 * @returns 分组后的二维数组 */ export function chunkArray(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>( array: T[], key: keyof T ): Record { return array.reduce((groups, item) => { const group = String(item[key]); groups[group] = groups[group] || []; groups[group].push(item); return groups; }, {} as Record); } /** * 等待指定时间 * @param ms - 等待时间(毫秒) * @returns Promise */ export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 重试函数 * @param fn - 要重试的函数 * @param retries - 重试次数 * @param delay - 重试间隔(毫秒) * @returns Promise */ export async function retry( fn: () => Promise, retries: number = 3, delay: number = 1000 ): Promise { 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); } }