简介:本资源是一套基于HTML+CSS+JS+ECharts技术栈实现的大屏可视化完整项目,面向前端初学者与数据可视化实践者,解决企业级数据看板快速搭建与动态渲染的核心需求。压缩包共1341个文件,涵盖229个JavaScript脚本(含ECharts初始化、数据轮询与交互逻辑)、139个CSS样式文件(含响应式布局、Flex/Grid排版及主题配色方案)、61个HTML页面(含主屏index.html及多级子模块结构),以及764张PNG图表素材与图标资源,整体大小为63.77MB。已有1677人学习下载,项目结构清晰,包含备份文件(如index.html.bak)与主流UI框架依赖(Bootstrap、AdminLTE等),便于理解工程化组织方式。读者可直接运行查看全屏自适应效果,掌握从页面骨架搭建、样式精细化控制、实时数据驱动到ECharts多图联动的全流程开发能力,并复用其中定时刷新、API模拟、主题切换等实用模块。
1. 大屏可视化不是“套个模板就完事”:一个真实产线监控场景里,我用纯前端三件套(HTML+CSS+JS+ECharts)把27个实时指标、6类告警状态、3路视频流缩略图全塞进一块4K屏,零后端API调用——它本质是个「可离线运行的动态数据看板」,适合中小团队快速验证大屏逻辑、嵌入已有Web系统、或作为IoT边缘设备本地监控终端。别被“可视化”三个字骗了,这项目不依赖Node服务、不连数据库、不走WebSocket长连接,所有数据靠JS定时模拟生成+本地JSON配置驱动;ECharts只负责渲染,真正的业务逻辑全在data.js和control.js里。如果你正卡在“想先跑通再对接真实数据”“被Vue/React框架绑架不敢动底层渲染”“需要部署到无Node环境的工控机”,这个包就是你的后悔药。
2. 项目结构与技术选型:为什么坚持用原生三件套而不是Vue大屏脚手架?
提示:这不是怀旧,是权衡。当你的目标设备是某国产ARM工控机(仅预装Chrome 89)、部署路径是
file:///协议、且不允许安装任何服务时,“轻量可控”比“开发体验”优先级高三个数量级。
2.1 目录结构:5个核心文件撑起整个大屏
项目解压后共12个文件,但真正参与运行的只有以下5个(其余为文档、图标、字体等辅助资源):
├── index.html # 入口页:定义4K分辨率容器、加载CSS/JS、预留视频流占位区 ├── style.css # 核心样式:采用CSS Grid+Flex混合布局,12列栅格系统适配4K屏(1920×1080基准,通过scale缩放) ├── data.js # 数据引擎:含3类数据源——定时器模拟的实时数值(如温度、压力)、静态配置表(设备ID→名称映射)、告警规则(阈值判定逻辑) ├── control.js # 控制中枢:处理时间轮询、数据更新、ECharts实例管理、告警状态同步、视频缩略图切换 └── charts/ # ECharts配置集:每个子文件对应一个图表(line-temperature.js, pie-device-status.js等),导出配置对象而非初始化代码 ├── line-temperature.js ├── bar-pressure.js ├── pie-device-status.js └── map-location.js关键设计点在于解耦渲染与数据:charts/*.js只导出option对象(纯JSON结构),control.js统一调用echarts.init()并绑定setOption(),避免每个图表重复写初始化逻辑。这种写法让二次开发时能直接替换某个图表配置而不动其他模块——比如把map-location.js换成百度地图SDK封装,只需保证导出结构一致即可。
2.2 技术栈选择依据:为什么不用Vue/React?为什么ECharts不升级到5.x?
- HTML/CSS/JS原生:规避打包构建链路。
index.html中所有<script>标签均使用defer属性,确保DOM加载完成后再执行;style.css内联了所有关键样式(如.screen-container { width: 3840px; height: 2160px; transform: scale(0.5); }),避免FOUC(Flash of Unstyled Content)。 - ECharts 4.9.0:这是最后一个支持IE11的稳定版,更重要的是其
setOption({})方法对增量更新更友好。实测ECharts 5.x在频繁调用setOption()时存在内存泄漏(尤其在graphic组件大量使用时),而本项目每秒更新12个图表,必须控制内存增长。data.js中所有数据更新均采用Array.prototype.map()生成新数组,杜绝直接修改原引用。 - 零构建工具:没有
package.json,没有webpack.config.js,没有vite.config.ts。你双击index.html就能看到效果——这对产线调试太重要了。某次现场部署,客户网络断开,我们靠U盘拷贝文件到工控机桌面,3分钟完成上线。
2.3 数据流设计:从模拟数据到图表渲染的完整链路
整个数据流动遵循单向流原则:data.js(数据源) →control.js(调度器) →charts/*.js(配置生成) →ECharts实例(渲染)
具体步骤如下:
data.js启动一个setInterval,每2秒触发一次generateMockData()函数;- 该函数返回一个包含
metrics(27个指标)、alerts(6类告警状态)、videos(3路缩略图URL)的对象; control.js监听此返回值,对metrics做归一化处理(如将原始温度值36.5转为{value: 36.5, unit: '℃'});- 遍历
charts/目录下所有配置文件,调用其getChartOption(data)方法传入处理后的数据; - 将生成的
option对象传给对应ECharts实例的setOption()方法。
关键代码在control.js第47行:
// control.js function updateAllCharts() { const mockData = generateMockData(); // 来自data.js const normalizedData = normalizeMetrics(mockData.metrics); // 归一化处理 // 遍历charts目录下所有配置文件 CHART_CONFIGS.forEach(config => { const chartInstance = echarts.getInstanceByDom(document.getElementById(config.id)); if (chartInstance) { // 每个配置文件必须暴露getChartOption方法 const option = config.module.getChartOption({ metrics: normalizedData, alerts: mockData.alerts, videos: mockData.videos }); chartInstance.setOption(option, true); // true表示不合并,完全替换 } }); }这里CHART_CONFIGS是一个静态数组,定义在control.js顶部:
// control.js 开头 const CHART_CONFIGS = [ { id: 'chart-temperature', module: temperatureChart }, { id: 'chart-pressure', module: pressureChart }, { id: 'chart-status-pie', module: statusPieChart } ];这种设计让新增图表只需往数组里加一项,并确保对应charts/*.js文件导出getChartOption方法即可,无需修改主流程。
3. 核心模块拆解:data.js如何模拟真实产线数据?control.js怎样管理27个指标的生命周期?
3.1data.js:不只是随机数生成器,而是带业务语义的数据工厂
很多人以为data.js就是Math.random()堆砌,实际它实现了三层抽象:
- 物理层:模拟传感器原始输出(如PT100热电阻阻值变化);
- 转换层:将原始值按公式转为工程单位(如
temp = (resistance - 100) * 2.5); - 业务层:叠加设备状态影响(如“设备B检修中”则温度值置为
null并触发告警)。
核心函数generateMockData()结构如下:
// data.js function generateMockData() { // 1. 物理层:模拟10个温度传感器原始阻值(单位:Ω) const rawTemps = Array.from({ length: 10 }, () => 100 + Math.sin(Date.now() / 2000 + Math.random()) * 15 + Math.random() * 2 ); // 2. 转换层:转为摄氏度(PT100标准公式简化版) const temps = rawTemps.map(r => parseFloat(((r - 100) * 2.5).toFixed(1))); // 3. 业务层:根据设备状态注入异常值 const deviceStatus = getDeviceStatus(); // 返回 { 'device-A': 'normal', 'device-B': 'maintenance' } const finalTemps = temps.map((t, i) => { const deviceId = `device-${String.fromCharCode(65 + i)}`; return deviceStatus[deviceId] === 'maintenance' ? null : t; }); return { metrics: { temperature: finalTemps, pressure: generatePressureData(), // 类似逻辑 flowRate: generateFlowData(), // ... 其他24个指标 }, alerts: generateAlerts(finalTemps), // 基于温度值判断告警等级 videos: getVideoThumbnails() // 返回3个base64编码的缩略图URL }; }重点看generateAlerts()函数——它不是简单阈值比较,而是实现多级告警联动:
// data.js function generateAlerts(temps) { const alerts = { highTemp: 0, // 高温告警数 lowTemp: 0, // 低温告警数 offline: 0, // 离线设备数 maintenance: 0 // 维护中设备数 }; temps.forEach(temp => { if (temp === null) alerts.offline++; else if (temp > 80) alerts.highTemp++; else if (temp < 0) alerts.lowTemp++; }); // 关键业务规则:若高温告警≥3台,自动触发“冷却系统异常”二级告警 if (alerts.highTemp >= 3) { alerts.coolingSystem = { level: 'critical', message: '冷却系统异常,建议检查水泵' }; } return alerts; }这种设计让data.js成为业务规则的集中地。当你对接真实API时,只需重写generateMockData()为fetchRealData(),内部规则逻辑完全复用。
3.2control.js:27个指标的“交通指挥中心”
control.js承担四个核心职责:时间调度、状态同步、图表生命周期管理、异常降级。
时间调度:避免 setInterval 堆积
常见错误是直接写setInterval(updateAllCharts, 2000),但若updateAllCharts执行耗时超过2秒,会导致定时器堆积。本项目采用setTimeout链式调用:
// control.js let isUpdating = false; function scheduleUpdate() { if (isUpdating) return; isUpdating = true; updateAllCharts().finally(() => { isUpdating = false; setTimeout(scheduleUpdate, 2000); }); } // 启动 scheduleUpdate();状态同步:让所有图表共享同一份时间戳
ECharts图表常需显示“最后更新时间”,若每个图表单独取new Date(),会出现毫秒级偏差。control.js在每次更新前生成统一时间戳:
function updateAllCharts() { const updateTime = new Date(); CHART_CONFIGS.forEach(config => { const option = config.module.getChartOption({ // ... 其他数据 meta: { lastUpdate: updateTime.toISOString() } }); // ... }); }charts/*.js中可直接使用:
// charts/line-temperature.js function getChartOption(data) { return { title: { text: `温度趋势(最后更新:${new Date(data.meta.lastUpdate).toLocaleTimeString()})` }, // ... }; }图表生命周期管理:防止DOM移除后仍尝试渲染
大屏常需动态显示/隐藏某些区域(如点击按钮切换视图)。control.js维护一个activeCharts映射表:
// control.js const activeCharts = new Map(); // 初始化时注册 function initChart(id, chartModule) { const dom = document.getElementById(id); if (!dom) return; const instance = echarts.init(dom, null, { renderer: 'canvas' }); activeCharts.set(id, { instance, module: chartModule }); } // 更新时只操作激活的图表 function updateAllCharts() { activeCharts.forEach(({ instance, module }, id) => { if (instance && instance.isDisposed !== true) { const option = module.getChartOption(data); instance.setOption(option, true); } }); }异常降级:当某个图表崩溃时不影响整体
ECharts在数据格式错误时可能抛出异常导致整个更新中断。control.js对每个图表更新做独立try-catch:
function updateAllCharts() { activeCharts.forEach(({ instance, module }, id) => { try { const option = module.getChartOption(data); instance.setOption(option, true); } catch (err) { console.error(`图表 ${id} 更新失败:`, err); // 记录错误但继续下一个 logErrorToConsole(`[${id}] ${err.message}`); } }); }4. 运行与调试:从双击打开到真机部署的全流程避坑指南
4.1 本地快速验证:绕过浏览器安全策略的三种方法
现象:双击index.html在Chrome中打开,控制台报错Access to script at 'file:///xxx/data.js' from origin 'null' has been blocked by CORS policy。
原因:Chrome出于安全限制,禁止file://协议下加载本地JS文件(即使同目录)。
解决:
- 推荐方案:用Python快速起一个HTTP服务(无需安装额外工具)
# Python 3.x python -m http.server 8000 # 然后访问 http://localhost:8000/index.html - 备选方案:Chrome启动时添加参数(仅限调试)
chrome.exe --user-data-dir="C:/temp/chrome_dev" --unsafely-treat-insecure-origin-as-secure="file://" --user-data-dir="C:/temp/chrome_dev" --allow-running-insecure-content file:///path/to/index.html - 终极方案:改用Firefox,它对
file://协议更宽容(默认允许同目录JS加载)。
注意:不要用VS Code Live Server插件!它默认启用CORS头,而本项目所有请求都是同域,反而会因
Access-Control-Allow-Origin: *触发Chrome的跨域校验逻辑。
4.2 真机部署踩坑:工控机Chrome版本太低怎么办?
现象:在某国产ARM工控机(Chrome 89)上,部分ECharts图形渲染错乱,折线图变成锯齿状,地图组件空白。
原因:ECharts 4.9.0依赖Canvas 2D API的globalCompositeOperation属性,Chrome 89对此支持不完整。
解决:
- 在
style.css顶部强制启用硬件加速:.echarts-container { transform: translateZ(0); will-change: transform; } - 修改
charts/map-location.js,禁用graphic组件(它在低版本Chrome中易崩溃):// 替换原配置中的 graphic: [...] 为 graphic: null, // 或注释掉整段
4.3 数据对接实战:如何把mock数据替换成真实API?
假设你有一个REST接口GET /api/metrics返回JSON:
{ "temperature": [36.2, 37.5, null, 35.8], "pressure": [1.2, 1.3, 1.1, 1.4] }只需修改data.js中的generateMockData()为:
// data.js async function fetchRealData() { try { const res = await fetch('/api/metrics', { cache: 'no-cache' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.warn('API请求失败,回退到模拟数据:', err); return generateMockData(); // 降级策略 } } // 替换原定时器逻辑 async function scheduleFetch() { const data = await fetchRealData(); // ... 后续处理同generateMockData() }关键经验:永远保留降级路径。某次现场网络波动,API连续5分钟不可用,因有
generateMockData()兜底,大屏未黑屏,只是数据暂停更新——运维人员甚至没发现异常。
4.4 视频流缩略图:如何在无后端情况下展示RTSP流?
现象:客户要求显示3路RTSP摄像头画面,但工控机不允许装VLC或FFmpeg。
解决:利用浏览器原生<video>标签+Canvas截图(本项目已内置):
data.js中getVideoThumbnails()返回3个blob:URL(由<video>元素抓帧生成);control.js每5秒调用captureVideoFrame()函数,将<video>当前帧绘制到<canvas>,再转为base64;- 缩略图区域用
<img src="data:image/png;base64,...">显示。
代码片段(control.js):
function captureVideoFrame(videoId, canvasId) { const video = document.getElementById(videoId); const canvas = document.getElementById(canvasId); const ctx = canvas.getContext('2d'); // 设置canvas尺寸匹配视频 canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); return canvas.toDataURL('image/png'); // 返回base64字符串 }血泪经验:
<video>必须设置muted和autoplay属性,否则Chrome会阻止自动播放;RTSP流需经WebRTC网关转成H.264+MP4格式才能被浏览器识别——这部分不在本项目范围内,但index.html已预留<video>标签位置和ID命名规范。
5. 二次开发改造:如何添加新图表、接入真实数据源、定制告警推送?
5.1 添加新图表:以“设备振动频谱图”为例
假设你需要增加一个FFT频谱分析图表,使用ECharts的lines类型绘制。
步骤1:创建配置文件
新建charts/line-vibration.js:
// charts/line-vibration.js function getChartOption(data) { // 模拟FFT计算:将原始振动值转为频域数据 const freqData = data.metrics.vibration?.map((v, i) => ({ name: `${i * 10}Hz`, value: [i * 10, v * (1 + Math.sin(i * 0.5))] })) || []; return { tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: freqData.map(d => d.name) }, yAxis: { type: 'value' }, series: [{ type: 'line', data: freqData.map(d => d.value[1]), smooth: true, areaStyle: {} }], title: { text: '设备振动频谱图', textStyle: { fontSize: 14 } } }; } export { getChartOption };步骤2:注册到控制中心
修改control.js中的CHART_CONFIGS数组:
// control.js import vibrationChart from './charts/line-vibration.js'; const CHART_CONFIGS = [ // ...原有项 { id: 'chart-vibration', module: vibrationChart } ];步骤3:在HTML中添加容器index.html中插入:
<div id="chart-vibration" class="echarts-container" style="width: 600px; height: 400px;"></div>步骤4:扩展数据源
修改data.js的generateMockData(),添加vibration字段:
// data.js function generateMockData() { // ...原有逻辑 return { metrics: { // ...原有字段 vibration: Array.from({ length: 50 }, (_, i) => Math.sin(Date.now() / 1000 + i * 0.1) * 5 + Math.random() * 2 ) }, // ...其他字段 }; }至此,新图表已集成完毕。无需重启、无需构建,刷新页面即生效。
5.2 接入真实数据源:从HTTP API到MQTT的平滑过渡
当客户从HTTP切换到MQTT时,只需修改data.js的数据获取层,其他模块完全不动。
HTTP方式(已存在):
// data.js async function fetchRealData() { return fetch('/api/metrics').then(r => r.json()); }MQTT方式(新增):
安装mqtt库(CDN引入):
<!-- index.html head中 --> <script src="https://unpkg.com/mqtt@4.3.7/dist/mqtt.min.js"></script>重写data.js:
// data.js let mqttClient; function initMQTT() { mqttClient = mqtt.connect('wss://your-mqtt-broker:8083', { clientId: 'dashboard-' + Math.random().toString(16).substr(2, 8), username: 'user', password: 'pass' }); mqttClient.on('connect', () => { console.log('MQTT connected'); mqttClient.subscribe('sensor/#'); }); mqttClient.on('message', (topic, payload) => { const data = JSON.parse(payload.toString()); // 将MQTT消息存入全局缓存,供generateMockData()读取 latestMQTTData = { ...latestMQTTData, [topic]: data }; }); } // 在generateMockData()中读取缓存 function generateMockData() { return { metrics: { temperature: latestMQTTData['sensor/temp']?.values || [25, 26, 24], // ... } }; }关键设计:
generateMockData()不关心数据来源,只负责组装输出结构。无论是HTTP、MQTT、WebSocket还是本地文件,只要最终能提供符合结构的数据,上层图表渲染完全无感。
5.3 定制告警推送:当温度超限时发微信通知
本项目默认只在页面右上角弹出告警气泡。若需外发通知,只需在data.js的告警判定逻辑后插入钩子:
// data.js function generateAlerts(temps) { const alerts = { /* ...原有逻辑 */ }; // 新增:检测到高温告警时触发推送 if (alerts.highTemp > 0 && shouldSendWechat()) { sendWechatAlert(alerts); } return alerts; } function shouldSendWechat() { // 防抖:5分钟内只发一次 const now = Date.now(); if (lastWechatTime && now - lastWechatTime < 5 * 60 * 1000) return false; lastWechatTime = now; return true; } async function sendWechatAlert(alerts) { try { // 调用企业微信机器人Webhook(需提前配置) await fetch('https://qyapi.weixin.qq.com/...webhook...', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ msgtype: 'text', text: { content: `【大屏告警】检测到${alerts.highTemp}台设备温度超标,请及时处理!` } }) }); } catch (err) { console.error('微信推送失败:', err); } }从那以后我每次加告警通道,都强制走一遍「模拟触发→查看日志→确认接收」闭环。哪怕只是发到钉钉,也得用curl手动调一次Webhook,因为线上环境的网络策略往往比本地严苛十倍。希望帮到你。
本文还有配套的精品资源,点击获取