Vue 3 + SVG :打造动态交互式智慧公厕可视化大屏

🚀 Vue 3 + SVG :打造“会呼吸”的智慧可视化大屏

在智慧城市建设的浪潮中,可视化大屏已成为展示数据的核心窗口。而在“智慧公厕”这一细分场景下,如何直观、实时、高保真地展示每个厕位的占用状态(有人/无人),是前端开发中一个既有趣又充满挑战的课题。

传统的做法往往是“切图一把梭”——使用多张图片进行绝对定位。但这种方式不仅适配性差(换个分辨率就由于),而且维护成本极高(加个厕位还得找 UI 重新切图)。

今天,我们将分享一种 基于 Vue 3 + 动态 SVG 的进阶方案,拒绝切图,直接操作矢量路径,实现一套高性能、任意缩放、毫秒级响应的厕位状态可视化系统。


💡 为什么选择 SVG?

相比于 Canvas 或位图,SVG 在这种场景下拥有降维打击般的优势:

  1. 💎 高保真矢量渲染:无论屏幕是 1080P 还是 4K,线条永远清晰锐利,告别锯齿。
  2. 🎮 动态 DOM 交互:SVG 本质上是 XML,注入页面后就是 DOM。这意味着我们可以像操作 <div> 一样,用 CSS 和 JS 直接控制它的颜色、大小甚至形状。
  3. ⚡ 实时 WebSocket 驱动:后端状态一推,前端毫秒级变色,无需轮询,无需刷新。
  4. 🧩 低代码维护:设计师只需遵循简单的命名规范(如 ID 命名),前端即可自动识别并绑定数据,新增设备无需改代码。

🛠️ 技术实现:三步走战略

我们的实现逻辑非常清晰,分为三步:加载 -> 绑定 -> 驱动

第一步:动态加载 SVG 源码

为了能够操作 SVG 内部的节点,我们不能简单地使用 <img> 标签,因为 <img> 引入的 SVG 是作为一个整体“黑盒”渲染的,JS 无法触及其内部灵魂。

目标:将 SVG 文件作为 XML 字符串获取,并注入到页面 DOM 中。

代码实现:

<!-- 容器,用于承载 SVG DOM -->
<div class="toilet">
  <div class="svgObject" v-html="svgContent" ref="svgContainer"></div>
</div>
const svgContent = ref("");
const svgContainer = ref(null);

// 核心方法:加载 SVG
const loadSVG = async (url) => {
  try {
    // 1. 发起 HTTP 请求,获取 SVG 文件的纯文本内容
    // 这里的 getSvgUrl 是封装好的 axios 请求
    const response = await getSvgUrl(url);
    const svgText = await response;

    // 2. 利用 v-html 将 SVG 字符串“注入”到 DOM 中
    svgContent.value = svgText;

    // 🌟 关键点:等待 Vue 完成 DOM 更新
    // 因为 v-html 的渲染是异步的,必须 await nextTick() 才能确保 DOM 节点已经存在
    await nextTick();

    // 3. SVG 加载完毕,开始初始化路径绑定
    initSvgPaths();
  } catch (error) {
    console.error("SVG加载失败:", error);
  }
};

第二步:智能解析与绑定

SVG 注入后,它还只是一堆静态的标签。我们需要找到那些代表“厕位”的 path 标签,把它们提取出来。

约定:设计师在绘制 SVG 时,将每个厕位的 path 元素的 id 设置为对应的业务编号(如 "1", "2", "1-1")。

目标:提取出所有具有有效 ID 的 path 节点,存入数组备用。

代码实现:

const svgPathList = ref([]);

const initSvgPaths = () => {
  // 1. 就像操作普通 HTML 一样,获取所有 path 标签
  const paths = svgContainer.value.querySelectorAll("path");

  // 2. 筛选出所有“智能厕位”节点
  svgPathList.value = Array.from(paths).filter(
    (path) =>
      // 🛡️ 正则过滤:只保留 ID 为数字或带连字符的节点
      // 这一步非常重要,能排除掉背景、装饰线条等无关元素,避免误操作
      path.id && /^[\d-]+$/.test(path.id)
  );
  console.log("SVG路径加载完成", svgPathList.value);
};

第三步:WebSocket 实时驱动(含性能优化)

这里是最核心的部分。当 WebSocket 推送最新的状态数据时,我们需要实时更新 SVG 的颜色。这里看似简单,实则隐藏着性能陷阱。

❌ 初级写法(踩坑版)

第一次的时候是写的写双重循环:遍历后端返回的数据,然后针对每一条数据去遍历 DOM 节点寻找匹配的 ID,找到后然后去更新颜色。

