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:
xuyueandClaude Opus 5.5 committed 2026-09-29 00:07:16 +08:00
1 parent ee66cef367
commit e7b95d211d
34 files changed
+579 -108

No files matched your search

@@ -23,7 +23,8 @@ function openHisSubmissions() {
if (!room || !problem.value) return
const target = router.resolve({
name: "submissions",
query: { problem: problem.value._id, username: room.peerName },
// exactUsername:整名匹配,不然学号相近的同学(ks24a1 / ks24a10…)会混进来
query: { problem: problem.value._id, username: room.peerName, exactUsername: "1" },
})
window.open(target.href, "_blank")
}
@@ -9,6 +9,7 @@ import { useProblemStore } from "oj/store/problem"
import { useUserStore } from "shared/store/user"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useThemeVars } from "naive-ui"
import { ContestStatus } from "utils/constants"
import type { ProblemSet, ProblemSetProblem } from "utils/types"
import { useProblemPageContext } from "../composables/problemPageContext"
import ProblemChips, { type ChipItem } from "./ProblemChips.vue"
@@ -213,6 +214,8 @@ const contestBar = computed(() => {
title: contest.title,
chips,
remaining: contestStore.remaining,
// 不在进行中的两种:管理员赛前看题是「还没开始」,不能一律写「已结束」
notStarted: contestStore.contestStatus === ContestStatus.not_started,
home: { name: "contest problems", params: { contestID: ctx.value.contestId } },
rank: { name: "contest rank", params: { contestID: ctx.value.contestId } },
}
@@ -352,7 +355,7 @@ onSolvedHere(() => {
<span class="meta">还剩</span>
<span class="countdown">{{ contestBar.remaining }}</span>
</template>
<span v-else class="meta">已结束</span>
<span v-else class="meta">{{ contestBar.notStarted ? "还没开始" : "已结束" }}</span>
<router-link :to="contestBar.rank" class="contest-rank">排名</router-link>
</div>
</template>
@@ -226,7 +226,7 @@ const changeLanguage = (v: LANGUAGE) => {
</n-button>
<n-button
v-if="statisticsInline && userStore.isTeacherOrAbove"
v-if="ctx.classStats && statisticsInline && userStore.isTeacherOrAbove"
:size="buttonSize"
@click="statisticsOpen = true"
>
@@ -4,6 +4,7 @@ import { storeToRefs } from "pinia"
import { useCodeStore } from "oj/store/code"
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
import { useProblemStore } from "oj/store/problem"
import { useUserStore } from "shared/store/user"
import { sortFlowchartCriteria } from "utils/constants"
import { decompressFromBase64, parseTime } from "utils/functions"
@@ -12,13 +13,29 @@ import { decompressFromBase64, parseTime } from "utils/functions"
*
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
*
* viewOnly:手机上只能看、不能画(设计文档 5.6),不给「照着它写代码」「载回画布」。
*/
const { viewOnly = false } = defineProps<{ viewOnly?: boolean }>()
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
const flowchartStore = useFlowchartStore()
const { phase, scores, hiddenCount, selectedId, details, detailLoading, editor } =
const { phase, scores, hiddenCount, selectedId, details, detailLoading, scoresFailed, editor } =
storeToRefs(flowchartStore)
const problemStore = useProblemStore()
const codeStore = useCodeStore()
const userStore = useUserStore()
// 历次分数原来只由题面(ProblemContent)去读。手机上题面是另一个页签、不切过去就不挂载,
// 直接打开 ?tab=result 的话「流程图点评」一条都没有。这里自己也读一次(同一道题只读一次)
watch(
() => [problemStore.problem?.id, userStore.isAuthed] as const,
([id, authed]) => {
if (id && authed) flowchartStore.ensureLoaded()
},
{ immediate: true },
)
const message = useMessage()
const theme = useThemeVars()
// 暗色主题的成功色是浅薄荷绿,上面的白字看不清
@@ -33,6 +50,23 @@ const current = computed(() => {
const detail = computed(() => (current.value ? details.value[current.value.id] : undefined))
const viewingLatest = computed(() => selectedId.value === null)
// ==================== 这一版的图 ====================
/**
* 点历史分数条上的哪一次,就摆哪一次交上去的图(只读的 Mermaid 渲染,不动画布)。
* 原来的评分弹框左边就是这张图;挪进结果页签时丢了,学生翻到「第 2 次 C 42」
* 只看得到评语,想不起来那一版画成什么样。图从评语同一个详情接口来,随评语一起现拉。
*/
const versionIndex = computed(() =>
current.value ? scores.value.findIndex((row) => row.id === current.value!.id) : -1,
)
const versionTitle = computed(() => {
if (!current.value) return ""
return `第 ${versionIndex.value + 1} 次画的图 · ${parseTime(current.value.createTime, "MM-DD HH:mm")}`
})
/** 收起来之后翻别的次也保持收着:左栏在机房屏上只有六百来像素高 */
const versionOpen = ref(true)
const versionZoom = ref(false)
// 评完的那次、点到的那次,评语都要现拉(历次分数里只有分数)
watch(
() => current.value?.id,
@@ -162,10 +196,18 @@ function goCode() {
<template>
<!-- 设计稿「画流程图:画布占满右栏,AI 点评在左边」「流程图评分中」 -->
<div class="flowchart-result">
<div v-if="scoresFailed && !scores.length && phase !== 'evaluating'" class="failed load-failed">
<span>以前交过的流程图评分没读出来,可能是网络断了一下</span>
<n-button size="small" secondary @click="flowchartStore.ensureLoaded()">再读一次</n-button>
</div>
<n-empty
v-if="!scores.length && phase !== 'evaluating'"
v-else-if="!scores.length && phase !== 'evaluating'"
class="empty"
description="还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里"
:description="
viewOnly
? '还没交过流程图。流程图要在电脑上画(手机上拖不动),交给 AI 点评之后,评分和建议在这里也看得到'
: '还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里'
"
/>
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
@@ -229,6 +271,35 @@ function goCode() {
</n-flex>
<template v-if="detail">
<div v-if="detail.mermaidCode" class="version">
<div class="version-head">
<span class="version-title">{{ versionTitle }}</span>
<n-button text type="primary" size="small" @click="versionZoom = true">
放大看
</n-button>
<n-button text size="small" @click="versionOpen = !versionOpen">
{{ versionOpen ? "收起" : "展开" }}
</n-button>
</div>
<div
v-if="versionOpen"
class="version-chart"
role="button"
:aria-label="`放大看${versionTitle}`"
@click="versionZoom = true"
>
<MyFlowchart compact :code="detail.mermaidCode" />
</div>
<n-modal
v-model:show="versionZoom"
preset="card"
:title="versionTitle"
:style="{ maxWidth: '900px' }"
>
<MyFlowchart :code="detail.mermaidCode" />
</n-modal>
</div>
<div v-if="criteria.length" class="criteria">
<div v-for="[name, item] in criteria" :key="name" class="criterion">
<div class="criterion-head">
@@ -254,7 +325,7 @@ function goCode() {
</div>
</div>
<div class="actions">
<div v-if="!viewOnly" class="actions">
<n-button
v-if="isFlowchartPass(current.grade) && canCode"
type="primary"
@@ -369,6 +440,41 @@ function goCode() {
border: 1px solid rgba(240, 160, 32, 0.35);
}
.load-failed {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
}
/* 这一版的图:和题面里「你画的流程图」同一个样子,点一下放大 */
.version {
margin-bottom: 14px;
}
.version-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
}
.version-title {
font-size: 14px;
font-weight: 600;
}
.version-chart {
cursor: zoom-in;
border-radius: 6px;
background-color: rgba(128, 128, 128, 0.06);
padding: 8px;
}
.version-chart :deep(.flowchart-container.compact) {
height: 180px;
}
.score-line {
display: flex;
align-items: baseline;
@@ -5,15 +5,17 @@ import { useMermaid } from "shared/composables/useMermaid"
/**
* 学生自己画到 A/S 的那张流程图。题面里是缩略的一小块(compact),点「放大看」弹出整张。
* 原来是左栏单独一个「我的流程图」页签,评到 A/S 时还会把学生强行切过去。
*
* 结果页签里翻历次评分时也用它摆「这一版的图」,那时由 code 传进来,不读 store。
*/
const props = defineProps<{ compact?: boolean }>()
const props = defineProps<{ compact?: boolean; code?: string }>()
const store = useMyFlowchartStore()
const { renderError, renderFlowchart } = useMermaid()
const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
watch(
() => store.mermaidCode,
() => props.code ?? store.mermaidCode,
async (code) => {
if (!code) return
await nextTick()
@@ -150,10 +150,14 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
<n-tag v-if="myStatus === 'solved'" size="small" type="success" :bordered="false">
已解决
</n-tag>
<n-tag v-else-if="myStatus === 'tried'" size="small" type="warning" :bordered="false">
做过,还没对
</n-tag>
<n-tag v-else-if="myStatus === 'none'" size="small" :bordered="false">还没交过</n-tag>
<!-- 流程图画到 A / S 就算做完了(决定 4):别在「流程图 A 级」旁边再写「还没交过」
「做过,还没对」—— 那两个说的是代码 -->
<template v-else-if="!flowchartGrade">
<n-tag v-if="myStatus === 'tried'" size="small" type="warning" :bordered="false">
做过,还没对
</n-tag>
<n-tag v-else-if="myStatus === 'none'" size="small" :bordered="false">还没交过</n-tag>
</template>
<n-tag v-if="flowchartGrade" size="small" type="success" :bordered="false">
流程图 {{ flowchartGrade }} 级
</n-tag>
@@ -12,7 +12,8 @@ import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
import { errorMarkExtensions } from "oj/problem/utils/errorMark"
import EditorToolbar from "./EditorToolbar.vue"
import { useDraftKey } from "../composables/draftKey"
import { useTeacherCollab } from "../composables/teacherCollab"
import { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
import { useProblemPageContext } from "../composables/problemPageContext"
import CollabBar from "./CollabBar.vue"
import { useFlowchartStore } from "oj/store/flowchart"
@@ -27,6 +28,7 @@ const codeStore = useCodeStore()
const problemStore = useProblemStore()
const collabStore = useCollabStore()
const { problem } = storeToRefs(problemStore)
const ctx = useProblemPageContext()
/**
* 课堂求助的协作就开在这道题上。
@@ -35,9 +37,7 @@ const { problem } = storeToRefs(problemStore)
* 直接跳到题目页、就在页面这一个编辑器里协作 —— 顺带治好了「按一下 Esc 弹框就关、
* 协作跟着结束」:页面上没有弹框可关,结束协作只有工具栏那个按钮和离开这一页两条路。
*/
const collabHere = computed(
() => collabStore.room !== null && collabStore.room.problemId === problem.value?._id,
)
const collabHere = useCollabHere()
/** 协作中的教师:编辑器里是学生的代码,不是他自己的 */
const teacherCollab = useTeacherCollab()
@@ -151,6 +151,20 @@ watch(
},
)
/**
* 画着流程图时忽然不让画了:工具栏的「写代码 / 画流程图」和语言下拉都跟着 canDraw 藏起来,
* 编辑器要是还停在画布上,学生就回不去写代码了。拖窄到手机宽度时页面会换一套布局、编辑器
* 重新挂载,loadCode 自己会把语言改掉;但同一道题重新拉题面(登录后补拉)时老师刚好关掉了
* 流程图,编辑器不重挂,只能靠这里。和工具栏的「写代码」走同一条路,回到他习惯的那门语言
*/
watch(
() => problemStore.canDraw,
(canDraw) => {
if (canDraw || codeStore.code.language !== "Flowchart") return
problemStore.switchLanguage(problemStore.supportedLanguage(codeStore.preferredLanguage()))
},
)
// 流程图编辑器交给 store:结果页签(在左栏,不在编辑器底下)要拿当前画布比一比、
// 把旧版本载回去。子组件 SubmitFlowchart 还是从 inject 拿
const flowchartStore = useFlowchartStore()
@@ -172,12 +186,17 @@ provide("flowchartEditorRef", flowchartEditorRef)
学生自己切到流程图就是不写代码了,编辑器卸载、协作正常结束(SyncCodeEditor
的 detach),这是原来就有的语义。
-->
<FlowchartEditor v-if="codeStore.code.language === 'Flowchart'" ref="flowchartEditorRef" />
<FlowchartEditor
v-if="codeStore.code.language === 'Flowchart'"
ref="flowchartEditorRef"
:height="editorHeight"
/>
<SyncCodeEditor
v-else
v-model:value="codeStore.code.value"
:language="codeStore.code.language"
:problem-id="problem!._id"
:collab="ctx.collab"
:height="editorHeight"
:extra-extensions="editorExtensions"
@update:model-value="changeCode"
@@ -109,6 +109,25 @@ async function listSubmissions() {
listed.value = true
}
/**
* 「我的提交统计」:这一页里各种结果各几次(「答案错误 × 3」「答案正确 × 1」),和原来一样
* 只数当前这一页。被题单闸门锁住的那几次不数 —— 它们在列表里只露一行说明,这里也不该露出结果
*/
const statusDistribution = computed(() => {
const counts = new Map<number, number>()
for (const row of submissions.value) {
if (row.showLink) counts.set(row.result, (counts.get(row.result) ?? 0) + 1)
}
return [...counts.entries()]
.sort((a, b) => a[0] - b[0])
.map(([result, count]) => ({
result,
name: JUDGE_STATUS[result as keyof typeof JUDGE_STATUS]?.name ?? "未知",
type: JUDGE_STATUS[result as keyof typeof JUDGE_STATUS]?.type ?? "info",
count,
}))
})
/** 列表是新的在前;「第几次」从最早那次数起 */
function attemptNo(index: number) {
return total.value - (query.page - 1) * query.limit - index
@@ -220,6 +239,19 @@ watch(query, () => {
</n-button>
</div>
<div v-if="statusDistribution.length" class="distribution">
<n-tag
v-for="item in statusDistribution"
:key="item.result"
:type="item.type"
size="small"
round
:bordered="false"
>
{{ item.name }} × {{ item.count }}
</n-tag>
</div>
<n-empty
v-if="listed && !submissions.length"
class="empty"
@@ -333,6 +365,13 @@ watch(query, () => {
</template>
<style scoped>
.distribution {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.rank {
display: flex;
flex-wrap: wrap;
@@ -4,6 +4,8 @@ import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission"
import { useCodeStore } from "oj/store/code"
import { useTeacherCollab } from "../composables/teacherCollab"
import { useProblemPageContext } from "../composables/problemPageContext"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useThemeVars } from "naive-ui"
import { parseTime } from "utils/functions"
import ResultHeader from "./ResultHeader.vue"
@@ -30,6 +32,8 @@ const { submission, syntaxErrorInfo, resultSegment, formattedBeforeSubmit } =
storeToRefs(submissionStore)
const { problem } = storeToRefs(useProblemStore())
const codeStore = useCodeStore()
const ctx = useProblemPageContext()
const { isDesktop } = useBreakpoints()
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
const teacherCollab = useTeacherCollab()
@@ -37,8 +41,25 @@ const teacherCollab = useTeacherCollab()
const theme = useThemeVars()
const { samplesRunAt } = submissionStore.trial
type Segment = "submit" | "samples" | "custom"
const segments = computed<{ value: Segment; label: string; time: string }[]>(() => [
/**
* 手机上流程图题只能看点评、不能画(设计文档 5.6)。画不了,语言也就落不到 Flowchart 上,
* 下面的 drawing 永远是假 —— 原来学生在电脑上画过、AI 评过的,手机上一条都看不到。
* 这时多给一段「流程图点评」,只读。
*/
const flowchartReadable = computed(
() => !isDesktop.value && !!problem.value?.allowFlowchart && ctx.value.entry !== "contest",
)
/** 在看「流程图点评」那一段。只在这一页记着:点运行例子、提交时照常切回对应的分段 */
const viewingFlowchart = ref(false)
watch(
() => [submissionStore.resultSeq, problem.value?.id, flowchartReadable.value],
() => {
viewingFlowchart.value = false
},
)
type Segment = "submit" | "samples" | "custom" | "flowchart"
const codeSegments = computed<{ value: Segment; label: string; time: string }[]>(() => [
{
value: "submit",
// 语法没过就没交上去:那一段不叫「提交结果」
@@ -64,32 +85,55 @@ const canTrial = computed(() => {
const languages = problem.value?.languages ?? []
return !languages.includes("SQL")
})
const segments = computed(() => {
const list = canTrial.value ? codeSegments.value : codeSegments.value.slice(0, 1)
return flowchartReadable.value
? [...list, { value: "flowchart" as const, label: "流程图点评", time: "" }]
: list
})
/** 只有一段(SQL 题、又不是手机上的流程图题)就不摆分段了 */
const showSegments = computed(() => segments.value.length > 1)
function isActive(segment: Segment) {
if (viewingFlowchart.value) return segment === "flowchart"
return resultSegment.value === segment
}
function pick(segment: Segment) {
if (segment === "flowchart") {
viewingFlowchart.value = true
return
}
viewingFlowchart.value = false
resultSegment.value = segment
}
</script>
<template>
<FlowchartResult v-if="drawing" />
<div v-show="!drawing" class="result-pane">
<!-- 设计稿:三个胶囊,选中的深色实心,带上那次结果的时间 -->
<div v-if="canTrial" class="segments" role="tablist" aria-label="结果">
<div v-if="showSegments" class="segments" role="tablist" aria-label="结果">
<button
v-for="segment in segments"
:key="segment.value"
type="button"
role="tab"
class="segment"
:class="{ active: resultSegment === segment.value }"
:aria-selected="resultSegment === segment.value"
@click="resultSegment = segment.value"
:class="{ active: isActive(segment.value) }"
:aria-selected="isActive(segment.value)"
@click="pick(segment.value)"
>
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
</button>
<span v-if="resultSegment === 'custom'" class="segment-note"
<span v-if="!viewingFlowchart && resultSegment === 'custom'" class="segment-note"
>自己编数据试试,这里不判对错</span
>
</div>
<FlowchartResult v-if="viewingFlowchart && flowchartReadable" view-only />
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
<div v-show="resultSegment === 'submit' || !canTrial">
<div v-show="!viewingFlowchart && (resultSegment === 'submit' || !canTrial)">
<template v-if="syntaxErrorInfo">
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
<PythonErrorExplain :err-info="syntaxErrorInfo" />
@@ -107,7 +151,7 @@ const canTrial = computed(() => {
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
/>
</div>
<template v-if="canTrial">
<template v-if="canTrial && !viewingFlowchart">
<SampleRunResult v-if="resultSegment === 'samples'" />
<CustomRun v-else-if="resultSegment === 'custom'" />
</template>
@@ -10,7 +10,12 @@ import { SubmissionStatus } from "utils/constants"
/**
* 「结果」页签的标题:带上这一次的状态。用图标而不是一个彩色小圆点 ——
* 机房投影、色弱的学生只靠颜色分不出对错。
*
* 新结果出来、学生还没点开看过:「结果」两个字跟着状态上色、加粗。看过一次就算已读,
* 图标留着、不再强调(设计文档 5.3)。流程图评完时学生在看题目不会被强制切过去,
* 靠的就是这一下提醒
*/
const props = defineProps<{ active: boolean }>()
const theme = useThemeVars()
const { submission, syntaxErrorInfo, judging, pending, submitting } =
@@ -44,10 +49,29 @@ const status = computed(() => {
return { icon: "ph:warning-bold", color: theme.value.warningColor }
return { icon: "ph:x-bold", color: theme.value.errorColor }
})
/**
* 这一次结果的身份:哪条提交判成了什么、第几次流程图评分、哪一次语法没过。
* 换了就是新结果,要重新提醒
*/
const resultKey = computed(() => {
if (!status.value) return ""
if (codeStore.code.language === "Flowchart")
return `flowchart:${flowchart.evaluatedSeq.value}:${flowchart.phase.value}`
if (syntaxErrorInfo.value) return `syntax:${syntaxErrorInfo.value}`
return `submission:${submission.value?.id}:${submission.value?.result}`
})
const readKey = ref("")
watchEffect(() => {
if (props.active) readKey.value = resultKey.value
})
const unread = computed(
() => !!status.value && !status.value.spin && !props.active && resultKey.value !== readKey.value,
)
</script>
<template>
<span class="label">
<span class="label" :class="{ unread }" :style="unread ? { color: status!.color } : undefined">
结果
<Icon
v-if="status"
@@ -66,6 +90,10 @@ const status = computed(() => {
gap: 4px;
}
.unread {
font-weight: 700;
}
.spin {
animation: spin 1s linear infinite;
}
@@ -76,8 +76,10 @@ function settleReview() {
function closeCommentPanel() {
commentPanel.value = false
// 「下一题」在结果页签里(LessonNext),评价完回到那里 —— 学生点评之前可能切去看了题目
submissionStore.revealResult()
// 「下一题」在结果页签里(LessonNext),评价完回到那里 —— 学生点评之前可能切去看了题目。
// 接着要换题的就别切:切页签会 router.replace 改 `?tab=`,排在下面那次 push 前头,
// 把 push 当成被新导航取消,人就停在原题
if (!navigationAfterReview) submissionStore.revealResult()
settleReview()
}
@@ -14,6 +14,7 @@ import type { Submission } from "utils/types"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission"
import { markdownToText } from "oj/problem/utils/plainText"
import PythonErrorExplain from "./PythonErrorExplain.vue"
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
@@ -219,7 +220,9 @@ const customRunHint = computed(() => {
(sample) => `${sample.input} ${sample.output}`.match(/-?\d+(?:\.\d+)?/g) ?? [],
),
)
const numbers = [...new Set(problem.description.match(/-?\d+(?:\.\d+)?/g) ?? [])]
// 先摊成纯文字:题面是 HTML,446 道题带着 style="color: rgb(51, 51, 51)",直接取数会建议「拿 51 试试」
const description = markdownToText(problem.description, Infinity)
const numbers = [...new Set(description.match(/-?\d+(?:\.\d+)?/g) ?? [])]
.filter((value) => !inSamples.has(value))
.slice(0, 2)
return numbers.length
@@ -15,7 +15,7 @@ const message = computed(() => {
switch (props.result) {
case SubmissionStatus.compile_error:
return props.language === "Python"
? "代码没能跑起来,写法有错误。按「提交代码」会先检查一遍,告诉你错在哪一行。"
? "代码有语法错误,程序一行都没跑。最常见的是用了中文标点(,:()“”)。按「提交」会先检查一遍,告诉你错在哪一行,中文标点能一键换成英文。"
: "代码没能编译通过,写法有错误。看看是不是漏了分号、括号没配对、变量没声明。"
case SubmissionStatus.runtime_error:
return "程序运行到一半出错,停下来了。检查一下输入是怎么读的、下标有没有越界、除数是不是 0。"
@@ -52,8 +52,11 @@ const requirement = computed(() => {
const TEXT_LIMIT = 2000
const onSample = computed(() => props.check.result === SubmissionStatus.wrong_answer)
// plain(比赛)连差异类型都不算:把空格画成「·」、把不一样的那段标红,和提示句一样是格式线索
const kind = computed(() =>
onSample.value ? classifyDiff(props.check.expected ?? "", props.check.output ?? "") : null,
onSample.value && !props.plain
? classifyDiff(props.check.expected ?? "", props.check.output ?? "")
: null,
)
const hint = computed(() => {
if (props.check.passed) return ""
@@ -95,7 +98,7 @@ function splitDiff(expected: string, output: string) {
}
const diffParts = computed(() => {
if (!onSample.value || diffLine.value < 0) return null
if (!onSample.value || props.plain || diffLine.value < 0) return null
const expected = lines(props.check.expected ?? "")[diffLine.value] ?? ""
const output = lines(props.check.output ?? "")[diffLine.value] ?? ""
return splitDiff(expected, output)