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

@@ -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;