From 0b7b08f2cc7a3ce9b4adc1f51988fe646dff2331 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 25 Aug 2026 22:42:20 -0600 Subject: [PATCH] =?UTF-8?q?refactor(=E5=89=8D=E7=AB=AF):=20=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=20{=20error,=20data=20}=20=E4=BF=A1=E5=B0=81=EF=BC=8C?= =?UTF-8?q?api2=20=E6=94=B9=E5=90=8D=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 信封是 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,文件末尾那句 `as unknown as Api2Client` 的类型谎言也少了一层。失败路径不动,仍然 reject `{ error: 错误码, data: 文案 }` —— 和成功路径不对称是故意的,成功没有错误码 可言,接口注释里写清楚了。 顺带把 api2 改回 api:utils/ 下早就没有 api.ts 了,"2" 是迁移期用来和旧 client 区分的,现在只剩下让人多想一秒的作用。 ## 怎么改的 **没有全局 sed。** 先把客户端的返回类型从 Promise> 改成 Promise,让 vue-tsc 把每一处报出来(210 条),再按它给的 file:line:col 精确删 `.data`(192 处),剩下的手工处理: - 6 处 `const { data } = await ...` 解构 → `const data = await ...` - 3 个 api 层函数(getProfile / getProblem / getSubmission)自己手工造信封, 改成直接返回值;getProfile 的返回类型跟着从 ApiResponse 变成 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 --- apps/api/src/auth/middleware.ts | 2 +- .../components/AchievementModal.vue | 2 +- apps/web/src/admin/achievement/list.vue | 2 +- apps/web/src/admin/ai/list.vue | 8 +- apps/web/src/admin/announcement/detail.vue | 10 +- apps/web/src/admin/announcement/list.vue | 6 +- apps/web/src/admin/api.ts | 206 +++++++++--------- .../src/admin/contest/components/Actions.vue | 2 +- apps/web/src/admin/contest/detail.vue | 2 +- apps/web/src/admin/contest/helper.vue | 10 +- apps/web/src/admin/contest/list.vue | 4 +- apps/web/src/admin/problem/Stuck.vue | 2 +- apps/web/src/admin/problem/TopACTrend.vue | 2 +- .../problem/components/BatchTagModal.vue | 4 +- .../problem/components/SQLTestcaseEditor.vue | 12 +- .../problem/components/TestcaseGenerator.vue | 4 +- apps/web/src/admin/problem/detail.vue | 10 +- apps/web/src/admin/problem/tags.vue | 6 +- apps/web/src/admin/problemset/detail.vue | 8 +- apps/web/src/admin/problemset/edit.vue | 2 +- apps/web/src/admin/problemset/list.vue | 4 +- apps/web/src/admin/setting/config.vue | 22 +- apps/web/src/admin/setting/home.vue | 12 +- apps/web/src/admin/user/list.vue | 8 +- apps/web/src/oj/achievement/api.ts | 10 +- apps/web/src/oj/achievement/index.vue | 7 +- apps/web/src/oj/announcement/list.vue | 6 +- apps/web/src/oj/api.ts | 138 ++++++------ apps/web/src/oj/class/pk.vue | 4 +- apps/web/src/oj/contest/list.vue | 4 +- apps/web/src/oj/contest/pages/rank.vue | 6 +- apps/web/src/oj/learn/index.vue | 4 +- .../oj/problem/components/ProblemContent.vue | 3 +- .../src/oj/problem/components/ProblemInfo.vue | 4 +- .../oj/problem/components/ProblemReaction.vue | 8 +- .../problem/components/ProblemSubmission.vue | 12 +- .../src/oj/problem/components/SubmitCode.vue | 6 +- .../oj/problem/components/SubmitFlowchart.vue | 6 +- .../composables/useSubmissionMonitor.ts | 6 +- apps/web/src/oj/problem/detail.vue | 2 +- apps/web/src/oj/problem/list.vue | 2 +- .../components/UserProgressView.vue | 12 +- apps/web/src/oj/problemset/detail.vue | 8 +- apps/web/src/oj/problemset/list.vue | 4 +- apps/web/src/oj/rank/list.vue | 24 +- apps/web/src/oj/store/ai.ts | 24 +- apps/web/src/oj/store/contest.ts | 10 +- .../components/FlowchartScoreDetail.vue | 2 +- apps/web/src/oj/submission/detail.vue | 2 +- apps/web/src/oj/submission/list.vue | 10 +- apps/web/src/oj/user/index.vue | 24 +- apps/web/src/oj/user/message.vue | 4 +- apps/web/src/shared/api.ts | 26 +-- .../src/shared/components/AuthorSelect.vue | 2 +- .../components/FlowchartStatisticsPanel.vue | 2 +- apps/web/src/shared/components/Hitokoto.vue | 2 +- apps/web/src/shared/components/Login.vue | 2 +- .../src/shared/components/StatisticsPanel.vue | 16 +- apps/web/src/shared/store/achievement.ts | 3 +- apps/web/src/shared/store/config.ts | 4 +- apps/web/src/shared/store/loginSummary.ts | 6 +- apps/web/src/shared/store/user.ts | 2 +- apps/web/src/utils/{api2.ts => api.ts} | 49 ++--- apps/web/src/utils/download.ts | 2 +- 64 files changed, 391 insertions(+), 427 deletions(-) rename apps/web/src/utils/{api2.ts => api.ts} (61%) diff --git a/apps/api/src/auth/middleware.ts b/apps/api/src/auth/middleware.ts index 83cbd38..0bf1716 100644 --- a/apps/api/src/auth/middleware.ts +++ b/apps/api/src/auth/middleware.ts @@ -41,7 +41,7 @@ export const requireAuth: MiddlewareHandler = async (c, next) => { * 后台接口的角色守卫,对应旧后端 `account/decorators.py` 的四个装饰器。 * * 未登录一律 401 `login-required`、登录但角色不够一律 403 `permission-denied`, - * 与旧 `BasePermissionDecorator._permission_error` 的两分支一致 —— 前端 `utils/api2.ts` + * 与旧 `BasePermissionDecorator._permission_error` 的两分支一致 —— 前端 `utils/api.ts` * 的拦截器就是按这两个 code 分别弹登录框和弹提示的。禁用账号走第三个码,见 denied()。 */ function requireRole( diff --git a/apps/web/src/admin/achievement/components/AchievementModal.vue b/apps/web/src/admin/achievement/components/AchievementModal.vue index d6481fb..1eaa74d 100644 --- a/apps/web/src/admin/achievement/components/AchievementModal.vue +++ b/apps/web/src/admin/achievement/components/AchievementModal.vue @@ -56,7 +56,7 @@ watch( if (!show) return if (!metrics.value.length) { const res = await getMetricOptions() - metrics.value = res.data + metrics.value = res } if (props.editing) { form.value = { ...emptyForm(), ...props.editing } diff --git a/apps/web/src/admin/achievement/list.vue b/apps/web/src/admin/achievement/list.vue index bb111a7..e7dd11f 100644 --- a/apps/web/src/admin/achievement/list.vue +++ b/apps/web/src/admin/achievement/list.vue @@ -30,7 +30,7 @@ async function load() { loading.value = true try { const res = await getAdminAchievements() - list.value = res.data + list.value = res } finally { loading.value = false } diff --git a/apps/web/src/admin/ai/list.vue b/apps/web/src/admin/ai/list.vue index 36ecfee..3ae0bb5 100644 --- a/apps/web/src/admin/ai/list.vue +++ b/apps/web/src/admin/ai/list.vue @@ -144,7 +144,7 @@ const columns: DataTableColumn[] = [ async function loadPinnedReports() { const res = await getPinnedAIReports() - pinnedReports.value = res.data.results + pinnedReports.value = res.results } async function togglePin(row: ReportItem) { @@ -155,8 +155,8 @@ async function togglePin(row: ReportItem) { async function listReports() { const offset = (query.page - 1) * query.limit const res = await getAIReportList(offset, query.limit, query.username) - reports.value = res.data.results - total.value = res.data.total + reports.value = res.results + total.value = res.total } async function openDetail(id: number) { @@ -165,7 +165,7 @@ async function openDetail(id: number) { detail.value = null try { const res = await getAIReportDetail(id) - detail.value = res.data + detail.value = res } finally { loadingDetail.value = false } diff --git a/apps/web/src/admin/announcement/detail.vue b/apps/web/src/admin/announcement/detail.vue index 258f594..aedad34 100644 --- a/apps/web/src/admin/announcement/detail.vue +++ b/apps/web/src/admin/announcement/detail.vue @@ -36,11 +36,11 @@ async function init() { const res = await getAnnouncement(id) toggleReady(true) announcement.id = id - announcement.title = res.data.title - announcement.content = res.data.content - announcement.visible = res.data.visible - announcement.tag = res.data.tag - announcement.top = res.data.top + announcement.title = res.title + announcement.content = res.content + announcement.visible = res.visible + announcement.tag = res.tag + announcement.top = res.top } async function submit() { diff --git a/apps/web/src/admin/announcement/list.vue b/apps/web/src/admin/announcement/list.vue index 4f23823..04ee8db 100644 --- a/apps/web/src/admin/announcement/list.vue +++ b/apps/web/src/admin/announcement/list.vue @@ -67,7 +67,7 @@ const columns: DataTableColumn[] = [ async function toggleVisible(announcement: AnnouncementListItem) { const next = !announcement.visible try { - const { data: full } = await getAnnouncement(announcement.id) + const full = await getAnnouncement(announcement.id) await editAnnouncement({ id: full.id, title: full.title, @@ -85,8 +85,8 @@ async function toggleVisible(announcement: AnnouncementListItem) { async function listAnnouncements() { const offset = (query.page - 1) * query.limit const res = await getAnnouncementList(offset, query.limit) - announcements.value = res.data.results - total.value = res.data.total + announcements.value = res.results + total.value = res.total } onMounted(listAnnouncements) diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index 307e902..b2e108a 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -1,4 +1,4 @@ -import api2 from "utils/api2" +import api from "utils/api" import { toProblemListItem } from "admin/transforms" import type { AcTrend, @@ -42,11 +42,11 @@ import type { } from "utils/types" export function getBaseInfo() { - return api2.get("admin/dashboard") + return api.get("admin/dashboard") } export function randomUser10(classroom: string) { - return api2.get("admin/random-usernames", { + return api.get("admin/random-usernames", { params: { classroom }, }) } @@ -62,69 +62,69 @@ export async function getProblemList( const endpoint = contestID ? `admin/contests/${contestID}/problems` : "admin/problems" - const res = await api2.get(endpoint, { + const res = await api.get(endpoint, { params: { offset, limit, keyword, author, tagId }, }) return { - results: res.data.results.map(toProblemListItem), - total: res.data.total, + results: res.results.map(toProblemListItem), + total: res.total, } } export function deleteProblem(id: number) { - return api2.delete(`admin/problems/${id}`) + return api.delete(`admin/problems/${id}`) } // 比赛题与公开题共用一条删除路由,比赛由后端从题目推导 export function deleteContestProblem(id: number) { - return api2.delete(`admin/problems/${id}`) + return api.delete(`admin/problems/${id}`) } export function editProblem(problem: AdminProblem | BlankProblem) { - return api2.put( + return api.put( `admin/problems/${(problem as AdminProblem).id}`, toProblemBody(problem), ) } export function toggleProblemVisible(problemID: number) { - return api2.put<{ visible: boolean }>( + return api.put<{ visible: boolean }>( `admin/problems/${problemID}/visibility`, ) } export function generateFlowchartFromPythonCode(python: string) { - return api2.post<{ flowchart: string }>("admin/problems/flowchart", { + return api.post<{ flowchart: string }>("admin/problems/flowchart", { python, }) } export function editContestProblem(problem: AdminProblem | BlankProblem) { - return api2.put( + return api.put( `admin/problems/${(problem as AdminProblem).id}`, toProblemBody(problem), ) } export function getProblem(id: string | number) { - return api2.get(`admin/problems/${id}`) + return api.get(`admin/problems/${id}`) } export function getContestProblem(id: number) { - return api2.get(`admin/problems/${id}`) + return api.get(`admin/problems/${id}`) } // 标签管理 export function getTagAdminList(keyword = "") { - return api2.get("admin/problem-tags", { params: { keyword } }) + return api.get("admin/problem-tags", { params: { keyword } }) } export function renameTag(id: number, name: string) { - return api2.put(`admin/problem-tags/${id}`, { name }) + return api.put(`admin/problem-tags/${id}`, { name }) } export function deleteTag(id: number) { - return api2.delete(`admin/problem-tags/${id}`) + return api.delete(`admin/problem-tags/${id}`) } export function batchTagProblems( @@ -132,7 +132,7 @@ export function batchTagProblems( tagNames: string[], action: "add" | "remove", ) { - return api2.post("admin/problems/batch-tag", { + return api.post("admin/problems/batch-tag", { problemIds, tagNames, action, @@ -141,7 +141,7 @@ export function batchTagProblems( // 用户排名(后台版,无 100 名上限;公开榜单是 oj/api.ts 的 getRank) export function getAdminUserRank(offset: number, limit: number, keyword: string) { - return api2.get("admin/rankings/users", { + return api.get("admin/rankings/users", { params: { offset, limit, keyword }, }) } @@ -154,7 +154,7 @@ export function getUserList( keyword: string, orderBy = "", ) { - return api2.get("admin/users", { + return api.get("admin/users", { // 旧接口的 order_by 只有 "-last_login" 一个取值 params: { offset, @@ -168,7 +168,7 @@ export function getUserList( // 编辑用户 export function editUser(user: User) { - return api2.put(`admin/users/${user.id}`, { + return api.put(`admin/users/${user.id}`, { username: user.username, email: user.email, adminType: user.adminType, @@ -180,27 +180,26 @@ export function editUser(user: User) { }) } -// 重置用户密码。调用方直接用 res.data 当密码字符串(旧后端返回的就是裸字符串), -// 新后端返回 { password },在这里解包,组件不动 +// 重置用户密码,返回新密码 export async function resetPassword(userID: number) { - const res = await api2.post<{ password: string }>( + const res = await api.post<{ password: string }>( `admin/users/${userID}/reset-password`, ) - return { error: res.error, data: res.data.password } + return res.password } // 导入用户 export function importUsers(users: string[][]) { - return api2.post("admin/users", { users }) + return api.post("admin/users", { users }) } // 批量删除用户 export function deleteUsers(userIDs: number[]) { - return api2.delete("admin/users", { data: { ids: userIDs } }) + return api.delete("admin/users", { data: { ids: userIDs } }) } export function getContestList(offset = 0, limit = 10, keyword: string) { - return api2.get("admin/contests", { + return api.get("admin/contests", { params: { offset, limit, keyword }, }) } @@ -209,14 +208,14 @@ export function getContestList(offset = 0, limit = 10, keyword: string) { export async function uploadImage(file: File): Promise { const form = new window.FormData() form.append("image", file) - const res = await api2.post<{ + const res = await api.post<{ success: boolean filePath: string msg: string }>("admin/upload-image", form, { headers: { "content-type": "multipart/form-data" }, }) - return res.data.success ? res.data.filePath : "" + return res.success ? res.filePath : "" } // 上传测试用例;SQL 题的压缩包是 1.sql..N.sql(每个文件一个测试点的建表+数据脚本) @@ -226,7 +225,7 @@ export function uploadTestcases(file: File, options: { sql?: boolean } = {}) { if (options.sql) { form.append("sql", "1") } - return api2.post("admin/test-cases", form, { + return api.post("admin/test-cases", form, { headers: { "content-type": "multipart/form-data" }, }) } @@ -237,12 +236,12 @@ export function previewSQLTestcase(data: { refSql: string mode: "query" | "modify" }) { - return api2.post("admin/sql-test-cases/preview", data) + return api.post("admin/sql-test-cases/preview", data) } // 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序) export function getSQLTestcaseScripts(problemId: number) { - return api2.get( + return api.get( `admin/problems/${problemId}/sql-scripts`, ) } @@ -252,7 +251,7 @@ export function generateSQLTestcase(data: { refSql: string mode: "query" | "modify" }) { - return api2.post( + return api.post( "admin/sql-test-cases/generate", data, ) @@ -292,12 +291,12 @@ function toProblemBody(problem: AdminProblem | BlankProblem) { } export function createProblem(problem: BlankProblem) { - return api2.post("admin/problems", toProblemBody(problem)) + return api.post("admin/problems", toProblemBody(problem)) } export function createContestProblem(problem: BlankProblem) { // contestId 由 detail.vue 在提交前写进 problem 对象 - return api2.post( + return api.post( `admin/contests/${problem.contestId}/problems`, toProblemBody(problem), ) @@ -318,22 +317,22 @@ function toContestBody(contest: Contest | BlankContest) { } export function createContest(contest: BlankContest) { - return api2.post("admin/contests", toContestBody(contest)) + return api.post("admin/contests", toContestBody(contest)) } export function editContest(contest: Contest | BlankContest) { - return api2.put( + return api.put( `admin/contests/${(contest as Contest).id}`, toContestBody(contest), ) } export function cloneContest(contestId: number) { - return api2.post(`admin/contests/${contestId}/clone`) + return api.post(`admin/contests/${contestId}/clone`) } export function getContest(id: string) { - return api2.get(`admin/contests/${id}`) + return api.get(`admin/contests/${id}`) } export function addProblemForContest( @@ -341,71 +340,67 @@ export function addProblemForContest( problemID: number, displayID: string, ) { - return api2.post( + return api.post( `admin/contests/${contestID}/problems/from-public`, { problemId: problemID, displayId: displayID }, ) } export function getWebsite() { - return api2.get("admin/website") + return api.get("admin/website") } export function editWebsite(data: WebsiteConfig) { - return api2.post("admin/website", data) + return api.post("admin/website", data) } export function listInvalidTestcases() { - return api2.get("admin/orphan-test-cases") + return api.get("admin/orphan-test-cases") } export function pruneInvalidTestcases(id?: string) { - return api2.delete("admin/orphan-test-cases", { params: { id } }) + return api.delete("admin/orphan-test-cases", { params: { id } }) } export function getJudgeServer() { - return api2.get("admin/judge-servers") + return api.get("admin/judge-servers") } export function deleteJudgeServer(hostname: string) { - return api2.delete(`admin/judge-servers/${encodeURIComponent(hostname)}`) + return api.delete(`admin/judge-servers/${encodeURIComponent(hostname)}`) } export function getAnnouncementList(offset = 0, limit = 10) { - return api2.get<{ results: AnnouncementListItem[]; total: number }>( + return api.get<{ results: AnnouncementListItem[]; total: number }>( "admin/announcements", { params: { offset, limit } }, ) } export function getAnnouncement(id: number) { - return api2.get(`admin/announcements/${id}`) + return api.get(`admin/announcements/${id}`) } export function deleteAnnouncement(id: number) { - return api2.delete(`admin/announcements/${id}`) + return api.delete(`admin/announcements/${id}`) } export function editAnnouncement(announcement: AnnouncementEdit) { const { id, ...body } = announcement - return api2.put(`admin/announcements/${id}`, body) + return api.put(`admin/announcements/${id}`, body) } export function createAnnouncement(announcement: AnnouncementEdit) { const { id: _id, ...body } = announcement - return api2.post("admin/announcements", body) + return api.post("admin/announcements", body) } -export async function getTutorialList() { - const res = await api2.get<{ [key: string]: TutorialListItem[] }>( - "admin/tutorials", - ) - return res.data +export function getTutorialList() { + return api.get<{ [key: string]: TutorialListItem[] }>("admin/tutorials") } -export async function getTutorial(id: number) { - const res = await api2.get(`admin/tutorials/${id}`) - return res.data +export function getTutorial(id: number) { + return api.get(`admin/tutorials/${id}`) } function toTutorialBody(data: Partial) { @@ -419,32 +414,27 @@ function toTutorialBody(data: Partial) { } } -export async function createTutorial(data: Partial) { - const res = await api2.post("admin/tutorials", toTutorialBody(data)) - return res.data +export function createTutorial(data: Partial) { + return api.post("admin/tutorials", toTutorialBody(data)) } -export async function updateTutorial(data: Partial) { - const res = await api2.put( +export function updateTutorial(data: Partial) { + return api.put( `admin/tutorials/${data.id}`, toTutorialBody(data), ) - return res.data } export function deleteTutorial(id: number) { - return api2.delete(`admin/tutorials/${id}`) + return api.delete(`admin/tutorials/${id}`) } export function setTutorialVisibility(id: number, isPublic: boolean) { - return api2.put(`admin/tutorials/${id}/visibility`, { isPublic }) + return api.put(`admin/tutorials/${id}/visibility`, { isPublic }) } -export async function getAdminExercises(tutorialId: number) { - const res = await api2.get( - `admin/tutorials/${tutorialId}/exercises`, - ) - return res.data +export function getAdminExercises(tutorialId: number) { + return api.get(`admin/tutorials/${tutorialId}/exercises`) } export async function createExercise(data: { @@ -453,8 +443,7 @@ export async function createExercise(data: { data: object order: number }) { - const res = await api2.post("admin/exercises", data) - return res.data + return api.post("admin/exercises", data) } export async function updateExercise(data: { @@ -463,21 +452,20 @@ export async function updateExercise(data: { data: object order: number }) { - const res = await api2.put(`admin/exercises/${data.id}`, { + return api.put(`admin/exercises/${data.id}`, { type: data.type, data: data.data, order: data.order, }) - return res.data } export function deleteExercise(id: number) { - return api2.delete(`admin/exercises/${id}`) + return api.delete(`admin/exercises/${id}`) } // 将竞赛题目转为公开题目 export function makeProblemPublic(id: number, displayId: string) { - return api2.post(`admin/problems/${id}/make-public`, { + return api.post(`admin/problems/${id}/make-public`, { displayId, }) } @@ -486,7 +474,7 @@ export function makeProblemPublic(id: number, displayId: string) { export function getACMHelperList(contestId: number) { // acInfo 在契约里是 Record(acm_contest_rank 的 JSONB 原文), // 组件侧按 SubmissionInfo 读,收窄放在这里 - return api2.get< + return api.get< Array & { acInfo: SubmissionInfo }> >(`admin/contests/${contestId}/acm-helper`) } @@ -497,7 +485,7 @@ export function updateACMHelperChecked( problem_id: string, checked: boolean, ) { - return api2.put(`admin/contests/${contest_id}/acm-helper`, { + return api.put(`admin/contests/${contest_id}/acm-helper`, { rankId: rank_id, problemId: problem_id, checked, @@ -512,13 +500,13 @@ export function getProblemSetList( difficulty = "", status = "", ) { - return api2.get("admin/problem-sets", { + return api.get("admin/problem-sets", { params: { offset, limit, keyword, difficulty, status }, }) } export function getProblemSetDetail(id: number) { - return api2.get(`admin/problem-sets/${id}`) + return api.get(`admin/problem-sets/${id}`) } interface ProblemSetBody { @@ -543,31 +531,31 @@ function toProblemSetBody(data: ProblemSetBody) { } export function createProblemSet(data: ProblemSetBody) { - return api2.post("admin/problem-sets", toProblemSetBody(data)) + return api.post("admin/problem-sets", toProblemSetBody(data)) } export function editProblemSet(data: ProblemSetBody & { id: number }) { - return api2.put( + return api.put( `admin/problem-sets/${data.id}`, toProblemSetBody(data), ) } export function deleteProblemSet(id: number) { - return api2.delete(`admin/problem-sets/${id}`) + return api.delete(`admin/problem-sets/${id}`) } export function toggleProblemSetVisible(id: number) { - return api2.put(`admin/problem-sets/${id}/visibility`) + return api.put(`admin/problem-sets/${id}/visibility`) } export function updateProblemSetStatus(id: number, status: string) { - return api2.put(`admin/problem-sets/${id}/status`, { status }) + return api.put(`admin/problem-sets/${id}/status`, { status }) } // 题单题目管理 API export function getProblemSetProblems(problemSetId: number) { - return api2.get( + return api.get( `admin/problem-sets/${problemSetId}/problems`, ) } @@ -582,7 +570,7 @@ export function addProblemToSet( hint?: string }, ) { - return api2.post(`admin/problem-sets/${problemSetId}/problems`, { + return api.post(`admin/problem-sets/${problemSetId}/problems`, { problemId: data.problemId, order: data.order ?? 0, isRequired: data.isRequired ?? true, @@ -601,7 +589,7 @@ export function editProblemInSet( hint?: string }, ) { - return api2.put( + return api.put( `admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`, data, ) @@ -611,14 +599,14 @@ export function removeProblemFromSet( problemSetId: number, problemSetProblemId: number, ) { - return api2.delete( + return api.delete( `admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`, ) } // 题单奖章管理 API export function getProblemSetBadges(problemSetId: number) { - return api2.get( + return api.get( `admin/problem-sets/${problemSetId}/badges`, ) } @@ -642,7 +630,7 @@ function toBadgeBody(data: BadgeBody) { } export function createProblemSetBadge(problemSetId: number, data: BadgeBody) { - return api2.post( + return api.post( `admin/problem-sets/${problemSetId}/badges`, toBadgeBody(data), ) @@ -653,31 +641,31 @@ export function editProblemSetBadge( badgeId: number, data: BadgeBody, ) { - return api2.put( + return api.put( `admin/problem-sets/${problemSetId}/badges/${badgeId}`, toBadgeBody(data), ) } export function deleteProblemSetBadge(problemSetId: number, badgeId: number) { - return api2.delete(`admin/problem-sets/${problemSetId}/badges/${badgeId}`) + return api.delete(`admin/problem-sets/${problemSetId}/badges/${badgeId}`) } // 题单进度管理 API // 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同 export function getProblemSetProgress(problemSetId: number) { - return api2.get( + return api.get( `admin/problem-sets/${problemSetId}/progress`, ) } export function removeUserFromProblemSet(problemSetId: number, userId: number) { - return api2.delete(`admin/problem-sets/${problemSetId}/progress/${userId}`) + return api.delete(`admin/problem-sets/${problemSetId}/progress/${userId}`) } // 学生卡点分析 export function getStuckProblems() { - return api2.get("admin/problem-analytics/stuck") + return api.get("admin/problem-analytics/stuck") } export function getTopACTrend(params: { @@ -685,26 +673,26 @@ export function getTopACTrend(params: { untilYear: number minPerYear: number }) { - return api2.get("admin/problem-analytics/ac-trend", { params }) + return api.get("admin/problem-analytics/ac-trend", { params }) } // AI 学习分析报告 export function getAIReportList(offset = 0, limit = 10, username = "") { - return api2.get("admin/ai/reports", { + return api.get("admin/ai/reports", { params: { offset, limit, username: username || undefined }, }) } export function getAIReportDetail(id: number) { - return api2.get(`admin/ai/reports/${id}`) + return api.get(`admin/ai/reports/${id}`) } export function pinAIReport(id: number) { - return api2.post<{ isPinned: boolean }>(`admin/ai/reports/${id}/pin`) + return api.post<{ isPinned: boolean }>(`admin/ai/reports/${id}/pin`) } export function getPinnedAIReports() { - return api2.get("admin/ai/reports", { + return api.get("admin/ai/reports", { params: { pinnedOnly: "true" }, }) } @@ -734,27 +722,27 @@ function toAchievementBody(data: Partial) { } export function getAdminAchievements() { - return api2.get("admin/achievements") + return api.get("admin/achievements") } export function getMetricOptions() { - return api2.get("admin/achievement-metrics") + return api.get("admin/achievement-metrics") } export function createAchievement(data: Partial) { - return api2.post( + return api.post( "admin/achievements", toAchievementBody(data), ) } export function updateAchievement(data: Partial) { - return api2.put( + return api.put( `admin/achievements/${data.id}`, toAchievementBody(data), ) } export function deleteAchievement(id: number) { - return api2.delete(`admin/achievements/${id}`) + return api.delete(`admin/achievements/${id}`) } diff --git a/apps/web/src/admin/contest/components/Actions.vue b/apps/web/src/admin/contest/components/Actions.vue index a936414..46fdd73 100644 --- a/apps/web/src/admin/contest/components/Actions.vue +++ b/apps/web/src/admin/contest/components/Actions.vue @@ -36,7 +36,7 @@ async function clone() { message.success("复制成功") router.push({ name: "admin contest edit", - params: { contestID: res.data.id }, + params: { contestID: res.id }, }) } catch { message.error("复制失败") diff --git a/apps/web/src/admin/contest/detail.vue b/apps/web/src/admin/contest/detail.vue index 55342a5..a3abe4e 100644 --- a/apps/web/src/admin/contest/detail.vue +++ b/apps/web/src/admin/contest/detail.vue @@ -69,7 +69,7 @@ async function getContestDetail() { toggleReady(true) return } - const { data } = await getContest(props.contestID) + const data = await getContest(props.contestID) toggleReady(true) contest.id = data.id contest.title = data.title diff --git a/apps/web/src/admin/contest/helper.vue b/apps/web/src/admin/contest/helper.vue index 79524ad..7f19951 100644 --- a/apps/web/src/admin/contest/helper.vue +++ b/apps/web/src/admin/contest/helper.vue @@ -142,18 +142,18 @@ async function viewSubmission(item: HelperItem) { limit: 1, }) - if (res.data.results.length === 0) { + if (res.results.length === 0) { message.warning("未找到该用户的 AC 提交") return } // 获取提交详情 - const submissionListItem = res.data.results[0] + const submissionListItem = res.results[0] const detailRes = await getSubmission(submissionListItem.id) // 手动添加 contest 字段(ACM模式下后端不返回此字段) currentSubmission.value = { - ...detailRes.data, + ...detailRes, contest: Number(props.contestID), problem_display_id: item.problemDisplayId, } @@ -169,10 +169,10 @@ async function loadData() { try { // 先获取比赛信息,获取开始时间 const contestRes = await getContest(props.contestID) - contestStartTime.value = new Date(contestRes.data.startTime) + contestStartTime.value = new Date(contestRes.startTime) // 再获取 AC 提交列表 - const { data } = await getACMHelperList(Number(props.contestID)) + const data = await getACMHelperList(Number(props.contestID)) submissions.value = data } catch (err: any) { message.error(err.data || "加载失败") diff --git a/apps/web/src/admin/contest/list.vue b/apps/web/src/admin/contest/list.vue index fa58c06..14d6dd0 100644 --- a/apps/web/src/admin/contest/list.vue +++ b/apps/web/src/admin/contest/list.vue @@ -87,8 +87,8 @@ const columns: DataTableColumn[] = [ async function listContests() { const offset = (query.page - 1) * query.limit const res = await getContestList(offset, query.limit, query.keyword) - contests.value = res.data.results - total.value = res.data.total + contests.value = res.results + total.value = res.total } onMounted(listContests) watch(() => [query.page, query.limit], listContests) diff --git a/apps/web/src/admin/problem/Stuck.vue b/apps/web/src/admin/problem/Stuck.vue index 67c1210..79c5598 100644 --- a/apps/web/src/admin/problem/Stuck.vue +++ b/apps/web/src/admin/problem/Stuck.vue @@ -29,7 +29,7 @@ const columns: DataTableColumn[] = [ onMounted(async () => { try { const res = await getStuckProblems() - data.value = res.data + data.value = res } finally { loading.value = false } diff --git a/apps/web/src/admin/problem/TopACTrend.vue b/apps/web/src/admin/problem/TopACTrend.vue index 59839c7..1bc5e52 100644 --- a/apps/web/src/admin/problem/TopACTrend.vue +++ b/apps/web/src/admin/problem/TopACTrend.vue @@ -121,7 +121,7 @@ async function fetchData() { untilYear: untilYear.value, minPerYear: minPerYear.value, }) - data.value = res.data + data.value = res } finally { loading.value = false } diff --git a/apps/web/src/admin/problem/components/BatchTagModal.vue b/apps/web/src/admin/problem/components/BatchTagModal.vue index 6125d42..1e70af5 100644 --- a/apps/web/src/admin/problem/components/BatchTagModal.vue +++ b/apps/web/src/admin/problem/components/BatchTagModal.vue @@ -41,7 +41,7 @@ function toggleTag(name: string) { async function listTags() { const res = await getTagAdminList() - tags.value = res.data + tags.value = res } function close() { @@ -60,7 +60,7 @@ async function submit() { ) const verb = props.action === "add" ? "添加" : "移除" message.success( - `已为 ${res.data.problemCount} 道题${verb} ${res.data.tagCount} 个标签`, + `已为 ${res.problemCount} 道题${verb} ${res.tagCount} 个标签`, ) close() emit("done") diff --git a/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue b/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue index c13d61c..6040f39 100644 --- a/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue +++ b/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue @@ -78,8 +78,8 @@ onMounted(async () => { if (!props.problemId) return try { const res = await getSQLTestcaseScripts(props.problemId) - if (res.data.length) { - scripts.value = res.data.map((f) => ({ ...blankEntry(), sql: f.content })) + if (res.length) { + scripts.value = res.map((f) => ({ ...blankEntry(), sql: f.content })) } } catch (err: any) { // 新题、以及旧格式(非 SQL)测试点,后端回 404/409,保持空白就是对的,不该报错。 @@ -128,7 +128,7 @@ async function generate() { refSql: refSQL.value, mode: props.mode, }) - s.sql = res.data.sql + s.sql = res.sql } catch (err) { const data = (err as { data?: unknown })?.data message.error(typeof data === "string" ? data : "AI 生成失败") @@ -158,7 +158,7 @@ async function preview() { refSql: refSQL.value, mode: props.mode, }) - s.display = res.data + s.display = res } catch (err) { const data = (err as { data?: unknown })?.data s.error = typeof data === "string" ? data : "预览失败" @@ -182,7 +182,7 @@ async function upload() { const res = await uploadTestcases(file, { sql: true }) // score 不在上传响应里,是这里按测试点数量平分补上的(余数给最后一个) - const entries = res.data.info + const entries = res.info const baseScore = Math.floor(100 / entries.length) const remainder = 100 - baseScore * entries.length const testcases: Testcase[] = entries.map((entry, i) => ({ @@ -192,7 +192,7 @@ async function upload() { ), })) - emit("uploaded", res.data.id, testcases) + emit("uploaded", res.id, testcases) message.success("上传成功") } catch { message.error("上传失败") diff --git a/apps/web/src/admin/problem/components/TestcaseGenerator.vue b/apps/web/src/admin/problem/components/TestcaseGenerator.vue index 3eb79a1..40680b3 100644 --- a/apps/web/src/admin/problem/components/TestcaseGenerator.vue +++ b/apps/web/src/admin/problem/components/TestcaseGenerator.vue @@ -170,7 +170,7 @@ async function upload() { const res = await uploadTestcases(file) // score 不在上传响应里,是这里按测试点数量平分补上的(余数给最后一个) - const entries = res.data.info + const entries = res.info const baseScore = Math.floor(100 / entries.length) const remainder = 100 - baseScore * entries.length const testcases: Testcase[] = entries.map((entry, i) => ({ @@ -180,7 +180,7 @@ async function upload() { ), })) - emit("uploaded", res.data.id, testcases) + emit("uploaded", res.id, testcases) message.success("上传成功") } catch { message.error("上传失败") diff --git a/apps/web/src/admin/problem/detail.vue b/apps/web/src/admin/problem/detail.vue index af71d82..ae424b7 100644 --- a/apps/web/src/admin/problem/detail.vue +++ b/apps/web/src/admin/problem/detail.vue @@ -208,7 +208,7 @@ async function getProblemDetail() { return } try { - const { data } = await getProblem(props.problemID) + const data = await getProblem(props.problemID) problem.value.id = data.id problem.value._id = data._id problem.value.title = data.title @@ -269,7 +269,7 @@ async function getProblemDetail() { async function getTagList() { const res = await getProblemTagList() - tagList.value = res.data + tagList.value = res tagListLoaded.value = true syncTagInputsFromProblemTags() } @@ -295,13 +295,13 @@ async function handleUploadTestcases({ file }: UploadCustomRequestOptions) { return } // score 不在上传响应里,前端按测试点数量平分补上 - const entries = res.data.info + const entries = res.info const testcases: Testcase[] = entries.map((entry) => ({ ...entry, score: (100 / entries.length).toFixed(0), })) problem.value.testCaseScore = testcases - problem.value.testCaseId = res.data.id + problem.value.testCaseId = res.id } catch (err) { message.error("上传测试用例失败") } @@ -495,7 +495,7 @@ async function generateMermaid() { ) isAIGenerating.value = false message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法") - problem.value.mermaidCode = res.data.flowchart + problem.value.mermaidCode = res.flowchart } const showGeneratorModal = ref(false) diff --git a/apps/web/src/admin/problem/tags.vue b/apps/web/src/admin/problem/tags.vue index a4f57dc..06e11ad 100644 --- a/apps/web/src/admin/problem/tags.vue +++ b/apps/web/src/admin/problem/tags.vue @@ -97,7 +97,7 @@ const columns: DataTableColumn[] = [ async function listTags() { const res = await getTagAdminList(keyword.value) - tags.value = res.data + tags.value = res } function startEdit(tag: AdminTag) { @@ -121,9 +121,9 @@ async function saveTag(tag: AdminTag) { return } const res = await renameTag(tag.id, name) - if (res.data.merged) { + if (res.merged) { message.success( - `已合并到「${res.data.name}」,影响 ${res.data.affectedCount} 道题`, + `已合并到「${res.name}」,影响 ${res.affectedCount} 道题`, ) } else { message.success("已重命名") diff --git a/apps/web/src/admin/problemset/detail.vue b/apps/web/src/admin/problemset/detail.vue index 9d393af..543083c 100644 --- a/apps/web/src/admin/problemset/detail.vue +++ b/apps/web/src/admin/problemset/detail.vue @@ -52,7 +52,7 @@ const editingBadge = ref(null) async function loadProblemSetDetail() { try { const res = await getProblemSetDetail(problemSetId.value) - problemSet.value = res.data + problemSet.value = res } catch (err: any) { message.error("加载题单详情失败:" + (err.data || "未知错误")) } @@ -61,7 +61,7 @@ async function loadProblemSetDetail() { async function loadProblems() { try { const res = await getProblemSetProblems(problemSetId.value) - problems.value = res.data + problems.value = res } catch (err: any) { message.error("加载题目列表失败:" + (err.data || "未知错误")) } @@ -70,7 +70,7 @@ async function loadProblems() { async function loadBadges() { try { const res = await getProblemSetBadges(problemSetId.value) - badges.value = res.data + badges.value = res } catch (err: any) { message.error("加载奖章列表失败:" + (err.data || "未知错误")) } @@ -79,7 +79,7 @@ async function loadBadges() { async function loadProgress() { try { const res = await getProblemSetProgress(problemSetId.value) - progress.value = res.data + progress.value = res } catch (err: any) { message.error("加载进度列表失败:" + (err.data || "未知错误")) } diff --git a/apps/web/src/admin/problemset/edit.vue b/apps/web/src/admin/problemset/edit.vue index 6d89b87..e20095c 100644 --- a/apps/web/src/admin/problemset/edit.vue +++ b/apps/web/src/admin/problemset/edit.vue @@ -45,7 +45,7 @@ async function loadProblemSetDetail() { try { const res = await getProblemSetDetail(problemSetId.value) - const data = res.data + const data = res formData.value = { id: data.id, title: data.title, diff --git a/apps/web/src/admin/problemset/list.vue b/apps/web/src/admin/problemset/list.vue index 5a9c656..5e2d231 100644 --- a/apps/web/src/admin/problemset/list.vue +++ b/apps/web/src/admin/problemset/list.vue @@ -123,8 +123,8 @@ async function listProblemSets() { query.difficulty, query.status, ) - total.value = res.data.total - problemSets.value = res.data.results + total.value = res.total + problemSets.value = res.results } async function toggleVisible(problemSetId: number) { diff --git a/apps/web/src/admin/setting/config.vue b/apps/web/src/admin/setting/config.vue index f4e9044..35ecdcd 100644 --- a/apps/web/src/admin/setting/config.vue +++ b/apps/web/src/admin/setting/config.vue @@ -129,14 +129,14 @@ const websiteConfig = reactive({ async function getWebsiteConfig() { const res = await getWebsite() - websiteConfig.websiteBaseUrl = res.data.websiteBaseUrl - websiteConfig.websiteName = res.data.websiteName - websiteConfig.websiteNameShortcut = res.data.websiteNameShortcut - websiteConfig.websiteFooter = res.data.websiteFooter - websiteConfig.allowRegister = res.data.allowRegister - websiteConfig.submissionListShowAll = res.data.submissionListShowAll - websiteConfig.classList = res.data.classList - websiteConfig.enableMaxkb = res.data.enableMaxkb + websiteConfig.websiteBaseUrl = res.websiteBaseUrl + websiteConfig.websiteName = res.websiteName + websiteConfig.websiteNameShortcut = res.websiteNameShortcut + websiteConfig.websiteFooter = res.websiteFooter + websiteConfig.allowRegister = res.allowRegister + websiteConfig.submissionListShowAll = res.submissionListShowAll + websiteConfig.classList = res.classList + websiteConfig.enableMaxkb = res.enableMaxkb } async function saveWebsiteConfig() { @@ -172,13 +172,13 @@ async function deleteTestcase(id?: string) { async function getTestcases() { const res = await listInvalidTestcases() - testcases.value = res.data + testcases.value = res } async function getJudgeServerData() { const res = await getJudgeServer() - token.value = res.data.token - servers.value = res.data.servers + token.value = res.token + servers.value = res.servers } async function delJudgeServer(hostname: string) { diff --git a/apps/web/src/admin/setting/home.vue b/apps/web/src/admin/setting/home.vue index 272195d..7330a67 100644 --- a/apps/web/src/admin/setting/home.vue +++ b/apps/web/src/admin/setting/home.vue @@ -64,16 +64,16 @@ const columns: DataTableColumn[] = [ onMounted(async () => { const res = await getBaseInfo() - userCount.value = res.data.userCount - submissionCount.value = res.data.todaySubmissionCount - contestCount.value = res.data.recentContestCount + userCount.value = res.userCount + submissionCount.value = res.todaySubmissionCount + contestCount.value = res.recentContestCount }) async function listRanks() { const offset = (query.page - 1) * query.limit const res = await getAdminUserRank(offset, query.limit, query.classroom) - data.value = res.data.results - total.value = res.data.total + data.value = res.results + total.value = res.total } function stopRolling() { @@ -108,7 +108,7 @@ function startRolling(finalName: string) { async function getRandom() { const res = await randomUser10(query.classroom) - const names = (res.data as string[]).map( + const names = (res as string[]).map( (name) => name.split(query.classroom)[1], ) rollingNames.value = names diff --git a/apps/web/src/admin/user/list.vue b/apps/web/src/admin/user/list.vue index f210236..e4bd0d7 100644 --- a/apps/web/src/admin/user/list.vue +++ b/apps/web/src/admin/user/list.vue @@ -129,8 +129,8 @@ async function listUsers() { query.keyword, query.orderBy, ) - total.value = res.data.total - users.value = res.data.results + total.value = res.total + users.value = res.results } function chooseUsers(rowKeys: DataTableRowKey[]) { @@ -144,10 +144,10 @@ async function onDeleteUsers(userIDs: DataTableRowKey[] | Ref) { async function onResetPassword(user: User) { const res = await resetPassword(user.id) - message.success(`【${user.username}】的密码已重置成【${res.data}】`) + message.success(`【${user.username}】的密码已重置成【${res}】`) users.value = users.value.map((it) => { if (it.id === user.id && user.adminType === USER_TYPE.REGULAR_USER) { - it.rawPassword = res.data + it.rawPassword = res } return it }) diff --git a/apps/web/src/oj/achievement/api.ts b/apps/web/src/oj/achievement/api.ts index 169aad4..873599d 100644 --- a/apps/web/src/oj/achievement/api.ts +++ b/apps/web/src/oj/achievement/api.ts @@ -1,4 +1,4 @@ -import api2 from "utils/api2" +import api from "utils/api" import type { AchievementList, AchievementSummary, @@ -6,21 +6,21 @@ import type { } from "utils/types" export function getAchievements(name?: string) { - return api2.get("achievements", { + return api.get("achievements", { params: name ? { username: name } : {}, }) } export function getAchievementSummary(name?: string) { - return api2.get("achievements/summary", { + return api.get("achievements/summary", { params: name ? { username: name } : {}, }) } export function getPendingAchievements() { - return api2.get("achievements/pending") + return api.get("achievements/pending") } export function markAchievementsRead(ids: number[]) { - return api2.post("achievements/pending/read", { ids }) + return api.post("achievements/pending/read", { ids }) } diff --git a/apps/web/src/oj/achievement/index.vue b/apps/web/src/oj/achievement/index.vue index f11d5e1..2fa8803 100644 --- a/apps/web/src/oj/achievement/index.vue +++ b/apps/web/src/oj/achievement/index.vue @@ -54,10 +54,9 @@ async function load() { getAchievementSummary(name.value), getUserBadges(name.value), ]) - // http 客户端返回 ApiResponse,真实载荷在 .data 里 - achievements.value = list.data.achievements - summary.value = sum.data - badges.value = badgeRes.data ?? [] + achievements.value = list.achievements + summary.value = sum + badges.value = badgeRes ?? [] } finally { loading.value = false } diff --git a/apps/web/src/oj/announcement/list.vue b/apps/web/src/oj/announcement/list.vue index d5c40e5..027dd29 100644 --- a/apps/web/src/oj/announcement/list.vue +++ b/apps/web/src/oj/announcement/list.vue @@ -56,15 +56,15 @@ async function showContent(announcement: Announcement) { const res = await getAnnouncement(announcement.id) toggleShow(true) title.value = announcement.title - content.value = res.data.content + content.value = res.content } const announcements = ref([]) async function listAnnouncements() { const offset = (query.page - 1) * query.limit const res = await getAnnouncementList(offset, query.limit) - total.value = res.data.total - announcements.value = res.data.results + total.value = res.total + announcements.value = res.results } onMounted(listAnnouncements) diff --git a/apps/web/src/oj/api.ts b/apps/web/src/oj/api.ts index c1cbeef..57a0039 100644 --- a/apps/web/src/oj/api.ts +++ b/apps/web/src/oj/api.ts @@ -37,7 +37,7 @@ import { type FlowchartStatistics, type SubmissionStatistics, } from "@oj2/contract" -import api2 from "utils/api2" +import api from "utils/api" import { filterResult } from "oj/transforms" import type { Announcement, @@ -65,7 +65,7 @@ function detailProblem(value: unknown): Problem { } export function getWebsiteConfig() { - return api2.get("site") + return api.get("site") } export async function getProblemList( @@ -73,17 +73,17 @@ export async function getProblemList( limit = 10, searchParams: Record = {}, ) { - const res = await api2.get("problems", { + const res = await api.get("problems", { params: { paging: true, offset, limit, ...searchParams }, }) return { - results: res.data.results.map(filterResult), - total: res.data.total, + results: res.results.map(filterResult), + total: res.total, } } export function getAuthors(all = false) { - return api2.get("problem-authors", { + return api.get("problem-authors", { params: { all: all ? "1" : "0" }, }) } @@ -92,27 +92,23 @@ export async function getProblem(problemID: string, contestID: string) { const endpoint = contestID ? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}` : `problems/${encodeURIComponent(problemID)}` - const response = await api2.get(endpoint) - return { error: null, data: detailProblem(response.data) } + return detailProblem(await api.get(endpoint)) } // 未登录返回 "0",登录后返回百分比字符串 export function getProblemBeatRate(problemID: number) { - return api2.get(`problems/${problemID}/beat-count`) + return api.get(`problems/${problemID}/beat-count`) } export async function getSubmission(id: string) { - const response = await api2.get( + const response = await api.get( `submissions/${encodeURIComponent(id)}`, ) - return { - error: null, - data: submissionDetailSchema.parse(response.data) as Submission, - } + return submissionDetailSchema.parse(response) as Submission } export function submitCode(data: SubmitCodePayload) { - return api2.post("submissions", data) + return api.post("submissions", data) } export function formatCode(data: { code: string; language: string }) { @@ -122,7 +118,7 @@ export function formatCode(data: { code: string; language: string }) { "C++": "cpp", SQL: "sql", } - return api2.post("code/format", { + return api.post("code/format", { code: data.code, language: languages[data.language] ?? data.language.toLowerCase(), }) @@ -134,22 +130,22 @@ export function getSubmissions(params: Partial) { : "submissions" // 契约里 language 是 z.string()(语言是配置项,随时可能加,收紧成枚举会让 // 新加的语言在后端 parse 时直接抛),前端在这一处收窄成 LANGUAGE - return api2.get<{ results: SubmissionListItem[]; total: number }>(endpoint, { + return api.get<{ results: SubmissionListItem[]; total: number }>(endpoint, { // contestId 走的是路径,page 只有前端分页器用 params: { ...params, contestId: undefined, page: undefined }, }) } export function getRankOfProblem(problemId: string) { - return api2.get(`problems/${encodeURIComponent(problemId)}/rank`) + return api.get(`problems/${encodeURIComponent(problemId)}/rank`) } export function getTodaySubmissionCount(language?: string) { - return api2.get("submissions/today-count", { params: { language } }) + return api.get("submissions/today-count", { params: { language } }) } export function adminRejudge(id: string) { - return api2.post<{ ok: boolean }>( + return api.post<{ ok: boolean }>( `submissions/${encodeURIComponent(id)}/rejudge`, ) } @@ -159,7 +155,7 @@ export function getSubmissionStatistics( problemID?: string, username?: string, ) { - return api2.get("submissions/statistics", { + return api.get("submissions/statistics", { params: { ...duration, problemId: problemID, username }, }) } @@ -169,17 +165,17 @@ export function getSubmissionStatistics( * 「全服 Top10」就是这个榜的第一页,取 limit=10 即可,不需要另一个上限参数。 */ export function getRank(offset: number, limit: number) { - return api2.get("rankings/users", { params: { offset, limit } }) + return api.get("rankings/users", { params: { offset, limit } }) } export function getActivityRank(start: string) { - return api2.get("rankings/activity", { + return api.get("rankings/activity", { params: { start }, }) } export function getClassRank(grade?: number | null) { - return api2.get("rankings/classes", { params: { grade } }) + return api.get("rankings/classes", { params: { grade } }) } export function getUserClassRank( @@ -187,7 +183,7 @@ export function getUserClassRank( offset?: number, limit?: number, ) { - return api2.get("me/class-rank", { + return api.get("me/class-rank", { params: { scope, offset, limit }, }) } @@ -197,7 +193,7 @@ export function getClassPK( startTime?: string, endTime?: string, ) { - return api2.post("classes/comparison", { + return api.post("classes/comparison", { classNames, ...(startTime ? { startTime } : {}), ...(endTime ? { endTime } : {}), @@ -211,20 +207,20 @@ export function getContestList(query: { status: string tag: string }) { - return api2.get("contests", { params: query }) + return api.get("contests", { params: query }) } export function getContest(id: string) { - return api2.get(`contests/${encodeURIComponent(id)}`) + return api.get(`contests/${encodeURIComponent(id)}`) } export function getContestAccess(id: string) { - return api2.get(`contests/${encodeURIComponent(id)}/access`) + return api.get(`contests/${encodeURIComponent(id)}/access`) } // 注意和 GET /access 不一样:这个返回裸 true,密码错是 403 走 catch export function checkContestPassword(contestID: string, password: string) { - return api2.post( + return api.post( `contests/${encodeURIComponent(contestID)}/access`, { password, @@ -233,10 +229,10 @@ export function checkContestPassword(contestID: string, password: string) { } export async function getContestProblems(contestID: string) { - const res = await api2.get( + const res = await api.get( `contests/${encodeURIComponent(contestID)}/problems`, ) - return res.data.map(filterResult) + return res.map(filterResult) } export function getContestRank( @@ -245,7 +241,7 @@ export function getContestRank( ) { // submissionInfo 在契约里是 Record(JSONB 原文), // 前端在这里收窄成 SubmissionInfo,见 utils/types 的 ContestRank - return api2.get<{ results: ContestRank[]; total: number }>( + return api.get<{ results: ContestRank[]; total: number }>( `contests/${encodeURIComponent(contestID)}/rank`, { params: query }, ) @@ -254,23 +250,23 @@ export function getContestRank( export function uploadAvatar(file: File) { const form = new window.FormData() form.append("image", file) - return api2.post("me/avatar", form, { + return api.post("me/avatar", form, { headers: { "content-type": "multipart/form-data" }, }) } export function updateProfile(data: { realName: string; mood: string }) { - return api2.put("me/profile", data) + return api.put("me/profile", data) } export function getAnnouncementList(offset = 0, limit = 10) { - return api2.get<{ results: Announcement[]; total: number }>("announcements", { + return api.get<{ results: Announcement[]; total: number }>("announcements", { params: { limit, offset }, }) } export function getAnnouncement(id: number) { - return api2.get(`announcements/${id}`) + return api.get(`announcements/${id}`) } export function createMessage(data: { @@ -278,7 +274,7 @@ export function createMessage(data: { message: string submission: string }) { - return api2.post("messages", { + return api.post("messages", { recipientId: data.recipient, message: data.message, submissionId: data.submission, @@ -287,33 +283,33 @@ export function createMessage(data: { export function getMessageList(offset = 0, limit = 10) { // language 的收窄同 getSubmissions,见那里的说明 - return api2.get<{ results: Message[]; total: number }>("messages", { + return api.get<{ results: Message[]; total: number }>("messages", { params: { limit, offset }, }) } export function getReaction(problemID: number) { - return api2.get(`problems/${problemID}/reaction`) + return api.get(`problems/${problemID}/reaction`) } export function setReaction(problemID: number, type: ReactionKey) { - return api2.post(`problems/${problemID}/reaction`, { type }) + return api.post(`problems/${problemID}/reaction`, { type }) } export function getMetrics(userid: number) { - return api2.get(`users/${userid}/metrics`) + return api.get(`users/${userid}/metrics`) } export function getTutorial(id: number) { - return api2.get(`tutorials/${id}`) + return api.get(`tutorials/${id}`) } export function getTutorials(type: "python" | "c") { - return api2.get("tutorials", { params: { type } }) + return api.get("tutorials", { params: { type } }) } export function getAIDetailData(start: string, end: string, username?: string) { - return api2.get("ai/detail", { params: { start, end, username } }) + return api.get("ai/detail", { params: { start, end, username } }) } export function getAIDurationData( @@ -321,40 +317,37 @@ export function getAIDurationData( duration: string, username?: string, ) { - return api2.get("ai/duration", { + return api.get("ai/duration", { params: { end, duration, username }, }) } export function getAIHeatmapData(username?: string) { - return api2.get("ai/heatmap", { + return api.get("ai/heatmap", { params: username ? { username } : {}, }) } export function getAILoginSummary() { - return api2.get("ai/login-summary") + return api.get("ai/login-summary") } export function getAIPinnedReport() { - return api2.get("ai/pinned") + return api.get("ai/pinned") } // ==================== 相似题目推荐 ==================== export function getSimilarProblems(problemId: string) { - return api2 + return api .get(`problems/${encodeURIComponent(problemId)}/similar`) - .then((response) => ({ - ...response, - data: response.data.map(filterResult), - })) + .then((response) => response.map(filterResult)) } export type { YearlyAc as YearlyACData } from "@oj2/contract" export function getProblemYearlyAC(problemId: string) { - return api2.get( + return api.get( `problems/${encodeURIComponent(problemId)}/yearly-ac`, ) } @@ -366,11 +359,11 @@ export function submitFlowchart(data: { mermaidCode: string flowchartData: Record // 压缩之后的,元数据太长了 }) { - return api2.post("flowcharts", data) + return api.post("flowcharts", data) } export function getFlowchartSubmission(id: string) { - return api2.get(`flowcharts/${encodeURIComponent(id)}`) + return api.get(`flowcharts/${encodeURIComponent(id)}`) } export function getFlowchartSubmissions(params: { @@ -382,7 +375,7 @@ export function getFlowchartSubmissions(params: { today?: string grade?: string }) { - return api2.get("flowcharts", { params }) + return api.get("flowcharts", { params }) } export function getFlowchartStatistics( @@ -390,23 +383,23 @@ export function getFlowchartStatistics( problemID?: string, username?: string, ) { - return api2.get("flowcharts/statistics", { + return api.get("flowcharts/statistics", { params: { ...duration, problemId: problemID, username }, }) } export function retryFlowchartSubmission(submissionId: string) { - return api2.post<{ status: string }>( + return api.post<{ status: string }>( `flowcharts/${encodeURIComponent(submissionId)}/retry`, ) } export function getCurrentProblemFlowchartSubmission(problemId: number) { - return api2.get(`problems/${problemId}/flowchart/current`) + return api.get(`problems/${problemId}/flowchart/current`) } export function getFlowchartSubmissionDetail(problemId: number, page = 0) { - return api2.get(`problems/${problemId}/flowchart/history`, { + return api.get(`problems/${problemId}/flowchart/history`, { params: { page }, }) } @@ -420,21 +413,21 @@ export function getProblemSetList( difficulty = "", status = "", ) { - return api2.get("problem-sets", { + return api.get("problem-sets", { params: { offset, limit, keyword, difficulty, status }, }) } export function getProblemSetDetail(id: number) { - return api2.get(`problem-sets/${id}`) + return api.get(`problem-sets/${id}`) } export function getProblemSetProblems(problemSetId: number) { - return api2.get(`problem-sets/${problemSetId}/problems`) + return api.get(`problem-sets/${problemSetId}/problems`) } export function joinProblemSet(problemSetId: number) { - return api2.post("problem-set-progress", { problemSetId }) + return api.post("problem-set-progress", { problemSetId }) } export function updateProblemSetProgress( @@ -442,7 +435,7 @@ export function updateProblemSetProgress( problemId: number, submissionId: string, ) { - return api2.put("problem-set-progress", { + return api.put("problem-set-progress", { problemSetId, problemId, submissionId, @@ -450,13 +443,13 @@ export function updateProblemSetProgress( } export function getUserBadges(username?: string) { - return api2.get( + return api.get( `users/${encodeURIComponent(username ?? "me")}/badges`, ) } export function getProblemSetBadges(problemSetId: number) { - return api2.get(`problem-sets/${problemSetId}/badges`) + return api.get(`problem-sets/${problemSetId}/badges`) } export function getProblemSetUserProgress( @@ -468,13 +461,12 @@ export function getProblemSetUserProgress( completionStatus?: "" | "completed" | "in_progress" | "not_started" }, ) { - return api2.get( + return api.get( `problem-sets/${problemSetId}/user-progress`, { params }, ) } -export async function getExercises(tutorialId: number): Promise { - const res = await api2.get(`tutorials/${tutorialId}/exercises`) - return res.data +export function getExercises(tutorialId: number): Promise { + return api.get(`tutorials/${tutorialId}/exercises`) } diff --git a/apps/web/src/oj/class/pk.vue b/apps/web/src/oj/class/pk.vue index 6b987f5..304c49f 100644 --- a/apps/web/src/oj/class/pk.vue +++ b/apps/web/src/oj/class/pk.vue @@ -122,8 +122,8 @@ async function compare() { const { startTime, endTime } = getTimeRange() const res = await getClassPK(selectedClasses.value, startTime, endTime) - comparisons.value = res.data.comparisons - hasTimeRange.value = res.data.hasTimeRange || false + comparisons.value = res.comparisons + hasTimeRange.value = res.hasTimeRange || false } catch (error) { message.error("获取数据失败") } finally { diff --git a/apps/web/src/oj/contest/list.vue b/apps/web/src/oj/contest/list.vue index 28d7b30..125bf52 100644 --- a/apps/web/src/oj/contest/list.vue +++ b/apps/web/src/oj/contest/list.vue @@ -93,8 +93,8 @@ async function listContests() { status: query.status, tag: query.tag, }) - data.value = res.data.results - total.value = res.data.total + data.value = res.results + total.value = res.total } function search(value: string) { diff --git a/apps/web/src/oj/contest/pages/rank.vue b/apps/web/src/oj/contest/pages/rank.vue index 2fb4443..afd842e 100644 --- a/apps/web/src/oj/contest/pages/rank.vue +++ b/apps/web/src/oj/contest/pages/rank.vue @@ -96,8 +96,8 @@ async function listRanks() { limit: query.limit, offset: query.limit * (query.page - 1), }) - total.value = res.data.total - data.value = res.data.results + total.value = res.total + data.value = res.results if (query.page === 1) { chart.value = data.value } @@ -225,7 +225,7 @@ async function downloadExcel() { limit: total.value || 10000, offset: 0, }) - const allRanks: ContestRank[] = res.data.results + const allRanks: ContestRank[] = res.results const rows = allRanks.map((rank, index) => { const rank1 = index + 1 diff --git a/apps/web/src/oj/learn/index.vue b/apps/web/src/oj/learn/index.vue index 6dc880f..97ae4a8 100644 --- a/apps/web/src/oj/learn/index.vue +++ b/apps/web/src/oj/learn/index.vue @@ -206,7 +206,7 @@ function goToNextLesson() { async function init() { const res1 = await getTutorials(type.value) - titles.value = res1.data + titles.value = res1 isEmpty.value = titles.value.length === 0 if (isEmpty.value) return const id = titles.value[step.value - 1].id @@ -214,7 +214,7 @@ async function init() { getTutorial(id), getExercises(id), ]) - if (res2.status === "fulfilled") tutorial.value = res2.value.data + if (res2.status === "fulfilled") tutorial.value = res2.value exercises.value = exs.status === "fulfilled" ? exs.value : [] learnStep.value[type.value] = step.value } diff --git a/apps/web/src/oj/problem/components/ProblemContent.vue b/apps/web/src/oj/problem/components/ProblemContent.vue index 3bf0384..fb45677 100644 --- a/apps/web/src/oj/problem/components/ProblemContent.vue +++ b/apps/web/src/oj/problem/components/ProblemContent.vue @@ -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 = [] } diff --git a/apps/web/src/oj/problem/components/ProblemInfo.vue b/apps/web/src/oj/problem/components/ProblemInfo.vue index 42d5113..02047c3 100644 --- a/apps/web/src/oj/problem/components/ProblemInfo.vue +++ b/apps/web/src/oj/problem/components/ProblemInfo.vue @@ -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(() => { diff --git a/apps/web/src/oj/problem/components/ProblemReaction.vue b/apps/web/src/oj/problem/components/ProblemReaction.vue index 4673c5f..fdaba67 100644 --- a/apps/web/src/oj/problem/components/ProblemReaction.vue +++ b/apps/web/src/oj/problem/components/ProblemReaction.vue @@ -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 { diff --git a/apps/web/src/oj/problem/components/ProblemSubmission.vue b/apps/web/src/oj/problem/components/ProblemSubmission.vue index 9627122..bb02a4c 100644 --- a/apps/web/src/oj/problem/components/ProblemSubmission.vue +++ b/apps/web/src/oj/problem/components/ProblemSubmission.vue @@ -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(() => { diff --git a/apps/web/src/oj/problem/components/SubmitCode.vue b/apps/web/src/oj/problem/components/SubmitCode.vue index 2192dfd..fd6f82b 100644 --- a/apps/web/src/oj/problem/components/SubmitCode.vue +++ b/apps/web/src/oj/problem/components/SubmitCode.vue @@ -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 diff --git a/apps/web/src/oj/problem/components/SubmitFlowchart.vue b/apps/web/src/oj/problem/components/SubmitFlowchart.vue index 26e7013..b6f3e41 100644 --- a/apps/web/src/oj/problem/components/SubmitFlowchart.vue +++ b/apps/web/src/oj/problem/components/SubmitFlowchart.vue @@ -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, ) diff --git a/apps/web/src/oj/problem/composables/useSubmissionMonitor.ts b/apps/web/src/oj/problem/composables/useSubmissionMonitor.ts index 832b380..3ff5632 100644 --- a/apps/web/src/oj/problem/composables/useSubmissionMonitor.ts +++ b/apps/web/src/oj/problem/composables/useSubmissionMonitor.ts @@ -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) }) diff --git a/apps/web/src/oj/problem/detail.vue b/apps/web/src/oj/problem/detail.vue index 3568b04..c8a20eb 100644 --- a/apps/web/src/oj/problem/detail.vue +++ b/apps/web/src/oj/problem/detail.vue @@ -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") { diff --git a/apps/web/src/oj/problem/list.vue b/apps/web/src/oj/problem/list.vue index 9f743da..a690ed2 100644 --- a/apps/web/src/oj/problem/list.vue +++ b/apps/web/src/oj/problem/list.vue @@ -81,7 +81,7 @@ async function listProblems() { async function listTags() { const res = await getProblemTagList() - tags.value = res.data.map((r: Omit) => ({ + tags.value = res.map((r: Omit) => ({ ...r, checked: query.tag === r.name, })) diff --git a/apps/web/src/oj/problemset/components/UserProgressView.vue b/apps/web/src/oj/problemset/components/UserProgressView.vue index 77ed79e..f3de6cf 100644 --- a/apps/web/src/oj/problemset/components/UserProgressView.vue +++ b/apps/web/src/oj/problemset/components/UserProgressView.vue @@ -56,15 +56,15 @@ async function loadUserProgress() { } const res = await getProblemSetUserProgress(problemSetId.value, params) - progress.value = res.data.results - total.value = res.data.total + progress.value = res.results + total.value = res.total // 使用后端返回的统计数据(基于所有数据) - if (res.data.statistics) { - statistics.value = res.data.statistics + if (res.statistics) { + statistics.value = res.statistics } // 保存所有题目信息 - if (res.data.problems) { - allProblems.value = res.data.problems + if (res.problems) { + allProblems.value = res.problems } loading.value = false } diff --git a/apps/web/src/oj/problemset/detail.vue b/apps/web/src/oj/problemset/detail.vue index 71994a3..d98a490 100644 --- a/apps/web/src/oj/problemset/detail.vue +++ b/apps/web/src/oj/problemset/detail.vue @@ -34,20 +34,20 @@ const activeTab = ref("problems") async function loadProblemSetDetail() { const res = await getProblemSetDetail(problemSetId.value) - problemSet.value = res.data - isJoined.value = res.data.userProgress?.isJoined || false + problemSet.value = res + isJoined.value = res.userProgress?.isJoined || false } async function loadProblems() { const res = await getProblemSetProblems(problemSetId.value) - problems.value = res.data + problems.value = res } async function loadUserBadges() { if (!isJoined.value) return const res = await getUserBadges() - userBadges.value = res.data.filter( + userBadges.value = res.filter( (badge: UserBadgeType) => badge.badge.problemsetId === problemSetId.value, ) } diff --git a/apps/web/src/oj/problemset/list.vue b/apps/web/src/oj/problemset/list.vue index 71be698..59f606c 100644 --- a/apps/web/src/oj/problemset/list.vue +++ b/apps/web/src/oj/problemset/list.vue @@ -55,8 +55,8 @@ async function listProblemSets() { query.difficulty, query.status, ) - total.value = res.data.total - problemSets.value = res.data.results + total.value = res.total + problemSets.value = res.results } function getDifficultyTag(difficulty: string) { diff --git a/apps/web/src/oj/rank/list.vue b/apps/web/src/oj/rank/list.vue index 87bcf5e..9c4436f 100644 --- a/apps/web/src/oj/rank/list.vue +++ b/apps/web/src/oj/rank/list.vue @@ -82,7 +82,7 @@ async function loadClassDetail(className: string) { classDetailData.value = null try { const res = await getClassPK([className]) - classDetailData.value = res.data.comparisons[0] ?? null + classDetailData.value = res.comparisons[0] ?? null } catch { // ignore } finally { @@ -151,10 +151,10 @@ async function analyzeSingleClassWithAI() { async function init() { const offset = (query.page - 1) * query.limit const res = await getRank(offset, query.limit) - data.value = res.data.results - total.value = res.data.total - me.value = res.data.me - return res.data.results + data.value = res.results + total.value = res.total + me.value = res.me + return res.results } function isMe(row: Rank) { @@ -256,7 +256,7 @@ async function listActivity() { const start = formatISO(sub(current, subOptions.value)) const res = await getActivityRank(start) // 活动榜只有「用户名 + 做题数」,塞进榜单图表复用的 Rank 形状里 - activityChart.value = res.data.map((d, index) => ({ + activityChart.value = res.map((d, index) => ({ id: index, user: { id: index, username: d.username, realName: null }, acceptedNumber: d.count, @@ -268,7 +268,7 @@ async function listActivity() { // 「全服 Top10」就是同一个榜的第一页 —— 上限由服务端定,这里只要前 10 条 async function listRank() { const res = await getRank(0, 10) - rankChart.value = res.data.results + rankChart.value = res.results } const options: SelectOption[] = [ @@ -439,7 +439,7 @@ async function listClassRank() { classQuery.grade = parseInt(className.slice(0, 2)) } const res = await getClassRank(classQuery.grade) - classData.value = res.data + classData.value = res } async function listMyClassRank() { @@ -450,10 +450,10 @@ async function listMyClassRank() { : 0 const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined const res = await getUserClassRank(myClassScope.value, offset, limit) - myRank.value = res.data.myRank - myClassName.value = res.data.className - myClassData.value = res.data.ranks - myClassTotal.value = res.data.total ?? res.data.ranks.length + myRank.value = res.myRank + myClassName.value = res.className + myClassData.value = res.ranks + myClassTotal.value = res.total ?? res.ranks.length if (myClassScope.value === "window") { myClassQuery.page = 1 } diff --git a/apps/web/src/oj/store/ai.ts b/apps/web/src/oj/store/ai.ts index e947deb..0f7df56 100644 --- a/apps/web/src/oj/store/ai.ts +++ b/apps/web/src/oj/store/ai.ts @@ -41,15 +41,15 @@ export const useAIStore = defineStore("ai", () => { end, targetUsername.value || undefined, ) - detailsData.start = res.data.start - detailsData.end = res.data.end - detailsData.solved = res.data.solved - detailsData.grade = res.data.grade - detailsData.className = res.data.className - detailsData.tags = res.data.tags - detailsData.difficulty = res.data.difficulty - detailsData.contestCount = res.data.contestCount - detailsData.flowcharts = res.data.flowcharts + detailsData.start = res.start + detailsData.end = res.end + detailsData.solved = res.solved + detailsData.grade = res.grade + detailsData.className = res.className + detailsData.tags = res.tags + detailsData.difficulty = res.difficulty + detailsData.contestCount = res.contestCount + detailsData.flowcharts = res.flowcharts } async function fetchDurationData(end: string, duration: string) { @@ -58,13 +58,13 @@ export const useAIStore = defineStore("ai", () => { duration, targetUsername.value || undefined, ) - durationData.value = res.data + durationData.value = res } async function fetchHeatmapData() { loading.heatmap = true const res = await getAIHeatmapData(targetUsername.value || undefined) - heatmapData.value = res.data + heatmapData.value = res loading.heatmap = false } @@ -165,7 +165,7 @@ export const useAIStore = defineStore("ai", () => { async function fetchPinnedReport() { const res = await getAIPinnedReport() - pinnedReport.value = res.data + pinnedReport.value = res } async function simulatePinnedStream() { diff --git a/apps/web/src/oj/store/contest.ts b/apps/web/src/oj/store/contest.ts index df19d59..4393ce8 100644 --- a/apps/web/src/oj/store/contest.ts +++ b/apps/web/src/oj/store/contest.ts @@ -58,9 +58,9 @@ export const useContestStore = defineStore("contest", () => { async function init(contestID: string) { problems.value = [] const res = await getContest(contestID) - contest.value = res.data + contest.value = res // now 是学生侧比赛专有的服务器时间,用来对齐倒计时 - now.value = getTime(parseISO(res.data.now ?? res.data.createTime)) + now.value = getTime(parseISO(res.now ?? res.createTime)) if (contestStatus.value !== ContestStatus.finished) { timer = setInterval(() => { now.value = now.value + 1000 @@ -68,7 +68,7 @@ export const useContestStore = defineStore("contest", () => { } if (contest.value?.contestType === ContestType.private) { const res = await getContestAccess(contestID) - toggleAccess(res.data.access) + toggleAccess(res.access) } _getProblems(contestID) } @@ -84,8 +84,8 @@ export const useContestStore = defineStore("contest", () => { async function checkPassword(contestID: string, password: string) { try { const res = await checkContestPassword(contestID, password) - toggleAccess(res.data) - if (res.data) { + toggleAccess(res) + if (res) { _getProblems(contestID) } } catch (err) { diff --git a/apps/web/src/oj/submission/components/FlowchartScoreDetail.vue b/apps/web/src/oj/submission/components/FlowchartScoreDetail.vue index 57816bb..6e242c9 100644 --- a/apps/web/src/oj/submission/components/FlowchartScoreDetail.vue +++ b/apps/web/src/oj/submission/components/FlowchartScoreDetail.vue @@ -175,7 +175,7 @@ async function loadSubmission() { try { const { getFlowchartSubmission } = await import("oj/api") const res = await getFlowchartSubmission(props.submissionId) - submission.value = res.data + submission.value = res // 渲染流程图 if (submission.value?.mermaidCode) { diff --git a/apps/web/src/oj/submission/detail.vue b/apps/web/src/oj/submission/detail.vue index 9e755a1..6d32baa 100644 --- a/apps/web/src/oj/submission/detail.vue +++ b/apps/web/src/oj/submission/detail.vue @@ -41,7 +41,7 @@ async function init() { if (submission.value) return loading.value = true const res = await getSubmission(props.submissionID) - submission.value = res.data + submission.value = res loading.value = false } diff --git a/apps/web/src/oj/submission/list.vue b/apps/web/src/oj/submission/list.vue index b6dab23..9b38c3f 100644 --- a/apps/web/src/oj/submission/list.vue +++ b/apps/web/src/oj/submission/list.vue @@ -109,8 +109,8 @@ async function listSubmissions() { today: query.today, grade: query.result, }) - total.value = res.data.total - flowcharts.value = res.data.results + total.value = res.total + flowcharts.value = res.results } else { const res = await getSubmissions({ ...query, @@ -120,14 +120,14 @@ async function listSubmissions() { language: query.language, today: query.today, }) - submissions.value = res.data.results - total.value = res.data.total + submissions.value = res.results + total.value = res.total } } async function getTodayCount() { const res = await getTodaySubmissionCount(query.language) - todayCount.value = res.data + todayCount.value = res } onMounted(() => { diff --git a/apps/web/src/oj/user/index.vue b/apps/web/src/oj/user/index.vue index 89da0c2..fcc80aa 100644 --- a/apps/web/src/oj/user/index.vue +++ b/apps/web/src/oj/user/index.vue @@ -63,10 +63,10 @@ async function init() { toggle(true) try { const res = await getProfile(route.query.name as string) - profile.value = res.data + profile.value = res // 用户不存在时后端返回 null,后面的统计全都无从算起 - if (!res.data) return - const acm = res.data.acmProblemsStatus.problems || {} + if (!res) return + const acm = res.acmProblemsStatus.problems || {} const ac: string[] = [] Object.keys(acm).forEach((id) => { if (acm[id]["status"] === 0) { @@ -76,17 +76,17 @@ async function init() { ac.sort() problems.value = ac - if (res.data.submissionNumber > 0) { - const metricsRes = await getMetrics(res.data.user.id) - firstSubmissionAt.value = parseTime(metricsRes.data.first) - latestSubmissionAt.value = parseTime(metricsRes.data.latest) + if (res.submissionNumber > 0) { + const metricsRes = await getMetrics(res.user.id) + firstSubmissionAt.value = parseTime(metricsRes.first) + latestSubmissionAt.value = parseTime(metricsRes.latest) toLatestAt.value = durationToDays( - metricsRes.data.latest, - metricsRes.data.now, + metricsRes.latest, + metricsRes.now, ) learnDuration.value = durationToDays( - metricsRes.data.first, - metricsRes.data.latest, + metricsRes.first, + metricsRes.latest, ) } } finally { @@ -101,7 +101,7 @@ async function loadAchievementSummary() { const res = await getAchievementSummary( (route.query.name as string) || undefined, ) - achievementSummary.value = res.data + achievementSummary.value = res } catch { achievementSummary.value = null } diff --git a/apps/web/src/oj/user/message.vue b/apps/web/src/oj/user/message.vue index b042b79..d0548c0 100644 --- a/apps/web/src/oj/user/message.vue +++ b/apps/web/src/oj/user/message.vue @@ -62,8 +62,8 @@ const query = reactive({ async function listMessages() { const offset = (query.page - 1) * query.limit const res = await getMessageList(offset, query.limit) - total.value = res.data.total - messages.value = res.data.results + total.value = res.total + messages.value = res.results } onMounted(listMessages) diff --git a/apps/web/src/shared/api.ts b/apps/web/src/shared/api.ts index 9d39083..cac9a32 100644 --- a/apps/web/src/shared/api.ts +++ b/apps/web/src/shared/api.ts @@ -1,10 +1,9 @@ import { userProfileSchema, type Quote } from "@oj2/contract" -import api2 from "utils/api2" -import type { ApiResponse } from "utils/api2" +import api from "utils/api" import type { Profile, Tag } from "utils/types" export function login(data: { username: string; password: string }) { - return api2.post("auth/login", data) + return api.post("auth/login", data) } export function signup(data: { @@ -12,37 +11,34 @@ export function signup(data: { email: string password: string }) { - return api2.post("users", data) + return api.post("users", data) } export function logout() { - return api2.delete("auth/session") + return api.delete("auth/session") } export async function getProfile( username: string = "", -): Promise> { - const response = await api2.get( +): Promise { + const response = await api.get( username ? `profiles/${encodeURIComponent(username)}` : "me", ) - if (response.data === null) return { error: null, data: null } + if (response === null) return null // 形状与契约一致,不再逐字段搬运;zod 解析仍保留,形状对不上要当场炸 - return { - error: null, - data: userProfileSchema.parse(response.data) as Profile, - } + return userProfileSchema.parse(response) as Profile } export function getProblemTagList() { - return api2.get("problem-tags") + return api.get("problem-tags") } export function getHitokoto() { - return api2.get("quotes/random") + return api.get("quotes/random") } export function getClassUsernames(classroom: string) { - return api2.get( + return api.get( `classes/${encodeURIComponent(classroom)}/usernames`, ) } diff --git a/apps/web/src/shared/components/AuthorSelect.vue b/apps/web/src/shared/components/AuthorSelect.vue index e6f5551..ff6df95 100644 --- a/apps/web/src/shared/components/AuthorSelect.vue +++ b/apps/web/src/shared/components/AuthorSelect.vue @@ -24,7 +24,7 @@ const authorOptions = ref([{ label: "全部", value: "" }]) async function getAuthorOptions() { authorOptions.value = [{ label: "全部", value: "" }] const res = await getAuthors(all) - const remotes = res.data.map((item) => ({ + const remotes = res.map((item) => ({ label: `${item.username} (${item.problemCount})`, value: item.username, })) diff --git a/apps/web/src/shared/components/FlowchartStatisticsPanel.vue b/apps/web/src/shared/components/FlowchartStatisticsPanel.vue index 641aba6..b110438 100644 --- a/apps/web/src/shared/components/FlowchartStatisticsPanel.vue +++ b/apps/web/src/shared/components/FlowchartStatisticsPanel.vue @@ -533,7 +533,7 @@ async function handleStatistics() { query.problem, query.username, ) - Object.assign(data, res.data) + Object.assign(data, res) await nextTick() renderWordCloud() } diff --git a/apps/web/src/shared/components/Hitokoto.vue b/apps/web/src/shared/components/Hitokoto.vue index d3ab4a0..145fb4b 100644 --- a/apps/web/src/shared/components/Hitokoto.vue +++ b/apps/web/src/shared/components/Hitokoto.vue @@ -10,7 +10,7 @@ async function receive() { try { const res = await getHitokoto() // 契约是 string | Record —— 一言数据集不在仓库里,形状留了余地 - const quote = res.data + const quote = res if (typeof quote === "string") { hitokoto.sentence = quote hitokoto.from = "" diff --git a/apps/web/src/shared/components/Login.vue b/apps/web/src/shared/components/Login.vue index 46039a2..edaae34 100644 --- a/apps/web/src/shared/components/Login.vue +++ b/apps/web/src/shared/components/Login.vue @@ -80,7 +80,7 @@ async function loadClassUsernames(selectedClass: string) { classUserLoading.value = true try { const res = await getClassUsernames(selectedClass) - classUserOptions.value = res.data.map((name: string) => ({ + classUserOptions.value = res.map((name: string) => ({ label: name, value: name, })) diff --git a/apps/web/src/shared/components/StatisticsPanel.vue b/apps/web/src/shared/components/StatisticsPanel.vue index 4c102eb..2cf9ed0 100644 --- a/apps/web/src/shared/components/StatisticsPanel.vue +++ b/apps/web/src/shared/components/StatisticsPanel.vue @@ -421,13 +421,15 @@ async function handleStatistics() { query.problem, query.username, ) - count.total = res.data.submissionCount - count.accepted = res.data.acceptedCount - count.rate = res.data.correctRate - list.value = res.data.data - listUnaccepted.value = res.data.dataUnaccepted - person.count = res.data.personCount - person.rate = res.data.personRate + count.total = res.submissionCount + count.accepted = res.acceptedCount + count.rate = res.correctRate + // 这里的 res.data 是载荷**自己**的 data 字段(每个学生一行), + // 不是原来那层信封 —— 契约 submissionStatisticsSchema 就是这么定的 + list.value = res.data + listUnaccepted.value = res.dataUnaccepted + person.count = res.personCount + person.rate = res.personRate } function rowKey(row: SubmissionStatisticsUser): DataTableRowKey { diff --git a/apps/web/src/shared/store/achievement.ts b/apps/web/src/shared/store/achievement.ts index a563dc9..82a3df8 100644 --- a/apps/web/src/shared/store/achievement.ts +++ b/apps/web/src/shared/store/achievement.ts @@ -36,9 +36,8 @@ export const useAchievementStore = defineStore("achievement", () => { async function fetchPending() { try { - // http 客户端返回 ApiResponse,真实载荷在 .data 里 const res = await getPendingAchievements() - enqueue(res.data ?? []) + enqueue(res ?? []) } catch { // 拉取失败静默处理,下次路由切换会再拉 } diff --git a/apps/web/src/shared/store/config.ts b/apps/web/src/shared/store/config.ts index eae5aa8..faff9b2 100644 --- a/apps/web/src/shared/store/config.ts +++ b/apps/web/src/shared/store/config.ts @@ -14,8 +14,8 @@ export const useConfigStore = defineStore("config", () => { }) async function getConfig() { const res = await getWebsiteConfig() - config.value = res.data - document.title = res.data.websiteName + config.value = res + document.title = res.websiteName } return { config, diff --git a/apps/web/src/shared/store/loginSummary.ts b/apps/web/src/shared/store/loginSummary.ts index 1a5ff05..90a7d0c 100644 --- a/apps/web/src/shared/store/loginSummary.ts +++ b/apps/web/src/shared/store/loginSummary.ts @@ -31,9 +31,9 @@ export const useLoginSummaryStore = defineStore("loginSummary", () => { analysisError.value = "" try { const res = await getAILoginSummary() - summary.value = res.data.summary - analysis.value = res.data.analysis || "" - analysisError.value = res.data.analysisError || "" + summary.value = res.summary + analysis.value = res.analysis || "" + analysisError.value = res.analysisError || "" } catch (err) { analysisError.value = "获取登录统计失败,请稍后再试" } finally { diff --git a/apps/web/src/shared/store/user.ts b/apps/web/src/shared/store/user.ts index 4936dad..8bceba4 100644 --- a/apps/web/src/shared/store/user.ts +++ b/apps/web/src/shared/store/user.ts @@ -63,7 +63,7 @@ export const useUserStore = defineStore("user", () => { async function getMyProfile() { isFinished.value = false const res = await getProfile() - profile.value = res.data + profile.value = res isFinished.value = true storage.set(STORAGE_KEY.AUTHED, !!user.value?.email) } diff --git a/apps/web/src/utils/api2.ts b/apps/web/src/utils/api.ts similarity index 61% rename from apps/web/src/utils/api2.ts rename to apps/web/src/utils/api.ts index 01c8940..5e6e541 100644 --- a/apps/web/src/utils/api2.ts +++ b/apps/web/src/utils/api.ts @@ -6,33 +6,25 @@ import storage from "./storage" const { message: toast } = createDiscreteApi(["message"]) -// 后端统一返回 { error, data } 信封;拦截器剥掉 axios 外层后, -// 调用方拿到的就是这个信封,data 才是真正的业务数据。 -export interface ApiResponse { - error: string | null - data: T -} - -interface Api2Error { +interface ApiError { error?: { code?: string message?: string } } -interface Api2Client { - get(url: string, config?: AxiosRequestConfig): Promise> - post( - url: string, - data?: unknown, - config?: AxiosRequestConfig, - ): Promise> - put( - url: string, - data?: unknown, - config?: AxiosRequestConfig, - ): Promise> - delete(url: string, config?: AxiosRequestConfig): Promise> +/** + * 成功时直接拿到业务数据本身。后端成功响应是 `{ data }`(见 apps/api/src/http.ts + * 的 success),拦截器把 axios 外层和这层信封一起剥掉。 + * + * 失败走 reject,形状是 `{ error: 错误码, data: 文案 }` —— 和成功路径不对称是 + * 故意的:成功没有错误码可言。分支处理一律判 `err.error` 里的错误码。 + */ +interface ApiClient { + get(url: string, config?: AxiosRequestConfig): Promise + post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise + put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise + delete(url: string, config?: AxiosRequestConfig): Promise } const instance = axios.create({ @@ -52,16 +44,13 @@ instance.interceptors.request.use((config) => { }) instance.interceptors.response.use( - // 这里**故意**不返回 AxiosResponse:把 { data } 信封剥掉,让调用方直接拿到 - // ApiResponse。类型上和 axios 的拦截器签名对不上(它期望原样返回响应), - // 文件末尾的 `as unknown as Api2Client` 就是为了把这个真实形状交出去。 + // 这里**故意**不返回 AxiosResponse:把 axios 的外层和后端的 { data } 信封一起 + // 剥掉,让调用方直接拿到业务数据。类型上和 axios 的拦截器签名对不上(它期望原样 + // 返回响应),文件末尾的 `as unknown as ApiClient` 就是为了把真实形状交出去。 ((response: AxiosResponse) => - Promise.resolve({ - error: null, - data: response.data.data, - })) as unknown as (response: AxiosResponse) => AxiosResponse, + response.data.data) as unknown as (response: AxiosResponse) => AxiosResponse, (error) => { - const payload = error.response?.data as Api2Error | undefined + const payload = error.response?.data as ApiError | undefined const code = payload?.error?.code ?? "network-error" const message = payload?.error?.message ?? "Request failed" @@ -85,4 +74,4 @@ instance.interceptors.response.use( }, ) -export default instance as unknown as Api2Client +export default instance as unknown as ApiClient diff --git a/apps/web/src/utils/download.ts b/apps/web/src/utils/download.ts index 3db8ef8..8859724 100644 --- a/apps/web/src/utils/download.ts +++ b/apps/web/src/utils/download.ts @@ -1,7 +1,7 @@ import axios from "axios" // 指向新后端的 /api/admin。响应是 zip 二进制,不走 { error, data } 信封, -// 所以不能复用 utils/api2 的拦截器(它会把 response.data.data 取出来)。 +// 所以不能复用 utils/api 的拦截器(它会把 response.data.data 取出来)。 const http = axios.create({ baseURL: "/api/admin", responseType: "blob",