mirror of
https://github.com/VirtualHotBar/NetMount.git
synced 2026-05-06 21:31:59 +08:00
fix(i18n): replace hardcoded Chinese messages with i18n keys and extract AppError
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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": "暂无日志"
|
||||
}
|
||||
@@ -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": "暫無日誌"
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
@@ -70,8 +72,8 @@ function DefaultErrorFallback({ error, onReset }: DefaultErrorFallbackProps): JS
|
||||
<Result
|
||||
status="error"
|
||||
icon={<IconBug style={{ fontSize: '3rem', color: 'var(--color-danger)' }} />}
|
||||
title="出现错误"
|
||||
subTitle="应用遇到了问题,但别担心,数据是安全的"
|
||||
title={t('error_boundary_title')}
|
||||
subTitle={t('error_boundary_subtitle')}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div
|
||||
@@ -94,7 +96,7 @@ function DefaultErrorFallback({ error, onReset }: DefaultErrorFallbackProps): JS
|
||||
</div>
|
||||
<Space style={{ justifyContent: 'center', width: '100%', marginTop: '1rem' }}>
|
||||
<Button type="primary" icon={<IconRefresh />} onClick={onReset}>
|
||||
重新加载
|
||||
{t('reload')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string, unknown>
|
||||
readonly timestamp: Date
|
||||
readonly originalError?: Error
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
category: ErrorCategory = ErrorCategory.UNKNOWN,
|
||||
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
||||
code: string = 'UNKNOWN_ERROR',
|
||||
context?: Record<string, unknown>,
|
||||
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, string> = {
|
||||
[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<string, unknown> {
|
||||
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<string, unknown>): AppError {
|
||||
return new AppError(
|
||||
message,
|
||||
ErrorCategory.NETWORK,
|
||||
ErrorSeverity.HIGH,
|
||||
'NETWORK_ERROR',
|
||||
context,
|
||||
original
|
||||
)
|
||||
}
|
||||
|
||||
static api(
|
||||
message: string,
|
||||
code: string = 'API_ERROR',
|
||||
context?: Record<string, unknown>,
|
||||
original?: Error
|
||||
): AppError {
|
||||
return new AppError(message, ErrorCategory.API, ErrorSeverity.HIGH, code, context, original)
|
||||
}
|
||||
|
||||
static validation(
|
||||
message: string,
|
||||
field?: string,
|
||||
context?: Record<string, unknown>
|
||||
): 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<string, unknown>): 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<void>
|
||||
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<void>> = []
|
||||
private globalHandler?: (error: AppError) => void
|
||||
private errorLog: AppError[] = []
|
||||
private maxLogSize = 100
|
||||
|
||||
/**
|
||||
* 注册错误处理器
|
||||
*/
|
||||
onError(handler: ErrorHandler): () => void {
|
||||
onError(handler: (error: AppError) => void | Promise<void>): () => 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<T>(
|
||||
fn: () => Promise<T>,
|
||||
errorHandler?: (error: AppError) => void
|
||||
): Promise<T | undefined> {
|
||||
wrap<T>(fn: () => Promise<T>, errorHandler?: (error: AppError) => void): Promise<T | undefined> {
|
||||
return fn().catch((error: unknown) => {
|
||||
const appError = this.normalize(error)
|
||||
this.handle(appError)
|
||||
@@ -433,9 +157,6 @@ class ErrorService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建安全的异步函数包装器
|
||||
*/
|
||||
safe<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
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<T>(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<AppError> =>
|
||||
errorService.handle(error, context)
|
||||
|
||||
export const safe = <T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
fn: T,
|
||||
context?: string
|
||||
) => errorService.safe(fn, context)
|
||||
export const safe = <T extends (...args: unknown[]) => Promise<unknown>>(fn: T, context?: string) =>
|
||||
errorService.safe(fn, context)
|
||||
|
||||
export const assert = (condition: boolean, message: string, category?: ErrorCategory): void =>
|
||||
errorService.assert(condition, message, category)
|
||||
|
||||
Reference in New Issue
Block a user