导出 VS 工程:实验页工具栏的"导出 EXE"改成下拉按钮"导出 ▾",里面是"导出 EXE"和"导出 VS 工程"。导出的 zip 是教材那样的 VS 2012 窗体应用工程(.sln + .csproj,.NET Framework 4.5),在机房电脑上解压、双击 .sln 用 VS 打开接着改,按 F5 运行。导出前照机房环境编译一遍要放进 zip 的代码:C# 5(VS 2012 的编译器)、真机版设备类库,有错不导出;新版 C# 写法报 CS8026,按功能名给出改法(字符串插值改 string.Format、?. 改先判断 null、=> 改大括号等 12 种)。导出 EXE 仍按最新 C#。平台的 Properties/Resources.Designer.cs 换成 VS 生成的写法和 Resources.resx,图片放 Resources\;代码里按文件名用的素材(ring.mp3)复制到输出目录,运行时在 bin\Debug\Resources\;每个窗体带 .resx(媒体播放器 OcxState 用教材默认状态),SubType、DependentUpon 和 VS 一样;补上 AssemblyInfo、Settings、App.config。教材 dll 全放 lib\,只引用代码直接用到的(和教材工程一样),只带这些编译不过时引用全部;用到书上的 ledHelper.Close() 时带上 LedHelperCompat.cs。文本文件用带 BOM 的 UTF-8 和 CRLF(VS 2012 打开不带 BOM 的文件会把中文当 GBK);zip 文件名用 GBK,另附 Info-ZIP Unicode 文件名字段(0x7075),中文 Windows 自带解压和 7-Zip 都显示正确(ZipWriter.cs)。23 个示例都能导出,导出的工程在 mono 的 msbuild 里全部编译通过、输出目录和教材 bin\Debug 一样、程序能启动;VS 2012 还要在机房实测。接口 POST /api/export-project,代码有错时 422。export-exe.ts 改名 export.ts(服务器和网页各一个)。使用说明、设计方案、README、部署说明同步
Deploy / deploy (push) Failing after 1m8s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-03 12:42:13 +08:00
1 parent 4c3ce31fe1
commit 2aa267839f
17 files changed
+1209 -80

No files matched your search

