Files
OJ2/apps/web/src/admin/api.ts
T
xuyueandClaude Opus 5.5 b09d479893 style(课堂任务): 后台列表分清写代码、先画再写、画流程图,能按做法筛
原来写代码当默认值不写出来,只剩「C语言」「Python」,看着和别的分不开。
现在三种都用不同颜色的标签写出来,筛选多一个「做什么」。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:05:23 +08:00

806 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`)
}