水质监测预警大屏:基于ECharts的实时可视化系统搭建实践
2026/9/16 1:25:47 网站建设 项目流程

简介:这是一套面向环保领域水资源管理场景的Web智能监控解决方案,主要服务于需要快速搭建水质监测大屏的前端开发者、大数据工程师及环保信息化项目团队。资源将HTML页面模板、大数据处理框架下的展示组件与Echarts可视化图表库整合在一起,覆盖pH值、溶解氧、氨氮等指标的实时监测与阈值预警逻辑,并能通过响应式布局适配多终端展示。压缩包共含8396个文件、约22.8MB,以js脚本、ts类型定义、map映射、md说明、json配置为主,同时带有html入口页面、css样式、png图标及npm工程化配套文件,目录结构完整,可作为大屏项目的直接素材。已有312人学习下载。借助其中预制的图表交互、阈值告警与数据渲染模块,使用者可快速搭建出具备实时监控和预警能力的水质数据看板,节省从零开发的时间。

1. 水质实时监测的难点不在采集,而在把数据变成看得懂的预警

水质监测系统最容易被低估的一环,是前端。传感器数据一直在产生,后端接口也写得完,但业务方要看的不是数据库字段,而是一块大屏上能直接判断「哪个断面超标、趋势在变好还是变差、需不需要派人去现场」的信息。市面上流传的「水质情况实时监测预警系统(Html模板、大数据模板、大屏echarts模板)」这类项目,本质上就是一个以 ECharts 为核心的可视化大屏前端工程,包含地图、折线图、饼图、滚动告警列表,再用阈值判定规则把异常数据变成声光告警。它特别适合环保项目的前期原型、大数据相关方向的毕业设计,以及需要快速对接已有物联网平台的工程实践。本文按搭建这类大屏的真实顺序展开:数据模型、实时通道、图表组织、预警判定、适配与交付。

2. 从数据接入开始:水质监测大屏的数据模型与实时通道

2.1 先定数据结构,再写前端逻辑

我一般会先让后端或者存储层把输出字段固定下来,再开始写大屏。水质监测的通用指标集中在 pH、溶解氧、浊度、氨氮、温度、电导率这几项,加上监测站点的经纬度和采样时间。字段统一之后,前端不管是渲染 ECharts 还是做阈值判定,都只依赖一套结构。

{ "stationId": "WS-001", "stationName": "北干渠断面", "lng": 120.1536, "lat": 30.2875, "timestamp": "2025-06-01T08:30:00+08:00", "indicators": { "ph": 7.82, "do": 5.6, "turbidity": 2.3, "ammonia": 0.42, "temperature": 22.1 } }

时间戳建议统一使用 ISO 8601 格式,带时区偏移。很多模板在折线图上出现 X 轴乱序,原因不是 ECharts 的问题,而是后端返回了"2025-06-01 08:30:00"这种字符串,type: 'time'的 X 轴无法直接解析。前端拿到数据后第一时间用Date.parse()校验,不合法就抛错,比画出一张错图再排查更省事。

function normalizePacket(raw) { const ts = Date.parse(raw.timestamp); if (Number.isNaN(ts)) { throw new Error(`非法时间戳: ${raw.timestamp}`); } return { ...raw, timestamp: new Date(ts), ph: Number(raw.indicators.ph), do: Number(raw.indicators.do) }; }

参数说明:Number()转换是为了防止后端把数值字段返回成字符串;Date.parse负责统一时间轴。模板项目如果直接吃 JSON 文件,也要在入口处做这道 normalize,否则后续所有图表都可能带着类型隐患。

2.2 轮询、WebSocket、MQTT over WebSocket 怎么选

大屏接数据常见方式有三种:定时轮询、WebSocket 长连接、MQTT over WebSocket。轮询是模板里最常见的初始做法,实现成本最低,但实时性差且容易造成无意义的请求浪费。WebSocket 适合标准 Web 后端推送,从 IoT 平台或自建服务端拿数据时最顺手,也是我个人最常用的方式。MQTT over WebSocket 适合设备侧直接接 EMQX 这类 Broker 的场景,水质监测设备通过 4G/NB-IoT 上报到 Broker,前端订阅对应 topic,一条链路直达。

