Files
OJ2/apps/web/src/oj/store/submission.ts
T
xuyueandClaude Opus 5.5 1b703d13ae
Deploy / deploy (push) Canceled after 0s
style(专题): 「题单」改名「专题」,顶栏、列表、详情、后台、成就说法统一
只改界面文字和注释:网址 /problemset、接口、表名、字段名都不动,老链接照常能打开。
设计稿页名「题单重设计」和已执行的迁移文件保留原样。
线上 3 个成就(题单收藏家 / 入门 / 通关)已同步改名。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:12:18 +08:00

233 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ResultSegment>('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<string>()
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),
}
})