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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:04:05 +08:00

254 lines
8.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
// 布置作业 / 修改作业。布置时选实验、起点和截止时间,给每个评分项设分值;
// 起点工程由浏览器用设计器引擎从参考答案生成(界面已做好:去掉事件和代码;空白工程:只有空的 Form1)。
// 任务说明从实验的 task.md 预填,老师可以改写(比如在前面加一段要求);修改时只能改名称、截止时间、分值和任务说明。
import { computed, ref, watch } from 'vue'
import { NButton, NDatePicker, NInput, NInputNumber, NModal, NRadio, NRadioGroup, NSelect, useMessage } from 'naive-ui'
import type { Assignment, Scoring, SourceFile, StartMode } from '@iot/shared'
import { api } from '../api'
import { assignmentTemplate } from '../designer/templates'
const props = defineProps<{ show: boolean; classId: number; editing: Assignment | null }>()
const emit = defineEmits<{ 'update:show': [show: boolean]; saved: [] }>()
const message = useMessage()
const DEFAULT_MANUAL = [
{ title: '窗体界面(布局合理、美观)', points: 10 },
{ title: '注释书写规范', points: 10 },
{ title: '变量与控件命名规范', points: 10 },
]
const samples = ref<{ label: string; value: string }[]>([])
const sampleId = ref<string | null>(null)
const title = ref('')
const startMode = ref<StartMode>('ui')
const deadline = ref<number | null>(null)
const checkTitles = ref<string[]>([])
const auto = ref<number[]>([])
const manual = ref<Scoring['manual']>([])
const task = ref('')
/** 实验原来的任务说明,"恢复成实验的说明"用 */
const sampleTask = ref('')
const saving = ref(false)
async function loadSampleTask(id: string) {
sampleTask.value = (await api<{ task: string | null }>('GET', `/api/samples/${encodeURIComponent(id)}/task`).catch(() => ({ task: null }))).task ?? ''
}
const total = computed(() => [...auto.value, ...manual.value.map((m) => m.points)].reduce((a, b) => a + (b || 0), 0))
watch(
() => props.show,
async (show) => {
if (!show) return
if (samples.value.length === 0) {
const list = await api<{ id: string; title: string }[]>('GET', '/api/samples')
samples.value = list.map((s) => ({ label: s.title, value: s.id }))
}
const e = props.editing
if (e) {
sampleId.value = e.sampleId
title.value = e.title
startMode.value = e.startMode
deadline.value = e.deadline
checkTitles.value = e.checkTitles
auto.value = [...e.scoring.auto]
manual.value = e.scoring.manual.map((m) => ({ ...m }))
task.value = e.task ?? ''
await loadSampleTask(e.sampleId)
// 加这个功能之前布置的作业没有保存说明,学生看到的是实验的说明
if (e.task === null) task.value = sampleTask.value
return
}
sampleId.value = null
title.value = ''
startMode.value = 'ui'
deadline.value = null
checkTitles.value = []
auto.value = []
manual.value = DEFAULT_MANUAL.map((m) => ({ ...m }))
task.value = ''
sampleTask.value = ''
},
)
async function chooseSample(id: string) {
sampleId.value = id
title.value = samples.value.find((s) => s.value === id)?.label ?? ''
void loadSampleTask(id).then(() => {
if (sampleId.value === id) task.value = sampleTask.value
})
try {
checkTitles.value = await api<string[]>('GET', `/api/samples/${encodeURIComponent(id)}/checks`)
auto.value = checkTitles.value.map(() => 10)
} catch (e) {
checkTitles.value = []
auto.value = []
message.error((e as Error).message)
}
}
async function save() {
if (!sampleId.value || saving.value) return
saving.value = true
const scoring: Scoring = { auto: auto.value.map((p) => p || 0), manual: manual.value.map((m) => ({ title: m.title.trim(), points: m.points || 0 })) }
try {
if (props.editing) {
await api('PATCH', `/api/teacher/assignments/${props.editing.id}`, { title: title.value, deadline: deadline.value, scoring, task: task.value })
} else {
const files = await api<SourceFile[]>('GET', `/api/samples/${encodeURIComponent(sampleId.value)}`)
await api('POST', '/api/teacher/assignments', {
classId: props.classId,
sampleId: sampleId.value,
title: title.value,
startMode: startMode.value,
template: assignmentTemplate(files, startMode.value),
scoring,
deadline: deadline.value,
task: task.value,
})
}
emit('saved')
emit('update:show', false)
} catch (e) {
message.error((e as Error).message)
} finally {
saving.value = false
}
}
</script>
<template>
<NModal
:show="show"
preset="card"
:title="editing ? '修改作业' : '布置作业'"
style="width: 620px; max-width: calc(100vw - 32px)"
@update:show="emit('update:show', $event)"
>
<div class="form">
<label>实验</label>
<NSelect :value="sampleId" :options="samples" :disabled="!!editing" placeholder="选择实验" @update:value="chooseSample" />
<label>作业名称</label>
<NInput v-model:value="title" placeholder="学生看到的作业名称" />
<label>学生的起点</label>
<NRadioGroup v-model:value="startMode" :disabled="!!editing">
<NRadio value="ui">界面已做好(只写事件和代码)</NRadio>
<NRadio value="blank">空白工程(从搭界面做起)</NRadio>
</NRadioGroup>
<label>截止时间</label>
<NDatePicker v-model:value="deadline" type="datetime" clearable placeholder="不限" format="yyyy-MM-dd HH:mm" />
<template v-if="checkTitles.length">
<label>自动评分</label>
<div class="points">
<div v-for="(t, i) in checkTitles" :key="i" class="point-row">
<span class="point-title">{{ t }}</span>
<NInputNumber v-model:value="auto[i]" :min="0" size="small" class="point-input" />
<span>分</span>
</div>
</div>
</template>
<label>任务说明</label>
<div class="task">
<NInput v-model:value="task" type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" placeholder="这个实验还没有任务说明" />
<div class="hint">
学生在实验页的"任务"面板里看到。可以在最前面加一段要求;格式:<code>## 标题</code>、<code>- 列表</code>、<code>**加粗**</code>
<NButton v-if="sampleTask && task !== sampleTask" size="tiny" quaternary @click="task = sampleTask">恢复成实验的说明</NButton>
</div>
</div>
<label>老师评分</label>
<div class="points">
<div v-for="(m, i) in manual" :key="i" class="point-row">
<NInput v-model:value="m.title" size="small" class="point-title" />
<NInputNumber v-model:value="m.points" :min="0" size="small" class="point-input" />
<span>分</span>
<NButton size="tiny" quaternary @click="manual.splice(i, 1)">删除</NButton>
</div>
<NButton size="small" dashed @click="manual.push({ title: '', points: 10 })">+ 添加老师评分项</NButton>
</div>
</div>
<template #footer>
<div class="footer">
<span class="total">总分 {{ total }} 分</span>
<NButton @click="emit('update:show', false)">取消</NButton>
<NButton type="primary" :loading="saving" :disabled="!sampleId || !title.trim()" @click="save">
{{ editing ? '保存' : '布置' }}
</NButton>
</div>
</template>
</NModal>
</template>
<style scoped>
.form {
display: grid;
grid-template-columns: 80px minmax(0, 1fr);
gap: 12px;
align-items: center;
}
.form label {
color: #4b5563;
font-size: 13px;
align-self: start;
padding-top: 6px;
}
.task {
display: flex;
flex-direction: column;
gap: 4px;
}
.hint {
color: #6b7280;
font-size: 12px;
}
.hint code {
padding: 0 3px;
background: #f3f4f6;
}
.points {
display: flex;
flex-direction: column;
gap: 6px;
align-items: flex-start;
}
.point-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.point-title {
flex: 1;
font-size: 13px;
}
.point-input {
width: 90px;
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.total {
margin-right: auto;
color: #4b5563;
}
</style>