原来写代码当默认值不写出来,只剩「C语言」「Python」,看着和别的分不开。 现在三种都用不同颜色的标签写出来,筛选多一个「做什么」。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
806 lines
26 KiB
TypeScript
806 lines
26 KiB
TypeScript
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<DashboardInfo>('admin/dashboard')
|
||
}
|
||
|
||
/** 后台首页「抽一个人」:从这个班(班号)随机拿 10 个显示用的名字 */
|
||
export function randomUser10(classroom: string) {
|
||
return api.get<string[]>('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<AdminProblemList>(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<AdminProblemList>('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<AdminProblem>(`admin/problems/${(problem as AdminProblem).id}`, toProblemBody(problem) satisfies Partial<UpdateProblemRequest>)
|
||
}
|
||
|
||
export function toggleProblemVisible(problemID: number) {
|
||
return api.put<{ visible: boolean }>(`admin/problems/${problemID}/visibility`)
|
||
}
|
||
|
||
export function generateFlowchartFromPythonCode(python: string) {
|
||
return api.post<{ doc: Record<string, unknown> }>('admin/problems/flowchart', {
|
||
python,
|
||
} satisfies GenerateFlowchartRequest)
|
||
}
|
||
|
||
export function getProblem(id: string | number) {
|
||
return api.get<AdminProblem>(`admin/problems/${id}`)
|
||
}
|
||
|
||
// 标签管理
|
||
export function getTagAdminList(keyword = '') {
|
||
return api.get<AdminTag[]>('admin/problem-tags', { params: { keyword } })
|
||
}
|
||
|
||
export function renameTag(id: number, name: string) {
|
||
return api.put<RenameTagResponse>(`admin/problem-tags/${id}`, { name } satisfies RenameTagRequest)
|
||
}
|
||
|
||
export function setTagCategory(id: number, category: TagCategory) {
|
||
return api.put<null>(`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<BatchProblemTagResponse>('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<AdminUserRank>('admin/rankings/users', {
|
||
params: { offset, limit, keyword },
|
||
})
|
||
}
|
||
|
||
// 用户列表
|
||
export function getUserList(
|
||
offset = 0,
|
||
limit = 10,
|
||
type = '',
|
||
keyword: string,
|
||
orderBy = '',
|
||
/** 班级,NO_CLASS 是「没填班级」 */
|
||
className = '',
|
||
) {
|
||
return api.get<AdminUserList>('admin/users', {
|
||
params: {
|
||
offset,
|
||
limit,
|
||
keyword,
|
||
type,
|
||
orderBy,
|
||
className: className || undefined,
|
||
},
|
||
})
|
||
}
|
||
|
||
/** 批量建号之前查重:这些用户名哪些已经有了 */
|
||
/** 批量建号查重:这个班里已经有的同名账号,和每个名字会建成的用户名 */
|
||
export function checkUsernames(className: string, names: string[]) {
|
||
return api.post<CheckUsernamesResponse>('admin/users/check', {
|
||
className,
|
||
names,
|
||
} satisfies CheckUsernamesRequest)
|
||
}
|
||
|
||
// 编辑用户
|
||
export function editUser(user: AdminUserForm) {
|
||
return api.put<AdminUser>(`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<AdminContestList>('admin/contests', {
|
||
params: { offset, limit, keyword },
|
||
})
|
||
}
|
||
|
||
// 上传图片
|
||
export async function uploadImage(file: File): Promise<string> {
|
||
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<UploadTestCaseResponse>('admin/test-cases', form)
|
||
}
|
||
|
||
// SQL 题测试点预览:后端跑一遍初始化脚本+标准答案,返回数据表和期望结果展示数据
|
||
export function previewSQLTestcase(data: {
|
||
initSql: string
|
||
refSql: string
|
||
mode: 'query' | 'modify'
|
||
/** 是不是题面上展示的那组(第 1 组):增删改题只有它必须改到行 */
|
||
shown: boolean
|
||
}) {
|
||
return api.post<SqlDisplay>('admin/sql-test-cases/preview', data satisfies SqlPreviewRequest)
|
||
}
|
||
|
||
// 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序)
|
||
export function getSQLTestcaseScripts(problemId: number) {
|
||
return api.get<SqlTestCaseScript[]>(`admin/problems/${problemId}/sql-scripts`)
|
||
}
|
||
|
||
// AI 根据标准答案生成一个 SQL 测试点初始化脚本
|
||
export function generateSQLTestcase(data: { refSql: string; mode: 'query' | 'modify' }) {
|
||
return api.post<GenerateSqlTestCaseResponse>('admin/sql-test-cases/generate', data satisfies GenerateSqlTestCaseRequest)
|
||
}
|
||
|
||
// 编程题出题页:读回已有测试点的原文(太大的只给数量和大小)
|
||
export function getTestCaseFiles(problemId: number) {
|
||
return api.get<TestCaseFiles>(`admin/problems/${problemId}/test-case-files`)
|
||
}
|
||
|
||
// 拿标准答案自测语法要求
|
||
export function checkAstRules(data: AstCheckRequest) {
|
||
return api.post<AstCheckResponse>('admin/ast-check', data satisfies AstCheckRequest)
|
||
}
|
||
|
||
// 出题页「挑战」的候选:拿这道题历史上做对的代码挖
|
||
export function getChallengeSuggestions(problemId: number, language: string) {
|
||
return api.post<ChallengeSuggestionsResponse>(`admin/problems/${problemId}/challenge-suggestions`, {
|
||
language,
|
||
})
|
||
}
|
||
|
||
// AI 按题面和标准答案想几组测试输入(输出由标准答案跑)
|
||
export function generateTestInputs(data: GenerateTestInputsRequest) {
|
||
return api.post<GenerateTestInputsResponse>('admin/test-inputs/generate', data satisfies GenerateTestInputsRequest)
|
||
}
|
||
|
||
/** 出站补默认值。字段名两边已经一致,不再做键名转换 */
|
||
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
||
const p = problem as Partial<Omit<AdminProblem, 'flowchartData' | 'challenges'>> & {
|
||
flowchartData?: Record<string, unknown> | null
|
||
challenges?: Record<string, ChallengeInput[]> | 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<AdminProblem>('admin/problems', toProblemBody(problem) satisfies Partial<CreateProblemRequest>)
|
||
}
|
||
|
||
export function createContestProblem(problem: BlankProblem) {
|
||
// contestId 由 detail.vue 在提交前写进 problem 对象
|
||
return api.post<AdminProblem>(`admin/contests/${problem.contestId}/problems`, toProblemBody(problem) satisfies Partial<CreateProblemRequest>)
|
||
}
|
||
|
||
/** 组件里的比赛对象是 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<AdminContest>('admin/contests', toContestBody(contest) satisfies CreateContestRequest)
|
||
}
|
||
|
||
export function editContest(contest: AdminContest | BlankContest) {
|
||
return api.put<AdminContest>(`admin/contests/${(contest as AdminContest).id}`, toContestBody(contest) satisfies UpdateContestRequest)
|
||
}
|
||
|
||
export function cloneContest(contestId: number) {
|
||
return api.post<AdminContest>(`admin/contests/${contestId}/clone`)
|
||
}
|
||
|
||
export function getContest(id: string) {
|
||
return api.get<AdminContest>(`admin/contests/${id}`)
|
||
}
|
||
|
||
export function addProblemForContest(contestID: string, problemID: number, displayID: string) {
|
||
return api.post<AdminProblem>(`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<WebsiteConfig>('admin/website')
|
||
}
|
||
|
||
export function editWebsite(data: WebsiteConfig) {
|
||
return api.post<WebsiteConfig>('admin/website', data satisfies UpdateWebsiteConfigRequest)
|
||
}
|
||
|
||
export function listInvalidTestcases() {
|
||
return api.get<OrphanTestCase[]>('admin/orphan-test-cases')
|
||
}
|
||
|
||
export function pruneInvalidTestcases(id?: string) {
|
||
return api.delete('admin/orphan-test-cases', { params: { id } })
|
||
}
|
||
|
||
export function listOrphanImages() {
|
||
return api.get<OrphanImage[]>('admin/orphan-images')
|
||
}
|
||
|
||
/** 不带参数是全删;带 folder 只删那个目录的,再带 name 只删一张 */
|
||
export function pruneOrphanImages(target?: Pick<OrphanImage, 'folder'> & { name?: string }) {
|
||
return api.delete<{ deleted: number }>('admin/orphan-images', { params: target })
|
||
}
|
||
|
||
export function getJudgeServer() {
|
||
return api.get<JudgeServerList>('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<AdminAnnouncement>(`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<AdminAnnouncement>(`admin/announcements/${id}`, body satisfies UpdateAnnouncementRequest)
|
||
}
|
||
|
||
export function createAnnouncement(announcement: AnnouncementEdit) {
|
||
const { id: _id, ...body } = announcement
|
||
return api.post<AdminAnnouncement>('admin/announcements', body satisfies CreateAnnouncementRequest)
|
||
}
|
||
|
||
export function getTutorialList() {
|
||
return api.get<{ [key: string]: TutorialListItem[] }>('admin/tutorials')
|
||
}
|
||
|
||
export function getTutorial(id: number) {
|
||
return api.get<Tutorial>(`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<Tutorial>('admin/tutorials', toTutorialBody(data) satisfies CreateTutorialRequest)
|
||
}
|
||
|
||
export function updateTutorial(data: TutorialEdit) {
|
||
return api.put<Tutorial>(`admin/tutorials/${data.id}`, toTutorialBody(data) satisfies UpdateTutorialRequest)
|
||
}
|
||
|
||
export function setTutorialProblems(id: number, problemIds: number[]) {
|
||
return api.put<Tutorial>(`admin/tutorials/${id}/problems`, {
|
||
problemIds,
|
||
} satisfies SetTutorialProblemsRequest)
|
||
}
|
||
|
||
export function getTutorialProblemCandidates(keyword: string) {
|
||
return api.get<TutorialProblemCandidate[]>('admin/tutorial-problem-candidates', {
|
||
params: keyword ? { keyword } : {},
|
||
})
|
||
}
|
||
|
||
export function reorderTutorials(ids: number[]) {
|
||
return api.put<null>('admin/tutorial-order', { ids } satisfies ReorderTutorialsRequest)
|
||
}
|
||
|
||
export function renameTutorialProject(type: 'python' | 'c', from: string, to: string) {
|
||
return api.put<null>('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<Tutorial>(`admin/tutorials/${id}/visibility`, {
|
||
isPublic,
|
||
} satisfies SetTutorialVisibilityRequest)
|
||
}
|
||
|
||
export function getAdminExercises(tutorialId: number) {
|
||
return api.get<Exercise[]>(`admin/tutorials/${tutorialId}/exercises`)
|
||
}
|
||
|
||
export async function createExercise(data: { tutorialId: number; type: ExerciseType; data: Record<string, unknown>; order: number }) {
|
||
return api.post<Exercise>('admin/exercises', data satisfies CreateExerciseRequest)
|
||
}
|
||
|
||
export async function updateExercise(data: { id: number; type: ExerciseType; data: Record<string, unknown>; order: number; resetAttempts: boolean }) {
|
||
return api.put<Exercise>(`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<AdminProblem>(`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<AdminProblemSetList>('admin/problem-sets', {
|
||
params: { offset, limit, keyword },
|
||
})
|
||
}
|
||
|
||
export function getProblemSetDetail(id: number) {
|
||
return api.get<AdminProblemSet>(`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<AdminProblemSet>('admin/problem-sets', data satisfies CreateProblemSetRequest)
|
||
}
|
||
|
||
export function editProblemSet(id: number, data: ProblemSetBody) {
|
||
return api.put<AdminProblemSet>(`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<AdminProblemSet>(`admin/problem-sets/${id}/visibility`)
|
||
}
|
||
|
||
// 专题题目管理 API
|
||
export function getProblemSetProblems(problemSetId: number) {
|
||
return api.get<AdminProblemSetProblem[]>(`admin/problem-sets/${problemSetId}/problems`)
|
||
}
|
||
|
||
/** 一次加好几道(题号),按给的顺序接在最后;找不到的、已经在里面的分开报回来 */
|
||
export function addProblemsToSet(problemSetId: number, problemIds: string[]) {
|
||
return api.post<AddProblemsToSetResult>(`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<AdminProblemSetBadge[]>(`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<AdminProblemSetBadge>(`admin/problem-sets/${problemSetId}/badges`, data satisfies CreateProblemSetBadgeRequest)
|
||
}
|
||
|
||
export function editProblemSetBadge(problemSetId: number, badgeId: number, data: BadgeBody) {
|
||
return api.put<AdminProblemSetBadge>(`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<StuckProblem[]>('admin/problem-analytics/stuck', { params })
|
||
}
|
||
|
||
export function getStuckProblemDetail(id: number, params: { className?: string; start?: string }) {
|
||
return api.get<StuckProblemDetail>(`admin/problem-analytics/stuck/${id}`, { params })
|
||
}
|
||
|
||
export function getLearnBoard(params: { type: 'python' | 'c'; className?: string }) {
|
||
return api.get<LearnBoard>('admin/learn-analytics/board', { params })
|
||
}
|
||
|
||
export function getLearnExercises(params: { type: 'python' | 'c'; className?: string }) {
|
||
return api.get<LearnExerciseProgressList>('admin/learn-analytics/exercises', {
|
||
params,
|
||
})
|
||
}
|
||
|
||
export function getLearnExerciseAttempts(exerciseId: number, params: { className?: string }) {
|
||
return api.get<LearnExerciseAttemptList>(`admin/learn-analytics/exercises/${exerciseId}/attempts`, { params })
|
||
}
|
||
|
||
export function getLearnStudent(userId: number, params: { type: 'python' | 'c' }) {
|
||
return api.get<LearnStudentDetail>(`admin/learn-analytics/students/${userId}`, { params })
|
||
}
|
||
|
||
export function getGradeTrend(params: { grade?: string; compare?: string }) {
|
||
return api.get<GradeTrend>('admin/problem-analytics/grades', { params })
|
||
}
|
||
|
||
// AI 学习分析报告
|
||
export function getAIReportList(offset = 0, limit = 10, username = '') {
|
||
return api.get<AdminAiReportList>('admin/ai/reports', {
|
||
params: { offset, limit, username: username || undefined },
|
||
})
|
||
}
|
||
|
||
export function getAIReportDetail(id: number) {
|
||
return api.get<AdminAiReport>(`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<AdminAiReportList>('admin/ai/reports', {
|
||
params: { pinnedOnly: 'true' },
|
||
})
|
||
}
|
||
|
||
// ==================== 成就 ====================
|
||
|
||
import type { AdminAchievement, AchievementMetric as MetricOption, AdminUserRank } from '@oj2/contract'
|
||
export type { AdminAchievement, MetricOption }
|
||
|
||
function toAchievementBody(data: Partial<AdminAchievement>) {
|
||
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<AdminAchievement[]>('admin/achievements')
|
||
}
|
||
|
||
export function getMetricOptions() {
|
||
return api.get<MetricOption[]>('admin/achievement-metrics')
|
||
}
|
||
|
||
export function createAchievement(data: Partial<AdminAchievement>) {
|
||
return api.post<AdminAchievement>('admin/achievements', toAchievementBody(data) satisfies Partial<CreateAchievementRequest>)
|
||
}
|
||
|
||
export function updateAchievement(data: Partial<AdminAchievement>) {
|
||
return api.put<AdminAchievement>(`admin/achievements/${data.id}`, toAchievementBody(data) satisfies Partial<UpdateAchievementRequest>)
|
||
}
|
||
|
||
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<AdminLessonList>('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<null>(`admin/lessons/${id}`, body)
|
||
}
|
||
|
||
export function deleteAdminLesson(id: number) {
|
||
return api.delete<null>(`admin/lessons/${id}`)
|
||
}
|