实验页"评价项"和自测:提交前自己检查,不计分

- 工具栏"评价项"打开侧栏,列出自动检查的功能项(作业显示布置时的规则和各项分值)
- "自己检查"在服务器上按评分规则运行学生现在的程序,逐项显示通过与否和原因;不计分、不留记录,
  自由练习和作业都能用;每人同时一次,结果保留到下一次,之后改过代码提示结果可能过时;
  和提交评分共用全站并发上限
- 实验的评价项接口学生也能用(老师布置作业的对话框改用它)
- 修正:运行区的窗体和"放大"按钮会盖住网页的抽屉和对话框

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-25 18:35:25 +08:00
1 parent aadb2c9e93
commit f256b313f7
11 files changed
+345 -15

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
<script setup lang="ts">
// 评价项面板:列出这个实验(或作业)自动检查的功能项;"自己检查"在服务器上运行学生现在的程序逐项检查,
// 告诉他哪几项不通过、为什么。不计分、不留记录,提交作业另算。
import { NButton, NDrawer, NDrawerContent, NSpin } from 'naive-ui'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import type { SelfCheck, SourceFile } from '@iot/shared'
import { api } from '../api'
const props = defineProps<{
show: boolean
/** 检查项名称 */
titles: string[]
/** sample:实验编号 或 assignment:作业编号,和服务器返回的 target 对应 */
target: string
/** 开始检查的接口(/api/samples/:id/selfcheck 或 /api/assignments/:id/selfcheck) */
url: string
/** 学生现在的代码 */
files: SourceFile[]
/** 开始检查前调用(先把没保存的改动存好) */
beforeStart?: () => Promise<void>
/** 作业的各项分值(自由练习没有) */
points?: number[]
}>()
const emit = defineEmits<{ 'update:show': [show: boolean] }>()
const state = ref<SelfCheck | null>(null)
const error = ref('')
/** 开始检查时的代码;之后改过代码就提示结果可能过时(刷新页面后不知道,不提示) */
const checkedFiles = ref<string>()
const current = computed(() => (state.value?.target === props.target ? state.value : null))
const running = computed(() => current.value?.status === 'running')
const outdated = computed(() => checkedFiles.value !== undefined && current.value?.status === 'done' && checkedFiles.value !== JSON.stringify(props.files))
const passed = computed(() => current.value?.result?.results.filter((r) => r.passed).length ?? 0)
let timer: ReturnType<typeof setTimeout> | undefined
async function refresh() {
clearTimeout(timer)
state.value = await api<SelfCheck | null>('GET', '/api/selfcheck').catch(() => state.value)
if (state.value?.status === 'running') timer = setTimeout(refresh, 2000)
}
onBeforeUnmount(() => clearTimeout(timer))
watch(
() => props.show,
(show) => {
if (show) void refresh()
},
{ immediate: true },
)
async function start() {
error.value = ''
try {
await props.beforeStart?.()
checkedFiles.value = JSON.stringify(props.files)
state.value = await api<SelfCheck>('POST', props.url, { files: props.files })
timer = setTimeout(refresh, 2000)
} catch (e) {
error.value = (e as Error).message
}
}
const formatTime = (ms: number) => new Date(ms).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
</script>
<template>
<NDrawer :show="show" :width="420" placement="right" @update:show="emit('update:show', $event)">
<NDrawerContent title="评价项" closable>
<p class="note">
下面这些功能项由平台自动检查。点"自己检查",平台会运行你现在的程序,逐项操作、核对结果,
告诉你哪几项还没做好。<b>不计分、不留记录</b>,可以随时检查。
</p>
<div class="actions">
<NButton type="primary" :loading="running" :disabled="running || titles.length === 0" @click="start">自己检查</NButton>
<span v-if="running" class="status"><NSpin size="small" /> 正在检查,每项都要启动一次程序,请稍等…</span>
<span v-else-if="current?.status === 'done' && current.result" class="status">
{{ formatTime(current.startedAt) }} 检查:通过 <b>{{ passed }}</b> / {{ current.result.results.length }} 项
</span>
<span v-else-if="current?.status === 'error'" class="status error">检查时服务器出错了,请再试一次</span>
</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="outdated" class="outdated">检查之后代码改过了,结果可能已经过时</div>
<div v-if="current?.result && !current.result.compiled" class="error">编译没有通过,先改正编译错误</div>
<ul class="items">
<li
v-for="(title, i) in titles"
:key="i"
:class="current?.result && !running ? (current.result.results[i]?.passed ? 'passed' : 'failed') : ''"
>
<span class="mark">{{ current?.result && !running ? (current.result.results[i]?.passed ? '✓' : '✗') : '·' }}</span>
<div>
<div class="title">
{{ title }}<span v-if="points" class="points">{{ points[i] }} 分</span>
</div>
<div v-if="current?.result && !running && current.result.results[i]?.message" class="message">
{{ current.result.results[i].message }}
</div>
</div>
</li>
</ul>
</NDrawerContent>
</NDrawer>
</template>
<style scoped>
.note {
margin: 0 0 12px;
font-size: 13px;
color: #4b5563;
line-height: 1.6;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #374151;
}
.error {
color: #b91c1c;
font-size: 13px;
}
.outdated {
margin: 4px 0;
color: #a16207;
font-size: 13px;
}
.items {
margin: 12px 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
}
.items li {
display: flex;
gap: 8px;
}
.mark {
width: 1em;
flex: none;
font-weight: bold;
color: #9ca3af;
}
.passed .mark {
color: #16a34a;
}
.failed .mark {
color: #dc2626;
}
.points {
margin-left: 8px;
color: #6b7280;
}
.message {
margin-top: 2px;
color: #b91c1c;
}
</style>
+34
View File
@@ -18,6 +18,7 @@ import { deleteWork, loadWork, sampleWorkKey, saveWork } from '../editor/works'
import RunArea from '../run/RunArea.vue'
import { createRunSession } from '../run/session'
import GradeResultView from '../grading/GradeResultView.vue'
import SelfCheckPanel from '../grading/SelfCheckPanel.vue'
import { captureForms } from '../grading/screenshots'
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
@@ -134,9 +135,28 @@ async function openProject(key: string, original: () => Promise<SourceFile[]>, d
async function loadSample(id: string, discardWork = false) {
sampleId.value = id
void router.replace({ query: { sample: id } })
sampleChecks.value = []
void api<string[]>('GET', `/api/samples/${encodeURIComponent(id)}/checks`)
.then((titles) => {
if (sampleId.value === id) sampleChecks.value = titles
})
.catch(() => {})
await openProject(sampleWorkKey(id), () => api<SourceFile[]>('GET', `/api/samples/${encodeURIComponent(id)}`), discardWork)
}
// ---------- 评价项和自测 ----------
const sampleChecks = ref<string[]>([])
const selfCheckOpen = ref(false)
/** 作业用布置时的评分规则,自由练习用实验的评分规则 */
const selfCheck = computed(() => {
if (props.assignmentId) {
const a = assignment.value
return a ? { titles: a.checkTitles, target: `assignment:${a.id}`, url: `/api/assignments/${a.id}/selfcheck`, points: a.scoring.auto } : null
}
const id = sampleId.value
return id && sampleChecks.value.length ? { titles: sampleChecks.value, target: `sample:${id}`, url: `/api/samples/${encodeURIComponent(id)}/selfcheck` } : null
})
// ---------- 作业 ----------
const assignment = ref<Assignment | null>(null)
const submission = ref<Submission | null>(null)
@@ -456,6 +476,9 @@ function run() {
{{ scoreText }}
</NButton>
</template>
<NButton v-if="selfCheck && !viewing" size="small" title="这个任务自动检查哪些功能;可以自己先检查一遍(不计分)" @click="selfCheckOpen = true">
评价项
</NButton>
<NPopconfirm v-if="modified && !viewing" positive-text="恢复" negative-text="取消" @positive-click="restoreOriginal">
<template #trigger>
<NButton quaternary size="small">{{ assignmentId ? '恢复到作业起点' : '恢复原始代码' }}</NButton>
@@ -488,6 +511,17 @@ function run() {
<GradeResultView :assignment="assignment" :submission="submission" :review="review" />
</NModal>
<SelfCheckPanel
v-if="selfCheck"
v-model:show="selfCheckOpen"
:titles="selfCheck.titles"
:target="selfCheck.target"
:url="selfCheck.url"
:points="selfCheck.points"
:files="files"
:before-start="flushSave"
/>
<main class="workspace" :class="{ designing: activeForm }">
<section class="editor">
<div class="tab-row">
+2
View File
@@ -142,6 +142,8 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
border-radius: 4px;
/* 窗体被拖到桌面外面时可以滚动查看 */
overflow: auto;
/* 窗体、提示框的层级只在桌面里比较,不会盖住网页的抽屉和对话框 */
isolation: isolate;
}
.desktop-canvas {
+1 -1
View File
@@ -63,7 +63,7 @@ async function chooseSample(id: string) {
sampleId.value = id
title.value = samples.value.find((s) => s.value === id)?.label ?? ''
try {
checkTitles.value = await api<string[]>('GET', `/api/teacher/samples/${encodeURIComponent(id)}/checks`)
checkTitles.value = await api<string[]>('GET', `/api/samples/${encodeURIComponent(id)}/checks`)
auto.value = checkTitles.value.map(() => 10)
} catch (e) {
checkTitles.value = []