diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index fdb7b18..d82c3fb 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -7,8 +7,6 @@ import type { GenerateSqlTestCaseResponse, RenameTagResponse, SqlTestCaseScript, - AcmHelperItem, - ContestSubmissionInfo, AdminAiReport, AdminAiReportList, StuckProblem, @@ -357,6 +355,11 @@ export function addProblemForContest(contestID: string, problemID: number, displ }) } +/** 比赛题按这个顺序重新编号 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") } @@ -478,14 +481,6 @@ export function makeProblemPublic(id: number, displayId: string) { } // 比赛辅助检查 -export function getACMHelperList(contestId: number) { - // acInfo 在契约里是 Record(acm_contest_rank 的 JSONB 原文), - // 组件侧按 ContestSubmissionInfo 读,收窄放在这里 - return api.get & { acInfo: ContestSubmissionInfo }>>( - `admin/contests/${contestId}/acm-helper`, - ) -} - export function updateACMHelperChecked( contest_id: number, rank_id: number, diff --git a/apps/web/src/admin/contest/components/Actions.vue b/apps/web/src/admin/contest/components/Actions.vue index dd3dfaf..8e16ed4 100644 --- a/apps/web/src/admin/contest/components/Actions.vue +++ b/apps/web/src/admin/contest/components/Actions.vue @@ -16,18 +16,9 @@ function goEdit() { }) } -function goEditProblems() { - router.push({ - name: "admin contest problem list", - params: { contestID: props.contest.id }, - }) -} - -function goACMHelper() { - router.push({ - name: "admin contest helper", - params: { contestID: props.contest.id }, - }) +// 原来的「审核」页并进了前台比赛页老师的「全班情况 / 成绩」:点格子看代码、标看过了 +function goClass() { + window.open(`/contest/${props.contest.id}/class`, "_blank") } async function clone() { @@ -45,9 +36,10 @@ async function clone() { diff --git a/apps/web/src/admin/contest/detail.vue b/apps/web/src/admin/contest/detail.vue index 093ffa9..9c5ec37 100644 --- a/apps/web/src/admin/contest/detail.vue +++ b/apps/web/src/admin/contest/detail.vue @@ -1,57 +1,40 @@ {{ contestBar.notStarted ? "还没开始" : "已结束" }} - 排名 + 排名 diff --git a/apps/web/src/oj/problem/components/ProblemInfo.vue b/apps/web/src/oj/problem/components/ProblemInfo.vue index 6c8d90e..627fc8e 100644 --- a/apps/web/src/oj/problem/components/ProblemInfo.vue +++ b/apps/web/src/oj/problem/components/ProblemInfo.vue @@ -197,6 +197,7 @@ function segmentWidth(value: number, total: number) { 现在只看得到你自己的提交,别人几次做对、错在哪,结束后一起公布。 diff --git a/apps/web/src/oj/store/contest.ts b/apps/web/src/oj/store/contest.ts index 1c700e6..8333207 100644 --- a/apps/web/src/oj/store/contest.ts +++ b/apps/web/src/oj/store/contest.ts @@ -3,8 +3,15 @@ import { formatISO, getTime, parseISO } from "date-fns" import { useUserStore } from "shared/store/user" import { ContestStatus, ContestType } from "utils/constants" import { duration, secondsToDuration } from "utils/functions" +import { isExamTag, type ContestScoreboard } from "@oj2/contract" import type { Contest, ProblemRow } from "utils/types" -import { checkContestPassword, getContest, getContestAccess, getContestProblems } from "../api" +import { + checkContestPassword, + getContest, + getContestAccess, + getContestProblems, + getContestScoreboard, +} from "../api" export const useContestStore = defineStore("contest", () => { const userStore = useUserStore() @@ -12,6 +19,10 @@ export const useContestStore = defineStore("contest", () => { const contest = ref(null) const problems = ref([]) const now = ref(0) + /** 整张榜(学生比赛页右边的名次、排名页共用),进行中由页面每 10 秒刷一次 */ + const scoreboard = ref(null) + /** 上次拉到榜的时刻(本机时间),「8 秒前更新」用 */ + const scoreboardAt = ref(0) let timer = 0 @@ -54,8 +65,37 @@ export const useContestStore = defineStore("contest", () => { (userStore.isAuthed && contest.value?.createdBy.id === userStore.user!.id), ) + /** 老师(出题人、超管、其他老师):看得到全班情况、真名,期中期末进行中也看得到排名 */ + const isTeacher = computed(() => isContestAdmin.value || userStore.isTeacherOrAbove) + const isPrivate = computed(() => contest.value!.contestType === ContestType.private) + /** 期中、期末进行中,学生看不到排名和每题做对人数(后端同样拦着,这里只管界面) */ + const rankHidden = computed( + () => + !!contest.value && + isExamTag(contest.value.tag) && + contestStatus.value === ContestStatus.underway && + !isTeacher.value, + ) + + /** 能不能看题目(也就能看榜):开始了、密码对了;老师 / 出题人随时 */ + const canEnter = computed(() => { + if (!contest.value) return false + if (isContestAdmin.value) return true + if (contestStatus.value === ContestStatus.not_started) return false + return !isPrivate.value || access.value + }) + + async function loadScoreboard(contestID: string) { + try { + scoreboard.value = await getContestScoreboard(contestID) + scoreboardAt.value = Date.now() + } catch { + // 没开始 / 没密码时拉不到,界面按 canEnter 自己会挡住 + } + } + async function init(contestID: string) { problems.value = [] const res = await getContest(contestID) @@ -81,6 +121,7 @@ export const useContestStore = defineStore("contest", () => { function clear() { contest.value = null + scoreboard.value = null problems.value = [] toggleAccess(false) now.value = 0 @@ -116,6 +157,13 @@ export const useContestStore = defineStore("contest", () => { contest, contestStatus, isContestAdmin, + isTeacher, + rankHidden, + canEnter, + scoreboard, + scoreboardAt, + loadScoreboard, + now, access, problems, isPrivate, diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts index d87987c..0175a32 100644 --- a/apps/web/src/routes.ts +++ b/apps/web/src/routes.ts @@ -55,6 +55,14 @@ export const ojs: RouteRecordRaw = { meta: { requiresAuth: true }, name: "contest rank", }, + // 老师的「全班情况」,考完是「成绩」(代替原来后台的「审核」页) + { + path: "class", + component: () => import("oj/contest/pages/class.vue"), + props: true, + meta: { requiresAuth: true }, + name: "contest class", + }, ], }, { @@ -244,13 +252,6 @@ export const admins: RouteRecordRaw = { props: true, meta: { requiresTeacherAdmin: true }, }, - { - path: "contest/:contestID/helper", - name: "admin contest helper", - component: () => import("admin/contest/helper.vue"), - props: true, - meta: { requiresTeacherAdmin: true }, - }, // 只有super_admin可以访问的路由 { path: "announcement/list", diff --git a/apps/web/src/utils/functions.ts b/apps/web/src/utils/functions.ts index 9e164b0..afa9f26 100644 --- a/apps/web/src/utils/functions.ts +++ b/apps/web/src/utils/functions.ts @@ -212,6 +212,24 @@ export function readableDuration(seconds: number): string { return rest ? `${hours} 小时 ${rest} 分` : `${hours} 小时` } +/** + * 比赛有多长,给人看的:「40 分钟」「1 小时 30 分钟」「3 天」。 + * `duration()` 会拆出「1月6天59分钟」这种,比赛列表里读不下去;一天以上只说几天 + */ +export function contestLength(start: Date | string, end: Date | string): string { + const minutes = Math.round( + (normalizeDate(end).getTime() - normalizeDate(start).getTime()) / 60_000, + ) + if (!Number.isFinite(minutes) || minutes <= 0) return "" + if (minutes < 60) return `${minutes} 分钟` + if (minutes < 24 * 60) { + const h = Math.floor(minutes / 60) + const m = minutes % 60 + return m ? `${h} 小时 ${m} 分钟` : `${h} 小时` + } + return `${Math.round(minutes / (24 * 60))} 天` +} + export function durationToDays(start: Date | string, end: Date | string): string { const durationObj = getDurationObject(start, end) const units = [ diff --git a/apps/web/src/utils/types.ts b/apps/web/src/utils/types.ts index 09e0667..0afff09 100644 --- a/apps/web/src/utils/types.ts +++ b/apps/web/src/utils/types.ts @@ -329,12 +329,6 @@ export type BlankContest = Omit< */ export type { ContestSubmissionInfo } from "@oj2/contract" -/** - * 榜单行。`submissionInfo` 的收窄(JSONB 原文的 snake_case 形状)已经搬进 - * `contestRankItemSchema`,这里不再需要 Omit + 覆盖。 - */ -export type ContestRank = import("@oj2/contract").ContestRankItem - export type { WebsiteConfig, OnlineCount } from "@oj2/contract" export type { @@ -344,7 +338,6 @@ export type { OrphanTestCase, AdminUser, AdminUserList, - AcmHelperItem, AdminContestList, AdminProblemSetProgress, AdminProblemSetProblem,