import { toAdminType, toProblemPermission, type AdminLessonList, type AdminLessonRequest } from '@oj2/contract' import type { AddContestProblemRequest, ChallengeInput, ChallengeSuggestionsResponse, AddProblemsToSetRequest, BatchProblemTagRequest, BatchProblemVisibleRequest, CheckUsernamesRequest, CreateAchievementRequest, CreateAnnouncementRequest, CreateContestRequest, CreateExerciseRequest, CreateProblemRequest, CreateProblemSetBadgeRequest, CreateProblemSetRequest, CreateTutorialRequest, GenerateFlowchartRequest, GenerateSqlTestCaseRequest, ImportUsersRequest, MakeProblemPublicRequest, RenameTagRequest, RenameTutorialProjectRequest, ReorderContestProblemsRequest, ReorderProblemSetProblemsRequest, ReorderTutorialsRequest, SetTagCategoryRequest, SetTutorialProblemsRequest, SetTutorialVisibilityRequest, SqlPreviewRequest, UpdateAchievementRequest, UpdateAcmHelperRequest, UpdateAnnouncementRequest, UpdateContestRequest, UpdateExerciseRequest, UpdateProblemInSetRequest, UpdateProblemRequest, UpdateProblemSetBadgeRequest, UpdateProblemSetRequest, UpdateTutorialRequest, UpdateUserRequest, UpdateWebsiteConfigRequest, } from '@oj2/contract' import api from 'utils/api' import { toAdminProblemRow } from 'admin/transforms' import type { BatchProblemTagResponse, GenerateSqlTestCaseResponse, RenameTagResponse, SqlTestCaseScript, AdminAiReport, AdminAiReportList, StuckProblem, StuckProblemDetail, GradeTrend, LearnBoard, LearnExerciseProgressList, LearnExerciseAttemptList, LearnStudentDetail, AdminContestList, AdminUser, AdminUserList, DashboardInfo, JudgeServerList, OrphanImage, 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, CheckUsernamesResponse, } from '@oj2/contract' export function getBaseInfo() { return api.get('admin/dashboard') } /** 后台首页「抽一个人」:从这个班(班号)随机拿 10 个显示用的名字 */ 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, } satisfies BatchProblemVisibleRequest) } // 比赛「从题库中选择」用:列所有可见的公开题,不只是自己创建的 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) satisfies Partial) } 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, } satisfies GenerateFlowchartRequest) } 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 } satisfies RenameTagRequest) } export function setTagCategory(id: number, category: TagCategory) { return api.put(`admin/problem-tags/${id}/category`, { category, } satisfies SetTagCategoryRequest) } 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, } satisfies BatchProblemTagRequest) } // 用户排名(后台版,无 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 = '', /** 班级,NO_CLASS 是「没填班级」 */ className = '', ) { return api.get('admin/users', { params: { offset, limit, keyword, type, orderBy, className: className || undefined, }, }) } /** 批量建号之前查重:这些用户名哪些已经有了 */ /** 批量建号查重:这个班里已经有的同名账号,和每个名字会建成的用户名 */ export function checkUsernames(className: string, names: string[]) { return api.post('admin/users/check', { className, names, } satisfies CheckUsernamesRequest) } // 编辑用户 export function editUser(user: AdminUserForm) { return api.put(`admin/users/${user.id}`, { username: user.username, // 库里 email 没有空的(2026-10 核过),响应类型是 nullable 只因为列可空 email: user.email ?? '', adminType: toAdminType(user.adminType), problemPermission: toProblemPermission(user.problemPermission), realName: user.realName ?? null, className: user.className ?? null, isDisabled: user.isDisabled, password: user.password ?? '', } satisfies UpdateUserRequest) } // 重置用户密码,返回新密码 export async function resetPassword(userID: number) { const res = await api.post<{ password: string }>(`admin/users/${userID}/reset-password`) return res.password } // 导入用户 /** 批量建号。addedClasses:这批里的新班级,后端已经顺手加进了班级列表 */ export function importUsers(users: ImportUsersRequest['users']) { return api.post<{ imported: number; addedClasses: string[] }>('admin/users', { users, } satisfies ImportUsersRequest) } // 批量删除用户 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) 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) } // SQL 题测试点预览:后端跑一遍初始化脚本+标准答案,返回数据表和期望结果展示数据 export function previewSQLTestcase(data: { initSql: string refSql: string mode: 'query' | 'modify' /** 是不是题面上展示的那组(第 1 组):增删改题只有它必须改到行 */ shown: boolean }) { return api.post('admin/sql-test-cases/preview', data satisfies SqlPreviewRequest) } // 回显已上传的 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 satisfies GenerateSqlTestCaseRequest) } // 编程题出题页:读回已有测试点的原文(太大的只给数量和大小) 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 satisfies AstCheckRequest) } // 出题页「挑战」的候选:拿这道题历史上做对的代码挖 export function getChallengeSuggestions(problemId: number, language: string) { return api.post(`admin/problems/${problemId}/challenge-suggestions`, { language, }) } // AI 按题面和标准答案想几组测试输入(输出由标准答案跑) export function generateTestInputs(data: GenerateTestInputsRequest) { return api.post('admin/test-inputs/generate', data satisfies GenerateTestInputsRequest) } /** 出站补默认值。字段名两边已经一致,不再做键名转换 */ function toProblemBody(problem: AdminProblem | BlankProblem) { const p = problem as Partial> & { flowchartData?: Record | null challenges?: 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, challenges: p.challenges ?? null, sqlConfig: p.sqlConfig ?? null, } } export function createProblem(problem: BlankProblem) { return api.post('admin/problems', toProblemBody(problem) satisfies Partial) } export function createContestProblem(problem: BlankProblem) { // contestId 由 detail.vue 在提交前写进 problem 对象 return api.post(`admin/contests/${problem.contestId}/problems`, toProblemBody(problem) satisfies Partial) } /** 组件里的比赛对象是 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) satisfies CreateContestRequest) } export function editContest(contest: AdminContest | BlankContest) { return api.put(`admin/contests/${(contest as AdminContest).id}`, toContestBody(contest) satisfies UpdateContestRequest) } 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, } satisfies AddContestProblemRequest) } /** 比赛题按这个顺序重新编号 1、2、3…(要给全这场比赛的题) */ export function reorderContestProblems(contestID: string, problemIds: number[]) { return api.put(`admin/contests/${contestID}/problems/order`, { problemIds, } satisfies ReorderContestProblemsRequest) } export function getWebsite() { return api.get('admin/website') } export function editWebsite(data: WebsiteConfig) { return api.post('admin/website', data satisfies UpdateWebsiteConfigRequest) } 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 listOrphanImages() { return api.get('admin/orphan-images') } /** 不带参数是全删;带 folder 只删那个目录的,再带 name 只删一张 */ export function pruneOrphanImages(target?: Pick & { name?: string }) { return api.delete<{ deleted: number }>('admin/orphan-images', { params: target }) } 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 satisfies UpdateAnnouncementRequest) } export function createAnnouncement(announcement: AnnouncementEdit) { const { id: _id, ...body } = announcement return api.post('admin/announcements', body satisfies CreateAnnouncementRequest) } 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) satisfies CreateTutorialRequest) } export function updateTutorial(data: TutorialEdit) { return api.put(`admin/tutorials/${data.id}`, toTutorialBody(data) satisfies UpdateTutorialRequest) } export function setTutorialProblems(id: number, problemIds: number[]) { return api.put(`admin/tutorials/${id}/problems`, { problemIds, } satisfies SetTutorialProblemsRequest) } 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 } satisfies ReorderTutorialsRequest) } export function renameTutorialProject(type: 'python' | 'c', from: string, to: string) { return api.put('admin/tutorial-projects', { type, from, to, } satisfies RenameTutorialProjectRequest) } 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, } satisfies SetTutorialVisibilityRequest) } export function getAdminExercises(tutorialId: number) { return api.get(`admin/tutorials/${tutorialId}/exercises`) } export async function createExercise(data: { tutorialId: number; type: ExerciseType; data: Record; order: number }) { return api.post('admin/exercises', data satisfies CreateExerciseRequest) } export async function updateExercise(data: { id: number; type: ExerciseType; data: Record; order: number; resetAttempts: boolean }) { return api.put(`admin/exercises/${data.id}`, { type: data.type, data: data.data, order: data.order, resetAttempts: data.resetAttempts, } satisfies UpdateExerciseRequest) } 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, } satisfies MakeProblemPublicRequest) } // 比赛辅助检查 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, } satisfies UpdateAcmHelperRequest) } // 专题管理 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 satisfies CreateProblemSetRequest) } export function editProblemSet(id: number, data: ProblemSetBody) { return api.put(`admin/problem-sets/${id}`, data satisfies UpdateProblemSetRequest) } 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, } satisfies AddProblemsToSetRequest) } export function setProblemRequired(problemSetId: number, itemId: number, isRequired: boolean) { return api.put(`admin/problem-sets/${problemSetId}/problems/${itemId}`, { isRequired, } satisfies UpdateProblemInSetRequest) } export function reorderProblemSetProblems(problemSetId: number, ids: number[]) { return api.put(`admin/problem-sets/${problemSetId}/problems/order`, { ids, } satisfies ReorderProblemSetProblemsRequest) } 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 satisfies CreateProblemSetBadgeRequest) } export function editProblemSetBadge(problemSetId: number, badgeId: number, data: BadgeBody) { return api.put(`admin/problem-sets/${problemSetId}/badges/${badgeId}`, data satisfies UpdateProblemSetBadgeRequest) } 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 getLearnStudent(userId: number, params: { type: 'python' | 'c' }) { return api.get(`admin/learn-analytics/students/${userId}`, { params }) } export function getGradeTrend(params: { grade?: string; compare?: string }) { return api.get('admin/problem-analytics/grades', { params }) } // 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) satisfies Partial) } export function updateAchievement(data: Partial) { return api.put(`admin/achievements/${data.id}`, toAchievementBody(data) satisfies Partial) } export function deleteAchievement(id: number) { return api.delete(`admin/achievements/${id}`) } // ---------- 课堂任务(class_lesson) ---------- export function getAdminLessons(offset: number, limit: number, filters: { className: string; teacher: number | null; mode: string }) { return api.get('admin/lessons', { params: { offset, limit, className: filters.className, teacher: filters.teacher ?? '', mode: filters.mode }, }) } export function createAdminLesson(body: AdminLessonRequest) { return api.post<{ id: number }>('admin/lessons', body) } export function updateAdminLesson(id: number, body: AdminLessonRequest) { return api.put(`admin/lessons/${id}`, body) } export function deleteAdminLesson(id: number) { return api.delete(`admin/lessons/${id}`) }