基于Vue的智慧水务管网泄漏检测系统实战:从架构到算法
简介这是一份基于Vue.js开发的智慧水务系统前端项目源码围绕泄漏监测等水务业务场景设计适合有一定前端基础的开发者用于实战进阶或毕业设计参考。项目完整覆盖组件化开发、路由配置、数据请求、状态管理以及环境变量切换等常见工程化环节为理解模块划分和协作规范提供了真实范例可以快速了解从页面搭建到数据展示的完整链路。资源包共229个文件其中45个Vue文件组成业务组件78个CSS文件定制日期选择、表格、分页等界面样式31个JS文件实现交互逻辑。另含16个SVG、28个PNG、7个GIF等可视化素材及多种字体文件整包仅2MB轻量且易于下载阅读。目前已有740人学习下载适合通过真实项目源码掌握组件复用、接口封装、目录划分等实用技巧。研读这份代码还能借鉴智慧水务场景中数据筛选、图表联动和响应式布局的落地方式对系统提升Vue开发能力颇有帮助。1. 为什么智慧水务系统里最先要做的是 Leakage做过水务系统的人都知道一个项目里最容易让甲方拍桌子验收不通过的不是报表不漂亮也不是地图不流畅而是漏水检测这一栏。管网里每天漏掉多少水哪个分区夜间流量异常这些指标一旦算错前面做得再花哨的界面都白搭。vueLeakage 这个名字看着像个普通 Vue 项目但它真正解决的问题是让监测人员在前端页面上直接判断“哪里漏了、漏了多少、要不要派人去现场”而不是每天对着 Excel 和曲线图做人工脑补。这套系统适合两类人。一类是水务企业里做管网监测的工程师手上有表计数据、有 SCADA 接口缺的是一个能快速响应、能把阈值规则讲清楚的展示终端另一类是接智慧城市项目的开发团队前端栈选了 Vue但不确定泄漏检测这块到底该在前端算多少、后端算多少想找一套不踩坑的最小实现做参考。我按自己做过的一个模拟项目X的套路来讲架构怎么搭、夜间最小流量怎么算、告警怎么推、参数怎么设最后把你大概率会翻车的几个地方提前列出来。看完你能直接动手搭一个跑得通的 Demo再决定要不要往生产环境推。2. 前端架构与数据链路Vue 项目里先分清“展示层”和“判断层”2.1 泄漏检测的前后端职责边界智慧水务系统的数据链路大体是三段表计/传感器采集 → 平台解析存储 → 前端展示与判定。很多团队把判断逻辑全部压在后端接口里前端只拿结论也有团队做得太激进把检测算法整个塞进浏览器。我倾向于取中间实时性要求高的判定比如瞬时流量高报放前端基于历史数据的趋势判定比如夜间最小流量对比、连续多日均值偏离放后端接口前端做二次加工。为什么这样切因为浏览器端拿到的数据永远是被过滤过的你不可能让前端直连数据库去跑 30 天的时序聚合那样页面会卡成 PPT。但反过来如果每次判定都要等后端重新算完再推给前端夜间突发爆管这类场景下你会损失至少 30 秒的处理时间这 30 秒可能就是几十吨水。我的做法是后端起一个 WebSocket 或者 SSE 通道推实时数据流前端维护最近若干小时的数据窗口泄漏检测的快速判定放在前端慢速趋势分析走后端定时任务。前端项目结构上我一般会按业务域拆模块而不是按页面拆。下面是一个参考的最小目录组织src/ api/ # 后端 HTTP 接口封装点位列表、历史曲线、工单提交 ws/ # WebSocket 连接管理、消息分发、重连逻辑 views/ Dashboard/ # 总览大屏分区压力、流量、漏损率 Monitor/ # 分区监测实时曲线 泄漏判定结果 Alarm/ # 告警列表与处置记录 leakage/ detector.js # 夜间最小流量法实现 thresholds.js # 阈值配置与读取 eventBus.js # 前端事件总线连接实时数据和 UI 组件 components/ ChartLine/ # 基于 ECharts 的流量曲线组件 MapPanel/ # 分区地图与点位标记这个结构的关键在leakage/目录它承担的是“判断层”的职责。注意不要把阈值规则散落在各个 Vue 组件里那样后期调参能把你调疯掉。统一收敛到thresholds.js每个参数带默认值运行时从后端拉最新配置覆盖。2.2 WebSocket 链路实时数据能推多快、丢了怎么办泄漏检测的前提是数据要准、要快。SCADA 系统的表计数据一般是分钟级上报个别重点点位能做到 10 秒级。Vue 前端接实时数据最稳的方案是 WebSocket接口地址由后端调度中心统一暴露前端只管订阅和解包。下面这段是一个我常用的连接封装放在ws/index.js里class LeakWS { constructor({ url, heartbeatInterval 30000, onMessage, onStatusChange }) { this.url url this.ws null this.heartbeatTimer null this.reconnectAttempts 0 this.onMessage onMessage this.onStatusChange onStatusChange } connect() { this.ws new WebSocket(this.url) this.ws.onopen () { this.reconnectAttempts 0 this.startHeartbeat() this.onStatusChange this.onStatusChange(online) // 订阅需要的点位msgType 由后端协议定义 this.ws.send(JSON.stringify({ type: subscribe, points: [DMA_001, DMA_002, DMA_003] })) } this.ws.onmessage (event) { const raw JSON.parse(event.data) // 统一交给 leakage/eventBus 分发不在这里做业务判断 this.onMessage this.onMessage(raw) } this.ws.onclose () { this.stopHeartbeat() this.onStatusChange this.onStatusChange(offline) this.scheduleReconnect() } this.ws.onerror (err) { // 错误先记日志不直接断线等 onclose 统一处理 console.warn([LeakWS] connection error:, err) } } startHeartbeat() { this.heartbeatTimer setInterval(() { if (this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: ping })) } }, this.heartbeatInterval) } stopHeartbeat() { if (this.heartbeatTimer) clearInterval(this.heartbeatTimer) } scheduleReconnect() { // 指数退避避免服务端刚恢复时客户端一拥而上 const delay Math.min(30000, 1000 * Math.pow(2, this.reconnectAttempts)) this.reconnectAttempts 1 setTimeout(() this.connect(), delay) } } export default LeakWS参数说明heartbeatInterval我一般设 30 秒一次服务端网关如果连续 90 秒没收到心跳就会主动断开这个 30 秒是留了余量的。reconnectAttempts控制重连频率第一次失败等 1 秒第二次 2 秒最多 30 秒一次这是为了防止服务端重启的瞬间几百个客户端同时重连造成连接风暴。订阅的points数组对应点位 ID不需要全量推推全量会浪费带宽且浏览器解析 JSON 也会卡。逻辑上有个容易被忽略的点onmessage里不要直接写业务判断。因为 WebSocket 消息频率高的时候泄漏检测逻辑如果写在这里会阻塞消息接收排队延迟会让曲线出现假毛刺。正确姿势是eventBus转发让检测模块自己去订阅需要的点位数据。这也是我上面在leakage/里单独放eventBus.js的原因。3. 泄漏检测算法落地夜间最小流量法与前端实现细节3.1 为什么选夜间最小流量法而不是突发流量阈值水务行业做管网泄漏判定有一堆算法最小夜间流量法MNF、流量平衡法、压力点相关法、机器学习异常检测。但落到 Vue 前端能跑、数据要求不高、物理意义又明确的只有 MNF 法。它原理很简单夜间凌晨 2 点到 5 点之间用户用水降到最低此时管网里的流量如果依然高于某个基准值那么多出来的部分大概率是漏损。这个方法不需要压力传感器布点模型只要有流量计数据就能跑对中小水司来说是最短路径。反直觉的点在于很多人以为泄漏检测是找“流量突然变大”的时刻实际做的时候会发现突发流量往往是瞬时波动过 20 分钟就恢复正常了不足以支撑抢修决策。真正的泄漏是持续的、缓慢抬升的逐日夜间最小流量曲线能看到一条“台阶式爬坡”。所以 Vue 前端里不要去做“尖峰检测”而是要做“基线偏离检测”。下面这段代码就是干这个的。// leakage/detector.js // 夜间最小流量法检测器 // 输入: 最近 N 天的分时流量记录按 5 分钟粒度 // 输出: 是否疑似泄漏、泄漏指数、建议复核时间段 const NIGHT_START_HOUR 2 // 夜间窗口开始 const NIGHT_END_HOUR 5 // 夜间窗口结束 const BASELINE_DAYS 7 // 参与基线计算的天数 const DEVIATION_RATIO 0.25 // 允许超过基线的比例超过即预警 function findNightMinFlow(dailyRecords, dateStr) { // 从某天的 5 分钟粒度的流量数组里取出夜间窗口内最小值 const nightPoints dailyRecords.filter((point) { const hour new Date(${dateStr}T${point.time}).getHours() return hour NIGHT_START_HOUR hour NIGHT_END_HOUR }) if (nightPoints.length 0) return null nightPoints.sort((a, b) a.flow - b.flow) return nightPoints[0].flow } function buildBaseline(historyDays) { // 取过去 BASELINE_DAYS 天的夜间最小流量去掉最大最小值后求平均 const minFlows historyDays .map((d) findNightMinFlow(d.records, d.date)) .filter((v) v ! null) .sort((a, b) a - b) if (minFlows.length 5) return null const trimmed minFlows.slice(1, -1) const sum trimmed.reduce((acc, cur) acc cur, 0) return { baseline: sum / trimmed.length, daysUsed: trimmed.length } } function detectLeakage(historyDays, latestDay) { const base buildBaseline(historyDays) if (!base) return { level: insufficient_data, leakIndex: 0 } const latestNightMin findNightMinFlow(latestDay.records, latestDay.date) if (latestNightMin null) return { level: no_night_data, leakIndex: 0 } const expectedMax base.baseline * (1 DEVIATION_RATIO) const leakIndex (latestNightMin - base.baseline) / base.baseline if (latestNightMin expectedMax) { return { level: suspected_leak, leakIndex: Number(leakIndex.toFixed(4)), baseline: Number(base.baseline.toFixed(2)), latest: latestNightMin, suggestTime: ${NIGHT_START_HOUR}:00-${NIGHT_END_HOUR}:00 } } return { level: normal, leakIndex: Number(leakIndex.toFixed(4)), baseline: Number(base.baseline.toFixed(2)), latest: latestNightMin } } export { detectLeakage, findNightMinFlow, buildBaseline }参数说明NIGHT_START_HOUR和NIGHT_END_HOUR建议别死写成 2 和 5不同区域用户的作息不一样我做过一个某县城项目那边夜市多凌晨 2 点流量还没降下来窗口要往后挪到 3 点到 6 点才稳定。DEVIATION_RATIO是最敏感的参数设小了误报多设大了漏报多一般从 0.25 起步数据积累多了再收敛到 0.15 左右。baseline计算我特意去掉了最大最小值再平均这样能抵抗单日异常波动比如某天凌晨恰好有消防用水。这段代码有个边界需要明确它只做“单日判断”做不了“连续 3 天都偏高”的慢趋势。实际使用中我会再加一个trendDays参数连续 N 天夜间最小流量递增且每天都超过基线才把告警升级到“建议关阀排查”。你可以把detectLeakage放进一个定时器里每 10 分钟跑一次但注意要拿到完整的一天数据才计算否则算出来的夜间最小值是片段的没有意义。3.2 为什么不能在 Vue 组件里直接写算法调用很多人会把检测逻辑直接写进Monitor.vue的watch或者computed里跑起来也能用但后面一定翻车。原因一是组件生命周期问题页面切换或者浏览器标签页被切到后台后setInterval会被浏览器节流甚至挂起导致检测任务漏跑二是数据更新和 UI 渲染耦合在一起一次 WebSocket 推送触发整个组件重渲性能扛不住。正确的姿势是把检测器做成独立的模块用定时器驱动检测结果通过状态管理或者事件总线暴露给组件。组件只负责读结果不负责算结果。换句话说检测模块是一个独立的、不依赖 Vue 生命周期的“后台运行者”。为了让它在标签页切到后台还能继续跑我会用Web Worker或者至少在beforeDestroy里不要清掉定时器而是把检测放到一个常驻的全局 manager 里。下面是一个简化的 manager// leakage/manager.js // 全局检测任务管理器负责拉数据、跑算法、派发结果到 store // 不依赖任何 Vue 组件生命周期在 main.js 中实例化并 attach 到全局 import { detectLeakage } from ./detector class LeakageManager { constructor({ apiClient, eventBus, intervalMs 600000 }) { this.apiClient apiClient this.eventBus eventBus this.intervalMs intervalMs this.timer null } start() { this.timer setInterval(async () { // 拉最近 8 天的数据7 天用于基线 1 天用于判断 const historyDays await this.apiClient.fetchLatest8Days() // 这里用最后一天作为最新日 const result detectLeakage(historyDays.slice(0, 7), historyDays[7]) this.eventBus.emit(leakage:result, result) }, this.intervalMs) } stop() { if (this.timer) clearInterval(this.timer) } } export default LeakageManager这段代码的逻辑本质是把检测从 Vue 的生命周期里剥离出来。start()在main.js里调用一次stop()基本不会用到除非你要做应用级热更新。intervalMs设 10 分钟是因为 5 分钟粒度的数据至少要攒够一个夜间窗口才有意义太频繁跑只是浪费请求。如果你的数据源改成 1 分钟粒度可以缩到 3 分钟。4. 大屏监控与告警联动让检测结果落到 UI 上4.1 分区视图与实时曲线的常见布局前端拿到检测结果之后要让看大屏的人一眼看明白哪里异常。分区级监控我一般用“左侧区域列表 主区曲线 右侧告警信息”的三栏布局地图放在中间偏左做底色点位用红黄绿三色标记漏损状态。这里的核心不是布局本身而是要让状态颜色有明确的数据驱动逻辑而不是写死。一个可复用的规则是点位颜色由leakage:result的level字段映射normal显示绿色suspected_leak显示红色insufficient_data显示灰色。leakIndex用于在弹窗里展示偏离百分比。地图选型上轻量项目直接用 ECharts 的自定义地图或者 scatter 图层就够如果甲方点名要 GIS 效果再引 Leaflet 或者 Mapbox但那样会让打包体积增加不少非必要不引入。下面是一个组件内处理告警状态的示例template div classdma-card :classstatus-${currentLevel} h4{{ dmaName }}/h4 p夜间最小流量: {{ latestNightMin }} m³/h/p p基线: {{ baseline }} m³/h/p p偏离指数: {{ leakIndex }}/p button v-ifcurrentLevel suspected_leak clickhandleCreateWorkOrder(dmaId) 派单复核 /button /div /template script setup // 简化版实际项目中数据来自 store 或 eventBus 注入 const props defineProps({ dmaId: String, dmaName: String, result: { type: Object, required: true } }) const currentLevel computed(() props.result.level) const latestNightMin computed(() props.result.latest ?? --) const baseline computed(() props.result.baseline ?? --) const leakIndex computed(() props.result.leakIndex ! undefined ? ${(props.result.leakIndex * 100).toFixed(1)}% : -- ) async function handleCreateWorkOrder(dmaId) { // 调后端创建工单接口带上前端检测的结果快照 await fetch(/api/work-orders, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ dmaId, source: frontend_leakage, leakIndex: props.result.leakIndex, baseline: props.result.baseline, latest: props.result.latest }) }) // 创建成功后提示操作员并且页面内按钮置灰避免重复提交 } /script这个组件里的handleCreateWorkOrder展示了“检测结果怎么变成业务动作”。注意提交给后端的source和快照数据生产环境里甲方会要求前端记录判定依据方便后续追溯你在开发阶段就把这些字段带上后面验收会省事很多。4.2 告警去重与升级机制前端检测跑起来了第二个坑紧跟着就来了告警刷屏。夜间窗口一旦数据异常管理端会收到七八条一模一样的告警操作员点掉一条又来一条。解决思路是引入“告警状态机”一个 DMA 分区在未处置之前相同级别的告警只生成一条后续只更新时间和数值直到操作员点了“已派单”或者系统判定恢复正常才能生成下一条。实现上不复杂用后端存告警状态前端只发“判定结果”后端决定是“新建告警”还是“更新已有告警”。前端要用一个有序的alarmId去关联 WebSocket 推送回来的处置结果。下面是一个伪代码级别的状态流前端检测结果 - 后端口径查询该分区是否已有未关闭的疑似泄漏告警 - 无创建新告警(状态待派单) - 有更新告警最后发生时间当前时刻数值最新检测值 - 操作员点击派单 - 状态改为处理中 - 操作员填写处置结果 - 状态改为已关闭 - 后续前端检测结果低于基线的 1.2 倍 - 后端自动关闭未派单的告警这套机制能解决 80% 的误报体验问题。真正让人头疼的其实是“无人值守时误报怎么办”。我的选择是研究夜间 2-5 点的连续性一个短时的管道气团或水锤造成的波动通常只会让某一两个 5 分钟粒度的点异常而真正的泄漏会让整晚每个点都比基线高。所以我在findNightMinFlow之外加了countAboveBaseline统计夜间窗口里高于基线的点数占比超过 60% 才判定为疑似泄漏否则认定为扰动。这一步能直接过滤掉大量误报推荐你把这个逻辑前置到算法里而不是在告警端做后置过滤。5. 避坑手册泄漏检测项目中常见的 4 个致命细节这里不列泛泛的“规范”只写我实际踩过的和帮别人排查过的问题每一条都按现象、原因、解决展开。5.1 夜间窗口的跨时区问题导致基线算错现象服务器和浏览器在不同时区前端用new Date().getHours()判断夜间窗口结果凌晨 3 点的数据被算到了白天基线偏差很大系统在白天疯狂误报。原因WebSocket 推送过来的时间戳是后端服务器时区的而浏览器getHours()返回的是本地时区时间两边不一致窗口判断就乱了。这个问题在部署到多云机房的项目里特别容易踩。解决前后端统一用 ISO 8601 字符串传输所有时间解析用dayjs配合时区插件解析时显式指定时区为Asia/Shanghai不要依赖运行环境。findNightMinFlow里解析时间戳先转成目标时区的“小时数”再判断窗口避免隐式转换。5.2 WebSocket 断线后检测结果停留在最后一次状态现象网络抖动导致 WebSocket 断开前端页面不再更新数据但大屏上仍显示“正常”操作员以为没泄漏实际已经漏了半天。原因前端在onclose只做了重连没有把点位状态标记为“数据过期”。旧数据没有失效机制业务层无法区分“数据正常”和“没有新数据”。解决在数据层加一个lastUpdateAt时间戳每次 WebSocket 收到点位数据时刷新。manager 里跑检测之前先检查时间戳如果超过 3 倍上报周期没有更新直接输出level: data_staleUI 显示灰色“数据中断”而不是绿色“正常”。另外重连成功后不要急着恢复绿色要先拉一次全量快照确认数据连续。5.3 定时器在低功耗笔记本上被节流导致漏跑现象开发者在笔记本上调试合上盖子再打开发现 Vue 里setInterval的定时器全部停摆检测任务没有执行曲线图中间缺了一段。原因浏览器为了省电会在标签页切换到后台或系统进入睡眠后把setInterval的最小间隔拉长到 1000ms 甚至暂停。检测定时器依赖浏览器调度自然不可靠。解决检测任务不要依赖 Vue 组件里创建的定时器用Web Worker里跑setInterval浏览器对 Worker 的节流策略宽松得多。如果不想引入 Worker至少把定时器注册在根组件外层的一个全局对象上并且用一个可见性检测document.visibilitychange监听页面重新可见后立即手动执行一次检测而不是等下一个周期。5.4 基线数据被异常点污染导致漏报现象某个 DMA 分区夜间流量长期偏高但系统一直判定为“正常”后来现场排查发现已经漏了半个月。原因buildBaseline里的基线计算用的是过去 7 天的历史数据如果某几天恰好也有泄漏基线会被抬得虚高后面的泄漏指数算出来就永远达不到阈值。解决基线要做“滚动修正”而不是固定 7 天。每天检测完成后如果当天的夜间最小流量高于当前基线这个值不应该立即进入基线库而是要等人工确认“没有泄漏”后才写入。或者更简单基线库取过去 30 天的“非告警日”数据告警日的数据不参与后续计算。这是我踩得最深的一个坑也是泄漏检测系统精度提升最明显的改进点。6. 把泄漏检测做成每天能自证清白的系统趋势自检与回放技巧前面讲的算法能帮你判断“今晚漏没漏”但长期运营中你会发现另一个需求怎么让监测人员信任系统、愿意按系统的结论去派单。这是比算法更难的问题。我的做法是加一个“自证”功能每天早晨自动生成一份前一日检测日报展示夜间流量曲线、基线带、判定依据和置信度。有这份东西操作员不用点开曲线再脑补直接看结论就行。具体实现不复杂但有个小技巧值得分享报表里的曲线图不要用 ECharts 默认的平滑曲线要用阶梯线step类型绘制流量数据因为阶梯线能清楚看到 5 分钟粒度的瞬时跳变平滑曲线会把关键突变抹掉。置信度我一般这样计算confidence 1 - (夜间窗口高于基线的点数百分比 - 0.5) * 2点数越集中偏离置信度越高低于 0.5 的判定结果不自动派单只标记“建议人工复核”。回放功能是另一个运营法宝。遇到突发爆管操作员在完成处置后希望复盘“到底是从什么时候开始漏的”。前端做一个时间轴回放把近 7 天的点位流量按分钟回放并用颜色深浅标记每次检测的状态变化。这个功能不需要额外接口前端把 WebSocket 累积的数据和检测结果按时间戳索引存进IndexedDB或者内存数组即可按时间轴拖动时从缓存里取不用再请求后端。需要提醒的是内存数组要限制长度比如每个点位最多保留 30 天的 5 分钟粒度数据那就是大概 8640 条折成对象约几 MB还能接受再多就该考虑用 IndexedDB。最后一个建议是关于“阈值由谁来决定”。我见过一个项目甲方要求所有 DMA 分区用同一个阈值参数结果 A 区夜市多误报整天B 区是工业区漏报一星期。我现在的习惯是给每个分区单独配一套阈值参数存放在后端的配置表里前端接口拉取下来后覆盖默认值。参数调整记录必须可追溯谁在什么时候改过哪个值都要有日志。这不是技术问题而是让系统能持续运营的信任问题。你看系统的打磨技巧往往不在算法本身。这套方案我从最简的 Demo 一直迭代到能稳定跑几个月的线上版本最大的教训就是一句泄漏检测系统的核心不是页面好看而是“每天的结果经得起复核”。你把基线计算和告警去重这两关做扎实了后续接再多的数据源、再加再多的可视化大屏都只是锦上添花。希望帮到你。本文还有配套的精品资源点击获取