import { defineStore } from 'pinia' import { errorCode, errorMessage } from 'utils/api' import { formatCode, isFormatLanguage, submitCode } from 'oj/api' import { useCodeStore } from 'oj/store/code' import { useProblemStore } from 'oj/store/problem' import { useSubmissionMonitor } from 'oj/problem/composables/useSubmissionMonitor' import { useSubmissionHint } from 'oj/problem/composables/useSubmissionHint' import { useTrialRun } from 'oj/problem/composables/useTrialRun' import { restartEditTrace, snapshotEditTrace } from 'oj/problem/utils/editTrace' import { useCollabStore } from 'shared/store/collab' import { LANGUAGE_FORMAT_VALUE } from 'utils/constants' import type { SubmitCodePayload } from 'utils/types' /** * 题目页上「这一次提交」的全部状态:正在跟的那条提交、判题跟踪(WebSocket + 轮询)、 * 提交前语法检查的错误、5 秒冷却、AI 提示。 * * 原来它们都长在提交按钮那个组件(SubmitCode)里,而那个组件在编辑器的工具栏里 —— * 手机上交完切到「描述」页签(Naive 的页签默认切走即卸载)、桌面切到「题目 / 自测」 * 模式,编辑器一卸载,判题结果、AI 提示、失败计数、通过后的点评和回专题全都丢了。 * 挪到 store 之后组件卸了再挂回来,状态还在。 * * 判完之后「该发生什么」(失败计数、标成已解决、烟花、点评、回专题)不在这里, * 在页面这一层的 SubmissionEffects —— 它们要路由守卫和弹窗,得挂在一直在的组件上。 */ export type ResultSegment = 'submit' | 'samples' | 'custom' export const useSubmissionStore = defineStore('submission', () => { const problemStore = useProblemStore() const codeStore = useCodeStore() const collabStore = useCollabStore() const monitor = useSubmissionMonitor() const hint = useSubmissionHint() const trial = useTrialRun() /** * 有新结果要给学生看了(交上去、或者语法检查没过)就 +1。题目页看着它把左栏切到 * 「结果」页签 —— store 不管页签,只说「该看结果了」。 */ const resultSeq = ref(0) /** 「结果」页签里看哪一段:提交结果 / 运行例子 / 自己输入 */ const resultSegment = ref('submit') function revealResult(segment: ResultSegment = 'submit') { resultSegment.value = segment resultSeq.value++ } /** 用题目里的例子试跑(本站判题机),不算提交 */ function runSamples() { const problem = problemStore.problem if (!problem?.samples.length) return revealResult('samples') trial.runSamples({ ...codeStore.code }, { problemId: problem.id, contestId: problem.contestId }, problem.samples) } /** 用「自己输入」框里的数据跑一次 */ function runCustom() { const problem = problemStore.problem if (!problem) return revealResult('custom') trial.runCustom({ ...codeStore.code }, { problemId: problem.id, contestId: problem.contestId }) } /** * 测试点一格一格亮(结果页签里的 CaseBlocks)只播一次,而且只播这一页上刚交、刚判完的那条: * 刷新、切页签回来、看以前的提交都直接全亮。系统设了「减少动画」的不播。 * 烟花(SubmissionEffects)按同一个判断决定要不要等方块亮完 */ const reducedMotion = usePreferredReducedMotion() const revealedCases = new Set() function shouldRevealCases(id: string | undefined) { return !!id && reducedMotion.value !== 'reduce' && id === monitor.submissionId.value && !revealedCases.has(id) } function markCasesRevealed(id: string) { revealedCases.add(id) } const isFormatting = ref(false) const isSubmittingRequest = ref(false) /** * 提交前语法检查查出来的错误(CPython 的报错原文),有它时结果面板显示中文说明、 * 不显示上一次的判题结果。这次没交上去,所以不是一条提交、也不数进失败次数。 * * 检查在服务端做(`/code/format` 里先用 CPython 编译一遍),原来是浏览器里的 Skulpt: * 那个只能报「第 N 行有错」,而且要下载 ~226KB。 */ const syntaxErrorInfo = ref('') /** * 这次提交之前自动排版改了代码。排版会直接改掉编辑器里的代码(缩进、空格、换行), * 学生交完发现代码「变了样」却不知道为什么 —— 结果页签里提一句 */ const formattedBeforeSubmit = ref(false) const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, { controls: true, immediate: false, }) /** * 交一次。`contestId` / `problemSetId` 是路由参数(空串 = 不是从那个入口进来的), * 由调用方给:store 不看路由。 */ 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 // 0. 提交前自动格式化(Python 用 ruff,C/C++ 用 clang-format,SQL 用 sqlparse)。 // Python 在格式化之前先由服务端的 CPython 查一遍语法,有错就不提交; // C / C++ 先在判题机上编一遍,编不过就原样交(格式化会把错的几行并成一行) const formatLang = LANGUAGE_FORMAT_VALUE[codeStore.code.language] if (isFormatLanguage(formatLang)) { isFormatting.value = true try { const res = await formatCode({ code: codeStore.code.value, language: formatLang, problemId: problem.id, }) 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 的报错原文,交给 CompileErrorExplain 翻译 syntaxErrorInfo.value = errorMessage(e) revealResult() return } // server-error / 网络异常:格式化工具问题,静默降级,提交原代码 } finally { isFormatting.value = false } } // 1. 构建提交数据 const data: SubmitCodePayload = { problemId: problem.id, language: codeStore.code.language, code: codeStore.code.value, // 编辑过程信号,见 utils/editTrace.ts。协作的判断和 ProblemEditor 的 collabHere // (teacherCollab.ts 的 useCollabHere)同一个口径:比赛入口不算协作 trace: snapshotEditTrace(!entry.contestId && collabStore.room !== null && collabStore.room.problemId === problem._id), } if (entry.contestId) { data.contestId = parseInt(entry.contestId) } // 从专题入口进来的,把来源专题一起报上去:提交列表要据此标出「来自专题」。 // 只是来源标记,专题进度仍由后端判完之后自己记账(judge/run.ts) if (entry.problemSetId) { data.problemSetId = parseInt(entry.problemSetId) } // 2. 提交代码到后端 isSubmittingRequest.value = true try { const res = await submitCode(data) console.log(`[Submit] 代码已提交: ID=${res.submissionId}`) // 这条照常判完落库、题库里查得到,只是这一页已经换了题,不再跟 if (stale()) return // 交上了才清零;被限流 / 网络失败的话这一段接着记,下次提交一起报 restartEditTrace(codeStore.code.value.length) // 3. 启动冷却 + 监控 startCooldown() monitor.startMonitoring(res.submissionId) revealResult() } finally { isSubmittingRequest.value = false } } /** * 收掉这一道题的所有东西:语法错误、还在跟的那条提交、AI 提示。 * 提交本身照常判完落库,只是这边不再跟 —— 不然上一道还在判的那条判完, * 会把**新题**标成已解决。 */ function reset() { syntaxErrorInfo.value = '' formattedBeforeSubmit.value = false resultSegment.value = 'submit' monitor.reset() hint.clearHint() trial.resetTrial() } // 题目页换题是同一个组件复用(顶栏题号直达、「下一题」都只换路由参数) watch( () => problemStore.problem?._id, (next, previous) => { if (!previous || next === previous) return reset() }, ) // 换了一次提交就清掉上一次的 AI 提示,否则新结果底下挂着上一次的提示, // 而「让 AI 分析」按钮已经被藏了,学生没法重新分析 watch(() => monitor.submission.value?.id, hint.clearHint) return { submission: monitor.submission, judging: monitor.judging, pending: monitor.pending, submitting: monitor.submitting, resultSeq, resultSegment, revealResult, shouldRevealCases, markCasesRevealed, runSamples, runCustom, isFormatting, isSubmittingRequest, syntaxErrorInfo, formattedBeforeSubmit, isCooldown, submit, reset, // markRaw:不让 store 把它包成 reactive,组件解构出来的还是 ref hint: markRaw(hint), trial: markRaw(trial), } })