实验下拉菜单按项目分组;路线图更新到已上线、待一个班试用
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 21:39:40 +08:00
1 parent 4ce800c77f
commit 61a32e900d
5 files changed
+68 -13

No files matched your search

+2 -1
View File
@@ -10,6 +10,7 @@ import EditTab from '../database/EditTab.vue'
import ObjectExplorer, { type ExplorerAction } from '../database/ObjectExplorer.vue'
import QueryTab from '../database/QueryTab.vue'
import { execute, loadExplorer, quoteName, type ExplorerDatabase, type ExplorerTable } from '../database/sql'
import { renderSampleTag, sampleOptions } from '../samples'
type Tab =
| { key: string; kind: 'query'; title: string; database: string; sql?: string; run?: boolean }
@@ -217,7 +218,7 @@ onMounted(refresh)
</NModal>
<NModal :show="restoring !== null" preset="card" title="恢复实验初始数据" style="width: 460px" @update:show="restoring = null">
<NSelect v-model:value="restoring" :options="experiments.map((e) => ({ label: e.title, value: e.id }))" />
<NSelect v-model:value="restoring" :options="sampleOptions(experiments)" :render-tag="renderSampleTag" />
<p class="warning">会用实验的初始数据替换实验用到的数据库(如"环境数据表"),你在这些库里做的改动会丢失;其他数据库不受影响。</p>
<template #footer>
<div class="dialog-buttons">
+13 -4
View File
@@ -4,7 +4,7 @@
// 老师运行学生提交的程序(/teacher/submissions/:id/run,改动只用于试运行,不保存)。
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 { NButton, NInput, NModal, NPopconfirm, NPopover, NSelect, NTab, NTabs, NTag, useMessage, type SelectGroupOption } from 'naive-ui'
import type { Assignment, Review, SourceFile, Submission } from '@iot/shared'
import FormDesigner from '../designer/FormDesigner.vue'
import { createDesignerHistory } from '../designer/history'
@@ -30,13 +30,14 @@ import { createRunSession } from '../run/session'
import GradeResultView from '../grading/GradeResultView.vue'
import TaskPanel from '../grading/TaskPanel.vue'
import { captureForms } from '../grading/screenshots'
import { renderSampleTag, sampleOptions } from '../samples'
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
const session = createRunSession()
const { state } = session
const samples = ref<{ label: string; value: string }[]>([])
const samples = ref<SelectGroupOption[]>([])
const sampleId = ref<string>()
const files = ref<SourceFile[]>([])
/** 当前标签页:文件路径,或 "设计:X.Designer.cs" 表示窗体 X 的设计器 */
@@ -126,7 +127,7 @@ onMounted(async () => {
return
}
const list = await api<{ id: string; title: string }[]>('GET', '/api/samples')
samples.value = list.map((s) => ({ label: s.title, value: s.id }))
samples.value = sampleOptions(list)
// 网址里带 ?sample=示例编号 时直接打开该示例(便于老师把链接发给学生)
const initial = list.find((s) => s.id === route.query.sample) ?? list[0]
if (initial) await loadSample(initial.id)
@@ -522,7 +523,15 @@ function run() {
</template>
<template v-else>
<strong>物联网编程仿真平台</strong>
<NSelect class="sample-select" :value="sampleId" :options="samples" placeholder="选择示例工程" @update:value="loadSample" />
<NSelect
class="sample-select"
:value="sampleId"
:options="samples"
:render-tag="renderSampleTag"
:consistent-menu-width="false"
placeholder="选择示例工程"
@update:value="loadSample"
/>
</template>
<NButton type="primary" :disabled="running" title="F5" @click="run">▶ 运行</NButton>
<NButton :disabled="!running" title="Shift+F5" @click="session.stop">■ 停止</NButton>
+34
View File
@@ -0,0 +1,34 @@
// 实验下拉菜单:按教材的项目分组,组名是项目名,组里每项只写"任务N …";选中后框里显示完整标题
import { h } from 'vue'
import type { SelectGroupOption, SelectOption, SelectRenderTag } from 'naive-ui'
/** 教材 8 个项目的名称(去掉"智慧校园系统——"前缀) */
const PROJECTS: Record<string, string> = {
1: 'C#入门基础',
2: '环境监测模块',
3: '智慧路灯控制模块',
4: '入侵信息监测模块',
5: '灾情信息监测模块',
6: '数据存储模块',
7: '情景控制模块',
8: '智慧校园管理平台',
}
/** 实验标题形如"项目3 任务2 实现人体红外传感器和路灯联动",按项目分组;不是这种格式的放进"其他" */
export function sampleOptions(list: { id: string; title: string }[]): SelectGroupOption[] {
const groups = new Map<string, SelectGroupOption & { children: SelectOption[] }>()
for (const s of list) {
const m = /^项目(\d+)\s*(.*)$/.exec(s.title)
const key = m ? m[1]! : '其他'
let group = groups.get(key)
if (!group) {
group = { type: 'group', key, label: m ? `项目${key} ${PROJECTS[key] ?? ''}`.trim() : '其他', children: [] }
groups.set(key, group)
}
group.children.push({ label: m ? m[2] : s.title, value: s.id, title: s.title })
}
return [...groups.values()]
}
/** 选择框里显示完整标题(带项目号),下拉列表里仍是简短的"任务N …" */
export const renderSampleTag: SelectRenderTag = ({ option }) => h('span', { title: option.title as string }, option.title as string)
+14 -5
View File
@@ -7,6 +7,7 @@ import { NButton, NDatePicker, NInput, NInputNumber, NModal, NRadio, NRadioGroup
import type { Assignment, Scoring, SourceFile, StartMode } from '@iot/shared'
import { api } from '../api'
import { assignmentTemplate } from '../designer/templates'
import { renderSampleTag, sampleOptions } from '../samples'
const props = defineProps<{ show: boolean; classId: number; editing: Assignment | null }>()
const emit = defineEmits<{ 'update:show': [show: boolean]; saved: [] }>()
@@ -18,7 +19,8 @@ const DEFAULT_MANUAL = [
{ title: '变量与控件命名规范', points: 10 },
]
const samples = ref<{ label: string; value: string }[]>([])
const samples = ref<{ id: string; title: string }[]>([])
const sampleGroups = computed(() => sampleOptions(samples.value))
const sampleId = ref<string | null>(null)
const title = ref('')
const startMode = ref<StartMode>('ui')
@@ -42,8 +44,7 @@ watch(
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 }))
samples.value = await api<{ id: string; title: string }[]>('GET', '/api/samples')
}
const e = props.editing
if (e) {
@@ -74,7 +75,7 @@ watch(
async function chooseSample(id: string) {
sampleId.value = id
title.value = samples.value.find((s) => s.value === id)?.label ?? ''
title.value = samples.value.find((s) => s.id === id)?.title ?? ''
void loadSampleTask(id).then(() => {
if (sampleId.value === id) task.value = sampleTask.value
})
@@ -128,7 +129,15 @@ async function save() {
>
<div class="form">
<label>实验</label>
<NSelect :value="sampleId" :options="samples" :disabled="!!editing" placeholder="选择实验" @update:value="chooseSample" />
<NSelect
:value="sampleId"
:options="sampleGroups"
:render-tag="renderSampleTag"
:consistent-menu-width="false"
:disabled="!!editing"
placeholder="选择实验"
@update:value="chooseSample"
/>
<label>作业名称</label>
<NInput v-model:value="title" placeholder="学生看到的作业名称" />