fix(题目): 修掉题目页重构审查查出的问题——提交途中换题会把结果算到新题上、运行例子把对的菱形判错、点评完「下一题」不跳等
题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修: 提交与换题 - 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、 弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号 - 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门 - 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题 - 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开 运行例子 - 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」 - C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out) - Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」 - 比赛里仍然把空格画成「·」、把不同的地方标红 入口与协作 - 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号 - 「看他交过什么」按整名匹配,不再混进学号相近的同学 - 协作中点上下文条、相似题、下一题开新标签,协作不断 - 题单入口给管理员的「⋯」里留「本题提交」 结果与题面 - 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 " - 流程图 A 级的学生标题下还写着「还没交过」 - 「我的提交」上方补回「答案错误 × 3」这样的计数 - 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评 - 「结果」页签有新结果没看时加粗上色,看过就恢复 - 空画布拖进第一个节点画布会跳一下;画布底下空着一条 - 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚 - 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB) - 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文 设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee66cef367
commit
e7b95d211d
34 files changed
+579
-108
No files matched your search
@@ -9,6 +9,7 @@ import { compressToBase64, copyToClipboard } from "utils/functions"
|
||||
import storage from "utils/storage"
|
||||
import { useDraftKey } from "./draftKey"
|
||||
import { useTeacherCollab } from "./teacherCollab"
|
||||
import { useProblemPageContext } from "./problemPageContext"
|
||||
|
||||
/**
|
||||
* 「课堂统计」弹窗开着没有。弹窗挂在题目页上(StatisticsModal),菜单可能在工具栏里、
|
||||
@@ -34,11 +35,15 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
const { isDesktop } = useBreakpoints()
|
||||
const teacherCollab = useTeacherCollab()
|
||||
const draftKey = useDraftKey()
|
||||
const ctx = useProblemPageContext()
|
||||
|
||||
const options = computed<DropdownOption[]>(() => {
|
||||
const items: DropdownOption[] = []
|
||||
// 「本题提交」挪到了「我的提交」抽屉的底部
|
||||
if (!statisticsInline.value && userStore.isTeacherOrAbove) {
|
||||
// 「本题提交」挪到了「我的提交」抽屉的底部;题单入口没有抽屉,给管理员角色留在这里
|
||||
if (ctx.value.allSubmissionsInMenu && userStore.isAdminRole) {
|
||||
items.push({ label: "本题提交", key: "allSubmissions" })
|
||||
}
|
||||
if (ctx.value.classStats && !statisticsInline.value && userStore.isTeacherOrAbove) {
|
||||
items.push({ label: "课堂统计", key: "statistics" })
|
||||
}
|
||||
if (codeStore.code.language !== "Flowchart") {
|
||||
@@ -92,6 +97,16 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
window.open(router.resolve(url).href, "_blank")
|
||||
}
|
||||
|
||||
function goAllSubmissions() {
|
||||
const href = router.resolve({
|
||||
name: "submissions",
|
||||
query: { problem: problem.value!._id },
|
||||
}).href
|
||||
// 协作中走新标签,跳走这一页协作就断了(设计文档第 9 节)
|
||||
if (teacherCollab.value) window.open(href, "_blank")
|
||||
else router.push(href)
|
||||
}
|
||||
|
||||
/** 菜单项被点了;不是这张菜单里的 key 返回 false,调用方自己处理(页签行的抽屉) */
|
||||
function select(key: string) {
|
||||
switch (key) {
|
||||
@@ -110,6 +125,9 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
case "edit":
|
||||
goEdit()
|
||||
return true
|
||||
case "allSubmissions":
|
||||
goAllSubmissions()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -32,6 +32,22 @@ interface EntryRules {
|
||||
backToProblemSet: boolean
|
||||
/** 举手求助 */
|
||||
help: boolean
|
||||
/**
|
||||
* 老师协作绑在这一页的编辑器上。房间只记了题号(`room.problemId` 是 displayId),
|
||||
* 比赛题的编号常是 1、2、3 —— 老师帮着学生做公开题 1 的时候打开比赛题 1,会绑上
|
||||
* 学生的文档、挂出协作条、禁掉语言选择
|
||||
*/
|
||||
collab: boolean
|
||||
/**
|
||||
* 教师的「课堂统计」。统计接口按 displayId 在公开题库里找题(`submission-statistics.ts`),
|
||||
* 比赛题号拿过去查到的是另一道公开题,或者 404
|
||||
*/
|
||||
classStats: boolean
|
||||
/**
|
||||
* 「看这道题所有人的提交」放进「⋯」(只给管理员角色)。它平时在「我的提交」抽屉底部,
|
||||
* 题单入口没有抽屉,老师就没地方点了
|
||||
*/
|
||||
allSubmissionsInMenu: boolean
|
||||
}
|
||||
|
||||
const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
@@ -43,6 +59,9 @@ const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
reviewAfterAccepted: true,
|
||||
backToProblemSet: false,
|
||||
help: true,
|
||||
collab: true,
|
||||
classStats: true,
|
||||
allSubmissionsInMenu: false,
|
||||
},
|
||||
// 题单里什么「以前的」都不给看:加入题单之前的提交、统计、点评都能拿来抄答案
|
||||
problemset: {
|
||||
@@ -55,6 +74,9 @@ const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
reviewAfterAccepted: false,
|
||||
backToProblemSet: true,
|
||||
help: true,
|
||||
collab: true,
|
||||
classStats: true,
|
||||
allSubmissionsInMenu: true,
|
||||
},
|
||||
contest: {
|
||||
drawers: ["info", "submission"],
|
||||
@@ -66,6 +88,9 @@ const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
reviewAfterAccepted: false,
|
||||
backToProblemSet: false,
|
||||
help: false,
|
||||
collab: false,
|
||||
classStats: false,
|
||||
allSubmissionsInMenu: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { useProblemPageContext } from "./problemPageContext"
|
||||
|
||||
/**
|
||||
* 老师正在这道题上协作:编辑器里是**学生的**代码,「我的提交」看的是学生的,
|
||||
@@ -8,11 +9,21 @@ import { useUserStore } from "shared/store/user"
|
||||
*/
|
||||
export function useTeacherCollab() {
|
||||
const userStore = useUserStore()
|
||||
const collabHere = useCollabHere()
|
||||
return computed(() => userStore.isTeacherOrAbove && collabHere.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 协作房间开在这道题上(老师、学生两边都算)。比赛入口一律不算:房间只记了题号,
|
||||
* 比赛题号和公开题撞号是常态,见 problemPageContext 的 `collab`
|
||||
*/
|
||||
export function useCollabHere() {
|
||||
const collabStore = useCollabStore()
|
||||
const problemStore = useProblemStore()
|
||||
const ctx = useProblemPageContext()
|
||||
return computed(
|
||||
() =>
|
||||
userStore.isTeacherOrAbove &&
|
||||
ctx.value.collab &&
|
||||
collabStore.room !== null &&
|
||||
collabStore.room.problemId === problemStore.problem?._id,
|
||||
)
|
||||
|
||||
@@ -18,10 +18,14 @@ export function useSubmissionMonitor() {
|
||||
// ==================== 轮询机制 ====================
|
||||
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
|
||||
async () => {
|
||||
if (!submissionId.value) return
|
||||
const id = submissionId.value
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
const res = await getSubmission(submissionId.value)
|
||||
const res = await getSubmission(id)
|
||||
// 请求在路上时换了题(reset)或者又交了一次:回来的是上一条,扔掉。
|
||||
// 不扔的话上一道题判对了,会把新题标成已解决、弹点评
|
||||
if (id !== submissionId.value) return
|
||||
submission.value = res
|
||||
|
||||
const result = res.result
|
||||
@@ -76,7 +80,9 @@ export function useSubmissionMonitor() {
|
||||
// 结果已经到手,别让重连再去重放这条早就判完的订阅
|
||||
unsubscribe()
|
||||
|
||||
getSubmission(submissionId.value).then((res) => {
|
||||
const id = submissionId.value
|
||||
getSubmission(id).then((res) => {
|
||||
if (id !== submissionId.value) return // 同上面轮询:换过题就扔掉
|
||||
submission.value = res
|
||||
// 15分钟无新提交则断开WebSocket(节省资源)
|
||||
scheduleDisconnect(15 * 60 * 1000)
|
||||
@@ -137,6 +143,9 @@ export function useSubmissionMonitor() {
|
||||
const reset = () => {
|
||||
pausePolling()
|
||||
unsubscribe()
|
||||
// 连接由 store 持有,离开题目页组件卸了它也还在。和判完之后一样,空闲 15 分钟断开;
|
||||
// 不在这里立刻断,是因为刚交的那条判完之后还可能推成就过来
|
||||
scheduleDisconnect(15 * 60 * 1000)
|
||||
submissionId.value = ""
|
||||
submission.value = undefined
|
||||
}
|
||||
|
||||
@@ -16,16 +16,27 @@ const JUDGE0_COMPILE_ERROR = 6
|
||||
const isJudge0RuntimeError = (status: number) => status >= 7 && status <= 12
|
||||
|
||||
/**
|
||||
* 把 Judge0 的状态换成判题机那套码,好直接交给 WrongAnswerExplain 和 JUDGE_STATUS。
|
||||
* 跑完、只是输出对不上的,按答案错误算。
|
||||
* Python 的语法错误。Judge0 的 Python 没有编译这一步,SyntaxError 回来的是运行时错误
|
||||
* (status 11),得看回溯的最后一行才认得出来
|
||||
*/
|
||||
export function trialStatus(status: number | null, passed: boolean) {
|
||||
const PYTHON_SYNTAX_ERROR = /^(SyntaxError|IndentationError|TabError): /m
|
||||
|
||||
/**
|
||||
* 把 Judge0 的状态换成判题机那套码,好直接交给 WrongAnswerExplain 和 JUDGE_STATUS。
|
||||
* 跑完、只是输出对不上的,按答案错误算。Python 的语法错误按编译失败算:
|
||||
* 学生最常犯的中文冒号、中文括号,原来在这里被说成「程序运行到一半出错……下标有没有越界」
|
||||
*/
|
||||
export function trialStatus(status: number | null, passed: boolean, output: string) {
|
||||
if (status === JUDGE0_ACCEPTED) {
|
||||
return passed ? SubmissionStatus.accepted : SubmissionStatus.wrong_answer
|
||||
}
|
||||
if (status === JUDGE0_TIME_LIMIT) return SubmissionStatus.real_time_limit_exceeded
|
||||
if (status === JUDGE0_COMPILE_ERROR) return SubmissionStatus.compile_error
|
||||
if (status !== null && isJudge0RuntimeError(status)) return SubmissionStatus.runtime_error
|
||||
if (status !== null && isJudge0RuntimeError(status)) {
|
||||
return PYTHON_SYNTAX_ERROR.test(output)
|
||||
? SubmissionStatus.compile_error
|
||||
: SubmissionStatus.runtime_error
|
||||
}
|
||||
return SubmissionStatus.system_error
|
||||
}
|
||||
|
||||
@@ -88,7 +99,7 @@ export function useTrialRun() {
|
||||
input: sample.input,
|
||||
expected: sample.output,
|
||||
output: run.output,
|
||||
result: trialStatus(run.status, passed),
|
||||
result: trialStatus(run.status, passed, run.output),
|
||||
}
|
||||
})
|
||||
samplesRunAt.value = new Date().toISOString()
|
||||
@@ -103,7 +114,7 @@ export function useTrialRun() {
|
||||
if (mine !== customGeneration) return
|
||||
customOutput.value = run.output
|
||||
// 自己输入的没有标准答案,跑完就算「对」,只看有没有出错
|
||||
customResult.value = trialStatus(run.status, true)
|
||||
customResult.value = trialStatus(run.status, true, run.output)
|
||||
customRunning.value = false
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user