feat(后台): 编程题出题页重做——左边写,右边是学生看到的样子;例子和测试数据的输出都由标准答案跑出来,改老题能直接看、改测试数据
- 例子就是测试数据里勾了「当例子」的几组,不再手抄输出;老题的例子不在测试数据里的补到最后 - 改了答案,输出变了的那组标黄、写清原来存的是什么;答案在某组上报错就挡住保存 - 没存标准答案的老题输出手写;太大的测试数据(>100 组或单文件 >32KB)只能导入 / 下载 zip - 预制代码只写学生看得到的那段,前后藏起来的代码收进折叠;语法要求先拿标准答案自测 - 流程图两个互斥开关改成三选一;时间 / 内存限制能改了;草稿只给新建页 - 后端:读回测试点原文、语法要求自测、AI 想测试输入三个接口 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
033a245abb
commit
ba5b9ada0e
15 files changed
+3043
-1068
No files matched your search
@@ -0,0 +1,257 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { caseStatus, type AnswerSource, type CaseRow } from "./caseRows"
|
||||
|
||||
/**
|
||||
* 测试数据一组一行:输入 | 输出 | 当例子(设计稿「编程题出题页」B)。
|
||||
* 有标准答案时输出是跑出来的、只读;没有时(老题)输出手写。
|
||||
*/
|
||||
const rows = defineModel<CaseRow[]>("rows", { required: true })
|
||||
const props = defineProps<{
|
||||
source: AnswerSource | null
|
||||
/** 只有例子这一列表的时候(测试数据太大、单独写例子)不显示「当例子」 */
|
||||
examplesOnly?: boolean
|
||||
}>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
|
||||
/** 输出最多先露这么多行,长了点开看全 */
|
||||
const CLIP_LINES = 6
|
||||
const expanded = ref(new Set<number>())
|
||||
|
||||
function lines(text: string) {
|
||||
return text.split("\n")
|
||||
}
|
||||
|
||||
function clipped(row: CaseRow) {
|
||||
const all = lines(row.output)
|
||||
if (expanded.value.has(row.key) || all.length <= CLIP_LINES + 1)
|
||||
return { text: row.output, rest: 0 }
|
||||
return { text: all.slice(0, CLIP_LINES).join("\n"), rest: all.length - CLIP_LINES }
|
||||
}
|
||||
|
||||
function toggle(key: number) {
|
||||
const next = new Set(expanded.value)
|
||||
if (next.has(key)) next.delete(key)
|
||||
else next.add(key)
|
||||
expanded.value = next
|
||||
}
|
||||
|
||||
function remove(index: number) {
|
||||
rows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const status = (row: CaseRow) => caseStatus(row, props.source)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table" :class="{ examplesOnly }">
|
||||
<div class="row head">
|
||||
<span></span>
|
||||
<span>输入</span>
|
||||
<span>{{ source ? "输出(答案跑出来的)" : "输出(手写)" }}</span>
|
||||
<span v-if="!examplesOnly">当例子</span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(row, index) in rows"
|
||||
:key="row.key"
|
||||
class="row"
|
||||
:class="{ bad: status(row) === 'error', changed: status(row) === 'changed' }"
|
||||
>
|
||||
<b class="no">{{ index + 1 }}</b>
|
||||
<n-input
|
||||
v-model:value="row.input"
|
||||
type="textarea"
|
||||
class="mono"
|
||||
size="small"
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
placeholder="(没有输入)"
|
||||
:aria-label="`第 ${index + 1} 组输入`"
|
||||
/>
|
||||
<n-input
|
||||
v-if="!source"
|
||||
v-model:value="row.output"
|
||||
type="textarea"
|
||||
class="mono"
|
||||
size="small"
|
||||
:autosize="{ minRows: 1, maxRows: 8 }"
|
||||
:aria-label="`第 ${index + 1} 组输出`"
|
||||
/>
|
||||
<div v-else class="outCell">
|
||||
<div v-if="status(row) === 'error'" class="cell errorCell">{{ row.error }}</div>
|
||||
<template v-else>
|
||||
<pre class="cell" :class="{ stale: status(row) === 'running' }">{{
|
||||
clipped(row).text || (status(row) === "running" ? "在跑……" : "(没有输出)")
|
||||
}}</pre>
|
||||
<button v-if="clipped(row).rest" type="button" class="more" @click="toggle(row.key)">
|
||||
⋯ 还有 {{ clipped(row).rest }} 行
|
||||
</button>
|
||||
<button
|
||||
v-else-if="expanded.has(row.key)"
|
||||
type="button"
|
||||
class="more"
|
||||
@click="toggle(row.key)"
|
||||
>
|
||||
收起
|
||||
</button>
|
||||
<div v-if="status(row) === 'changed'" class="was">
|
||||
变了 · 原来存的是
|
||||
<del>{{
|
||||
lines(row.saved ?? "")
|
||||
.slice(0, 3)
|
||||
.join(" ⏎ ") || "(空)"
|
||||
}}</del>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<n-checkbox
|
||||
v-if="!examplesOnly"
|
||||
v-model:checked="row.example"
|
||||
class="example"
|
||||
:aria-label="`第 ${index + 1} 组当例子`"
|
||||
/>
|
||||
<span class="mark" :class="status(row)">
|
||||
<template v-if="status(row) === 'ok'">✓</template>
|
||||
<template v-else-if="status(row) === 'error'">✗</template>
|
||||
<template v-else-if="status(row) === 'changed'">●</template>
|
||||
</span>
|
||||
<n-button
|
||||
quaternary
|
||||
size="tiny"
|
||||
class="remove"
|
||||
:aria-label="`删掉第 ${index + 1} 组`"
|
||||
@click="remove(index)"
|
||||
>
|
||||
删掉
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 22px minmax(0, 1fr) minmax(0, 1fr) 44px 16px 40px;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
padding: 7px 4px;
|
||||
border-top: 1px solid v-bind("theme.dividerColor");
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.examplesOnly .row {
|
||||
grid-template-columns: 22px minmax(0, 1fr) minmax(0, 1fr) 16px 40px;
|
||||
}
|
||||
|
||||
.row.head {
|
||||
border-top: 0;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.row.changed {
|
||||
background-color: rgba(240, 160, 32, 0.08);
|
||||
}
|
||||
|
||||
.no {
|
||||
line-height: 26px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mono :deep(textarea),
|
||||
.cell {
|
||||
font-family: ui-monospace, "Cascadia Mono", Consolas, "Microsoft YaHei", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.outCell {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 26px;
|
||||
padding: 3px 7px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
background-color: rgba(128, 128, 128, 0.05);
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cell.stale {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.errorCell {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: v-bind("theme.errorColor");
|
||||
border-color: v-bind("theme.errorColorSuppl");
|
||||
background-color: rgba(208, 48, 80, 0.06);
|
||||
}
|
||||
|
||||
.more {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font: inherit;
|
||||
color: v-bind("theme.textColor3");
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.was {
|
||||
color: v-bind("theme.warningColorPressed");
|
||||
}
|
||||
|
||||
.was del {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.example {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
line-height: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mark.ok {
|
||||
color: v-bind("theme.successColor");
|
||||
}
|
||||
|
||||
.mark.error {
|
||||
color: v-bind("theme.errorColor");
|
||||
}
|
||||
|
||||
.mark.changed {
|
||||
color: v-bind("theme.warningColor");
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin-top: 1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.row:hover .remove,
|
||||
.remove:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user