From fe62d5088130aa364e2ecb0f7e2e4f421252d496 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 25 Oct 2026 18:42:14 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=A4=A7=E7=AA=97=E4=BD=93?= =?UTF-8?q?=EF=BC=9A=E8=BF=90=E8=A1=8C=E5=8C=BA=E5=8F=AF=E6=94=B6=E8=B5=B7?= =?UTF-8?q?=E3=80=81=E8=AE=BE=E8=AE=A1=E5=99=A8=E7=BC=A9=E6=94=BE=EF=BC=9B?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=AE=B1=E8=A1=A5=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 编辑区和运行区之间的竖条点一下收起运行区,代码和设计器占满宽度;点"运行"自动展开,浏览器记住选择 - 设计器缩放:适应宽度(默认,只缩小不放大)、100%、75%、50%;缩放时拖动、改大小、 双击、拖入控件的坐标按比例换算(教材项目 8 的窗体有 1043×791) - 工具箱给列表框、图片框、日期时间选择器、滑动条、数据表格、图表、媒体播放器补上图标 Co-Authored-By: Claude Opus 5.5 --- web/src/designer/FormDesigner.vue | 75 ++++++++++++++++++++++++++----- web/src/pages/WorkbenchPage.vue | 55 +++++++++++++++++++++-- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/web/src/designer/FormDesigner.vue b/web/src/designer/FormDesigner.vue index 3173a08..bf972e7 100644 --- a/web/src/designer/FormDesigner.vue +++ b/web/src/designer/FormDesigner.vue @@ -2,7 +2,7 @@ // 窗体设计器(仿 VS):左边工具箱,中间窗体,下方组件栏,右边属性 / 事件窗口。 // 窗体用运行时同一套 ControlView 渲染;上面盖一层透明的操作层,负责选中、拖动、改大小、放置新控件。 // 每次改动都重新生成 Designer.cs,通过 update 事件交给页面保存。 -import { computed, ref, shallowRef, watch } from 'vue' +import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue' import ControlView from '../winforms/ControlView.vue' import { displayText, fontCss, type FontValue } from '../winforms/format' import { CONTROLS, type ControlDef, FORM_DEF, controlDef, isContainerType } from './catalog' @@ -164,6 +164,41 @@ const selectedAutoSize = computed(() => doc.value && selected.value && getPropTe // ---------- 指针操作:选中、移动、改大小 ---------- const surface = ref() + +// ---------- 缩放:教材项目 8 的窗体有 1043×791,"适应"时按画布宽度缩小(不放大) ---------- +type ZoomChoice = 'fit' | '1' | '0.75' | '0.5' +const ZOOM_KEY = 'iot-sim:designer-zoom' +const ZOOM_OPTIONS: [ZoomChoice, string][] = [['fit', '适应宽度'], ['1', '100%'], ['0.75', '75%'], ['0.5', '50%']] +const zoomChoice = ref(readZoom()) +function readZoom(): ZoomChoice { + try { + const saved = localStorage.getItem(ZOOM_KEY) + return ZOOM_OPTIONS.some(([v]) => v === saved) ? (saved as ZoomChoice) : 'fit' + } catch { + return 'fit' + } +} +function setZoom(value: ZoomChoice) { + zoomChoice.value = value + try { + localStorage.setItem(ZOOM_KEY, value) + } catch { + // 浏览器不让存时只在这次有效 + } +} +const canvas = ref() +const canvasWidth = ref(0) +const canvasObserver = new ResizeObserver(([entry]) => { + canvasWidth.value = entry.contentRect.width +}) +onMounted(() => canvas.value && canvasObserver.observe(canvas.value)) +onBeforeUnmount(() => canvasObserver.disconnect()) +/** 窗体的缩放比例;鼠标位置除以它换算回窗体坐标 */ +const zoom = computed(() => { + if (zoomChoice.value !== 'fit') return Number(zoomChoice.value) + const width = clientSize.value.width + 2 + 8 + return canvasWidth.value > 0 ? Math.max(0.3, Math.min(1, canvasWidth.value / width)) : 1 +}) type Handle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' const HANDLES: Handle[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'] @@ -173,9 +208,9 @@ let drag: | { mode: 'form'; startX: number; startY: number; origin: { width: number; height: number } } | undefined -function pointer(event: PointerEvent | DragEvent) { +function pointer(event: PointerEvent | DragEvent | MouseEvent) { const box = surface.value!.getBoundingClientRect() - return { x: event.clientX - box.left, y: event.clientY - box.top } + return { x: (event.clientX - box.left) / zoom.value, y: (event.clientY - box.top) / zoom.value } } function onPointerDown(event: PointerEvent) { @@ -286,8 +321,9 @@ function onKeyDown(event: KeyboardEvent) { // ---------- 工具箱 ---------- const TOOLBOX_GROUPS = ['常用控件', '容器', '数据', '组件', '物联网设备'] as const const ICONS: Record = { - 按钮: '▭', 标签: 'A', 文本框: '▤', 下拉框: '☰', 复选框: '☑', 单选按钮: '◉', 分组框: '⬚', 面板: '▢', - 定时器: '⏱', 'LED 显示屏': '▬', 灯泡: '💡', 报警灯: '🚨', + 按钮: '▭', 标签: 'A', 文本框: '▤', 下拉框: '☰', 列表框: '≣', 图片框: '🖼', 日期时间选择器: '📅', 滑动条: '⊶', + 复选框: '☑', 单选按钮: '◉', 分组框: '⬚', 面板: '▢', 数据表格: '▦', 图表: '📈', + 定时器: '⏱', 媒体播放器: '🔊', 'LED 显示屏': '▬', 灯泡: '💡', 报警灯: '🚨', } const DRAG_TYPE = 'application/x-winforms-control' @@ -391,8 +427,8 @@ function clearEvent(target: string, eventName: string) { function onDoubleClick(event: MouseEvent) { const d = parsed.value.doc if (!d) return - const box = surface.value!.getBoundingClientRect() - const hit = hitTest(d, event.clientX - box.left, event.clientY - box.top) ?? '' + const { x, y } = pointer(event) + const hit = hitTest(d, x, y) ?? '' const def = hit ? controlDef(typeOf(d, hit) ?? '') : FORM_DEF openEvent(hit, def.defaultEvent) } @@ -440,16 +476,26 @@ const noop = () => {} -
+
拖动工具箱里的控件到窗体上;双击控件生成事件处理方法;Delete 删除 +
-
+
{{ displayText(formTitle) }} ─ ☐ ✕ @@ -601,8 +647,17 @@ const noop = () => {} cursor: default; } -.view-code { +.zoom { margin-left: auto; + padding: 1px 4px; + border: 1px solid #c5ccd5; + border-radius: 3px; + background: #fff; + font: inherit; +} + +.view-code { + flex: none; } .tips { diff --git a/web/src/pages/WorkbenchPage.vue b/web/src/pages/WorkbenchPage.vue index 354ebd9..1976ea2 100644 --- a/web/src/pages/WorkbenchPage.vue +++ b/web/src/pages/WorkbenchPage.vue @@ -427,8 +427,28 @@ function restoreOriginal() { else if (sampleId.value) void loadSample(sampleId.value, true) } +// ---------- 收起运行区(设计大窗体、写代码时左栏占满宽度;运行程序时自动展开) ---------- +const RUN_COLLAPSED_KEY = 'iot-sim:run-collapsed' +const runCollapsed = ref(readFlag(RUN_COLLAPSED_KEY)) +function readFlag(key: string) { + try { + return localStorage.getItem(key) === '1' + } catch { + return false + } +} +function toggleRunArea(collapsed = !runCollapsed.value) { + runCollapsed.value = collapsed + try { + localStorage.setItem(RUN_COLLAPSED_KEY, collapsed ? '1' : '0') + } catch { + // 浏览器不让存时只在这次有效 + } +} + function run() { if (running.value) return + toggleRunArea(false) session.run( files.value.map((f) => ({ ...f })), props.assignmentId ? assignment.value?.sampleId : sampleId.value, @@ -522,7 +542,7 @@ function run() { :before-start="flushSave" /> -
+
@@ -593,7 +613,14 @@ function run() { />
- + +
@@ -665,13 +692,33 @@ function run() { .workspace { flex: 1; display: grid; - grid-template-columns: minmax(360px, 2fr) 3fr; + grid-template-columns: minmax(360px, 2fr) 14px 3fr; min-height: 0; } /* 设计器需要工具箱、窗体和属性窗口,左栏放宽 */ .workspace.designing { - grid-template-columns: minmax(0, 3fr) minmax(360px, 2fr); + grid-template-columns: minmax(0, 3fr) 14px minmax(360px, 2fr); +} + +.workspace.run-collapsed { + grid-template-columns: minmax(0, 1fr) 14px; +} + +/* 编辑区和运行区之间的竖条:点一下收起、展开运行区 */ +.run-toggle { + padding: 0; + border: none; + border-right: 1px solid #e5e7eb; + background: #f3f4f6; + color: #6b7280; + font-size: 10px; + cursor: pointer; +} + +.run-toggle:hover { + background: #e5e7eb; + color: #111827; } .editor {