@@ -1,18 +1,23 @@
// 导出 EXE:服务器照教材工程(真机版设备类库)编译,下载一个 exe,在机房电脑上双击就能控制真的实训台。
// 导出:服务器照教材工程(真机版设备类库)编译,下载到机房电脑上用。
// EXE:一个 exe,双击就能控制真的实训台;VS 工程:教材那样的 VS 2012 工程(zip),用 VS 打开接着改。
import type { Diagnostic, SourceFile } from '@iot/shared'
import { apiResponse, ApiError } from '../api'
import { projectNamespace } from '../designer/templates'
export type ExportKind = 'exe' | 'project'
const EXT: Record<ExportKind, string> = { exe: 'exe', project: 'zip' }
/** 下载成功返回 null;代码有错误返回报错(照真机版类库编译,和 VS 里一样);其他问题抛出 ApiError */
export async function downloadExe(files: SourceFile[]): Promise<Diagnostic[] | null> {
export async function downloadExport(kind: ExportKind, files: SourceFile[]): Promise<Diagnostic[] | null> {
let response: Response
try {
response = await apiResponse('POST', '/api/export-exe', { files, name: projectNamespace(files) })
response = await apiResponse('POST', `/api/export-${kind}`, { files, name: projectNamespace(files) })
} catch (error) {
if (error instanceof ApiError && error.status === 422 && Array.isArray(error.data.diagnostics)) return error.data.diagnostics as Diagnostic[]
throw error
}
const name = fileName(response.headers.get('Content-Disposition')) ?? `${projectNamespace(files)}.exe`
const name = fileName(response.headers.get('Content-Disposition')) ?? `${projectNamespace(files)}.${EXT[kind]}`
const url = URL.createObjectURL(await response.blob())
const link = document.createElement('a')
link.href = url
+10 -2
View File
@@ -168,16 +168,24 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
<figcaption>数据库页面:左边是你的库和表(点表名编辑数据),右边写 SQL、按 F5 执行,下面是结果</figcaption>
</figure>
<h3>8. 导出 EXE,在机房电脑上运行</h3>
<h3>8. 导出 EXE 或 VS 工程,在机房电脑上运行</h3>
<ul>
<li>
点工具栏的<b>导出 EXE</b>,下载一个
点工具栏的<b>导出</b>→<b>导出 EXE</b>,下载一个
exe(名字是工程名,比如“教室情景模式.exe”)。拷到接着实训台的机房电脑上双击运行,选对串口、点打开,就能控制<b>真的</b>设备。
</li>
<li>导出时照机房电脑上的设备类库编译,和在 VS 里编译一样。代码有错误时弹出报错,点它跳到出错的地方。</li>
<li>设备类库已经装在 exe 里,不用另外拷 dll。项目 7 寝室的音乐第一次运行时会放到 exe 旁边的 Resources 文件夹。</li>
<li>项目 6、8 的程序连的是机房电脑上真的 SQL Server,那台电脑上要有书上建的库。</li>
<li>第一次运行时 Windows 可能提示“Windows 已保护你的电脑”:点<b>更多信息</b>→<b>仍要运行</b>。</li>
<li>
想在机房的 VS 里接着改,点<b>导出</b>→<b>导出 VS 工程</b>,下载一个 zip。解压后双击里面的 .sln 文件用 VS 打开,按 F5
运行。工程和书上的一样:设备类库在 lib 文件夹里,图片、声音在 Resources 文件夹里。
</li>
<li>
机房的 VS 2012 只认 C# 5 的写法。导出工程时照它检查,用了新写法(比如
<code>$"温度{t}"</code>)会弹出报错和改法,改好再导出。
</li>
</ul>
</section>
+42 -10
View File
@@ -4,7 +4,21 @@
// 老师运行学生提交的程序(/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, type SelectGroupOption } from 'naive-ui'
import {
NButton,
NDropdown,
NInput,
NModal,
NPopconfirm,
NPopover,
NSelect,
NTab,
NTabs,
NTag,
useMessage,
type DropdownOption,
type SelectGroupOption,
} from 'naive-ui'
import type { Assignment, Diagnostic, Review, SourceFile, Submission } from '@iot/shared'
import FormDesigner from '../designer/FormDesigner.vue'
import { createDesignerHistory } from '../designer/history'
@@ -25,7 +39,7 @@ import CodeEditor from '../editor/CodeEditor.vue'
import { checkProject } from '../editor/language'
import { api } from '../api'
import { currentUser, logout } from '../auth'
import { downloadExe } from '../editor/export-exe'
import { downloadExport, type ExportKind } from '../editor/export'
import { deleteWork, loadWork, sampleWorkKey, saveWork } from '../editor/works'
import RunArea from '../run/RunArea.vue'
import { createRunSession } from '../run/session'
@@ -479,22 +493,37 @@ async function showDiagnostic(file: string, line: number, column: number) {
editorRef.value?.reveal(file, line, column)
}
// ---------- 导出 EXE:照真机版类库编译,在机房电脑上双击运行 ----------
// ---------- 导出:照真机版类库编译,EXE 在机房电脑上双击运行,VS 工程用机房的 VS 2012 打开 ----------
const exporting = ref(false)
/** 导出时的编译报错(照真机版类库编译,和平台上运行时可能不同) */
const exportErrors = ref<Diagnostic[]>([])
const exportErrorsOpen = ref(false)
const exportKind = ref<ExportKind>('exe')
async function exportExe() {
const EXPORT_OPTIONS: DropdownOption[] = [
{ key: 'exe', label: '导出 EXE', props: { title: '编译成 Windows 程序(.exe),在机房电脑上双击运行,控制真的实训台' } },
{ key: 'project', label: '导出 VS 工程', props: { title: '打包成 VS 2012 工程(.zip),在机房电脑上解压后用 VS 打开,接着改代码、按 F5 运行' } },
]
const EXPORT_DONE: Record<ExportKind, string> = {
exe: '已导出,在机房电脑上双击运行(要接真的实训台)',
project: '已导出,解压后双击 .sln 文件用 VS 打开,按 F5 运行(要接真的实训台)',
}
async function exportProgram(kind: ExportKind) {
if (exporting.value) return
exporting.value = true
exportKind.value = kind
try {
const errors = await downloadExe(files.value.map((f) => ({ ...f })))
const errors = await downloadExport(
kind,
files.value.map((f) => ({ ...f })),
)
if (errors) {
exportErrors.value = errors
exportErrorsOpen.value = true
} else {
message.success('已导出,在机房电脑上双击运行(要接真的实训台)')
message.success(EXPORT_DONE[kind])
}
} catch (error) {
message.error(error instanceof Error ? error.message : String(error))
@@ -630,9 +659,9 @@ onBeforeUnmount(() => clearTimeout(rerunTimer))
<NButton v-if="taskPanel && !viewing" size="small" title="任务说明、自动检查哪些功能;可以自己先检查一遍(不计分)" @click="taskOpen = !taskOpen">
任务
</NButton>
<NButton size="small" :loading="exporting" title="编译成 Windows 程序(.exe),在机房电脑上双击运行,控制真的实训台" @click="exportExe">
导出 EXE
</NButton>
<NDropdown trigger="click" :options="EXPORT_OPTIONS" :disabled="exporting" @select="(key: ExportKind) => exportProgram(key)">
<NButton size="small" :loading="exporting" title="导出到机房电脑上用:EXE 双击运行,VS 工程用 VS 打开">导出 ▾</NButton>
</NDropdown>
<NPopconfirm v-if="modified && !viewing" positive-text="恢复" negative-text="取消" @positive-click="restoreOriginal">
<template #trigger>
<NButton quaternary size="small">{{ assignmentId ? '恢复到作业起点' : '恢复原始代码' }}</NButton>
@@ -657,7 +686,10 @@ onBeforeUnmount(() => clearTimeout(rerunTimer))
</header>
<NModal v-model:show="exportErrorsOpen" preset="card" title="代码有错误,不能导出" style="width: 640px; max-width: calc(100vw - 32px)">
<p class="export-errors-note">导出时照机房电脑上的设备类库编译,和在 VS 里编译一样。点击报错跳到出错的代码。</p>
<p v-if="exportKind === 'project'" class="export-errors-note">
导出时照机房电脑上的 VS 2012 编译(只支持 C# 5,用真机版设备类库)。点击报错跳到出错的代码。
</p>
<p v-else class="export-errors-note">导出时照机房电脑上的设备类库编译,和在 VS 里编译一样。点击报错跳到出错的代码。</p>
<div v-for="(d, i) in exportErrors.slice(0, 20)" :key="i" class="export-error" @click="jumpToExportError(d)">
{{ d.file }}({{ d.line }},{{ d.column }}) {{ d.code }}:{{ d.message }}
<div v-if="d.hint" class="hint">提示:{{ d.hint }}</div>