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
@@ -71,6 +71,11 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
/** 看过的几次的完整评语,按 id 缓存(评完就不会再变) */
|
||||
const details = ref<Record<string, FlowchartSubmission>>({})
|
||||
const detailLoading = ref(false)
|
||||
/**
|
||||
* 进题时历次分数没读出来。原来读失败就当没交过,结果页签写着「还没交过流程图」——
|
||||
* 学生明明交过五次,会以为记录丢了
|
||||
*/
|
||||
const scoresFailed = ref(false)
|
||||
|
||||
/** 画布本身。结果页签要拿当前画布和某一版比一比、把那一版载回去 */
|
||||
const editor = shallowRef<FlowchartEditorInstance | null>(null)
|
||||
@@ -81,6 +86,11 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
const lastSubmittedMermaidCode = ref("")
|
||||
/** 最近一次分数是哪道题的;换题后置空,下次挂载时重新拉 */
|
||||
let loadedFor: number | null = null
|
||||
/**
|
||||
* 换一次题(reset)加一。每个 await 回来都要对一下:请求在路上时学生换了题,
|
||||
* 回来的是上一道题的东西,不能落到这一道上
|
||||
*/
|
||||
let generation = 0
|
||||
|
||||
// ==================== 评分结果监听 ====================
|
||||
/**
|
||||
@@ -92,6 +102,8 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
* 有兜底轮询,流程图这边没有,丢一次消息按钮就一直转圈转到用户自己刷新。
|
||||
*/
|
||||
let monitoringId = ""
|
||||
/** 正在跟的那一次是哪道题的,结算时再对一遍 */
|
||||
let monitoringProblemId: number | null = null
|
||||
|
||||
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
|
||||
async () => {
|
||||
@@ -141,20 +153,26 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
/** 不再跟这一次评分:结算完、超时、或者换了题 */
|
||||
function stopMonitoring() {
|
||||
monitoringId = ""
|
||||
monitoringProblemId = null
|
||||
unsubscribe()
|
||||
pausePolling()
|
||||
stopPollingFallback()
|
||||
stopPollingDeadline()
|
||||
loading.value = false
|
||||
// 连接挂在 store 上,离开题目页也不会跟着组件断。不立刻断:接着还可能再交一次,
|
||||
// A/S 之后的成就也走这条连接推过来。和判题那边一样,闲 15 分钟再断
|
||||
scheduleDisconnect(15 * 60 * 1000)
|
||||
}
|
||||
|
||||
function settle(submissionId: string, outcome: Outcome) {
|
||||
// 一个学生可能同时开着几道题的页面,每条连接都订在同一个用户 topic 上,
|
||||
// 别的页面的评分结果照样会推到这里来 —— 必须认 id,不然会张冠李戴
|
||||
if (!submissionId || submissionId !== monitoringId) return
|
||||
// 换题时 reset 已经把 monitoringId 清了,这里再对一遍题号:别的题评完的分数、烟花、
|
||||
// 「这节课做完」、A/S 那张图,都不能落到眼前这道题上
|
||||
const problemId = monitoringProblemId
|
||||
stopMonitoring()
|
||||
// 评完了就不急着断:接着还可能再交一次。和判题那边一样,闲 15 分钟再断
|
||||
scheduleDisconnect(15 * 60 * 1000)
|
||||
if (!problemId || problemStore.problem?.id !== problemId) return
|
||||
|
||||
if (!outcome.ok) {
|
||||
phase.value = "failed"
|
||||
@@ -170,8 +188,22 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
if (isFlowchartPass(outcome.grade) && lastSubmittedMermaidCode.value) {
|
||||
myFlowchartStore.show(lastSubmittedMermaidCode.value)
|
||||
}
|
||||
const problemId = problemStore.problem?.id
|
||||
if (problemId) loadScores(problemId)
|
||||
// 先把这一次记进历次分数,再去拉全量:拉失败的话,结果页签照样看得到这次的分数,
|
||||
// 不会还停在上一次(第一次交时则是「还没交过流程图」)
|
||||
if (!scores.value.some((row) => row.id === submissionId)) {
|
||||
scores.value = [
|
||||
...scores.value,
|
||||
{
|
||||
id: submissionId,
|
||||
score: outcome.score,
|
||||
grade: outcome.grade,
|
||||
createTime: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
}
|
||||
loadScores(problemId).catch((error) => {
|
||||
console.error("[Flowchart] 刷新历次评分失败:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
|
||||
@@ -208,6 +240,7 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
return
|
||||
}
|
||||
|
||||
const gen = generation
|
||||
lastSubmittedMermaidCode.value = mermaidCode
|
||||
loading.value = true
|
||||
phase.value = "evaluating"
|
||||
@@ -226,16 +259,21 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
},
|
||||
})
|
||||
|
||||
// 请求在路上时换了题:这一次照常评完落库,回到那道题时在历次分数里看得到;
|
||||
// 但不能再跟它 —— 评完会把分数、烟花、「做完了」都落到新题上
|
||||
if (gen !== generation) return
|
||||
if (response.submissionId) {
|
||||
// 订阅提交更新,同时开启轮询兜底。connect() 幂等;没连上时 subscribe 会先记着
|
||||
cancelScheduledDisconnect()
|
||||
connect()
|
||||
monitoringId = response.submissionId
|
||||
monitoringProblemId = problem.id
|
||||
subscribe(response.submissionId)
|
||||
startPollingFallback()
|
||||
startPollingDeadline()
|
||||
}
|
||||
} catch (error) {
|
||||
if (gen !== generation) return
|
||||
loading.value = false
|
||||
phase.value = "idle"
|
||||
// 按错误码分支(见 utils/api.ts 的约定)。限流是最容易撞上的一种:
|
||||
@@ -272,9 +310,11 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
const problemId = problemStore.problem?.id
|
||||
if (!problemId || loadedFor === problemId) return
|
||||
loadedFor = problemId
|
||||
const gen = generation
|
||||
try {
|
||||
await loadScores(problemId)
|
||||
if (problemStore.problem?.id !== problemId) return
|
||||
scoresFailed.value = false
|
||||
const passed = scores.value.filter((row) => isFlowchartPass(row.grade)).at(-1)
|
||||
if (passed) {
|
||||
const detail = await fetchDetail(passed.id)
|
||||
@@ -283,17 +323,22 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
if (detail?.mermaidCode) myFlowchartStore.show(detail.mermaidCode)
|
||||
}
|
||||
} catch (error) {
|
||||
// 拉不到就当没交过;下次挂载再试
|
||||
loadedFor = null
|
||||
console.error("[Flowchart] 读取历次评分失败:", error)
|
||||
// 换过题了:这是上一道题的失败,别把新题的状态改掉
|
||||
if (gen !== generation) return
|
||||
// 下次挂载、或者结果页签里点「再读一次」时重试
|
||||
loadedFor = null
|
||||
if (!scores.value.length) scoresFailed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDetail(id: string) {
|
||||
const cached = details.value[id]
|
||||
if (cached) return cached
|
||||
const gen = generation
|
||||
const detail = await getFlowchartSubmission(id)
|
||||
details.value[id] = detail
|
||||
// 换过题就不进缓存:reset 刚清过,别把上一道题的塞回去
|
||||
if (gen === generation) details.value[id] = detail
|
||||
return detail
|
||||
}
|
||||
|
||||
@@ -302,13 +347,14 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
selectedId.value = id
|
||||
const target = id ?? scores.value.at(-1)?.id
|
||||
if (!target || details.value[target]) return
|
||||
const gen = generation
|
||||
detailLoading.value = true
|
||||
try {
|
||||
await fetchDetail(target)
|
||||
} catch {
|
||||
message.error("这一次的评语读不出来,过一会儿再试")
|
||||
if (gen === generation) message.error("这一次的评语读不出来,过一会儿再试")
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
if (gen === generation) detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +364,7 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
* 连卸载都不会 —— 不收的话上一道还在评的那次评完,会把它的图挂到新题上。
|
||||
*/
|
||||
function reset() {
|
||||
generation++
|
||||
stopMonitoring()
|
||||
latestRating.value = { score: 0, grade: "" }
|
||||
phase.value = "idle"
|
||||
@@ -325,6 +372,8 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
hiddenCount.value = 0
|
||||
selectedId.value = null
|
||||
details.value = {}
|
||||
detailLoading.value = false
|
||||
scoresFailed.value = false
|
||||
lastSubmittedMermaidCode.value = ""
|
||||
loadedFor = null
|
||||
}
|
||||
@@ -348,6 +397,7 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
selectedId,
|
||||
details,
|
||||
detailLoading,
|
||||
scoresFailed,
|
||||
editor,
|
||||
attachEditor,
|
||||
submit,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from "pinia"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import type { LANGUAGE, ProblemDetail } from "utils/types"
|
||||
import { problemEntryOf } from "oj/problem/composables/problemPageContext"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
|
||||
export const useProblemStore = defineStore("problem", () => {
|
||||
const problem = ref<ProblemDetail | null>(null)
|
||||
@@ -59,12 +60,17 @@ export const useProblemStore = defineStore("problem", () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 这道题收不收当前语言,不收就退到它支持的第一种(SQL 题只有 "SQL",硬编码的
|
||||
* 这道题收不收当前语言,不收就退到学生习惯的那门、再退到它支持的第一种(SQL 题只有 "SQL",硬编码的
|
||||
* Python 会被后端拒绝)。编辑器每次载入代码之前都要过一遍:草稿的键里带着语言,
|
||||
* 先载入再改语言,编辑器里摆的就是另一种语言的模板。
|
||||
*/
|
||||
function supportedLanguage(current: LANGUAGE): LANGUAGE {
|
||||
if (languages.value.includes(current)) return current
|
||||
// 当前这门这道题不给,先回到学生习惯用的那门。current 是 Flowchart 时最要紧:
|
||||
// 在能画图的题上画着图换到不能画的题,直接取第一项会把偏好 C 的学生扔到 Python,
|
||||
// 而且 code store 的 watch 会把 Python 记成新的偏好
|
||||
const preferred = useCodeStore().preferredLanguage()
|
||||
if (preferred !== "Flowchart" && languages.value.includes(preferred)) return preferred
|
||||
// 兜底落在编程语言上:languages 的第一项可能是 Flowchart,不能默认把人扔到画布上
|
||||
return codeLanguages.value[0] ?? languages.value[0] ?? "Python"
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ export const useSubmissionStore = defineStore("submission", () => {
|
||||
async function submit(entry: { contestId: string; problemSetId: string }) {
|
||||
const problem = problemStore.problem
|
||||
if (!problem) return
|
||||
// 格式化、提交都要等请求回来,这中间学生可能已经点了「下一题」或课堂条上的别的题。
|
||||
// 换了题就什么都别再写:不然 A 格式化好的代码会盖掉 B 的编辑器(连带 B 的草稿),
|
||||
// 或者开始跟 A 的那条提交,A 判对了却把 B 标成已解决、弹 B 的点评
|
||||
const stale = () => problemStore.problem?.id !== problem.id
|
||||
syntaxErrorInfo.value = ""
|
||||
formattedBeforeSubmit.value = false
|
||||
|
||||
@@ -102,11 +106,13 @@ export const useSubmissionStore = defineStore("submission", () => {
|
||||
code: codeStore.code.value,
|
||||
language: formatLang,
|
||||
})
|
||||
if (stale()) return
|
||||
if (res.code !== codeStore.code.value) {
|
||||
codeStore.setCode(res.code)
|
||||
formattedBeforeSubmit.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
if (stale()) return
|
||||
if (errorCode(e) === "syntax-error") {
|
||||
// 仅 Python 会出现:message 是 CPython 的报错原文,交给 PythonErrorExplain 翻译
|
||||
syntaxErrorInfo.value = errorMessage(e)
|
||||
@@ -124,9 +130,10 @@ export const useSubmissionStore = defineStore("submission", () => {
|
||||
problemId: problem.id,
|
||||
language: codeStore.code.language,
|
||||
code: codeStore.code.value,
|
||||
// 编辑过程信号,见 utils/editTrace.ts。协作的判断和 ProblemEditor 的 collabHere 同一个口径
|
||||
// 编辑过程信号,见 utils/editTrace.ts。协作的判断和 ProblemEditor 的 collabHere
|
||||
// (teacherCollab.ts 的 useCollabHere)同一个口径:比赛入口不算协作
|
||||
trace: snapshotEditTrace(
|
||||
collabStore.room !== null && collabStore.room.problemId === problem._id,
|
||||
!entry.contestId && collabStore.room !== null && collabStore.room.problemId === problem._id,
|
||||
),
|
||||
}
|
||||
if (entry.contestId) {
|
||||
@@ -142,6 +149,8 @@ export const useSubmissionStore = defineStore("submission", () => {
|
||||
try {
|
||||
const res = await submitCode(data)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.submissionId}`)
|
||||
// 这条照常判完落库、题库里查得到,只是这一页已经换了题,不再跟
|
||||
if (stale()) return
|
||||
// 交上了才清零;被限流 / 网络失败的话这一段接着记,下次提交一起报
|
||||
restartEditTrace(codeStore.code.value.length)
|
||||
|
||||
|
||||
Reference in new issue
Block a user