ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

动态设计洗衣机创意控制面板:HTML+CSS+JS实战教程

动态设计洗衣机创意控制面板:HTML+CSS+JS实战教程 日常开发中大家接触最多的可能是后台管理系统、数据大屏、营销活动页很少会把“洗衣机”和“动态设计”联系在一起。但如果把洗衣机控制面板当成一个交互产品来拆解你会发现它非常适合用来练习动态设计的实战能力有状态变化、有动效反馈、有进度模拟、有边界处理。本文围绕“【动态设计】洗衣机创意”这一主题从需求拆解、动效设计到完整代码实现带大家实现一个可在浏览器中直接运行的高保真动态洗衣机控制面板 Demo。本文适合前端入门到进阶的开发者阅读也适合需要做智能家居控制端、可视化交互面板、数字孪生演示项目的朋友参考。学完后你将掌握洗衣机运行状态机的建模方式、CSS 动画模拟核心转筒动效、JavaScript 定时器驱动的进度逻辑、动态设计在功能型界面中的落地方法。1. 动态设计视角下的“洗衣机创意”到底是什么1.1 动态设计不只是“会动”在 UI 和前端开发里动态设计Motion Design并不是单纯地给页面加几个动画。它强调的是动效与用户行为的关联、动效对信息的传达以及动效对操作反馈的强化。洗衣机的控制面板是一个非常典型的场景用户选择洗衣模式、点击启动、观察运行进度、等待结束提醒每一步都涉及状态切换和信息反馈。如果只是静态展示一个洗衣机外观那不算完整的设计如果只是让按钮 hover 变个颜色那也不叫动态设计。真正的“洗衣机创意”是把物理设备运行过程转化为界面上的动态表达滚筒转动、水流波动、进度条推进、剩余时间更新、结束提醒这些动态信息共同构成用户的完整操作感知。1.2 功能型界面的动效价值动态设计在工具型、功能型界面中的价值不只是“好看”。具体到洗衣机控制面板动效帮助用户解决三个核心问题状态可见洗衣机运行中、暂停中、已完成通过动效一眼就能看出来。进度可感剩余时间数字是冷冰冰的配合水流或转筒动画用户能感受到设备在工作。操作可预期点击启动后按钮状态变化、指示灯亮起用户知道操作已被系统接收。因此本文的“洗衣机创意”不是做一个装饰性的洗衣机页面而是做一个有状态管理、有动效反馈、有交互闭环的动态控制面板。1.3 技术选型说明为了让教程足够通用本文不引入 Vue、React 或其他重型框架而是使用最基础的前端三件套HTML CSS JavaScript。这样做的好处是零依赖直接复制代码到浏览器就能运行。核心 HTML 标签和 CSS 动画逻辑可以迁移到 Vue、React 或微信小程序中。方便讲解状态管理、定时器、CSS 动画等基础概念适合新人理解。如果你已经在使用现代框架可以把本文的代码思路直接转换为组件逻辑状态机部分可以替换为Pinia、Redux或Zustand管理模式。2. 环境准备与文件结构2.1 运行环境本文示例不需要安装任何第三方依赖只需要一个现代浏览器即可。建议使用 Chrome 或 Edge 最新版本以便获得较好的 CSS 动画和 JavaScript 语法支持。操作系统不限Windows、macOS、Linux 均可。代码编辑器推荐 VS Code但你用其他编辑器也不影响。2.2 项目文件结构为了便于阅读和维护我们把代码拆成三个文件laundry-demo/ ├── index.html ├── css/ │ └── style.css └── js/ └── app.js其中index.html页面结构展示洗衣机外观、控制面板、模式选择区域。css/style.css样式与动效定义包括滚筒旋转动画、水流波纹、按钮状态、进度条样式。js/app.js交互逻辑与状态管理包括模式选择、启动暂停、定时器控制、进度计算。2.3 版本兼容说明本文使用的 CSS 属性主要包括transition、animation、transform、box-shadow、linear-gradient、clamp()等均已得到现代浏览器良好支持。JavaScript 使用 ES6 语法包括let、const、箭头函数、模板字符串、class等在现代浏览器中可以直接运行。如果你需要兼容老旧浏览器如 IE11需要对代码做较大调整包括使用var替代let/const、补充 CSS 前缀等。本文不展开讨论旧浏览器兼容重点演示思路。3. 核心功能拆解与状态设计3.1 洗衣机控制面板的功能清单一个具有动态设计感并且可交互的洗衣机控制面板需要包含以下功能点功能说明交互方式模式选择不同洗衣模式有不同的时长和动态效果点击模式卡片启动/暂停开始运行或暂时停止状态可切换点击主控按钮运行进度进度条与剩余时间同步更新定时器驱动转筒动态滚筒转动效果模拟洗衣过程CSS 动画水流动态水流波纹效果增加真实感CSS 动画完成提示运行结束后按钮状态变化面板提示状态判断3.2 状态机模型洗衣机的运行过程非常适合用状态机来描述。我们简化后定义四个状态IDLE - RUNNING - PAUSED - RUNNING - COMPLETED各状态说明如下IDLE待机状态用户未启动或运行结束。RUNNING运行中转筒转动、进度推进、剩余时间递减。PAUSED暂停中进度暂停计时器挂起。COMPLETED已完成动效停止显示完成提示。在app.js中我们用变量记录当前状态并提供一个状态切换函数避免直接散落修改状态的代码。3.3 定时器驱动的进度模拟进度条和剩余时间采用setInterval驱动。每一个模式配置了总时长页面每隔设定的时间间隔更新一次剩余时间同时根据已消耗时间计算进度百分比。核心逻辑需要注意一个坑setInterval不是绝对精准的计时工具。如果页面切到后台浏览器会降低定时器执行频率。更严谨的做法是记录开始时间戳每次刷新时基于当前时间戳重新计算进度。本文为了演示简单采用按秒递减的方式但会在最佳实践部分说明更可靠的实现方式。3.4 动效设计拆解在动效层面我们主要使用三类动画转筒旋转动画使用animation: spin 2s linear infinite。水体波纹动画使用::before和::after伪元素叠加缩放与透明度变化。按钮与指示灯的过渡反馈使用transition实现背景色、阴影、缩放的状态切换。动态设计不只是“添加动画”还要求动画之间有节奏、有层次。比如启动时先让指示灯亮起再启动转筒旋转而不是所有元素同一帧变化。实现时可以通过animation-delay或 JavaScript 控制 class 的添加顺序。4. 完整实战动态洗衣机控制面板实现4.1 创建项目目录先在本地创建一个laundry-demo文件夹并在内部创建css和js子目录。mkdir -p laundry-demo/css mkdir -p laundry-demo/js后续所有代码文件都放在这个目录下。4.2 编写 HTML 结构打开index.html写入页面结构。页面分为三个区域洗衣机外观展示区、控制面板区、状态信息区。代码如下!-- 文件路径laundry-demo/index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title动态洗衣机创意控制面板/title link relstylesheet hrefcss/style.css /head body div classapp !-- 洗衣机外观与滚筒展示区 -- div classwasher div classwasher-body div classwasher-top div classstatus-bar span idstatusText待机/span span idstatusDot classdot dot-idle/span /div /div div classdrum-area div classdrum iddrum div classdrum-inner iddrumInner div classwave wave-one/div div classwave wave-two/div /div div classdrum-bubble bubble-1/div div classdrum-bubble bubble-2/div div classdrum-bubble bubble-3/div /div /div /div /div !-- 控制面板区 -- div classcontrol-panel h2选择洗衣模式/h2 div classmode-list idmodeList button classmode-item active>/* 文件路径laundry-demo/css/style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: PingFang SC, Microsoft YaHei, sans-serif; background: linear-gradient(135deg, #e8f4f8 0%, #c9e3ef 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; } .app { width: min(420px, 92vw); background: #f5f9fc; border-radius: 28px; padding: 24px; box-shadow: 0 20px 40px rgba(0, 30, 50, 0.15); } /* 洗衣机外观 */ .washer { background: #dce9f0; border-radius: 20px; padding: 16px; box-shadow: inset 0 2px 6px rgba(255, 255, 255, 0.8), inset 0 -4px 8px rgba(0, 0, 0, 0.06); } .washer-top { display: flex; justify-content: flex-end; align-items: center; padding-bottom: 12px; } .status-bar { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #3a5a6a; } .dot { width: 12px; height: 12px; border-radius: 50%; transition: background-color 0.3s, box-shadow 0.3s; } .dot-idle { background: #a0b8c4; } .dot-running { background: #2ecc71; box-shadow: 0 0 8px #2ecc71; } .dot-paused { background: #f39c12; box-shadow: 0 0 8px #f39c12; } .dot-completed { background: #3498db; box-shadow: 0 0 8px #3498db; } /* 滚筒区域 */ .drum-area { background: #1a2d38; border-radius: 50%; width: 100%; aspect-ratio: 1; position: relative; overflow: hidden; border: 6px solid #b8ccd6; box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.6), 0 6px 12px rgba(0, 0, 0, 0.2); } .drum { position: absolute; inset: 8%; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #3b5d6e, #16252e); overflow: hidden; border: 2px solid rgba(255, 255, 255, 0.15); } .drum-inner { position: absolute; inset: 0; border-radius: 50%; transform-origin: center center; animation: none; } .drum.spinning .drum-inner { animation: spin 2.4s linear infinite; } keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* 水流波动效果 */ .wave { position: absolute; left: -20%; right: -20%; height: 34%; background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.02)); border-radius: 45%; top: 70%; opacity: 0; transition: opacity 0.4s; } .drum.spinning .wave { opacity: 1; } .wave-one { animation: waveMove 3s ease-in-out infinite; } .wave-two { animation: waveMove 3.6s ease-in-out infinite reverse; top: 76%; } keyframes waveMove { 0% { transform: rotate(0deg) translateY(0); } 50% { transform: rotate(6deg) translateY(-8px); } 100% { transform: rotate(0deg) translateY(0); } } /* 气泡效果 */ .drum-bubble { position: absolute; background: rgba(255, 255, 255, 0.2); border-radius: 50%; width: 10px; height: 10px; opacity: 0; } .drum.spinning .drum-bubble { animation: bubbleRise 2.8s ease-in infinite; } .bubble-1 { left: 20%; bottom: 20%; } .bubble-2 { left: 55%; bottom: 30%; width: 6px; height: 6px; animation-delay: 0.8s; } .bubble-3 { left: 75%; bottom: 15%; width: 8px; height: 8px; animation-delay: 1.4s; } keyframes bubbleRise { 0% { transform: translateY(0) scale(0.8); opacity: 0; } 20% { opacity: 0.8; } 100% { transform: translateY(-220px) scale(1.4); opacity: 0; } } /* 控制面板 */ .control-panel { margin-top: 20px; padding: 16px; background: #ffffff; border-radius: 18px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } .control-panel h2 { font-size: 16px; color: #2f4858; margin-bottom: 14px; } .mode-list { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 18px; } .mode-item { padding: 10px 4px; background: #f0f5f8; border: 2px solid transparent; border-radius: 12px; font-size: 13px; color: #3a5a6a; cursor: pointer; transition: all 0.25s ease; } .mode-item:hover { background: #e1edf3; } .mode-item.active { background: #2e86ab; color: #ffffff; border-color: #1f5a75; box-shadow: 0 4px 10px rgba(46, 134, 171, 0.35); } .time-info { display: flex; justify-content: space-between; align-items: center; font-size: 14px; color: #7a8f9c; margin-bottom: 12px; } .time-value { font-size: 26px; font-weight: 700; color: #2f4858; font-variant-numeric: tabular-nums; } /* 进度条 */ .progress-wrap { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; } .progress-bar { flex: 1; height: 10px; background: #e8eef2; border-radius: 6px; overflow: hidden; } .progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #43c6ac, #2e86ab); border-radius: 6px; transition: width 0.6s ease; } .progress-text { font-size: 13px; color: #2f4858; width: 44px; text-align: right; font-variant-numeric: tabular-nums; } /* 主按钮 */ .main-btn { width: 100%; padding: 14px; border: none; border-radius: 14px; font-size: 16px; font-weight: 600; color: #ffffff; background: #2e86ab; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(46, 134, 171, 0.3); } .main-btn:hover { background: #266f8e; transform: translateY(-2px); } .main-btn:active { transform: translateY(0); } .main-btn.running { background: #27ae60; box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3); } .main-btn.paused { background: #f39c12; box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3); } .main-btn.completed { background: #3498db; box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); } /* 响应式处理 */ media (max-width: 380px) { .mode-list { grid-template-columns: repeat(2, 1fr); } }这段 CSS 有以下设计要点drum.spinning .drum-inner通过父元素带spinningclass 来触发旋转动画这是一种非常优雅的动效控制方式避免直接操作每个子元素。水流波纹使用伪元素和半透明渐变模拟水面通过waveMove动画营造波动的感觉。气泡动画设置了不同的animation-delay避免所有气泡同时上升视觉上更自然。进度条填充层使用transition属性因此进度更新时不是跳变而是平滑推进。按钮的四种状态默认、运行、暂停、完成对应不同背景色用户能直观感知当前状态。4.4 编写 JavaScript 交互逻辑接下来编写js/app.js。这一部分是洗衣机的“大脑”负责状态切换、定时器、进度计算和 DOM 更新。// 文件路径laundry-demo/js/app.js (function () { use strict; // 模式配置 const MODES { standard: { label: 标准洗, duration: 45 }, quick: { label: 快速洗, duration: 20 }, heavy: { label: 强力洗, duration: 60 }, rinse: { label: 漂洗, duration: 15 } }; // 状态定义 const STATE { IDLE: IDLE, RUNNING: RUNNING, PAUSED: PAUSED, COMPLETED: COMPLETED }; // DOM 元素 const statusText document.getElementById(statusText); const statusDot document.getElementById(statusDot); const drum document.getElementById(drum); const modeList document.getElementById(modeList); const timeValue document.getElementById(timeValue); const progressFill document.getElementById(progressFill); const progressText document.getElementById(progressText); const mainBtn document.getElementById(mainBtn); // 当前状态 let currentState STATE.IDLE; let currentMode standard; let remainingSeconds MODES.standard.duration; let totalSeconds MODES.standard.duration; let timer null; // DOM 初始化 updateUI(); // 格式化时间秒 - mm:ss function formatTime(sec) { const m Math.floor(sec / 60); const s sec % 60; const mm String(m).padStart(2, 0); const ss String(s).padStart(2, 0); return ${mm}:${ss}; } // 更新进度百分比 function getProgress() { if (totalSeconds 0) return 0; const elapsed totalSeconds - remainingSeconds; return Math.min(100, Math.round((elapsed / totalSeconds) * 100)); } // 更新界面 function updateUI() { // 模式按钮状态 document.querySelectorAll(.mode-item).forEach(item { item.classList.toggle(active, item.dataset.mode currentMode); }); // 剩余时间 timeValue.textContent formatTime(remainingSeconds); // 进度条 const progress getProgress(); progressFill.style.width progress %; progressText.textContent progress %; // 状态灯与文案 if (currentState STATE.IDLE) { statusText.textContent 待机; statusDot.className dot dot-idle; mainBtn.textContent 启动; mainBtn.className main-btn; } else if (currentState STATE.RUNNING) { statusText.textContent 运行中; statusDot.className dot dot-running; mainBtn.textContent 暂停; mainBtn.className main-btn running; } else if (currentState STATE.PAUSED) { statusText.textContent 已暂停; statusDot.className dot dot-paused; mainBtn.textContent 继续; mainBtn.className main-btn paused; } else if (currentState STATE.COMPLETED) { statusText.textContent 已完成; statusDot.className dot dot-completed; mainBtn.textContent 重置; mainBtn.className main-btn completed; } // 滚筒旋转状态 if (currentState STATE.RUNNING) { drum.classList.add(spinning); } else { drum.classList.remove(spinning); } } // 启动当前模式 function startMode(mode) { currentMode mode; totalSeconds MODES[mode].duration; remainingSeconds totalSeconds; currentState STATE.RUNNING; startTimer(); updateUI(); } // 暂停当前运行 function pauseMode() { clearTimer(); currentState STATE.PAUSED; updateUI(); } // 继续当前运行 function resumeMode() { currentState STATE.RUNNING; startTimer(); updateUI(); } // 完成运行 function completeMode() { clearTimer(); remainingSeconds 0; currentState STATE.COMPLETED; updateUI(); } // 重置初始状态 function resetMode() { clearTimer(); currentMode standard; totalSeconds MODES.standard.duration; remainingSeconds totalSeconds; currentState STATE.IDLE; updateUI(); } // 创建定时器 function startTimer() { clearTimer(); timer setInterval(() { if (remainingSeconds 1) { remainingSeconds - 1; updateUI(); } else { remainingSeconds 0; updateUI(); completeMode(); } }, 1000); } // 清除定时器 function clearTimer() { if (timer) { clearInterval(timer); timer null; } } // 模式选择事件 modeList.addEventListener(click, event { const target event.target.closest(.mode-item); if (!target) return; const mode target.dataset.mode; // 运行中或已暂停状态下不允许直接切换模式 if (currentState STATE.RUNNING || currentState STATE.PAUSED) { alert(请先暂停或重置后再切换模式); return; } startMode(mode); }); // 主按钮事件 mainBtn.addEventListener(click, () { if (currentState STATE.IDLE) { // 待机状态点击启动 startMode(currentMode); } else if (currentState STATE.RUNNING) { pauseMode(); } else if (currentState STATE.PAUSED) { resumeMode(); } else if (currentState STATE.COMPLETED) { resetMode(); } }); })();这段代码的核心设计如下MODES对象存储不同模式的总时长后续如果增加模式或调整时长只需要修改配置对象这体现了配置与逻辑分离的思想。STATE对象定义四类状态代码中不直接使用字符串散落各处而是统一从状态对象读取减少拼写错误。updateUI()函数集中处理所有 DOM 更新避免在定时器回调和事件回调里东一处西一处地操作 DOM。点击模式卡片时如果当前处于运行中或暂停中会弹出提示防止用户误操作导致状态错乱。主按钮根据当前状态执行不同操作相当于一个简单的有限状态机。4.5 运行与验证在浏览器中打开index.html你应该看到页面加载后处于“待机”状态滚筒静止进度为 0%主按钮显示“启动”。接下来依次验证点击“标准洗”模式卡片观察是否立即启动滚筒开始旋转状态灯变为绿色剩余时间开始递减。点击主按钮“暂停”观察滚筒停止旋转状态灯变为橙色按钮文字变为“继续”。再次点击主按钮观察运行恢复进度继续推进。等待剩余时间归零页面应自动切换为“已完成”状态灯变蓝按钮变为“重置”。点击“重置”页面回到初始待机状态。尝试在运行中点击其他模式卡片应弹出提示且模式不切换。如果以上功能都正常那么一个完整的动态洗衣机控制面板就成功了。5. 常见问题与排查思路5.1 滚筒不旋转问题现象常见原因解决思路点击启动后滚筒没有旋转drum元素没有成功添加spinning类打开控制台检查元素类名变化动画定义了但不生效CSS 优先级被覆盖或动画属性名称拼写错误检查animation缩写是否包含名称和时长转筒旋转方向不符合预期默认旋转方向与设计预期相反调整keyframes中transform的角度方向排查时优先在浏览器开发者工具中确认drum的 class 是否包含spinning。如果 class 存在但不旋转说明 CSS 动画属性有问题。5.2 剩余时间不走或跳动异常问题现象常见原因解决思路定时器不触发setInterval被clearInterval提前清除在startTimer开头调用clearTimer避免重复创建剩余时间跳变浏览器后台节流或系统负载高使用时间戳计算而不是单纯依赖setInterval页面多个实例共用定时器变量作用域未隔离使用 IIFE 包裹代码确保定时器变量不泄漏到全局本文示例中setInterval每秒钟回调一次正常跑没问题。但如果你要做更严谨的计时器建议在startTimer中记录startTime Date.now()每次回调时基于startTime与duration计算剩余时间这样才能规避浏览器后台节流带来的偏差。5.3 模式切换被错误允许问题现象常见原因解决思路运行中点击模式页面无响应且无提示事件监听未绑定或closest选择器未命中检查modeList元素和.mode-item是否存在嵌套结构点击模式直接切过去了状态判断条件写得不对确认currentState在点击时是否被正确修改另一种做法是把模式选择按钮加上disabled属性运行中不可点击。视觉上会更清晰但需要额外处理disabled状态下的样式。5.4 小程序或 Vue 里怎么迁移如果你后续要把这个 Demo 迁移到 Vue 或 React需要注意setInterval的清理必须放在组件的beforeUnmount或useEffect的清理函数中防止组件销毁后定时器继续执行。状态管理建议改为组合式 API 或 Hooks把currentState、remainingSeconds作为响应式变量。CSS 动画部分基本可以直接复制但如果是小程序需要把部分 CSS 选择器改写为小程序支持的写法比如避免使用*全局选择器。6. 动态设计的工程化建议6.1 状态与 UI 分离在实际项目中不要让每个事件回调都直接操作 DOM。正确的做法是统一维护一个状态对象。所有界面更新走同一个渲染函数。状态变化通过事件或 Actions 触发。这样做的好处是当你需要接入真实设备数据时只需要把setInterval模拟的逻辑替换为 WebSocket 推送的数据界面部分不需要大改。6.2 动效性能注意事项动态设计最怕性能问题。当前示例中使用了transform、opacity和box-shadow三种属性。其中transform和opacity推荐使用因为浏览器会将其交给 GPU 合成层处理动画流畅box-shadow在复杂界面中可能引起重绘不宜大面积使用。如果后续增强动效建议优先考虑使用transform: translate/rotate/scale代替top/left/width/height动画。控制will-change的使用范围不要给所有元素添加。6.3 时间计算可靠性家用电器控制面板如果接入真实硬件剩余时间的准确度直接影响用户体验。建议在工程项目中做到记录任务开始时间戳而不是累加定时器次数。每次更新时计算真实剩余时间。支持“暂停累积”逻辑暂停时保存已运行时间。页面可见性变化时重新校正剩余时间。6.4 可访问性增强动态设计不应牺牲可访问性。当前 Demo 已经具备基础文案与状态灯双通道反馈但在生产环境还建议为状态变化添加aria-livepolite区域屏幕阅读器可以播报状态变化。为图标或色块增加文字说明避免用户只依赖颜色判断状态。按钮的:focus-visible样式应明显保证键盘操作可见。6.5 模块化扩展方向如果你想让这个 Demo 更接近真实智能家居控制端可以考虑以下扩展方向增加水温、转速、漂洗次数等参数调节。把模式配置改为后端接口下发前端动态渲染。使用 WebSocket 接入真实洗衣机状态上报。增加语音播报或消息通知模拟设备完成提醒。把面板封装为 Web Component复用多个页面。7. 总结与后续方向本文以“【动态设计】洗衣机创意”为切入点完整实现了一个具备动态视觉效果和交互逻辑的洗衣机控制面板。重点不是教你做一台洗衣机而是借助这个贴近生活的场景练习动效设计、状态管理、定时器控制、CSS 动画与 JavaScript 事件处理的综合能力。你可以直接从文章复制 HTML、CSS、JavaScript 三份代码在浏览器中运行并验证完整流程。后续如果想继续深入建议尝试把本文的代码迁移到 Vue 或 React 中并增加参数配置面板或者接入真实硬件上报数据这样你就掌握了一套从“静态页面”到“动态设备控制端”的完整实现思路。如果你在运行过程中遇到本文未覆盖的问题欢迎在评论区描述你的报错信息和浏览器版本我后续会整理补充到排错清单中。也可以把文章收藏备用方便以后做物联网控制面板时快速参考。
返回列表