方式实现成本实时性典型场景
HTTP 轮询一般,取决于间隔静态模板、演示原型
WebSocket自建服务端推送、大屏常驻页面
MQTT over WebSocket中高设备直连 Broker、已有 IoT 平台

前端 WebSocket 连接的核心代码并不复杂,但有一个坑必须处理:断线重连。大屏挂在展厅里可能一开就是几天,网络抖动一次,如果不重连,整块屏就变成死数据。

let ws = null; let heartbeatTimer = null; function connectWaterQualityWS() { const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'; ws = new WebSocket(`${protocol}${location.host}/ws/water-quality`); ws.onopen = () => { ws.send(JSON.stringify({ action: 'subscribe', topics: ['station:WS-001', 'station:WS-002'] })); heartbeatTimer = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'ping' })); } }, 30000); }; ws.onmessage = (evt) => { const packet = JSON.parse(evt.data); window.dispatchEvent(new CustomEvent('water-quality', { detail: packet })); }; ws.onclose = () => { clearInterval(heartbeatTimer); setTimeout(connectWaterQualityWS, 3000); }; ws.onerror = () => ws.close(); } connectWaterQualityWS();

这段代码做了三件事:订阅感兴趣的监测站、每 30 秒发一次心跳、断开 3 秒后自动重连。通过window.dispatchEvent广播数据,是为了让折线图、地图、告警列表各自监听自己的事件,而不是在onmessage里到处调用图表函数,耦合度更低。心跳间隔建议小于服务端超时时间的二分之一,比如服务端 60 秒无消息断开,前端就 25 到 30 秒发一次。

2.3 Mock 数据与真实接口一键切换的封装

很多大屏模板自带的静态 JSON 数据是为了先跑通效果,真正对接时需要换成接口。我习惯在模板里加一层数据访问封装,用配置开关决定走静态数据还是 HTTP 接口,这样前端开发时不依赖后端,联调时也不需要改图表组件。

const USE_MOCK = window.APP_CONFIG?.USE_MOCK ?? true; const API_BASE = window.APP_CONFIG?.API_BASE ?? '/api/v1'; async function getLatestReadings() { if (USE_MOCK) { const mock = await import('./mock-stations.js'); return mock.stations; } const res = await fetch(`${API_BASE}/stations/latest`, { headers: { Accept: 'application/json' } }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } const json = await res.json(); return json.data; }

提示:window.APP_CONFIG是运行时注入的全局配置,可以在 index.html 里用<script>写死,也可以由后端在部署时动态生成。它的优势是打包后不用改代码就能切换环境,比把接口地址写死在源码里更适合交付。

3. 用 ECharts 搭出一张水质可视化大屏

3.1 大屏布局与图表实例管理

一张典型的大屏布局是上下左右分区:顶部放标题和时间,中间放中国地图,两侧分别放实时指标卡片、水质趋势折线图、预警级别饼图和滚动告警列表。不管是 Vue3 可视化大屏还是 React + TypeScript 的大屏项目,最终都是把这些图表组件摆进栅格容器里。

布局常见做法是设计稿固定 1920×1080,页面内部用 CSS Grid 或绝对定位来摆位置。有一点容易被忽略:ECharts 实例的初始化时机必须等容器元素有实际宽度高度之后。如果发现图表渲染出来是 0 宽或者只占一小条,多半是init时容器还在display: none状态。

我倾向维护一个统一的实例池,便于在页面卸载或切换时统一销毁。

const chartInstances = new Map(); export function getChart(el) { if (!chartInstances.has(el)) { chartInstances.set(el, echarts.init(el)); } return chartInstances.get(el); } export function disposeAllCharts() { chartInstances.forEach((instance) => instance.dispose()); chartInstances.clear(); } window.addEventListener('resize', () => { chartInstances.forEach((instance) => instance.resize()); });

参数说明:echarts.init(el)的第二个参数可以传主题,模板工程里如果想整体换色,不用改每个 option,改成注册自定义主题即可。resize是防抖处理过的,否则拖拽浏览器窗口时会有明显卡顿。

3.2 折线图:趋势是水质大屏的第一优先级

水质大屏上最常用的图是时序折线图,用来展示某个站点的 pH、溶解氧、浊度随时间变化。它比仪表盘更适合判断趋势:pH 在一个小时内持续走低,比单次 7.2 的读数更有预警价值。

const phChart = getChart(document.getElementById('phTrend')); phChart.setOption({ tooltip: { trigger: 'axis' }, grid: { left: 50, right: 20, top: 30, bottom: 60 }, xAxis: { type: 'time', boundaryGap: false, axisLabel: { color: '#9fe6ff' } }, yAxis: { type: 'value', min: 6.5, max: 8.5, splitLine: { lineStyle: { color: 'rgba(90, 140, 200, .2)' } }, axisLabel: { color: '#cde9ff' } }, dataZoom: [ { type: 'inside', start: 0, end: 100 } ], series: [{ name: 'pH', type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 2, color: '#38e6ff' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(56, 230, 255, .35)' }, { offset: 1, color: 'rgba(56, 230, 255, .02)' } ]) }, data: points }] });

参数说明:boundaryGap: false让折线从第一个点开始贴住坐标轴边缘,不会有空白;showSymbol: false是数据密度大时的必要设置,否则每个点都画圆点,大屏会显得杂乱,配合tooltip仍然可以查看具体数值;dataZoominside模式支持鼠标滚轮缩放,查看历史趋势时很实用。

3.3 中国地图与测点分布:echarts 地图的最佳落地方式

大屏模板里出现地图,最常见的是两种:一种是展示监测站点的地理位置分布,另一种是区域着色表达各省水质等级。前者用effectScatter叠加在geo坐标系上,后者用map系列配合visualMap

地图数据来源必须注意:GeoJSON 的边界需要来自规范的合规渠道,发布前要核对地图边界是否准确。ECharts 从 5.x 开始不再内置地图数据,需要手动registerMap

import chinaJson from '@/assets/map/china.json'; echarts.registerMap('china', chinaJson); const mapChart = getChart(document.getElementById('stationMap')); mapChart.setOption({ tooltip: { formatter: (params) => { const st = params.data; return `${st.name}<br/>pH: ${st.ph}<br/>溶解氧: ${st.do} mg/L`; } }, geo: { map: 'china', roam: true, zoom: 1.2, itemStyle: { areaColor: '#10316b', borderColor: '#41d6ff' }, emphasis: { itemStyle: { areaColor: '#1f4d96' } } }, series: [{ name: '监测站点', type: 'effectScatter', coordinateSystem: 'geo', data: stations.map((s) => ({ name: s.stationName, value: [s.lng, s.lat, s.ph], ph: s.ph, do: s.do })), rippleEffect: { period: 4, scale: 3 }, itemStyle: { color: LEVEL_COLOR[s.level] } }] });

参数说明:effectScatter的涟漪动画很适合表达实时监测点,但不要所有站点都开大 radius,否则地图上会糊成一片。站点数量超过 50 时,建议把rippleEffect.scale调小或者改用普通scatterroam: true允许用户拖拽缩放地图,但展厅大屏如果无人交互,可以把roam关掉避免误触。

markPoint在地图上标注单点数值也很常用,但要注意:当geoseries同时存在时,markPoint的坐标需要显式用coord指定经纬度,直接塞value数组容易定位失败。

3.4 饼图、3D 效果与滚动告警列表

饼图主要用于水质类别占比,比如Ⅰ类至Ⅴ类的站数比例。普通饼图可以满足大部分需求,但大屏视觉上往往想要立体感。echarts 3d pie 的做法通常依赖 echarts-gl 的bar3D来拼合,实现成本和渲染开销都不低。我一般建议先用普通饼图加阴影模拟立体效果。

series: [{ type: 'pie', radius: ['45%', '70%'], itemStyle: { borderRadius: 6, borderColor: '#0a1e3f', borderWidth: 2 }, label: { show: true, formatter: '{b}: {c} 站' }, emphasis: { scaleSize: 8 } }]

borderRadius让扇区边缘带圆角,视觉上比平面饼图精致很多。真要在 3D 饼图上下文章的模板,用 echarts-gl 的bar3D把圆柱切面叠成环,需要额外处理光照和相机角度,演示效果虽好,维护成本也明显高出一截,不建议在数据刷新频繁的场景使用。

滚动告警列表是另一个标配组件,它不属于 ECharts,而是 DOM 列表。实现方式上不要用定时器逐条操作 DOM,而是整体位移加上无缝衔接。

let alarmOffset = 0; const listEl = document.getElementById('alarmList'); const itemHeight = 34; const maxOffset = listEl.children.length - 6; function tickAlarmList() { if (listEl.children.length > 6) { alarmOffset = (alarmOffset + 1) % (maxOffset + 1); listEl.style.transform = `translateY(-${alarmOffset * itemHeight}px)`; } requestAnimationFrame(() => setTimeout(tickAlarmList, 2500)); }

代码说明:requestAnimationFrame保证动画帧跟随屏幕刷新率,setTimeout控制每条告警停留 2.5 秒。容器需要设置固定高度和overflow: hidden,否则列表不会被裁切。这种方式比逐条销毁创建 DOM 节点要平滑得多。

4. 预警判定与可视化大屏适配:把模板变成监测系统

4.1 阈值判定引擎:去抖比报警逻辑本身更重要

模板里最常见的预警逻辑是硬编码判断,比如if (ph > 8.5) alarm()。小型演示够用,但接真实数据后会暴露两个问题:传感器偶发毛刺导致误报,一个超标点反复触发告警。我一般会加一个简单的去抖机制,连续多次超过阈值才确认告警。

const thresholds = { ph: { min: 6.5, max: 8.5 }, do: { min: 3.0, max: null }, turbidity: { min: null, max: 5.0 }, ammonia: { min: null, max: 2.0 } }; const counter = {}; export function judgeWaterQuality(packet) { const hits = []; const indicators = packet.indicators; for (const [key, range] of Object.entries(thresholds)) { const value = Number(indicators[key]); if (Number.isNaN(value)) continue; let triggered = false; if (range.min !== null && value < range.min) triggered = true; if (range.max !== null && value > range.max) triggered = true; if (triggered) { counter[key] = (counter[key] || 0) + 1; if (counter[key] >= 3) hits.push(`${key}:${value}`); } else { counter[key] = 0; } } return hits.length > 0 ? { level: hits.length > 1 ? 'red' : 'orange', msg: hits.join('; ') } : null; }

参数说明:counter[key] >= 3表示连续三条数据都超标才真正报出来,之后每次触发都会持续上报,直到数值恢复正常。恢复逻辑也必须是连续多次正常,这里简单置零即达到效果。level的判定规则,红色表示多项超标,橙色表示单项超标。更严格的项目可以把恢复也要连续 N 次正常作为解除条件,避免告警在阈值边缘反复翻转。

4.2 告警在 ECharts 上的连贯呈现:markArea、闪烁与语音播报

预警不只是弹一个列表项,它要在图表上有视觉落点。折线图上把超标时间段用markArea标出来,是最直观的做法。

series: [{ // ... markArea: { silent: true, itemStyle: { color: 'rgba(255, 77, 79, .15)' }, data: alarmWindows.map((w) => [ { xAxis: w.start, name: '超标时段' }, { xAxis: w.end } ]) } }]

markArea的数据结构是一对起点和终点,silent: true禁用这个区域的鼠标事件,不然 tooltip 会被大块阴影挡住。

语音播报在展厅场景中很实用。浏览器原生speechSynthesis就能实现,不用引第三方库。注意浏览器对自动播放语音有策略限制,首次播报前必须有一次用户点击交互,大屏启动时可以做一次「允许播放」的按钮引导。

export function speakAlarm(msg, level) { if (level !== 'red' && level !== 'orange') return; const utter = new SpeechSynthesisUtterance(`${level}预警:${msg}`); utter.lang = 'zh-CN'; speechSynthesis.speak(utter); }

4.3 可视化大屏适配:scale 方案的完整代码与取舍

大屏适配是模板落地最容易翻车的地方。同一套设计稿要在 1080P、2K、4K 甚至竖屏拼接屏上不变形,常见方案有三类:rem 方案、vw/vh 方案和 transform scale 方案。

方案原理优点缺点
rem根字体随屏宽变化布局自适应、文字清晰设计稿像素换算繁琐
vw/vh尺寸直接按视口比例实现最简单字体和间距会偏小或偏大
scale整体缩放完美还原设计稿缩放后文字可能轻微模糊

三种方案里我偏好在 1920×1080 的模板工程里用 scale 方案,因为设计稿的绝对定位布局可以原样保留,不做任何换算。

const designWidth = 1920; const designHeight = 1080; const screenEl = document.getElementById('screen'); function resizeScreen() { const scale = Math.min( window.innerWidth / designWidth, window.innerHeight / designHeight ); screenEl.style.transform = `scale(${scale})`; } window.addEventListener('resize', resizeScreen); resizeScreen();

注意:transform: scale缩放后,元素仍然占据原始布局尺寸,所以screenEl的外层容器需要设置overflow: hidden,否则会出现滚动条。还有一种做法是用transform-origin: center center配合display: flex居中,保证缩放比例不一致时画面居中而不是偏向某一边。

4.4 渲染性能:按需引入 echarts 模块

模板工程如果用了全量 ECharts,首屏性能会有明显损耗,特别是在大屏这种图表数量多的页面。强烈建议改成按需引入,只加载用到的图表类型和组件。

import * as echarts from 'echarts/core'; import { LineChart, PieChart, BarChart, MapChart, EffectScatterChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, GeoComponent, VisualMapComponent, DataZoomComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ LineChart, PieChart, BarChart, MapChart, EffectScatterChart, GridComponent, TooltipComponent, GeoComponent, VisualMapComponent, DataZoomComponent, CanvasRenderer ]);

参数说明:echarts/core是模块化入口,必须调用use注册后才能使用对应图表。如果项目里用了炫光或者 3D 效果,才需要额外引入 echarts-gl 和SVGRenderer。对于大屏这种以 Canvas 为主的场景,CanvasRenderer是默认选项。

5. 从模板到交付:接口对接、打包与验证清单

5.1 数据刷新的三种触发方式

交付前需要确认数据刷新机制。轮询场景下,通过setInterval定时调用getLatestReadings,建议间隔与数据源更新频率匹配,传感器 1 分钟上报一次,前端就没必要 5 秒拉一次。WebSocket 场景下,数据推送事件驱动,前端收到water-quality事件后统一更新图表。

// 轮询模式 setInterval(async () => { try { const stations = await getLatestReadings(); updateChartsAndAlarms(stations); } catch (err) { console.error(err); } }, 30000); // WebSocket 模式 window.addEventListener('water-quality', (evt) => { const packet = evt.detail; appendTrendPoint(packet); const alarm = judgeWaterQuality(packet); if (alarm) pushAlarmToList(alarm); });

提示:两种刷新方式不要同时用。既开 WebSocket 又跑轮询,会造成重复渲染和告警重复。如果用 zabbix 类监控系统当作数据源,一般走 webhook 推送告警事件,前端同理只需监听事件即可。

5.2 交付前验证清单

对接完成后按下面的清单走一遍,能省去很多现场调试的尴尬。

验证项操作预期结果
数据链路打开 Network 面板WebSocket 或 fetch 请求频率稳定,无连续报错
预警触发后端注入一组超标值10 秒内折线图出现 markArea,告警列表新增记录
大屏缩放窗口从 1920 缩至 1366布局不变形、文字不换行、图表不溢出
内存占用切换页面后再观察页面状态ECharts 实例数不持续增长
断线重连停掉服务端再恢复前端 5 秒内自动恢复数据刷新
时间展示对比设备时间与图表 X 轴折线图时间与服务器时间一致,无 8 小时偏移

模拟超标数据时,注意别直接改生产指标,可以给judgeWaterQuality传一个测试 packet。模板切换到真实接口前,也值得做一个自动重置 ECharts 实例的逻辑:window.addEventListener('pagehide', disposeAllCharts),防止单页应用缓存导致后端已经换了 token,旧图表还在拿着失效数据轮询。把 mock 文件里的时间字段统一成 ISO 8601,然后将USE_MOCK置为falseAPI_BASE指向真实网关,一块静态模板就算真正接入了水质实时监测的业务链路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询