Files
qiming/qiming-vite-plugin-design-mode/examples/full-featured/src/utils/index.ts
2026-06-01 13:43:09 +08:00

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);
}
}