项目 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:
xuyueandClaude Opus 5.5 committed 2026-10-23 14:11:05 +08:00
1 parent a9af26c2d6
commit eedf1aa2fb
66 files changed
+3538 -43

No files matched your search

+156 -3
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
// 虚拟实训台:按教材实训台布局图(顶棚 → LED 屏 → 传感器 → 采集模块)排列。
// 传感器卡片上的滑块和开关调节环境(经服务器推给学生程序),灯和继电器的状态由学生程序控制。
import { computed, ref } from 'vue'
import { ANALOG_INFO, type AnalogKey, type BoardState, type ClientMessage, type DigitalKey } from '@iot/shared'
import { computed, onBeforeUnmount, ref } from 'vue'
import { ANALOG_INFO, clockWall, type AnalogKey, type BoardState, type ClientMessage, type DigitalKey } from '@iot/shared'
type BoardInput = Omit<Extract<ClientMessage, { type: 'boardInput' }>, 'type'>
const props = defineProps<{ board: BoardState | null }>()
/** playing:学生程序正在播放的声音(媒体播放器),广播喇叭亮起 */
const props = defineProps<{ board: BoardState | null; playing?: { name: string } | null }>()
const emit = defineEmits<{ set: [input: BoardInput]; reset: [] }>()
const MORE_SENSORS: AnalogKey[] = ['airPressure', 'airQuality', 'co2', 'wind']
@@ -78,6 +79,38 @@ const streetLampOn = computed(() => props.board?.zigbee[1] === true)
const alarmOn = computed(() => relays.value[0] === true)
const ledText = computed(() => props.board?.ledText ?? '')
const camera = computed(() => props.board?.camera ?? { on: false, x: 3, y: 1 })
// ---------- 虚拟时钟:显示学生程序看到的时间,可以拨时间、调倍速 ----------
const SPEEDS = [1, 10, 60]
const tick = ref(Date.now())
const ticker = setInterval(() => (tick.value = Date.now()), 250)
onBeforeUnmount(() => clearInterval(ticker))
const clock = computed(() => props.board?.clock ?? null)
/** 现在的墙上时间 yyyy-MM-ddTHH:mm:ss:拨过时间时按虚拟时钟算,否则是本机时间 */
const wall = computed(() => {
if (clock.value) return clockWall(clock.value, tick.value)
const d = new Date(tick.value)
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
})
const clockDate = ref('')
const clockTime = ref('')
function openClockEditor() {
clockDate.value = wall.value.slice(0, 10)
clockTime.value = wall.value.slice(11, 19)
}
function setClock() {
if (!clockDate.value || !clockTime.value) return
const time = clockTime.value.length === 5 ? `${clockTime.value}:00` : clockTime.value
emit('set', { clock: { time: `${clockDate.value}T${time}` } })
}
function setSpeed(speed: number) {
emit('set', { clock: { speed } })
}
const cameraTitle = computed(() =>
camera.value.on ? `网络摄像头:正在传送画面(云台左右 ${camera.value.x + 1}/7,上下 ${camera.value.y + 1}/3)` : '网络摄像头(项目 4 使用):未开启',
)
@@ -106,6 +139,7 @@ const cameraTitle = computed(() =>
<i />{{ input.label }}
</button>
<span v-if="ledText" class="summary-led" :title="ledText">LED:{{ ledText }}</span>
<span v-if="clock" class="summary-clock" title="虚拟时钟:学生程序里的 DateTime.Now">🕒 {{ wall.slice(11) }}({{ clock.speed }}×)</span>
<button class="link expand" @click="setCollapsed(false)">展开 ▾</button>
</div>
@@ -190,6 +224,15 @@ const cameraTitle = computed(() =>
</label>
</div>
<div class="card" :title="playing ? `广播正在播放 ${playing.name}` : '广播(教材项目 7 用 Windows Media Player 播放铃声)'">
<svg viewBox="0 0 24 24" width="30" height="30" class="icon speaker" :class="{ on: playing }">
<path fill="currentColor" d="M4 9h4l5-4v14l-5-4H4z" />
<path v-if="playing" d="M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
<div class="card-title">广播</div>
<div class="card-note">{{ playing ? `播放 ${playing.name}` : '静音' }}</div>
</div>
<button class="card toggle" :class="{ active: digital.smoke }" title="点击切换:有烟雾 / 正常" @click="toggle('smoke')">
<div class="smoke" :class="{ on: digital.smoke }"><i /><i /><i /></div>
<div class="card-title">烟雾传感器</div>
@@ -254,6 +297,31 @@ const cameraTitle = computed(() =>
<button class="reset" :disabled="!board" @click="emit('reset')">实训台复位</button>
</div>
</div>
<!-- 虚拟时钟:学生程序里的 DateTime.Now 跟着它走 -->
<div class="clock-strip" :class="{ virtual: clock }">
<span class="module-title">时钟</span>
<b class="clock-time">{{ wall.slice(11) }}</b>
<span class="clock-date">{{ wall.slice(0, 10) }}</span>
<span class="clock-tag">{{ clock ? `虚拟时间 · ${clock.speed}×` : '真实时间' }}</span>
<span class="spacer" />
<input v-model="clockDate" type="date" :disabled="!board" @focus="!clockDate && openClockEditor()" />
<input v-model="clockTime" type="time" step="1" :disabled="!board" @focus="!clockTime && openClockEditor()" />
<button class="chip-button" :disabled="!board || !clockDate || !clockTime" title="把学生程序看到的时间拨到这个时刻" @click="setClock">拨到</button>
<span class="speeds" title="倍速:60× 时真实的 1 秒是 1 分钟">
<button
v-for="speed in SPEEDS"
:key="speed"
class="chip-button"
:class="{ active: (clock?.speed ?? 1) === speed && (clock !== null || speed === 1) }"
:disabled="!board"
@click="setSpeed(speed)"
>
{{ speed }}×
</button>
</span>
<button v-if="clock" class="chip-button" :disabled="!board" @click="emit('set', { clock: null })">恢复真实时间</button>
</div>
</div>
</template>
@@ -580,6 +648,91 @@ button {
}
/* 模块 */
.clock-strip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
padding: 6px 8px;
background: #3a4556;
border-radius: 4px;
}
.clock-strip.virtual {
box-shadow: inset 0 0 0 1px #f5a623;
}
.clock-time {
font-size: 16px;
font-variant-numeric: tabular-nums;
color: #fff;
}
.clock-date {
color: var(--muted);
}
.clock-tag {
color: var(--muted);
}
.virtual .clock-tag {
color: #f5a623;
}
.clock-strip .spacer {
flex: 1;
}
.clock-strip input {
background: #2b3340;
color: var(--text);
border: 1px solid #556175;
border-radius: 3px;
padding: 1px 4px;
font: inherit;
color-scheme: dark;
}
.chip-button {
background: #2b3340;
color: var(--text);
border: 1px solid #556175;
border-radius: 3px;
padding: 1px 8px;
font: inherit;
cursor: pointer;
}
.chip-button.active {
border-color: var(--accent);
color: #fff;
background: #2f5f99;
}
.chip-button:disabled {
opacity: 0.5;
cursor: default;
}
.speeds {
display: inline-flex;
gap: 4px;
}
.speaker {
color: var(--muted);
}
.speaker.on {
color: #4cd964;
animation: blink 0.8s steps(2) infinite;
}
.summary-clock {
color: #f5a623;
}
.modules {
display: grid;
grid-template-columns: 1.2fr 1.5fr 1fr;