
大屏数据可视化这个方向听着确实高大上但真正做起来之后你会发现它就是一场“体力活细心活”的结合。去年年中我接了一个“疫情监控系统”的二期改造任务也就是标题里这个“疫情监控系统2”核心目标是把第一版那种静态看板彻底推翻做成一个真正跑在Web端、数据能够动态刷新的可视化大屏。这个项目上线后我回头整理了一下整个研发过程发现里面值得记录的东西特别多从技术选型、布局设计到数据轮询、大屏适配再到上线后踩过的那些坑每一个环节都能单独写一篇复盘。这篇内容更适合两类人看一类是刚接触可视化大屏项目的开发者可以把它当成一份完整的实操参考另一类是已经做过大屏但遇到性能、适配或者数据刷新问题的人可以直接跳到第4章查问题。我会把整个项目的拆解思路、核心代码、参数取舍和避坑经验全部写出来尽量做到你拿过去就能用。1. 项目整体设计思路与技术选型大屏可视化项目跟普通后台管理系统完全不同。后台系统讲究功能密度用户可以靠鼠标点击、键盘输入来完成操作而大屏的核心目标是“让站在几米之外的人一眼看懂当前发生了什么”。所以第一件事不是去找图表库而是先搞清楚三个问题给谁看、看什么、呈现到什么程度。1.1 需求定位与功能拆解“给谁看”决定了信息的颗粒度。这个疫情监控系统的使用者是应急指挥中心和防疫工作专班不是普通市民。值班人员需要在一面大屏上同时掌握这几个维度的信息当前疫情总体态势累计确诊、现有确诊、治愈、死亡等核心指标区域风险分布哪个区县风险等级高、新增病例集中在哪时间变化趋势最近14天的新增病例曲线是抬头还是回落资源保障情况核酸采样点数量、隔离床位使用率、物资储备状态。基于这些诉求我把大屏的信息架构拆成了四个区域顶部是全局标题和统计时间中间主视觉区域放风险地图左右两翼放核心指标卡、趋势图和排名列表底部加一条实时滚动公告栏用来播报最新通知和防疫提示。注意这里有个关键点大屏不是报表数据维度不能贪多。每个区域只承担一个职责指标卡只显示数字趋势图只画曲线地图只展示空间分布。如果试图在一个图表里塞下所有信息远观时反而什么都看不清。1.2 技术栈选型与方案对比技术上我没有选择特别花哨的方案核心是“稳定、快、省心”。最终定下来的技术栈是 Vue 3 ECharts 5 WebSocket 轻量级 Node.js 后端服务。这里我对比过几个方案把当时考虑过的技术选型整理成了表格方案优势劣势适用场景Vue 3 ECharts 5生态成熟、文档全、图表类型丰富、上手快大数据量渲染时需手动优化绝大多数中后台大屏场景React AntV G2图形语法灵活、适合定制化图表学习成本略高、社区方案相对分散对图表形态有强定制诉求的团队原生 JS Leaflet地图渲染性能极好图表体系需要自己造轮子以GIS为主、图表为辅的场景开源可视化平台DataV等开箱即用、组件丰富定制能力受限、商用授权成本高非技术团队快速搭建原型考虑到开发周期和后续维护成本Vue ECharts 的组合最稳妥。ECharts 本身就是百度开源的老牌项目疫情监控地图、趋势图、散点图它都能直接支持社区资料也很丰富遇到问题一搜就能解决。数据通信方面我采用“轮询为主、WebSocket为辅”的混合模式。为什么不做纯 WebSocket因为疫情数据的更新频率通常不高大部分接口是每30分钟甚至每小时汇总一次轮询的成本并不可怕。但为了让大屏在数据变化时能立刻响应我给实时性要求最高的“新增病例数”单独拉了 WebSocket 通道其他指标走普通 HTTP 接口轮询。后面第3章会详细讲这个取舍的逻辑。1.3 数据链路与接口设计大屏的数据链路看似简单其实就是“前端-后端-数据库”三个节点但真正做起来容易在这里翻车。第一版系统最大的问题就是前端直接查明细表几千条记录一次性丢给浏览器ECharts 直接卡死。这次我重新梳理了数据链路数据源上报系统/数据库 ↓ 服务端聚合处理按区域、按日期汇总 ↓ 轻量级API只输出大屏需要的结果 ↓ 前端WebSocket/轮询拉取 ↓ ECharts渲染服务端聚合这一步是整个架构的关键。大屏前端不应该拿到明细数据而是应该拿到已经算好的指标结果。比如地图组件需要的是“每个区县现有确诊数”那后端给到的数据就应该是[ { area: A区, confirmed: 12, riskLevel: high }, { area: B区, confirmed: 5, riskLevel: medium }, { area: C区, confirmed: 0, riskLevel: low } ]前端只负责把结构化的数据映射到地图上不做任何计算。这样既减轻了浏览器的负担也避免了不同图表各自计算导致的数据不一致。我踩过的一个明显的坑是第一版系统里趋势图的数据来自一个接口排名列表的数据来自另一个接口两边对“新增病例”的统计口径不一致导致大屏上半张图显示上升、下半张表却在下降非常尴尬。所以二期项目我把所有统计口径都统一收敛到服务端由服务端保证大屏上所有数字来自同一份数据源前端只信接口不自己做二次聚合。2. 大屏布局、地图与可视化组件实现技术方案定好之后接下来就是最耗时的工作把大屏从视觉稿变成真实可交互的Web页面。这个阶段要处理三件核心的事页面布局的拆分、地图可视化的搭建、各类图表组件的细节调优。2.1 布局规划与组件拆分大屏的分辨率基准我定的是 1920x1080这是目前标准拼接屏和LED大屏最通用的规格。在这个画布下我用了典型的“上-中-下”三段式结构配合左右两侧的纵向面板顶部区域高度约100px展示系统标题、当前时间、数据更新时间中部主区域宽度约60%全屏风险地图这是视觉重心左侧面板宽度约20%疫情核心指标卡、风险区县排行右侧面板宽度约20%新增病例趋势图、资源保障进度条底部区域高度约60px滚动公告栏。这种三段式布局的好处是信息层级非常清楚中间地图负责空间感两侧面板负责数据密度顶部和底部负责时间和文本信息。用户站在远处视线首先会落在地图上然后自然流向两侧的数据面板。组件拆分上我把每个可视区域都单独封装成了Vue组件比如HeaderBar.vue、RiskMap.vue、TrendChart.vue、MetricCard.vue。组件独立之后有几个直接的好处每个组件自己管理自己的数据和渲染逻辑互不干扰某个图表出问题时只需要定位到对应组件不需要翻遍整个页面代码后期如果想把某个模块单独拆出去给手机端用只需要复用组件即可。需要注意的一点是组件的data和timer生命周期一定要管好。大屏页面是长时间挂机的轮询定时器如果不清除组件切换或者热更新时会在后台越积越多最终把浏览器拖垮。这些问题我会在第3章和第4章重点展开。2.2 地图可视化实现细节地图是整个大屏的核心也是实现起来最容易出问题的地方。疫情数据天然带有空间属性指挥中心最关心的是什么是“疫情发生在哪、扩散到什么范围”。所以风险地图默认展示区域划分用不同颜色标注不同风险等级并叠加散点图展示具体风险点。ECharts 的map类型地图有两种做法一种是用 ECharts 内置的地图数据另一种是引入外部 GeoJSON。内置地图用起来简单但更新慢、边界精度一般GeoJSON 可以非常精确地展示每个区县边界。我做的是市-区县两级下钻所以选择了 GeoJSON 方案。GeoJSON 的加载有几项注意事项我这里直接列给后来的人参考文件格式优先用精简过的 TopoJSON 或压缩过的 GeoJSON不要直接丢一个几十MB的原始文件到前端可以通过地图数据网站的 json 精简工具去除多余的坐标精度经纬度保留4位小数足够用了ECharts 通过echarts.registerMap(region, geoJson)注册地图注册之后才能用map: region这个配置项。地图的核心配置我写了一个示例这是当时真实在用的简化版本option { tooltip: { trigger: item, formatter: (params) { if (params.data) { return ${params.name}br/现有确诊${params.data.confirmed}br/风险等级${params.data.riskLevel high ? 高风险 : 中风险}; } return params.name; } }, visualMap: { type: piecewise, pieces: [ { min: 1, max: 5, label: 低风险, color: #ffd666 }, { min: 6, max: 20, label: 中风险, color: #ff9c6e }, { min: 21, label: 高风险, color: #e64545 } ], textStyle: { color: #fff } }, series: [{ type: map, map: region, roam: true, label: { show: false }, data: mapData }] };这里的visualMap是最关键的配置项它决定了不同风险等级的区域会显示什么颜色。我遇到过一个问题地图数据更新后颜色却不变。排查了半天发现是visualMap的pieces区间和实际数据范围不匹配数据里出现了21以上的值但pieces只定义到20超出部分就没有颜色规则。所以在设置区间时上限一定不要封死要给极端情况留一个兜底区间。散点图叠加也用 ECharts直接在series里再增加一个scatter或effectScatter系列。这里注意一下geo坐标系和map系列的匹配问题如果是用map系列来画地图叠加散点时建议通过geoIndex关联避免散点位置错乱。2.3 指标卡、趋势图与排行列表的实现技巧地图之外大屏两边还有很多“小而关键”的组件。这些组件看起来简单实际写代码时有不少细节。指标卡的实现是最简单的但它承担了大屏最重要的信息传达任务。设计上要突出数字本身所以字号要大、数字要有动态滚动效果。数字滚动的实现我没有用额外的动画库而是直接监听数据变化用requestAnimationFrame做数字过渡function animateNumber(el, from, to, duration 800) { const start performance.now(); function update(now) { const progress Math.min((now - start) / duration, 1); const current Math.floor(from (to - from) * progress); el.textContent current.toLocaleString(); if (progress 1) requestAnimationFrame(update); } requestAnimationFrame(update); }这个实现方式有几个好处数字变化时视觉上更有冲击力而且不会改变DOM结构避免了表格重排等性能问题。趋势图我用的是折线图加面积填充。趋势图最核心的诉求是让“上升”还是“回落”一目了然所以不需要过多装饰只要清晰呈现14天内新增病例的变化即可。需要注意一个细节折线图的时间轴X轴最好保持固定区间例如最近14天。不要把没数据的日期直接丢掉否则折线在视觉上会前移给值班人员造成错误的趋势判断。排行列表用了一个简单带有滚动效果的表格优先展示“今日新增病例最多的5个区县”红色字体高亮第一名。这里有个小技巧排行数据如果长期不变列表不要一直滚动会干扰阅读。我做了个判断仅当数据更新或前五名排序变化时才触发滚动动画数据保持稳定时列表就静止显示。3. 实时数据刷新机制与大屏适配方案大屏和普通网页另一个不同点是它可能连续运行几个星期不需要人去碰它。所以“实时刷新”和“适配稳定性”这两个问题如果没处理好上线后就是灾难。3.1 轮询与WebSocket实时刷新方案的取舍实时刷新有两种主流玩法轮询和服务端推送。轮询就是前端按固定时间间隔去拉取一次接口实现简单兼容性最好。疫情监控系统的数据汇总节奏通常是半小时级所以30秒轮询一次已经是非常保守的频率了既能保证数据延迟在一分钟内也能极大降低后端压力。WebSocket 则是由服务端主动推送数据延迟更低但对服务端要求更高连接管理、断线重连、推送频率控制都要自己处理。我当时做了一个混合设计具体分工是这样的地图数据和指标卡数据HTTP轮询间隔30秒新增病例数、公告信息WebSocket实时推送趋势图和排行列表HTTP轮询间隔60秒。为什么不全部走 WebSocket因为大屏的模块对实时性要求差异很大。趋势图是14天的数据60秒刷新一次和秒级刷新一次视觉上没有任何区别指标卡是新发布的数据30秒内的延迟也完全可以接受。只有“新增病例数”这种关键指标才值得为它单独维护一条长连接。这其实是一个资源投入的权衡不应该为了技术炫耀而放弃简单可靠的方案。轮询的实现我封装了一个可复用的工具函数function startPolling(requestFn, interval, onSuccess) { let timer null; let isRunning false; async function poll() { if (isRunning) return; isRunning true; try { const res await requestFn(); onSuccess(res); } catch (e) { console.error([polling error], e); } finally { isRunning false; } } poll(); timer setInterval(poll, interval); return { stop() { if (timer) clearInterval(timer); } }; }这里isRunning这个标志位非常重要。如果不加这个判断当接口响应较慢时上一次请求还没结束下一次心跳又触发了会造成请求堆积最终导致数据乱序和页面卡顿。WebSocket 断线重连也是必须处理的。值班大屏不能因为网络波动就永远停在旧数据上。我写了一个带指数退避的重连策略断线后延迟1秒重连不断失败则逐步拉长延迟到30秒封顶避免服务端被大量重连请求打爆。3.2 大屏分辨率的适配策略大屏的适配是个老生常谈但又绕不开的问题。各种大屏硬件的物理分辨率五花八门有1920x1080的液晶拼接屏也有单块4K的LED巨幕。如果只按一种分辨率写死换一块屏幕就全乱。我采用的适配方案是整体缩放 自适应居中。具体做法是以1920x1080为设计基准页面加载时读取当前视口尺寸计算缩放比例用CSS3的transform: scale()对整个大屏容器进行等比缩放保证在任何分辨率下布局都不变形。function handleResize() { const designWidth 1920; const designHeight 1080; const scaleX window.innerWidth / designWidth; const scaleY window.innerHeight / designHeight; const scale Math.min(scaleX, scaleY); const app document.getElementById(screen-container); app.style.transform scale(${scale}); app.style.transformOrigin center center; } window.addEventListener(resize, handleResize); handleResize();用Math.min(scaleX, scaleY)的目的是保证整体等比缩放宁可上下或左右留一点黑边也不能让画面变形拉伸。如果是真正用在拼接墙上最好再联系硬件厂商拿到准确的物理分辨率这样才能做到像素级对齐。这里有一个非常容易忽略的细节ECharts 在缩放容器中渲染时图表本身的像素值是固定的。如果外部的transform: scale()已经放大或缩小图表内部的字体和图标不会跟着一起缩放就会出现“图表整体放大了但文字和图例模糊成一团”的情况。解决办法是监听缩放比例动态调用chart.resize()并且在resize时对 ECharts 的文字大小做相应调整。3.3 性能优化与长时间运行稳定性大屏项目另一个容易翻车的地方是性能。一个页面放了十几张图表每30秒刷新一次连续跑24小时如果不做性能优化浏览器很容易内存不断上涨最终变得卡顿甚至白屏。我总结出三个最值得做的优化点第一组件销毁时一定要销毁图表实例。Vue 3 中组件beforeUnmount生命周期里要做chart.dispose()否则图表实例会残留在内存中。长时间运行的大屏页面如果经历过组件热更新或者手动刷新就会出现多个图表实例并存的情况。我后来在页面里主动打印过实例数量最多的时候出现过十几份重复实例内存直接飙到800MB。onBeforeUnmount(() { if (chartInstance) { chartInstance.dispose(); chartInstance null; } });第二数据刷新时不要setOption全量覆盖。ECharts 的setOption支持传第二个参数notMerge如果设置为true它会把旧图表的配置全部清空重来非常消耗性能。大屏轮询时数据一般只更新数值图表的配置项、坐标系、颜色映射都没变此时应该保持notMerge为false默认让 ECharts 做增量更新。第三优化数据量的粒度和轮询频率。地图下钻到区县级别每张地图最多几十个区域完全没必要高频刷新。对实时性要求不高的数据轮询间隔可以放到60秒甚至更长给浏览器留出喘息的时间。4. 部署上线与常见问题排查实录这个项目上线前的部署和上线后的运维其实比开发本身更磨人。尤其是大屏环境经常是内网、特殊网络、甚至没有公网各种奇怪问题都会冒出来。我把遇到过的典型问题和排查思路整理成了清单供参考。4.1 部署环境与基础配置后端接口是一个轻量服务部署在一台内网服务器上前端构建后的静态资源直接放到 Nginx 里。基础配置大概是这样的server { listen 8080; server_name localhost; root /data/epidemic-screen/dist; index index.html; # 前端静态资源缓存 location /assets/ { expires 7d; add_header Cache-Control public; } # 后端API转发 location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 解决前端路由history模式刷新404问题 location / { try_files $uri $uri/ /index.html; } }大屏这种长时间挂机的页面可靠性是第一位的。静态资源要设置好缓存index.html最好不缓存这样前端每次发布新版本后刷新页面就能拿到最新的入口而assets/下面的 JS、CSS 都带上了内容哈希可以长期缓存减少重复拉取。接口服务如果跟前端页面不在同一个域名下就会涉及到跨域问题。这里我强烈建议通过 Nignx 反向代理来解决而不是在接口代码里乱加Access-Control-Allow-Origin。代理让前后端在同一个域名下接口地址以/api前缀区分既解决了跨域又隐藏了后端真实端口安全性也更好。4.2 常见问题与排查方法大屏项目的问题很多都是“只在现场出现、开发环境复现不了”的所以排查思路特别重要。我把遇到典型的四类问题整理成了速查表现象可能原因排查方法与处理大屏画面被拉伸变形分辨率基准与墙不匹配或CSS缩放失败核对物理分辨率检查容器transform是否生效页面跑一晚上后越来越卡图表实例没销毁、轮询请求堆积检查内存快照beforeUnmount中dispose轮询接口加并发锁地图新增数据后颜色不更新visualMap区间未覆盖新增值检查数据极值确认pieces区间下限/上限无遗漏大屏白屏或显示陈旧数据WebSocket 断线且无重连、或接口超时看网络面板确认连接状态确认重连机制是否执行排查大屏问题时有个经验优先看浏览器的网络面板和内存面板而不是直接翻代码。网络面板能确认每个轮询请求是否正常返回内存面板能快速发现是不是存在内存泄漏。很多时候问题并不在渲染逻辑里而是在数据链路和生命周期管理上。4.3 值班场景下的稳定性增强大屏一旦上线通常没有前端工程师在旁边盯着改代码所以上线前要做一些“抗造”处理后端接口挂掉时前端要保留最近一次成功的数据并显示在页面上同时在界面角落提示“数据更新异常”不能直接把整屏清成空白给页面加一个“手动刷新”按钮放在角落不要明显方便现场人员临时触发数据更新所有轮询请求需要设置超时时间接口如果5秒内没有响应就自动跳过这次不能无限等待。另外如果大屏需要通过遥控器或中控系统切换信号源那么页面还应该接一个可视化的“退出”按钮或者至少提供一键清屏的能力不然晚会或者汇报结束后大屏会一直挂着没人会关。这些都是实际带队时会被现场人员反复提出的需求提前做掉能省掉后面很多沟通成本。5. 项目迭代过程中沉淀的几个关键经验项目做完之后我回头复盘了一下觉得有几个开发习惯和做项目的理念是这个项目带给我最有价值的东西。单独拿出来跟大家分享一下都是很现实的经验。第一大屏项目一定要先确认“屏”的真实参数再动手写代码。很多项目开发时用的是1920x1080的普通显示器结果上线前一看是块长方形的异形拼接屏比例完全不同前端的所有布局都要重做。我的建议是进场前就跟硬件沟通清楚拿到准确的显示分辨率和尺寸宁可在需求阶段多问几句也不要最后阶段通宵返工。第二数据结构和统计口径要在开发第一天就定义清楚。大屏上如果同时出现“累计确诊”和“现有确诊”逻辑关系是什么后端返回的字段是confirmed还是totalConfirmed这些如果不在接口文档里统一前端后端反复扯皮是小上线后数据对不上才是大问题。这个系统二期我强制推行了接口契约文档所有字段命名、含义、取值单位都在文档里规定好后面联调阶段顺利了很多。第三用 Mock 数据先开发前端。大屏项目往往图表交互复杂而后端数据还在采集清洗阶段。如果非要等接口好了再开发页面工期一定扛不住。我在这个项目里把所有接口都先定义成 Mock 数据前端先把页面和图表全部调通后端好了之后直接替换请求地址。这样做最大的价值是前端可以提前预演所有边界情况例如数据为空时怎么显示、接口异常时怎么降级这些逻辑如果在上线前没想清楚上线后一定会暴露。第四给大屏加上“数据时点”的展示。大屏上必须能看到“数据更新于什么时间”这是整个项目里指挥中心反馈最强烈的一个需求。早期版本没有这个时间戳值班人员看着屏幕永远是慌的不知道当前数字是几分钟前还是几天前的。加了更新时间后一眼就知道系统有没有在正常刷新。这个功能实现成本极低价值却极高越早做越好。