473 lines
11 KiB
TypeScript
473 lines
11 KiB
TypeScript
|
|
/**
|
|||
|
|
* 工具函数示例代码
|
|||
|
|
* 展示项目中工具函数的编写规范和最佳实践
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化日期
|
|||
|
|
* @param date 日期(Date | string | number)
|
|||
|
|
* @param format 格式化模式(默认 'YYYY-MM-DD HH:mm:ss')
|
|||
|
|
* @returns 格式化后的日期字符串
|
|||
|
|
*/
|
|||
|
|
export function formatDate(date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
|||
|
|
// 如果传入的是字符串或数字,先转换为 Date 对象
|
|||
|
|
const dateObj = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date;
|
|||
|
|
|
|||
|
|
// 验证日期有效性
|
|||
|
|
if (isNaN(dateObj.getTime())) {
|
|||
|
|
throw new Error('无效的日期格式');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const year = dateObj.getFullYear();
|
|||
|
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|||
|
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
|||
|
|
const hours = String(dateObj.getHours()).padStart(2, '0');
|
|||
|
|
const minutes = String(dateObj.getMinutes()).padStart(2, '0');
|
|||
|
|
const seconds = String(dateObj.getSeconds()).padStart(2, '0');
|
|||
|
|
|
|||
|
|
return format
|
|||
|
|
.replace('YYYY', String(year))
|
|||
|
|
.replace('MM', month)
|
|||
|
|
.replace('DD', day)
|
|||
|
|
.replace('HH', hours)
|
|||
|
|
.replace('mm', minutes)
|
|||
|
|
.replace('ss', seconds);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 相对时间格式化
|
|||
|
|
* @param date 日期
|
|||
|
|
* @returns 相对时间字符串(如:2分钟前、3小时前)
|
|||
|
|
*/
|
|||
|
|
export function formatRelativeTime(date: Date | string | number): string {
|
|||
|
|
const now = new Date();
|
|||
|
|
const targetDate = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date;
|
|||
|
|
const diff = now.getTime() - targetDate.getTime();
|
|||
|
|
|
|||
|
|
const minute = 60 * 1000;
|
|||
|
|
const hour = 60 * minute;
|
|||
|
|
const day = 24 * hour;
|
|||
|
|
const week = 7 * day;
|
|||
|
|
const month = 30 * day;
|
|||
|
|
const year = 365 * day;
|
|||
|
|
|
|||
|
|
if (diff < minute) {
|
|||
|
|
return '刚刚';
|
|||
|
|
} else if (diff < hour) {
|
|||
|
|
return `${Math.floor(diff / minute)}分钟前`;
|
|||
|
|
} else if (diff < day) {
|
|||
|
|
return `${Math.floor(diff / hour)}小时前`;
|
|||
|
|
} else if (diff < week) {
|
|||
|
|
return `${Math.floor(diff / day)}天前`;
|
|||
|
|
} else if (diff < month) {
|
|||
|
|
return `${Math.floor(diff / week)}周前`;
|
|||
|
|
} else if (diff < year) {
|
|||
|
|
return `${Math.floor(diff / month)}月前`;
|
|||
|
|
} else {
|
|||
|
|
return `${Math.floor(diff / year)}年前`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 防抖函数
|
|||
|
|
* @param func 要防抖的函数
|
|||
|
|
* @param delay 延迟时间(毫秒)
|
|||
|
|
* @returns 防抖后的函数
|
|||
|
|
*/
|
|||
|
|
export function debounce<T extends (...args: any[]) => any>(
|
|||
|
|
func: T,
|
|||
|
|
delay: number
|
|||
|
|
): (...args: Parameters<T>) => void {
|
|||
|
|
let timerId: NodeJS.Timeout | null = null;
|
|||
|
|
|
|||
|
|
return function (this: any, ...args: Parameters<T>) {
|
|||
|
|
// 清除之前的定时器
|
|||
|
|
if (timerId) {
|
|||
|
|
clearTimeout(timerId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置新的定时器
|
|||
|
|
timerId = setTimeout(() => {
|
|||
|
|
func.apply(this, args);
|
|||
|
|
}, delay);
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 节流函数
|
|||
|
|
* @param func 要节流的函数
|
|||
|
|
* @param delay 间隔时间(毫秒)
|
|||
|
|
* @returns 节流后的函数
|
|||
|
|
*/
|
|||
|
|
export function throttle<T extends (...args: any[]) => any>(
|
|||
|
|
func: T,
|
|||
|
|
delay: number
|
|||
|
|
): (...args: Parameters<T>) => void {
|
|||
|
|
let lastExecTime = 0;
|
|||
|
|
|
|||
|
|
return function (this: any, ...args: Parameters<T>) {
|
|||
|
|
const currentTime = Date.now();
|
|||
|
|
|
|||
|
|
// 如果间隔时间未到,不执行函数
|
|||
|
|
if (currentTime - lastExecTime < delay) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新最后执行时间
|
|||
|
|
lastExecTime = currentTime;
|
|||
|
|
|
|||
|
|
// 执行函数
|
|||
|
|
func.apply(this, args);
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 深拷贝
|
|||
|
|
* @param obj 要拷贝的对象
|
|||
|
|
* @returns 深拷贝后的对象
|
|||
|
|
*/
|
|||
|
|
export function deepClone<T>(obj: T): T {
|
|||
|
|
// 处理基本类型和 null
|
|||
|
|
if (obj === null || typeof obj !== 'object') {
|
|||
|
|
return obj;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理 Date 类型
|
|||
|
|
if (obj instanceof Date) {
|
|||
|
|
return new Date(obj.getTime()) as unknown as T;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理 Array 类型
|
|||
|
|
if (Array.isArray(obj)) {
|
|||
|
|
return obj.map(item => deepClone(item)) as unknown as T;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理对象类型
|
|||
|
|
const clonedObj = {} as T;
|
|||
|
|
for (const key in obj) {
|
|||
|
|
if (obj.hasOwnProperty(key)) {
|
|||
|
|
clonedObj[key] = deepClone(obj[key]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return clonedObj;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成唯一ID
|
|||
|
|
* @param prefix 前缀(可选)
|
|||
|
|
* @returns 唯一ID字符串
|
|||
|
|
*/
|
|||
|
|
export function generateId(prefix = 'id'): string {
|
|||
|
|
const timestamp = Date.now().toString(36);
|
|||
|
|
const randomStr = Math.random().toString(36).substring(2, 8);
|
|||
|
|
return `${prefix}_${timestamp}_${randomStr}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 数字格式化(添加千位分隔符)
|
|||
|
|
* @param num 数字
|
|||
|
|
* @param precision 小数位数(默认2)
|
|||
|
|
* @returns 格式化后的数字字符串
|
|||
|
|
*/
|
|||
|
|
export function formatNumber(num: number, precision = 2): string {
|
|||
|
|
return Number(num).toLocaleString('zh-CN', {
|
|||
|
|
minimumFractionDigits: precision,
|
|||
|
|
maximumFractionDigits: precision,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 价格格式化
|
|||
|
|
* @param price 价格
|
|||
|
|
* @returns 格式化后的价格字符串(如:¥100.00)
|
|||
|
|
*/
|
|||
|
|
export function formatPrice(price: number): string {
|
|||
|
|
return `¥${formatNumber(price, 2)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证手机号
|
|||
|
|
* @param phone 手机号
|
|||
|
|
* @returns 是否为有效的手机号
|
|||
|
|
*/
|
|||
|
|
export function validatePhone(phone: string): boolean {
|
|||
|
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
|||
|
|
return phoneRegex.test(phone);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证邮箱
|
|||
|
|
* @param email 邮箱
|
|||
|
|
* @returns 是否为有效的邮箱
|
|||
|
|
*/
|
|||
|
|
export function validateEmail(email: string): boolean {
|
|||
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|||
|
|
return emailRegex.test(email);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证身份证号
|
|||
|
|
* @param idCard 身份证号
|
|||
|
|
* @returns 是否为有效的身份证号
|
|||
|
|
*/
|
|||
|
|
export function validateIdCard(idCard: string): boolean {
|
|||
|
|
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
|||
|
|
return idCardRegex.test(idCard);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取图片尺寸(异步)
|
|||
|
|
* @param src 图片地址
|
|||
|
|
* @returns Promise<{width: number, height: number}>
|
|||
|
|
*/
|
|||
|
|
export function getImageSize(src: string): Promise<{ width: number; height: number }> {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const img = new Image();
|
|||
|
|
img.onload = () => {
|
|||
|
|
resolve({
|
|||
|
|
width: img.width,
|
|||
|
|
height: img.height,
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
img.onerror = reject;
|
|||
|
|
img.src = src;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 下载文件
|
|||
|
|
* @param url 文件地址
|
|||
|
|
* @param filename 文件名(可选)
|
|||
|
|
*/
|
|||
|
|
export function downloadFile(url: string, filename?: string): void {
|
|||
|
|
const link = document.createElement('a');
|
|||
|
|
link.href = url;
|
|||
|
|
if (filename) {
|
|||
|
|
link.download = filename;
|
|||
|
|
}
|
|||
|
|
document.body.appendChild(link);
|
|||
|
|
link.click();
|
|||
|
|
document.body.removeChild(link);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 复制到剪贴板
|
|||
|
|
* @param text 要复制的文本
|
|||
|
|
*/
|
|||
|
|
export function copyToClipboard(text: string): Promise<void> {
|
|||
|
|
if (navigator.clipboard) {
|
|||
|
|
return navigator.clipboard.writeText(text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 兼容旧版浏览器
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const textarea = document.createElement('textarea');
|
|||
|
|
textarea.value = text;
|
|||
|
|
document.body.appendChild(textarea);
|
|||
|
|
textarea.select();
|
|||
|
|
try {
|
|||
|
|
document.execCommand('copy');
|
|||
|
|
resolve();
|
|||
|
|
} catch (err) {
|
|||
|
|
reject(err);
|
|||
|
|
} finally {
|
|||
|
|
document.body.removeChild(textarea);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取URL参数
|
|||
|
|
* @param url URL地址(可选,默认当前页面的URL)
|
|||
|
|
* @returns 参数对象
|
|||
|
|
*/
|
|||
|
|
export function getUrlParams(url?: string): Record<string, string> {
|
|||
|
|
const urlObj = new URL(url || window.location.href);
|
|||
|
|
const params: Record<string, string> = {};
|
|||
|
|
urlObj.searchParams.forEach((value, key) => {
|
|||
|
|
params[key] = value;
|
|||
|
|
});
|
|||
|
|
return params;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置URL参数
|
|||
|
|
* @param params 参数对象
|
|||
|
|
* @param replace 是否替换当前历史记录(默认false)
|
|||
|
|
*/
|
|||
|
|
export function setUrlParams(params: Record<string, any>, replace = false): void {
|
|||
|
|
const url = new URL(window.location.href);
|
|||
|
|
|
|||
|
|
Object.entries(params).forEach(([key, value]) => {
|
|||
|
|
if (value !== undefined && value !== null) {
|
|||
|
|
url.searchParams.set(key, String(value));
|
|||
|
|
} else {
|
|||
|
|
url.searchParams.delete(key);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (replace) {
|
|||
|
|
window.history.replaceState({}, '', url.toString());
|
|||
|
|
} else {
|
|||
|
|
window.history.pushState({}, '', url.toString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 数组分组
|
|||
|
|
* @param array 数组
|
|||
|
|
* @param size 每组大小
|
|||
|
|
* @returns 分组后的二维数组
|
|||
|
|
*/
|
|||
|
|
export function chunk<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 unique<T>(array: T[], key?: keyof T): T[] {
|
|||
|
|
if (!key) {
|
|||
|
|
return Array.from(new Set(array));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const seen = new Set();
|
|||
|
|
return array.filter(item => {
|
|||
|
|
const value = item[key];
|
|||
|
|
if (seen.has(value)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
seen.add(value);
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 数组排序
|
|||
|
|
* @param array 数组
|
|||
|
|
* @param key 排序键
|
|||
|
|
* @param order 排序方向('asc' | 'desc',默认'asc')
|
|||
|
|
* @returns 排序后的数组
|
|||
|
|
*/
|
|||
|
|
export function sortBy<T>(array: T[], key: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {
|
|||
|
|
return [...array].sort((a, b) => {
|
|||
|
|
const aVal = a[key];
|
|||
|
|
const bVal = b[key];
|
|||
|
|
|
|||
|
|
if (aVal < bVal) {
|
|||
|
|
return order === 'asc' ? -1 : 1;
|
|||
|
|
}
|
|||
|
|
if (aVal > bVal) {
|
|||
|
|
return order === 'asc' ? 1 : -1;
|
|||
|
|
}
|
|||
|
|
return 0;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 树形结构转换
|
|||
|
|
* @param data 源数据
|
|||
|
|
* @param options 配置
|
|||
|
|
* @returns 树形结构数据
|
|||
|
|
*/
|
|||
|
|
export function arrayToTree<T>(
|
|||
|
|
data: T[],
|
|||
|
|
options: {
|
|||
|
|
id?: keyof T;
|
|||
|
|
parentId?: keyof T;
|
|||
|
|
children?: string;
|
|||
|
|
}
|
|||
|
|
): T[] {
|
|||
|
|
const { id = 'id', parentId = 'parentId', children = 'children' } = options;
|
|||
|
|
const tree: T[] = [];
|
|||
|
|
const map = new Map<string, T & { [key: string]: any }>();
|
|||
|
|
|
|||
|
|
// 建立索引
|
|||
|
|
data.forEach(item => {
|
|||
|
|
map.set(String(item[id]), { ...item, [children]: [] });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 构建树
|
|||
|
|
data.forEach(item => {
|
|||
|
|
const node = map.get(String(item[id]))!;
|
|||
|
|
const parentIdValue = item[parentId];
|
|||
|
|
|
|||
|
|
if (parentIdValue && map.has(String(parentIdValue))) {
|
|||
|
|
map.get(String(parentIdValue))![children].push(node);
|
|||
|
|
} else {
|
|||
|
|
tree.push(node as unknown as T);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return tree;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 延迟执行(Promise)
|
|||
|
|
* @param ms 延迟时间(毫秒)
|
|||
|
|
* @returns Promise
|
|||
|
|
*/
|
|||
|
|
export function delay(ms: number): Promise<void> {
|
|||
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重试函数
|
|||
|
|
* @param fn 要执行的函数
|
|||
|
|
* @param retries 重试次数(默认3)
|
|||
|
|
* @param delay 重试间隔(默认1000ms)
|
|||
|
|
* @returns Promise
|
|||
|
|
*/
|
|||
|
|
export async function retry<T>(
|
|||
|
|
fn: () => Promise<T>,
|
|||
|
|
retries = 3,
|
|||
|
|
delayMs = 1000
|
|||
|
|
): Promise<T> {
|
|||
|
|
let lastError: Error;
|
|||
|
|
|
|||
|
|
for (let i = 0; i <= retries; i++) {
|
|||
|
|
try {
|
|||
|
|
return await fn();
|
|||
|
|
} catch (err) {
|
|||
|
|
lastError = err as Error;
|
|||
|
|
if (i === retries) {
|
|||
|
|
throw lastError;
|
|||
|
|
}
|
|||
|
|
await delay(delayMs);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw lastError!;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取随机颜色
|
|||
|
|
* @returns 十六进制颜色值
|
|||
|
|
*/
|
|||
|
|
export function getRandomColor(): string {
|
|||
|
|
return `#${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算两点距离
|
|||
|
|
* @param point1 点1 {x, y}
|
|||
|
|
* @param point2 点2 {x, y}
|
|||
|
|
* @returns 距离
|
|||
|
|
*/
|
|||
|
|
export function getDistance(
|
|||
|
|
point1: { x: number; y: number },
|
|||
|
|
point2: { x: number; y: number }
|
|||
|
|
): number {
|
|||
|
|
const dx = point2.x - point1.x;
|
|||
|
|
const dy = point2.y - point1.y;
|
|||
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|||
|
|
}
|