fix(后台): 出题页读回来的测试数据每组多一行空白;导入大包后原来的例子丢了
Deploy / deploy (push) Canceled after 0s

- 测试点文件末尾的换行读回来时去掉(上传时补回去),比对改没改过也按去掉后的比,没改的题不会白白重传
- 导入超过 100 组的 zip 时先把例子拿下来再切成「不展开」,例子不再丢
- 「不展开」时保存栏改说「加一个例子」;刚导入还没保存时不给「下载 zip」(下到的是库里的旧数据)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 20:14:36 +08:00
1 parent ba5b9ada0e
commit 4ef2794552
3 files changed
+31 -7

No files matched your search

@@ -27,6 +27,8 @@ const props = defineProps<{
examples: { input: string; output: string }[] examples: { input: string; output: string }[]
/** 例子来自哪几组,比如「测试数据第 1–4 组」 */ /** 例子来自哪几组,比如「测试数据第 1–4 组」 */
examplesNote: string examplesNote: string
/** 测试数据太大不展开、例子单独写的时候 */
bigData?: boolean
languages: LANGUAGE[] languages: LANGUAGE[]
templates: Partial<Record<LANGUAGE, TemplateParts>> templates: Partial<Record<LANGUAGE, TemplateParts>>
astRules: AstRules | null astRules: AstRules | null
@@ -196,7 +198,9 @@ onMounted(() => nextTick(render))
<pre class="sampleCell" role="cell">{{ sample.output }}</pre> <pre class="sampleCell" role="cell">{{ sample.output }}</pre>
</template> </template>
</div> </div>
<p v-else class="placeholder">在左边的测试数据里勾几组「当例子」</p> <p v-else class="placeholder">
{{ bigData ? "在左边加一个例子" : "在左边的测试数据里勾几组「当例子」" }}
</p>
<template v-if="!isBlankHtml(hint)"> <template v-if="!isBlankHtml(hint)">
<h3 class="title">提示</h3> <h3 class="title">提示</h3>
@@ -49,6 +49,17 @@ export function newRow(input = "", output = "", example = false, saved: string |
} satisfies CaseRow } satisfies CaseRow
} }
/**
* 测试点文件读回来时的样子:CRLF 统一成 LF,输入去掉末尾那一个换行(文件里都有,摆在输入框里
* 就是多出来的一行空白;上传时 caseFiles 会补回去),输出去掉末尾空白(判题机比对时本来就不看)
*/
export function normalizeCase(input: string, output: string) {
return {
input: input.replace(/\r\n/g, "\n").replace(/\n$/, ""),
output: output.replace(/\r\n/g, "\n").trimEnd(),
}
}
/** 判题机比对时去掉的是整段输出末尾的空白(见 services/test-case.ts 的 rstrip),这里同一个口径 */ /** 判题机比对时去掉的是整段输出末尾的空白(见 services/test-case.ts 的 rstrip),这里同一个口径 */
export function sameOutput(a: string, b: string) { export function sameOutput(a: string, b: string) {
return a.replace(/\r\n/g, "\n").trimEnd() === b.replace(/\r\n/g, "\n").trimEnd() return a.replace(/\r\n/g, "\n").trimEnd() === b.replace(/\r\n/g, "\n").trimEnd()
@@ -171,7 +182,7 @@ export function readCaseZip(
output.length > TEST_CASE_EDIT_MAX_FILE_BYTES output.length > TEST_CASE_EDIT_MAX_FILE_BYTES
) )
return { ok: false, reason: "too-large" } return { ok: false, reason: "too-large" }
cases.push({ input: strFromU8(input), output: strFromU8(output) }) cases.push(normalizeCase(strFromU8(input), strFromU8(output)))
} }
if (!cases.length) if (!cases.length)
return { ok: false, reason: "压缩包里没有从 1.in / 1.out 开始连续编号的测试点" } return { ok: false, reason: "压缩包里没有从 1.in / 1.out 开始连续编号的测试点" }
+14 -5
View File
@@ -26,6 +26,7 @@ import {
caseFiles, caseFiles,
caseStatus, caseStatus,
newRow, newRow,
normalizeCase,
readCaseZip, readCaseZip,
sameOutput, sameOutput,
useCaseRunner, useCaseRunner,
@@ -343,6 +344,8 @@ async function importZip({ file }: UploadCustomRequestOptions) {
return return
} }
try { try {
// 先把现在的例子拿下来:big 一设上,examples 就改读 exampleRows 了
const keep = examples.value
const res = await uploadTestcases(raw) const res = await uploadTestcases(raw)
setTestCase(res.id, res.info) setTestCase(res.id, res.info)
big.value = { big.value = {
@@ -350,7 +353,7 @@ async function importZip({ file }: UploadCustomRequestOptions) {
totalBytes: res.info.reduce((sum, e) => sum + e.input_size + e.output_size, 0), totalBytes: res.info.reduce((sum, e) => sum + e.input_size + e.output_size, 0),
} }
if (!exampleRows.value.length) { if (!exampleRows.value.length) {
exampleRows.value = examples.value.map((e) => newRow(e.input, e.output, true)) exampleRows.value = keep.map((e) => newRow(e.input, e.output, true))
} }
rows.value = [] rows.value = []
loadedCases = null loadedCases = null
@@ -604,8 +607,8 @@ async function loadProblem(data: AdminProblem) {
exampleRows.value = data.samples.map((s) => newRow(s.input, s.output, true, s.output)) exampleRows.value = data.samples.map((s) => newRow(s.input, s.output, true, s.output))
return return
} }
loadedCases = files.cases loadedCases = files.cases.map((c) => normalizeCase(c.input, c.output))
rows.value = files.cases.map((c) => newRow(c.input, c.output, false, c.output)) rows.value = loadedCases.map((c) => newRow(c.input, c.output, false, c.output))
// 例子对回测试数据:输入一样的那组勾上;对不上的(老题的例子多半不在测试数据里)补到最后 // 例子对回测试数据:输入一样的那组勾上;对不上的(老题的例子多半不在测试数据里)补到最后
let appended = 0 let appended = 0
for (const sample of data.samples) { for (const sample of data.samples) {
@@ -658,7 +661,12 @@ const blockers = computed<Blocker[]>(() => {
if (!source.value && caseRows.some((r) => !r.output.trim())) if (!source.value && caseRows.some((r) => !r.output.trim()))
list.push({ text: "补上没写的输出", section: "cases" }) list.push({ text: "补上没写的输出", section: "cases" })
if (!examples.value.length) if (!examples.value.length)
list.push({ text: "勾一组当例子(题面上至少给学生一个)", section: "cases" }) list.push({
text: big.value
? "加一个例子(题面上至少给学生一个)"
: "勾一组当例子(题面上至少给学生一个)",
section: "cases",
})
for (const [lang, state] of Object.entries(astChecks.value)) { for (const [lang, state] of Object.entries(astChecks.value)) {
if (!form.languages.includes(lang as RunnableLanguage)) continue if (!form.languages.includes(lang as RunnableLanguage)) continue
@@ -1007,7 +1015,7 @@ onMounted(async () => {
<n-button size="tiny" quaternary>导入 zip</n-button> <n-button size="tiny" quaternary>导入 zip</n-button>
</n-upload> </n-upload>
<n-button <n-button
v-if="!big ? rows.length : !!initial" v-if="!big ? rows.length : !!initial && testCase.id === initial.testCaseId"
size="tiny" size="tiny"
quaternary quaternary
@click="downloadZip" @click="downloadZip"
@@ -1276,6 +1284,7 @@ onMounted(async () => {
:hint="form.hint" :hint="form.hint"
:examples="examples" :examples="examples"
:examples-note="examplesNote" :examples-note="examplesNote"
:big-data="!!big"
:languages="form.languages" :languages="form.languages"
:templates="templatesForPreview" :templates="templatesForPreview"
:ast-rules="form.astRules" :ast-rules="form.astRules"