
很多年前刚开始练 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 classfridge-panel div classscreen div classtop-bar span classclock08:30/span span classsys-icons i classicon-lock/i i classicon-alert/i /span /div div classtemp-area div classtemp-zone idzone-fridge span classzone-label冷藏室/span span classtemp-value idtemp-fridge4/span span classtemp-unit℃/span /div div classzone-divider/div div classtemp-zone idzone-freezer span classzone-label冷冻室/span span classtemp-value idtemp-freezer-18/span span classtemp-unit℃/span /div /div div classmode-area span classmode-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的 classCSS 里让它变灰调温事件里再统一拦截一次。这样界面反馈和数据约束是双保险体验比较周全。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-themelight]这类属性切换主题。这样一套逻辑能出三种壳对练习 CSS 变量和设计系统思维非常有帮助。如果你想进一步做得更像产品还可以把整个控制面板嵌进一张实拍冰箱门的背景图里用绝对定位把面板贴在门上视觉冲击力直接翻倍。6.3 和其它练习项目组合成一套「智能家居控制台」我做了一系列类似的控制界面后发现它们天然可以拼接在一起冰箱面板、空调遥控面板、洗衣机面板甚至加上一个模拟的智能家庭总览页就是一个迷你「智能家居中控台」。互相之间用 iframe 拼装或者用统一的状态管理串起来页面左侧选择设备右侧加载对应面板非常像一个正经的智能家居演示系统。我的感受是做这类 HTML 小项目最大的收获不是某一个特效怎么写而是建立了一套「拆解真实设备、抽象状态、设计反馈、打磨细节」的完整流程。冰箱面板看起来是最不起眼的家电面板之一却是把这套流程跑通的最佳载体至少对我来说是这样。做完它之后你会不太愿意再看那种「画皮不画骨」的纯静态界面了。下次再让我选一个小家电练手我还是会打开编辑器先画那张状态表然后写第一行div classfridge-panel。