
简介这是一套面向环保领域水资源管理场景的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:0008: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仍然可以查看具体数值dataZoom的inside模式支持鼠标滚轮缩放查看历史趋势时很实用。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调小或者改用普通scatter。roam: true允许用户拖拽缩放地图但展厅大屏如果无人交互可以把roam关掉避免误触。markPoint在地图上标注单点数值也很常用但要注意当geo和series同时存在时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置为false、API_BASE指向真实网关一块静态模板就算真正接入了水质实时监测的业务链路。本文还有配套的精品资源点击获取