项目 7 情景控制:虚拟时钟、项目资源、Windows Media Player、TrackBar
- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把 DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤 - 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选, 平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources 指向素材库;未添加资源时给中文提示 - Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音, 实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob) - TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器" - 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表) - 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行 - 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9af26c2d6
commit
eedf1aa2fb
66 files changed
+3538
-43
No files matched your search
@@ -31,6 +31,8 @@ const props = defineProps<{
|
||||
codeText: string
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
/** 项目资源(图片属性的选项) */
|
||||
resources?: { names: string[]; prefix: string }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -42,6 +44,8 @@ const emit = defineEmits<{
|
||||
openDesignerFile: []
|
||||
undo: []
|
||||
redo: []
|
||||
/** 属性窗口里要添加项目资源 */
|
||||
addResource: []
|
||||
}>()
|
||||
|
||||
/** Ctrl+Z 撤销,Ctrl+Y / Ctrl+Shift+Z 重做;在属性窗口的输入框里打字时不拦截(输入框有自己的撤销) */
|
||||
@@ -492,11 +496,13 @@ const noop = () => {}
|
||||
class="properties"
|
||||
:doc="doc"
|
||||
:selected="selected"
|
||||
:resources="resources"
|
||||
@set-prop="onSetProp"
|
||||
@set-items="onSetItems"
|
||||
@rename="onRename"
|
||||
@open-event="openEvent"
|
||||
@clear-event="clearEvent"
|
||||
@add-resource="emit('addResource')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,12 +4,15 @@ import { computed, ref, watch } from 'vue'
|
||||
import { type ControlDef, FORM_DEF, type PropDef, controlDef } from './catalog'
|
||||
import type { DesignerDocument } from './document'
|
||||
import { checkName, getEventHandler, getItems, getPropText, typeOf } from './edit'
|
||||
import { resourceNameOf } from './resources'
|
||||
import { NAMED_COLORS, hexToRgb, parseValue, rgb, type Value } from './values'
|
||||
|
||||
const props = defineProps<{
|
||||
doc: DesignerDocument
|
||||
/** '' 表示窗体本身 */
|
||||
selected: string
|
||||
/** 项目资源(图片属性从这里选)和引用资源的前缀 global::命名空间.Properties.Resources. */
|
||||
resources?: { names: string[]; prefix: string }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -19,6 +22,8 @@ const emit = defineEmits<{
|
||||
/** 双击事件或点"生成":绑定(必要时生成)事件处理方法并跳到代码 */
|
||||
openEvent: [target: string, event: string, handler?: string]
|
||||
clearEvent: [target: string, event: string]
|
||||
/** 图片属性里选了"添加资源…":打开项目资源对话框 */
|
||||
addResource: []
|
||||
}>()
|
||||
|
||||
const tab = ref<'props' | 'events'>('props')
|
||||
@@ -37,6 +42,22 @@ function set(prop: PropDef, value: Value) {
|
||||
emit('setProp', props.selected, prop.name, isDefault ? null : value)
|
||||
}
|
||||
|
||||
/** 图片属性:项目资源里的名字,没有设置时为空 */
|
||||
function imageOf(prop: PropDef) {
|
||||
const text = getPropText(props.doc, props.selected, prop.name)
|
||||
return text === undefined ? '' : resourceNameOf(text) ?? text
|
||||
}
|
||||
|
||||
function setImage(prop: PropDef, choice: string, select: HTMLSelectElement) {
|
||||
if (choice === '+') {
|
||||
select.value = imageOf(prop)
|
||||
emit('addResource')
|
||||
return
|
||||
}
|
||||
if (choice === '') reset(prop)
|
||||
else emit('setProp', props.selected, prop.name, { kind: 'raw', text: `${props.resources?.prefix ?? ''}${choice}` })
|
||||
}
|
||||
|
||||
function reset(prop: PropDef) {
|
||||
emit('setProp', props.selected, prop.name, null)
|
||||
}
|
||||
@@ -152,7 +173,7 @@ function renameHandler(event: string, value: string) {
|
||||
<div v-for="prop in def.props" :key="prop.name" class="row">
|
||||
<label :title="`${prop.name}:${prop.description}`">{{ prop.label }}</label>
|
||||
<div class="editor">
|
||||
<code v-if="raw(prop) !== undefined" class="raw" :title="raw(prop)">{{ raw(prop) }}</code>
|
||||
<code v-if="prop.type !== 'image' && raw(prop) !== undefined" class="raw" :title="raw(prop)">{{ raw(prop) }}</code>
|
||||
|
||||
<input v-else-if="prop.type === 'string'" :value="str(prop)" @change="set(prop, { kind: 'string', value: ($event.target as HTMLInputElement).value })" />
|
||||
|
||||
@@ -213,6 +234,14 @@ function renameHandler(event: string, value: string) {
|
||||
<textarea v-else-if="prop.type === 'items'" rows="4" :value="itemsText" placeholder="每行一项"
|
||||
@change="setItemsFromText(($event.target as HTMLTextAreaElement).value)" />
|
||||
|
||||
<select v-else-if="prop.type === 'image'" :value="imageOf(prop)"
|
||||
@change="setImage(prop, ($event.target as HTMLSelectElement).value, $event.target as HTMLSelectElement)">
|
||||
<option value="">(无)</option>
|
||||
<option v-for="name in resources?.names ?? []" :key="name" :value="name">Resources.{{ name }}</option>
|
||||
<option v-if="imageOf(prop) && !resources?.names.includes(imageOf(prop))" :value="imageOf(prop)">{{ imageOf(prop) }}(不在项目资源里)</option>
|
||||
<option value="+">添加资源…</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
// 项目资源(相当于 VS 的"项目属性 → 资源 → 添加资源"):从平台素材库里勾选图片和声音,
|
||||
// 代码里用 Properties.Resources.名字,设计器里图片框的"图片"属性也可以选它们。
|
||||
import { NButton, NCheckbox, NModal } from 'naive-ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { gallery, loadGallery } from './resources'
|
||||
|
||||
const props = defineProps<{ show: boolean; selected: string[] }>()
|
||||
const emit = defineEmits<{ 'update:show': [show: boolean]; save: [names: string[]] }>()
|
||||
|
||||
const chosen = ref(new Set<string>())
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) return
|
||||
void loadGallery()
|
||||
chosen.value = new Set(props.selected)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function toggle(name: string, on: boolean) {
|
||||
const next = new Set(chosen.value)
|
||||
if (on) next.add(name)
|
||||
else next.delete(name)
|
||||
chosen.value = next
|
||||
}
|
||||
|
||||
let audio: HTMLAudioElement | undefined
|
||||
function listen(file: string) {
|
||||
audio?.pause()
|
||||
audio = new Audio(`/resources/${file}`)
|
||||
void audio.play()
|
||||
}
|
||||
|
||||
function save() {
|
||||
// 按素材库的顺序保存,生成的代码稳定
|
||||
emit('save', gallery.value.map((g) => g.name).filter((name) => chosen.value.has(name)))
|
||||
emit('update:show', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="show" preset="card" title="项目资源" style="width: 560px; max-width: calc(100vw - 32px)" @update:show="emit('update:show', $event)">
|
||||
<p class="note">勾选要加入项目的图片和声音(平台素材库,名字和教材一致)。代码里写 <code>Properties.Resources.名字</code>,图片框的"图片"属性也可以直接选。</p>
|
||||
<div class="items">
|
||||
<label v-for="item in gallery" :key="item.name" class="item" :class="{ on: chosen.has(item.name) }">
|
||||
<NCheckbox :checked="chosen.has(item.name)" @update:checked="toggle(item.name, $event)" />
|
||||
<img v-if="item.kind === 'image'" :src="`/resources/${item.file}`" alt="" class="thumb" />
|
||||
<NButton v-else size="tiny" class="audio" @click.prevent="listen(item.file)">▶ 试听</NButton>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</label>
|
||||
<div v-if="gallery.length === 0" class="note">正在加载素材库…</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="buttons">
|
||||
<NButton type="primary" @click="save">确定</NButton>
|
||||
<NButton @click="emit('update:show', false)">取消</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.note {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 64px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item.on {
|
||||
border-color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.item > :first-child {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.thumb,
|
||||
.audio {
|
||||
grid-row: span 2;
|
||||
max-width: 64px;
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.audio {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
// 只列出仿真运行时已经实现的控件和属性;默认值与 WinForms 一致(等于默认值的属性不写进 Designer.cs)。
|
||||
import type { Value } from './values'
|
||||
|
||||
export type PropType = 'string' | 'multiline' | 'bool' | 'int' | 'char' | 'point' | 'size' | 'color' | 'font' | 'enum' | 'items'
|
||||
export type PropType = 'string' | 'multiline' | 'bool' | 'int' | 'char' | 'point' | 'size' | 'color' | 'font' | 'enum' | 'items' | 'image'
|
||||
|
||||
export interface PropDef {
|
||||
/** WinForms 的属性名,写进 Designer.cs,代码里也用它 */
|
||||
@@ -187,6 +187,7 @@ export const CONTROLS: ControlDef[] = [
|
||||
defaultEvent: 'Click',
|
||||
props: [
|
||||
...COMMON_PROPS.filter((p) => p.name !== 'Text' && p.name !== 'ForeColor' && p.name !== 'Font'),
|
||||
{ name: 'Image', label: '图片', description: '显示的图片:从项目资源里选(Properties.Resources)', type: 'image', category: '外观' },
|
||||
{
|
||||
name: 'SizeMode', label: '显示方式', description: '图片在图片框里怎样显示', type: 'enum', enumType: `${FORMS}.PictureBoxSizeMode`,
|
||||
options: ['Normal', 'StretchImage', 'AutoSize', 'CenterImage', 'Zoom'],
|
||||
@@ -254,6 +255,38 @@ export const CONTROLS: ControlDef[] = [
|
||||
events: [...COMMON_EVENTS.filter((e) => e.name !== 'TextChanged'), { name: 'ValueChanged', label: '值改变', ...EVENT }],
|
||||
initial: () => [['Size', size(200, 21)]],
|
||||
},
|
||||
{
|
||||
type: `${FORMS}.TrackBar`,
|
||||
label: '滑动条',
|
||||
group: '常用控件',
|
||||
namePrefix: 'trackBar',
|
||||
defaultEvent: 'Scroll',
|
||||
props: [
|
||||
...COMMON_PROPS.filter((p) => p.name !== 'Text' && p.name !== 'ForeColor' && p.name !== 'Font'),
|
||||
{ name: 'Minimum', label: '最小值', description: '滑块在最小端时的值', type: 'int', default: int(0), category: '行为' },
|
||||
{ name: 'Maximum', label: '最大值', description: '滑块在最大端时的值', type: 'int', default: int(10), category: '行为' },
|
||||
{ name: 'Value', label: '值', description: '滑块现在的值', type: 'int', default: int(0), category: '行为' },
|
||||
{ name: 'TickFrequency', label: '刻度间隔', description: '每隔多少画一个刻度', type: 'int', default: int(1), category: '外观' },
|
||||
{ name: 'LargeChange', label: '大步长', description: '点击轨道时每次改变多少', type: 'int', default: int(5), category: '行为' },
|
||||
{ name: 'SmallChange', label: '小步长', description: '按方向键时每次改变多少', type: 'int', default: int(1), category: '行为' },
|
||||
{
|
||||
name: 'TickStyle', label: '刻度位置', description: '刻度画在哪一边', type: 'enum', enumType: `${FORMS}.TickStyle`,
|
||||
options: ['None', 'TopLeft', 'BottomRight', 'Both'], optionLabels: { None: '不画刻度', TopLeft: '上方(左边)', BottomRight: '下方(右边)', Both: '两边' },
|
||||
default: enumValue(`${FORMS}.TickStyle`, 'BottomRight'), category: '外观',
|
||||
},
|
||||
{
|
||||
name: 'Orientation', label: '方向', description: '横向或竖向', type: 'enum', enumType: `${FORMS}.Orientation`,
|
||||
options: ['Horizontal', 'Vertical'], optionLabels: { Horizontal: '横向', Vertical: '竖向' },
|
||||
default: enumValue(`${FORMS}.Orientation`, 'Horizontal'), category: '外观',
|
||||
},
|
||||
],
|
||||
events: [
|
||||
...COMMON_EVENTS.filter((e) => e.name !== 'TextChanged'),
|
||||
{ name: 'Scroll', label: '拖动滑块', ...EVENT },
|
||||
{ name: 'ValueChanged', label: '值改变', ...EVENT },
|
||||
],
|
||||
initial: () => [['Size', size(104, 45)]],
|
||||
},
|
||||
{
|
||||
type: `${FORMS}.CheckBox`,
|
||||
label: '复选框',
|
||||
@@ -312,6 +345,16 @@ export const CONTROLS: ControlDef[] = [
|
||||
events: [{ name: 'Tick', label: '计时到', ...EVENT }],
|
||||
initial: () => [],
|
||||
},
|
||||
{
|
||||
type: 'AxWMPLib.AxWindowsMediaPlayer',
|
||||
label: '媒体播放器',
|
||||
group: '物联网设备',
|
||||
namePrefix: 'axWindowsMediaPlayer',
|
||||
defaultEvent: 'Enter',
|
||||
props: COMMON_PROPS.filter((p) => ['Location', 'Size', 'Enabled', 'Visible', 'TabIndex'].includes(p.name)),
|
||||
events: [{ name: 'Enter', label: '获得焦点', ...EVENT }],
|
||||
initial: () => [['Enabled', bool(true)], ['Size', size(245, 240)]],
|
||||
},
|
||||
{
|
||||
type: 'WinFormsDeviceControlLibrary.LED',
|
||||
label: 'LED 显示屏',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ControlNode } from '@iot/shared'
|
||||
import { controlDef } from './catalog'
|
||||
import type { DesignerDocument } from './document'
|
||||
import { childrenOf, findBlock, getItems, listControls } from './edit'
|
||||
import { resourceNameOf, resourceUrl } from './resources'
|
||||
import { parseValue, type Value } from './values'
|
||||
|
||||
/** 窗体在预览树里的 id;控件的 id 就是控件名 */
|
||||
@@ -11,6 +12,7 @@ export const FORM_ID = '__form'
|
||||
|
||||
/** 运行时控件树里的类型名:System.Windows.Forms.Button → Button,教材控件库保留完整类名 */
|
||||
function clientType(type: string) {
|
||||
if (type === 'AxWMPLib.AxWindowsMediaPlayer') return 'AxWindowsMediaPlayer'
|
||||
return type.startsWith('System.Windows.Forms.') ? type.slice('System.Windows.Forms.'.length) : type
|
||||
}
|
||||
|
||||
@@ -43,6 +45,12 @@ function propsOf(doc: DesignerDocument, target: string): Record<string, unknown>
|
||||
const props: Record<string, unknown> = {}
|
||||
for (const stmt of findBlock(doc, target)?.stmts ?? []) {
|
||||
if (stmt.kind !== 'prop' || stmt.path.includes('.')) continue
|
||||
// 项目资源里的图片:预览时直接显示素材文件
|
||||
const resource = resourceNameOf(stmt.value)
|
||||
if (resource) {
|
||||
props[stmt.path] = { key: '', url: resourceUrl(resource), width: 0, height: 0 }
|
||||
continue
|
||||
}
|
||||
const value = runtimeValue(parseValue(stmt.value))
|
||||
if (value !== undefined) props[stmt.path] = value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// 项目资源(Properties.Resources):学生从平台素材库里选图片和声音,平台生成 Properties/Resources.Designer.cs。
|
||||
// 生成的类和 VS 的强类型资源类用法一样(Properties.Resources.switch_on 是 Bitmap,声音是 byte[]),
|
||||
// 取值时调用运行器的素材库。这个文件由平台生成,不在标签页里显示。
|
||||
import { ref } from 'vue'
|
||||
import type { SourceFile } from '@iot/shared'
|
||||
|
||||
export interface GalleryItem {
|
||||
/** 资源名,也是 Properties.Resources 的属性名,和教材一致(如 switch_on) */
|
||||
name: string
|
||||
file: string
|
||||
/** 中文说明 */
|
||||
label: string
|
||||
kind: 'image' | 'audio'
|
||||
}
|
||||
|
||||
export const RESOURCES_FILE = 'Properties/Resources.Designer.cs'
|
||||
|
||||
/** 素材清单(服务器上 /resources/index.json),第一次用到时加载 */
|
||||
export const gallery = ref<GalleryItem[]>([])
|
||||
let loading: Promise<void> | undefined
|
||||
|
||||
export function loadGallery() {
|
||||
loading ??= fetch('/resources/index.json')
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((items: GalleryItem[]) => {
|
||||
gallery.value = items
|
||||
})
|
||||
.catch(() => {
|
||||
loading = undefined
|
||||
})
|
||||
return loading
|
||||
}
|
||||
|
||||
/** 素材文件的网址(设计器预览、资源对话框里显示) */
|
||||
export function resourceUrl(name: string) {
|
||||
const item = gallery.value.find((g) => g.name === name)
|
||||
return `/resources/${item?.file ?? `${name}.png`}`
|
||||
}
|
||||
|
||||
/** 工程里已添加的资源名(从生成的 Resources.Designer.cs 里读出) */
|
||||
export function projectResources(files: SourceFile[]): string[] {
|
||||
const text = files.find((f) => f.path === RESOURCES_FILE)?.content ?? ''
|
||||
return [...text.matchAll(/internal static \S+ ([\p{L}\p{N}_]+) \{/gu)].map((m) => m[1])
|
||||
}
|
||||
|
||||
/** 设计器里引用资源的写法:global::命名空间.Properties.Resources.名字 */
|
||||
export const resourcePrefix = (namespace: string) => `global::${namespace}.Properties.Resources.`
|
||||
|
||||
/** 从属性值里认出资源名:global::X.Properties.Resources.switch_off → switch_off */
|
||||
export function resourceNameOf(text: string) {
|
||||
return /\.Properties\.Resources\.([\p{L}\p{N}_]+)$/u.exec(text.trim())?.[1]
|
||||
}
|
||||
|
||||
/** 生成 Properties/Resources.Designer.cs;没有资源时返回 null(删掉这个文件) */
|
||||
export function resourcesFile(namespace: string, names: string[]): SourceFile | null {
|
||||
const items = names.map((name) => gallery.value.find((g) => g.name === name)).filter((g): g is GalleryItem => g !== undefined)
|
||||
if (items.length === 0) return null
|
||||
const members = items.map((item) =>
|
||||
item.kind === 'image'
|
||||
? ` /// <summary>${item.label}</summary>\n internal static System.Drawing.Bitmap ${item.name} {\n get { return global::Sim.Host.ProjectResources.Image("${item.file}"); }\n }`
|
||||
: ` /// <summary>${item.label}</summary>\n internal static byte[] ${item.name} {\n get { return global::Sim.Host.ProjectResources.Bytes("${item.file}"); }\n }`,
|
||||
)
|
||||
const content = `//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ${namespace}.Properties {
|
||||
|
||||
/// <summary>
|
||||
/// 项目资源:Properties.Resources.资源名
|
||||
/// </summary>
|
||||
internal class Resources {
|
||||
|
||||
${members.join('\n\n')}
|
||||
}
|
||||
}
|
||||
`
|
||||
return { path: RESOURCES_FILE, content }
|
||||
}
|
||||
@@ -31,7 +31,7 @@ test('新窗体的 Designer.cs 能被设计器打开,添加控件后格式正
|
||||
/** 读取 samples/ 下的示例工程 */
|
||||
async function sample(id: string) {
|
||||
const dir = `${import.meta.dir}/../../../samples/${id}`
|
||||
const paths = await Array.fromAsync(new Bun.Glob('*.cs').scan({ cwd: dir }))
|
||||
const paths = await Array.fromAsync(new Bun.Glob('**/*.cs').scan({ cwd: dir }))
|
||||
return Promise.all(paths.sort().map(async (path) => ({ path, content: await Bun.file(`${dir}/${path}`).text() })))
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ test('作业起点"界面已做好":多个窗体都保留,其他类(Public
|
||||
expect(files.find((f) => f.path === 'AddUser.cs')!.content).toContain('public partial class AddUser : Form')
|
||||
})
|
||||
|
||||
test('作业起点"界面已做好":图片引用了项目资源时带上资源类,资源类不当作窗体', async () => {
|
||||
const files = assignmentTemplate(await sample('p7-t1-智慧教室'), 'ui')
|
||||
expect(files.map((f) => f.path).sort()).toEqual(['Program.cs', 'Properties/Resources.Designer.cs', '教室情景模式.Designer.cs', '教室情景模式.cs'])
|
||||
expect(files.find((f) => f.path === '教室情景模式.Designer.cs')!.content).toContain('global::教室情景模式.Properties.Resources.switch_off')
|
||||
})
|
||||
|
||||
test('作业起点"空白工程":Program.cs 启动空的 Form1', async () => {
|
||||
const files = assignmentTemplate(await sample('p2-t2-模拟量采集'), 'blank')
|
||||
expect(files.map((f) => f.path).sort()).toEqual(['Form1.Designer.cs', 'Form1.cs', 'Program.cs'])
|
||||
|
||||
@@ -123,7 +123,10 @@ export function assignmentTemplate(files: SourceFile[], mode: StartMode): Source
|
||||
return [...(programText ? [{ path: 'Program.cs', content: programText }] : []), ...newFormFiles(namespace, 'Form1')]
|
||||
}
|
||||
const result: SourceFile[] = program ? [{ ...program }] : []
|
||||
for (const designer of files.filter((f) => f.path.endsWith('.Designer.cs'))) {
|
||||
// 界面里的图片引用了项目资源,资源类一起带上(空白工程由学生自己按书上的步骤添加)
|
||||
const resources = files.find((f) => f.path.startsWith('Properties/') && f.path.endsWith('.Designer.cs'))
|
||||
if (resources) result.push({ ...resources })
|
||||
for (const designer of files.filter((f) => isFormDesigner(f.path))) {
|
||||
const doc = parseDesigner(designer.content)
|
||||
for (const block of doc.blocks) block.stmts = block.stmts.filter((s) => s.kind !== 'event')
|
||||
const [code] = newFormFiles(namespace, doc.className)
|
||||
@@ -131,3 +134,6 @@ export function assignmentTemplate(files: SourceFile[], mode: StartMode): Source
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** 窗体的 Designer.cs(Properties/ 下平台生成的 Resources.Designer.cs 不是窗体) */
|
||||
export const isFormDesigner = (path: string) => path.endsWith('.Designer.cs') && !path.startsWith('Properties/')
|
||||
Reference in new issue
Block a user