- 编译时把 Thread 的 Suspend / Resume / Abort、ThreadState、Sleep、new Thread 改写成平台的 协作式实现:被挂起或终止的线程在 Sleep、后台改控件、调设备类库、弹提示框时停下或结束; ThreadStateException 等行为与 .NET Framework 一致;编辑器不再提示"已过时" - ListBox:列表项、单选、SelectedIndexChanged 先于 Click、删除选中项后取消选中; ComboBox 与 ListBox 共用 ListControl 和列表项集合;设计器工具箱加"列表框" - 评分规则的"包含"对列表框检查所有列表项 - 示例:项目 5 任务 2(烟雾,线程)、任务 3(推送灾情,二维数组、两个窗体、ListBox), 参考答案修正书上直接 Abort 挂起线程、烟雾记成"发现火情"的问题 - 一直不换行的 Console 输出攒满 4096 字符先发出,避免占满内存 - 测试:旧版线程 API、ListBox 事件顺序、两个示例的端到端和评分测试 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
6.2 KiB
Vue
174 lines
6.2 KiB
Vue
<script setup lang="ts">
|
||
// 按控件类型渲染一个 WinForms 控件及其子控件。
|
||
// 用户操作只通过 act() 上报,由运行器按真实 WinForms 的顺序触发事件。
|
||
import { computed, ref } from 'vue'
|
||
import type { ControlNode, UserAction } from '@iot/shared'
|
||
import { boxCss, displayText } from './format'
|
||
import LedControl from './LedControl.vue'
|
||
|
||
const props = defineProps<{
|
||
node: ControlNode
|
||
act: (id: string, action: UserAction) => void
|
||
}>()
|
||
|
||
const p = computed(() => props.node.props)
|
||
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染(后渲染的 DOM 元素在上面)
|
||
const childrenTopLast = computed(() => [...props.node.children].reverse())
|
||
const enabled = computed(() => p.value.Enabled !== false)
|
||
const style = computed(() => boxCss(p.value, props.node.type === 'Label' && p.value.AutoSize === true))
|
||
|
||
function act(action: UserAction) {
|
||
if (enabled.value) props.act(props.node.id, action)
|
||
}
|
||
|
||
// TextBox
|
||
const passwordChar = computed(() => {
|
||
if (p.value.UseSystemPasswordChar) return '●'
|
||
const ch = p.value.PasswordChar as string | undefined
|
||
return ch && ch !== '\0' ? ch : ''
|
||
})
|
||
|
||
// ComboBox
|
||
const dropDownOpen = ref(false)
|
||
const items = computed(() => (p.value.Items as string[] | undefined) ?? [])
|
||
const editable = computed(() => p.value.DropDownStyle !== 'DropDownList')
|
||
|
||
function openDropDown() {
|
||
if (!enabled.value) return
|
||
if (dropDownOpen.value) {
|
||
dropDownOpen.value = false
|
||
return
|
||
}
|
||
// 先触发 Click(教材在 Click 里填充串口列表),列表随后按最新的 Items 显示
|
||
act({ action: 'dropDown' })
|
||
dropDownOpen.value = true
|
||
}
|
||
|
||
function selectItem(index: number) {
|
||
dropDownOpen.value = false
|
||
act({ action: 'select', index })
|
||
}
|
||
|
||
// ListBox
|
||
const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined) ?? 12}px`)
|
||
</script>
|
||
|
||
<template>
|
||
<!-- Button -->
|
||
<button
|
||
v-if="node.type === 'Button'"
|
||
class="wf-control wf-button"
|
||
:style="style"
|
||
:disabled="!enabled"
|
||
@click="act({ action: 'click' })"
|
||
>
|
||
{{ displayText(p.Text) }}
|
||
</button>
|
||
|
||
<!-- Label -->
|
||
<div
|
||
v-else-if="node.type === 'Label'"
|
||
class="wf-control wf-label"
|
||
:class="{ 'wf-autosize': p.AutoSize === true, 'wf-disabled': !enabled }"
|
||
:style="style"
|
||
@click="act({ action: 'click' })"
|
||
>
|
||
{{ displayText(p.Text) }}
|
||
</div>
|
||
|
||
<!-- TextBox -->
|
||
<div v-else-if="node.type === 'TextBox'" class="wf-control wf-textbox" :style="style">
|
||
<textarea
|
||
v-if="p.Multiline"
|
||
:value="String(p.Text ?? '')"
|
||
:readonly="p.ReadOnly === true"
|
||
:disabled="!enabled"
|
||
@input="act({ action: 'text', value: ($event.target as HTMLTextAreaElement).value })"
|
||
/>
|
||
<input
|
||
v-else
|
||
:type="passwordChar ? 'password' : 'text'"
|
||
:value="String(p.Text ?? '')"
|
||
:readonly="p.ReadOnly === true"
|
||
:disabled="!enabled"
|
||
@input="act({ action: 'text', value: ($event.target as HTMLInputElement).value })"
|
||
/>
|
||
</div>
|
||
|
||
<!-- ComboBox -->
|
||
<div v-else-if="node.type === 'ComboBox'" class="wf-control wf-combobox" :style="style">
|
||
<input
|
||
:value="String(p.Text ?? '')"
|
||
:readonly="!editable"
|
||
:disabled="!enabled"
|
||
@mousedown="!editable && openDropDown()"
|
||
@input="act({ action: 'text', value: ($event.target as HTMLInputElement).value })"
|
||
/>
|
||
<span class="wf-combobox-arrow" @mousedown.prevent="openDropDown">▾</span>
|
||
<ul v-if="dropDownOpen" class="wf-combobox-list">
|
||
<li v-for="(item, index) in items" :key="index" @mousedown.prevent="selectItem(index)">{{ item }}</li>
|
||
<li v-if="items.length === 0" class="wf-combobox-empty"> </li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- ListBox:点空白处只触发 Click,点某一项先选中再触发 Click -->
|
||
<div
|
||
v-else-if="node.type === 'ListBox'"
|
||
class="wf-control wf-listbox"
|
||
:class="[`wf-listbox-border-${p.BorderStyle ?? 'Fixed3D'}`, { 'wf-disabled': !enabled }]"
|
||
:style="style"
|
||
@click.self="act({ action: 'click' })"
|
||
>
|
||
<div
|
||
v-for="(item, index) in items"
|
||
:key="index"
|
||
class="wf-listbox-item"
|
||
:class="{ 'wf-listbox-selected': index === p.SelectedIndex }"
|
||
:style="{ height: itemHeight, lineHeight: itemHeight }"
|
||
@click="act({ action: 'select', index })"
|
||
@dblclick="act({ action: 'doubleClick' })"
|
||
>
|
||
{{ item }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CheckBox / RadioButton:自绘勾选框,状态完全由运行器决定(点击只上报,不在浏览器里自行切换) -->
|
||
<div
|
||
v-else-if="node.type === 'CheckBox' || node.type === 'RadioButton'"
|
||
class="wf-control wf-check"
|
||
:class="{ 'wf-disabled': !enabled, 'wf-autosize': p.AutoSize === true }"
|
||
:style="style"
|
||
@click="act({ action: 'click' })"
|
||
>
|
||
<span :class="node.type === 'CheckBox' ? 'wf-checkbox-box' : 'wf-radio-dot'" :data-checked="p.Checked === true" />
|
||
<span class="wf-check-text">{{ displayText(p.Text) }}</span>
|
||
</div>
|
||
|
||
<!-- 教材控件库:灯泡、报警灯(自绘图片,尺寸与教材控件一致) -->
|
||
<div v-else-if="node.type === 'WinFormsDeviceControlLibrary.Lamp'" class="wf-control wf-device-image" :style="style">
|
||
<img :src="p.IsOn ? '/devices/lamp-on.svg' : '/devices/lamp-off.svg'" alt="" draggable="false" />
|
||
</div>
|
||
<div v-else-if="node.type === 'WinFormsDeviceControlLibrary.Alarm'" class="wf-control wf-device-image" :style="style">
|
||
<img :src="p.IsOn ? '/devices/alarm-on.svg' : '/devices/alarm-off.svg'" alt="" draggable="false" />
|
||
</div>
|
||
|
||
<!-- GroupBox -->
|
||
<div v-else-if="node.type === 'GroupBox'" class="wf-control wf-groupbox" :style="style">
|
||
<div class="wf-groupbox-frame" />
|
||
<span v-if="p.Text" class="wf-groupbox-caption">{{ displayText(p.Text) }}</span>
|
||
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="props.act" />
|
||
</div>
|
||
|
||
<!-- 教材控件库:LED 显示屏 -->
|
||
<LedControl
|
||
v-else-if="node.type === 'WinFormsDeviceControlLibrary.LED'"
|
||
:node="node"
|
||
:style="style"
|
||
/>
|
||
|
||
<!-- 其他容器(Panel、UserControl 等):按普通容器渲染 -->
|
||
<div v-else class="wf-control wf-panel" :style="style" :title="node.type">
|
||
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="props.act" />
|
||
</div>
|
||
</template>
|