Deploy / deploy (push) Canceled after 0s
只改界面文字和注释:网址 /problemset、接口、表名、字段名都不动,老链接照常能打开。 设计稿页名「题单重设计」和已执行的迁移文件保留原样。 线上 3 个成就(题单收藏家 / 入门 / 通关)已同步改名。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
233 lines
9.6 KiB
TypeScript
233 lines
9.6 KiB
TypeScript
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),
|
||
}
|
||
})
|