shop-wx/doc/examples/utils-example.ts

473 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 工具函数示例代码
* 展示项目中工具函数的编写规范和最佳实践
*/
/**
* 格式化日期
* @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);
}