Twilioapp/src/utils/index.ts

443 lines
11 KiB
TypeScript
Raw 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.

import { DATE_FORMATS } from '@/constants';
// 日期格式化
export const formatDate = (
date: string | Date,
format: string = DATE_FORMATS.DISPLAY_DATETIME
): string => {
if (!date) return '';
const d = new Date(date);
if (isNaN(d.getTime())) return '';
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.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)
.replace('年', '年')
.replace('月', '月')
.replace('日', '日');
};
// 日期时间格式化formatDate 的别名,用于向后兼容)
export const formatDateTime = formatDate;
// 相对时间格式化
export const formatRelativeTime = (date: string | Date): string => {
if (!date) return '';
const d = new Date(date);
if (isNaN(d.getTime())) return '';
const now = new Date();
const diff = now.getTime() - d.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
return formatDate(date, DATE_FORMATS.DISPLAY_DATE);
};
// 货币格式化
export const formatCurrency = (
amount: number,
currency: string = 'USD',
locale: string = 'zh-CN'
): string => {
if (typeof amount !== 'number' || isNaN(amount)) return '¥0.00';
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
return formatter.format(amount);
};
// 数字格式化
export const formatNumber = (
num: number,
locale: string = 'zh-CN'
): string => {
if (typeof num !== 'number' || isNaN(num)) return '0';
return new Intl.NumberFormat(locale).format(num);
};
// 百分比格式化
export const formatPercentage = (
value: number,
decimals: number = 1
): string => {
if (typeof value !== 'number' || isNaN(value)) return '0%';
return `${value.toFixed(decimals)}%`;
};
// 文件大小格式化
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
// 时长格式化(秒转换为时分秒)
export const formatDuration = (seconds: number): string => {
if (typeof seconds !== 'number' || isNaN(seconds) || seconds < 0) return '0秒';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
if (hours > 0) {
return `${hours}小时${minutes}分钟${remainingSeconds}`;
} else if (minutes > 0) {
return `${minutes}分钟${remainingSeconds}`;
} else {
return `${remainingSeconds}`;
}
};
// 字符串截断
export const truncateText = (
text: string,
maxLength: number = 50,
suffix: string = '...'
): string => {
if (!text || typeof text !== 'string') return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - suffix.length) + suffix;
};
// 邮箱验证
export const isValidEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// 手机号验证(中国大陆)
export const isValidPhone = (phone: string): boolean => {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
};
// URL验证
export const isValidUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// 密码强度验证
export const getPasswordStrength = (password: string): {
score: number;
level: 'weak' | 'medium' | 'strong';
feedback: string[];
} => {
const feedback: string[] = [];
let score = 0;
if (password.length >= 8) {
score += 1;
} else {
feedback.push('密码长度至少8位');
}
if (/[a-z]/.test(password)) {
score += 1;
} else {
feedback.push('包含小写字母');
}
if (/[A-Z]/.test(password)) {
score += 1;
} else {
feedback.push('包含大写字母');
}
if (/\d/.test(password)) {
score += 1;
} else {
feedback.push('包含数字');
}
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
score += 1;
} else {
feedback.push('包含特殊字符');
}
let level: 'weak' | 'medium' | 'strong';
if (score <= 2) {
level = 'weak';
} else if (score <= 3) {
level = 'medium';
} else {
level = 'strong';
}
return { score, level, feedback };
};
// 深拷贝
export const 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 { [key: string]: any };
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone((obj as { [key: string]: any })[key]);
}
}
return clonedObj as T;
}
return obj;
};
// 防抖函数
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
// 节流函数
export const throttle = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let inThrottle = false;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, wait);
}
};
};
// 生成随机字符串
export const generateRandomString = (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;
};
// 生成UUID
export const generateUUID = (): string => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
// 数组去重
export const uniqueArray = <T>(array: T[], key?: keyof T): T[] => {
if (!key) {
return [...new Set(array)];
}
const seen = new Set();
return array.filter(item => {
const keyValue = item[key];
if (seen.has(keyValue)) {
return false;
}
seen.add(keyValue);
return true;
});
};
// 数组分组
export const groupBy = <T>(
array: T[],
key: keyof T | ((item: T) => string | number)
): Record<string, T[]> => {
return array.reduce((groups, item) => {
const groupKey = typeof key === 'function' ? key(item) : item[key];
const keyStr = String(groupKey);
if (!groups[keyStr]) {
groups[keyStr] = [];
}
groups[keyStr].push(item);
return groups;
}, {} as Record<string, T[]>);
};
// 对象转查询字符串
export const objectToQueryString = (obj: Record<string, any>): string => {
const params = new URLSearchParams();
Object.entries(obj).forEach(([key, value]) => {
if (value !== null && value !== undefined && value !== '') {
if (Array.isArray(value)) {
value.forEach(item => params.append(key, String(item)));
} else {
params.append(key, String(value));
}
}
});
return params.toString();
};
// 查询字符串转对象
export const queryStringToObject = (queryString: string): Record<string, any> => {
const params = new URLSearchParams(queryString);
const result: Record<string, any> = {};
for (const [key, value] of params.entries()) {
if (result[key]) {
if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
} else {
result[key] = value;
}
}
return result;
};
// 本地存储封装
export const storage = {
get: <T>(key: string, defaultValue?: T): T | null => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue || null;
} catch {
return defaultValue || null;
}
},
set: (key: string, value: any): void => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Storage set error:', error);
}
},
remove: (key: string): void => {
try {
localStorage.removeItem(key);
} catch (error) {
console.error('Storage remove error:', error);
}
},
clear: (): void => {
try {
localStorage.clear();
} catch (error) {
console.error('Storage clear error:', error);
}
}
};
// 颜色工具
export const colorUtils = {
// 十六进制转RGB
hexToRgb: (hex: string): { r: number; g: number; b: number } | null => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
},
// RGB转十六进制
rgbToHex: (r: number, g: number, b: number): string => {
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
},
// 获取对比色
getContrastColor: (hex: string): string => {
const rgb = colorUtils.hexToRgb(hex);
if (!rgb) return '#000000';
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
return brightness > 128 ? '#000000' : '#ffffff';
}
};
// 设备检测
export const deviceUtils = {
isMobile: (): boolean => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
},
isTablet: (): boolean => {
return /iPad|Android/i.test(navigator.userAgent) && !deviceUtils.isMobile();
},
isDesktop: (): boolean => {
return !deviceUtils.isMobile() && !deviceUtils.isTablet();
},
getScreenSize: (): 'xs' | 'sm' | 'md' | 'lg' | 'xl' => {
const width = window.innerWidth;
if (width < 576) return 'xs';
if (width < 768) return 'sm';
if (width < 992) return 'md';
if (width < 1200) return 'lg';
return 'xl';
}
};
// 错误处理
export const handleError = (error: any): string => {
if (typeof error === 'string') return error;
if (error?.message) return error.message;
if (error?.response?.data?.message) return error.response.data.message;
return '操作失败,请稍后重试';
};
// 成功提示
export const handleSuccess = (message?: string): string => {
return message || '操作成功';
};