diff --git a/docs/设计方案.md b/docs/设计方案.md index ba98a58..0046c3e 100644 --- a/docs/设计方案.md +++ b/docs/设计方案.md @@ -235,9 +235,9 @@ IOT/ ```mermaid flowchart TB P0["阶段 0 · 技术验证(已完成 2026-10-22)教材项目 2 任务 1 原样运行;实测编译、延迟、内存"] - P1["阶段 1 · MVP(进行中,约 3 个月)虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔部署(沙箱、Docker Compose、GitHub Actions)✔"] - P2["阶段 2 · 教学闭环(进行中,约 2 个月)账号与班级 ✔、作品保存在服务器 ✔、自动评分 ✔、布置与提交作业 ✔评阅(看代码、运行、老师评分、评语)✔、成绩表与导出 ✔;一个班试用"] - P3["阶段 3 · 扩展(进行中)教材项目 4–8:摄像头 ✔、线程 ✔、SQLite 模拟数据库 ✔、虚拟时钟 ✔、图表 ✔"] + P1["阶段 1 · MVP(已完成 2026-10-22)虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔部署(沙箱、Docker Compose、GitHub Actions)✔"] + P2["阶段 2 · 教学闭环(功能已完成,待一个班试用)账号与班级 ✔、作品保存在服务器 ✔、自动评分 ✔、布置与提交作业 ✔评阅(看代码、运行、老师评分、评语)✔、成绩表与导出 ✔一个班试用(2026-10-26 已部署上线,待试用)"] + P3["阶段 3 · 扩展(功能已完成 2026-10-23)教材项目 4–8:摄像头 ✔、线程 ✔、SQLite 模拟数据库 ✔、虚拟时钟 ✔、图表 ✔教材 8 个项目共 23 个示例 ✔、任务说明 ✔"] P4["阶段 4 · 进阶(之后)校园街景场景、故障注入题库、导出 VS 工程"] P0 -->|"关口:链路跑通,延迟和资源占用可接受"| P1 P1 -->|"关口:教材项目 1–3 能在平台上完整做完"| P2 @@ -247,6 +247,8 @@ flowchart TB 并行事项:尽早办理 ICP 备案(需数周)。 +**当前状态(2026-10-26)**:第 1–3 阶段的功能已全部完成,平台已部署到线上服务器(GitHub Actions 自动部署)。下一步是第 2 阶段的关口:带一个班试用,按试用中暴露的问题修改,之后再考虑第 4 阶段。 + **第 0 阶段结果(2026-10-22,已完成)**:教材项目 2 任务 1 的书上代码一字未改,在网页中完整运行:选串口、打开、推送文字到 LED 屏,未打开串口时弹出 "Error:发送失败"。本机实测(16 核、28 GB): | 指标 | 结果 | diff --git a/web/src/pages/DatabasePage.vue b/web/src/pages/DatabasePage.vue index 98d3da6..87205ab 100644 --- a/web/src/pages/DatabasePage.vue +++ b/web/src/pages/DatabasePage.vue @@ -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) - + 会用实验的初始数据替换实验用到的数据库(如"环境数据表"),你在这些库里做的改动会丢失;其他数据库不受影响。 diff --git a/web/src/pages/WorkbenchPage.vue b/web/src/pages/WorkbenchPage.vue index 7aa9582..27c3eb9 100644 --- a/web/src/pages/WorkbenchPage.vue +++ b/web/src/pages/WorkbenchPage.vue @@ -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([]) const sampleId = ref() const files = ref([]) /** 当前标签页:文件路径,或 "设计: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() { 物联网编程仿真平台 - + ▶ 运行 ■ 停止 diff --git a/web/src/samples.ts b/web/src/samples.ts new file mode 100644 index 0000000..75f7527 --- /dev/null +++ b/web/src/samples.ts @@ -0,0 +1,34 @@ +// 实验下拉菜单:按教材的项目分组,组名是项目名,组里每项只写"任务N …";选中后框里显示完整标题 +import { h } from 'vue' +import type { SelectGroupOption, SelectOption, SelectRenderTag } from 'naive-ui' + +/** 教材 8 个项目的名称(去掉"智慧校园系统——"前缀) */ +const PROJECTS: Record = { + 1: 'C#入门基础', + 2: '环境监测模块', + 3: '智慧路灯控制模块', + 4: '入侵信息监测模块', + 5: '灾情信息监测模块', + 6: '数据存储模块', + 7: '情景控制模块', + 8: '智慧校园管理平台', +} + +/** 实验标题形如"项目3 任务2 实现人体红外传感器和路灯联动",按项目分组;不是这种格式的放进"其他" */ +export function sampleOptions(list: { id: string; title: string }[]): SelectGroupOption[] { + const groups = new Map() + 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) diff --git a/web/src/teacher/AssignmentDialog.vue b/web/src/teacher/AssignmentDialog.vue index 181e1fd..0e24ce7 100644 --- a/web/src/teacher/AssignmentDialog.vue +++ b/web/src/teacher/AssignmentDialog.vue @@ -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(null) const title = ref('') const startMode = ref('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() { > 实验 - + 作业名称
会用实验的初始数据替换实验用到的数据库(如"环境数据表"),你在这些库里做的改动会丢失;其他数据库不受影响。