feat: 移动端开发完成 - 包含完整的移动端应用和Web管理后台

This commit is contained in:
2025-06-28 12:07:25 +08:00
commit 1a3e922235
75 changed files with 23857 additions and 0 deletions
+443
View File
@@ -0,0 +1,443 @@
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 || '操作成功';
};
+247
View File
@@ -0,0 +1,247 @@
import {
User,
CallSession,
DocumentTranslation,
Appointment,
Notification,
Contract,
Language
} from '@/types';
// 模拟用户数据
export const mockUser: User = {
id: 'user-123',
email: 'test@example.com',
role: 'client',
idNumber: '123456789012345678',
stripeCustomerId: 'cus_test123',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-15T10:30:00Z',
};
// 模拟合约数据
export const mockContract: Contract = {
contractId: 'contract-456',
userId: 'user-123',
billingType: 'per_minute',
creditBalance: 150,
monthlyMinutes: 300,
createdAt: '2024-01-01T00:00:00Z',
expiresAt: '2024-12-31T23:59:59Z',
};
// 模拟通话记录
export const mockCallSessions: CallSession[] = [
{
id: 'call-001',
userId: 'user-123',
interpreterId: 'interpreter-001',
mode: 'human',
sourceLanguage: 'zh',
targetLanguage: 'en',
status: 'completed',
duration: 1800, // 30分钟
cost: 45.00,
twilioRoomId: 'room-abc123',
recordingUrl: 'https://recordings.example.com/call-001.mp3',
createdAt: '2024-01-15T09:00:00Z',
endedAt: '2024-01-15T09:30:00Z',
},
{
id: 'call-002',
userId: 'user-123',
mode: 'ai',
sourceLanguage: 'en',
targetLanguage: 'es',
status: 'completed',
duration: 900, // 15分钟
cost: 15.00,
twilioRoomId: 'room-def456',
createdAt: '2024-01-14T14:30:00Z',
endedAt: '2024-01-14T14:45:00Z',
},
{
id: 'call-003',
userId: 'user-123',
mode: 'video',
sourceLanguage: 'zh',
targetLanguage: 'fr',
status: 'cancelled',
duration: 0,
cost: 0,
createdAt: '2024-01-13T16:00:00Z',
},
];
// 模拟文档翻译数据
export const mockDocuments: DocumentTranslation[] = [
{
id: 'doc-001',
userId: 'user-123',
originalFileName: '合同文件.pdf',
translatedFileName: 'contract_document.pdf',
sourceLanguage: 'zh',
targetLanguage: 'en',
status: 'completed',
s3Key: 'documents/original/doc-001.pdf',
translatedS3Key: 'documents/translated/doc-001-en.pdf',
cost: 25.00,
createdAt: '2024-01-12T10:00:00Z',
completedAt: '2024-01-12T12:30:00Z',
},
{
id: 'doc-002',
userId: 'user-123',
originalFileName: '技术说明书.docx',
sourceLanguage: 'zh',
targetLanguage: 'en',
status: 'review',
s3Key: 'documents/original/doc-002.docx',
cost: 35.00,
reviewNotes: '专业术语需要进一步确认',
createdAt: '2024-01-11T15:20:00Z',
},
{
id: 'doc-003',
userId: 'user-123',
originalFileName: '用户手册.txt',
sourceLanguage: 'en',
targetLanguage: 'ja',
status: 'processing',
s3Key: 'documents/original/doc-003.txt',
cost: 20.00,
createdAt: '2024-01-10T08:45:00Z',
},
];
// 模拟预约数据
export const mockAppointments: Appointment[] = [
{
id: 'apt-001',
userId: 'user-123',
interpreterId: 'interpreter-002',
title: '商务会议翻译',
description: '与美国客户的重要商务谈判',
startTime: '2024-01-20T10:00:00Z',
endTime: '2024-01-20T12:00:00Z',
mode: 'human',
sourceLanguage: 'zh',
targetLanguage: 'en',
status: 'confirmed',
reminderSent: false,
googleEventId: 'google-event-123',
createdAt: '2024-01-15T14:30:00Z',
},
{
id: 'apt-002',
userId: 'user-123',
title: 'AI翻译测试',
description: '测试新的AI翻译功能',
startTime: '2024-01-18T15:30:00Z',
endTime: '2024-01-18T16:00:00Z',
mode: 'ai',
sourceLanguage: 'en',
targetLanguage: 'fr',
status: 'scheduled',
reminderSent: true,
createdAt: '2024-01-16T09:15:00Z',
},
];
// 模拟通知数据
export const mockNotifications: Notification[] = [
{
id: 'notif-001',
userId: 'user-123',
title: '文档翻译完成',
body: '您的文档"合同文件.pdf"翻译已完成,请查看结果。',
type: 'document',
read: false,
data: { documentId: 'doc-001' },
createdAt: '2024-01-15T12:30:00Z',
},
{
id: 'notif-002',
userId: 'user-123',
title: '预约提醒',
body: '您有一个商务会议翻译预约将在1小时后开始。',
type: 'call',
read: false,
data: { appointmentId: 'apt-001' },
createdAt: '2024-01-15T09:00:00Z',
},
{
id: 'notif-003',
userId: 'user-123',
title: '余额不足提醒',
body: '您的账户余额不足,请及时充值以免影响服务使用。',
type: 'payment',
read: true,
createdAt: '2024-01-14T16:45:00Z',
},
];
// 支持的语言列表
export const mockLanguages: Language[] = [
{ code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳' },
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦' },
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇵🇹' },
];
// 模拟统计数据
export const mockUsageStats = {
totalCalls: 25,
totalMinutes: 1250,
totalDocuments: 12,
totalSpent: 485.50,
monthlyBreakdown: [
{ month: '2024-01', calls: 8, minutes: 420, cost: 165.00 },
{ month: '2023-12', calls: 12, minutes: 580, cost: 220.50 },
{ month: '2023-11', calls: 5, minutes: 250, cost: 100.00 },
],
};
// 获取随机模拟数据的工具函数
export const getRandomCallSession = (): CallSession => {
const modes: Array<'ai' | 'human' | 'video' | 'sign'> = ['ai', 'human', 'video', 'sign'];
const statuses: Array<'pending' | 'active' | 'completed' | 'cancelled'> = ['completed', 'cancelled'];
const languages = ['zh', 'en', 'es', 'fr', 'de', 'ja'];
return {
id: `call-${Date.now()}`,
userId: 'user-123',
mode: modes[Math.floor(Math.random() * modes.length)],
sourceLanguage: languages[Math.floor(Math.random() * languages.length)],
targetLanguage: languages[Math.floor(Math.random() * languages.length)],
status: statuses[Math.floor(Math.random() * statuses.length)],
duration: Math.floor(Math.random() * 3600), // 0-60分钟
cost: Math.floor(Math.random() * 100) + 10, // 10-110美元
createdAt: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(),
};
};
export const getRandomDocument = (): DocumentTranslation => {
const statuses: Array<'uploading' | 'processing' | 'review' | 'completed' | 'failed'> =
['processing', 'review', 'completed'];
const fileNames = ['文档1.pdf', '合同.docx', '说明书.txt', '报告.xlsx'];
const languages = ['zh', 'en', 'es', 'fr', 'de', 'ja'];
return {
id: `doc-${Date.now()}`,
userId: 'user-123',
originalFileName: fileNames[Math.floor(Math.random() * fileNames.length)],
sourceLanguage: languages[Math.floor(Math.random() * languages.length)],
targetLanguage: languages[Math.floor(Math.random() * languages.length)],
status: statuses[Math.floor(Math.random() * statuses.length)],
s3Key: `documents/original/doc-${Date.now()}.pdf`,
cost: Math.floor(Math.random() * 50) + 10, // 10-60美元
createdAt: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(),
};
};
+82
View File
@@ -0,0 +1,82 @@
import * as Keychain from 'react-native-keychain';
import AsyncStorage from '@react-native-async-storage/async-storage';
// 安全存储(用于敏感信息如token)
export const setToken = async (token: string): Promise<void> => {
try {
await Keychain.setInternetCredentials(
'TranslatePro',
'auth_token',
token
);
} catch (error) {
console.error('Error storing token:', error);
}
};
export const getToken = async (): Promise<string | null> => {
try {
const credentials = await Keychain.getInternetCredentials('TranslatePro');
if (credentials) {
return credentials.password;
}
return null;
} catch (error) {
console.error('Error retrieving token:', error);
return null;
}
};
export const removeToken = async (): Promise<void> => {
try {
await Keychain.resetInternetCredentials('TranslatePro');
} catch (error) {
console.error('Error removing token:', error);
}
};
// 普通存储(用于非敏感信息)
export const setStorageItem = async (key: string, value: any): Promise<void> => {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
} catch (error) {
console.error('Error storing item:', error);
}
};
export const getStorageItem = async <T>(key: string): Promise<T | null> => {
try {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error('Error retrieving item:', error);
return null;
}
};
export const removeStorageItem = async (key: string): Promise<void> => {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('Error removing item:', error);
}
};
export const clearAllStorage = async (): Promise<void> => {
try {
await AsyncStorage.clear();
await Keychain.resetInternetCredentials('TranslatePro');
} catch (error) {
console.error('Error clearing storage:', error);
}
};
// 存储键常量
export const STORAGE_KEYS = {
USER_PREFERENCES: 'user_preferences',
LANGUAGE_SETTINGS: 'language_settings',
CALL_HISTORY: 'call_history',
DOCUMENT_CACHE: 'document_cache',
NOTIFICATION_SETTINGS: 'notification_settings',
} as const;