import api from "utils/api" import { toAdminProblemRow } from "admin/transforms" import type { BatchProblemTagResponse, GenerateSqlTestCaseResponse, RenameTagResponse, SqlTestCaseScript, AdminAiReport, AdminAiReportList, StuckProblem, StuckProblemDetail, GradeTrendProblem, LearnBoard, LearnExerciseProgressList, LearnExerciseAttempt, AdminContestList, AdminUser, AdminUserList, DashboardInfo, JudgeServerList, OrphanTestCase, AdminProblem, AdminProblemList, AdminTag, AdminAnnouncement, AnnouncementEdit, AdminAnnouncementListItem, BlankContest, BlankProblem, AdminContest, Exercise, ExerciseType, SqlDisplay, UploadTestCaseResponse, Tutorial, AdminUserForm, WebsiteConfig, AdminProblemSetBadge, AdminProblemSetList, AdminProblemSet, AdminProblemSetProblem, AddProblemsToSetResult, TutorialListItem, TutorialEdit, TagCategory, } from "utils/types" import type { TutorialProblemCandidate, AstCheckRequest, AstCheckResponse, GenerateTestInputsRequest, GenerateTestInputsResponse, TestCaseFiles, } from "@oj2/contract" export function getBaseInfo() { return api.get("admin/dashboard") } export function randomUser10(classroom: string) { return api.get("admin/random-usernames", { params: { classroom }, }) } export async function getProblemList( offset = 0, limit = 10, keyword: string, author?: string, contestID?: string, tagId?: number, /** 题型:sql = SQL 题,code = 编程题;不传是全部(比赛题列表不认) */ kind?: "sql" | "code" | "", /** 1 = 学生看得到的,0 = 藏着的;不传是全部(比赛题列表不认) */ visible?: "1" | "0" | "", ) { const endpoint = contestID ? `admin/contests/${contestID}/problems` : "admin/problems" const res = await api.get(endpoint, { params: { offset, limit, keyword, author, tagId, kind: kind || undefined, visible: visible || undefined, }, }) return { results: res.results.map(toAdminProblemRow), total: res.total, counts: res.counts ?? null, } } /** 勾了几道,一次让学生看得到 / 藏起来 */ export function batchSetProblemVisible(problemIds: number[], visible: boolean) { return api.post<{ problemCount: number }>("admin/problems/batch-visible", { problemIds, visible, }) } // 比赛「从题库中选择」用:列所有可见的公开题,不只是自己创建的 export async function getLibraryProblemList(offset = 0, limit = 10, keyword: string) { const res = await api.get("admin/problems", { params: { offset, limit, keyword, library: 1 }, }) return { results: res.results.map(toAdminProblemRow), total: res.total, } } export function deleteProblem(id: number) { return api.delete(`admin/problems/${id}`) } // 比赛题与公开题共用一条删除路由,比赛由后端从题目推导 export function deleteContestProblem(id: number) { return api.delete(`admin/problems/${id}`) } // 比赛题也走这条:后端是同一条 PUT admin/problems/:id,比赛由题目自己推导, // 所以不像新建那样需要分两个函数 export function editProblem(problem: AdminProblem | BlankProblem) { return api.put( `admin/problems/${(problem as AdminProblem).id}`, toProblemBody(problem), ) } export function toggleProblemVisible(problemID: number) { return api.put<{ visible: boolean }>(`admin/problems/${problemID}/visibility`) } export function generateFlowchartFromPythonCode(python: string) { return api.post<{ doc: Record }>("admin/problems/flowchart", { python, }) } export function getProblem(id: string | number) { return api.get(`admin/problems/${id}`) } // 标签管理 export function getTagAdminList(keyword = "") { return api.get("admin/problem-tags", { params: { keyword } }) } export function renameTag(id: number, name: string) { return api.put(`admin/problem-tags/${id}`, { name }) } export function setTagCategory(id: number, category: TagCategory) { return api.put(`admin/problem-tags/${id}/category`, { category }) } export function deleteTag(id: number) { return api.delete(`admin/problem-tags/${id}`) } export function batchTagProblems( problemIds: number[], tagNames: string[], action: "add" | "remove", ) { return api.post("admin/problems/batch-tag", { problemIds, tagNames, action, }) } // 用户排名(后台版,无 100 名上限;公开榜单是 oj/api.ts 的 getRank) export function getAdminUserRank(offset: number, limit: number, keyword: string) { return api.get("admin/rankings/users", { params: { offset, limit, keyword }, }) } // 用户列表 export function getUserList(offset = 0, limit = 10, type = "", keyword: string, orderBy = "") { return api.get("admin/users", { // "-last_login" 是旧接口传下来的取值(路由 query 里可能还存着),改叫 "-lastLogin"; // "-online" 是新增的,原样透传 params: { offset, limit, keyword, type, orderBy: orderBy === "-last_login" ? "-lastLogin" : orderBy, }, }) } // 编辑用户 export function editUser(user: AdminUserForm) { return api.put(`admin/users/${user.id}`, { username: user.username, email: user.email, adminType: user.adminType, problemPermission: user.problemPermission, realName: user.realName ?? null, isDisabled: user.isDisabled, password: user.password ?? "", }) } // 重置用户密码,返回新密码 export async function resetPassword(userID: number) { const res = await api.post<{ password: string }>(`admin/users/${userID}/reset-password`) return res.password } // 导入用户 export function importUsers(users: string[][]) { return api.post("admin/users", { users }) } // 批量删除用户 export function deleteUsers(userIDs: number[]) { return api.delete("admin/users", { data: { ids: userIDs } }) } export function getContestList(offset = 0, limit = 10, keyword: string) { return api.get("admin/contests", { params: { offset, limit, keyword }, }) } // 上传图片 export async function uploadImage(file: File): Promise { const form = new window.FormData() form.append("image", file) const res = await api.post<{ success: boolean filePath: string msg: string }>("admin/upload-image", form, { headers: { "content-type": "multipart/form-data" }, }) return res.success ? res.filePath : "" } // 上传测试用例;SQL 题的压缩包是 1.sql..N.sql(每个文件一个测试点的建表+数据脚本) export function uploadTestcases(file: File, options: { sql?: boolean } = {}) { const form = new window.FormData() form.append("file", file) if (options.sql) { form.append("sql", "1") } return api.post("admin/test-cases", form, { headers: { "content-type": "multipart/form-data" }, }) } // SQL 题测试点预览:后端跑一遍初始化脚本+标准答案,返回数据表和期望结果展示数据 export function previewSQLTestcase(data: { initSql: string refSql: string mode: "query" | "modify" /** 是不是题面上展示的那组(第 1 组):增删改题只有它必须改到行 */ shown: boolean }) { return api.post("admin/sql-test-cases/preview", data) } // 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序) export function getSQLTestcaseScripts(problemId: number) { return api.get(`admin/problems/${problemId}/sql-scripts`) } // AI 根据标准答案生成一个 SQL 测试点初始化脚本 export function generateSQLTestcase(data: { refSql: string; mode: "query" | "modify" }) { return api.post("admin/sql-test-cases/generate", data) } // 编程题出题页:读回已有测试点的原文(太大的只给数量和大小) export function getTestCaseFiles(problemId: number) { return api.get(`admin/problems/${problemId}/test-case-files`) } // 拿标准答案自测语法要求 export function checkAstRules(data: AstCheckRequest) { return api.post("admin/ast-check", data) } // AI 按题面和标准答案想几组测试输入(输出由标准答案跑) export function generateTestInputs(data: GenerateTestInputsRequest) { return api.post("admin/test-inputs/generate", data) } /** 出站补默认值。字段名两边已经一致,不再做键名转换 */ function toProblemBody(problem: AdminProblem | BlankProblem) { const p = problem as Partial> & { flowchartData?: Record | null } return { _id: p._id, title: p.title, description: p.description, inputDescription: p.inputDescription ?? "", outputDescription: p.outputDescription ?? "", samples: p.samples ?? [], testCaseId: p.testCaseId, testCaseScore: p.testCaseScore ?? [], timeLimit: p.timeLimit, memoryLimit: p.memoryLimit, languages: p.languages ?? [], template: p.template ?? {}, visible: p.visible, difficulty: p.difficulty, tags: p.tags ?? [], hint: p.hint ?? null, source: p.source ?? null, prompt: p.prompt ?? null, answers: p.answers ?? [], allowFlowchart: p.allowFlowchart ?? false, showFlowchart: p.showFlowchart ?? false, mermaidCode: p.mermaidCode ?? null, flowchartData: p.flowchartData ?? null, flowchartHint: p.flowchartHint ?? null, astRules: p.astRules ?? null, sqlConfig: p.sqlConfig ?? null, } } export function createProblem(problem: BlankProblem) { return api.post("admin/problems", toProblemBody(problem)) } export function createContestProblem(problem: BlankProblem) { // contestId 由 detail.vue 在提交前写进 problem 对象 return api.post( `admin/contests/${problem.contestId}/problems`, toProblemBody(problem), ) } /** 组件里的比赛对象是 snake_case,出站转成新后端要的 camelCase */ function toContestBody(contest: AdminContest | BlankContest) { return { title: contest.title, description: contest.description, tag: contest.tag, startTime: contest.startTime, endTime: contest.endTime, password: contest.password || null, visible: contest.visible, } } export function createContest(contest: BlankContest) { return api.post("admin/contests", toContestBody(contest)) } export function editContest(contest: AdminContest | BlankContest) { return api.put( `admin/contests/${(contest as AdminContest).id}`, toContestBody(contest), ) } export function cloneContest(contestId: number) { return api.post(`admin/contests/${contestId}/clone`) } export function getContest(id: string) { return api.get(`admin/contests/${id}`) } export function addProblemForContest(contestID: string, problemID: number, displayID: string) { return api.post(`admin/contests/${contestID}/problems/from-public`, { problemId: problemID, displayId: displayID, }) } /** 比赛题按这个顺序重新编号 1、2、3…(要给全这场比赛的题) */ export function reorderContestProblems(contestID: string, problemIds: number[]) { return api.put(`admin/contests/${contestID}/problems/order`, { problemIds }) } export function getWebsite() { return api.get("admin/website") } export function editWebsite(data: WebsiteConfig) { return api.post("admin/website", data) } export function listInvalidTestcases() { return api.get("admin/orphan-test-cases") } export function pruneInvalidTestcases(id?: string) { return api.delete("admin/orphan-test-cases", { params: { id } }) } export function getJudgeServer() { return api.get("admin/judge-servers") } export function deleteJudgeServer(hostname: string) { return api.delete(`admin/judge-servers/${encodeURIComponent(hostname)}`) } export function getAnnouncementList(offset = 0, limit = 10) { return api.get<{ results: AdminAnnouncementListItem[]; total: number }>("admin/announcements", { params: { offset, limit }, }) } export function getAnnouncement(id: number) { return api.get(`admin/announcements/${id}`) } export function deleteAnnouncement(id: number) { return api.delete(`admin/announcements/${id}`) } export function editAnnouncement(announcement: AnnouncementEdit) { const { id, ...body } = announcement return api.put(`admin/announcements/${id}`, body) } export function createAnnouncement(announcement: AnnouncementEdit) { const { id: _id, ...body } = announcement return api.post("admin/announcements", body) } export function getTutorialList() { return api.get<{ [key: string]: TutorialListItem[] }>("admin/tutorials") } export function getTutorial(id: number) { return api.get(`admin/tutorials/${id}`) } /** 「示例代码」界面上去掉了,`code` 不带:后端照原样留着发布脚本写进去的那份 */ function toTutorialBody(data: TutorialEdit) { return { title: data.title, content: data.content, project: data.project, isPublic: data.isPublic, order: data.order, type: data.type, } } export function createTutorial(data: TutorialEdit) { return api.post("admin/tutorials", toTutorialBody(data)) } export function updateTutorial(data: TutorialEdit) { return api.put(`admin/tutorials/${data.id}`, toTutorialBody(data)) } export function setTutorialProblems(id: number, problemIds: number[]) { return api.put(`admin/tutorials/${id}/problems`, { problemIds }) } export function getTutorialProblemCandidates(keyword: string) { return api.get("admin/tutorial-problem-candidates", { params: keyword ? { keyword } : {}, }) } export function reorderTutorials(ids: number[]) { return api.put("admin/tutorial-order", { ids }) } export function renameTutorialProject(type: "python" | "c", from: string, to: string) { return api.put("admin/tutorial-projects", { type, from, to }) } export function deleteTutorial(id: number) { return api.delete(`admin/tutorials/${id}`) } export function setTutorialVisibility(id: number, isPublic: boolean) { return api.put(`admin/tutorials/${id}/visibility`, { isPublic }) } export function getAdminExercises(tutorialId: number) { return api.get(`admin/tutorials/${tutorialId}/exercises`) } export async function createExercise(data: { tutorialId: number type: ExerciseType data: object order: number }) { return api.post("admin/exercises", data) } export async function updateExercise(data: { id: number type: ExerciseType data: object order: number }) { return api.put(`admin/exercises/${data.id}`, { type: data.type, data: data.data, order: data.order, }) } export function deleteExercise(id: number) { return api.delete(`admin/exercises/${id}`) } // 将竞赛题目转为公开题目 export function makeProblemPublic(id: number, displayId: string) { return api.post(`admin/problems/${id}/make-public`, { displayId, }) } // 比赛辅助检查 export function updateACMHelperChecked( contest_id: number, rank_id: number, problem_id: string, checked: boolean, ) { return api.put(`admin/contests/${contest_id}/acm-helper`, { rankId: rank_id, problemId: problem_id, checked, }) } // 题单管理 API export function getProblemSetList(offset = 0, limit = 10, keyword = "") { return api.get("admin/problem-sets", { params: { offset, limit, keyword }, }) } export function getProblemSetDetail(id: number) { return api.get(`admin/problem-sets/${id}`) } export interface ProblemSetBody { title: string description: string /** 布置到哪天(东八区当天结束的 ISO 时刻);null = 不布置 */ assignedUntil: string | null visible: boolean } export function createProblemSet(data: ProblemSetBody) { return api.post("admin/problem-sets", data) } export function editProblemSet(id: number, data: ProblemSetBody) { return api.put(`admin/problem-sets/${id}`, data) } export function deleteProblemSet(id: number) { return api.delete(`admin/problem-sets/${id}`) } export function toggleProblemSetVisible(id: number) { return api.put(`admin/problem-sets/${id}/visibility`) } // 题单题目管理 API export function getProblemSetProblems(problemSetId: number) { return api.get(`admin/problem-sets/${problemSetId}/problems`) } /** 一次加好几道(题号),按给的顺序接在最后;找不到的、已经在里面的分开报回来 */ export function addProblemsToSet(problemSetId: number, problemIds: string[]) { return api.post(`admin/problem-sets/${problemSetId}/problems`, { problemIds, }) } export function setProblemRequired(problemSetId: number, itemId: number, isRequired: boolean) { return api.put(`admin/problem-sets/${problemSetId}/problems/${itemId}`, { isRequired }) } export function reorderProblemSetProblems(problemSetId: number, ids: number[]) { return api.put(`admin/problem-sets/${problemSetId}/problems/order`, { ids }) } export function removeProblemFromSet(problemSetId: number, itemId: number) { return api.delete(`admin/problem-sets/${problemSetId}/problems/${itemId}`) } // 题单奖章管理 API export function getProblemSetBadges(problemSetId: number) { return api.get(`admin/problem-sets/${problemSetId}/badges`) } export interface BadgeBody { name: string description: string icon: string conditionType: AdminProblemSetBadge["conditionType"] conditionValue: number } export function createProblemSetBadge(problemSetId: number, data: BadgeBody) { return api.post(`admin/problem-sets/${problemSetId}/badges`, data) } export function editProblemSetBadge(problemSetId: number, badgeId: number, data: BadgeBody) { return api.put(`admin/problem-sets/${problemSetId}/badges/${badgeId}`, data) } export function deleteProblemSetBadge(problemSetId: number, badgeId: number) { return api.delete(`admin/problem-sets/${problemSetId}/badges/${badgeId}`) } // 卡点分析:start 是时间段的起点(ISO),不给就是全部时间 export function getStuckProblems(params: { className?: string; start?: string }) { return api.get("admin/problem-analytics/stuck", { params }) } export function getStuckProblemDetail(id: number, params: { className?: string; start?: string }) { return api.get(`admin/problem-analytics/stuck/${id}`, { params }) } export function getLearnBoard(params: { type: "python" | "c"; className?: string }) { return api.get("admin/learn-analytics/board", { params }) } export function getLearnExercises(params: { type: "python" | "c"; className?: string }) { return api.get("admin/learn-analytics/exercises", { params, }) } export function getLearnExerciseAttempts(exerciseId: number, params: { className?: string }) { return api.get(`admin/learn-analytics/exercises/${exerciseId}/attempts`, { params, }) } export function getGradeTrend() { return api.get("admin/problem-analytics/grades") } // AI 学习分析报告 export function getAIReportList(offset = 0, limit = 10, username = "") { return api.get("admin/ai/reports", { params: { offset, limit, username: username || undefined }, }) } export function getAIReportDetail(id: number) { return api.get(`admin/ai/reports/${id}`) } export function pinAIReport(id: number) { return api.post<{ isPinned: boolean }>(`admin/ai/reports/${id}/pin`) } export function getPinnedAIReports() { return api.get("admin/ai/reports", { params: { pinnedOnly: "true" }, }) } // ==================== 成就 ==================== import type { AdminAchievement, AchievementMetric as MetricOption, AdminUserRank, } from "@oj2/contract" export type { AdminAchievement, MetricOption } function toAchievementBody(data: Partial) { return { name: data.name, description: data.description, icon: data.icon, rarity: data.rarity, hidden: data.hidden ?? false, metric: data.metric, operator: data.operator, threshold: data.threshold, visible: data.visible ?? true, order: data.order ?? 0, } } export function getAdminAchievements() { return api.get("admin/achievements") } export function getMetricOptions() { return api.get("admin/achievement-metrics") } export function createAchievement(data: Partial) { return api.post("admin/achievements", toAchievementBody(data)) } export function updateAchievement(data: Partial) { return api.put(`admin/achievements/${data.id}`, toAchievementBody(data)) } export function deleteAchievement(id: number) { return api.delete(`admin/achievements/${id}`) }