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:
1 parent
6406794971
commit
7a390323fe
29 files changed
+167
-106
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { errorMessage } from "utils/api"
|
||||
import TextEditor from "shared/components/TextEditor.vue"
|
||||
import type { AnnouncementEdit } from "utils/types"
|
||||
import { createAnnouncement, editAnnouncement, getAnnouncement } from "../api"
|
||||
@@ -63,8 +64,8 @@ async function submit() {
|
||||
message.success("修改已保存")
|
||||
}
|
||||
router.push({ name: "admin announcement list" })
|
||||
} catch (err: any) {
|
||||
message.error(err.data)
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import { formatISO } from "date-fns"
|
||||
import TextEditor from "shared/components/TextEditor.vue"
|
||||
import { fromPickerValue, parseTime, toPickerValue } from "utils/functions"
|
||||
@@ -102,8 +103,8 @@ async function submit() {
|
||||
message.success("修改已保存")
|
||||
}
|
||||
router.push({ name: "admin contest list" })
|
||||
} catch (err: any) {
|
||||
message.error(err.data)
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import type { AcmHelperItem, SubmissionInfo } from "utils/types"
|
||||
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
|
||||
import { parseTime } from "utils/functions"
|
||||
@@ -70,8 +71,8 @@ async function toggleChecked(item: HelperItem) {
|
||||
submissions.value = [...submissions.value]
|
||||
|
||||
message.success(newChecked ? "已标记为已检查" : "已取消标记")
|
||||
} catch (err: any) {
|
||||
message.error(err.data || "操作失败")
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err, "操作失败"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +102,9 @@ async function markAllAsChecked() {
|
||||
|
||||
loadingMsg.destroy()
|
||||
message.success(`已标记 ${unchecked.length} 个提交为已检查`)
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
loadingMsg.destroy()
|
||||
message.error(err.data || "批量操作失败")
|
||||
message.error(errorMessage(err, "批量操作失败"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +160,8 @@ async function viewSubmission(item: HelperItem) {
|
||||
}
|
||||
|
||||
toggleCodePanel(true)
|
||||
} catch (err: any) {
|
||||
message.error(err.data || "加载提交失败")
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err, "加载提交失败"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +175,8 @@ async function loadData() {
|
||||
// 再获取 AC 提交列表
|
||||
const data = await getACMHelperList(Number(props.contestID))
|
||||
submissions.value = data
|
||||
} catch (err: any) {
|
||||
message.error(err.data || "加载失败")
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err, "加载失败"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { errorCode, errorMessage } from "utils/api"
|
||||
import {
|
||||
deleteContestProblem,
|
||||
deleteProblem,
|
||||
@@ -33,8 +34,8 @@ async function handleDeleteProblem() {
|
||||
}
|
||||
message.success("删除成功")
|
||||
emit("updated")
|
||||
} catch (err: any) {
|
||||
if (err.error === "problem-has-submissions") {
|
||||
} catch (err) {
|
||||
if (errorCode(err) === "problem-has-submissions") {
|
||||
message.error("这道题有提交之后,就不能被删除")
|
||||
} else {
|
||||
message.error("删除失败")
|
||||
@@ -81,13 +82,13 @@ async function handleMakePublic() {
|
||||
message.success("已成功转为公开题目(需要手动设置可见)")
|
||||
showMakePublicModal.value = false
|
||||
emit("updated") // 刷新列表
|
||||
} catch (err: any) {
|
||||
if (err.error === "display-id-exists") {
|
||||
} catch (err) {
|
||||
if (errorCode(err) === "display-id-exists") {
|
||||
message.error("该题目编号已存在,请使用其他编号")
|
||||
} else if (err.error === "already-public") {
|
||||
} else if (errorCode(err) === "already-public") {
|
||||
message.error("该题目已经是公开题目")
|
||||
} else {
|
||||
message.error("转换失败:" + (err.data || "未知错误"))
|
||||
message.error("转换失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode, errorMessage } from "utils/api"
|
||||
import { addProblemForContest } from "admin/api"
|
||||
|
||||
interface Props {
|
||||
@@ -22,13 +23,13 @@ async function addProblem() {
|
||||
displayID.value,
|
||||
)
|
||||
emit("added")
|
||||
} catch (err: any) {
|
||||
if (err.error === "display-id-exists") {
|
||||
} catch (err) {
|
||||
if (errorCode(err) === "display-id-exists") {
|
||||
message.error("显示编号重复了,请重新写一个")
|
||||
} else if (err.error === "contest-ended") {
|
||||
} else if (errorCode(err) === "contest-ended") {
|
||||
message.error("这场比赛已经结束了,不能添加题目")
|
||||
} else {
|
||||
message.error(err.data)
|
||||
message.error(errorMessage(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode, isApiError } from "utils/api"
|
||||
import type { LANGUAGE, SqlDisplay, Testcase } from "utils/types"
|
||||
import { createZipBlob } from "utils/functions"
|
||||
import SQLDataTable from "oj/problem/components/SQLDataTable.vue"
|
||||
@@ -81,15 +82,15 @@ onMounted(async () => {
|
||||
if (res.length) {
|
||||
scripts.value = res.map((f) => ({ ...blankEntry(), sql: f.content }))
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
// 新题、以及旧格式(非 SQL)测试点,后端回 404/409,保持空白就是对的,不该报错。
|
||||
// 但**其它**失败必须说出来:读不出来时编辑器长得和「这题本来就没测试点」一模一样,
|
||||
// 教师看不出区别,会以为要自己重新填。保存本身是安全的(后端读不到测试点信息会拒绝
|
||||
// 整个保存),所以这里只提示、不阻塞。
|
||||
const code = err?.error
|
||||
const code = errorCode(err)
|
||||
if (code && code !== "problem-not-found" && code !== "not-sql-test-case") {
|
||||
// 后端那句话(如"测试点信息读取失败")只说了现象,教师需要的是「下面是空的、别存」
|
||||
const detail = err?.data ? `(${err.data})` : ""
|
||||
const detail = isApiError(err) && err.data ? `(${err.data})` : ""
|
||||
loadError.value =
|
||||
`已有测试点脚本没能读出来${detail}。下方是空白模板,不是本题真实的测试点 —— ` +
|
||||
`直接保存不会丢数据(后端会拒绝),但也不会生效。请先排查测试点文件,或重新上传。`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode, errorMessage } from "utils/api"
|
||||
import { PROBLEM_TAG_MAX_LENGTH } from "@oj2/contract"
|
||||
import { getProblemTagList } from "shared/api"
|
||||
import TextEditor from "shared/components/TextEditor.vue"
|
||||
@@ -484,11 +485,11 @@ async function submit() {
|
||||
params: { contestID: props.contestID },
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.error === "display-id-exists") {
|
||||
} catch (err) {
|
||||
if (errorCode(err) === "display-id-exists") {
|
||||
message.error("显示编号重复了,请换一个显示编号")
|
||||
} else {
|
||||
message.error(err.data)
|
||||
message.error(errorMessage(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,9 +516,9 @@ async function generateMermaid() {
|
||||
)
|
||||
problem.value.mermaidCode = res.flowchart
|
||||
message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法")
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
// 没有 finally 的话,AI 接口回 502 时按钮永远卡在 loading,只能刷页面
|
||||
message.error(err.data || "生成失败,请稍后再试")
|
||||
message.error(errorMessage(err, "生成失败,请稍后再试"))
|
||||
} finally {
|
||||
isAIGenerating.value = false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { errorMessage } from "utils/api"
|
||||
import { deleteProblemSet, updateProblemSetStatus } from "admin/api"
|
||||
|
||||
interface Props {
|
||||
@@ -24,8 +25,8 @@ async function handleDeleteProblemSet() {
|
||||
await deleteProblemSet(props.problemSetId)
|
||||
message.success("删除成功")
|
||||
emit("updated")
|
||||
} catch (err: any) {
|
||||
message.error("删除失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("删除失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +54,8 @@ async function handleUpdateStatus() {
|
||||
message.success("状态更新成功")
|
||||
showStatusModal.value = false
|
||||
emit("updated")
|
||||
} catch (err: any) {
|
||||
message.error("状态更新失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("状态更新失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import { NTabPane, NTabs, NButton, NFlex } from "naive-ui"
|
||||
import type {
|
||||
AdminProblemSet,
|
||||
@@ -61,8 +62,8 @@ async function loadProblemSetDetail() {
|
||||
try {
|
||||
const res = await getProblemSetDetail(problemSetId.value)
|
||||
problemSet.value = res
|
||||
} catch (err: any) {
|
||||
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("加载题单详情失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +71,8 @@ async function loadProblems() {
|
||||
try {
|
||||
const res = await getProblemSetProblems(problemSetId.value)
|
||||
problems.value = res
|
||||
} catch (err: any) {
|
||||
message.error("加载题目列表失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("加载题目列表失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +80,8 @@ async function loadBadges() {
|
||||
try {
|
||||
const res = await getProblemSetBadges(problemSetId.value)
|
||||
badges.value = res
|
||||
} catch (err: any) {
|
||||
message.error("加载奖章列表失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("加载奖章列表失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +89,8 @@ async function loadProgress() {
|
||||
try {
|
||||
const res = await getProblemSetProgress(problemSetId.value)
|
||||
progress.value = res
|
||||
} catch (err: any) {
|
||||
message.error("加载进度列表失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("加载进度列表失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +101,8 @@ async function handleAddProblem(data: AddProblemToSetRequest) {
|
||||
showAddProblemModal.value = false
|
||||
loadProblems()
|
||||
loadProblemSetDetail() // 刷新题目数量
|
||||
} catch (err: any) {
|
||||
message.error("添加题目失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("添加题目失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +112,8 @@ async function handleRemoveProblem(problemSetProblemId: number) {
|
||||
message.success("题目移除成功")
|
||||
loadProblems()
|
||||
loadProblemSetDetail() // 刷新题目数量
|
||||
} catch (err: any) {
|
||||
message.error("移除题目失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("移除题目失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +125,8 @@ async function handleEditProblem(data: UpdateProblemInSetRequest) {
|
||||
message.success("题目编辑成功")
|
||||
showEditProblemModal.value = false
|
||||
loadProblems()
|
||||
} catch (err: any) {
|
||||
message.error("编辑题目失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("编辑题目失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,8 +136,8 @@ async function handleAddBadge(data: BadgeFormData) {
|
||||
message.success("奖章创建成功")
|
||||
showAddBadgeModal.value = false
|
||||
loadBadges()
|
||||
} catch (err: any) {
|
||||
message.error("创建奖章失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("创建奖章失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +146,8 @@ async function handleDeleteBadge(badgeId: number) {
|
||||
await deleteProblemSetBadge(problemSetId.value, badgeId)
|
||||
message.success("奖章删除成功")
|
||||
loadBadges()
|
||||
} catch (err: any) {
|
||||
message.error("删除奖章失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("删除奖章失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +159,8 @@ async function handleEditBadge(data: BadgeFormData) {
|
||||
message.success("奖章编辑成功")
|
||||
showEditBadgeModal.value = false
|
||||
loadBadges()
|
||||
} catch (err: any) {
|
||||
message.error("编辑奖章失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("编辑奖章失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +169,8 @@ async function handleRemoveUser(userId: number) {
|
||||
await removeUserFromProblemSet(problemSetId.value, userId)
|
||||
message.success("用户移除成功")
|
||||
loadProgress()
|
||||
} catch (err: any) {
|
||||
message.error("移除用户失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("移除用户失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import type { CreateProblemSetData, EditProblemSetData } from "utils/types"
|
||||
import { fromPickerValue, toPickerValue } from "utils/functions"
|
||||
import { getProblemSetDetail, createProblemSet, editProblemSet } from "../api"
|
||||
@@ -60,8 +61,8 @@ async function loadProblemSetDetail() {
|
||||
visible: data.visible,
|
||||
endTime: data.endTime ? new Date(data.endTime) : null,
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error("加载题单详情失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("加载题单详情失败:" + errorMessage(err, "未知错误"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +88,11 @@ async function handleSubmit() {
|
||||
message.success("题单创建成功")
|
||||
}
|
||||
router.push({ name: "admin problemset list" })
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
message.error(
|
||||
(isEdit.value ? "更新" : "创建") +
|
||||
"题单失败:" +
|
||||
(err.data || "未知错误"),
|
||||
errorMessage(err, "未知错误"),
|
||||
)
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import { NButton, NTag } from "naive-ui"
|
||||
import {
|
||||
CLASS_NAME_MAX_DIGITS,
|
||||
@@ -210,8 +211,8 @@ async function saveWebsiteConfig() {
|
||||
}
|
||||
try {
|
||||
await editWebsite(websiteConfig)
|
||||
} catch (err: any) {
|
||||
message.error("保存失败:" + err.data)
|
||||
} catch (err) {
|
||||
message.error("保存失败:" + errorMessage(err))
|
||||
return
|
||||
}
|
||||
message.success("网站配置保存成功")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { errorMessage } from "utils/api"
|
||||
import { deleteTutorial } from "admin/api"
|
||||
|
||||
interface Props {
|
||||
@@ -22,8 +23,8 @@ async function handleDelete() {
|
||||
await deleteTutorial(props.tutorialID)
|
||||
message.success("删除成功")
|
||||
emit("deleted")
|
||||
} catch (err: any) {
|
||||
message.error(err.data)
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import type {
|
||||
Exercise,
|
||||
ExerciseType,
|
||||
@@ -317,8 +318,8 @@ async function save() {
|
||||
}
|
||||
showForm.value = false
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
message.error(e.data ?? "保存失败")
|
||||
} catch (e) {
|
||||
message.error(errorMessage(e, "保存失败"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { errorMessage } from "utils/api"
|
||||
import CodeEditor from "shared/components/CodeEditor.vue"
|
||||
import MarkdownEditor from "shared/components/MarkdownEditor.vue"
|
||||
import type { TutorialEdit } from "utils/types"
|
||||
@@ -63,8 +64,8 @@ async function submit() {
|
||||
await updateTutorial(tutorial)
|
||||
message.success("修改已保存")
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error(err.data)
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import { NSwitch } from "naive-ui"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { TutorialListItem } from "utils/types"
|
||||
@@ -63,8 +64,8 @@ async function toggleVisible(tutorial: TutorialListItem) {
|
||||
try {
|
||||
await setTutorialVisibility(tutorial.id, tutorial.isPublic)
|
||||
message.success("更新成功")
|
||||
} catch (err: any) {
|
||||
message.error(err.data)
|
||||
} catch (err) {
|
||||
message.error(errorMessage(err))
|
||||
tutorial.isPublic = !tutorial.isPublic
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import {
|
||||
CLASS_NAME_MAX_DIGITS,
|
||||
CLASS_NAME_MAX_VALUE,
|
||||
@@ -66,8 +67,8 @@ async function uploadUsers() {
|
||||
hiddenElement.download = prefix.value + ".csv"
|
||||
hiddenElement.click()
|
||||
hiddenElement.remove()
|
||||
} catch (err: any) {
|
||||
message.error("上传失败:" + err.data)
|
||||
} catch (err) {
|
||||
message.error("上传失败:" + errorMessage(err))
|
||||
} finally {
|
||||
toggleLoading(false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import { DataTableRowKey, NFlex, NTag, SelectOption } from "naive-ui"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { usePagination } from "shared/composables/pagination"
|
||||
@@ -250,8 +251,8 @@ async function handleEditUser() {
|
||||
})
|
||||
await editUser(user)
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error("保存失败:" + err.data)
|
||||
} catch (err) {
|
||||
message.error("保存失败:" + errorMessage(err))
|
||||
return
|
||||
}
|
||||
userEditing.value = null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode, errorMessage } from "utils/api"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { formatCode, getReaction, submitCode } from "oj/api"
|
||||
@@ -133,10 +134,10 @@ async function submit() {
|
||||
language: formatLang,
|
||||
})
|
||||
codeStore.setCode(res.code)
|
||||
} catch (e: any) {
|
||||
if (e?.error === "format-error") {
|
||||
} catch (e) {
|
||||
if (errorCode(e) === "format-error") {
|
||||
// 仅 Python 会出现:代码本身存在语法错误
|
||||
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
|
||||
message.warning(`代码格式化失败:${errorMessage(e)},请检查代码后重试`)
|
||||
return
|
||||
}
|
||||
// server-error / 网络异常:格式化工具问题,静默降级,提交原代码
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { errorCode } from "utils/api"
|
||||
import { toRefs } from "vue"
|
||||
|
||||
// 工具函数
|
||||
@@ -256,13 +257,13 @@ async function submitFlowchartData() {
|
||||
}
|
||||
|
||||
message.success("流程图已提交,请耐心等待评分")
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
loading.value = false
|
||||
// 按错误码分支(见 utils/api.ts 的约定)。限流是最容易撞上的一种:
|
||||
// 只说「提交失败」的话,学生会以为是自己的图有问题,然后反复点,越点越久
|
||||
if (error?.error === "too-many-submissions") {
|
||||
if (errorCode(error) === "too-many-submissions") {
|
||||
message.warning("提交太频繁了,缓一会儿再交")
|
||||
} else if (error?.error === "flowchart-not-allowed") {
|
||||
} else if (errorCode(error) === "flowchart-not-allowed") {
|
||||
message.error("这道题不接受流程图提交")
|
||||
} else {
|
||||
message.error("流程图提交失败")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode } from "utils/api"
|
||||
import { getProblem } from "oj/api"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { storeToRefs } from "pinia"
|
||||
@@ -120,9 +121,9 @@ async function init() {
|
||||
try {
|
||||
const res = await getProblem(problemID, contestID)
|
||||
problem.value = res
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
problem.value = null
|
||||
if (err.error === "contest-not-started") {
|
||||
if (errorCode(err) === "contest-not-started") {
|
||||
errMsg.value = "比赛还没有开始"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,10 @@ export async function checkPythonSyntax(
|
||||
try {
|
||||
Sk.compile(code, "prog.py", "exec")
|
||||
return null
|
||||
} catch (e: any) {
|
||||
const line: number = e?.traceback?.[0]?.lineno ?? 1
|
||||
} catch (e) {
|
||||
// Skulpt 的语法错误对象,不是 Error 的子类
|
||||
const traceback = (e as { traceback?: { lineno?: number }[] }).traceback
|
||||
const line = traceback?.[0]?.lineno ?? 1
|
||||
return { line }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import { h, computed, ref, onMounted, watch } from "vue"
|
||||
import { watchDebounced } from "@vueuse/core"
|
||||
import { parseTime } from "utils/functions"
|
||||
@@ -68,10 +69,10 @@ async function loadUserProgress() {
|
||||
if (res.problems) {
|
||||
allProblems.value = res.problems
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
// finally 里收掉 loading:以前 loading.value = false 写在 await 之后,
|
||||
// 请求一失败(403、断网)转圈就永远停不下来
|
||||
message.error("加载用户进度失败:" + (err.data || "未知错误"))
|
||||
message.error("加载用户进度失败:" + errorMessage(err, "未知错误"))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessage } from "utils/api"
|
||||
import {
|
||||
getProblemSetDetail,
|
||||
getProblemSetProblems,
|
||||
@@ -90,8 +91,8 @@ async function handleJoinProblemSet() {
|
||||
message.success("成功加入题单!")
|
||||
// 加入题单后加载用户徽章
|
||||
await loadUserBadges()
|
||||
} catch (err: any) {
|
||||
message.error("加入题单失败:" + (err.data || "未知错误"))
|
||||
} catch (err) {
|
||||
message.error("加入题单失败:" + errorMessage(err, "未知错误"))
|
||||
} finally {
|
||||
isJoining.value = false
|
||||
}
|
||||
|
||||
@@ -505,7 +505,7 @@ async function listMyClassRank() {
|
||||
if (myClassScope.value === "window") {
|
||||
myClassQuery.page = 1
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
@@ -517,7 +517,7 @@ async function listWeeklyRank() {
|
||||
weeklyMe.value = res.me
|
||||
weeklyTotal.value = res.total
|
||||
weeklyStart.value = res.start
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { errorCode } from "utils/api"
|
||||
import { formatISO, getTime, parseISO } from "date-fns"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { ContestStatus, ContestType } from "utils/constants"
|
||||
@@ -96,9 +97,9 @@ export const useContestStore = defineStore("contest", () => {
|
||||
_getProblems(contestID)
|
||||
}
|
||||
return null
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
toggleAccess(false)
|
||||
return (err?.error as string | undefined) ?? "unknown"
|
||||
return errorCode(err) ?? "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode } from "utils/api"
|
||||
import { NButton, NFlex, NTag, NText } from "naive-ui"
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import {
|
||||
@@ -225,8 +226,8 @@ async function retryFlowchart(submissionId: string) {
|
||||
}
|
||||
try {
|
||||
await retryFlowchartSubmission(submissionId)
|
||||
} catch (err: any) {
|
||||
message.error(retryTips[err?.error] ?? "重新评分失败")
|
||||
} catch (err) {
|
||||
message.error(retryTips[errorCode(err) ?? ""] ?? "重新评分失败")
|
||||
return
|
||||
}
|
||||
message.success("重新评分已提交")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode } from "utils/api"
|
||||
import { STORAGE_KEY } from "utils/constants"
|
||||
import storage from "utils/storage"
|
||||
import { getClassUsernames, login } from "../api"
|
||||
@@ -124,13 +125,13 @@ function submit() {
|
||||
merged.username = form.value.class + merged.username
|
||||
}
|
||||
await login(merged)
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
// 判错误码而不是错误文案:文案在后端,改一个字这里就静默掉进「无法登录」
|
||||
if (err.error === "account-disabled") {
|
||||
if (errorCode(err) === "account-disabled") {
|
||||
authStore.setLoginError("此账号已被封禁")
|
||||
} else if (err.error === "invalid-credentials") {
|
||||
} else if (errorCode(err) === "invalid-credentials") {
|
||||
authStore.setLoginError("用户名或密码不正确")
|
||||
} else if (err.error === "too-many-login-attempts") {
|
||||
} else if (errorCode(err) === "too-many-login-attempts") {
|
||||
authStore.setLoginError("密码错误次数太多,请 15 分钟后再试")
|
||||
} else {
|
||||
authStore.setLoginError("无法登录")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { errorCode } from "utils/api"
|
||||
import { signup } from "../api"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useAuthModalStore } from "../store/authModal"
|
||||
@@ -47,12 +48,12 @@ function submit() {
|
||||
email: form.value.email,
|
||||
password: form.value.password,
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (err.error === "username-exists") {
|
||||
} catch (err) {
|
||||
if (errorCode(err) === "username-exists") {
|
||||
authStore.setSignupError("用户名已存在")
|
||||
} else if (err.error === "email-exists") {
|
||||
} else if (errorCode(err) === "email-exists") {
|
||||
authStore.setSignupError("邮箱已存在")
|
||||
} else if (err.error === "too-many-registrations") {
|
||||
} else if (errorCode(err) === "too-many-registrations") {
|
||||
authStore.setSignupError("注册太频繁,请稍后再试")
|
||||
} else {
|
||||
authStore.setSignupError("无法注册")
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user