Files
IOT_SIM/web/src/winforms/FormWindow.vue
T
xuyueandClaude Opus 5.5 98be8331bc 第 0 阶段:教材项目 2 任务 1 在网页中原样运行
- runtime:仿真 WinForms 子集(Form、Button、Label、TextBox、ComboBox、
  GroupBox、Timer、MessageBox、界面线程消息循环、跨线程检查)、
  教材设备类库(DeviceCtrlHelper、LED 控件)、运行器(常驻编译服务 + 运行模式)
- server:Bun + Hono,WebSocket 会话,每次运行一个进程,消息转发
- web:Vue + Naive UI,WinForms 风格窗体渲染、MessageBox、实训台 LED 屏
- 端到端测试与 100 个程序并发的压力测试
- 教材逐任务摘要(docs/教材摘要)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:15:14 +08:00

66 lines
2.5 KiB
Vue

<script setup lang="ts">
// 一个 WinForms 窗体:标题栏 + 客户区。窗体可以拖动标题栏移动。
import { computed, ref } from 'vue'
import type { ControlNode, UserAction } from '@iot/shared'
import ControlView from './ControlView.vue'
import { displayText, fontCss, type FontValue, type SizeValue } from './format'
const props = defineProps<{
form: ControlNode
modal: boolean
offset: number
act: (id: string, action: UserAction) => void
}>()
const p = computed(() => props.form.props)
const borderless = computed(() => p.value.FormBorderStyle === 'None')
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染
const childrenTopLast = computed(() => [...props.form.children].reverse())
const clientSize = computed(() => (p.value.ClientSize as SizeValue | undefined) ?? { width: 300, height: 300 })
const position = ref({ x: 24 + props.offset * 32, y: 24 + props.offset * 32 })
let dragStart: { x: number; y: number; px: number; py: number } | undefined
function startDrag(event: PointerEvent) {
dragStart = { x: event.clientX, y: event.clientY, px: position.value.x, py: position.value.y }
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
}
function drag(event: PointerEvent) {
if (!dragStart) return
position.value = { x: dragStart.px + event.clientX - dragStart.x, y: dragStart.py + event.clientY - dragStart.y }
}
function close() {
props.act(props.form.id, { action: 'close' })
}
</script>
<template>
<div
v-show="p.Visible !== false"
class="wf-window"
:class="{ 'wf-window-modal': modal }"
:style="{ left: `${position.x}px`, top: `${position.y}px` }"
>
<div v-if="!borderless" class="wf-titlebar" @pointerdown="startDrag" @pointermove="drag" @pointerup="dragStart = undefined">
<span class="wf-title">{{ displayText(p.Text) }}</span>
<span v-if="p.MinimizeBox !== false" class="wf-titlebar-button">─</span>
<span v-if="p.MaximizeBox !== false" class="wf-titlebar-button">☐</span>
<span class="wf-titlebar-button wf-titlebar-close" @pointerdown.stop @click="close">✕</span>
</div>
<div
class="wf-client"
:style="{
width: `${clientSize.width}px`,
height: `${clientSize.height}px`,
color: (p.ForeColor as string | null) ?? undefined,
backgroundColor: (p.BackColor as string | null) ?? undefined,
...fontCss(p.Font as FontValue | null),
}"
>
<ControlView v-for="child in childrenTopLast" :key="child.id" :node="child" :act="act" />
</div>
</div>
</template>