feat(比赛): 比赛页一页看题目和自己的名次;排名竞争感做足、结束有领奖台;老师考试中看全班、考完看成绩;新建比赛一屏做完
Deploy / deploy (push) Canceled after 0s

- 学生比赛页(设计稿 B):左边题目带几人做对、谁最先做对;右边我第几名、5 分钟里升了几名、再做对 1 道能到第几名、我前后的人
- 期中、期末考试中只有自己的进度,写明「排名考完公布」,题目页的排名入口也藏起来
- 排名页:按班筛、名次变化、最先做对的深绿格、自己那行钉住;结束后前三名领奖台 + 我的成绩卡;排名曲线和学生的「导出数据」去掉
- 老师的「全班情况」:卡在一道题、15 分钟没交的放最上面,没进来的人对上看板的请假;考完变「成绩」,点格子看代码、标看过了(代替后台审核页),导出成绩
- 比赛列表:进行中的一大块「接着比」,已结束的写我第几名,前三带奖杯
- 新建比赛:默认 10 分钟后开始,时长点按钮,搜题号把题加进来自动编号、拖动排顺序

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 20:58:59 +08:00
1 parent b94e028ff4
commit 151bbdcb50
19 files changed
+3768 -743

No files matched your search

+49 -1
View File
@@ -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<Contest | null>(null)
const problems = ref<ProblemRow[]>([])
const now = ref(0)
/** 整张榜(学生比赛页右边的名次、排名页共用),进行中由页面每 10 秒刷一次 */
const scoreboard = ref<ContestScoreboard | null>(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,