拦截器原来 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>
109 lines
2.5 KiB
Vue
109 lines
2.5 KiB
Vue
<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"
|
|
import { getTutorialList, setTutorialVisibility } from "../api"
|
|
import Actions from "./components/Actions.vue"
|
|
|
|
const tutorials = ref<{ [key: string]: TutorialListItem[] }>({
|
|
python: [],
|
|
c: [],
|
|
})
|
|
const message = useMessage()
|
|
const activeTab = ref("python")
|
|
|
|
const columns: DataTableColumn<TutorialListItem>[] = [
|
|
{
|
|
title: "顺序",
|
|
key: "order",
|
|
width: 80,
|
|
},
|
|
{ title: "标题", key: "title", minWidth: 200 },
|
|
{
|
|
title: "作者",
|
|
key: "createdBy",
|
|
render: (row) => row.createdBy?.username,
|
|
width: 80,
|
|
},
|
|
{
|
|
title: "创建时间",
|
|
key: "createdAt",
|
|
width: 180,
|
|
render: (row) => parseTime(row.createdAt!, "YYYY-MM-DD HH:mm:ss"),
|
|
},
|
|
{
|
|
title: "更新时间",
|
|
key: "updatedAt",
|
|
width: 180,
|
|
render: (row) => parseTime(row.updatedAt!, "YYYY-MM-DD HH:mm:ss"),
|
|
},
|
|
{
|
|
title: "可见",
|
|
key: "isPublic",
|
|
width: 100,
|
|
render: (row) =>
|
|
h(NSwitch, {
|
|
value: row.isPublic,
|
|
size: "small",
|
|
rubberBand: false,
|
|
onUpdateValue: () => toggleVisible(row),
|
|
}),
|
|
},
|
|
{
|
|
title: "操作",
|
|
key: "actions",
|
|
width: 140,
|
|
render: (row) =>
|
|
h(Actions, { tutorialID: row.id, onDeleted: listTutorials }),
|
|
},
|
|
]
|
|
|
|
async function toggleVisible(tutorial: TutorialListItem) {
|
|
tutorial.isPublic = !tutorial.isPublic
|
|
try {
|
|
await setTutorialVisibility(tutorial.id, tutorial.isPublic)
|
|
message.success("更新成功")
|
|
} catch (err) {
|
|
message.error(errorMessage(err))
|
|
tutorial.isPublic = !tutorial.isPublic
|
|
}
|
|
}
|
|
|
|
async function listTutorials() {
|
|
tutorials.value = await getTutorialList()
|
|
}
|
|
|
|
onMounted(listTutorials)
|
|
</script>
|
|
|
|
<template>
|
|
<n-flex align="center" class="titleWrapper">
|
|
<h2 class="title">教程列表</h2>
|
|
<n-button
|
|
type="primary"
|
|
@click="$router.push({ name: 'admin tutorial create' })"
|
|
>
|
|
新建
|
|
</n-button>
|
|
</n-flex>
|
|
<n-tabs v-model:value="activeTab" type="line" animated>
|
|
<n-tab-pane name="python" tab="Python">
|
|
<n-data-table striped :columns="columns" :data="tutorials.python" />
|
|
</n-tab-pane>
|
|
<n-tab-pane name="c" tab="C 语言">
|
|
<n-data-table striped :columns="columns" :data="tutorials.c" />
|
|
</n-tab-pane>
|
|
</n-tabs>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.titleWrapper {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.title {
|
|
margin: 0;
|
|
}
|
|
</style>
|