很多年前刚开始练 HTML/CSS/JS 的时候,我给自己定了个小目标:不依赖任何框架,用原生代码把日常家电的控制面板都做一遍。空调、洗衣机、烤箱……做到第 62 个的时候,我选了冰箱控制界面。别小看这玩意,它表面上是「几个温度数字 + 几颗按键」,实际做下来才发现,布局分层、交互状态、边界条件、视觉拟真,哪一样都不比写一个普通后台页面轻松。这篇文章就把「冰箱控制界面-html」这个项目的完整思路和个人踩坑记录捋一遍,适合正在练原生前端的朋友参考,也适合想快速出一版家电 UI 演示原型的人抄作业。
1. 为什么我坚持用原生 HTML 做家电控制面板
先说说立项动机。冰箱控制界面这类东西,不少人一听就觉得简单:不就是个带温度数字的页面吗?但实际上它有很强的交互状态要求,比如冷藏室、冷冻室是独立控温的,模式切换要联动显示,门开了要有告警。用 Vue、React 当然可以做,但我不建议上来就上框架。原因有三个。
第一,纯 HTML + CSS + JS 没有依赖,拷到任何地方都能跑。我在做这个项目的时候,经常需要在客户的演示机上临时打开页面,那台机器不见得装了 Node.js,更不一定有网络能拉 CDN。单文件或者一个文件夹拖进浏览器就能运行的方案,在真实演示场景里最稳。
第二,家电控制界面本质上是一套「有限状态机」,逻辑并不复杂,用原生 JS 的对象和事件完全够用。用框架反而要处理生命周期、组件通信这些额外概念,严重跑题。做完这个项目你会有一个很明显的感受:DOM 操作顺手了之后,手搓小交互比想象中快得多,而且完全没有构建工具的编译等待时间。
第三,原生 HTML 做出来的界面,可以直接嵌入其它技术栈。比如用 PyQt5 的 QWebEngineView 加载、用 Electron 打包成桌面应用、甚至塞进嵌入式设备的 WebView 里,跨平台能力极强。我当时做完这个冰箱界面,就顺手挂到了 PyQt 程序里当实时监控面板,只改了几行通信代码。
这个项目的终极目标,是用纯前端还原一个「接近实机体验」的冰箱控制面板,重点攻克以下问题:信息层级怎么划分、温度如何调节、模式如何联动、开门告警怎么写、视觉上怎么做出屏幕和按键的真实感、以及整套 UI 在手机和电脑上都能正常使用。如果你是刚学完 JS 基础、想找一个综合练手项目的人,这个选题非常合适。
2. 冰箱界面信息架构:先理清显示区、控制区与状态区
动笔写代码之前,我先把冰箱的实体控制面板拆了一遍。市面上常见冰箱的面板大概分三种:纯机械旋钮式、老式 LED 数码管按键式、以及带彩屏的智能触摸式。我这版做的是第二种形态的提级版——保留数字显示的真实感,同时加入更多状态反馈。
2.1 功能分区:面板上到底要放什么
我的面板结构分成了五个大区:
| 区域 | 对应元素 | 核心功能 |
|---|---|---|
| 顶部状态条 | 时间、锁定图标、告警图标 | 展示冰箱当前系统状态 |
| 主显示区(左) | 冷藏室温度大字 | 显示当前温度与设定温度 |
| 主显示区(右) | 冷冻室温度大字 | 显示当前温度与设定温度 |
| 中央状态区 | 模式指示灯、门状态图标 | 标识智能/速冻/假日/节能 |
| 操作区 | 温区切换、加减键、模式键 | 全部交互入口 |
这种分区的核心依据只有一个:用户的视线动线。人看冰箱控制面板,永远是先瞄一眼温度,然后再去操作按键。所以温度显示必须是最大、最醒目的,操作按键次之,模式指示灯是辅助信息,放在温度下方不抢占焦点。很多新手做这类界面,一上来就把按钮做得比温度数字还大,这就是典型的主次不分。
2.2 状态机设计:搞清楚交互之间的约束关系
冰箱交互的精髓不是「能按」,而是「在什么状态下能按」。我一开始做得太简单,每个按键独立触发,结果发现会出现一堆逻辑漏洞:冰箱处于速冻模式时,冷冻室温度应该被强制拉低,不允许用户手动调到不合理的值;锁定开启后,所有按键都不能响应,但时间显示不能停。
我在动手前画了一张状态表,把整个界面的运作逻辑锁定了。
| 当前状态 | 可进行操作 | 禁止操作 | 备注 |
|---|---|---|---|
| 正常运行 | 温区切换、调节温度、模式切换、锁定 | 无 | 默认状态 |
| 速冻模式 | 切换回智能/假日、锁定 | 手动调冷冻温度 | 速冻期间冷冻显示设定值 |
| 门开启 | 关闭告警、调节温度、锁定 | 无 | 告警标志常亮并闪烁 |
| 锁定状态 | 解锁 | 一切调温/切模式操作 | 按键响应被统一拦截 |
有了这张状态表,后面写 JS 的时候思路就非常顺。逻辑不是散落在一堆事件回调里,而是从一个state对象出发,任何操作先检查当前 state 是否允许,再做具体响应。这一做法的好处是:加新功能时不需要翻遍所有事件,只需要往状态表里加一行。
3. 静态骨架搭建:HTML 分层结构与 CSS 视觉细节
信息架构理清之后,静态页面的搭建就顺理成章了。我直接写了一个单页结构,整体分三层容器:面板外壳、显示屏区域、按键区域。刻意不把结构和样式混在一起,每层各司其职,方便后面改样式。
3.1 HTML 分层:外壳、屏体、按键
面板外壳我做成一个圆角矩形,模拟冰箱门上的那块控制面板整体。面板内部最核心的容器是显示屏区域,我给它一个独立的screen类,背景用深色渐变,模拟液晶屏的底光。显示屏里又分割成左右两个温度区,中间用一条竖线分隔,视觉上有「两个独立传感器」的暗示。
控制按键区放在屏幕下方,用的是grid布局,排成两行。第一行是冷藏和冷冻的温区切换键,第二行是减温度、加温度、模式切换这三个大键。最右侧放锁定键,用一个小锁图案表示,避免和调温键混在一起误触。
下面是关键 HTML 骨架,实际使用时可以按需调整:
<div class="fridge-panel"> <div class="screen"> <div class="top-bar"> <span class="clock">08:30</span> <span class="sys-icons"> <i class="icon-lock"></i> <i class="icon-alert"></i> </span> </div> <div class="temp-area"> <div class="temp-zone" id="zone-fridge"> <span class="zone-label">冷藏室</span> <span class="temp-value" id="temp-fridge">4</span> <span class="temp-unit">℃</span> </div> <div class="zone-divider"></div> <div class="temp-zone" id="zone-freezer"> <span class="zone-label">冷冻室</span> <span class="temp-value" id="temp-freezer">-18</span> <span class="temp-unit">℃</span> </div> </div> <div class="mode-area"> <span class="mode-light">.screen { background: radial-gradient(circle at 30% 20%, #263238, #101417); border-radius: 12px; padding: 18px 20px; box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.8); } .temp-value { font-family: "DSEG7", "Consolas", monospace; font-size: 72px; letter-spacing: 4px; color: #63ff7a; text-shadow: 0 0 8px rgba(99, 255, 122, 0.6); }显示屏的底色不能是纯黑,我用了一个径向渐变,中间稍微偏亮,模拟背光从中心发散的感觉。外面再加一层向内的阴影,好像屏幕比面板稍微凹陷下去。这个细节很多人忽略,但一旦加上,拟真感立刻上升一大截。
按键材质方面,我用的思路是「物理按键的凸起要靠多重阴影叠出来」:底部深色阴影模拟按键被垫高的底座,顶部同色亮边模拟刻字按键的反光。按键按下时,把阴影缩短甚至去掉,同时把文字颜色变暗,就能做出「被压下去」的效果。
.buttons button { background: linear-gradient(145deg, #3c4043, #292d31); border: none; border-radius: 10px; color: #dfe1e5; box-shadow: 0 4px 0 #1a1c1e, 0 6px 8px rgba(0, 0, 0, 0.4); transition: all 0.08s linear; } .buttons button:active { box-shadow: 0 1px 0 #1a1c1e, 0 2px 3px rgba(0, 0, 0, 0.3); transform: translateY(3px); }这种box-shadow叠加 +transform: translateY的做法,比单纯改透明度要有质感得多。注意transition时间一定要短,按键的物理回弹速度极快,超过 0.1 秒就会拖泥带水,按下时的手感就废了。
4. 交互逻辑编写:温度调节、模式联动与门状态告警
静态页面搭完,剩下的就是交互。冰箱控制界面真正考验 JS 能力的地方是:如何用一套状态驱动所有界面元素的更新,而不是东一榔头西一棒子地操作 DOM。
4.1 统一状态对象:所有显示都从它派生
我在代码里维护了一个名叫fridgeState的对象,里面存放所有需要持久化的数据。温度、模式、锁定、门状态,全在这里。然后写了一个render()函数,它读取fridgeState,把所有变化写到页面上。这种「数据驱动视图」的思路,就算只用原生 JS,也一样可以实现得很优雅。
const fridgeState = { zone: 'fridge', // 当前处于调节状态的温区 fridgeTemp: 4, // 冷藏当前温度 freezerTemp: -18, // 冷冻当前温度 mode: 'smart', // smart / fast / holiday / eco locked: false, doorOpen: false, }; function render() { document.getElementById('temp-fridge').textContent = fridgeState.fridgeTemp; document.getElementById('temp-freezer').textContent = fridgeState.freezerTemp; document.querySelectorAll('.mode-light').forEach(el => { el.classList.toggle('active', el.dataset.mode === fridgeState.mode); }); document.querySelector('.icon-lock').classList.toggle('active', fridgeState.locked); const doorEl = document.querySelector('.door-status'); if (fridgeState.doorOpen) { doorEl.textContent = '门未关'; doorEl.classList.add('alert'); } else { doorEl.textContent = '关门状态正常'; doorEl.classList.remove('alert'); } }这套写法的精髓在于:任何操作函数都不直接改 DOM,只改fridgeState,然后调用render()。比如按「+」键,函数只做一件事:判断当前处于锁定还是门开告警状态,如果不允许操作就直接 return,否则把对应温区的温度加一档,然后 render。所有 UI 更新都集中在 render 里,调试的时候只需盯着模型数据看,Bug 出现的概率直线下降。
4.2 加减温度:范围限制才是关键
冰箱温度调节有个容易被忽略的重点:温度不是无限制加减的,硬件有一个安全范围。冷藏室一般允许设定在 2℃ 到 8℃ 之间,冷冻室则是 -25℃ 到 -15℃。如果用户按住「+」键不放,数值到了边界必须停下来,界面上的按钮也应该有对应的禁用视觉反馈。
function adjustTemp(direction) { if (fridgeState.locked || fridgeState.doorOpen) return; const zone = fridgeState.zone; const min = zone === 'fridge' ? 2 : -25; const max = zone === 'fridge' ? 8 : -15; let next = zone === 'fridge' ? fridgeState.fridgeTemp : fridgeState.freezerTemp; next += direction; // direction 为 +1 或 -1 next = Math.max(min, Math.min(max, next)); if (zone === 'fridge') { fridgeState.fridgeTemp = next; } else { fridgeState.freezerTemp = next; } render(); }注意我在调节前用Math.max和Math.min把数值钳制在合法区间内,同时从一开始就阻止锁定和门开启状态下的调节。这种边界处理的优先级顺序很重要:先检查全局状态(锁定、门开),再检查数值区间,逻辑才不会互相打架。
4.3 模式联动:速冻状态的特殊逻辑
模式切换是我觉得这个冰箱界面最有意思的部分。四种模式(智能、速冻、假日、节能)并不是简单的「换个亮灯」,它们会真实影响温度显示和调节逻辑。
最典型的是速冻模式。速冻激活以后,冰箱会把冷冻室温度强制调到很低的设定值(一般是 -24℃ 而不允许用户手动改),速冻时间到了之后自动退回智能模式。因此在状态表里,速冻状态下客户端要同时满足「显示强制低温」和「屏蔽冷冻室手动调节」两个条件。
function switchMode() { if (fridgeState.locked) return; const order = ['smart', 'fast', 'holiday', 'eco']; const idx = order.indexOf(fridgeState.mode); fridgeState.mode = order[(idx + 1) % order.length]; if (fridgeState.mode === 'fast') { fridgeState.freezerTemp = -24; } render(); }实现上我没有用一堆 if/else 去区分「现在是速冻、所以不能调」,而是在 render 的时候根据 mode 动态决定按钮的行为状态。比如 mode 是 fast 时,给冷冻室区域加一个locked-adjust的 class,CSS 里让它变灰,调温事件里再统一拦截一次。这样界面反馈和数据约束是双保险,体验比较周全。
4.4 门状态与告警:模拟真实交互反馈
我没有真的接一个门磁传感器,毕竟这是个 HTML 练习项目,所以门状态用一个模拟开关按钮触发。在面板上方放了一个「模拟开门」的小按钮,点击后doorOpen变为 true。此时屏幕上的门状态文字变成「门未关」,并开始闪烁,告警图标也亮起。闪烁效果用 CSS 动画实现,只在 alert 状态下启用。
.alert { color: #ff5252; animation: blink 0.8s steps(2, start) infinite; } @keyframes blink { 50% { opacity: 0.2; } }为了更像真实冰箱的蜂鸣声,我还用 Web Audio API 做了一小段提示音:门未关时播放短促的 beep,每秒一次。这个功能虽然看起来不起眼,但在演示时非常加分。代码很短,就是创建一个 OscillatorNode 生成一个 800Hz 左右的方波,每次播放 0.1 秒。演示过程中,在场的人不用看屏幕,光听声音就知道「门被打开了」,整个 Demo 的真实度瞬间拔高一截。
5. 从「能跑」到「像真机」:打磨交互体验的细节
功能和大部分逻辑都写完以后,我开始进入「把玩阶段」。作为拿它去演示的项目,光能跑是不够的,手感和观感必须跟上。这个环节我花了比写逻辑更多的时间,因为细节全藏在那些没有被人明确要求的功能里。
5.1 数码管数字的动态切换:别用突兀的整块替换
一开始,温度数字的刷新是整体替换的,比如从4直接变成5。刷新速度太快,看起来像屏幕闪了一下。真实冰箱的数码管在切换时会有「部分笔画先灭、再亮新笔画」的过渡感。我尝试了两种方案:一是用 CSS 给.temp-value加一个极短的scale缩放动画,模拟刷新;二是直接切两个字体实例做交叉淡入淡出。最终我选了更省事的 CSS 方案:
.temp-value { transition: transform 0.05s ease-out, opacity 0.05s ease-out; } .temp-value.refresh { transform: scale(1.04); opacity: 0.6; }JS 里在修改数值后给元素临时加上.refresh类,用requestAnimationFrame在下一帧移除。效果是数字变化时有一个轻微的「跳动」和「闪暗」,很接近真实家电屏幕换字时的观感。这个技巧也适用于其它仪表类项目,比如时钟、温湿度计、电表读数。
5.2 长按连续调温:模拟真实物理按键行为
真机上长按调温键会连续跳动,而不是按一下跳一下。这个功能实现起来有个小坑:不能直接把「间隔触发」注册在click事件上,因为click在手指松开时才触发。正确做法是监听mousedown和touchstart,触发第一下后立刻启动一个setInterval,每 200ms 调一次温度;然后在mouseup/touchend和mouseleave时清除计时器。
function bindHoldAdjust(btn, direction) { function start() { adjustTemp(direction); timer = setInterval(() => adjustTemp(direction), 200); } function stop() { clearInterval(timer); } btn.addEventListener('mousedown', start); btn.addEventListener('mouseup', stop); btn.addEventListener('mouseleave', stop); btn.addEventListener('touchstart', e => { e.preventDefault(); start(); }); btn.addEventListener('touchend', stop); }这个功能有一个必须注意的点:连续调节到边界后,即使长按也不会有反应,但 visual feedback(比如按钮变灰)要跟上,否则用户会以为按钮坏了。
5.3 移动端适配:小屏冰箱面板的关键调整
做完桌面版之后,我把它拿手机浏览器打开,发现版式全乱了。冰箱面板在桌面看得很舒服,但在 375px 宽的屏幕下,两区温度的字号和按键尺寸都需要降级。我的解决方法是使用clamp()函数做流式字号,并在max-width: 480px的媒体查询里重新排布按键网格。
.temp-value { font-size: clamp(48px, 12vw, 72px); } @media (max-width: 480px) { .buttons { grid-template-columns: 1fr 1fr 1fr; gap: 10px; } .screen { padding: 12px; } }移动端的另一个坑是点击延迟。旧版浏览器上触摸之后会有 300ms 的延迟,操作起来「肉肉的」。我在 CSS 里给所有按钮加了touch-action: manipulation,并在 HTML 头部显式声明了viewport。这两步做完之后,手机上的点击响应基本能做到跟手。还有,按钮在 iOS 上有默认的灰色半透明高亮,需要用-webkit-tap-highlight-color: transparent去掉,不然和冰箱的暗色面板风格冲突非常大。
6. 拓展思路:从练习项目到可复用 UI 组件
做完「冰箱控制界面-html」之后,我没有把它当一次性练习丢掉。这套代码的架构其实可以直接复用给其它家电产品,稍微改改状态对象就能变成空调面板、洗衣机面板、烤箱面板。我更想分享的是后续的几个扩展方向,如果你正在考虑类似的项目,可以直接照这个思路往下走。
6.1 数据接口预留:为真实硬件接入做准备
冰箱控制界面真正的价值在于交互逻辑,而不是静态外观。如果后续要接真实设备,只需要把fridgeState的变更同步到后端。我预留了一个数据同步的抽象层,核心思路是:每次render()之后,调用一个syncState()函数。这个函数里可以是 WebSocket 上报,也可以是 MQTT 消息推送,甚至只是localStorage存储,都不影响界面逻辑。
function syncState() { // 示例:发送到后端 // ws.send(JSON.stringify(fridgeState)); localStorage.setItem('fridgeState', JSON.stringify(fridgeState)); }面向接口编程的好处就是:HTML 部分完全不变,只要替换syncState内部实现,就能把一套纯前端 Demo 变成真实设备的监控面板。我当时用这种方式把它接进了 PyQt5 程序,用QWebEngineView加载 HTML,后端用信号槽接收状态变更,控制真实硬件的继电器。整个改造只用了很短的时间。
6.2 视觉变体:多形态面板的快速迭代
冰箱控制面板在不同品牌、不同产品定位下差异很大。我在基础版之外做了两个变体,一共只改了几行 CSS 变量:一个深色石墨灰版本,适合高端嵌入式和厨房一体化设计;一个小尺寸纯显示版本,去掉全部按键,只保留屏幕显示和状态指示,用于「只读监控」场景。
变体的做法很简单,把颜色、阴影、圆角、字号全部抽到 CSS 变量里,然后通过body[data-theme="light"]这类属性切换主题。这样一套逻辑能出三种壳,对练习 CSS 变量和设计系统思维非常有帮助。如果你想进一步做得更像产品,还可以把整个控制面板嵌进一张实拍冰箱门的背景图里,用绝对定位把面板贴在门上,视觉冲击力直接翻倍。
6.3 和其它练习项目组合成一套「智能家居控制台」
我做了一系列类似的控制界面后,发现它们天然可以拼接在一起:冰箱面板、空调遥控面板、洗衣机面板,甚至加上一个模拟的智能家庭总览页,就是一个迷你「智能家居中控台」。互相之间用 iframe 拼装,或者用统一的状态管理串起来,页面左侧选择设备,右侧加载对应面板,非常像一个正经的智能家居演示系统。
我的感受是:做这类 HTML 小项目,最大的收获不是某一个特效怎么写,而是建立了一套「拆解真实设备、抽象状态、设计反馈、打磨细节」的完整流程。冰箱面板看起来是最不起眼的家电面板之一,却是把这套流程跑通的最佳载体,至少对我来说是这样。做完它之后,你会不太愿意再看那种「画皮不画骨」的纯静态界面了。下次再让我选一个小家电练手,我还是会打开编辑器,先画那张状态表,然后写第一行div class="fridge-panel"。