refactor(前端): 去掉 { error, data } 信封,api2 改名 api
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
信封是 Django 时代的形状:拦截器手工造一个**恒为 null** 的 error 字段,再把
真正的载荷塞进 data。后端 http.ts 的 success 其实只返回 { data },那个 error
从头到尾没人用 —— 全站成功路径读 res.error 的只有 admin/api.ts 的
resetPassword 一处,而它自己就是个把信封拆开再重新包一遍的 shim。
代价是每个调用点都要 .data 一次:47 个组件、3 个 api 层文件、200 多处。
现在拦截器直接返回 response.data.data,ApiResponse<T> 退化成 T,文件末尾那句
`as unknown as Api2Client` 的类型谎言也少了一层。失败路径不动,仍然 reject
`{ error: 错误码, data: 文案 }` —— 和成功路径不对称是故意的,成功没有错误码
可言,接口注释里写清楚了。
顺带把 api2 改回 api:utils/ 下早就没有 api.ts 了,"2" 是迁移期用来和旧
client 区分的,现在只剩下让人多想一秒的作用。
## 怎么改的
**没有全局 sed。** 先把客户端的返回类型从 Promise<ApiResponse<T>> 改成
Promise<T>,让 vue-tsc 把每一处报出来(210 条),再按它给的 file:line:col
精确删 `.data`(192 处),剩下的手工处理:
- 6 处 `const { data } = await ...` 解构 → `const data = await ...`
- 3 个 api 层函数(getProfile / getProblem / getSubmission)自己手工造信封,
改成直接返回值;getProfile 的返回类型跟着从 ApiResponse<Profile|null>
变成 Profile|null
**类型检查抓不到的,人工把剩下的每一处 `.data` 过了一遍** —— 载荷本身带
data 字段、或者载荷是索引签名时,`res.data` 照样过类型。这一遍捞出三条真 bug:
- `getTutorialList` 的载荷是 `{ [key: string]: TutorialListItem[] }`(按
python / c 分组)。索引签名让 `res.data` 编译通过、运行时是 undefined ——
改完信封之后教程列表会**两个 tab 全空且不报错**。实跑确认过修好了。
- `createExercise` / `updateExercise` 返回 `res.data`,而 Exercise 自己有
data 字段(练习内容)。两个调用方都不看返回值,所以类型和运行时都不响。
- `getSimilarProblems` 的 `.then(r => ({ ...r, data: r.data.map(...) }))`
删掉 .data 之后变成往对象里摊一个数组,能跑但形状是错的。
另外两处是**对的**,加了注释免得下次被"顺手清理"掉:
StatisticsPanel 的 `res.data` 是契约 submissionStatisticsSchema 自己的 data
字段(每个学生一行);download.ts 是独立 axios 实例,`res.data` 是 axios 的
响应体(zip 二进制,不走信封)。
## 验证
tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、vite build
通过。**因为这改动碰的是每一个请求,静态检查不够,起了全套服务用浏览器实跑:**
- oj 侧 12 个页面 + 后台 13 个页面逐个打开,断言没有重定向、console 无报错。
- 关键页面进一步断言渲染出了真数据(后台用户列表 3 行、题目列表 10 行、
站点配置表单三个输入框有值、教程列表分组正确)。
- 三条写路径实打:重置密码(库里 student123 → 531554,表格当场刷新)、
公告可见性开关(走 getAnnouncement + editAnnouncement,就是手改解构那处,
库里 visible t → f)、提交代码(POST → 判题机真跑出 -2 → 提交列表和详情页
都正确渲染状态、语言、代码)。
- /rank 有一条 `{error: "class-missing"}` 的未捕获 reject,stash 掉本次改动
复现同样报错,**是既有问题**,不在本次范围内。
本地 dev 库为了打通后台测试改了三处,都只影响本机:devadmin 补了 email 和
user_profile 行(原来缺这两样,getProfile 报 profile-not-found,AUTHED 存不
进去,所有 /admin 路由被守卫弹回首页)、密码重置成 devpass123。冒烟用的教程/
公告/提交三条测试数据已删干净,题目和用户的提交计数也回滚了。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,8 +52,7 @@ const similarLoaded = ref(false)
|
||||
async function loadSimilarProblems() {
|
||||
if (similarLoaded.value || !problem.value) return
|
||||
try {
|
||||
const res = await getSimilarProblems(problem.value._id)
|
||||
similarProblems.value = res.data || []
|
||||
similarProblems.value = await getSimilarProblems(problem.value._id)
|
||||
} catch {
|
||||
similarProblems.value = []
|
||||
}
|
||||
|
||||
@@ -91,12 +91,12 @@ const options = {
|
||||
|
||||
async function getBeatRate() {
|
||||
const res = await getProblemBeatRate(problem.value!.id)
|
||||
beatRate.value = res.data
|
||||
beatRate.value = res
|
||||
}
|
||||
|
||||
async function getYearlyAC() {
|
||||
const res = await getProblemYearlyAC(problem.value!._id)
|
||||
yearlyACData.value = res.data
|
||||
yearlyACData.value = res
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -239,8 +239,8 @@ async function pick(key: ReactionKey) {
|
||||
submitting.value = key
|
||||
try {
|
||||
const res = await setReaction(problem.value.id, key)
|
||||
mine.value = res.data.mine
|
||||
counts.value = res.data.counts
|
||||
mine.value = res.mine
|
||||
counts.value = res.counts
|
||||
emit("submitted")
|
||||
} catch {
|
||||
message.error("提交失败,请重试")
|
||||
@@ -255,8 +255,8 @@ async function load(problemId: number) {
|
||||
try {
|
||||
const res = await getReaction(problemId)
|
||||
if (sequence !== loadSequence) return
|
||||
mine.value = res.data.mine
|
||||
counts.value = res.data.counts
|
||||
mine.value = res.mine
|
||||
counts.value = res.counts
|
||||
} catch {
|
||||
if (sequence === loadSequence) message.error("暂时无法读取题目点评")
|
||||
} finally {
|
||||
|
||||
@@ -129,8 +129,8 @@ async function listSubmissions() {
|
||||
problemId: (route.params.problemID as string) ?? "",
|
||||
contestId: (route.params.contestID as string) ?? "",
|
||||
})
|
||||
submissions.value = res.data.results
|
||||
total.value = res.data.total
|
||||
submissions.value = res.results
|
||||
total.value = res.total
|
||||
}
|
||||
|
||||
async function getRankOfThisProblem() {
|
||||
@@ -138,10 +138,10 @@ async function getRankOfThisProblem() {
|
||||
const res = await getRankOfProblem((route.params.problemID as string) ?? "")
|
||||
loading.value = false
|
||||
|
||||
class_name.value = res.data.className
|
||||
rank.value = res.data.rank
|
||||
class_ac_count.value = res.data.classAcCount
|
||||
all_ac_count.value = res.data.allAcCount
|
||||
class_name.value = res.className
|
||||
rank.value = res.rank
|
||||
class_ac_count.value = res.classAcCount
|
||||
all_ac_count.value = res.allAcCount
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -115,7 +115,7 @@ async function submit() {
|
||||
code: codeStore.code.value,
|
||||
language: formatLang,
|
||||
})
|
||||
codeStore.setCode(res.data.code)
|
||||
codeStore.setCode(res.code)
|
||||
} catch (e: any) {
|
||||
if (e?.error === "format-error") {
|
||||
// 仅 Python3 会出现:代码本身存在语法错误
|
||||
@@ -141,11 +141,11 @@ async function submit() {
|
||||
isSubmittingRequest.value = true
|
||||
try {
|
||||
const res = await submitCode(data)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.data.submissionId}`)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.submissionId}`)
|
||||
|
||||
// 3. 启动冷却 + 监控
|
||||
startCooldown()
|
||||
startMonitoring(res.data.submissionId)
|
||||
startMonitoring(res.submissionId)
|
||||
showResult.value = true
|
||||
} finally {
|
||||
isSubmittingRequest.value = false
|
||||
|
||||
@@ -144,7 +144,7 @@ async function submitFlowchartData() {
|
||||
})
|
||||
|
||||
// 获取提交ID并订阅更新
|
||||
const submissionId = response.data.submissionId
|
||||
const submissionId = response.submissionId
|
||||
|
||||
if (submissionId) {
|
||||
subscribeToSubmission(submissionId)
|
||||
@@ -167,7 +167,7 @@ function submit() {
|
||||
|
||||
async function getCurrentSubmission() {
|
||||
if (!problem.value?.id) return
|
||||
const { data } = await getCurrentProblemFlowchartSubmission(problem.value.id)
|
||||
const data = await getCurrentProblemFlowchartSubmission(problem.value.id)
|
||||
submissionCount.value = data.count
|
||||
latestRating.value = {
|
||||
score: data.score,
|
||||
@@ -177,7 +177,7 @@ async function getCurrentSubmission() {
|
||||
|
||||
async function getSubmission(submissionPage = 0) {
|
||||
if (!problem.value?.id) return
|
||||
const { data } = await getFlowchartSubmissionDetail(
|
||||
const data = await getFlowchartSubmissionDetail(
|
||||
problem.value.id,
|
||||
submissionPage,
|
||||
)
|
||||
|
||||
@@ -25,9 +25,9 @@ export function useSubmissionMonitor() {
|
||||
|
||||
try {
|
||||
const res = await getSubmission(submissionId.value)
|
||||
submission.value = res.data
|
||||
submission.value = res
|
||||
|
||||
const result = res.data.result
|
||||
const result = res.result
|
||||
// 判题完成,停止轮询
|
||||
if (
|
||||
result !== SubmissionStatus.judging &&
|
||||
@@ -83,7 +83,7 @@ export function useSubmissionMonitor() {
|
||||
pausePolling()
|
||||
|
||||
getSubmission(submissionId.value).then((res) => {
|
||||
submission.value = res.data
|
||||
submission.value = res
|
||||
// 15分钟无新提交则断开WebSocket(节省资源)
|
||||
scheduleDisconnect(15 * 60 * 1000)
|
||||
})
|
||||
|
||||
@@ -110,7 +110,7 @@ async function init() {
|
||||
;(inProblem.value ? loadProblemEditor : loadContestEditor)()
|
||||
try {
|
||||
const res = await getProblem(problemID, contestID)
|
||||
problem.value = res.data
|
||||
problem.value = res
|
||||
} catch (err: any) {
|
||||
problem.value = null
|
||||
if (err.error === "contest-not-started") {
|
||||
|
||||
@@ -81,7 +81,7 @@ async function listProblems() {
|
||||
|
||||
async function listTags() {
|
||||
const res = await getProblemTagList()
|
||||
tags.value = res.data.map((r: Omit<Tag, "checked">) => ({
|
||||
tags.value = res.map((r: Omit<Tag, "checked">) => ({
|
||||
...r,
|
||||
checked: query.tag === r.name,
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user