refactor(前端): 去掉 { error, data } 信封,api2 改名 api
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:
2026-08-25 22:42:20 -06:00
parent b35118f32e
commit 0b7b08f2cc
64 changed files with 391 additions and 427 deletions

View File

@@ -41,7 +41,7 @@ export const requireAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
* 后台接口的角色守卫,对应旧后端 `account/decorators.py` 的四个装饰器。 * 后台接口的角色守卫,对应旧后端 `account/decorators.py` 的四个装饰器。
* *
* 未登录一律 401 `login-required`、登录但角色不够一律 403 `permission-denied` * 未登录一律 401 `login-required`、登录但角色不够一律 403 `permission-denied`
* 与旧 `BasePermissionDecorator._permission_error` 的两分支一致 —— 前端 `utils/api2.ts` * 与旧 `BasePermissionDecorator._permission_error` 的两分支一致 —— 前端 `utils/api.ts`
* 的拦截器就是按这两个 code 分别弹登录框和弹提示的。禁用账号走第三个码,见 denied()。 * 的拦截器就是按这两个 code 分别弹登录框和弹提示的。禁用账号走第三个码,见 denied()。
*/ */
function requireRole( function requireRole(

View File

@@ -56,7 +56,7 @@ watch(
if (!show) return if (!show) return
if (!metrics.value.length) { if (!metrics.value.length) {
const res = await getMetricOptions() const res = await getMetricOptions()
metrics.value = res.data metrics.value = res
} }
if (props.editing) { if (props.editing) {
form.value = { ...emptyForm(), ...props.editing } form.value = { ...emptyForm(), ...props.editing }

View File

@@ -30,7 +30,7 @@ async function load() {
loading.value = true loading.value = true
try { try {
const res = await getAdminAchievements() const res = await getAdminAchievements()
list.value = res.data list.value = res
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -144,7 +144,7 @@ const columns: DataTableColumn<ReportItem>[] = [
async function loadPinnedReports() { async function loadPinnedReports() {
const res = await getPinnedAIReports() const res = await getPinnedAIReports()
pinnedReports.value = res.data.results pinnedReports.value = res.results
} }
async function togglePin(row: ReportItem) { async function togglePin(row: ReportItem) {
@@ -155,8 +155,8 @@ async function togglePin(row: ReportItem) {
async function listReports() { async function listReports() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getAIReportList(offset, query.limit, query.username) const res = await getAIReportList(offset, query.limit, query.username)
reports.value = res.data.results reports.value = res.results
total.value = res.data.total total.value = res.total
} }
async function openDetail(id: number) { async function openDetail(id: number) {
@@ -165,7 +165,7 @@ async function openDetail(id: number) {
detail.value = null detail.value = null
try { try {
const res = await getAIReportDetail(id) const res = await getAIReportDetail(id)
detail.value = res.data detail.value = res
} finally { } finally {
loadingDetail.value = false loadingDetail.value = false
} }

View File

@@ -36,11 +36,11 @@ async function init() {
const res = await getAnnouncement(id) const res = await getAnnouncement(id)
toggleReady(true) toggleReady(true)
announcement.id = id announcement.id = id
announcement.title = res.data.title announcement.title = res.title
announcement.content = res.data.content announcement.content = res.content
announcement.visible = res.data.visible announcement.visible = res.visible
announcement.tag = res.data.tag announcement.tag = res.tag
announcement.top = res.data.top announcement.top = res.top
} }
async function submit() { async function submit() {

View File

@@ -67,7 +67,7 @@ const columns: DataTableColumn<AnnouncementListItem>[] = [
async function toggleVisible(announcement: AnnouncementListItem) { async function toggleVisible(announcement: AnnouncementListItem) {
const next = !announcement.visible const next = !announcement.visible
try { try {
const { data: full } = await getAnnouncement(announcement.id) const full = await getAnnouncement(announcement.id)
await editAnnouncement({ await editAnnouncement({
id: full.id, id: full.id,
title: full.title, title: full.title,
@@ -85,8 +85,8 @@ async function toggleVisible(announcement: AnnouncementListItem) {
async function listAnnouncements() { async function listAnnouncements() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getAnnouncementList(offset, query.limit) const res = await getAnnouncementList(offset, query.limit)
announcements.value = res.data.results announcements.value = res.results
total.value = res.data.total total.value = res.total
} }
onMounted(listAnnouncements) onMounted(listAnnouncements)

View File

@@ -1,4 +1,4 @@
import api2 from "utils/api2" import api from "utils/api"
import { toProblemListItem } from "admin/transforms" import { toProblemListItem } from "admin/transforms"
import type { import type {
AcTrend, AcTrend,
@@ -42,11 +42,11 @@ import type {
} from "utils/types" } from "utils/types"
export function getBaseInfo() { export function getBaseInfo() {
return api2.get<DashboardInfo>("admin/dashboard") return api.get<DashboardInfo>("admin/dashboard")
} }
export function randomUser10(classroom: string) { export function randomUser10(classroom: string) {
return api2.get<string[]>("admin/random-usernames", { return api.get<string[]>("admin/random-usernames", {
params: { classroom }, params: { classroom },
}) })
} }
@@ -62,69 +62,69 @@ export async function getProblemList(
const endpoint = contestID const endpoint = contestID
? `admin/contests/${contestID}/problems` ? `admin/contests/${contestID}/problems`
: "admin/problems" : "admin/problems"
const res = await api2.get<AdminProblemList>(endpoint, { const res = await api.get<AdminProblemList>(endpoint, {
params: { offset, limit, keyword, author, tagId }, params: { offset, limit, keyword, author, tagId },
}) })
return { return {
results: res.data.results.map(toProblemListItem), results: res.results.map(toProblemListItem),
total: res.data.total, total: res.total,
} }
} }
export function deleteProblem(id: number) { export function deleteProblem(id: number) {
return api2.delete(`admin/problems/${id}`) return api.delete(`admin/problems/${id}`)
} }
// 比赛题与公开题共用一条删除路由,比赛由后端从题目推导 // 比赛题与公开题共用一条删除路由,比赛由后端从题目推导
export function deleteContestProblem(id: number) { export function deleteContestProblem(id: number) {
return api2.delete(`admin/problems/${id}`) return api.delete(`admin/problems/${id}`)
} }
export function editProblem(problem: AdminProblem | BlankProblem) { export function editProblem(problem: AdminProblem | BlankProblem) {
return api2.put<AdminProblem>( return api.put<AdminProblem>(
`admin/problems/${(problem as AdminProblem).id}`, `admin/problems/${(problem as AdminProblem).id}`,
toProblemBody(problem), toProblemBody(problem),
) )
} }
export function toggleProblemVisible(problemID: number) { export function toggleProblemVisible(problemID: number) {
return api2.put<{ visible: boolean }>( return api.put<{ visible: boolean }>(
`admin/problems/${problemID}/visibility`, `admin/problems/${problemID}/visibility`,
) )
} }
export function generateFlowchartFromPythonCode(python: string) { export function generateFlowchartFromPythonCode(python: string) {
return api2.post<{ flowchart: string }>("admin/problems/flowchart", { return api.post<{ flowchart: string }>("admin/problems/flowchart", {
python, python,
}) })
} }
export function editContestProblem(problem: AdminProblem | BlankProblem) { export function editContestProblem(problem: AdminProblem | BlankProblem) {
return api2.put<AdminProblem>( return api.put<AdminProblem>(
`admin/problems/${(problem as AdminProblem).id}`, `admin/problems/${(problem as AdminProblem).id}`,
toProblemBody(problem), toProblemBody(problem),
) )
} }
export function getProblem(id: string | number) { export function getProblem(id: string | number) {
return api2.get<AdminProblem>(`admin/problems/${id}`) return api.get<AdminProblem>(`admin/problems/${id}`)
} }
export function getContestProblem(id: number) { export function getContestProblem(id: number) {
return api2.get<AdminProblem>(`admin/problems/${id}`) return api.get<AdminProblem>(`admin/problems/${id}`)
} }
// 标签管理 // 标签管理
export function getTagAdminList(keyword = "") { export function getTagAdminList(keyword = "") {
return api2.get<AdminTag[]>("admin/problem-tags", { params: { keyword } }) return api.get<AdminTag[]>("admin/problem-tags", { params: { keyword } })
} }
export function renameTag(id: number, name: string) { export function renameTag(id: number, name: string) {
return api2.put<RenameTagResponse>(`admin/problem-tags/${id}`, { name }) return api.put<RenameTagResponse>(`admin/problem-tags/${id}`, { name })
} }
export function deleteTag(id: number) { export function deleteTag(id: number) {
return api2.delete(`admin/problem-tags/${id}`) return api.delete(`admin/problem-tags/${id}`)
} }
export function batchTagProblems( export function batchTagProblems(
@@ -132,7 +132,7 @@ export function batchTagProblems(
tagNames: string[], tagNames: string[],
action: "add" | "remove", action: "add" | "remove",
) { ) {
return api2.post<BatchProblemTagResponse>("admin/problems/batch-tag", { return api.post<BatchProblemTagResponse>("admin/problems/batch-tag", {
problemIds, problemIds,
tagNames, tagNames,
action, action,
@@ -141,7 +141,7 @@ export function batchTagProblems(
// 用户排名(后台版,无 100 名上限;公开榜单是 oj/api.ts 的 getRank // 用户排名(后台版,无 100 名上限;公开榜单是 oj/api.ts 的 getRank
export function getAdminUserRank(offset: number, limit: number, keyword: string) { export function getAdminUserRank(offset: number, limit: number, keyword: string) {
return api2.get<AdminUserRank>("admin/rankings/users", { return api.get<AdminUserRank>("admin/rankings/users", {
params: { offset, limit, keyword }, params: { offset, limit, keyword },
}) })
} }
@@ -154,7 +154,7 @@ export function getUserList(
keyword: string, keyword: string,
orderBy = "", orderBy = "",
) { ) {
return api2.get<AdminUserList>("admin/users", { return api.get<AdminUserList>("admin/users", {
// 旧接口的 order_by 只有 "-last_login" 一个取值 // 旧接口的 order_by 只有 "-last_login" 一个取值
params: { params: {
offset, offset,
@@ -168,7 +168,7 @@ export function getUserList(
// 编辑用户 // 编辑用户
export function editUser(user: User) { export function editUser(user: User) {
return api2.put<AdminUser>(`admin/users/${user.id}`, { return api.put<AdminUser>(`admin/users/${user.id}`, {
username: user.username, username: user.username,
email: user.email, email: user.email,
adminType: user.adminType, adminType: user.adminType,
@@ -180,27 +180,26 @@ export function editUser(user: User) {
}) })
} }
// 重置用户密码。调用方直接用 res.data 当密码字符串(旧后端返回的就是裸字符串) // 重置用户密码,返回新密码
// 新后端返回 { password },在这里解包,组件不动
export async function resetPassword(userID: number) { 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`, `admin/users/${userID}/reset-password`,
) )
return { error: res.error, data: res.data.password } return res.password
} }
// 导入用户 // 导入用户
export function importUsers(users: string[][]) { export function importUsers(users: string[][]) {
return api2.post("admin/users", { users }) return api.post("admin/users", { users })
} }
// 批量删除用户 // 批量删除用户
export function deleteUsers(userIDs: number[]) { 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) { export function getContestList(offset = 0, limit = 10, keyword: string) {
return api2.get<AdminContestList>("admin/contests", { return api.get<AdminContestList>("admin/contests", {
params: { offset, limit, keyword }, params: { offset, limit, keyword },
}) })
} }
@@ -209,14 +208,14 @@ export function getContestList(offset = 0, limit = 10, keyword: string) {
export async function uploadImage(file: File): Promise<string> { export async function uploadImage(file: File): Promise<string> {
const form = new window.FormData() const form = new window.FormData()
form.append("image", file) form.append("image", file)
const res = await api2.post<{ const res = await api.post<{
success: boolean success: boolean
filePath: string filePath: string
msg: string msg: string
}>("admin/upload-image", form, { }>("admin/upload-image", form, {
headers: { "content-type": "multipart/form-data" }, headers: { "content-type": "multipart/form-data" },
}) })
return res.data.success ? res.data.filePath : "" return res.success ? res.filePath : ""
} }
// 上传测试用例SQL 题的压缩包是 1.sql..N.sql每个文件一个测试点的建表+数据脚本) // 上传测试用例SQL 题的压缩包是 1.sql..N.sql每个文件一个测试点的建表+数据脚本)
@@ -226,7 +225,7 @@ export function uploadTestcases(file: File, options: { sql?: boolean } = {}) {
if (options.sql) { if (options.sql) {
form.append("sql", "1") form.append("sql", "1")
} }
return api2.post<TestcaseUploadedReturns>("admin/test-cases", form, { return api.post<TestcaseUploadedReturns>("admin/test-cases", form, {
headers: { "content-type": "multipart/form-data" }, headers: { "content-type": "multipart/form-data" },
}) })
} }
@@ -237,12 +236,12 @@ export function previewSQLTestcase(data: {
refSql: string refSql: string
mode: "query" | "modify" mode: "query" | "modify"
}) { }) {
return api2.post<SqlDisplay>("admin/sql-test-cases/preview", data) return api.post<SqlDisplay>("admin/sql-test-cases/preview", data)
} }
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序) // 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
export function getSQLTestcaseScripts(problemId: number) { export function getSQLTestcaseScripts(problemId: number) {
return api2.get<SqlTestCaseScript[]>( return api.get<SqlTestCaseScript[]>(
`admin/problems/${problemId}/sql-scripts`, `admin/problems/${problemId}/sql-scripts`,
) )
} }
@@ -252,7 +251,7 @@ export function generateSQLTestcase(data: {
refSql: string refSql: string
mode: "query" | "modify" mode: "query" | "modify"
}) { }) {
return api2.post<GenerateSqlTestCaseResponse>( return api.post<GenerateSqlTestCaseResponse>(
"admin/sql-test-cases/generate", "admin/sql-test-cases/generate",
data, data,
) )
@@ -292,12 +291,12 @@ function toProblemBody(problem: AdminProblem | BlankProblem) {
} }
export function createProblem(problem: BlankProblem) { export function createProblem(problem: BlankProblem) {
return api2.post<AdminProblem>("admin/problems", toProblemBody(problem)) return api.post<AdminProblem>("admin/problems", toProblemBody(problem))
} }
export function createContestProblem(problem: BlankProblem) { export function createContestProblem(problem: BlankProblem) {
// contestId 由 detail.vue 在提交前写进 problem 对象 // contestId 由 detail.vue 在提交前写进 problem 对象
return api2.post<AdminProblem>( return api.post<AdminProblem>(
`admin/contests/${problem.contestId}/problems`, `admin/contests/${problem.contestId}/problems`,
toProblemBody(problem), toProblemBody(problem),
) )
@@ -318,22 +317,22 @@ function toContestBody(contest: Contest | BlankContest) {
} }
export function createContest(contest: BlankContest) { export function createContest(contest: BlankContest) {
return api2.post<Contest>("admin/contests", toContestBody(contest)) return api.post<Contest>("admin/contests", toContestBody(contest))
} }
export function editContest(contest: Contest | BlankContest) { export function editContest(contest: Contest | BlankContest) {
return api2.put<Contest>( return api.put<Contest>(
`admin/contests/${(contest as Contest).id}`, `admin/contests/${(contest as Contest).id}`,
toContestBody(contest), toContestBody(contest),
) )
} }
export function cloneContest(contestId: number) { export function cloneContest(contestId: number) {
return api2.post<Contest>(`admin/contests/${contestId}/clone`) return api.post<Contest>(`admin/contests/${contestId}/clone`)
} }
export function getContest(id: string) { export function getContest(id: string) {
return api2.get<Contest>(`admin/contests/${id}`) return api.get<Contest>(`admin/contests/${id}`)
} }
export function addProblemForContest( export function addProblemForContest(
@@ -341,71 +340,67 @@ export function addProblemForContest(
problemID: number, problemID: number,
displayID: string, displayID: string,
) { ) {
return api2.post<AdminProblem>( return api.post<AdminProblem>(
`admin/contests/${contestID}/problems/from-public`, `admin/contests/${contestID}/problems/from-public`,
{ problemId: problemID, displayId: displayID }, { problemId: problemID, displayId: displayID },
) )
} }
export function getWebsite() { export function getWebsite() {
return api2.get<WebsiteConfig>("admin/website") return api.get<WebsiteConfig>("admin/website")
} }
export function editWebsite(data: WebsiteConfig) { export function editWebsite(data: WebsiteConfig) {
return api2.post<WebsiteConfig>("admin/website", data) return api.post<WebsiteConfig>("admin/website", data)
} }
export function listInvalidTestcases() { export function listInvalidTestcases() {
return api2.get<OrphanTestCase[]>("admin/orphan-test-cases") return api.get<OrphanTestCase[]>("admin/orphan-test-cases")
} }
export function pruneInvalidTestcases(id?: string) { 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() { export function getJudgeServer() {
return api2.get<JudgeServerList>("admin/judge-servers") return api.get<JudgeServerList>("admin/judge-servers")
} }
export function deleteJudgeServer(hostname: string) { 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) { export function getAnnouncementList(offset = 0, limit = 10) {
return api2.get<{ results: AnnouncementListItem[]; total: number }>( return api.get<{ results: AnnouncementListItem[]; total: number }>(
"admin/announcements", "admin/announcements",
{ params: { offset, limit } }, { params: { offset, limit } },
) )
} }
export function getAnnouncement(id: number) { export function getAnnouncement(id: number) {
return api2.get<Announcement>(`admin/announcements/${id}`) return api.get<Announcement>(`admin/announcements/${id}`)
} }
export function deleteAnnouncement(id: number) { export function deleteAnnouncement(id: number) {
return api2.delete(`admin/announcements/${id}`) return api.delete(`admin/announcements/${id}`)
} }
export function editAnnouncement(announcement: AnnouncementEdit) { export function editAnnouncement(announcement: AnnouncementEdit) {
const { id, ...body } = announcement const { id, ...body } = announcement
return api2.put<Announcement>(`admin/announcements/${id}`, body) return api.put<Announcement>(`admin/announcements/${id}`, body)
} }
export function createAnnouncement(announcement: AnnouncementEdit) { export function createAnnouncement(announcement: AnnouncementEdit) {
const { id: _id, ...body } = announcement const { id: _id, ...body } = announcement
return api2.post<Announcement>("admin/announcements", body) return api.post<Announcement>("admin/announcements", body)
} }
export async function getTutorialList() { export function getTutorialList() {
const res = await api2.get<{ [key: string]: TutorialListItem[] }>( return api.get<{ [key: string]: TutorialListItem[] }>("admin/tutorials")
"admin/tutorials",
)
return res.data
} }
export async function getTutorial(id: number) { export function getTutorial(id: number) {
const res = await api2.get<Tutorial>(`admin/tutorials/${id}`) return api.get<Tutorial>(`admin/tutorials/${id}`)
return res.data
} }
function toTutorialBody(data: Partial<Tutorial>) { function toTutorialBody(data: Partial<Tutorial>) {
@@ -419,32 +414,27 @@ function toTutorialBody(data: Partial<Tutorial>) {
} }
} }
export async function createTutorial(data: Partial<Tutorial>) { export function createTutorial(data: Partial<Tutorial>) {
const res = await api2.post<Tutorial>("admin/tutorials", toTutorialBody(data)) return api.post<Tutorial>("admin/tutorials", toTutorialBody(data))
return res.data
} }
export async function updateTutorial(data: Partial<Tutorial>) { export function updateTutorial(data: Partial<Tutorial>) {
const res = await api2.put<Tutorial>( return api.put<Tutorial>(
`admin/tutorials/${data.id}`, `admin/tutorials/${data.id}`,
toTutorialBody(data), toTutorialBody(data),
) )
return res.data
} }
export function deleteTutorial(id: number) { export function deleteTutorial(id: number) {
return api2.delete(`admin/tutorials/${id}`) return api.delete(`admin/tutorials/${id}`)
} }
export function setTutorialVisibility(id: number, isPublic: boolean) { export function setTutorialVisibility(id: number, isPublic: boolean) {
return api2.put<Tutorial>(`admin/tutorials/${id}/visibility`, { isPublic }) return api.put<Tutorial>(`admin/tutorials/${id}/visibility`, { isPublic })
} }
export async function getAdminExercises(tutorialId: number) { export function getAdminExercises(tutorialId: number) {
const res = await api2.get<Exercise[]>( return api.get<Exercise[]>(`admin/tutorials/${tutorialId}/exercises`)
`admin/tutorials/${tutorialId}/exercises`,
)
return res.data
} }
export async function createExercise(data: { export async function createExercise(data: {
@@ -453,8 +443,7 @@ export async function createExercise(data: {
data: object data: object
order: number order: number
}) { }) {
const res = await api2.post<Exercise>("admin/exercises", data) return api.post<Exercise>("admin/exercises", data)
return res.data
} }
export async function updateExercise(data: { export async function updateExercise(data: {
@@ -463,21 +452,20 @@ export async function updateExercise(data: {
data: object data: object
order: number order: number
}) { }) {
const res = await api2.put<Exercise>(`admin/exercises/${data.id}`, { return api.put<Exercise>(`admin/exercises/${data.id}`, {
type: data.type, type: data.type,
data: data.data, data: data.data,
order: data.order, order: data.order,
}) })
return res.data
} }
export function deleteExercise(id: number) { export function deleteExercise(id: number) {
return api2.delete(`admin/exercises/${id}`) return api.delete(`admin/exercises/${id}`)
} }
// 将竞赛题目转为公开题目 // 将竞赛题目转为公开题目
export function makeProblemPublic(id: number, displayId: string) { export function makeProblemPublic(id: number, displayId: string) {
return api2.post<AdminProblem>(`admin/problems/${id}/make-public`, { return api.post<AdminProblem>(`admin/problems/${id}/make-public`, {
displayId, displayId,
}) })
} }
@@ -486,7 +474,7 @@ export function makeProblemPublic(id: number, displayId: string) {
export function getACMHelperList(contestId: number) { export function getACMHelperList(contestId: number) {
// acInfo 在契约里是 Record<string, unknown>acm_contest_rank 的 JSONB 原文), // acInfo 在契约里是 Record<string, unknown>acm_contest_rank 的 JSONB 原文),
// 组件侧按 SubmissionInfo 读,收窄放在这里 // 组件侧按 SubmissionInfo 读,收窄放在这里
return api2.get< return api.get<
Array<Omit<AcmHelperItem, "acInfo"> & { acInfo: SubmissionInfo }> Array<Omit<AcmHelperItem, "acInfo"> & { acInfo: SubmissionInfo }>
>(`admin/contests/${contestId}/acm-helper`) >(`admin/contests/${contestId}/acm-helper`)
} }
@@ -497,7 +485,7 @@ export function updateACMHelperChecked(
problem_id: string, problem_id: string,
checked: boolean, checked: boolean,
) { ) {
return api2.put(`admin/contests/${contest_id}/acm-helper`, { return api.put(`admin/contests/${contest_id}/acm-helper`, {
rankId: rank_id, rankId: rank_id,
problemId: problem_id, problemId: problem_id,
checked, checked,
@@ -512,13 +500,13 @@ export function getProblemSetList(
difficulty = "", difficulty = "",
status = "", status = "",
) { ) {
return api2.get<ProblemSetList>("admin/problem-sets", { return api.get<ProblemSetList>("admin/problem-sets", {
params: { offset, limit, keyword, difficulty, status }, params: { offset, limit, keyword, difficulty, status },
}) })
} }
export function getProblemSetDetail(id: number) { export function getProblemSetDetail(id: number) {
return api2.get<ProblemSet>(`admin/problem-sets/${id}`) return api.get<ProblemSet>(`admin/problem-sets/${id}`)
} }
interface ProblemSetBody { interface ProblemSetBody {
@@ -543,31 +531,31 @@ function toProblemSetBody(data: ProblemSetBody) {
} }
export function createProblemSet(data: ProblemSetBody) { export function createProblemSet(data: ProblemSetBody) {
return api2.post<ProblemSet>("admin/problem-sets", toProblemSetBody(data)) return api.post<ProblemSet>("admin/problem-sets", toProblemSetBody(data))
} }
export function editProblemSet(data: ProblemSetBody & { id: number }) { export function editProblemSet(data: ProblemSetBody & { id: number }) {
return api2.put<ProblemSet>( return api.put<ProblemSet>(
`admin/problem-sets/${data.id}`, `admin/problem-sets/${data.id}`,
toProblemSetBody(data), toProblemSetBody(data),
) )
} }
export function deleteProblemSet(id: number) { export function deleteProblemSet(id: number) {
return api2.delete(`admin/problem-sets/${id}`) return api.delete(`admin/problem-sets/${id}`)
} }
export function toggleProblemSetVisible(id: number) { export function toggleProblemSetVisible(id: number) {
return api2.put<ProblemSet>(`admin/problem-sets/${id}/visibility`) return api.put<ProblemSet>(`admin/problem-sets/${id}/visibility`)
} }
export function updateProblemSetStatus(id: number, status: string) { export function updateProblemSetStatus(id: number, status: string) {
return api2.put<ProblemSet>(`admin/problem-sets/${id}/status`, { status }) return api.put<ProblemSet>(`admin/problem-sets/${id}/status`, { status })
} }
// 题单题目管理 API // 题单题目管理 API
export function getProblemSetProblems(problemSetId: number) { export function getProblemSetProblems(problemSetId: number) {
return api2.get<ProblemSetProblem[]>( return api.get<ProblemSetProblem[]>(
`admin/problem-sets/${problemSetId}/problems`, `admin/problem-sets/${problemSetId}/problems`,
) )
} }
@@ -582,7 +570,7 @@ export function addProblemToSet(
hint?: string hint?: string
}, },
) { ) {
return api2.post(`admin/problem-sets/${problemSetId}/problems`, { return api.post(`admin/problem-sets/${problemSetId}/problems`, {
problemId: data.problemId, problemId: data.problemId,
order: data.order ?? 0, order: data.order ?? 0,
isRequired: data.isRequired ?? true, isRequired: data.isRequired ?? true,
@@ -601,7 +589,7 @@ export function editProblemInSet(
hint?: string hint?: string
}, },
) { ) {
return api2.put( return api.put(
`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`, `admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`,
data, data,
) )
@@ -611,14 +599,14 @@ export function removeProblemFromSet(
problemSetId: number, problemSetId: number,
problemSetProblemId: number, problemSetProblemId: number,
) { ) {
return api2.delete( return api.delete(
`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`, `admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`,
) )
} }
// 题单奖章管理 API // 题单奖章管理 API
export function getProblemSetBadges(problemSetId: number) { export function getProblemSetBadges(problemSetId: number) {
return api2.get<ProblemSetBadge[]>( return api.get<ProblemSetBadge[]>(
`admin/problem-sets/${problemSetId}/badges`, `admin/problem-sets/${problemSetId}/badges`,
) )
} }
@@ -642,7 +630,7 @@ function toBadgeBody(data: BadgeBody) {
} }
export function createProblemSetBadge(problemSetId: number, data: BadgeBody) { export function createProblemSetBadge(problemSetId: number, data: BadgeBody) {
return api2.post<ProblemSetBadge>( return api.post<ProblemSetBadge>(
`admin/problem-sets/${problemSetId}/badges`, `admin/problem-sets/${problemSetId}/badges`,
toBadgeBody(data), toBadgeBody(data),
) )
@@ -653,31 +641,31 @@ export function editProblemSetBadge(
badgeId: number, badgeId: number,
data: BadgeBody, data: BadgeBody,
) { ) {
return api2.put<ProblemSetBadge>( return api.put<ProblemSetBadge>(
`admin/problem-sets/${problemSetId}/badges/${badgeId}`, `admin/problem-sets/${problemSetId}/badges/${badgeId}`,
toBadgeBody(data), toBadgeBody(data),
) )
} }
export function deleteProblemSetBadge(problemSetId: number, badgeId: number) { 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 // 题单进度管理 API
// 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同 // 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同
export function getProblemSetProgress(problemSetId: number) { export function getProblemSetProgress(problemSetId: number) {
return api2.get<AdminProblemSetProgress[]>( return api.get<AdminProblemSetProgress[]>(
`admin/problem-sets/${problemSetId}/progress`, `admin/problem-sets/${problemSetId}/progress`,
) )
} }
export function removeUserFromProblemSet(problemSetId: number, userId: number) { 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() { export function getStuckProblems() {
return api2.get<StuckProblem[]>("admin/problem-analytics/stuck") return api.get<StuckProblem[]>("admin/problem-analytics/stuck")
} }
export function getTopACTrend(params: { export function getTopACTrend(params: {
@@ -685,26 +673,26 @@ export function getTopACTrend(params: {
untilYear: number untilYear: number
minPerYear: number minPerYear: number
}) { }) {
return api2.get<AcTrend[]>("admin/problem-analytics/ac-trend", { params }) return api.get<AcTrend[]>("admin/problem-analytics/ac-trend", { params })
} }
// AI 学习分析报告 // AI 学习分析报告
export function getAIReportList(offset = 0, limit = 10, username = "") { export function getAIReportList(offset = 0, limit = 10, username = "") {
return api2.get<AdminAiReportList>("admin/ai/reports", { return api.get<AdminAiReportList>("admin/ai/reports", {
params: { offset, limit, username: username || undefined }, params: { offset, limit, username: username || undefined },
}) })
} }
export function getAIReportDetail(id: number) { export function getAIReportDetail(id: number) {
return api2.get<AdminAiReport>(`admin/ai/reports/${id}`) return api.get<AdminAiReport>(`admin/ai/reports/${id}`)
} }
export function pinAIReport(id: number) { 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() { export function getPinnedAIReports() {
return api2.get<AdminAiReportList>("admin/ai/reports", { return api.get<AdminAiReportList>("admin/ai/reports", {
params: { pinnedOnly: "true" }, params: { pinnedOnly: "true" },
}) })
} }
@@ -734,27 +722,27 @@ function toAchievementBody(data: Partial<AdminAchievement>) {
} }
export function getAdminAchievements() { export function getAdminAchievements() {
return api2.get<AdminAchievement[]>("admin/achievements") return api.get<AdminAchievement[]>("admin/achievements")
} }
export function getMetricOptions() { export function getMetricOptions() {
return api2.get<MetricOption[]>("admin/achievement-metrics") return api.get<MetricOption[]>("admin/achievement-metrics")
} }
export function createAchievement(data: Partial<AdminAchievement>) { export function createAchievement(data: Partial<AdminAchievement>) {
return api2.post<AdminAchievement>( return api.post<AdminAchievement>(
"admin/achievements", "admin/achievements",
toAchievementBody(data), toAchievementBody(data),
) )
} }
export function updateAchievement(data: Partial<AdminAchievement>) { export function updateAchievement(data: Partial<AdminAchievement>) {
return api2.put<AdminAchievement>( return api.put<AdminAchievement>(
`admin/achievements/${data.id}`, `admin/achievements/${data.id}`,
toAchievementBody(data), toAchievementBody(data),
) )
} }
export function deleteAchievement(id: number) { export function deleteAchievement(id: number) {
return api2.delete(`admin/achievements/${id}`) return api.delete(`admin/achievements/${id}`)
} }

View File

@@ -36,7 +36,7 @@ async function clone() {
message.success("复制成功") message.success("复制成功")
router.push({ router.push({
name: "admin contest edit", name: "admin contest edit",
params: { contestID: res.data.id }, params: { contestID: res.id },
}) })
} catch { } catch {
message.error("复制失败") message.error("复制失败")

View File

@@ -69,7 +69,7 @@ async function getContestDetail() {
toggleReady(true) toggleReady(true)
return return
} }
const { data } = await getContest(props.contestID) const data = await getContest(props.contestID)
toggleReady(true) toggleReady(true)
contest.id = data.id contest.id = data.id
contest.title = data.title contest.title = data.title

View File

@@ -142,18 +142,18 @@ async function viewSubmission(item: HelperItem) {
limit: 1, limit: 1,
}) })
if (res.data.results.length === 0) { if (res.results.length === 0) {
message.warning("未找到该用户的 AC 提交") message.warning("未找到该用户的 AC 提交")
return return
} }
// 获取提交详情 // 获取提交详情
const submissionListItem = res.data.results[0] const submissionListItem = res.results[0]
const detailRes = await getSubmission(submissionListItem.id) const detailRes = await getSubmission(submissionListItem.id)
// 手动添加 contest 字段ACM模式下后端不返回此字段 // 手动添加 contest 字段ACM模式下后端不返回此字段
currentSubmission.value = { currentSubmission.value = {
...detailRes.data, ...detailRes,
contest: Number(props.contestID), contest: Number(props.contestID),
problem_display_id: item.problemDisplayId, problem_display_id: item.problemDisplayId,
} }
@@ -169,10 +169,10 @@ async function loadData() {
try { try {
// 先获取比赛信息,获取开始时间 // 先获取比赛信息,获取开始时间
const contestRes = await getContest(props.contestID) const contestRes = await getContest(props.contestID)
contestStartTime.value = new Date(contestRes.data.startTime) contestStartTime.value = new Date(contestRes.startTime)
// 再获取 AC 提交列表 // 再获取 AC 提交列表
const { data } = await getACMHelperList(Number(props.contestID)) const data = await getACMHelperList(Number(props.contestID))
submissions.value = data submissions.value = data
} catch (err: any) { } catch (err: any) {
message.error(err.data || "加载失败") message.error(err.data || "加载失败")

View File

@@ -87,8 +87,8 @@ const columns: DataTableColumn<Contest>[] = [
async function listContests() { async function listContests() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getContestList(offset, query.limit, query.keyword) const res = await getContestList(offset, query.limit, query.keyword)
contests.value = res.data.results contests.value = res.results
total.value = res.data.total total.value = res.total
} }
onMounted(listContests) onMounted(listContests)
watch(() => [query.page, query.limit], listContests) watch(() => [query.page, query.limit], listContests)

View File

@@ -29,7 +29,7 @@ const columns: DataTableColumn<StuckProblem>[] = [
onMounted(async () => { onMounted(async () => {
try { try {
const res = await getStuckProblems() const res = await getStuckProblems()
data.value = res.data data.value = res
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -121,7 +121,7 @@ async function fetchData() {
untilYear: untilYear.value, untilYear: untilYear.value,
minPerYear: minPerYear.value, minPerYear: minPerYear.value,
}) })
data.value = res.data data.value = res
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -41,7 +41,7 @@ function toggleTag(name: string) {
async function listTags() { async function listTags() {
const res = await getTagAdminList() const res = await getTagAdminList()
tags.value = res.data tags.value = res
} }
function close() { function close() {
@@ -60,7 +60,7 @@ async function submit() {
) )
const verb = props.action === "add" ? "添加" : "移除" const verb = props.action === "add" ? "添加" : "移除"
message.success( message.success(
`已为 ${res.data.problemCount} 道题${verb} ${res.data.tagCount} 个标签`, `已为 ${res.problemCount} 道题${verb} ${res.tagCount} 个标签`,
) )
close() close()
emit("done") emit("done")

View File

@@ -78,8 +78,8 @@ onMounted(async () => {
if (!props.problemId) return if (!props.problemId) return
try { try {
const res = await getSQLTestcaseScripts(props.problemId) const res = await getSQLTestcaseScripts(props.problemId)
if (res.data.length) { if (res.length) {
scripts.value = res.data.map((f) => ({ ...blankEntry(), sql: f.content })) scripts.value = res.map((f) => ({ ...blankEntry(), sql: f.content }))
} }
} catch (err: any) { } catch (err: any) {
// 新题、以及旧格式(非 SQL测试点后端回 404/409保持空白就是对的不该报错。 // 新题、以及旧格式(非 SQL测试点后端回 404/409保持空白就是对的不该报错。
@@ -128,7 +128,7 @@ async function generate() {
refSql: refSQL.value, refSql: refSQL.value,
mode: props.mode, mode: props.mode,
}) })
s.sql = res.data.sql s.sql = res.sql
} catch (err) { } catch (err) {
const data = (err as { data?: unknown })?.data const data = (err as { data?: unknown })?.data
message.error(typeof data === "string" ? data : "AI 生成失败") message.error(typeof data === "string" ? data : "AI 生成失败")
@@ -158,7 +158,7 @@ async function preview() {
refSql: refSQL.value, refSql: refSQL.value,
mode: props.mode, mode: props.mode,
}) })
s.display = res.data s.display = res
} catch (err) { } catch (err) {
const data = (err as { data?: unknown })?.data const data = (err as { data?: unknown })?.data
s.error = typeof data === "string" ? data : "预览失败" s.error = typeof data === "string" ? data : "预览失败"
@@ -182,7 +182,7 @@ async function upload() {
const res = await uploadTestcases(file, { sql: true }) const res = await uploadTestcases(file, { sql: true })
// score 不在上传响应里,是这里按测试点数量平分补上的(余数给最后一个) // score 不在上传响应里,是这里按测试点数量平分补上的(余数给最后一个)
const entries = res.data.info const entries = res.info
const baseScore = Math.floor(100 / entries.length) const baseScore = Math.floor(100 / entries.length)
const remainder = 100 - baseScore * entries.length const remainder = 100 - baseScore * entries.length
const testcases: Testcase[] = entries.map((entry, i) => ({ 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("上传成功") message.success("上传成功")
} catch { } catch {
message.error("上传失败") message.error("上传失败")

View File

@@ -170,7 +170,7 @@ async function upload() {
const res = await uploadTestcases(file) const res = await uploadTestcases(file)
// score 不在上传响应里,是这里按测试点数量平分补上的(余数给最后一个) // score 不在上传响应里,是这里按测试点数量平分补上的(余数给最后一个)
const entries = res.data.info const entries = res.info
const baseScore = Math.floor(100 / entries.length) const baseScore = Math.floor(100 / entries.length)
const remainder = 100 - baseScore * entries.length const remainder = 100 - baseScore * entries.length
const testcases: Testcase[] = entries.map((entry, i) => ({ 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("上传成功") message.success("上传成功")
} catch { } catch {
message.error("上传失败") message.error("上传失败")

View File

@@ -208,7 +208,7 @@ async function getProblemDetail() {
return return
} }
try { 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._id = data._id problem.value._id = data._id
problem.value.title = data.title problem.value.title = data.title
@@ -269,7 +269,7 @@ async function getProblemDetail() {
async function getTagList() { async function getTagList() {
const res = await getProblemTagList() const res = await getProblemTagList()
tagList.value = res.data tagList.value = res
tagListLoaded.value = true tagListLoaded.value = true
syncTagInputsFromProblemTags() syncTagInputsFromProblemTags()
} }
@@ -295,13 +295,13 @@ async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
return return
} }
// score 不在上传响应里,前端按测试点数量平分补上 // score 不在上传响应里,前端按测试点数量平分补上
const entries = res.data.info const entries = res.info
const testcases: Testcase[] = entries.map((entry) => ({ const testcases: Testcase[] = entries.map((entry) => ({
...entry, ...entry,
score: (100 / entries.length).toFixed(0), score: (100 / entries.length).toFixed(0),
})) }))
problem.value.testCaseScore = testcases problem.value.testCaseScore = testcases
problem.value.testCaseId = res.data.id problem.value.testCaseId = res.id
} catch (err) { } catch (err) {
message.error("上传测试用例失败") message.error("上传测试用例失败")
} }
@@ -495,7 +495,7 @@ async function generateMermaid() {
) )
isAIGenerating.value = false isAIGenerating.value = false
message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法") message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法")
problem.value.mermaidCode = res.data.flowchart problem.value.mermaidCode = res.flowchart
} }
const showGeneratorModal = ref(false) const showGeneratorModal = ref(false)

View File

@@ -97,7 +97,7 @@ const columns: DataTableColumn<AdminTag>[] = [
async function listTags() { async function listTags() {
const res = await getTagAdminList(keyword.value) const res = await getTagAdminList(keyword.value)
tags.value = res.data tags.value = res
} }
function startEdit(tag: AdminTag) { function startEdit(tag: AdminTag) {
@@ -121,9 +121,9 @@ async function saveTag(tag: AdminTag) {
return return
} }
const res = await renameTag(tag.id, name) const res = await renameTag(tag.id, name)
if (res.data.merged) { if (res.merged) {
message.success( message.success(
`已合并到「${res.data.name}」,影响 ${res.data.affectedCount} 道题`, `已合并到「${res.name}」,影响 ${res.affectedCount} 道题`,
) )
} else { } else {
message.success("已重命名") message.success("已重命名")

View File

@@ -52,7 +52,7 @@ const editingBadge = ref<ProblemSetBadge | null>(null)
async function loadProblemSetDetail() { async function loadProblemSetDetail() {
try { try {
const res = await getProblemSetDetail(problemSetId.value) const res = await getProblemSetDetail(problemSetId.value)
problemSet.value = res.data problemSet.value = res
} catch (err: any) { } catch (err: any) {
message.error("加载题单详情失败:" + (err.data || "未知错误")) message.error("加载题单详情失败:" + (err.data || "未知错误"))
} }
@@ -61,7 +61,7 @@ async function loadProblemSetDetail() {
async function loadProblems() { async function loadProblems() {
try { try {
const res = await getProblemSetProblems(problemSetId.value) const res = await getProblemSetProblems(problemSetId.value)
problems.value = res.data problems.value = res
} catch (err: any) { } catch (err: any) {
message.error("加载题目列表失败:" + (err.data || "未知错误")) message.error("加载题目列表失败:" + (err.data || "未知错误"))
} }
@@ -70,7 +70,7 @@ async function loadProblems() {
async function loadBadges() { async function loadBadges() {
try { try {
const res = await getProblemSetBadges(problemSetId.value) const res = await getProblemSetBadges(problemSetId.value)
badges.value = res.data badges.value = res
} catch (err: any) { } catch (err: any) {
message.error("加载奖章列表失败:" + (err.data || "未知错误")) message.error("加载奖章列表失败:" + (err.data || "未知错误"))
} }
@@ -79,7 +79,7 @@ async function loadBadges() {
async function loadProgress() { async function loadProgress() {
try { try {
const res = await getProblemSetProgress(problemSetId.value) const res = await getProblemSetProgress(problemSetId.value)
progress.value = res.data progress.value = res
} catch (err: any) { } catch (err: any) {
message.error("加载进度列表失败:" + (err.data || "未知错误")) message.error("加载进度列表失败:" + (err.data || "未知错误"))
} }

View File

@@ -45,7 +45,7 @@ async function loadProblemSetDetail() {
try { try {
const res = await getProblemSetDetail(problemSetId.value) const res = await getProblemSetDetail(problemSetId.value)
const data = res.data const data = res
formData.value = { formData.value = {
id: data.id, id: data.id,
title: data.title, title: data.title,

View File

@@ -123,8 +123,8 @@ async function listProblemSets() {
query.difficulty, query.difficulty,
query.status, query.status,
) )
total.value = res.data.total total.value = res.total
problemSets.value = res.data.results problemSets.value = res.results
} }
async function toggleVisible(problemSetId: number) { async function toggleVisible(problemSetId: number) {

View File

@@ -129,14 +129,14 @@ const websiteConfig = reactive<WebsiteConfig>({
async function getWebsiteConfig() { async function getWebsiteConfig() {
const res = await getWebsite() const res = await getWebsite()
websiteConfig.websiteBaseUrl = res.data.websiteBaseUrl websiteConfig.websiteBaseUrl = res.websiteBaseUrl
websiteConfig.websiteName = res.data.websiteName websiteConfig.websiteName = res.websiteName
websiteConfig.websiteNameShortcut = res.data.websiteNameShortcut websiteConfig.websiteNameShortcut = res.websiteNameShortcut
websiteConfig.websiteFooter = res.data.websiteFooter websiteConfig.websiteFooter = res.websiteFooter
websiteConfig.allowRegister = res.data.allowRegister websiteConfig.allowRegister = res.allowRegister
websiteConfig.submissionListShowAll = res.data.submissionListShowAll websiteConfig.submissionListShowAll = res.submissionListShowAll
websiteConfig.classList = res.data.classList websiteConfig.classList = res.classList
websiteConfig.enableMaxkb = res.data.enableMaxkb websiteConfig.enableMaxkb = res.enableMaxkb
} }
async function saveWebsiteConfig() { async function saveWebsiteConfig() {
@@ -172,13 +172,13 @@ async function deleteTestcase(id?: string) {
async function getTestcases() { async function getTestcases() {
const res = await listInvalidTestcases() const res = await listInvalidTestcases()
testcases.value = res.data testcases.value = res
} }
async function getJudgeServerData() { async function getJudgeServerData() {
const res = await getJudgeServer() const res = await getJudgeServer()
token.value = res.data.token token.value = res.token
servers.value = res.data.servers servers.value = res.servers
} }
async function delJudgeServer(hostname: string) { async function delJudgeServer(hostname: string) {

View File

@@ -64,16 +64,16 @@ const columns: DataTableColumn<Rank>[] = [
onMounted(async () => { onMounted(async () => {
const res = await getBaseInfo() const res = await getBaseInfo()
userCount.value = res.data.userCount userCount.value = res.userCount
submissionCount.value = res.data.todaySubmissionCount submissionCount.value = res.todaySubmissionCount
contestCount.value = res.data.recentContestCount contestCount.value = res.recentContestCount
}) })
async function listRanks() { async function listRanks() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getAdminUserRank(offset, query.limit, query.classroom) const res = await getAdminUserRank(offset, query.limit, query.classroom)
data.value = res.data.results data.value = res.results
total.value = res.data.total total.value = res.total
} }
function stopRolling() { function stopRolling() {
@@ -108,7 +108,7 @@ function startRolling(finalName: string) {
async function getRandom() { async function getRandom() {
const res = await randomUser10(query.classroom) 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], (name) => name.split(query.classroom)[1],
) )
rollingNames.value = names rollingNames.value = names

View File

@@ -129,8 +129,8 @@ async function listUsers() {
query.keyword, query.keyword,
query.orderBy, query.orderBy,
) )
total.value = res.data.total total.value = res.total
users.value = res.data.results users.value = res.results
} }
function chooseUsers(rowKeys: DataTableRowKey[]) { function chooseUsers(rowKeys: DataTableRowKey[]) {
@@ -144,10 +144,10 @@ async function onDeleteUsers(userIDs: DataTableRowKey[] | Ref<number[]>) {
async function onResetPassword(user: User) { async function onResetPassword(user: User) {
const res = await resetPassword(user.id) const res = await resetPassword(user.id)
message.success(`${user.username}】的密码已重置成【${res.data}`) message.success(`${user.username}】的密码已重置成【${res}`)
users.value = users.value.map((it) => { users.value = users.value.map((it) => {
if (it.id === user.id && user.adminType === USER_TYPE.REGULAR_USER) { if (it.id === user.id && user.adminType === USER_TYPE.REGULAR_USER) {
it.rawPassword = res.data it.rawPassword = res
} }
return it return it
}) })

View File

@@ -1,4 +1,4 @@
import api2 from "utils/api2" import api from "utils/api"
import type { import type {
AchievementList, AchievementList,
AchievementSummary, AchievementSummary,
@@ -6,21 +6,21 @@ import type {
} from "utils/types" } from "utils/types"
export function getAchievements(name?: string) { export function getAchievements(name?: string) {
return api2.get<AchievementList>("achievements", { return api.get<AchievementList>("achievements", {
params: name ? { username: name } : {}, params: name ? { username: name } : {},
}) })
} }
export function getAchievementSummary(name?: string) { export function getAchievementSummary(name?: string) {
return api2.get<AchievementSummary>("achievements/summary", { return api.get<AchievementSummary>("achievements/summary", {
params: name ? { username: name } : {}, params: name ? { username: name } : {},
}) })
} }
export function getPendingAchievements() { export function getPendingAchievements() {
return api2.get<PendingAchievement[]>("achievements/pending") return api.get<PendingAchievement[]>("achievements/pending")
} }
export function markAchievementsRead(ids: number[]) { export function markAchievementsRead(ids: number[]) {
return api2.post("achievements/pending/read", { ids }) return api.post("achievements/pending/read", { ids })
} }

View File

@@ -54,10 +54,9 @@ async function load() {
getAchievementSummary(name.value), getAchievementSummary(name.value),
getUserBadges(name.value), getUserBadges(name.value),
]) ])
// http 客户端返回 ApiResponse<T>,真实载荷在 .data 里 achievements.value = list.achievements
achievements.value = list.data.achievements summary.value = sum
summary.value = sum.data badges.value = badgeRes ?? []
badges.value = badgeRes.data ?? []
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -56,15 +56,15 @@ async function showContent(announcement: Announcement) {
const res = await getAnnouncement(announcement.id) const res = await getAnnouncement(announcement.id)
toggleShow(true) toggleShow(true)
title.value = announcement.title title.value = announcement.title
content.value = res.data.content content.value = res.content
} }
const announcements = ref<Announcement[]>([]) const announcements = ref<Announcement[]>([])
async function listAnnouncements() { async function listAnnouncements() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getAnnouncementList(offset, query.limit) const res = await getAnnouncementList(offset, query.limit)
total.value = res.data.total total.value = res.total
announcements.value = res.data.results announcements.value = res.results
} }
onMounted(listAnnouncements) onMounted(listAnnouncements)

View File

@@ -37,7 +37,7 @@ import {
type FlowchartStatistics, type FlowchartStatistics,
type SubmissionStatistics, type SubmissionStatistics,
} from "@oj2/contract" } from "@oj2/contract"
import api2 from "utils/api2" import api from "utils/api"
import { filterResult } from "oj/transforms" import { filterResult } from "oj/transforms"
import type { import type {
Announcement, Announcement,
@@ -65,7 +65,7 @@ function detailProblem(value: unknown): Problem {
} }
export function getWebsiteConfig() { export function getWebsiteConfig() {
return api2.get<WebsiteConfig>("site") return api.get<WebsiteConfig>("site")
} }
export async function getProblemList( export async function getProblemList(
@@ -73,17 +73,17 @@ export async function getProblemList(
limit = 10, limit = 10,
searchParams: Record<string, unknown> = {}, searchParams: Record<string, unknown> = {},
) { ) {
const res = await api2.get<ProblemList>("problems", { const res = await api.get<ProblemList>("problems", {
params: { paging: true, offset, limit, ...searchParams }, params: { paging: true, offset, limit, ...searchParams },
}) })
return { return {
results: res.data.results.map(filterResult), results: res.results.map(filterResult),
total: res.data.total, total: res.total,
} }
} }
export function getAuthors(all = false) { export function getAuthors(all = false) {
return api2.get<ProblemAuthor[]>("problem-authors", { return api.get<ProblemAuthor[]>("problem-authors", {
params: { all: all ? "1" : "0" }, params: { all: all ? "1" : "0" },
}) })
} }
@@ -92,27 +92,23 @@ export async function getProblem(problemID: string, contestID: string) {
const endpoint = contestID const endpoint = contestID
? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}` ? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}`
: `problems/${encodeURIComponent(problemID)}` : `problems/${encodeURIComponent(problemID)}`
const response = await api2.get<unknown>(endpoint) return detailProblem(await api.get<unknown>(endpoint))
return { error: null, data: detailProblem(response.data) }
} }
// 未登录返回 "0",登录后返回百分比字符串 // 未登录返回 "0",登录后返回百分比字符串
export function getProblemBeatRate(problemID: number) { export function getProblemBeatRate(problemID: number) {
return api2.get<string>(`problems/${problemID}/beat-count`) return api.get<string>(`problems/${problemID}/beat-count`)
} }
export async function getSubmission(id: string) { export async function getSubmission(id: string) {
const response = await api2.get<unknown>( const response = await api.get<unknown>(
`submissions/${encodeURIComponent(id)}`, `submissions/${encodeURIComponent(id)}`,
) )
return { return submissionDetailSchema.parse(response) as Submission
error: null,
data: submissionDetailSchema.parse(response.data) as Submission,
}
} }
export function submitCode(data: SubmitCodePayload) { export function submitCode(data: SubmitCodePayload) {
return api2.post<CreateSubmissionResponse>("submissions", data) return api.post<CreateSubmissionResponse>("submissions", data)
} }
export function formatCode(data: { code: string; language: string }) { export function formatCode(data: { code: string; language: string }) {
@@ -122,7 +118,7 @@ export function formatCode(data: { code: string; language: string }) {
"C++": "cpp", "C++": "cpp",
SQL: "sql", SQL: "sql",
} }
return api2.post<FormatCodeResponse>("code/format", { return api.post<FormatCodeResponse>("code/format", {
code: data.code, code: data.code,
language: languages[data.language] ?? data.language.toLowerCase(), language: languages[data.language] ?? data.language.toLowerCase(),
}) })
@@ -134,22 +130,22 @@ export function getSubmissions(params: Partial<SubmissionListPayload>) {
: "submissions" : "submissions"
// 契约里 language 是 z.string()(语言是配置项,随时可能加,收紧成枚举会让 // 契约里 language 是 z.string()(语言是配置项,随时可能加,收紧成枚举会让
// 新加的语言在后端 parse 时直接抛),前端在这一处收窄成 LANGUAGE // 新加的语言在后端 parse 时直接抛),前端在这一处收窄成 LANGUAGE
return api2.get<{ results: SubmissionListItem[]; total: number }>(endpoint, { return api.get<{ results: SubmissionListItem[]; total: number }>(endpoint, {
// contestId 走的是路径page 只有前端分页器用 // contestId 走的是路径page 只有前端分页器用
params: { ...params, contestId: undefined, page: undefined }, params: { ...params, contestId: undefined, page: undefined },
}) })
} }
export function getRankOfProblem(problemId: string) { export function getRankOfProblem(problemId: string) {
return api2.get<ProblemRank>(`problems/${encodeURIComponent(problemId)}/rank`) return api.get<ProblemRank>(`problems/${encodeURIComponent(problemId)}/rank`)
} }
export function getTodaySubmissionCount(language?: string) { export function getTodaySubmissionCount(language?: string) {
return api2.get<number>("submissions/today-count", { params: { language } }) return api.get<number>("submissions/today-count", { params: { language } })
} }
export function adminRejudge(id: string) { export function adminRejudge(id: string) {
return api2.post<{ ok: boolean }>( return api.post<{ ok: boolean }>(
`submissions/${encodeURIComponent(id)}/rejudge`, `submissions/${encodeURIComponent(id)}/rejudge`,
) )
} }
@@ -159,7 +155,7 @@ export function getSubmissionStatistics(
problemID?: string, problemID?: string,
username?: string, username?: string,
) { ) {
return api2.get<SubmissionStatistics>("submissions/statistics", { return api.get<SubmissionStatistics>("submissions/statistics", {
params: { ...duration, problemId: problemID, username }, params: { ...duration, problemId: problemID, username },
}) })
} }
@@ -169,17 +165,17 @@ export function getSubmissionStatistics(
* 「全服 Top10」就是这个榜的第一页取 limit=10 即可,不需要另一个上限参数。 * 「全服 Top10」就是这个榜的第一页取 limit=10 即可,不需要另一个上限参数。
*/ */
export function getRank(offset: number, limit: number) { export function getRank(offset: number, limit: number) {
return api2.get<UserRank>("rankings/users", { params: { offset, limit } }) return api.get<UserRank>("rankings/users", { params: { offset, limit } })
} }
export function getActivityRank(start: string) { export function getActivityRank(start: string) {
return api2.get<ActivityRankItem[]>("rankings/activity", { return api.get<ActivityRankItem[]>("rankings/activity", {
params: { start }, params: { start },
}) })
} }
export function getClassRank(grade?: number | null) { export function getClassRank(grade?: number | null) {
return api2.get<ClassRankItem[]>("rankings/classes", { params: { grade } }) return api.get<ClassRankItem[]>("rankings/classes", { params: { grade } })
} }
export function getUserClassRank( export function getUserClassRank(
@@ -187,7 +183,7 @@ export function getUserClassRank(
offset?: number, offset?: number,
limit?: number, limit?: number,
) { ) {
return api2.get<ClassUserRank>("me/class-rank", { return api.get<ClassUserRank>("me/class-rank", {
params: { scope, offset, limit }, params: { scope, offset, limit },
}) })
} }
@@ -197,7 +193,7 @@ export function getClassPK(
startTime?: string, startTime?: string,
endTime?: string, endTime?: string,
) { ) {
return api2.post<ClassComparisonResponse>("classes/comparison", { return api.post<ClassComparisonResponse>("classes/comparison", {
classNames, classNames,
...(startTime ? { startTime } : {}), ...(startTime ? { startTime } : {}),
...(endTime ? { endTime } : {}), ...(endTime ? { endTime } : {}),
@@ -211,20 +207,20 @@ export function getContestList(query: {
status: string status: string
tag: string tag: string
}) { }) {
return api2.get<ContestList>("contests", { params: query }) return api.get<ContestList>("contests", { params: query })
} }
export function getContest(id: string) { export function getContest(id: string) {
return api2.get<OjContest>(`contests/${encodeURIComponent(id)}`) return api.get<OjContest>(`contests/${encodeURIComponent(id)}`)
} }
export function getContestAccess(id: string) { export function getContestAccess(id: string) {
return api2.get<ContestAccess>(`contests/${encodeURIComponent(id)}/access`) return api.get<ContestAccess>(`contests/${encodeURIComponent(id)}/access`)
} }
// 注意和 GET /access 不一样:这个返回裸 true密码错是 403 走 catch // 注意和 GET /access 不一样:这个返回裸 true密码错是 403 走 catch
export function checkContestPassword(contestID: string, password: string) { export function checkContestPassword(contestID: string, password: string) {
return api2.post<boolean>( return api.post<boolean>(
`contests/${encodeURIComponent(contestID)}/access`, `contests/${encodeURIComponent(contestID)}/access`,
{ {
password, password,
@@ -233,10 +229,10 @@ export function checkContestPassword(contestID: string, password: string) {
} }
export async function getContestProblems(contestID: string) { export async function getContestProblems(contestID: string) {
const res = await api2.get<ProblemListItem[]>( const res = await api.get<ProblemListItem[]>(
`contests/${encodeURIComponent(contestID)}/problems`, `contests/${encodeURIComponent(contestID)}/problems`,
) )
return res.data.map(filterResult) return res.map(filterResult)
} }
export function getContestRank( export function getContestRank(
@@ -245,7 +241,7 @@ export function getContestRank(
) { ) {
// submissionInfo 在契约里是 Record<string, unknown>JSONB 原文), // submissionInfo 在契约里是 Record<string, unknown>JSONB 原文),
// 前端在这里收窄成 SubmissionInfo见 utils/types 的 ContestRank // 前端在这里收窄成 SubmissionInfo见 utils/types 的 ContestRank
return api2.get<{ results: ContestRank[]; total: number }>( return api.get<{ results: ContestRank[]; total: number }>(
`contests/${encodeURIComponent(contestID)}/rank`, `contests/${encodeURIComponent(contestID)}/rank`,
{ params: query }, { params: query },
) )
@@ -254,23 +250,23 @@ export function getContestRank(
export function uploadAvatar(file: File) { export function uploadAvatar(file: File) {
const form = new window.FormData() const form = new window.FormData()
form.append("image", file) form.append("image", file)
return api2.post("me/avatar", form, { return api.post("me/avatar", form, {
headers: { "content-type": "multipart/form-data" }, headers: { "content-type": "multipart/form-data" },
}) })
} }
export function updateProfile(data: { realName: string; mood: string }) { export function updateProfile(data: { realName: string; mood: string }) {
return api2.put<Profile>("me/profile", data) return api.put<Profile>("me/profile", data)
} }
export function getAnnouncementList(offset = 0, limit = 10) { 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 }, params: { limit, offset },
}) })
} }
export function getAnnouncement(id: number) { export function getAnnouncement(id: number) {
return api2.get<Announcement>(`announcements/${id}`) return api.get<Announcement>(`announcements/${id}`)
} }
export function createMessage(data: { export function createMessage(data: {
@@ -278,7 +274,7 @@ export function createMessage(data: {
message: string message: string
submission: string submission: string
}) { }) {
return api2.post("messages", { return api.post("messages", {
recipientId: data.recipient, recipientId: data.recipient,
message: data.message, message: data.message,
submissionId: data.submission, submissionId: data.submission,
@@ -287,33 +283,33 @@ export function createMessage(data: {
export function getMessageList(offset = 0, limit = 10) { export function getMessageList(offset = 0, limit = 10) {
// language 的收窄同 getSubmissions见那里的说明 // language 的收窄同 getSubmissions见那里的说明
return api2.get<{ results: Message[]; total: number }>("messages", { return api.get<{ results: Message[]; total: number }>("messages", {
params: { limit, offset }, params: { limit, offset },
}) })
} }
export function getReaction(problemID: number) { export function getReaction(problemID: number) {
return api2.get<ReactionState>(`problems/${problemID}/reaction`) return api.get<ReactionState>(`problems/${problemID}/reaction`)
} }
export function setReaction(problemID: number, type: ReactionKey) { export function setReaction(problemID: number, type: ReactionKey) {
return api2.post<ReactionState>(`problems/${problemID}/reaction`, { type }) return api.post<ReactionState>(`problems/${problemID}/reaction`, { type })
} }
export function getMetrics(userid: number) { export function getMetrics(userid: number) {
return api2.get<Metrics>(`users/${userid}/metrics`) return api.get<Metrics>(`users/${userid}/metrics`)
} }
export function getTutorial(id: number) { export function getTutorial(id: number) {
return api2.get<Tutorial>(`tutorials/${id}`) return api.get<Tutorial>(`tutorials/${id}`)
} }
export function getTutorials(type: "python" | "c") { export function getTutorials(type: "python" | "c") {
return api2.get<TutorialSummary[]>("tutorials", { params: { type } }) return api.get<TutorialSummary[]>("tutorials", { params: { type } })
} }
export function getAIDetailData(start: string, end: string, username?: string) { export function getAIDetailData(start: string, end: string, username?: string) {
return api2.get<AiDetail>("ai/detail", { params: { start, end, username } }) return api.get<AiDetail>("ai/detail", { params: { start, end, username } })
} }
export function getAIDurationData( export function getAIDurationData(
@@ -321,40 +317,37 @@ export function getAIDurationData(
duration: string, duration: string,
username?: string, username?: string,
) { ) {
return api2.get<DurationData[]>("ai/duration", { return api.get<DurationData[]>("ai/duration", {
params: { end, duration, username }, params: { end, duration, username },
}) })
} }
export function getAIHeatmapData(username?: string) { export function getAIHeatmapData(username?: string) {
return api2.get<HeatmapItem[]>("ai/heatmap", { return api.get<HeatmapItem[]>("ai/heatmap", {
params: username ? { username } : {}, params: username ? { username } : {},
}) })
} }
export function getAILoginSummary() { export function getAILoginSummary() {
return api2.get<LoginSummary>("ai/login-summary") return api.get<LoginSummary>("ai/login-summary")
} }
export function getAIPinnedReport() { export function getAIPinnedReport() {
return api2.get<AiAnalysisRecord | null>("ai/pinned") return api.get<AiAnalysisRecord | null>("ai/pinned")
} }
// ==================== 相似题目推荐 ==================== // ==================== 相似题目推荐 ====================
export function getSimilarProblems(problemId: string) { export function getSimilarProblems(problemId: string) {
return api2 return api
.get<ProblemListItem[]>(`problems/${encodeURIComponent(problemId)}/similar`) .get<ProblemListItem[]>(`problems/${encodeURIComponent(problemId)}/similar`)
.then((response) => ({ .then((response) => response.map(filterResult))
...response,
data: response.data.map(filterResult),
}))
} }
export type { YearlyAc as YearlyACData } from "@oj2/contract" export type { YearlyAc as YearlyACData } from "@oj2/contract"
export function getProblemYearlyAC(problemId: string) { export function getProblemYearlyAC(problemId: string) {
return api2.get<YearlyAc[]>( return api.get<YearlyAc[]>(
`problems/${encodeURIComponent(problemId)}/yearly-ac`, `problems/${encodeURIComponent(problemId)}/yearly-ac`,
) )
} }
@@ -366,11 +359,11 @@ export function submitFlowchart(data: {
mermaidCode: string mermaidCode: string
flowchartData: Record<string, unknown> // 压缩之后的,元数据太长了 flowchartData: Record<string, unknown> // 压缩之后的,元数据太长了
}) { }) {
return api2.post<CreateFlowchartResponse>("flowcharts", data) return api.post<CreateFlowchartResponse>("flowcharts", data)
} }
export function getFlowchartSubmission(id: string) { export function getFlowchartSubmission(id: string) {
return api2.get<FlowchartSubmission>(`flowcharts/${encodeURIComponent(id)}`) return api.get<FlowchartSubmission>(`flowcharts/${encodeURIComponent(id)}`)
} }
export function getFlowchartSubmissions(params: { export function getFlowchartSubmissions(params: {
@@ -382,7 +375,7 @@ export function getFlowchartSubmissions(params: {
today?: string today?: string
grade?: string grade?: string
}) { }) {
return api2.get<FlowchartList>("flowcharts", { params }) return api.get<FlowchartList>("flowcharts", { params })
} }
export function getFlowchartStatistics( export function getFlowchartStatistics(
@@ -390,23 +383,23 @@ export function getFlowchartStatistics(
problemID?: string, problemID?: string,
username?: string, username?: string,
) { ) {
return api2.get<FlowchartStatistics>("flowcharts/statistics", { return api.get<FlowchartStatistics>("flowcharts/statistics", {
params: { ...duration, problemId: problemID, username }, params: { ...duration, problemId: problemID, username },
}) })
} }
export function retryFlowchartSubmission(submissionId: string) { export function retryFlowchartSubmission(submissionId: string) {
return api2.post<{ status: string }>( return api.post<{ status: string }>(
`flowcharts/${encodeURIComponent(submissionId)}/retry`, `flowcharts/${encodeURIComponent(submissionId)}/retry`,
) )
} }
export function getCurrentProblemFlowchartSubmission(problemId: number) { export function getCurrentProblemFlowchartSubmission(problemId: number) {
return api2.get<FlowchartCurrent>(`problems/${problemId}/flowchart/current`) return api.get<FlowchartCurrent>(`problems/${problemId}/flowchart/current`)
} }
export function getFlowchartSubmissionDetail(problemId: number, page = 0) { export function getFlowchartSubmissionDetail(problemId: number, page = 0) {
return api2.get<FlowchartDetail>(`problems/${problemId}/flowchart/history`, { return api.get<FlowchartDetail>(`problems/${problemId}/flowchart/history`, {
params: { page }, params: { page },
}) })
} }
@@ -420,21 +413,21 @@ export function getProblemSetList(
difficulty = "", difficulty = "",
status = "", status = "",
) { ) {
return api2.get<ProblemSetList>("problem-sets", { return api.get<ProblemSetList>("problem-sets", {
params: { offset, limit, keyword, difficulty, status }, params: { offset, limit, keyword, difficulty, status },
}) })
} }
export function getProblemSetDetail(id: number) { export function getProblemSetDetail(id: number) {
return api2.get<ProblemSet>(`problem-sets/${id}`) return api.get<ProblemSet>(`problem-sets/${id}`)
} }
export function getProblemSetProblems(problemSetId: number) { export function getProblemSetProblems(problemSetId: number) {
return api2.get<ProblemSetProblem[]>(`problem-sets/${problemSetId}/problems`) return api.get<ProblemSetProblem[]>(`problem-sets/${problemSetId}/problems`)
} }
export function joinProblemSet(problemSetId: number) { export function joinProblemSet(problemSetId: number) {
return api2.post("problem-set-progress", { problemSetId }) return api.post("problem-set-progress", { problemSetId })
} }
export function updateProblemSetProgress( export function updateProblemSetProgress(
@@ -442,7 +435,7 @@ export function updateProblemSetProgress(
problemId: number, problemId: number,
submissionId: string, submissionId: string,
) { ) {
return api2.put("problem-set-progress", { return api.put("problem-set-progress", {
problemSetId, problemSetId,
problemId, problemId,
submissionId, submissionId,
@@ -450,13 +443,13 @@ export function updateProblemSetProgress(
} }
export function getUserBadges(username?: string) { export function getUserBadges(username?: string) {
return api2.get<UserBadge[]>( return api.get<UserBadge[]>(
`users/${encodeURIComponent(username ?? "me")}/badges`, `users/${encodeURIComponent(username ?? "me")}/badges`,
) )
} }
export function getProblemSetBadges(problemSetId: number) { export function getProblemSetBadges(problemSetId: number) {
return api2.get<ProblemSetBadge[]>(`problem-sets/${problemSetId}/badges`) return api.get<ProblemSetBadge[]>(`problem-sets/${problemSetId}/badges`)
} }
export function getProblemSetUserProgress( export function getProblemSetUserProgress(
@@ -468,13 +461,12 @@ export function getProblemSetUserProgress(
completionStatus?: "" | "completed" | "in_progress" | "not_started" completionStatus?: "" | "completed" | "in_progress" | "not_started"
}, },
) { ) {
return api2.get<ProblemSetProgressList>( return api.get<ProblemSetProgressList>(
`problem-sets/${problemSetId}/user-progress`, `problem-sets/${problemSetId}/user-progress`,
{ params }, { params },
) )
} }
export async function getExercises(tutorialId: number): Promise<Exercise[]> { export function getExercises(tutorialId: number): Promise<Exercise[]> {
const res = await api2.get<Exercise[]>(`tutorials/${tutorialId}/exercises`) return api.get<Exercise[]>(`tutorials/${tutorialId}/exercises`)
return res.data
} }

View File

@@ -122,8 +122,8 @@ async function compare() {
const { startTime, endTime } = getTimeRange() const { startTime, endTime } = getTimeRange()
const res = await getClassPK(selectedClasses.value, startTime, endTime) const res = await getClassPK(selectedClasses.value, startTime, endTime)
comparisons.value = res.data.comparisons comparisons.value = res.comparisons
hasTimeRange.value = res.data.hasTimeRange || false hasTimeRange.value = res.hasTimeRange || false
} catch (error) { } catch (error) {
message.error("获取数据失败") message.error("获取数据失败")
} finally { } finally {

View File

@@ -93,8 +93,8 @@ async function listContests() {
status: query.status, status: query.status,
tag: query.tag, tag: query.tag,
}) })
data.value = res.data.results data.value = res.results
total.value = res.data.total total.value = res.total
} }
function search(value: string) { function search(value: string) {

View File

@@ -96,8 +96,8 @@ async function listRanks() {
limit: query.limit, limit: query.limit,
offset: query.limit * (query.page - 1), offset: query.limit * (query.page - 1),
}) })
total.value = res.data.total total.value = res.total
data.value = res.data.results data.value = res.results
if (query.page === 1) { if (query.page === 1) {
chart.value = data.value chart.value = data.value
} }
@@ -225,7 +225,7 @@ async function downloadExcel() {
limit: total.value || 10000, limit: total.value || 10000,
offset: 0, offset: 0,
}) })
const allRanks: ContestRank[] = res.data.results const allRanks: ContestRank[] = res.results
const rows = allRanks.map((rank, index) => { const rows = allRanks.map((rank, index) => {
const rank1 = index + 1 const rank1 = index + 1

View File

@@ -206,7 +206,7 @@ function goToNextLesson() {
async function init() { async function init() {
const res1 = await getTutorials(type.value) const res1 = await getTutorials(type.value)
titles.value = res1.data titles.value = res1
isEmpty.value = titles.value.length === 0 isEmpty.value = titles.value.length === 0
if (isEmpty.value) return if (isEmpty.value) return
const id = titles.value[step.value - 1].id const id = titles.value[step.value - 1].id
@@ -214,7 +214,7 @@ async function init() {
getTutorial(id), getTutorial(id),
getExercises(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 : [] exercises.value = exs.status === "fulfilled" ? exs.value : []
learnStep.value[type.value] = step.value learnStep.value[type.value] = step.value
} }

View File

@@ -52,8 +52,7 @@ const similarLoaded = ref(false)
async function loadSimilarProblems() { async function loadSimilarProblems() {
if (similarLoaded.value || !problem.value) return if (similarLoaded.value || !problem.value) return
try { try {
const res = await getSimilarProblems(problem.value._id) similarProblems.value = await getSimilarProblems(problem.value._id)
similarProblems.value = res.data || []
} catch { } catch {
similarProblems.value = [] similarProblems.value = []
} }

View File

@@ -91,12 +91,12 @@ const options = {
async function getBeatRate() { async function getBeatRate() {
const res = await getProblemBeatRate(problem.value!.id) const res = await getProblemBeatRate(problem.value!.id)
beatRate.value = res.data beatRate.value = res
} }
async function getYearlyAC() { async function getYearlyAC() {
const res = await getProblemYearlyAC(problem.value!._id) const res = await getProblemYearlyAC(problem.value!._id)
yearlyACData.value = res.data yearlyACData.value = res
} }
onMounted(() => { onMounted(() => {

View File

@@ -239,8 +239,8 @@ async function pick(key: ReactionKey) {
submitting.value = key submitting.value = key
try { try {
const res = await setReaction(problem.value.id, key) const res = await setReaction(problem.value.id, key)
mine.value = res.data.mine mine.value = res.mine
counts.value = res.data.counts counts.value = res.counts
emit("submitted") emit("submitted")
} catch { } catch {
message.error("提交失败,请重试") message.error("提交失败,请重试")
@@ -255,8 +255,8 @@ async function load(problemId: number) {
try { try {
const res = await getReaction(problemId) const res = await getReaction(problemId)
if (sequence !== loadSequence) return if (sequence !== loadSequence) return
mine.value = res.data.mine mine.value = res.mine
counts.value = res.data.counts counts.value = res.counts
} catch { } catch {
if (sequence === loadSequence) message.error("暂时无法读取题目点评") if (sequence === loadSequence) message.error("暂时无法读取题目点评")
} finally { } finally {

View File

@@ -129,8 +129,8 @@ async function listSubmissions() {
problemId: (route.params.problemID as string) ?? "", problemId: (route.params.problemID as string) ?? "",
contestId: (route.params.contestID as string) ?? "", contestId: (route.params.contestID as string) ?? "",
}) })
submissions.value = res.data.results submissions.value = res.results
total.value = res.data.total total.value = res.total
} }
async function getRankOfThisProblem() { async function getRankOfThisProblem() {
@@ -138,10 +138,10 @@ async function getRankOfThisProblem() {
const res = await getRankOfProblem((route.params.problemID as string) ?? "") const res = await getRankOfProblem((route.params.problemID as string) ?? "")
loading.value = false loading.value = false
class_name.value = res.data.className class_name.value = res.className
rank.value = res.data.rank rank.value = res.rank
class_ac_count.value = res.data.classAcCount class_ac_count.value = res.classAcCount
all_ac_count.value = res.data.allAcCount all_ac_count.value = res.allAcCount
} }
onMounted(() => { onMounted(() => {

View File

@@ -115,7 +115,7 @@ async function submit() {
code: codeStore.code.value, code: codeStore.code.value,
language: formatLang, language: formatLang,
}) })
codeStore.setCode(res.data.code) codeStore.setCode(res.code)
} catch (e: any) { } catch (e: any) {
if (e?.error === "format-error") { if (e?.error === "format-error") {
// 仅 Python3 会出现:代码本身存在语法错误 // 仅 Python3 会出现:代码本身存在语法错误
@@ -141,11 +141,11 @@ async function submit() {
isSubmittingRequest.value = true isSubmittingRequest.value = true
try { try {
const res = await submitCode(data) const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submissionId}`) console.log(`[Submit] 代码已提交: ID=${res.submissionId}`)
// 3. 启动冷却 + 监控 // 3. 启动冷却 + 监控
startCooldown() startCooldown()
startMonitoring(res.data.submissionId) startMonitoring(res.submissionId)
showResult.value = true showResult.value = true
} finally { } finally {
isSubmittingRequest.value = false isSubmittingRequest.value = false

View File

@@ -144,7 +144,7 @@ async function submitFlowchartData() {
}) })
// 获取提交ID并订阅更新 // 获取提交ID并订阅更新
const submissionId = response.data.submissionId const submissionId = response.submissionId
if (submissionId) { if (submissionId) {
subscribeToSubmission(submissionId) subscribeToSubmission(submissionId)
@@ -167,7 +167,7 @@ function submit() {
async function getCurrentSubmission() { async function getCurrentSubmission() {
if (!problem.value?.id) return if (!problem.value?.id) return
const { data } = await getCurrentProblemFlowchartSubmission(problem.value.id) const data = await getCurrentProblemFlowchartSubmission(problem.value.id)
submissionCount.value = data.count submissionCount.value = data.count
latestRating.value = { latestRating.value = {
score: data.score, score: data.score,
@@ -177,7 +177,7 @@ async function getCurrentSubmission() {
async function getSubmission(submissionPage = 0) { async function getSubmission(submissionPage = 0) {
if (!problem.value?.id) return if (!problem.value?.id) return
const { data } = await getFlowchartSubmissionDetail( const data = await getFlowchartSubmissionDetail(
problem.value.id, problem.value.id,
submissionPage, submissionPage,
) )

View File

@@ -25,9 +25,9 @@ export function useSubmissionMonitor() {
try { try {
const res = await getSubmission(submissionId.value) const res = await getSubmission(submissionId.value)
submission.value = res.data submission.value = res
const result = res.data.result const result = res.result
// 判题完成,停止轮询 // 判题完成,停止轮询
if ( if (
result !== SubmissionStatus.judging && result !== SubmissionStatus.judging &&
@@ -83,7 +83,7 @@ export function useSubmissionMonitor() {
pausePolling() pausePolling()
getSubmission(submissionId.value).then((res) => { getSubmission(submissionId.value).then((res) => {
submission.value = res.data submission.value = res
// 15分钟无新提交则断开WebSocket节省资源 // 15分钟无新提交则断开WebSocket节省资源
scheduleDisconnect(15 * 60 * 1000) scheduleDisconnect(15 * 60 * 1000)
}) })

View File

@@ -110,7 +110,7 @@ async function init() {
;(inProblem.value ? loadProblemEditor : loadContestEditor)() ;(inProblem.value ? loadProblemEditor : loadContestEditor)()
try { try {
const res = await getProblem(problemID, contestID) const res = await getProblem(problemID, contestID)
problem.value = res.data problem.value = res
} catch (err: any) { } catch (err: any) {
problem.value = null problem.value = null
if (err.error === "contest-not-started") { if (err.error === "contest-not-started") {

View File

@@ -81,7 +81,7 @@ async function listProblems() {
async function listTags() { async function listTags() {
const res = await getProblemTagList() const res = await getProblemTagList()
tags.value = res.data.map((r: Omit<Tag, "checked">) => ({ tags.value = res.map((r: Omit<Tag, "checked">) => ({
...r, ...r,
checked: query.tag === r.name, checked: query.tag === r.name,
})) }))

View File

@@ -56,15 +56,15 @@ async function loadUserProgress() {
} }
const res = await getProblemSetUserProgress(problemSetId.value, params) const res = await getProblemSetUserProgress(problemSetId.value, params)
progress.value = res.data.results progress.value = res.results
total.value = res.data.total total.value = res.total
// 使用后端返回的统计数据(基于所有数据) // 使用后端返回的统计数据(基于所有数据)
if (res.data.statistics) { if (res.statistics) {
statistics.value = res.data.statistics statistics.value = res.statistics
} }
// 保存所有题目信息 // 保存所有题目信息
if (res.data.problems) { if (res.problems) {
allProblems.value = res.data.problems allProblems.value = res.problems
} }
loading.value = false loading.value = false
} }

View File

@@ -34,20 +34,20 @@ const activeTab = ref("problems")
async function loadProblemSetDetail() { async function loadProblemSetDetail() {
const res = await getProblemSetDetail(problemSetId.value) const res = await getProblemSetDetail(problemSetId.value)
problemSet.value = res.data problemSet.value = res
isJoined.value = res.data.userProgress?.isJoined || false isJoined.value = res.userProgress?.isJoined || false
} }
async function loadProblems() { async function loadProblems() {
const res = await getProblemSetProblems(problemSetId.value) const res = await getProblemSetProblems(problemSetId.value)
problems.value = res.data problems.value = res
} }
async function loadUserBadges() { async function loadUserBadges() {
if (!isJoined.value) return if (!isJoined.value) return
const res = await getUserBadges() const res = await getUserBadges()
userBadges.value = res.data.filter( userBadges.value = res.filter(
(badge: UserBadgeType) => badge.badge.problemsetId === problemSetId.value, (badge: UserBadgeType) => badge.badge.problemsetId === problemSetId.value,
) )
} }

View File

@@ -55,8 +55,8 @@ async function listProblemSets() {
query.difficulty, query.difficulty,
query.status, query.status,
) )
total.value = res.data.total total.value = res.total
problemSets.value = res.data.results problemSets.value = res.results
} }
function getDifficultyTag(difficulty: string) { function getDifficultyTag(difficulty: string) {

View File

@@ -82,7 +82,7 @@ async function loadClassDetail(className: string) {
classDetailData.value = null classDetailData.value = null
try { try {
const res = await getClassPK([className]) const res = await getClassPK([className])
classDetailData.value = res.data.comparisons[0] ?? null classDetailData.value = res.comparisons[0] ?? null
} catch { } catch {
// ignore // ignore
} finally { } finally {
@@ -151,10 +151,10 @@ async function analyzeSingleClassWithAI() {
async function init() { async function init() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getRank(offset, query.limit) const res = await getRank(offset, query.limit)
data.value = res.data.results data.value = res.results
total.value = res.data.total total.value = res.total
me.value = res.data.me me.value = res.me
return res.data.results return res.results
} }
function isMe(row: Rank) { function isMe(row: Rank) {
@@ -256,7 +256,7 @@ async function listActivity() {
const start = formatISO(sub(current, subOptions.value)) const start = formatISO(sub(current, subOptions.value))
const res = await getActivityRank(start) const res = await getActivityRank(start)
// 活动榜只有「用户名 + 做题数」,塞进榜单图表复用的 Rank 形状里 // 活动榜只有「用户名 + 做题数」,塞进榜单图表复用的 Rank 形状里
activityChart.value = res.data.map((d, index) => ({ activityChart.value = res.map((d, index) => ({
id: index, id: index,
user: { id: index, username: d.username, realName: null }, user: { id: index, username: d.username, realName: null },
acceptedNumber: d.count, acceptedNumber: d.count,
@@ -268,7 +268,7 @@ async function listActivity() {
// 「全服 Top10」就是同一个榜的第一页 —— 上限由服务端定,这里只要前 10 条 // 「全服 Top10」就是同一个榜的第一页 —— 上限由服务端定,这里只要前 10 条
async function listRank() { async function listRank() {
const res = await getRank(0, 10) const res = await getRank(0, 10)
rankChart.value = res.data.results rankChart.value = res.results
} }
const options: SelectOption[] = [ const options: SelectOption[] = [
@@ -439,7 +439,7 @@ async function listClassRank() {
classQuery.grade = parseInt(className.slice(0, 2)) classQuery.grade = parseInt(className.slice(0, 2))
} }
const res = await getClassRank(classQuery.grade) const res = await getClassRank(classQuery.grade)
classData.value = res.data classData.value = res
} }
async function listMyClassRank() { async function listMyClassRank() {
@@ -450,10 +450,10 @@ async function listMyClassRank() {
: 0 : 0
const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined const limit = myClassScope.value === "all" ? myClassQuery.limit : undefined
const res = await getUserClassRank(myClassScope.value, offset, limit) const res = await getUserClassRank(myClassScope.value, offset, limit)
myRank.value = res.data.myRank myRank.value = res.myRank
myClassName.value = res.data.className myClassName.value = res.className
myClassData.value = res.data.ranks myClassData.value = res.ranks
myClassTotal.value = res.data.total ?? res.data.ranks.length myClassTotal.value = res.total ?? res.ranks.length
if (myClassScope.value === "window") { if (myClassScope.value === "window") {
myClassQuery.page = 1 myClassQuery.page = 1
} }

View File

@@ -41,15 +41,15 @@ export const useAIStore = defineStore("ai", () => {
end, end,
targetUsername.value || undefined, targetUsername.value || undefined,
) )
detailsData.start = res.data.start detailsData.start = res.start
detailsData.end = res.data.end detailsData.end = res.end
detailsData.solved = res.data.solved detailsData.solved = res.solved
detailsData.grade = res.data.grade detailsData.grade = res.grade
detailsData.className = res.data.className detailsData.className = res.className
detailsData.tags = res.data.tags detailsData.tags = res.tags
detailsData.difficulty = res.data.difficulty detailsData.difficulty = res.difficulty
detailsData.contestCount = res.data.contestCount detailsData.contestCount = res.contestCount
detailsData.flowcharts = res.data.flowcharts detailsData.flowcharts = res.flowcharts
} }
async function fetchDurationData(end: string, duration: string) { async function fetchDurationData(end: string, duration: string) {
@@ -58,13 +58,13 @@ export const useAIStore = defineStore("ai", () => {
duration, duration,
targetUsername.value || undefined, targetUsername.value || undefined,
) )
durationData.value = res.data durationData.value = res
} }
async function fetchHeatmapData() { async function fetchHeatmapData() {
loading.heatmap = true loading.heatmap = true
const res = await getAIHeatmapData(targetUsername.value || undefined) const res = await getAIHeatmapData(targetUsername.value || undefined)
heatmapData.value = res.data heatmapData.value = res
loading.heatmap = false loading.heatmap = false
} }
@@ -165,7 +165,7 @@ export const useAIStore = defineStore("ai", () => {
async function fetchPinnedReport() { async function fetchPinnedReport() {
const res = await getAIPinnedReport() const res = await getAIPinnedReport()
pinnedReport.value = res.data pinnedReport.value = res
} }
async function simulatePinnedStream() { async function simulatePinnedStream() {

View File

@@ -58,9 +58,9 @@ export const useContestStore = defineStore("contest", () => {
async function init(contestID: string) { async function init(contestID: string) {
problems.value = [] problems.value = []
const res = await getContest(contestID) const res = await getContest(contestID)
contest.value = res.data contest.value = res
// now 是学生侧比赛专有的服务器时间,用来对齐倒计时 // now 是学生侧比赛专有的服务器时间,用来对齐倒计时
now.value = getTime(parseISO(res.data.now ?? res.data.createTime)) now.value = getTime(parseISO(res.now ?? res.createTime))
if (contestStatus.value !== ContestStatus.finished) { if (contestStatus.value !== ContestStatus.finished) {
timer = setInterval(() => { timer = setInterval(() => {
now.value = now.value + 1000 now.value = now.value + 1000
@@ -68,7 +68,7 @@ export const useContestStore = defineStore("contest", () => {
} }
if (contest.value?.contestType === ContestType.private) { if (contest.value?.contestType === ContestType.private) {
const res = await getContestAccess(contestID) const res = await getContestAccess(contestID)
toggleAccess(res.data.access) toggleAccess(res.access)
} }
_getProblems(contestID) _getProblems(contestID)
} }
@@ -84,8 +84,8 @@ export const useContestStore = defineStore("contest", () => {
async function checkPassword(contestID: string, password: string) { async function checkPassword(contestID: string, password: string) {
try { try {
const res = await checkContestPassword(contestID, password) const res = await checkContestPassword(contestID, password)
toggleAccess(res.data) toggleAccess(res)
if (res.data) { if (res) {
_getProblems(contestID) _getProblems(contestID)
} }
} catch (err) { } catch (err) {

View File

@@ -175,7 +175,7 @@ async function loadSubmission() {
try { try {
const { getFlowchartSubmission } = await import("oj/api") const { getFlowchartSubmission } = await import("oj/api")
const res = await getFlowchartSubmission(props.submissionId) const res = await getFlowchartSubmission(props.submissionId)
submission.value = res.data submission.value = res
// 渲染流程图 // 渲染流程图
if (submission.value?.mermaidCode) { if (submission.value?.mermaidCode) {

View File

@@ -41,7 +41,7 @@ async function init() {
if (submission.value) return if (submission.value) return
loading.value = true loading.value = true
const res = await getSubmission(props.submissionID) const res = await getSubmission(props.submissionID)
submission.value = res.data submission.value = res
loading.value = false loading.value = false
} }

View File

@@ -109,8 +109,8 @@ async function listSubmissions() {
today: query.today, today: query.today,
grade: query.result, grade: query.result,
}) })
total.value = res.data.total total.value = res.total
flowcharts.value = res.data.results flowcharts.value = res.results
} else { } else {
const res = await getSubmissions({ const res = await getSubmissions({
...query, ...query,
@@ -120,14 +120,14 @@ async function listSubmissions() {
language: query.language, language: query.language,
today: query.today, today: query.today,
}) })
submissions.value = res.data.results submissions.value = res.results
total.value = res.data.total total.value = res.total
} }
} }
async function getTodayCount() { async function getTodayCount() {
const res = await getTodaySubmissionCount(query.language) const res = await getTodaySubmissionCount(query.language)
todayCount.value = res.data todayCount.value = res
} }
onMounted(() => { onMounted(() => {

View File

@@ -63,10 +63,10 @@ async function init() {
toggle(true) toggle(true)
try { try {
const res = await getProfile(route.query.name as string) const res = await getProfile(route.query.name as string)
profile.value = res.data profile.value = res
// 用户不存在时后端返回 null后面的统计全都无从算起 // 用户不存在时后端返回 null后面的统计全都无从算起
if (!res.data) return if (!res) return
const acm = res.data.acmProblemsStatus.problems || {} const acm = res.acmProblemsStatus.problems || {}
const ac: string[] = [] const ac: string[] = []
Object.keys(acm).forEach((id) => { Object.keys(acm).forEach((id) => {
if (acm[id]["status"] === 0) { if (acm[id]["status"] === 0) {
@@ -76,17 +76,17 @@ async function init() {
ac.sort() ac.sort()
problems.value = ac problems.value = ac
if (res.data.submissionNumber > 0) { if (res.submissionNumber > 0) {
const metricsRes = await getMetrics(res.data.user.id) const metricsRes = await getMetrics(res.user.id)
firstSubmissionAt.value = parseTime(metricsRes.data.first) firstSubmissionAt.value = parseTime(metricsRes.first)
latestSubmissionAt.value = parseTime(metricsRes.data.latest) latestSubmissionAt.value = parseTime(metricsRes.latest)
toLatestAt.value = durationToDays( toLatestAt.value = durationToDays(
metricsRes.data.latest, metricsRes.latest,
metricsRes.data.now, metricsRes.now,
) )
learnDuration.value = durationToDays( learnDuration.value = durationToDays(
metricsRes.data.first, metricsRes.first,
metricsRes.data.latest, metricsRes.latest,
) )
} }
} finally { } finally {
@@ -101,7 +101,7 @@ async function loadAchievementSummary() {
const res = await getAchievementSummary( const res = await getAchievementSummary(
(route.query.name as string) || undefined, (route.query.name as string) || undefined,
) )
achievementSummary.value = res.data achievementSummary.value = res
} catch { } catch {
achievementSummary.value = null achievementSummary.value = null
} }

View File

@@ -62,8 +62,8 @@ const query = reactive({
async function listMessages() { async function listMessages() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getMessageList(offset, query.limit) const res = await getMessageList(offset, query.limit)
total.value = res.data.total total.value = res.total
messages.value = res.data.results messages.value = res.results
} }
onMounted(listMessages) onMounted(listMessages)

View File

@@ -1,10 +1,9 @@
import { userProfileSchema, type Quote } from "@oj2/contract" import { userProfileSchema, type Quote } from "@oj2/contract"
import api2 from "utils/api2" import api from "utils/api"
import type { ApiResponse } from "utils/api2"
import type { Profile, Tag } from "utils/types" import type { Profile, Tag } from "utils/types"
export function login(data: { username: string; password: string }) { export function login(data: { username: string; password: string }) {
return api2.post("auth/login", data) return api.post("auth/login", data)
} }
export function signup(data: { export function signup(data: {
@@ -12,37 +11,34 @@ export function signup(data: {
email: string email: string
password: string password: string
}) { }) {
return api2.post("users", data) return api.post("users", data)
} }
export function logout() { export function logout() {
return api2.delete("auth/session") return api.delete("auth/session")
} }
export async function getProfile( export async function getProfile(
username: string = "", username: string = "",
): Promise<ApiResponse<Profile | null>> { ): Promise<Profile | null> {
const response = await api2.get<unknown>( const response = await api.get<unknown>(
username ? `profiles/${encodeURIComponent(username)}` : "me", username ? `profiles/${encodeURIComponent(username)}` : "me",
) )
if (response.data === null) return { error: null, data: null } if (response === null) return null
// 形状与契约一致不再逐字段搬运zod 解析仍保留,形状对不上要当场炸 // 形状与契约一致不再逐字段搬运zod 解析仍保留,形状对不上要当场炸
return { return userProfileSchema.parse(response) as Profile
error: null,
data: userProfileSchema.parse(response.data) as Profile,
}
} }
export function getProblemTagList() { export function getProblemTagList() {
return api2.get<Tag[]>("problem-tags") return api.get<Tag[]>("problem-tags")
} }
export function getHitokoto() { export function getHitokoto() {
return api2.get<Quote>("quotes/random") return api.get<Quote>("quotes/random")
} }
export function getClassUsernames(classroom: string) { export function getClassUsernames(classroom: string) {
return api2.get<string[]>( return api.get<string[]>(
`classes/${encodeURIComponent(classroom)}/usernames`, `classes/${encodeURIComponent(classroom)}/usernames`,
) )
} }

View File

@@ -24,7 +24,7 @@ const authorOptions = ref([{ label: "全部", value: "" }])
async function getAuthorOptions() { async function getAuthorOptions() {
authorOptions.value = [{ label: "全部", value: "" }] authorOptions.value = [{ label: "全部", value: "" }]
const res = await getAuthors(all) const res = await getAuthors(all)
const remotes = res.data.map((item) => ({ const remotes = res.map((item) => ({
label: `${item.username} (${item.problemCount})`, label: `${item.username} (${item.problemCount})`,
value: item.username, value: item.username,
})) }))

View File

@@ -533,7 +533,7 @@ async function handleStatistics() {
query.problem, query.problem,
query.username, query.username,
) )
Object.assign(data, res.data) Object.assign(data, res)
await nextTick() await nextTick()
renderWordCloud() renderWordCloud()
} }

View File

@@ -10,7 +10,7 @@ async function receive() {
try { try {
const res = await getHitokoto() const res = await getHitokoto()
// 契约是 string | Record —— 一言数据集不在仓库里,形状留了余地 // 契约是 string | Record —— 一言数据集不在仓库里,形状留了余地
const quote = res.data const quote = res
if (typeof quote === "string") { if (typeof quote === "string") {
hitokoto.sentence = quote hitokoto.sentence = quote
hitokoto.from = "" hitokoto.from = ""

View File

@@ -80,7 +80,7 @@ async function loadClassUsernames(selectedClass: string) {
classUserLoading.value = true classUserLoading.value = true
try { try {
const res = await getClassUsernames(selectedClass) const res = await getClassUsernames(selectedClass)
classUserOptions.value = res.data.map((name: string) => ({ classUserOptions.value = res.map((name: string) => ({
label: name, label: name,
value: name, value: name,
})) }))

View File

@@ -421,13 +421,15 @@ async function handleStatistics() {
query.problem, query.problem,
query.username, query.username,
) )
count.total = res.data.submissionCount count.total = res.submissionCount
count.accepted = res.data.acceptedCount count.accepted = res.acceptedCount
count.rate = res.data.correctRate count.rate = res.correctRate
list.value = res.data.data // 这里的 res.data 是载荷**自己**的 data 字段(每个学生一行),
listUnaccepted.value = res.data.dataUnaccepted // 不是原来那层信封 —— 契约 submissionStatisticsSchema 就是这么定的
person.count = res.data.personCount list.value = res.data
person.rate = res.data.personRate listUnaccepted.value = res.dataUnaccepted
person.count = res.personCount
person.rate = res.personRate
} }
function rowKey(row: SubmissionStatisticsUser): DataTableRowKey { function rowKey(row: SubmissionStatisticsUser): DataTableRowKey {

View File

@@ -36,9 +36,8 @@ export const useAchievementStore = defineStore("achievement", () => {
async function fetchPending() { async function fetchPending() {
try { try {
// http 客户端返回 ApiResponse<T>,真实载荷在 .data 里
const res = await getPendingAchievements() const res = await getPendingAchievements()
enqueue(res.data ?? []) enqueue(res ?? [])
} catch { } catch {
// 拉取失败静默处理,下次路由切换会再拉 // 拉取失败静默处理,下次路由切换会再拉
} }

View File

@@ -14,8 +14,8 @@ export const useConfigStore = defineStore("config", () => {
}) })
async function getConfig() { async function getConfig() {
const res = await getWebsiteConfig() const res = await getWebsiteConfig()
config.value = res.data config.value = res
document.title = res.data.websiteName document.title = res.websiteName
} }
return { return {
config, config,

View File

@@ -31,9 +31,9 @@ export const useLoginSummaryStore = defineStore("loginSummary", () => {
analysisError.value = "" analysisError.value = ""
try { try {
const res = await getAILoginSummary() const res = await getAILoginSummary()
summary.value = res.data.summary summary.value = res.summary
analysis.value = res.data.analysis || "" analysis.value = res.analysis || ""
analysisError.value = res.data.analysisError || "" analysisError.value = res.analysisError || ""
} catch (err) { } catch (err) {
analysisError.value = "获取登录统计失败,请稍后再试" analysisError.value = "获取登录统计失败,请稍后再试"
} finally { } finally {

View File

@@ -63,7 +63,7 @@ export const useUserStore = defineStore("user", () => {
async function getMyProfile() { async function getMyProfile() {
isFinished.value = false isFinished.value = false
const res = await getProfile() const res = await getProfile()
profile.value = res.data profile.value = res
isFinished.value = true isFinished.value = true
storage.set(STORAGE_KEY.AUTHED, !!user.value?.email) storage.set(STORAGE_KEY.AUTHED, !!user.value?.email)
} }

View File

@@ -6,33 +6,25 @@ import storage from "./storage"
const { message: toast } = createDiscreteApi(["message"]) const { message: toast } = createDiscreteApi(["message"])
// 后端统一返回 { error, data } 信封;拦截器剥掉 axios 外层后, interface ApiError {
// 调用方拿到的就是这个信封data 才是真正的业务数据。
export interface ApiResponse<T = any> {
error: string | null
data: T
}
interface Api2Error {
error?: { error?: {
code?: string code?: string
message?: string message?: string
} }
} }
interface Api2Client { /**
get<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> * `{ data }` apps/api/src/http.ts
post<T>( * success axios
url: string, *
data?: unknown, * reject `{ error: 错误码, data: 文案 }`
config?: AxiosRequestConfig, * `err.error`
): Promise<ApiResponse<T>> */
put<T>( interface ApiClient {
url: string, get<T>(url: string, config?: AxiosRequestConfig): Promise<T>
data?: unknown, post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>
config?: AxiosRequestConfig, put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T>
): Promise<ApiResponse<T>> delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>
delete<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>>
} }
const instance = axios.create({ const instance = axios.create({
@@ -52,16 +44,13 @@ instance.interceptors.request.use((config) => {
}) })
instance.interceptors.response.use( instance.interceptors.response.use(
// 这里**故意**不返回 AxiosResponse{ data } 信封剥掉,让调用方直接拿到 // 这里**故意**不返回 AxiosResponseaxios 的外层和后端的 { data } 信封一起
// ApiResponse。类型上和 axios 的拦截器签名对不上(它期望原样返回响应), // 剥掉,让调用方直接拿到业务数据。类型上和 axios 的拦截器签名对不上(它期望原样
// 文件末尾的 `as unknown as Api2Client` 就是为了把这个真实形状交出去。 // 返回响应),文件末尾的 `as unknown as ApiClient` 就是为了把真实形状交出去。
((response: AxiosResponse) => ((response: AxiosResponse) =>
Promise.resolve({ response.data.data) as unknown as (response: AxiosResponse) => AxiosResponse,
error: null,
data: response.data.data,
})) as unknown as (response: AxiosResponse) => AxiosResponse,
(error) => { (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 code = payload?.error?.code ?? "network-error"
const message = payload?.error?.message ?? "Request failed" 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

View File

@@ -1,7 +1,7 @@
import axios from "axios" import axios from "axios"
// 指向新后端的 /api/admin。响应是 zip 二进制,不走 { error, data } 信封, // 指向新后端的 /api/admin。响应是 zip 二进制,不走 { error, data } 信封,
// 所以不能复用 utils/api2 的拦截器(它会把 response.data.data 取出来)。 // 所以不能复用 utils/api 的拦截器(它会把 response.data.data 取出来)。
const http = axios.create({ const http = axios.create({
baseURL: "/api/admin", baseURL: "/api/admin",
responseType: "blob", responseType: "blob",