【ES6】day41-capstone-project

作者:mario 发布时间: 2026-08-27 阅读量:4 评论数:0

第四十一天:智能设备监控台 — 阶段1 毕业项目

蜗牛往上爬 · 前端工业可视化学习笔记 第 41 篇
16 周的终点站:把 Class、ES Module、async/await、Map/Set、DOM、防抖、CSS 动画全部装进一个工程——智能设备监控台。数据每 3 秒自动刷新、温度随机波动、超温告警呼吸灯闪烁、搜索防抖、告警历史滚动。这是你前端之路的第一个"完整作品"。


目录


一、项目需求

功能

实现技术

每 3 秒轮询刷新设备数据

setInterval + async/await

温度随机波动(模拟传感器)

Promise + setTimeout

统计面板(总数/运行/均温/告警)

reduce + filter

设备卡片网格 + hover 效果

CSS Grid + transition

超温告警卡片呼吸灯闪烁

CSS animation + class 切换

告警历史(最新 5 条)

数组 + 模板字符串

搜索(防抖 400ms)

闭包 + 高阶函数

多文件分层工程

ES Module


二、项目结构

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 周大屏的样式体系。


四、知识点对照表

知识点

用在

学于

Class + getter + static

Device 类

第 15 周

ES Module 分层

6 个模块

第 15 周

Promise + async/await

接口 + refresh

第 14 周

Map + Set + 模块私有

store 状态

第 13 周

防抖(闭包+高阶函数)

搜索框

第 8/16 周

解构 + 箭头函数 + 模板字符串

渲染层

第 11-12 周

filter/reduce/map

统计搜索

第 9/12 周

DOM + 事件

渲染交互

第 10 周

CSS 动画 + Grid

呼吸灯 + 网格

阶段0

一个项目,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 时,你会发现框架做的就是"把这张图自动化"。


六、扩展方向

  1. 点击卡片弹详情模态框(阶段0 CSS + 第 10 周事件委托)

  2. 告警声音(Audio API)

  3. 换成真实接口(fetch + response.ok + 降级策略,第 14 周)

  4. localStorage 持久化告警历史

  5. 暂停/恢复轮询开关

  6. 用 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 见!

评论