feat(题目): 工具栏加「运行例子」、结果页签加「自己输入」,去掉每个例子旁的「测试」和顶栏的分栏切换
Deploy / deploy (push) Canceled after 0s

- 工具栏「运行例子」:一键用全部例子试跑(Judge0,不算提交),结果进「结果 › 运行例子」,
  每个例子 ✓ / ✕,没对上的沿用答案错误那套中文说明;比赛里只摆输出、不给提示句
  (后端在比赛里故意不做例子对比);SQL 题不给
- 「结果」页签分成提交结果 / 运行例子 / 自己输入三段;「自己输入」顶替顶栏「自测」模式
  那个单独的编辑器
- 试跑出错(编译、运行、超时)给中文说明,Judge0 的英文原文折叠起来
- 例子比对两边都去掉末尾空白:原来只 trim 了运行输出,样例末尾带空格的题永远「不通过」
- 去掉题面里每个例子旁的「测试」、顶栏的「双栏 / 自测 / 题目」切换、EditorForTest 和
  screenMode store

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-28 14:35:58 +08:00
1 parent f4db7dc81c
commit 45505e8384
16 files changed
+562 -374

No files matched your search

@@ -0,0 +1,95 @@
<script setup lang="ts">
import { storeToRefs } from "pinia"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission"
import { SubmissionStatus } from "utils/constants"
import TrialErrorNote from "./TrialErrorNote.vue"
/**
* 「结果」页签里「自己输入」那一段:自己编一组输入跑一下,看看输出。不判对错、不算提交。
* 顶替原来顶栏「自测」模式那个单独的编辑器(EditorForTest)。
*/
const submissionStore = useSubmissionStore()
const { customInput, customOutput, customResult, customRunning } = submissionStore.trial
const { problem } = storeToRefs(useProblemStore())
const codeStore = useCodeStore()
function fillSample(input: string) {
customInput.value = input
}
</script>
<template>
<n-flex vertical :size="10">
<n-flex align="center" justify="space-between">
<span class="label">输入</span>
<n-text depth="3" class="hint">自己编数据试试,这里不判对错、不算提交</n-text>
</n-flex>
<n-input
v-model:value="customInput"
type="textarea"
:autosize="{ minRows: 4, maxRows: 10 }"
placeholder="在这里写输入,和题目里的「输入」格式一样"
class="mono"
/>
<n-flex align="center" :size="8">
<n-button
type="primary"
secondary
:loading="customRunning"
:disabled="!codeStore.code.value.trim()"
@click="submissionStore.runCustom()"
>
用这组数据运行
</n-button>
<n-button
v-for="(sample, index) in problem?.samples ?? []"
:key="index"
quaternary
size="small"
@click="fillSample(sample.input)"
>
填入例子 {{ index + 1 }}
</n-button>
</n-flex>
<template v-if="customResult !== null">
<span class="label">输出</span>
<TrialErrorNote
v-if="customResult !== SubmissionStatus.accepted"
:result="customResult"
:output="customOutput"
:language="codeStore.code.language"
/>
<pre v-else-if="customOutput" class="output">{{ customOutput }}</pre>
<n-text v-else depth="3">(什么都没有输出)</n-text>
</template>
</n-flex>
</template>
<style scoped>
.label {
font-weight: 600;
}
.hint {
font-size: 13px;
}
.mono :deep(textarea) {
font-family: Monaco, Consolas, monospace;
}
.output {
margin: 0;
padding: 8px 10px;
border-radius: 6px;
background-color: rgba(128, 128, 128, 0.08);
white-space: pre-wrap;
word-break: break-all;
font-family: Monaco, Consolas, monospace;
font-size: 14px;
}
</style>
@@ -1,145 +0,0 @@
<script lang="ts" setup>
import { storeToRefs } from "pinia"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { SOURCES } from "utils/constants"
import CodeEditor from "shared/components/CodeEditor.vue"
import storage from "utils/storage"
import { createTestSubmission } from "utils/judge"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import type { DropdownOption } from "naive-ui"
import { copyToClipboard } from "utils/functions"
const message = useMessage()
const route = useRoute()
const contestID = route.params.contestID ? route.params.contestID : null
const codeStore = useCodeStore()
const problemStore = useProblemStore()
const { input, output } = storeToRefs(codeStore)
const { problem } = storeToRefs(problemStore)
const storageKey = computed(
() => `problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
)
onMounted(() => {
if (storage.get(storageKey.value)) {
codeStore.setCode(storage.get(storageKey.value))
} else {
codeStore.setCode(
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
}
})
function changeCode(v: string) {
storage.set(storageKey.value, v)
}
function changeLanguage(v: string) {
if (storage.get(storageKey.value) && storageKey.value.split("_").pop() === v) {
codeStore.setCode(storage.get(storageKey.value))
} else {
codeStore.setCode(
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
}
}
const copy = async () => {
const success = await copyToClipboard(codeStore.code.value)
message[success ? "success" : "error"](`代码复制${success ? "成功" : "失败"}`)
}
const reset = () => {
codeStore.setCode(
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
storage.remove(storageKey.value)
message.success("代码重置成功")
}
const runCode = async () => {
const res = await createTestSubmission(codeStore.code, input.value)
output.value = res.output
}
const languageOptions: DropdownOption[] = problem.value!.languages.map((it) => ({
label: () => LANGUAGE_SHOW_VALUE[it],
value: it,
}))
</script>
<template>
<n-flex vertical style="height: calc(100vh - 92px)">
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
<template #1>
<n-flex vertical>
<n-flex align="center">
<n-select
v-model:value="codeStore.code.language"
style="width: 120px"
:options="languageOptions"
@update:value="changeLanguage"
/>
<n-button @click="copy">复制代码</n-button>
<n-button @click="reset">重置代码</n-button>
<n-button type="primary" secondary @click="runCode"> 运行代码 </n-button>
</n-flex>
<CodeEditor
v-model:value="codeStore.code.value"
@update:model-value="changeCode"
:language="codeStore.code.language"
/>
</n-flex>
</template>
<template #2>
<n-split direction="vertical" :default-size="1 / 3" :min="1 / 5" :max="3 / 5">
<template #1>
<div class="title">输入框</div>
<n-input
v-model:value="input"
type="textarea"
:bordered="false"
:resizable="false"
class="box"
/>
</template>
<template #2>
<div class="title">输出框</div>
<n-input
class="box output"
v-model:value="output"
placeholder=""
type="textarea"
:bordered="false"
:resizable="false"
readonly
/>
</template>
</n-split>
</template>
</n-split>
</n-flex>
</template>
<style scoped>
.title {
height: 40px;
line-height: 40px;
padding-left: 20px;
font-size: 16px;
}
.box {
padding-left: 10px;
box-sizing: border-box;
height: calc(100% - 40px);
font-size: 20px;
}
.output {
font-family: "Monaco";
}
</style>
@@ -4,6 +4,7 @@ import { copyToClipboard, compressToBase64 } from "utils/functions"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useCollabStore } from "shared/store/collab"
import { useSubmissionStore } from "oj/store/submission"
import {
ICON_SET,
LANGUAGE_FORMAT_VALUE,
@@ -48,6 +49,20 @@ const { isDesktop } = useBreakpoints()
const statisticPanel = ref(false)
/**
* 「运行例子」:用题目里的例子试跑(Judge0),结果在左栏「结果」页签。顶替原来题面里
* 每个例子旁的「测试」按钮 —— 那个在左栏、离编辑器远,只给通过 / 不通过、2 秒后复位。
* Judge0 跑不了 SQL,流程图也没得跑;没有例子的题不给。
*/
const submissionStore = useSubmissionStore()
const { samplesRunning } = submissionStore.trial
const canRunSamples = computed(
() =>
!!problem.value?.samples.length &&
codeStore.code.language !== "Flowchart" &&
codeStore.code.language !== "SQL",
)
// 计算属性
const isContestMode = computed(() => route.name === "contest problem")
const buttonSize = computed(() => (isDesktop.value ? "medium" : "small"))
@@ -265,6 +280,16 @@ const goEdit = () => {
@update:value="changeLanguage"
/>
<n-button
v-if="canRunSamples"
:size="buttonSize"
:loading="samplesRunning"
:disabled="!codeStore.code.value.trim()"
@click="submissionStore.runSamples()"
>
运行例子
</n-button>
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart'" />
<SubmitCode v-else />
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { DIFFICULTY } from "utils/constants"
import { getTagColor } from "utils/functions"
import { createTestSubmission } from "utils/judge"
import type { ProblemDetail, ProblemRow, ProblemStatus } from "utils/types"
import { useSubmissionStore } from "oj/store/submission"
import type { ProblemRow } from "utils/types"
import Copy from "shared/components/Copy.vue"
import { useDark } from "@vueuse/core"
import { MdPreview } from "md-editor-v3"
@@ -14,16 +13,8 @@ import "md-editor-v3/lib/preview.css"
import { getSimilarProblems } from "oj/api"
import SQLDataTable from "./SQLDataTable.vue"
type Sample = ProblemDetail["samples"][number] & {
id: number
msg: string
status: ProblemStatus
loading: boolean
}
const isDark = useDark()
const route = useRoute()
const codeStore = useCodeStore()
const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
@@ -88,26 +79,10 @@ const hasTriedButNotPassed = computed(() => {
)
})
function freshSamples(): Sample[] {
return (problem.value?.samples ?? []).map((sample, index) => ({
...sample,
id: index,
msg: "",
status: "not_test",
loading: false,
}))
}
const samples = ref<Sample[]>(freshSamples())
// 题目页换题是同一个组件复用(顶栏题号直达、「下一题」、相似题都是只换路由参数),
// 不跟着重建的话,新题下面摆的还是上一道题的例子,「测试」也拿旧例子去比
watch(
() => problem.value?._id,
() => {
samples.value = freshSamples()
},
)
// 例子只是摆出来。原来每个例子旁有个「测试」按钮(结果只给通过 / 不通过、2 秒后复位),
// 现在试跑统一走工具栏的「运行例子」和结果页签的「自己输入」
const samples = computed(() => problem.value?.samples ?? [])
const submissionStore = useSubmissionStore()
// 文案和配色分类都由后端生成 —— 原来这里有一份 NODE_TARGET_LABELS +
// ruleDescription + ruleTagType,和判题机那份几乎一模一样,见契约
@@ -119,66 +94,6 @@ const KIND_TAG_TYPE = {
} as const
const astRequirements = computed(() => Object.entries(problem.value?.astRequirements ?? {}))
async function test(sample: Sample, index: number) {
samples.value = samples.value.map((sample) => {
if (sample.id === index) {
sample.loading = true
}
return sample
})
const problemId = problem.value?._id
const res = await createTestSubmission(codeStore.code, sample.input)
// 跑的这一会儿换了题:结果是上一道题的例子的,按下标写进去就串到新题上了
if (problem.value?._id !== problemId) return
samples.value = samples.value.map((sample) => {
if (sample.id === index) {
const status = res.status === 3 && res.output.trim() === sample.output ? "passed" : "failed"
return {
...sample,
msg: res.output,
status: status,
loading: false,
}
} else {
return sample
}
})
const id = setTimeout(() => {
clearTimeout(id)
if (problem.value?._id !== problemId) return
samples.value = samples.value.map((sample) => {
if (sample.id === index) {
return {
...sample,
msg: res.output,
status: "not_test",
loading: false,
}
} else {
return sample
}
})
}, 2000)
}
function label(status: ProblemStatus, loading: boolean) {
if (loading) return "测试中"
return {
not_test: "测试",
failed: "不通过",
passed: "通过",
}[status]
}
function type(status: ProblemStatus) {
return {
not_test: "",
failed: "error",
passed: "success",
}[status] as "warning" | "error" | "success"
}
</script>
<template>
@@ -280,14 +195,15 @@ function type(status: ProblemStatus) {
<Icon icon="streamline-emojis:microscope"></Icon>
例子 {{ index + 1 }}
</h3>
<!-- 只在第一个例子旁放一个入口:每个都放就成了一排一样的链接 -->
<n-button
v-if="index === 0"
text
type="primary"
size="small"
secondary
:type="type(sample.status) || 'default'"
:loading="sample.loading"
@click="test(sample, index)"
@click="submissionStore.revealResult('custom')"
>
{{ label(sample.status, sample.loading) }}
自己输入数据试试 ›
</n-button>
</n-flex>
<div class="sample-grid">
@@ -306,10 +222,6 @@ function type(status: ProblemStatus) {
<pre class="testcase">{{ sample.output }}</pre>
</div>
</div>
<div v-if="sample.msg" class="sample-box sample-result">
<div class="sample-label">运行结果</div>
<pre class="testcase">{{ sample.msg }}</pre>
</div>
</section>
</template>
@@ -521,10 +433,6 @@ function type(status: ProblemStatus) {
padding: 6px 10px 10px;
}
.sample-result {
margin-top: 8px;
}
.sample-label {
font-size: 12px;
opacity: 0.7;
@@ -1,34 +1,59 @@
<script setup lang="ts">
import { storeToRefs } from "pinia"
import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission"
/**
* 左栏「结果」页签的内容:提交前语法检查没过的说明、这一次提交的判题结果,或者空状态。
* 左栏「结果」页签:三段 —— 提交结果 / 运行例子 / 自己输入。
*
* 原来是提交按钮下面的一个浮层(n-popover),点一下外面就收起、最高 600px、盖住编辑器,
* 学生没法一边看「正确输出 / 你的输出」一边改代码。放进左栏页签之后编辑器整列都在,
* 改代码时结果就摆在旁边(见 docs/specs/2026-09-28-problem-page-redesign-design.md)。
* 原来提交结果是提交按钮下面的一个浮层(n-popover),点一下外面就收起、最高 600px、
* 盖住编辑器;运行例子是题面里每个例子旁的「测试」按钮,只给通过 / 不通过、2 秒后复位;
* 自己输入是顶栏「自测」模式里单独的一个编辑器。现在三样都在这一个页签里,
* 编辑器整列都在,改代码时结果就摆在旁边(见 docs/specs/2026-09-28-problem-page-redesign-design.md)。
*/
// 第一次切到「结果」时才加载:它带着 DataTable 和 Markdown 渲染,只看题不提交的人不用付
const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue"))
const PythonErrorExplain = defineAsyncComponent(() => import("./PythonErrorExplain.vue"))
const SampleRunResult = defineAsyncComponent(() => import("./SampleRunResult.vue"))
const CustomRun = defineAsyncComponent(() => import("./CustomRun.vue"))
const { submission, syntaxErrorInfo } = storeToRefs(useSubmissionStore())
const submissionStore = useSubmissionStore()
const { submission, syntaxErrorInfo, resultSegment } = storeToRefs(submissionStore)
const { problem } = storeToRefs(useProblemStore())
// 试跑走 Judge0,它跑不了 SQL;流程图也没得跑。这两种只留「提交结果」
const canTrial = computed(() => {
const languages = problem.value?.languages ?? []
return !languages.includes("SQL")
})
</script>
<template>
<div class="result-pane">
<n-flex v-if="syntaxErrorInfo" vertical>
<n-alert type="warning" title="代码有语法错误,还没有提交" />
<PythonErrorExplain :err-info="syntaxErrorInfo" />
</n-flex>
<SubmissionResult v-else-if="submission" :submission="submission" />
<n-empty
v-else
class="empty"
description="还没有提交过。写完代码按「提交代码」,结果会出现在这里"
/>
<n-radio-group v-if="canTrial" v-model:value="resultSegment" size="small" class="segments">
<n-radio-button value="submit">提交结果</n-radio-button>
<n-radio-button value="samples">运行例子</n-radio-button>
<n-radio-button value="custom">自己输入</n-radio-button>
</n-radio-group>
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
<div v-show="resultSegment === 'submit' || !canTrial">
<n-flex v-if="syntaxErrorInfo" vertical>
<n-alert type="warning" title="代码有语法错误,还没有提交" />
<PythonErrorExplain :err-info="syntaxErrorInfo" />
</n-flex>
<SubmissionResult v-else-if="submission" :submission="submission" />
<n-empty
v-else
class="empty"
description="还没有提交过。写完代码按「提交代码」,结果会出现在这里"
/>
</div>
<template v-if="canTrial">
<SampleRunResult v-if="resultSegment === 'samples'" />
<CustomRun v-else-if="resultSegment === 'custom'" />
</template>
</div>
</template>
@@ -37,6 +62,10 @@ const { submission, syntaxErrorInfo } = storeToRefs(useSubmissionStore())
padding: 4px 0 16px;
}
.segments {
margin-bottom: 14px;
}
.empty {
margin-top: 48px;
}
@@ -0,0 +1,161 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui"
import { useSubmissionStore } from "oj/store/submission"
import { SubmissionStatus } from "utils/constants"
import { parseTime } from "utils/functions"
import TrialErrorNote from "./TrialErrorNote.vue"
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
/**
* 「结果」页签里「运行例子」那一段:每个例子对没对上,点一个看细节。
* 没对上的说明复用判完之后那一套(WrongAnswerExplain);比赛里只摆输出、不给提示句。
*/
const { sampleRuns, samplesRunning, samplesRunAt, samplesCode } = useSubmissionStore().trial
const theme = useThemeVars()
const route = useRoute()
const inContest = computed(() => !!route.params.contestID)
const failedCount = computed(
() => sampleRuns.value.filter((run) => run.result !== SubmissionStatus.accepted).length,
)
/** 看的是哪一个例子:默认第一个没对上的 */
const selected = ref(0)
watch(sampleRuns, (runs) => {
const firstFailed = runs.findIndex((run) => run.result !== SubmissionStatus.accepted)
selected.value = firstFailed >= 0 ? firstFailed : 0
})
const current = computed(() => sampleRuns.value[selected.value] ?? null)
</script>
<template>
<n-flex v-if="samplesRunning" align="center" class="state">
<n-spin size="small" />
<span>正在用例子试跑……</span>
</n-flex>
<n-empty
v-else-if="!sampleRuns.length"
class="state"
description="还没运行过。点工具栏的「运行例子」,用题目里的例子试跑一遍,不算提交"
/>
<n-flex v-else vertical :size="12">
<n-flex align="center" :size="10">
<n-icon :size="22" :color="failedCount ? theme.errorColor : theme.successColor">
<Icon :icon="failedCount ? 'ph:x-circle-fill' : 'ph:check-circle-fill'" />
</n-icon>
<span class="title" :style="{ color: failedCount ? theme.errorColor : theme.successColor }">
{{ failedCount ? `试跑:${failedCount} 个例子没对上` : "试跑:例子都对上了" }}
</span>
<n-text depth="3"
>(不算提交{{ samplesRunAt ? ` · ${parseTime(samplesRunAt, "HH:mm")}` : "" }})</n-text
>
</n-flex>
<n-flex :size="6">
<n-button
v-for="run in sampleRuns"
:key="run.index"
size="small"
round
:type="run.result === SubmissionStatus.accepted ? 'success' : 'error'"
:secondary="selected !== run.index"
@click="selected = run.index"
>
<template #icon>
<Icon :icon="run.result === SubmissionStatus.accepted ? 'ph:check-bold' : 'ph:x-bold'" />
</template>
例子 {{ run.index + 1 }}
</n-button>
</n-flex>
<template v-if="current">
<!-- 对上了:三栏摆出来就行 -->
<n-card v-if="current.result === SubmissionStatus.accepted" embedded size="small">
<div class="blocks">
<div class="block">
<div class="label">输入</div>
<pre>{{ current.input }}</pre>
</div>
<div class="block">
<div class="label">正确输出</div>
<pre>{{ current.expected }}</pre>
</div>
<div class="block">
<div class="label">你的输出</div>
<pre>{{ current.output }}</pre>
</div>
</div>
</n-card>
<!-- 跑完了、输出对不上:和判完之后同一套说明 -->
<WrongAnswerExplain
v-else-if="current.result === SubmissionStatus.wrong_answer"
:check="{
passed: false,
index: current.index,
input: current.input,
expected: current.expected,
output: current.output,
result: SubmissionStatus.wrong_answer,
}"
:code="samplesCode?.value ?? ''"
:language="samplesCode?.language ?? ''"
:plain="inContest"
/>
<!-- 没跑完:编译不过、运行出错、超时 -->
<TrialErrorNote
v-else
:result="current.result"
:output="current.output"
:language="samplesCode?.language ?? ''"
/>
</template>
<n-text depth="3" class="note">
例子都对上了再按「提交代码」。例子对了也可能在别的测试点上错,提交才算数。
</n-text>
</n-flex>
</template>
<style scoped>
.state {
margin-top: 32px;
justify-content: center;
}
.title {
font-size: 17px;
font-weight: 700;
}
.blocks {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.block {
min-width: 0;
}
.label {
font-size: 12px;
opacity: 0.7;
margin-bottom: 4px;
}
.block pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
font-family: Monaco, Consolas, monospace;
font-size: 14px;
}
.note {
font-size: 13px;
}
</style>
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { SubmissionStatus } from "utils/constants"
/**
* 试跑(Judge0)没跑完时给学生看的中文说明。Judge0 回来的是英文原文(Traceback、
* gcc 的报错),学生读不懂,原文收进折叠区。
*/
const props = defineProps<{
result: SubmissionStatus
output: string
language: string
}>()
const message = computed(() => {
switch (props.result) {
case SubmissionStatus.compile_error:
return props.language === "Python"
? "代码没能跑起来,写法有错误。按「提交代码」会先检查一遍,告诉你错在哪一行。"
: "代码没能编译通过,写法有错误。看看是不是漏了分号、括号没配对、变量没声明。"
case SubmissionStatus.runtime_error:
return "程序运行到一半出错,停下来了。检查一下输入是怎么读的、下标有没有越界、除数是不是 0。"
case SubmissionStatus.real_time_limit_exceeded:
return "运行超时了:程序跑了太久还没结束。看看是不是有死循环,或者在等一个例子里没有的输入。"
default:
return "试跑的服务暂时连不上,稍后再试一次。"
}
})
</script>
<template>
<n-flex vertical :size="8">
<n-alert type="error" :show-icon="false">{{ message }}</n-alert>
<n-collapse v-if="output">
<n-collapse-item title="原始输出(英文)" name="raw">
<pre class="raw">{{ output }}</pre>
</n-collapse-item>
</n-collapse>
</n-flex>
</template>
<style scoped>
.raw {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
font-family: Monaco, Consolas, monospace;
font-size: 13px;
}
</style>
@@ -15,6 +15,12 @@ const props = defineProps<{
check: SampleCheck
code: string
language: string
/**
* 只摆输入、正确输出、你的输出,不给中文提示句。比赛里的「运行例子」用:后端在比赛里
* 故意不做例子对比(judge/run.ts:比赛里有期末考试,格式提示在那里不该给),
* 前端试跑也不能绕过去
*/
plain?: boolean
}>()
/** 后端每段截到 2000 字,到了上限就是被截过 */
@@ -61,7 +67,8 @@ function lines(text: string) {
</div>
<n-flex v-else vertical :size="12">
<div class="explain">
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过:</b>{{ hint }}
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过{{ plain ? "。" : ":" }}</b
><template v-if="!plain">{{ hint }}</template>
</div>
<div class="blocks">
<div class="block input">