feat(frontend): 新增 API 错误类型定义,优化错误处理

- 新增 api-error.ts 定义标准错误类型和工具函数
- 重构 error.ts 和 errorParser.ts 使用新类型
- 更新 api client 的类型定义
This commit is contained in:
fawney19
2025-12-12 16:14:33 +08:00
parent 348b454e1e
commit 738a8459c9
5 changed files with 107 additions and 24 deletions

View File

@@ -59,7 +59,7 @@ export const adminApi = {
is_active?: boolean is_active?: boolean
}): Promise<AdminApiKeysResponse> { }): Promise<AdminApiKeysResponse> {
const response = await apiClient.get<AdminApiKeysResponse>('/api/admin/api-keys', { const response = await apiClient.get<AdminApiKeysResponse>('/api/admin/api-keys', {
params: params params
}) })
return response.data return response.data
}, },

View File

@@ -204,7 +204,7 @@ class ApiClient {
await this.refreshPromise await this.refreshPromise
originalRequest.headers.Authorization = `Bearer ${this.getToken()}` originalRequest.headers.Authorization = `Bearer ${this.getToken()}`
return this.client.request(originalRequest) return this.client.request(originalRequest)
} catch (refreshError) { } catch {
return Promise.reject(error) return Promise.reject(error)
} }
} }

View File

@@ -0,0 +1,83 @@
/**
* API 错误类型定义
* 用于替代各处的 any 类型,提供类型安全的错误处理
*/
import type { AxiosError } from 'axios'
/**
* 后端标准错误响应格式
*/
export interface ApiErrorResponse {
error?: {
type?: string
message?: string
}
detail?: string
message?: string
}
/**
* API 错误类型
* 封装 Axios 错误,提供类型安全的错误处理
*/
export interface ApiError extends AxiosError<ApiErrorResponse> {
response?: AxiosError<ApiErrorResponse>['response'] & {
data?: ApiErrorResponse
}
}
/**
* 类型守卫:检查是否为 API 错误
*/
export function isApiError(error: unknown): error is ApiError {
return (
typeof error === 'object' &&
error !== null &&
'response' in error
)
}
/**
* 从错误对象中安全提取错误消息
*/
export function getErrorMessage(error: unknown, defaultMessage = '操作失败'): string {
if (!error) return defaultMessage
if (isApiError(error)) {
// 优先从标准错误格式提取
if (error.response?.data?.error?.message) {
return error.response.data.error.message
}
// FastAPI 标准 detail 字段
if (error.response?.data?.detail) {
return error.response.data.detail
}
// 通用 message 字段
if (error.response?.data?.message) {
return error.response.data.message
}
}
// Error 实例
if (error instanceof Error) {
return error.message
}
// 字符串错误
if (typeof error === 'string') {
return error
}
return defaultMessage
}
/**
* 从错误对象中安全提取 HTTP 状态码
*/
export function getErrorStatus(error: unknown): number | undefined {
if (isApiError(error)) {
return error.response?.status
}
return undefined
}

View File

@@ -2,19 +2,10 @@
* 从后端响应中提取错误消息 * 从后端响应中提取错误消息
* 后端统一返回格式: {"error": {"type": "...", "message": "..."}} * 后端统一返回格式: {"error": {"type": "...", "message": "..."}}
*/ */
export function extractErrorMessage(error: any, defaultMessage = '操作失败'): string { import { type ApiError, isApiError, getErrorMessage as _getErrorMessage } from '@/types/api-error'
// 优先从响应中提取错误消息
if (error.response?.data?.error?.message) {
return error.response.data.error.message
}
// 如果是网络错误或其他异常 export function extractErrorMessage(error: unknown, defaultMessage = '操作失败'): string {
if (error.message) { return _getErrorMessage(error, defaultMessage)
return error.message
}
// 返回默认消息
return defaultMessage
} }
/** /**
@@ -38,8 +29,8 @@ export type ErrorType = typeof ErrorType[keyof typeof ErrorType]
/** /**
* 从后端响应中提取错误类型 * 从后端响应中提取错误类型
*/ */
export function extractErrorType(error: any): ErrorType | null { export function extractErrorType(error: unknown): ErrorType | null {
if (error.response?.data?.error?.type) { if (isApiError(error) && error.response?.data?.error?.type) {
return error.response.data.error.type as ErrorType return error.response.data.error.type as ErrorType
} }
return null return null
@@ -48,6 +39,10 @@ export function extractErrorType(error: any): ErrorType | null {
/** /**
* 检查是否为特定类型的错误 * 检查是否为特定类型的错误
*/ */
export function isErrorType(error: any, type: ErrorType): boolean { export function isErrorType(error: unknown, type: ErrorType): boolean {
return extractErrorType(error) === type return extractErrorType(error) === type
} }
// 重新导出类型
export type { ApiError }
export { isApiError }

View File

@@ -2,6 +2,8 @@
* 解析 API 错误响应,提取友好的错误信息 * 解析 API 错误响应,提取友好的错误信息
*/ */
import { isApiError } from '@/types/api-error'
/** /**
* Pydantic 验证错误项 * Pydantic 验证错误项
*/ */
@@ -9,7 +11,7 @@ interface ValidationError {
loc: (string | number)[] loc: (string | number)[]
msg: string msg: string
type: string type: string
ctx?: Record<string, any> ctx?: Record<string, unknown>
} }
/** /**
@@ -138,11 +140,14 @@ function formatValidationError(error: ValidationError): string {
* @param defaultMessage 默认错误信息 * @param defaultMessage 默认错误信息
* @returns 格式化的错误信息 * @returns 格式化的错误信息
*/ */
export function parseApiError(err: any, defaultMessage: string = '操作失败'): string { export function parseApiError(err: unknown, defaultMessage: string = '操作失败'): string {
if (!err) return defaultMessage if (!err) return defaultMessage
// 处理网络错误 // 处理网络错误
if (!err.response) { if (!isApiError(err) || !err.response) {
if (err instanceof Error) {
return err.message || defaultMessage
}
return '无法连接到服务器,请检查网络连接' return '无法连接到服务器,请检查网络连接'
} }
@@ -169,8 +174,8 @@ export function parseApiError(err: any, defaultMessage: string = '操作失败')
// 3. 处理对象错误 // 3. 处理对象错误
if (typeof detail === 'object') { if (typeof detail === 'object') {
// 可能是自定义错误对象 // 可能是自定义错误对象
if (detail.message) { if ((detail as Record<string, unknown>).message) {
return detail.message return String((detail as Record<string, unknown>).message)
} }
// 尝试 JSON 序列化 // 尝试 JSON 序列化
try { try {
@@ -186,7 +191,7 @@ export function parseApiError(err: any, defaultMessage: string = '操作失败')
/** /**
* 解析并提取第一个错误信息(用于简短提示) * 解析并提取第一个错误信息(用于简短提示)
*/ */
export function parseApiErrorShort(err: any, defaultMessage: string = '操作失败'): string { export function parseApiErrorShort(err: unknown, defaultMessage: string = '操作失败'): string {
const fullError = parseApiError(err, defaultMessage) const fullError = parseApiError(err, defaultMessage)
// 如果有多行错误,只取第一行 // 如果有多行错误,只取第一行