Files
IOT_SIM/web/src/grading/TaskPanel.vue
T
xuyueandClaude Opus 5.5 f6024dc113
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
试用前走查的修改:报错看得见、学生找得到作业、运行区和设计器的若干体验问题
- 运行区:编译错误、运行异常、控制台输出和保存的图片移到窗体下面,不再被实训台挤成 0 高;缩放和放大按钮移到桌面上方,不压窗体的关闭按钮;放大后也能停止程序;点停止后清掉窗体
- 运行异常:常见 .NET 异常配中文说明,显示出错的文件、行号和方法,点击跳到代码
- 运行中的下拉列表挂到页面最外层,能伸出窗体和分组框(小窗体也能选到 COM4、COM5)
- 学生登录后进"我的作业";任务面板从工具栏下面开始,不挡导航;记住上次做的实验,从数据库页面返回、恢复数据时默认用它
- 代码页和设计页左栏都占 3/5,文件标签放不下时换行;实验选择框加宽;关闭粘性滚动
- 设计器:双击生成的事件方法带缩进;双击工具箱放进选中控件所在的容器并错开位置;新控件比容器宽时按比例缩小;无边框窗体不画标题栏
- 中文界面:代码编辑器(查找替换等)、属性窗口的颜色选项
- SQL:DATEADD、DATEDIFF、DATEPART 的日期单位(day、hour)解析成 IdentifierLiteral 时报"不支持",补上
- 项目 8 的预置环境数据改成建库那天之前的 7 天;p6-t3 评分项改为"按日期查询环境数据"
- 没有 AI 建议时不显示"全部采纳 AI 建议"和相关说明;导出 CSV 只给纯数字的文字加制表符

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:49:42 +08:00

283 lines
7.5 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">
// 任务面板:上面是任务说明(task.md,作业则是布置时的说明),下面是评价项——这个实验(或作业)自动检查的功能项;
// "自己检查"在服务器上运行学生现在的程序逐项检查,告诉他哪几项不通过、为什么。不计分、不留记录,提交作业另算。
// 面板不遮挡页面,打开着也能接着搭界面、写代码。
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'
import { renderMarkdown } from './markdown'
const props = defineProps<{
show: boolean
/** 任务说明(Markdown),没有时为 null */
task: string | null
/** 检查项名称(没有评分规则时为空) */
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)
const taskHtml = computed(() => (props.task ? renderMarkdown(props.task) : ''))
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 && props.titles.length) 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"
:default-width="460"
resizable
placement="right"
:show-mask="false"
:trap-focus="false"
:block-scroll="false"
class="task-drawer"
@update:show="emit('update:show', $event)"
>
<NDrawerContent title="任务" closable>
<!-- renderMarkdown 先转义全部 HTML,只输出它自己生成的标签 -->
<div v-if="taskHtml" class="task" v-html="taskHtml" />
<template v-if="titles.length">
<h4 class="section">评价项</h4>
<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>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style>
/* 从顶部工具栏下面开始,不挡住"我的作业""数据库""退出"等链接 */
.n-drawer.task-drawer {
top: 49px;
}
</style>
<style scoped>
.task {
font-size: 14px;
line-height: 1.7;
color: #1f2937;
}
.task :deep(h4),
.section {
margin: 16px 0 6px;
font-size: 15px;
}
.task :deep(h4:first-child) {
margin-top: 0;
}
.task :deep(h5) {
margin: 12px 0 4px;
font-size: 14px;
}
.task :deep(p),
.task :deep(ul),
.task :deep(ol) {
margin: 4px 0;
}
.task :deep(ul),
.task :deep(ol) {
padding-left: 20px;
}
.task :deep(code) {
padding: 0 3px;
border-radius: 3px;
background: #f3f4f6;
font-size: 13px;
}
.task :deep(table) {
width: 100%;
margin: 6px 0;
border-collapse: collapse;
font-size: 13px;
}
.task :deep(th),
.task :deep(td) {
padding: 3px 6px;
border: 1px solid #e5e7eb;
text-align: left;
}
.task :deep(th) {
background: #f9fafb;
}
/* 控件类型和控件名不折行,说明列让出宽度 */
.task :deep(th),
.task :deep(td:first-child),
.task :deep(td code) {
white-space: nowrap;
}
.task :deep(blockquote) {
margin: 8px 0;
padding: 6px 10px;
border-left: 3px solid #f59e0b;
background: #fffbeb;
font-size: 13px;
}
.section {
padding-top: 12px;
border-top: 1px solid #e5e7eb;
}
.task + .section {
margin-top: 20px;
}
.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>