diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index d82c3fb..0ed99a4 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -2,7 +2,6 @@ import api from "utils/api" import { toAdminProblemRow } from "admin/transforms" import type { AcTrend, - AdminProblemSetProgress, BatchProblemTagResponse, GenerateSqlTestCaseResponse, RenameTagResponse, @@ -40,6 +39,7 @@ import type { AdminProblemSetList, AdminProblemSet, AdminProblemSetProblem, + AddProblemsToSetResult, TutorialListItem, TagCategory, } from "utils/types" @@ -495,15 +495,9 @@ export function updateACMHelperChecked( } // 题单管理 API -export function getProblemSetList( - offset = 0, - limit = 10, - keyword = "", - difficulty = "", - status = "", -) { +export function getProblemSetList(offset = 0, limit = 10, keyword = "") { return api.get("admin/problem-sets", { - params: { offset, limit, keyword, difficulty, status }, + params: { offset, limit, keyword }, }) } @@ -511,33 +505,20 @@ export function getProblemSetDetail(id: number) { return api.get(`admin/problem-sets/${id}`) } -interface ProblemSetBody { - title?: string - description?: string - difficulty?: AdminProblemSet["difficulty"] - status?: AdminProblemSet["status"] - // 表单里是 Date,出站要 ISO 串 - endTime?: Date | null - visible?: boolean -} - -function toProblemSetBody(data: ProblemSetBody) { - return { - title: data.title, - description: data.description ?? "", - difficulty: data.difficulty ?? "Easy", - status: data.status ?? "active", - endTime: data.endTime ? new Date(data.endTime).toISOString() : null, - visible: data.visible ?? true, - } +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", toProblemSetBody(data)) + return api.post("admin/problem-sets", data) } -export function editProblemSet(data: ProblemSetBody & { id: number }) { - return api.put(`admin/problem-sets/${data.id}`, toProblemSetBody(data)) +export function editProblemSet(id: number, data: ProblemSetBody) { + return api.put(`admin/problem-sets/${id}`, data) } export function deleteProblemSet(id: number) { @@ -548,49 +529,28 @@ export function toggleProblemSetVisible(id: number) { return api.put(`admin/problem-sets/${id}/visibility`) } -export function updateProblemSetStatus(id: number, status: string) { - return api.put(`admin/problem-sets/${id}/status`, { status }) -} - // 题单题目管理 API export function getProblemSetProblems(problemSetId: number) { return api.get(`admin/problem-sets/${problemSetId}/problems`) } -export function addProblemToSet( - problemSetId: number, - data: { - problemId: string - order?: number - isRequired?: boolean - score?: number - hint?: string - }, -) { - return api.post(`admin/problem-sets/${problemSetId}/problems`, { - problemId: data.problemId, - order: data.order ?? 0, - isRequired: data.isRequired ?? true, - score: data.score ?? 0, - hint: data.hint ?? "", +/** 一次加好几道(题号),按给的顺序接在最后;找不到的、已经在里面的分开报回来 */ +export function addProblemsToSet(problemSetId: number, problemIds: string[]) { + return api.post(`admin/problem-sets/${problemSetId}/problems`, { + problemIds, }) } -export function editProblemInSet( - problemSetId: number, - problemSetProblemId: number, - data: { - order?: number - isRequired?: boolean - score?: number - hint?: string - }, -) { - return api.put(`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`, data) +export function setProblemRequired(problemSetId: number, itemId: number, isRequired: boolean) { + return api.put(`admin/problem-sets/${problemSetId}/problems/${itemId}`, { isRequired }) } -export function removeProblemFromSet(problemSetId: number, problemSetProblemId: number) { - return api.delete(`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`) +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 @@ -598,52 +558,26 @@ export function getProblemSetBadges(problemSetId: number) { return api.get(`admin/problem-sets/${problemSetId}/badges`) } -interface BadgeBody { - name?: string - description?: string - icon?: string - conditionType?: AdminProblemSetBadge["conditionType"] - conditionValue?: number -} - -function toBadgeBody(data: BadgeBody) { - return { - name: data.name, - description: data.description ?? "", - icon: data.icon ?? "", - conditionType: data.conditionType, - conditionValue: data.conditionValue ?? 0, - } +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`, - toBadgeBody(data), - ) + 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}`, - toBadgeBody(data), - ) + 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}`) } -// 题单进度管理 API -// 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同 -export function getProblemSetProgress(problemSetId: number) { - return api.get(`admin/problem-sets/${problemSetId}/progress`) -} - -export function removeUserFromProblemSet(problemSetId: number, userId: number) { - return api.delete(`admin/problem-sets/${problemSetId}/progress/${userId}`) -} - // 学生卡点分析 export function getStuckProblems() { return api.get("admin/problem-analytics/stuck") diff --git a/apps/web/src/admin/problemset/components/Actions.vue b/apps/web/src/admin/problemset/components/Actions.vue deleted file mode 100644 index d736360..0000000 --- a/apps/web/src/admin/problemset/components/Actions.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/components/BadgeManagement.vue b/apps/web/src/admin/problemset/components/BadgeManagement.vue deleted file mode 100644 index bbb27ec..0000000 --- a/apps/web/src/admin/problemset/components/BadgeManagement.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/components/BadgeModal.vue b/apps/web/src/admin/problemset/components/BadgeModal.vue index 312bec4..7dd44db 100644 --- a/apps/web/src/admin/problemset/components/BadgeModal.vue +++ b/apps/web/src/admin/problemset/components/BadgeModal.vue @@ -1,155 +1,168 @@ + + diff --git a/apps/web/src/admin/problemset/components/ProblemManagement.vue b/apps/web/src/admin/problemset/components/ProblemManagement.vue deleted file mode 100644 index 50d7236..0000000 --- a/apps/web/src/admin/problemset/components/ProblemManagement.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/components/ProblemModal.vue b/apps/web/src/admin/problemset/components/ProblemModal.vue deleted file mode 100644 index 88102b6..0000000 --- a/apps/web/src/admin/problemset/components/ProblemModal.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/components/ProblemSetInfo.vue b/apps/web/src/admin/problemset/components/ProblemSetInfo.vue deleted file mode 100644 index 8185159..0000000 --- a/apps/web/src/admin/problemset/components/ProblemSetInfo.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/components/ProgressManagement.vue b/apps/web/src/admin/problemset/components/ProgressManagement.vue deleted file mode 100644 index ac2c8b9..0000000 --- a/apps/web/src/admin/problemset/components/ProgressManagement.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/detail.vue b/apps/web/src/admin/problemset/detail.vue deleted file mode 100644 index f07aef2..0000000 --- a/apps/web/src/admin/problemset/detail.vue +++ /dev/null @@ -1,261 +0,0 @@ - - - diff --git a/apps/web/src/admin/problemset/edit.vue b/apps/web/src/admin/problemset/edit.vue index 643515b..4fcf0a5 100644 --- a/apps/web/src/admin/problemset/edit.vue +++ b/apps/web/src/admin/problemset/edit.vue @@ -1,168 +1,820 @@ + + diff --git a/apps/web/src/admin/problemset/list.vue b/apps/web/src/admin/problemset/list.vue index ba52632..774d384 100644 --- a/apps/web/src/admin/problemset/list.vue +++ b/apps/web/src/admin/problemset/list.vue @@ -5,87 +5,68 @@ import { usePagination } from "shared/composables/pagination" import { parseTime } from "utils/functions" import type { AdminProblemSet } from "utils/types" import { getProblemSetList, toggleProblemSetVisible } from "../api" -import Actions from "./components/Actions.vue" -import { NTag, NSwitch } from "naive-ui" +import { NButton, NSwitch, NTag } from "naive-ui" +/** + * 后台题单列表。难度、状态两列和筛选拿掉了(线上全是简单 / 活跃,「状态 + 可见」合成了「公开」), + * 换成老师真正关心的:几道题、多少人加入 / 做完、布置到哪天。点一行进一屏编辑 + */ +const router = useRouter() const total = ref(0) const problemSets = ref([]) -interface ProblemSetQuery { - keyword: string - difficulty: string - status: string +const { query, clearQuery } = usePagination<{ keyword: string }>({ keyword: "" }) + +function edit(row: AdminProblemSet) { + router.push({ name: "admin problemset edit", params: { problemSetId: row.id } }) } -// 使用分页 composable -const { query, clearQuery } = usePagination({ - keyword: "", - difficulty: "", - status: "", -}) - -const difficultyOptions = [ - { label: "全部", value: "" }, - { label: "简单", value: "Easy" }, - { label: "中等", value: "Medium" }, - { label: "困难", value: "Hard" }, -] - -const statusOptions = [ - { label: "全部", value: "" }, - { label: "活跃", value: "active" }, - { label: "已归档", value: "archived" }, - { label: "草稿", value: "draft" }, -] - const columns: DataTableColumn[] = [ - { title: "ID", key: "id", width: 80 }, - { title: "标题", key: "title", minWidth: 200 }, - { title: "描述", key: "description", minWidth: 300, ellipsis: true }, + { title: "ID", key: "id", width: 70 }, + { + title: "题单", + key: "title", + minWidth: 200, + render: (row) => + h(NButton, { text: true, onClick: () => edit(row) }, { default: () => row.title }), + }, + { title: "题数", key: "problemsCount", width: 70 }, + { + title: "加入 / 做完", + key: "participantCount", + width: 110, + render: (row) => `${row.participantCount} / ${row.completedCount}`, + }, + { title: "奖章", key: "badgeCount", width: 70 }, + { + title: "布置", + key: "assignedUntil", + width: 130, + render: (row) => + row.assigning + ? h( + NTag, + { type: "info", size: "small", bordered: false }, + { default: () => `到 ${parseTime(row.assignedUntil!, "M月D日")}` }, + ) + : "—", + }, { title: "创建者", - key: "created_by", - width: 120, + key: "createdBy", + width: 110, render: (row) => row.createdBy.username, }, - { - title: "难度", - key: "difficulty", - width: 100, - render: (row) => { - const difficultyMap = { - Easy: { type: "success" as const, text: "简单" }, - Medium: { type: "warning" as const, text: "中等" }, - Hard: { type: "error" as const, text: "困难" }, - } - const config = difficultyMap[row.difficulty] - return h(NTag, { type: config.type, size: "small" }, { default: () => config.text }) - }, - }, - { - title: "状态", - key: "status", - width: 100, - render: (row) => { - const statusMap = { - active: { type: "success" as const, text: "活跃" }, - archived: { type: "default" as const, text: "已归档" }, - draft: { type: "info" as const, text: "草稿" }, - } - const config = statusMap[row.status] - return h(NTag, { type: config.type, size: "small" }, { default: () => config.text }) - }, - }, { title: "创建时间", - key: "create_time", - width: 180, - render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"), + key: "createTime", + width: 120, + render: (row) => parseTime(row.createTime, "YYYY-MM-DD"), }, { - title: "可见", + title: "公开", key: "visible", - width: 100, + width: 80, render: (row) => h(NSwitch, { value: row.visible, @@ -94,84 +75,41 @@ const columns: DataTableColumn[] = [ onUpdateValue: () => toggleVisible(row.id), }), }, - { - title: "选项", - key: "actions", - fixed: "right", - width: 300, - render: (row) => - h(Actions, { - problemSetId: row.id, - onUpdated: listProblemSets, - }), - }, ] async function listProblemSets() { if (query.page < 1) query.page = 1 const offset = (query.page - 1) * query.limit - const res = await getProblemSetList( - offset, - query.limit, - query.keyword, - query.difficulty, - query.status, - ) + const res = await getProblemSetList(offset, query.limit, query.keyword) total.value = res.total problemSets.value = res.results } async function toggleVisible(problemSetId: number) { - await toggleProblemSetVisible(problemSetId) - problemSets.value = problemSets.value.map((it) => { - if (it.id === problemSetId) { - it.visible = !it.visible - } - return it - }) + const updated = await toggleProblemSetVisible(problemSetId) + problemSets.value = problemSets.value.map((it) => (it.id === problemSetId ? updated : it)) } onMounted(listProblemSets) - -// 监听搜索关键词变化(防抖) -watchDebounced(() => query.keyword, listProblemSets, { - debounce: 500, - maxWait: 1000, -}) - -// 监听其他查询条件变化 -watch(() => [query.page, query.limit, query.difficulty, query.status], listProblemSets) +watchDebounced(() => query.keyword, listProblemSets, { debounce: 500, maxWait: 1000 }) +watch(() => [query.page, query.limit], listProblemSets) diff --git a/apps/web/src/oj/store/flowchart.ts b/apps/web/src/oj/store/flowchart.ts index 9872b47..7155e28 100644 --- a/apps/web/src/oj/store/flowchart.ts +++ b/apps/web/src/oj/store/flowchart.ts @@ -62,7 +62,7 @@ export const useFlowchartStore = defineStore("flowchart", () => { * 1.5 秒后跳回题单,都只认这一下 —— 进页面读到以前的 A 不该再跳一次 */ const evaluatedSeq = ref(0) - /** 这道题评完的每一次,早的在前;hidden = 加入题单之前、被藏起来的次数 */ + /** 这道题评完的每一次,早的在前;hidden = 题单布置期内被藏起来的次数 */ const scores = ref([]) const hiddenCount = ref(0) const submissionCount = computed(() => scores.value.length) diff --git a/apps/web/src/oj/submission/components/FlowchartPane.vue b/apps/web/src/oj/submission/components/FlowchartPane.vue index 873f2db..c40d1c1 100644 --- a/apps/web/src/oj/submission/components/FlowchartPane.vue +++ b/apps/web/src/oj/submission/components/FlowchartPane.vue @@ -202,7 +202,9 @@ const canRetry = computed( 这张流程图看不到 - 别人画的看不到;自己的在题单里做完这道题就能看到。 + 别人画的看不到;自己以前画的在题单布置期内先藏着,在题单里做对或者布置期结束就能看到。
diff --git a/apps/web/src/oj/submission/components/SubmissionPane.vue b/apps/web/src/oj/submission/components/SubmissionPane.vue index 29a4b08..582e0fc 100644 --- a/apps/web/src/oj/submission/components/SubmissionPane.vue +++ b/apps/web/src/oj/submission/components/SubmissionPane.vue @@ -1,4 +1,5 @@ - - diff --git a/apps/web/src/utils/types.ts b/apps/web/src/utils/types.ts index 0afff09..5b51903 100644 --- a/apps/web/src/utils/types.ts +++ b/apps/web/src/utils/types.ts @@ -195,31 +195,12 @@ export type { ProblemSetList, ProblemSetBadge, ProblemSetProblem, - ProblemSetProgress, - ProblemSetProgressList, + ProblemSetClassView, + ProblemSetClassStudent, + ProblemSetLock, UserBadge, } from "@oj2/contract" -export type { CompletedProblem } from "@oj2/contract" - -export interface CreateProblemSetData { - title: string - description: string - difficulty: "Easy" | "Medium" | "Hard" - status: "active" | "archived" | "draft" - endTime?: Date | null -} - -export interface EditProblemSetData { - id: number - title?: string - description?: string - difficulty?: "Easy" | "Medium" | "Hard" - status?: "active" | "archived" | "draft" - endTime?: Date | null - visible?: boolean -} - export interface Code { language: LANGUAGE value: string @@ -339,12 +320,11 @@ export type { AdminUser, AdminUserList, AdminContestList, - AdminProblemSetProgress, AdminProblemSetProblem, AdminProblemSet, AdminProblemSetList, AdminProblemSetBadge, - AddProblemToSetRequest, + AddProblemsToSetResult, UpdateProblemInSetRequest, CreateProblemSetBadgeRequest, AdminAiReport,