第四十一天:智能设备监控台 — 阶段1 毕业项目
蜗牛往上爬 · 前端工业可视化学习笔记 第 41 篇
16 周的终点站:把 Class、ES Module、async/await、Map/Set、DOM、防抖、CSS 动画全部装进一个工程——智能设备监控台。数据每 3 秒自动刷新、温度随机波动、超温告警呼吸灯闪烁、搜索防抖、告警历史滚动。这是你前端之路的第一个"完整作品"。
目录
一、项目需求
二、项目结构
device-monitor/
├── index.html # 页面骨架
├── css/style.css # 深色工业风样式
└── js/
├── app.js # 入口:初始化 + 事件 + 轮询
├── classes/
│ └── Device.js # 设备模型(Class + getter + 静态方法)
├── services/
│ ├── api.js # 模拟异步接口(Promise)
│ └── store.js # 状态管理(Map + Set + 模块私有)
├── utils/
│ ├── format.js # 格式化工具
│ └── throttle.js # 防抖
└── views/
└── render.js # 渲染层(DOM + 模板字符串)
分层原则:数据自上而下(api → store → render),依赖单向清晰——入口组装一切。
三、核心代码
模型层:Device 类
export default class Device {
constructor(id, name, temp, status = "运行中") {
this.id = id;
this.name = name;
this.temp = temp;
this.status = status;
}
// getter 派生值:温度等级
get level() {
if (this.temp >= 80) return "危险";
if (this.temp >= 60) return "偏高";
return "正常";
}
// getter:是否告警
get isAlert() {
return this.temp >= 80;
}
// 静态工厂方法:从接口数据创建实例
static from(data) {
return new Device(data.id, data.name, data.temp, data.status);
}
}
数据层:模拟接口
const DEVICES = [
{ id: "CNC-001", name: "数控机床-1", temp: 65, status: "运行中" },
{ id: "CNC-002", name: "数控机床-2", temp: 72, status: "运行中" },
{ id: "AGV-001", name: "搬运车-1", temp: 42, status: "运行中" },
{ id: "AGV-002", name: "搬运车-2", temp: 38, status: "待机" },
{ id: "ARM-001", name: "机械臂-1", temp: 55, status: "运行中" },
{ id: "ARM-002", name: "机械臂-2", temp: 81, status: "运行中" }
];
// 模拟接口:温度在原值 ±5 波动
export function fetchDevices() {
return new Promise(resolve => {
setTimeout(() => {
const data = DEVICES.map(d => ({
...d,
temp: Math.max(20, Math.min(95, d.temp + (Math.random() * 10 - 5)))
}));
resolve(data);
}, 300);
});
}
状态层:store(Map + Set + 私有数据)
import Device from "../classes/Device.js";
// ⭐ 模块私有:外部无法直接篡改
const deviceMap = new Map(); // id → Device 实例
const alertSet = new Set(); // 当前告警设备 id
const alertLogs = []; // 告警历史
export function syncDevices(list) {
const newAlerts = [];
for (const item of list) {
const device = Device.from(item);
const wasAlert = alertSet.has(device.id);
deviceMap.set(device.id, device);
// 告警状态变化 → 记录日志(进入/恢复)
if (device.isAlert && !wasAlert) {
alertSet.add(device.id);
newAlerts.push({ ...device, time: new Date() });
} else if (!device.isAlert && wasAlert) {
alertSet.delete(device.id);
}
}
if (newAlerts.length) alertLogs.push(...newAlerts);
return newAlerts;
}
export function getDevices(keyword = "") {
const all = [...deviceMap.values()];
if (!keyword) return all;
return all.filter(d =>
d.id.toLowerCase().includes(keyword.toLowerCase()) ||
d.name.includes(keyword));
}
export function getStats() {
const all = [...deviceMap.values()];
return {
total: all.length,
running: all.filter(d => d.status === "运行中").length,
avgTemp: all.length
? (all.reduce((s, d) => s + d.temp, 0) / all.length).toFixed(1) : 0,
alertCount: alertSet.size
};
}
export function getRecentAlerts() {
return [...alertLogs].reverse().slice(0, 5);
}
工具层:防抖(闭包 + 高阶函数)
export function debounce(fn, delay = 400) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
视图层:render(模板字符串 + 解构)
import { formatTemp, formatTime } from "../utils/format.js";
export function renderStats(stats, container) {
const cards = [
{ label: "设备总数", value: stats.total },
{ label: "运行中", value: stats.running },
{ label: "平均温度", value: `${stats.avgTemp}°C` },
{ label: "告警设备", value: stats.alertCount, alert: stats.alertCount > 0 }
];
container.innerHTML = cards.map(c => `
<div class="stat-card ${c.alert ? "alerting" : ""}">
<span>${c.label}</span><strong>${c.value}</strong>
</div>
`).join("");
}
export function renderDevices(devices, container) {
container.innerHTML = devices.map(d => `
<div class="device-card ${d.isAlert ? "alert" : ""}">
<h3>${d.id}</h3>
<div>${d.name}</div>
<div class="temp">${formatTemp(d.temp)}</div>
<div class="meta">${d.status} · ${d.level}</div>
</div>
`).join("");
}
export function renderAlerts(alerts, container) {
if (!alerts.length) {
container.innerHTML = `<li style="border-color:#00ff88;background:rgba(0,255,136,0.05)">✅ 暂无告警</li>`;
return;
}
container.innerHTML = alerts.map(a => `
<li>
<span class="time">${formatTime(a.time)}</span>
⚠ ${a.name}(${a.id})温度达到 ${formatTemp(a.temp)},请检查!
</li>
`).join("");
}
入口:app.js(async/await + 轮询)
import { fetchDevices } from "./services/api.js";
import { syncDevices, getDevices, getStats, getRecentAlerts } from "./services/store.js";
import { renderStats, renderDevices, renderAlerts } from "./views/render.js";
import { debounce } from "./utils/throttle.js";
const statsPanel = document.getElementById("statsPanel");
const deviceGrid = document.getElementById("deviceGrid");
const alertList = document.getElementById("alertList");
const searchInput = document.getElementById("searchInput");
// 刷新数据 + 渲染
async function refresh() {
try {
const data = await fetchDevices(); // 1. 拉数据
syncDevices(data); // 2. 更新状态
renderStats(getStats(), statsPanel); // 3. 渲染统计
renderDevices(getDevices(searchInput.value.trim()), deviceGrid);
renderAlerts(getRecentAlerts(), alertList);
} catch (err) {
console.error("刷新失败:", err.message);
}
}
// 搜索防抖
searchInput.addEventListener("input", debounce(e => {
renderDevices(getDevices(e.target.value.trim()), deviceGrid);
}));
// 每 3 秒轮询
setInterval(refresh, 3000);
refresh();
CSS 部分(深色工业风 + 呼吸灯动画)见学习笔记
week16-advanced-capstone.md,或直接复用阶段0 第 6 周大屏的样式体系。
四、知识点对照表
一个项目,16 周知识全串联。
五、数据流转全景图
setInterval (3s)
│
▼
app.refresh() ──── await ────► api.fetchDevices() [Promise 模拟接口]
│ │ 温度波动数据
▼ ▼
store.syncDevices(data) ◄────────────┘
│ Map 更新 + Set 告警判断 + 日志记录
▼
getStats() / getDevices(keyword) / getRecentAlerts()
│ 纯读取,返回派生数据
▼
render.renderStats / renderDevices / renderAlerts
│ 模板字符串拼 HTML
▼
innerHTML 更新页面 → CSS 动画(呼吸灯/滑入)
记住这张图——未来学 Vue/React 时,你会发现框架做的就是"把这张图自动化"。
六、扩展方向
点击卡片弹详情模态框(阶段0 CSS + 第 10 周事件委托)
告警声音(Audio API)
换成真实接口(fetch + response.ok + 降级策略,第 14 周)
localStorage 持久化告警历史
暂停/恢复轮询开关
用 Proxy 拦截 temp 修改,自动触发告警(预习 Vue3 响应式)
七、阶段1 毕业总结
16 周走过来的路
第 7 周 环境与类型 → 会运行、会声明、懂 ===
第 8 周 流程控制与函数 → 程序有了逻辑,函数有了封装
第 9 周 对象数组 JSON → 数据有了结构
第 10 周 原型 DOM 事件 → 页面有了交互(第一个增删改查)
第 11 周 let/const 解构 → ES6 起航
第 12 周 箭头函数 扩展 → 代码现代化
第 13 周 对象扩展 Set/Map → 数据结构版图补齐
第 14 周 异步编程 → 大屏命脉打通
第 15 周 Class 模块化 → 工程化地基
第 16 周 进阶 + 毕业项目 → 一切融会贯通
你现在拥有的能力
语法地基:ES5 + ES6 全覆盖,能读懂任何现代 JS 代码
工程思维:分层架构、模块化、单向数据流
异步能力:Promise / async/await / 并发控制 / 降级策略
调试能力:DevTools 定位问题、模块化排查
作品集:大屏静态页(阶段0)+ 设备管理系统 + 智能监控台(阶段1)
数据
16 周 | 41 篇博客 | 3 个完整项目 | 100+ 知识点 | 50+ 练习
下一站
阶段2:智慧工厂大屏项目(0-3 个月)——Canvas 图表、ECharts、WebSocket 实时数据、Vue3 框架。你的第一个求职名片即将诞生。
🎓 阶段1 毕业!从"什么是变量"到"分层架构的实时监控台",你用了 16 周。保持节奏,阶段2 见!