Vue 3 + SVG :打造动态交互式智慧公厕可视化大屏
🚀 Vue 3 + SVG :打造“会呼吸”的智慧可视化大屏
在智慧城市建设的浪潮中,可视化大屏已成为展示数据的核心窗口。而在“智慧公厕”这一细分场景下,如何直观、实时、高保真地展示每个厕位的占用状态(有人/无人),是前端开发中一个既有趣又充满挑战的课题。
传统的做法往往是“切图一把梭”——使用多张图片进行绝对定位。但这种方式不仅适配性差(换个分辨率就由于),而且维护成本极高(加个厕位还得找 UI 重新切图)。
今天,我们将分享一种 基于 Vue 3 + 动态 SVG 的进阶方案,拒绝切图,直接操作矢量路径,实现一套高性能、任意缩放、毫秒级响应的厕位状态可视化系统。
💡 为什么选择 SVG?
相比于 Canvas 或位图,SVG 在这种场景下拥有降维打击般的优势:
- 💎 高保真矢量渲染:无论屏幕是 1080P 还是 4K,线条永远清晰锐利,告别锯齿。
- 🎮 动态 DOM 交互:SVG 本质上是 XML,注入页面后就是 DOM。这意味着我们可以像操作
<div>一样,用 CSS 和 JS 直接控制它的颜色、大小甚至形状。 - ⚡ 实时 WebSocket 驱动:后端状态一推,前端毫秒级变色,无需轮询,无需刷新。
- 🧩 低代码维护:设计师只需遵循简单的命名规范(如 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)。
核心逻辑步骤:
-
建立索引(Mapping):
首先,将后端返回的数组转换为Map结构。- 为什么? 数组查找元素需要遍历,时间复杂度是 O(n);而
Map基于哈希表,查找时间复杂度接近 O(1)。 - 我们将
stallNumber作为 Key,stallStatus作为 Value。
- 为什么? 数组查找元素需要遍历,时间复杂度是 O(n);而
-
单次遍历(Single Pass):
直接遍历页面上的 SVG 路径节点(svgPathList)。- 怎么做? 对于每一个 Path 节点,直接去
Map中询问:“我是 3 号坑位,现在有人吗?” - 结果:
Map会瞬间返回状态,无需再次遍历数据源。
- 怎么做? 对于每一个 Path 节点,直接去
-
状态驱动视图:
根据拿到的状态,动态修改fill属性,并配合 CSStransition实现丝滑的颜色过渡。
代码实现:
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 吧!

浙公网安备 33010602011771号