// 🚫 糟糕的实现:O(n*m) 复杂度
// 假设有 50 个厕位,后端推送了 50 条数据,这里就要执行 2500 次判断
// 这个方式是接收websocket推送的消息,然后针对每一个消息做处理,大家可以自行封装一个websocket请求然后绑定处理函数
const handleNewMessage = (data) => {
  if (data.type === "STALL" && data.code == 200) {
    const stallData = data.data;
    for (let i = 0; i < stallData.value.length; i++) {
        // 遍历后端数据,针对每一个厕位做是否有人判断,
        // 如果是“2”(有人),则遍历所有 SVG 路径,找到匹配 ID 的那个,更新颜色为 "#F98DB1"
      if (stallData.value[i].stallStatus == "2") {
        const targetPath = svgPathList.value.forEach((path) => {
          if (path.id === stallData.value[i].stallNumber) {
            path.style.fill = "#F98DB1";
          } else {
            path.style.fill = "#0FE7FC";
          }
        });
      }
    }
  }
};

这种写法不仅性能随着节点数量增加而指数级下降(O(n²)),而且容易出现逻辑漏洞:如果后端数据有重复或顺序问题,可能会导致状态被错误覆盖。

✅ 进阶写法(优化版)

为了实现极致性能,我们采用“空间换时间”的策略,将算法复杂度降维到 O(n)。

核心逻辑步骤:

  1. 建立索引(Mapping)
    首先,将后端返回的数组转换为 Map 结构。

    • 为什么? 数组查找元素需要遍历,时间复杂度是 O(n);而 Map 基于哈希表,查找时间复杂度接近 O(1)。
    • 我们将 stallNumber 作为 Key,stallStatus 作为 Value。
  2. 单次遍历(Single Pass)
    直接遍历页面上的 SVG 路径节点(svgPathList)。

    • 怎么做? 对于每一个 Path 节点,直接去 Map 中询问:“我是 3 号坑位,现在有人吗?”
    • 结果Map 会瞬间返回状态,无需再次遍历数据源。
  3. 状态驱动视图
    根据拿到的状态,动态修改 fill 属性,并配合 CSS transition 实现丝滑的颜色过渡。

代码实现:

const handleNewMessage = (data) => {
  if (data.type === "STALL" && data.code == 200) {
    const stallData = data.data;

    // ------------------------------------------------------
    // 步骤 1: 构建高效查找表 (Lookup Table)
    // ------------------------------------------------------
    // 将数组 [ { stallNumber: "1", stallStatus: "2" }, ... ]
    // 转换为 Map { "1" => "2", ... }
    const statusMap = new Map(
      stallData.map((item) => [item.stallNumber, item.stallStatus])
    );

    // ------------------------------------------------------
    // 步骤 2: 遍历 DOM 节点,O(1) 读取状态
    // ------------------------------------------------------
    svgPathList.value.forEach((path) => {
      // 核心优化:直接通过 ID 从 Map 中取值,无需循环查找
      const status = statusMap.get(path.id);

      // ----------------------------------------------------
      // 步骤 3: 响应式更新视图
      // ----------------------------------------------------
      if (status === "2") { 
        // 🌸 状态 2:有人 (粉色)
        path.style.fill = "#F98DB1";
        path.style.transition = "fill 0.5s ease"; // 加上过渡,体验瞬间提升
      } else {
        // 💧 其他状态:无人 (青色)
        // 注意:这里包含了 status 为 undefined 的情况(即数据中未包含该坑位),默认置为无人
        path.style.fill = "#0FE7FC";
      }
    });
  }
};

此优化方案确保了无论有多少个坑位,更新逻辑都像“点名”一样快,不会随着数据量增长而卡顿,更加一般化。
说明一下哦,这个需要改变颜色的状态这里我跟后端约定的是2,其他状态默认是无人。


✨ 最终效果

1.识别智能厕位控制台打印结果如图!(https://img2024.cnblogs.com/blog/2819675/202601/2819675-20260115135236034-274228499.png)
2.初始公测图片!(https://img2024.cnblogs.com/blog/2819675/202601/2819675-20260115135527559-1121130856.png)
3.有人进入后图片!(https://img2024.cnblogs.com/blog/2819675/202601/2819675-20260115140745975-1728210400.png)

通过这套方案,我们实现了一个有生命力的公厕平面图:

  • 默认状态:所有厕位静谧地呈现为 科技青(#0FE7FC)
  • 有人进入:传感器触发,WebSocket 消息瞬间抵达,对应的厕位平滑过渡为 醒目粉(#F98DB1)
  • 无损缩放:无论是在 80 寸的指挥中心大屏,还是在 手机端查看,线条永远清晰,体验拉满。

🚀 总结

在 Vue 3 项目中,将 SVG 视为“可编程代码”而非“静态图片”,能极大地拓展前端可视化的边界,svg图片跟切图人员约定号相关图形编号规则后可以上传后台,前端根据svg的url获取,这个项目涉及到100多个公厕,所以涉及到的编号规则一定要统一准确。

这种 “SVG DOM + 数据驱动” 的模式,不仅完美解决了智慧公厕的痛点,还可以广泛“复制粘贴”到其他领域:

  • 🚗 智慧停车:车位占用监控
  • 🏭 工业互联网:流水线设备故障红绿灯
  • 🏥 智慧医疗:病房床位管理系统

掌握这一招,让你的可视化大屏瞬间“活”起来!拒绝死板的切图,拥抱灵动的 SVG 吧!

posted @ 2026-01-15 14:19  此颜差矣。  阅读(5)  评论(0)    收藏  举报