feat: 完成口译服务管理后台核心功能开发
This commit is contained in:
+260
@@ -0,0 +1,260 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
|
||||
// 合并 CSS 类名
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return clsx(inputs);
|
||||
}
|
||||
|
||||
// 格式化货币
|
||||
export function formatCurrency(amount: number, currency: 'CNY' | 'USD' = 'CNY') {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
export function formatTime(date: string | Date) {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(date));
|
||||
}
|
||||
|
||||
// 格式化相对时间
|
||||
export function formatRelativeTime(date: string | Date) {
|
||||
const now = new Date();
|
||||
const target = new Date(date);
|
||||
const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return `${diffInSeconds}秒前`;
|
||||
} 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 formatTime(date);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化通话时长
|
||||
export function formatDuration(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
export function formatFileSize(bytes: number) {
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 生成随机ID
|
||||
export function generateId() {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
export function 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 function throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle: boolean;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 深拷贝
|
||||
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 any;
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item)) as any;
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {} as any;
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 验证邮箱
|
||||
export function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
export function isValidPhone(phone: string): boolean {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
|
||||
// 获取通话状态的中文描述
|
||||
export function getCallStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '等待中',
|
||||
active: '通话中',
|
||||
ended: '已结束',
|
||||
cancelled: '已取消',
|
||||
failed: '失败',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 获取通话模式的中文描述
|
||||
export function getCallModeText(mode: string): string {
|
||||
const modeMap: Record<string, string> = {
|
||||
ai_voice: 'AI语音',
|
||||
ai_video: 'AI视频',
|
||||
sign_language: '手语翻译',
|
||||
human_interpreter: '真人翻译',
|
||||
};
|
||||
return modeMap[mode] || mode;
|
||||
}
|
||||
|
||||
// 获取用户类型的中文描述
|
||||
export function getUserTypeText(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
individual: '个人用户',
|
||||
enterprise: '企业用户',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
// 获取订单状态的中文描述
|
||||
export function getOrderStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
refunded: '已退款',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 获取文档翻译状态的中文描述
|
||||
export function getDocumentStatusText(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
uploaded: '已上传',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 计算通话费用(按分钟向上取整)
|
||||
export function calculateCallCost(durationInSeconds: number, ratePerMinute: number): number {
|
||||
const minutes = Math.ceil(durationInSeconds / 60);
|
||||
return minutes * ratePerMinute;
|
||||
}
|
||||
|
||||
// 检查余额是否足够
|
||||
export function checkBalanceSufficient(balance: number, requiredAmount: number): boolean {
|
||||
return balance >= requiredAmount;
|
||||
}
|
||||
|
||||
// 获取颜色类名基于状态
|
||||
export function getStatusColor(status: string): string {
|
||||
const colorMap: Record<string, string> = {
|
||||
active: 'text-green-600 bg-green-100',
|
||||
pending: 'text-yellow-600 bg-yellow-100',
|
||||
ended: 'text-gray-600 bg-gray-100',
|
||||
cancelled: 'text-red-600 bg-red-100',
|
||||
failed: 'text-red-600 bg-red-100',
|
||||
completed: 'text-green-600 bg-green-100',
|
||||
processing: 'text-blue-600 bg-blue-100',
|
||||
uploaded: 'text-purple-600 bg-purple-100',
|
||||
};
|
||||
return colorMap[status] || 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
|
||||
// 安全地解析JSON
|
||||
export function safeJsonParse<T>(str: string, fallback: T): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成头像URL
|
||||
export function generateAvatarUrl(name: string): string {
|
||||
const firstChar = name.charAt(0).toUpperCase();
|
||||
return `https://ui-avatars.com/api/?name=${encodeURIComponent(firstChar)}&background=random&color=fff&size=40`;
|
||||
}
|
||||
|
||||
// 导出所有状态文本映射
|
||||
export const STATUS_TEXTS = {
|
||||
CALL_STATUS: {
|
||||
pending: '等待中',
|
||||
active: '通话中',
|
||||
ended: '已结束',
|
||||
cancelled: '已取消',
|
||||
failed: '失败',
|
||||
},
|
||||
CALL_MODE: {
|
||||
ai_voice: 'AI语音',
|
||||
ai_video: 'AI视频',
|
||||
sign_language: '手语翻译',
|
||||
human_interpreter: '真人翻译',
|
||||
},
|
||||
USER_TYPE: {
|
||||
individual: '个人用户',
|
||||
enterprise: '企业用户',
|
||||
},
|
||||
ORDER_STATUS: {
|
||||
pending: '待处理',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
refunded: '已退款',
|
||||
},
|
||||
DOCUMENT_STATUS: {
|
||||
uploaded: '已上传',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
},
|
||||
NOTIFICATION_TYPE: {
|
||||
info: '信息',
|
||||
warning: '警告',
|
||||
error: '错误',
|
||||
success: '成功',
|
||||
},
|
||||
} as const;
|
||||
Reference in New Issue
Block a user