refactor(前端): 接口错误改成 ApiError 类,catch 不再写 any

拦截器原来 reject 一个裸对象 `{ error, data }`,没有类型也没有类型守卫,于是 46 处
`catch (err: any)` 直接读 `err.data` / `err.error` —— 代码 bug 抛出的 TypeError 也按
接口错误读,弹出来的是 undefined 或英文。

- utils/api.ts:reject 改成 `ApiError extends Error`,字段名沿用 error(错误码)/
  data(文案),存量读法语义不变;加 isApiError / errorCode / errorMessage。
  errorMessage 对非接口错误一律用 fallback,不把 TypeError 的英文甩给学生
- 46 处 catch 全部换掉 any(45 处接口错误 + pythonSyntaxCheck 里 Skulpt 的错误对象)
- 断网等没有响应体的情况,文案从英文 "Request failed" 换成「网络异常,请检查网络后重试」

浏览器实跑核过:登录密码错误仍显示「用户名或密码不正确」(错误码分支)、
接口错误 reject 出来是 ApiError、网络不通时拿到中文文案、非接口错误走 fallback。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 15:12:40 +08:00
1 parent 6406794971
commit 7a390323fe
29 files changed
+167 -106

No files matched your search

+37 -4
View File
@@ -6,13 +6,45 @@ import storage from "./storage"
const { message: toast } = createDiscreteApi(["message"])
interface ApiError {
/** 后端失败响应体的形状,见 apps/api/src/http.ts 的 failure */
interface ErrorPayload {
error?: {
code?: string
message?: string
}
}
/**
* 拦截器 reject 出来的就是它:`error` 是错误码,`data` 是后端文案。
*
* 字段名沿用原来那个裸对象 `{ error, data }`,存量调用点读 `err.error` / `err.data`
* 的语义不变;做成 Error 的子类是为了在 catch 里能和代码 bug(TypeError 之类)分开,
* 也顺带有了堆栈。catch 块里别写 `err: any`,用下面的 errorCode / errorMessage。
*/
export class ApiError extends Error {
constructor(
readonly error: string,
readonly data: string,
) {
super(data)
this.name = "ApiError"
}
}
export function isApiError(err: unknown): err is ApiError {
return err instanceof ApiError
}
/** 接口错误的错误码;不是接口错误(代码 bug、被中止……)就是 undefined */
export function errorCode(err: unknown) {
return isApiError(err) ? err.error : undefined
}
/** 给用户看的文案:接口错误用后端文案,其它一律 fallback —— 别把 TypeError 的英文甩给学生 */
export function errorMessage(err: unknown, fallback = "未知错误") {
return isApiError(err) && err.data ? err.data : fallback
}
/**
* 成功时直接拿到业务数据本身。后端成功响应是 `{ data }`(见 apps/api/src/http.ts
* 的 success),拦截器把 axios 外层和这层信封一起剥掉。
@@ -74,12 +106,13 @@ instance.interceptors.response.use(
response: AxiosResponse,
) => AxiosResponse,
(error) => {
const payload = error.response?.data as ApiError | undefined
const payload = error.response?.data as ErrorPayload | undefined
const code = payload?.error?.code ?? "network-error"
const message = payload?.error?.message ?? "Request failed"
// 没有响应体 = 请求根本没到后端(断网、反代挂了、超时),学生看到的就是这一句
const message = payload?.error?.message ?? "网络异常,请检查网络后重试"
handleGlobalApiError(code, message)
return Promise.reject({ error: code, data: message })
return Promise.reject(new ApiError(code, message))
},
)