🚀 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 吧!