实验下拉菜单按项目分组;路线图更新到已上线、待一个班试用
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ce800c77f
commit
61a32e900d
5 files changed
+68
-13
No files matched your search
+5
-3
@@ -235,9 +235,9 @@ IOT/
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
P0["阶段 0 · 技术验证(已完成 2026-10-22)<br/>教材项目 2 任务 1 原样运行;实测编译、延迟、内存"]
|
P0["阶段 0 · 技术验证(已完成 2026-10-22)<br/>教材项目 2 任务 1 原样运行;实测编译、延迟、内存"]
|
||||||
P1["阶段 1 · MVP(进行中,约 3 个月)<br/>虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔<br/>教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔<br/>部署(沙箱、Docker Compose、GitHub Actions)✔"]
|
P1["阶段 1 · MVP(已完成 2026-10-22)<br/>虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔<br/>教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔<br/>部署(沙箱、Docker Compose、GitHub Actions)✔"]
|
||||||
P2["阶段 2 · 教学闭环(进行中,约 2 个月)<br/>账号与班级 ✔、作品保存在服务器 ✔、自动评分 ✔、布置与提交作业 ✔<br/>评阅(看代码、运行、老师评分、评语)✔、成绩表与导出 ✔;一个班试用"]
|
P2["阶段 2 · 教学闭环(功能已完成,待一个班试用)<br/>账号与班级 ✔、作品保存在服务器 ✔、自动评分 ✔、布置与提交作业 ✔<br/>评阅(看代码、运行、老师评分、评语)✔、成绩表与导出 ✔<br/>一个班试用(2026-10-26 已部署上线,待试用)"]
|
||||||
P3["阶段 3 · 扩展(进行中)<br/>教材项目 4–8:摄像头 ✔、线程 ✔、SQLite 模拟数据库 ✔、虚拟时钟 ✔、图表 ✔"]
|
P3["阶段 3 · 扩展(功能已完成 2026-10-23)<br/>教材项目 4–8:摄像头 ✔、线程 ✔、SQLite 模拟数据库 ✔、虚拟时钟 ✔、图表 ✔<br/>教材 8 个项目共 23 个示例 ✔、任务说明 ✔"]
|
||||||
P4["阶段 4 · 进阶(之后)<br/>校园街景场景、故障注入题库、导出 VS 工程"]
|
P4["阶段 4 · 进阶(之后)<br/>校园街景场景、故障注入题库、导出 VS 工程"]
|
||||||
P0 -->|"关口:链路跑通,延迟和资源占用可接受"| P1
|
P0 -->|"关口:链路跑通,延迟和资源占用可接受"| P1
|
||||||
P1 -->|"关口:教材项目 1–3 能在平台上完整做完"| P2
|
P1 -->|"关口:教材项目 1–3 能在平台上完整做完"| P2
|
||||||
@@ -247,6 +247,8 @@ flowchart TB
|
|||||||
|
|
||||||
并行事项:尽早办理 ICP 备案(需数周)。
|
并行事项:尽早办理 ICP 备案(需数周)。
|
||||||
|
|
||||||
|
**当前状态(2026-10-26)**:第 1–3 阶段的功能已全部完成,平台已部署到线上服务器(GitHub Actions 自动部署)。下一步是第 2 阶段的关口:带一个班试用,按试用中暴露的问题修改,之后再考虑第 4 阶段。
|
||||||
|
|
||||||
**第 0 阶段结果(2026-10-22,已完成)**:教材项目 2 任务 1 的书上代码一字未改,在网页中完整运行:选串口、打开、推送文字到 LED 屏,未打开串口时弹出 "Error:发送失败"。本机实测(16 核、28 GB):
|
**第 0 阶段结果(2026-10-22,已完成)**:教材项目 2 任务 1 的书上代码一字未改,在网页中完整运行:选串口、打开、推送文字到 LED 屏,未打开串口时弹出 "Error:发送失败"。本机实测(16 核、28 GB):
|
||||||
|
|
||||||
| 指标 | 结果 |
|
| 指标 | 结果 |
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import EditTab from '../database/EditTab.vue'
|
|||||||
import ObjectExplorer, { type ExplorerAction } from '../database/ObjectExplorer.vue'
|
import ObjectExplorer, { type ExplorerAction } from '../database/ObjectExplorer.vue'
|
||||||
import QueryTab from '../database/QueryTab.vue'
|
import QueryTab from '../database/QueryTab.vue'
|
||||||
import { execute, loadExplorer, quoteName, type ExplorerDatabase, type ExplorerTable } from '../database/sql'
|
import { execute, loadExplorer, quoteName, type ExplorerDatabase, type ExplorerTable } from '../database/sql'
|
||||||
|
import { renderSampleTag, sampleOptions } from '../samples'
|
||||||
|
|
||||||
type Tab =
|
type Tab =
|
||||||
| { key: string; kind: 'query'; title: string; database: string; sql?: string; run?: boolean }
|
| { key: string; kind: 'query'; title: string; database: string; sql?: string; run?: boolean }
|
||||||
@@ -217,7 +218,7 @@ onMounted(refresh)
|
|||||||
</NModal>
|
</NModal>
|
||||||
|
|
||||||
<NModal :show="restoring !== null" preset="card" title="恢复实验初始数据" style="width: 460px" @update:show="restoring = null">
|
<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>
|
<p class="warning">会用实验的初始数据替换实验用到的数据库(如"环境数据表"),你在这些库里做的改动会丢失;其他数据库不受影响。</p>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-buttons">
|
<div class="dialog-buttons">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// 老师运行学生提交的程序(/teacher/submissions/:id/run,改动只用于试运行,不保存)。
|
// 老师运行学生提交的程序(/teacher/submissions/:id/run,改动只用于试运行,不保存)。
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
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 type { Assignment, Review, SourceFile, Submission } from '@iot/shared'
|
||||||
import FormDesigner from '../designer/FormDesigner.vue'
|
import FormDesigner from '../designer/FormDesigner.vue'
|
||||||
import { createDesignerHistory } from '../designer/history'
|
import { createDesignerHistory } from '../designer/history'
|
||||||
@@ -30,13 +30,14 @@ import { createRunSession } from '../run/session'
|
|||||||
import GradeResultView from '../grading/GradeResultView.vue'
|
import GradeResultView from '../grading/GradeResultView.vue'
|
||||||
import TaskPanel from '../grading/TaskPanel.vue'
|
import TaskPanel from '../grading/TaskPanel.vue'
|
||||||
import { captureForms } from '../grading/screenshots'
|
import { captureForms } from '../grading/screenshots'
|
||||||
|
import { renderSampleTag, sampleOptions } from '../samples'
|
||||||
|
|
||||||
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
|
const props = defineProps<{ assignmentId?: string; submissionId?: string }>()
|
||||||
|
|
||||||
const session = createRunSession()
|
const session = createRunSession()
|
||||||
const { state } = session
|
const { state } = session
|
||||||
|
|
||||||
const samples = ref<{ label: string; value: string }[]>([])
|
const samples = ref<SelectGroupOption[]>([])
|
||||||
const sampleId = ref<string>()
|
const sampleId = ref<string>()
|
||||||
const files = ref<SourceFile[]>([])
|
const files = ref<SourceFile[]>([])
|
||||||
/** 当前标签页:文件路径,或 "设计:X.Designer.cs" 表示窗体 X 的设计器 */
|
/** 当前标签页:文件路径,或 "设计:X.Designer.cs" 表示窗体 X 的设计器 */
|
||||||
@@ -126,7 +127,7 @@ onMounted(async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const list = await api<{ id: string; title: string }[]>('GET', '/api/samples')
|
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=示例编号 时直接打开该示例(便于老师把链接发给学生)
|
// 网址里带 ?sample=示例编号 时直接打开该示例(便于老师把链接发给学生)
|
||||||
const initial = list.find((s) => s.id === route.query.sample) ?? list[0]
|
const initial = list.find((s) => s.id === route.query.sample) ?? list[0]
|
||||||
if (initial) await loadSample(initial.id)
|
if (initial) await loadSample(initial.id)
|
||||||
@@ -522,7 +523,15 @@ function run() {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<strong>物联网编程仿真平台</strong>
|
<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>
|
</template>
|
||||||
<NButton type="primary" :disabled="running" title="F5" @click="run">▶ 运行</NButton>
|
<NButton type="primary" :disabled="running" title="F5" @click="run">▶ 运行</NButton>
|
||||||
<NButton :disabled="!running" title="Shift+F5" @click="session.stop">■ 停止</NButton>
|
<NButton :disabled="!running" title="Shift+F5" @click="session.stop">■ 停止</NButton>
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -7,6 +7,7 @@ import { NButton, NDatePicker, NInput, NInputNumber, NModal, NRadio, NRadioGroup
|
|||||||
import type { Assignment, Scoring, SourceFile, StartMode } from '@iot/shared'
|
import type { Assignment, Scoring, SourceFile, StartMode } from '@iot/shared'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { assignmentTemplate } from '../designer/templates'
|
import { assignmentTemplate } from '../designer/templates'
|
||||||
|
import { renderSampleTag, sampleOptions } from '../samples'
|
||||||
|
|
||||||
const props = defineProps<{ show: boolean; classId: number; editing: Assignment | null }>()
|
const props = defineProps<{ show: boolean; classId: number; editing: Assignment | null }>()
|
||||||
const emit = defineEmits<{ 'update:show': [show: boolean]; saved: [] }>()
|
const emit = defineEmits<{ 'update:show': [show: boolean]; saved: [] }>()
|
||||||
@@ -18,7 +19,8 @@ const DEFAULT_MANUAL = [
|
|||||||
{ title: '变量与控件命名规范', points: 10 },
|
{ 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 sampleId = ref<string | null>(null)
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
const startMode = ref<StartMode>('ui')
|
const startMode = ref<StartMode>('ui')
|
||||||
@@ -42,8 +44,7 @@ watch(
|
|||||||
async (show) => {
|
async (show) => {
|
||||||
if (!show) return
|
if (!show) return
|
||||||
if (samples.value.length === 0) {
|
if (samples.value.length === 0) {
|
||||||
const list = await api<{ id: string; title: string }[]>('GET', '/api/samples')
|
samples.value = await api<{ id: string; title: string }[]>('GET', '/api/samples')
|
||||||
samples.value = list.map((s) => ({ label: s.title, value: s.id }))
|
|
||||||
}
|
}
|
||||||
const e = props.editing
|
const e = props.editing
|
||||||
if (e) {
|
if (e) {
|
||||||
@@ -74,7 +75,7 @@ watch(
|
|||||||
|
|
||||||
async function chooseSample(id: string) {
|
async function chooseSample(id: string) {
|
||||||
sampleId.value = id
|
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(() => {
|
void loadSampleTask(id).then(() => {
|
||||||
if (sampleId.value === id) task.value = sampleTask.value
|
if (sampleId.value === id) task.value = sampleTask.value
|
||||||
})
|
})
|
||||||
@@ -128,7 +129,15 @@ async function save() {
|
|||||||
>
|
>
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<label>实验</label>
|
<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>
|
<label>作业名称</label>
|
||||||
<NInput v-model:value="title" placeholder="学生看到的作业名称" />
|
<NInput v-model:value="title" placeholder="学生看到的作业名称" />
|
||||||
|
|||||||
Reference in new issue
Block a user