任务说明:实验页"任务"面板、布置作业时可改写;先写项目 3 任务 2、项目 6 任务 2 两份

- 每个实验一份 task.md(任务目标、控件名表格、实现步骤、和书上不一样的地方、在平台上怎么试)
- 工具栏"评价项"改成"任务":上面是任务说明,下面是评价项和自己检查;不遮挡页面,第一次打开实验时自动打开
- 自己写的 Markdown 子集渲染,先转义全部 HTML(老师可以改写说明)
- 作业保存任务说明(新列 task),以前布置的作业用实验现在的说明
- 测试:任务说明表格里的控件名都在示例的 Designer.cs 里
- 修改作业时实验下拉框显示标题而不是编号

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 10:04:05 +08:00
1 parent 3b9f635c35
commit 90b739a2c7
17 files changed
+759 -219

No files matched your search

+44 -17
View File
@@ -2,7 +2,7 @@
// 实验页:编辑代码 → 运行 → 在网页里操作学生的窗体和虚拟实训台。
// 三种用法:自由练习(选择示例工程);做作业(/assignments/:id,从作业起点开始,可以提交评分);
// 老师运行学生提交的程序(/teacher/submissions/:id/run,改动只用于试运行,不保存)。
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NButton, NInput, NModal, NPopconfirm, NPopover, NSelect, NTab, NTabs, NTag, useMessage } from 'naive-ui'
import type { Assignment, Review, SourceFile, Submission } from '@iot/shared'
@@ -18,7 +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 TaskPanel from '../grading/TaskPanel.vue'
import { captureForms } from '../grading/screenshots'
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
@@ -136,27 +136,36 @@ async function loadSample(id: string, discardWork = false) {
sampleId.value = id
void router.replace({ query: { sample: id } })
sampleChecks.value = []
sampleTask.value = null
void api<string[]>('GET', `/api/samples/${encodeURIComponent(id)}/checks`)
.then((titles) => {
if (sampleId.value === id) sampleChecks.value = titles
})
.catch(() => {})
void api<{ task: string | null }>('GET', `/api/samples/${encodeURIComponent(id)}/task`)
.then(({ task }) => {
if (sampleId.value === id) sampleTask.value = task
})
.catch(() => {})
await openProject(sampleWorkKey(id), () => api<SourceFile[]>('GET', `/api/samples/${encodeURIComponent(id)}`), discardWork)
}
// ---------- 评价项和自测 ----------
// ---------- 任务说明、评价项和自测 ----------
const sampleChecks = ref<string[]>([])
const selfCheckOpen = ref(false)
/** 作业用布置时的评分规则,自由练习用实验的评分规则 */
const selfCheck = computed(() => {
const sampleTask = ref<string | null>(null)
const taskOpen = ref(false)
/** 作业用布置时的任务说明和评分规则,自由练习用实验的 */
const taskPanel = 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
return a ? { task: a.task, 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
if (!id || (!sampleTask.value && !sampleChecks.value.length)) return null
return { task: sampleTask.value, titles: sampleChecks.value, target: `sample:${id}`, url: `/api/samples/${encodeURIComponent(id)}/selfcheck` }
})
// ---------- 作业 ----------
const assignment = ref<Assignment | null>(null)
const submission = ref<Submission | null>(null)
@@ -197,6 +206,23 @@ async function loadAssignment(discardWork = false) {
// ---------- 老师运行学生提交的程序 ----------
const viewing = ref<{ studentName: string; username: string; assignmentTitle: string } | null>(null)
// 第一次打开一个实验(或作业)时自动打开任务面板;记在浏览器里,之后不再自动打开
const TASK_SEEN_KEY = 'iot-sim:task-seen'
watch(
() => (taskPanel.value?.task && !viewing.value ? taskPanel.value.target : null),
(target: string | null) => {
if (!target) return
try {
const seen: string[] = JSON.parse(localStorage.getItem(TASK_SEEN_KEY) ?? '[]')
if (seen.includes(target)) return
localStorage.setItem(TASK_SEEN_KEY, JSON.stringify([...seen, target].slice(-200)))
} catch {
return
}
taskOpen.value = true
},
)
async function loadSubmission() {
try {
const result = await api<{ files: SourceFile[]; studentName: string; username: string; assignmentTitle: string }>(
@@ -496,8 +522,8 @@ function run() {
{{ scoreText }}
</NButton>
</template>
<NButton v-if="selfCheck && !viewing" size="small" title="这个任务自动检查哪些功能;可以自己先检查一遍(不计分)" @click="selfCheckOpen = true">
评价项
<NButton v-if="taskPanel && !viewing" size="small" title="任务说明、自动检查哪些功能;可以自己先检查一遍(不计分)" @click="taskOpen = !taskOpen">
任务
</NButton>
<NPopconfirm v-if="modified && !viewing" positive-text="恢复" negative-text="取消" @positive-click="restoreOriginal">
<template #trigger>
@@ -532,13 +558,14 @@ 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"
<TaskPanel
v-if="taskPanel"
v-model:show="taskOpen"
:task="taskPanel.task"
:titles="taskPanel.titles"
:target="taskPanel.target"
:url="taskPanel.url"
:points="taskPanel.points"
:files="files"
:before-start="flushSave"
/>