diff --git a/docs/设计方案.md b/docs/设计方案.md
index 8a3653b..9ccebf5 100644
--- a/docs/设计方案.md
+++ b/docs/设计方案.md
@@ -334,6 +334,7 @@ flowchart TB
- 2026-10-26:**窗体改名**。23 个示例里 11 个的主窗体不叫 Form1(Login、frm_analog、frm_LED、frm_control、Main),学生的空白工程却只有 Form1,以前属性窗口改不了窗体名(项目 8 任务 2 的评分还要找 Main 窗体)。现在选中窗体可以改"名称":和 VS 一样,类名、构造函数、各文件里的 `new X()` 和 X 类型的变量都改(跳过字符串和注释),Designer.cs 里窗体的块头注释和 `this.Name` 也改,两个文件跟着改名;事件处理方法名(如 Form1_Load)和窗体标题不变。改名不进设计器的撤销历史。任务说明里提示先改名再双击窗体生成 Load 事件。
- 2026-10-26:代码用 **bun fmt** 格式化,格式写在 `bunfig.toml`(不写分号、单引号、一行最长 160,和原来的写法一致);`docs/教材摘要/` 是书上的摘录,不格式化。
- 2026-10-28:**兼容机房的 Chrome 94**。用 Chromium 94 实测,打开 http 地址时实验页报 `crypto.randomUUID is not a function`(这个函数只在 https 和 localhost 下有);还时好时坏地一片空白:Chrome 94 在 Promise 回调里调用 `import()`(Vite 的预加载就是这样)时,有时把 `import("./X.js")` 相对于网页地址解析,请求 `/X.js` 拿到首页 HTML。改法:打包目标改成 Chrome / Edge 94;`web/src/polyfills.ts` 补上 `crypto.randomUUID`、`structuredClone`(只复制普通数据)、`findLast` / `findLastIndex`;`vite.config.ts` 的小插件在 Vite 找完预加载依赖之后把代码块之间的 `import("./X.js")` 改成 `import("/assets/X.js")`,改过的代码块换新文件名(不用缓存里的旧文件);容器查询(Chrome 105)改成 ResizeObserver 量宽度加 class(实训台的三档布局、设计器窄时隐藏提示文字);`:has()`(Chrome 105)改成 class(只读和禁用的文本框灰底、没标题的分组框)。Monaco 自带的几条 `:has()`、`color-mix()` 样式在 Chrome 94 里不生效,只影响 Markdown 悬停提示里的复选框等细节。实测 Chrome 94 经 http 打开登录、实验页(设计器改属性、代码补全、运行程序)、数据库、班级管理、使用说明,各页面直接打开 5 遍都正常。
+- 2026-10-28:**运行中改了自动重新运行**。以前改了代码或设计器,运行区的窗体不变,要按 F5 才看得到。现在程序运行时(`WorkbenchPage.vue`)每次修改后等 1.5 秒没有新的改动,先用语言服务检查整个工程(和边写边检查同一个接口,不生成程序集):没有错误就重新编译运行;有错误就不重启,原来的程序接着运行,工具栏提示"代码有错误,改好后自动重新运行"。检查期间又改了、程序已经停了或换了工程,这次作废。只在运行中触发,停止后改动不会自动启动程序。
## 技术风险与待确认事项
diff --git a/web/src/pages/HelpPage.vue b/web/src/pages/HelpPage.vue
index 615f89d..cef4237 100644
--- a/web/src/pages/HelpPage.vue
+++ b/web/src/pages/HelpPage.vue
@@ -91,6 +91,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
4. 写代码、运行
- F5 或 ▶ 运行:编译并运行,窗体显示在右边的运行区;Shift+F5 或 ■ 停止 结束程序。
+ - 程序运行时改了代码或窗体,停手 1.5 秒后自动重新运行,不用再按 F5;代码有错误时原来的程序接着运行,改好后再自动重新运行。
- 编译错误显示在窗体下面,点它跳到出错的代码;常见错误会给出中文提示。
- 程序运行时出错(比如文本框是空的就转换成数字),窗体下面显示中文说明和出错的文件、行号,点它跳到那一行。
Console.WriteLine 的输出、程序保存的图片(项目 4 的拍照)也显示在窗体下面。
diff --git a/web/src/pages/WorkbenchPage.vue b/web/src/pages/WorkbenchPage.vue
index 6b620fc..ddd507b 100644
--- a/web/src/pages/WorkbenchPage.vue
+++ b/web/src/pages/WorkbenchPage.vue
@@ -22,6 +22,7 @@ import {
startupForm,
} from '../designer/templates'
import CodeEditor from '../editor/CodeEditor.vue'
+import { checkProject } from '../editor/language'
import { api } from '../api'
import { currentUser, logout } from '../auth'
import { deleteWork, loadWork, sampleWorkKey, saveWork } from '../editor/works'
@@ -136,6 +137,8 @@ onMounted(async () => {
/** 打开一个工程:服务器上有保存的作品就用作品,没有就用原始代码;discardWork 表示丢弃作品恢复原始代码 */
async function openProject(key: string, original: () => Promise, discardWork: boolean) {
await flushSave()
+ // 换了工程:之前安排的自动重新运行作废
+ clearTimeout(rerunTimer)
workKey.value = key
designerHistory.clear()
if (discardWork) await deleteWork(key).catch(() => {})
@@ -331,6 +334,7 @@ function updateFile(path: string, content: string) {
function filesChanged() {
modified.value = true
+ scheduleRerun()
// 老师试运行学生的提交时没有保存位置,不保存
if (workKey.value) scheduleSave()
}
@@ -501,11 +505,50 @@ function toggleRunArea(collapsed = !runCollapsed.value) {
function run() {
if (running.value) return
toggleRunArea(false)
+ start()
+}
+
+function start() {
+ clearTimeout(rerunTimer)
+ rerunBlocked.value = false
session.run(
files.value.map((f) => ({ ...f })),
props.assignmentId ? assignment.value?.sampleId : sampleId.value,
)
}
+
+// ---------- 运行中改了代码或窗体:自动重新运行,不用再按 F5 ----------
+/** 停止修改多久后重新运行 */
+const RERUN_DELAY_MS = 1500
+let rerunTimer: ReturnType | undefined
+/** 每次修改都递增,用来判断检查期间是否又改了 */
+let editCount = 0
+/** 改出了编译错误:原来的程序继续运行,改好后再重新运行 */
+const rerunBlocked = ref(false)
+
+function scheduleRerun() {
+ editCount++
+ if (!running.value) return
+ clearTimeout(rerunTimer)
+ rerunTimer = setTimeout(() => void rerun(), RERUN_DELAY_MS)
+}
+
+async function rerun() {
+ // 上一次还在编译:等它运行起来再说
+ if (state.status === 'compiling') {
+ rerunTimer = setTimeout(() => void rerun(), RERUN_DELAY_MS)
+ return
+ }
+ if (state.status !== 'running') return
+ const edit = editCount
+ // 先检查一遍:有错误就不重启,免得正在运行的程序被换成一堆报错(错误照常用波浪线标在代码里)
+ const diagnostics = await checkProject()
+ // 检查期间又改了(会再安排一次)、程序已经停了,或者检查失败:这次不重新运行
+ if (edit !== editCount || state.status !== 'running' || !diagnostics) return
+ rerunBlocked.value = diagnostics.some((d) => d.severity === 'error')
+ if (!rerunBlocked.value) start()
+}
+onBeforeUnmount(() => clearTimeout(rerunTimer))
@@ -561,6 +604,7 @@ function run() {
{{ SAVE_TEXT[saveState] }}
{{ STATUS_TEXT[state.status] }}
+ 代码有错误,改好后自动重新运行
编译 {{ state.metrics.compileMs }} ms · 启动 {{ state.metrics.startMs }} ms · 最近一次操作往返 {{ state.metrics.lastRoundTripMs }} ms
@@ -746,6 +790,12 @@ function run() {
color: #dc2626;
}
+.rerun-blocked {
+ color: #b45309;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
.user {
font-size: 13px;
white-space: nowrap;