diff --git a/src-tauri/locales/en.json b/src-tauri/locales/en.json index 2c0f476..bedde3d 100644 --- a/src-tauri/locales/en.json +++ b/src-tauri/locales/en.json @@ -708,6 +708,37 @@ "confirm_import": "Confirm Import", "export_import_config_description": "Export current configuration or restore configuration from file", "confirm_import_description": "Importing configuration will overwrite all current settings and automatically restart the software. Continue?", - "password_obscured_hint": "Password is obscured by rclone and cannot be displayed. Please re-enter to modify" +"password_obscured_hint": "Password is obscured by rclone and cannot be displayed. Please re-enter to modify", + + "error_boundary_title": "An error occurred", + "error_boundary_subtitle": "The application encountered a problem, but your data is safe", + "reload": "Reload", + "validation_storage_name_empty": "Storage name cannot be empty", + "validation_storage_name_too_long": "Storage name length cannot exceed 128 characters", + "validation_storage_name_invalid_chars": "Storage name contains invalid characters", + "validation_storage_type_empty": "Storage type cannot be empty", + "validation_storage_params_empty": "Storage parameters cannot be empty", + "validation_input_invalid": "Invalid input parameters", + "error_unsupported_storage_type": "Unsupported storage type", + "error_storage_params_serialization": "Storage parameter serialization failed", + "error_storage_id_not_found": "Cannot get storage ID", + "error_operation_failed": "Operation failed", + "error_unsupported_framework": "Unsupported storage framework", + "error_storage_network_failure": "Storage operation failed, please check network connection", + "error_network": "Network connection failed, please check network settings", + "error_api": "Server request failed, please try again later", + "error_validation": "Invalid input data, please check and try again", + "error_auth": "Login expired, please login again", + "error_forbidden": "No permission to perform this operation", + "error_not_found": "Requested resource not found", + "error_timeout": "Request timeout, please try again later", + "error_file_system": "File operation failed", + "error_config": "Configuration error", + "error_unknown": "An unknown error occurred, please try again later", + "error_auth_failed": "Authentication failed", + "error_no_permission": "No permission", + "error_resource_not_found": "{{resource}} not found", + "error_operation_timeout": "{{operation}} operation timeout", + "no_logs": "No logs" } diff --git a/src-tauri/locales/zh-cn.json b/src-tauri/locales/zh-cn.json index a29c7ca..960f467 100644 --- a/src-tauri/locales/zh-cn.json +++ b/src-tauri/locales/zh-cn.json @@ -718,5 +718,36 @@ "proxy_range": "Proxy Range", "app_id": "App ID", "sign_key": "Sign Key", - "password_obscured_hint": "密码已被rclone混淆存储,无法显示原始值,如需修改请重新输入" + "password_obscured_hint": "密码已被rclone混淆存储,无法显示原始值,如需修改请重新输入", + + "error_boundary_title": "出现错误", + "error_boundary_subtitle": "应用遇到了问题,但别担心,数据是安全的", + "reload": "重新加载", + "validation_storage_name_empty": "存储名称不能为空", + "validation_storage_name_too_long": "存储名称长度不能超过128字符", + "validation_storage_name_invalid_chars": "存储名称包含非法字符", + "validation_storage_type_empty": "存储类型不能为空", + "validation_storage_params_empty": "存储参数不能为空", + "validation_input_invalid": "输入参数无效", + "error_unsupported_storage_type": "不支持的存储类型", + "error_storage_params_serialization": "存储参数序列化失败", + "error_storage_id_not_found": "无法获取存储 ID", + "error_operation_failed": "操作失败", + "error_unsupported_framework": "不支持的存储框架", + "error_storage_network_failure": "存储操作失败,请检查网络连接", + "error_network": "网络连接失败,请检查网络设置", + "error_api": "服务器请求失败,请稍后重试", + "error_validation": "输入数据有误,请检查后重试", + "error_auth": "登录已过期,请重新登录", + "error_forbidden": "没有权限执行此操作", + "error_not_found": "请求的资源不存在", + "error_timeout": "请求超时,请稍后重试", + "error_file_system": "文件操作失败", + "error_config": "配置错误", + "error_unknown": "发生未知错误,请稍后重试", + "error_auth_failed": "认证失败", + "error_no_permission": "没有权限", + "error_resource_not_found": "{{resource}} 不存在", + "error_operation_timeout": "{{operation}} 操作超时", + "no_logs": "暂无日志" } \ No newline at end of file diff --git a/src-tauri/locales/zh-hant.json b/src-tauri/locales/zh-hant.json index 88e95ed..7d937d2 100644 --- a/src-tauri/locales/zh-hant.json +++ b/src-tauri/locales/zh-hant.json @@ -708,6 +708,37 @@ "confirm_import": "確認導入", "export_import_config_description": "導出當前配置或者從文件恢復配置", "confirm_import_description": "導入配置將覆蓋當前所有配置並自動重啓軟件,是否繼續?", - "password_obscured_hint": "密碼已被rclone混淆存儲,無法顯示原始值,如需修改請重新輸入" +"password_obscured_hint": "密碼已被rclone混淆存儲,無法顯示原始值,如需修改請重新輸入", + + "error_boundary_title": "出現錯誤", + "error_boundary_subtitle": "應用遇到了問題,但不用擔心,數據是安全的", + "reload": "重新載入", + "validation_storage_name_empty": "存儲名稱不能為空", + "validation_storage_name_too_long": "存儲名稱長度不能超過128字符", + "validation_storage_name_invalid_chars": "存儲名稱包含非法字符", + "validation_storage_type_empty": "存儲類型不能為空", + "validation_storage_params_empty": "存儲參數不能為空", + "validation_input_invalid": "輸入參數無效", + "error_unsupported_storage_type": "不支持的存儲類型", + "error_storage_params_serialization": "存儲參數序列化失敗", + "error_storage_id_not_found": "無法獲取存儲 ID", + "error_operation_failed": "操作失敗", + "error_unsupported_framework": "不支持的存儲框架", + "error_storage_network_failure": "存儲操作失敗,請檢查網絡連接", + "error_network": "網絡連接失敗,請檢查網絡設置", + "error_api": "服務器請求失敗,請稍後重試", + "error_validation": "輸入數據有誤,請檢查後重試", + "error_auth": "登錄已過期,請重新登錄", + "error_forbidden": "沒有權限執行此操作", + "error_not_found": "請求的資源不存在", + "error_timeout": "請求超時,請稍後重試", + "error_file_system": "文件操作失敗", + "error_config": "配置錯誤", + "error_unknown": "發生未知錯誤,請稍後重試", + "error_auth_failed": "認證失敗", + "error_no_permission": "沒有權限", + "error_resource_not_found": "{{resource}} 不存在", + "error_operation_timeout": "{{operation}} 操作超時", + "no_logs": "暫無日誌" } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 9fc1d74..de89301 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -17,6 +17,7 @@ */ import { Component, ErrorInfo, ReactNode, JSX } from 'react' +import { useTranslation } from 'react-i18next' import { Button, Card, Result, Space, Typography } from '@arco-design/web-react' import { IconRefresh, IconBug } from '@arco-design/web-react/icon' import { logger } from '../services/LoggerService' @@ -54,6 +55,7 @@ interface DefaultErrorFallbackProps { } function DefaultErrorFallback({ error, onReset }: DefaultErrorFallbackProps): JSX.Element { + const { t } = useTranslation() return (
} - title="出现错误" - subTitle="应用遇到了问题,但别担心,数据是安全的" + title={t('error_boundary_title')} + subTitle={t('error_boundary_subtitle')} >
diff --git a/src/controller/storage/create.ts b/src/controller/storage/create.ts index b7668bb..5a7dc92 100644 --- a/src/controller/storage/create.ts +++ b/src/controller/storage/create.ts @@ -1,4 +1,5 @@ import { Message } from '@arco-design/web-react' +import { t } from 'i18next' import { ParametersType } from '../../type/defaults' import { openlist_api_post } from '../../utils/openlist/request' import { rclone_api_post } from '../../utils/rclone/request' @@ -12,17 +13,16 @@ import { logger } from '../../services/LoggerService' */ function validateStorageName(name: string): string | null { if (!name || typeof name !== 'string') { - return '存储名称不能为空' + return t('validation_storage_name_empty') } if (name.trim().length === 0) { - return '存储名称不能为空' + return t('validation_storage_name_empty') } if (name.length > 128) { - return '存储名称长度不能超过128字符' + return t('validation_storage_name_too_long') } - // 检查非法字符 if (/[<>:"|?*/\\]/.test(name)) { - return '存储名称包含非法字符' + return t('validation_storage_name_invalid_chars') } return null } @@ -32,7 +32,7 @@ function validateStorageName(name: string): string | null { */ function validateStorageType(type: string): string | null { if (!type || typeof type !== 'string') { - return '存储类型不能为空' + return t('validation_storage_type_empty') } return null } @@ -42,7 +42,7 @@ function validateStorageType(type: string): string | null { */ function validateParameters(parameters: ParametersType): string | null { if (!parameters || typeof parameters !== 'object') { - return '存储参数不能为空' + return t('validation_storage_params_empty') } return null } @@ -77,14 +77,14 @@ async function createStorage( // 输入验证 const validation = validateStorageInput(name, type, parameters) if (!validation.valid) { - Message.error(validation.error || '输入参数无效') + Message.error(validation.error || t('validation_input_invalid')) logger.error('Storage validation failed', undefined, 'StorageCreate', { error: validation.error }) return false } const storageInfo = searchStorageInfo(type) if (!storageInfo) { - Message.error('不支持的存储类型: ' + type) + Message.error(t('error_unsupported_storage_type') + ': ' + type) logger.error('Storage type not found', undefined, 'StorageCreate', { type }) return false } @@ -117,7 +117,7 @@ async function createStorage( serializedAddition = JSON.stringify(parameters.addition) } catch (e) { logger.error('Failed to serialize addition', e as Error, 'StorageCreate') - Message.error('存储参数序列化失败') + Message.error(t('error_storage_params_serialization')) return false } @@ -135,7 +135,7 @@ async function createStorage( // 更新现有存储 const storageId = storage.other?.openlist?.id if (!storageId) { - Message.error('无法获取存储 ID') + Message.error(t('error_storage_id_not_found')) return false } backData = await openlist_api_post('/api/admin/storage/update', { @@ -145,7 +145,7 @@ async function createStorage( } if (backData.code !== 200) { - Message.error(backData.message || '操作失败') + Message.error(backData.message || t('error_operation_failed')) return false } @@ -154,12 +154,12 @@ async function createStorage( } default: - Message.error('不支持的存储框架: ' + storageInfo.framework) + Message.error(t('error_unsupported_framework') + ': ' + storageInfo.framework) return false } } catch (error) { logger.error('Storage operation failed', error as Error, 'StorageCreate') - Message.error('存储操作失败,请检查网络连接') + Message.error(t('error_storage_network_failure')) return false } } diff --git a/src/page/setting/setting.tsx b/src/page/setting/setting.tsx index 8ba5204..f59a238 100644 --- a/src/page/setting/setting.tsx +++ b/src/page/setting/setting.tsx @@ -23,7 +23,7 @@ export default function Setting_page() { const showLogFromFileTail = async (path: string) => { try { const content = await readTextFileTail(path, { maxBytes: 256 * 1024, allowMissing: true }) - showLog(modal, (content || '').trim() ? content : '暂无日志') + showLog(modal, (content || '').trim() ? content : t('no_logs')) } catch (e) { const msg = e instanceof Error ? e.message : String(e) Message.error(msg) diff --git a/src/services/ErrorService.ts b/src/services/ErrorService.ts index 1c0fa0d..0b2cc2c 100644 --- a/src/services/ErrorService.ts +++ b/src/services/ErrorService.ts @@ -2,239 +2,18 @@ * Error Service - 统一的错误处理服务 * * 特性: - * 1. 统一的错误类型和分类 - * 2. 错误转换和包装 - * 3. 用户友好的错误消息(支持i18n) - * 4. 自动UI提示(Message/Notification) - * 5. 内存错误日志 - * 6. 与 LoggerService 集成 + * 1. 统一的错误处理 + * 2. 自动UI提示(Message/Notification) + * 3. 内存错误日志 + * 4. 与 LoggerService 集成 */ import { Message, Notification } from '@arco-design/web-react' import { t } from 'i18next' import { logger } from './LoggerService' +import { AppError, ErrorCategory, ErrorSeverity } from './AppError' -// ============================================ -// 错误类型枚举 -// ============================================ -export enum ErrorCategory { - NETWORK = 'NETWORK', // 网络错误 - API = 'API', // API 错误 - VALIDATION = 'VALIDATION', // 验证错误 - AUTHENTICATION = 'AUTH', // 认证错误 - AUTHORIZATION = 'FORBIDDEN', // 权限错误 - NOT_FOUND = 'NOT_FOUND', // 资源不存在 - TIMEOUT = 'TIMEOUT', // 超时错误 - FILE_SYSTEM = 'FILE_SYSTEM', // 文件系统错误 - CONFIGURATION = 'CONFIG', // 配置错误 - UNKNOWN = 'UNKNOWN', // 未知错误 -} - -// ============================================ -// 错误严重级别 -// ============================================ -export enum ErrorSeverity { - LOW = 'low', // 轻微问题,可忽略 - MEDIUM = 'medium', // 一般问题,需要关注 - HIGH = 'high', // 严重问题,需要处理 - CRITICAL = 'critical', // 关键问题,影响核心功能 -} - -// ============================================ -// 应用错误类 -// ============================================ -export class AppError extends Error { - readonly category: ErrorCategory - readonly severity: ErrorSeverity - readonly code: string - readonly context?: Record - readonly timestamp: Date - readonly originalError?: Error - - constructor( - message: string, - category: ErrorCategory = ErrorCategory.UNKNOWN, - severity: ErrorSeverity = ErrorSeverity.MEDIUM, - code: string = 'UNKNOWN_ERROR', - context?: Record, - originalError?: Error - ) { - super(message) - this.name = 'AppError' - this.category = category - this.severity = severity - this.code = code - this.context = context - this.timestamp = new Date() - this.originalError = originalError - - // 保持堆栈跟踪 - if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, AppError) - } - } - - /** - * 获取用户友好的错误消息(支持 i18n) - */ - getUserMessage(): string { - // 尝试使用国际化消息 - const i18nKey = `errors.${this.code}` - const i18nMessage = t(i18nKey, { defaultValue: '' }) - if (i18nMessage) { - return i18nMessage - } - - // 开发环境:记录缺失的 i18n key - if (import.meta.env.DEV && !i18nMessage) { - logger.warn(`Missing i18n key: ${i18nKey}`, 'ErrorService', { code: this.code, category: this.category }) - } - - // 回退到默认分类消息 - const messages: Record = { - [ErrorCategory.NETWORK]: '网络连接失败,请检查网络设置', - [ErrorCategory.API]: '服务器请求失败,请稍后重试', - [ErrorCategory.VALIDATION]: '输入数据有误,请检查后重试', - [ErrorCategory.AUTHENTICATION]: '登录已过期,请重新登录', - [ErrorCategory.AUTHORIZATION]: '没有权限执行此操作', - [ErrorCategory.NOT_FOUND]: '请求的资源不存在', - [ErrorCategory.TIMEOUT]: '请求超时,请稍后重试', - [ErrorCategory.FILE_SYSTEM]: '文件操作失败', - [ErrorCategory.CONFIGURATION]: '配置错误', - [ErrorCategory.UNKNOWN]: '发生未知错误,请稍后重试', - } - - return messages[this.category] || this.message - } - - /** - * 是否应该显示给用户 - */ - shouldShowToUser(): boolean { - return this.severity !== ErrorSeverity.LOW - } - - /** - * 是否应该上报 - */ - shouldReport(): boolean { - return this.severity === ErrorSeverity.HIGH || this.severity === ErrorSeverity.CRITICAL - } - - /** - * 转换为 JSON 对象(生产环境不包含 stack trace) - */ - toJSON(): Record { - return { - name: this.name, - message: this.message, - category: this.category, - severity: this.severity, - code: this.code, - // 仅在开发环境包含 context,生产环境可能包含敏感数据 - context: import.meta.env.DEV ? this.context : undefined, - timestamp: this.timestamp.toISOString(), - // 仅在开发环境包含 stack trace,生产环境不暴露内部实现细节 - stack: import.meta.env.DEV ? this.stack : undefined, - } - } - - // ========================================== - // 工厂方法 - // ========================================== - - static network(message: string, original?: Error, context?: Record): AppError { - return new AppError( - message, - ErrorCategory.NETWORK, - ErrorSeverity.HIGH, - 'NETWORK_ERROR', - context, - original - ) - } - - static api( - message: string, - code: string = 'API_ERROR', - context?: Record, - original?: Error - ): AppError { - return new AppError(message, ErrorCategory.API, ErrorSeverity.HIGH, code, context, original) - } - - static validation( - message: string, - field?: string, - context?: Record - ): AppError { - return new AppError( - message, - ErrorCategory.VALIDATION, - ErrorSeverity.MEDIUM, - 'VALIDATION_ERROR', - { ...context, field } - ) - } - - static auth(message: string = '认证失败'): AppError { - return new AppError(message, ErrorCategory.AUTHENTICATION, ErrorSeverity.HIGH, 'AUTH_ERROR') - } - - static forbidden(message: string = '没有权限'): AppError { - return new AppError( - message, - ErrorCategory.AUTHORIZATION, - ErrorSeverity.HIGH, - 'FORBIDDEN_ERROR' - ) - } - - static notFound(resource: string, context?: Record): AppError { - return new AppError( - `${resource} 不存在`, - ErrorCategory.NOT_FOUND, - ErrorSeverity.MEDIUM, - 'NOT_FOUND_ERROR', - context - ) - } - - static timeout(operation: string, timeoutMs: number): AppError { - return new AppError( - `${operation} 操作超时`, - ErrorCategory.TIMEOUT, - ErrorSeverity.HIGH, - 'TIMEOUT_ERROR', - { timeout: timeoutMs } - ) - } - - static fileSystem(message: string, original?: Error): AppError { - return new AppError( - message, - ErrorCategory.FILE_SYSTEM, - ErrorSeverity.HIGH, - 'FILE_SYSTEM_ERROR', - undefined, - original - ) - } - - static config(message: string): AppError { - return new AppError( - message, - ErrorCategory.CONFIGURATION, - ErrorSeverity.CRITICAL, - 'CONFIG_ERROR' - ) - } -} - -// ============================================ -// 错误处理器类型 -// ============================================ -type ErrorHandler = (error: AppError) => void | Promise +export { AppError, ErrorCategory, ErrorSeverity } // ============================================ // 错误处理配置 @@ -257,15 +36,12 @@ const DEFAULT_HANDLER_CONFIG: ErrorHandlerConfig = { // Error Service // ============================================ class ErrorService { - private handlers: ErrorHandler[] = [] + private handlers: Array<(error: AppError) => void | Promise> = [] private globalHandler?: (error: AppError) => void private errorLog: AppError[] = [] private maxLogSize = 100 - /** - * 注册错误处理器 - */ - onError(handler: ErrorHandler): () => void { + onError(handler: (error: AppError) => void | Promise): () => void { this.handlers.push(handler) return () => { const index = this.handlers.indexOf(handler) @@ -275,16 +51,10 @@ class ErrorService { } } - /** - * 设置全局错误处理器 - */ setGlobalHandler(handler: (error: AppError) => void): void { this.globalHandler = handler } - /** - * 处理错误 - */ async handle( error: unknown, context?: string, @@ -293,35 +63,25 @@ class ErrorService { const mergedConfig = { ...DEFAULT_HANDLER_CONFIG, ...config } const appError = this.normalize(error) - // 记录错误到日志服务 - logger.error( - appError.message, - appError.originalError, - context || 'ErrorService', - { - category: appError.category, - severity: appError.severity, - code: appError.code, - ...appError.context, - } - ) + logger.error(appError.message, appError.originalError, context || 'ErrorService', { + category: appError.category, + severity: appError.severity, + code: appError.code, + ...appError.context, + }) - // 记录到内存日志 if (mergedConfig.logToMemory) { this.logToMemory(appError) } - // 显示UI提示 if (mergedConfig.showMessage) { this.showMessage(appError) } - // 显示通知(仅严重错误) if (mergedConfig.showNotification && appError.severity === ErrorSeverity.CRITICAL) { this.showNotification(appError) } - // 调用注册的处理器 for (const handler of this.handlers) { try { await handler(appError) @@ -330,12 +90,10 @@ class ErrorService { } } - // 调用全局处理器 if (this.globalHandler) { this.globalHandler(appError) } - // 是否重新抛出 if (mergedConfig.rethrow) { throw appError } @@ -343,9 +101,6 @@ class ErrorService { return appError } - /** - * 记录到内存日志 - */ private logToMemory(error: AppError): void { this.errorLog.unshift(error) if (this.errorLog.length > this.maxLogSize) { @@ -353,9 +108,6 @@ class ErrorService { } } - /** - * 显示消息提示 - */ private showMessage(error: AppError): void { if (error.severity === ErrorSeverity.CRITICAL || error.severity === ErrorSeverity.HIGH) { Message.error(error.getUserMessage()) @@ -364,9 +116,6 @@ class ErrorService { } } - /** - * 显示通知 - */ private showNotification(error: AppError): void { Notification.error({ title: t('error'), @@ -375,54 +124,29 @@ class ErrorService { }) } - /** - * 获取错误日志 - */ getErrorLog(): AppError[] { return [...this.errorLog] } - /** - * 清除错误日志 - */ clearErrorLog(): void { this.errorLog = [] } - /** - * 标准化错误为 AppError - */ normalize(error: unknown): AppError { if (error instanceof AppError) { return error } if (error instanceof Error) { - return new AppError( - error.message, - ErrorCategory.UNKNOWN, - ErrorSeverity.MEDIUM, - 'UNKNOWN_ERROR', - { name: error.name }, - error - ) + return new AppError(error.message, ErrorCategory.UNKNOWN, ErrorSeverity.MEDIUM, 'UNKNOWN_ERROR', { + name: error.name, + }, error) } - return new AppError( - String(error), - ErrorCategory.UNKNOWN, - ErrorSeverity.MEDIUM, - 'UNKNOWN_ERROR' - ) + return new AppError(String(error), ErrorCategory.UNKNOWN, ErrorSeverity.MEDIUM, 'UNKNOWN_ERROR') } - /** - * 包装异步函数,自动处理错误 - */ - wrap( - fn: () => Promise, - errorHandler?: (error: AppError) => void - ): Promise { + wrap(fn: () => Promise, errorHandler?: (error: AppError) => void): Promise { return fn().catch((error: unknown) => { const appError = this.normalize(error) this.handle(appError) @@ -433,9 +157,6 @@ class ErrorService { }) } - /** - * 创建安全的异步函数包装器 - */ safe Promise>( fn: T, context?: string @@ -450,23 +171,12 @@ class ErrorService { } } - /** - * 验证条件,失败时抛出错误 - */ assert(condition: boolean, message: string, category?: ErrorCategory): void { if (!condition) { - throw new AppError( - message, - category || ErrorCategory.VALIDATION, - ErrorSeverity.MEDIUM, - 'ASSERTION_ERROR' - ) + throw new AppError(message, category || ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM, 'ASSERTION_ERROR') } } - /** - * 验证值是否存在,不存在时抛出错误 - */ assertExists(value: T | null | undefined, name: string): T { if (value === null || value === undefined) { throw AppError.notFound(name) @@ -486,10 +196,8 @@ export const errorService = new ErrorService() export const handleError = (error: unknown, context?: string): Promise => errorService.handle(error, context) -export const safe = Promise>( - fn: T, - context?: string -) => errorService.safe(fn, context) +export const safe = Promise>(fn: T, context?: string) => + errorService.safe(fn, context) export const assert = (condition: boolean, message: string, category?: ErrorCategory): void => errorService.assert(condition, message, category)