简介:这份资源面向电力、油田燃气、供水管网等工业自动化领域的 Web SCADA 前端开发者,提供一套基于 HTML5 Canvas 的电力接线系统图实现方案。在 Flex、Silverlight 等重客户端方案逐渐退场、SVG 在大数据量场景下性能受限的背景下,Canvas 已成为当今 Web SCADA 前端技术的首选标配,本资源正是这一技术路线的实践参考。压缩包为 rar 格式,共 43 个文件,约 434KB,其中 27 个 js 文件承担核心绘图与交互逻辑,7 个 json 与 7 个 png 分别用于图元配置数据和电力符号图标资源,另有 1 个 html 入口页面串联整体演示。资源围绕电力符号、隧道图标、地线、箭头、指示灯等图元展开,包含 lightBling 等模块化组件,目录结构清晰,便于按图元类型检索与二次开发。目前已有 3519 人学习下载,适合具备一定前端基础、希望快速搭建 Web SCADA 电力接线图原型的开发者参考借鉴。
1. Web SCADA 电力接线系统图:为什么浏览器里画一张“活”的单线图这么难
很多做电力监控的团队都遇到过这个场景:现场有一套配电柜,柜门上贴着一张 CAD 打印的单线图,值班员拿着对讲机报“2 号进线开关跳了”,调度端的人却要翻半天图纸才能对上位置。于是大家想当然地认为,把这张图搬到浏览器里,做成 Web SCADA 电力接线系统图,不就能点一下开关、实时看电流了吗?真动手才发现,难点根本不在“画图”,而在于这张图必须同时是画面、是数据模型、是通信入口,还得在浏览器里跑得动、连得稳、改得快。
Web SCADA 电力接线系统图,本质是用 Web 技术(HTML/SVG/Canvas + 前端框架)承载传统组态软件里的电力单线图,把断路器、隔离开关、母线、变压器、馈线这些图元,和实时遥测、遥信、遥控点位绑定,再通过 WebSocket 或 SSE 把后台采集数据推到浏览器。它解决的是“跨地域、免安装、多终端看同一张图”的问题,适合配电运维、新能源场站监控、工业园区能源管理这类需要远程集中监视的场景。下面按“先立模型、再画图、后接数据、最后避坑”的顺序,把可复现的路径讲清楚。
2. 先定图元模型与坐标体系:别让接线图变成一张死图片
2.1 为什么不能直接用一张 SVG 背景图
最常见的翻车做法,是让美工导出一张 SVG 或 PNG 当背景,然后在上面绝对定位一堆透明 div 当热区。图是好看,但设备一多就崩:缩放后热区错位、设备增删要重新导图、不同电压等级没法分层过滤。更致命的是,这张图没有“模型”,后台推来一个点位YC_101,前端根本不知道它对应图上哪个开关。
正确做法是把接线图拆成“图元实例 + 拓扑关系 + 点位绑定”三层。图元实例描述“这里有一个断路器,坐标 (x, y),旋转 0 度”;拓扑关系描述“这个断路器连在哪条母线和哪条馈线之间”;点位绑定描述“这个断路器的遥信是哪个 tag、遥测是哪个 tag”。这样图才能“活”。
2.2 用 JSON 描述一张最小单线图
下面是一个可直接抄的图元模型,用 JSON 描述一条进线、一个断路器、一段母线、一条出线。字段命名我一般用type、id、points、tags四件套,够用且好扩展。
{ "canvas": { "width": 1200, "height": 800, "grid": 10 }, "symbols": [ { "id": "bus_1", "type": "busbar", "points": [[200, 300], [900, 300]], "voltageLevel": "10kV", "tags": {} }, { "id": "cb_101", "type": "breaker", "points": [[300, 300], [300, 400]], "rotate": 0, "tags": { "yx": "DEV_CB101_STATE", // 遥信:分/合 "yc": "DEV_CB101_IA", // 遥测:A 相电流 "yk": "DEV_CB101_CMD" // 遥控:分合闸命令 } }, { "id": "line_201", "type": "feeder", "points": [[300, 400], [300, 600]], "voltageLevel": "10kV", "tags": { "yc": "LINE201_P" } } ], "links": [ { "from": "bus_1", "to": "cb_101" }, { "from": "cb_101", "to": "line_201" } ] }逻辑说明:canvas定义画布尺寸和网格,网格用于吸附对齐,避免手工拖拽后线条歪斜。symbols是图元数组,每个图元有唯一id、类型、几何点、电压等级和点位映射。links描述拓扑连接,用于后续做带电着色和拓扑分析。参数上,points用绝对坐标而不是相对坐标,是为了让缩放时只改 viewBox,不改模型;tags里yx/yc/yk分别对应遥信、遥测、遥控,后台按这个 key 推数据即可。
2.3 坐标体系与缩放:viewBox 是后悔药
SVG 的viewBox是这套方案里最值得早定的一件事。把画布逻辑坐标固定为 1200×800,所有图元按这个坐标系画,缩放时只改viewBox的宽高和偏移,图元坐标永远不动。这样导出、打印、大屏适配都不用重算坐标。如果一开始就用屏幕像素当坐标,后面换分辨率就是血泪经验。
<svg id="scada" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid meet"> <!-- 图元由 JS 动态插入 --> </svg>preserveAspectRatio="xMidYMid meet"保证等比缩放且居中,大屏和笔记本上都不会拉伸变形。网格吸附在拖拽结束时做一次Math.round(x / grid) * grid即可,不要每帧都吸附,否则拖拽手感会发涩。
3. 用 SVG + 前端框架渲染图元:让开关真的能点、能变色
3.1 图元渲染:一个断路器组件该长什么样
断路器在单线图里通常是一个矩形加一个斜杠,分合状态用颜色区分。我一般把每个图元做成一个渲染函数,输入模型,输出 SVG 片段。下面用原生 JS 写一个最小断路器渲染,不依赖框架,方便你移植到 Vue 或 React。
function renderBreaker(sym) { const [x1, y1] = sym.points[0]; const [x2, y2] = sym.points[1]; const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const state = sym.state || 'open'; // open / closed const color = state === 'closed' ? '#00b050' : '#c00000'; return ` <g class="symbol breaker">const domCache = new Map(); function initSymbols(symbols) { const layer = document.getElementById('symbolLayer'); symbols.forEach(sym => { layer.insertAdjacentHTML('beforeend', renderBreaker(sym)); domCache.set(sym.id, layer.lastElementChild); }); } function updateBreaker(id, state) { const el = domCache.get(id); if (!el) return; const color = state === 'closed' ? '#00b050' : '#c00000'; el.querySelector('rect').setAttribute('stroke', color); el.querySelector('line:last-of-type').setAttribute('stroke', color); }逻辑说明:domCache用图元 id 做 key,避免每次querySelector遍历。updateBreaker只改两个属性,浏览器重绘成本极低。参数上,如果点位量超过 2000,建议把更新合并到requestAnimationFrame里批量执行,否则频繁 setAttribute 也会触发多次样式计算。
3.3 带电着色与拓扑:从 links 推颜色
单线图里“哪段带电”是值班员最关心的信息。有了links,可以从电源点做一次广度优先遍历,把连通的母线、线路标成带电色。下面是一个简化实现。
function calcEnergized(symbols, links, sourceIds) { const graph = new Map(); links.forEach(l => { if (!graph.has(l.from)) graph.set(l.from, []); if (!graph.has(l.to)) graph.set(l.to, []); graph.get(l.from).push(l.to); graph.get(l.to).push(l.from); }); const energized = new Set(); const queue = [...sourceIds]; while (queue.length) { const cur = queue.shift(); if (energized.has(cur)) continue; energized.add(cur); (graph.get(cur) || []).forEach(nxt => { const sym = symbols.find(s => s.id === nxt); // 只有闭合的开关才导通 if (sym && sym.type === 'breaker' && sym.state !== 'closed') return; queue.push(nxt); }); } return energized; }逻辑说明:先把links转成邻接表,再从电源点 BFS。遇到断路器且状态不是closed就跳过,模拟“断开点阻断导通”。返回的energized集合用于给图元加energizedclass,CSS 里统一改颜色。参数上,sourceIds一般来自进线柜或变压器高压侧,实际项目里可能多个电源,全部塞进队列即可。
4. 接实时数据:WebSocket 点位推送与前端绑定
4.1 通信选型:WebSocket 还是 SSE
Web SCADA 对实时性要求通常在秒级,WebSocket 双向、SSE 单向但更简单。如果只做监视不做遥控,SSE 足够且断线重连逻辑更省心;如果要做遥控下发,WebSocket 更合适。我一般默认 WebSocket,因为后面加遥控不用换协议。
let ws; let reconnectTimer; function connect(url) { ws = new WebSocket(url); ws.onopen = () => { console.log('SCADA link up'); ws.send(JSON.stringify({ type: 'subscribe', tags: getAllTags() })); }; ws.onmessage = (evt) => { const msg = JSON.parse(evt.data); if (msg.type === 'update') { msg.points.forEach(p => dispatchPoint(p.tag, p.value)); } }; ws.onclose = () => { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => connect(url), 3000); }; }逻辑说明:连接建立后先发订阅消息,只订阅当前画面上用到的 tag,避免推全量。dispatchPoint根据 tag 找到绑定的图元并更新。断线后 3 秒重连,这是经验值,太短会在服务端重启时疯狂重试,太长值班员会觉得“卡住了”。参数上,getAllTags()从模型里收集所有tags值去重即可。
4.2 点位分发:tag 到图元的反向索引
如果每次收到点位都遍历所有图元找绑定,点位一多就是 O(n²)。初始化时建一个tag -> [symbolId]的反向索引,更新时直接查。
const tagIndex = new Map(); function buildTagIndex(symbols) { symbols.forEach(sym => { Object.values(sym.tags || {}).forEach(tag => { if (!tagIndex.has(tag)) tagIndex.set(tag, []); tagIndex.get(tag).push(sym.id); }); }); } function dispatchPoint(tag, value) { const ids = tagIndex.get(tag) || []; ids.forEach(id => { const sym = symbolMap.get(id); if (sym.type === 'breaker') updateBreaker(id, value === 1 ? 'closed' : 'open'); if (sym.type === 'feeder') updateFeederValue(id, value); }); }逻辑说明:buildTagIndex在模型加载后调用一次,把每个 tag 映射到所有引用它的图元。dispatchPoint只更新相关图元,与画面规模无关。参数上,遥信值约定 1 为合、0 为分,遥测直接显示数值,单位在模型里加unit字段。
4.3 数值显示与格式化:别让 0.30000000000000004 上屏
浮点误差在电力数据里很常见,直接toFixed(2)又可能把 0.005 显示成 0.01。我一般按量程决定小数位,电流两位、电压一位、功率一位,并在模型里存precision。
function formatValue(value, precision = 2) { if (value === null || value === undefined || Number.isNaN(value)) return '--'; return Number(value).toFixed(precision); }逻辑说明:无效值显示--而不是 0,避免值班员误判。precision从模型读,不同设备可以不同。参数上,Number(value)先转数字再 toFixed,防止字符串直接调用报错。
5. 避坑与排查:接线图上线后最容易翻车的 5 个点
5.1 图元点击无响应,但控制台没报错
现象:点击断路器没反应,onclick不触发。原因:SVG 里后插入的图元被透明层或pointer-events: none的父级挡住,或者事件绑在了svg上但target是line而不是g。解决:用事件委托绑在svg根节点,处理时evt.target.closest('.symbol'),并检查父级没有pointer-events: none。
5.2 缩放后热区错位
现象:放大后点击位置和图形对不上。原因:用了屏幕像素坐标做热区,或者 CSStransform: scale和 SVGviewBox混用。解决:统一用viewBox缩放,热区就是图元本身,不要额外叠 div。如果必须叠,用getScreenCTM()换算坐标。
5.3 点位更新导致页面卡顿
现象:数据一多,页面滚动都卡。原因:每个点位单独触发 DOM 更新,或者用了innerHTML重建。解决:建domCache,只改属性;更新合并到requestAnimationFrame;点位超过 2000 时考虑 Canvas 渲染或分层渲染。
5.4 断线重连后数据不刷新
现象:网络恢复后图还是旧的。原因:重连后没有重新订阅,或者服务端认为旧连接还在。解决:onopen里重新发订阅;服务端用连接 id 做会话,重连即新会话;前端重连后主动拉一次全量快照。
5.5 遥控误操作没有二次确认
现象:点一下开关就下发分闸。原因:遥控按钮直接绑了发送逻辑。解决:遥控必须二次确认,弹窗显示设备名和操作类型,确认后才发;后台再做一次权限和闭锁校验。这是电力行业的基本要求,前端不能省。
6. 进阶:用快照回放验证接线图状态机
接线图做完了,怎么证明它“对”?我一般会加一个快照回放功能:把一段时间内的点位变化按时间戳存下来,回放时按时间轴驱动图元状态,肉眼比对每一步是否符合预期。这比写单元测试更直观,也更容易发现拓扑着色和开关联动的逻辑错误。
const snapshots = []; // { ts, points: [{tag, value}] } function replay(snapshots, speed = 1) { let i = 0; const startTs = snapshots[0].ts; const timer = setInterval(() => { const elapsed = (Date.now() - startTs) * speed; while (i < snapshots.length && snapshots[i].ts - startTs <= elapsed) { snapshots[i].points.forEach(p => dispatchPoint(p.tag, p.value)); i++; } if (i >= snapshots.length) clearInterval(timer); }, 100); }逻辑说明:snapshots按时间戳升序存,回放时用elapsed对齐,speed控制倍速。dispatchPoint复用实时更新逻辑,保证回放和实时走同一条代码路径。参数上,setInterval100ms 是刷新粒度,够用且不费 CPU;speed设 1 是实时,设 10 是十倍速排查。
一个我踩过的坑:早期把回放和实时更新写成两套逻辑,结果回放正常、实时翻车,排查半天才发现是实时路径少了一个状态判断。后来强制两者共用dispatchPoint,这类玄学问题就没了。做 Web SCADA 电力接线系统图,最值得养成的习惯是“模型先行、渲染后置、更新只改属性”,图元模型定好了,后面换框架、换协议、换大屏都只是换皮。希望帮到你。
本文还有配套的精品资源,点击获取