Vue 2 + ECharts 能源管理系统前端实战:从电表抄数到能效看板

发布时间:2026/10/17 16:31:07
Vue 2 + ECharts 能源管理系统前端实战:从电表抄数到能效看板
简介这是一套基于Vue.js构建的能源管理系统完整项目源码面向前端开发者、企业能源管理岗位人员用于解决能耗数据采集、可视化展示、统计分析及能效告警等实际业务需求。压缩包共含477个文件包括138个Vue组件、108张PNG图片、92个CSS样式及49个JavaScript脚本等另有SVG图标、SCSS样式、字体文件与Dockerfile等配套资源整体大小约20.97MB目录结构完整清晰。资源在CSDN已有1274人浏览学习。源码中融合了Vuex状态管理、ECharts图表展示、Axios请求封装以及Element UI组件库等主流技术实践并涵盖数据采集、能耗分析、报表生成、权限管理、移动端适配等模块可直接用于二次开发或作为企业级中后台项目的参考范例。1. 能源管理系统 Vue从“电表抄数”到“能效看板”的一次务实改造今年年初接手了一个园区能源管理系统的前端重构需求说简单也简单——把原来每天靠人工抄表、月底用 Excel 汇总的流程换成一套能在手机上随时查看水电用量的 Web 界面。说复杂也复杂因为能源数据的采集点分散既有电表、水表又有独立的空调和照明回路光是把这些数据源统一到一张看板上就够折腾一阵。这个项目最终以 Vue 2 Element UI ECharts 为前端核心后端用 Spring Boot 提供接口整体是典型的「轻前端、薄后端」架构。它适合两类人一类是想找一套能直接改改就跑的能源大屏 / 管理后台模板的开发者另一类是正在做 IoT 数据可视化、需要参考前端图表联动和告警交互逻辑的同学。下文会把它拆开从目录结构、核心代码到跑通步骤、改造方法逐一过一遍。2. 项目定位与技术选型为什么是 Vue 2 而不是 Vue 32.1 这套系统要解决的核心问题很多中小型园区的能源管理现状是各栋楼的电表独立计量数据散落在不同供应商的采集器里运维人员每周去现场抄表再手工录入系统。这套能源管理系统的目标就是把「采集、传输、存储、展示、告警」串成一条完整的链路。前端负责的是链路的后半段——从后端接口拿数据用表格、图表、大屏卡片把它呈现出来同时提供设备管理和告警处理入口。从功能模块来看前端主要包含五个页面群总览看板Dashboard、用电/用水趋势分析、设备管理、告警中心、报表导出。其中最核心的不是某个页面有多炫而是图表与筛选条件的联动关系。比如在总览看板里选择「4 号楼」下面所有图表都要跟着切换数据源在趋势分析里切换「日 / 月 / 年」维度折线图和柱状图要同步更新。这个联动逻辑决定了状态管理方案。2.2 技术栈选型的取舍这套项目用的是 Vue 2.6.11 Element UI 2.15 ECharts 5 vue-router 3 vuex 3 axios。有人可能会问为什么不用 Vue 3考虑到接手成本和稳定性——后端同事对 Vue 2 更熟而且 Element UI 的表格、表单、树控件在管理后台场景足够成熟踩坑资料也多。ECharts 5 则是在图形渲染和按需引入上比 4.x 更友好打包体积也能压小一些。状态管理上项目没有滥用 Vuex只在store/modules/energy.js里维护了「当前选中的楼栋 / 区域 / 时间维度」三个全局状态以及对应的 getter。页面内部的交互状态比如表格当前页、图表 loading 状态放在组件自身 data 里。这个取舍很务实跨页面共享的才进 store页面私有的就地管理避免复杂化。2.3 与后端对接的接口约定前端所有数据请求都收敛在src/api/目录下按业务域拆成了dashboard.js、energy.js、device.js、alarm.js、report.js五个文件。后端接口约定统一返回格式{ code: 200, message: success, data: {} }axios 封装在utils/request.js里统一处理请求前带上 token响应后先判断code非 200 直接走全局错误提示并支持通过responseType: blob透传下载请求。这套约定让前端不需要在每个页面里重复做异常判断也不容易出现「接口返回了但页面不知道是该弹错误还是渲染数据」的模糊状态。3. 目录结构与数据流一张图看懂前端工程骨架3.1 前端工程目录说明拿到源码后第一件事不是急着npm install而是先看目录结构搞清楚每个目录的职责边界。这套项目解压后的关键目录如下energy-management-frontend/ ├── public/ │ └── index.html ├── src/ │ ├── api/ # 接口请求层按业务域拆文件 │ ├── assets/ # 静态资源图片、全局样式 │ ├── components/ # 通用组件图表、上传、分页 │ ├── router/ # 路由配置含懒加载 │ ├── store/ # vuex 状态管理 │ │ ├── modules/ │ │ │ └── energy.js # 能源模块全局状态 │ │ └── index.js │ ├── utils/ # 工具函数request、echarts 封装 │ ├── views/ │ │ ├── dashboard/ # 总览看板 │ │ ├── energy/ # 用能趋势分析 │ │ ├── device/ # 设备管理 │ │ ├── alarm/ # 告警中心 │ │ └── report/ # 报表导出 │ ├── App.vue │ └── main.js ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 └── package.json这套目录设计遵循了一个原则按业务模块组织 views按复用性组织 components。components/下的组件是无状态的、通过 props 接收数据的views/下的页面组件负责拉数据和拼装。比如components/EnergyChart.vue不关心数据从哪来只接收option和loading两个 prop这样就保证了图表组件可以在趋势分析、总览看板等多个地方复用而不需要复制粘贴初始化代码。3.2 路由与页面懒加载路由配置在src/router/index.js所有页面都用了动态导入做懒加载这样首屏只加载 dashboard 相关的代码块其他模块在路由切换时才按需拉取。路由表大致如下const routes [ { path: /, redirect: /dashboard }, { path: /dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 总览看板, icon: dashboard } }, { path: /energy, name: Energy, component: () import(/views/energy/index.vue), meta: { title: 用能趋势, icon: trend } }, { path: /device, name: Device, component: () import(/views/device/index.vue), meta: { title: 设备管理, icon: device } }, { path: /alarm, name: Alarm, component: () import(/views/alarm/index.vue), meta: { title: 告警中心, icon: alarm } }, { path: /report, name: Report, component: () import(/views/report/index.vue), meta: { title: 报表导出, icon: report } } ];这里有个细节值得注意meta.title字段不仅用于浏览器标签页标题还驱动了侧边栏菜单的渲染。菜单没有单独写死而是通过遍历路由表动态生成这样后续新增页面只需要在路由表加一条记录侧边栏和面包屑会自动跟着变。这就是为什么在后续二次开发时你不需要去改菜单组件只要改路由就行。3.3 从接口到图表的完整数据流理解这套系统的数据流只需要抓住一条主线页面 mounted → 读取 vuex 里的筛选条件 → 调用 api 方法 → 拿到后端数据 → 拼装 ECharts option → 渲染图表 → 用户操作 → 更新 vuex → 触发重新拉数。以总览看板为例views/dashboard/index.vue在created生命周期里派发fetchDashboardData这个 actionaction 内部先读取当前楼栋和时间维度然后并发调用api/dashboard.js里的多个接口用电总量、趋势、占比、告警数。接口数据返回后分别交给不同的图表组件渲染。由于数据源和图表组件是分离的你可以在 DevTools 的 vuex 面板里直接修改当前选中的楼栋 ID然后观察所有图表是否联动更新——这是验证状态管理是否合理的最快方式。4. 核心模块拆解一个请求从进入到返回的完整链路4.1 axios 封装拦住所有异常的黑匣子utils/request.js是整个前端的数据出入口所有请求都经由它发出。它的核心逻辑是请求拦截器和响应拦截器代码结构大致如下import axios from axios; import { Message } from element-ui; import store from /store; const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }); // 请求拦截器附加 token service.interceptors.request.use( config { if (store.getters.token) { config.headers[Authorization] Bearer store.getters.token; } return config; }, error Promise.reject(error) ); // 响应拦截器统一处理业务错误 service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message || 请求失败); if (res.code 401) { store.dispatch(user/logout); } return Promise.reject(new Error(res.message)); } return res.data; }, error { Message.error(error.message || 网络异常); return Promise.reject(error); } ); export default service;这段代码的关键在于return res.data而不是return response——它把业务数据直接抛给了调用方页面里就不用写res.data.data这种丑代码了。拦截器里把所有非 200 的响应都做成了全局 Message 提示这样页面组件里只需要关心成功分支异常分支统一兜底省掉了大量重复的 try/catch。401 退出登录的逻辑也放在这里而不是散落在各个页面。用的时候API 模块里的写法就非常干净import request from /utils/request; export function getEnergyTrend(params) { return request({ url: /energy/trend, method: get, params }); }页面里调getEnergyTrend({ buildingId: 4, period: month })拿到的直接就是data字段里的内容。如果你在跑项目时发现所有接口都报「Cannot read property data of undefined」大概率是后端返回值没按{ code, message, data }约定来拦截器拿不到data就会抛异常这是最常见的对接翻车点。4.2 图表联动ECharts 封装与响应式容器的处理图表是这个项目的灵魂而图表最容易出的问题就是容器没高度导致渲染空白。项目中用components/EnergyChart.vue统一封装了 ECharts 的初始化和自适应逻辑template div refchartRef classenergy-chart :style{ height: height }/div /template script import * as echarts from echarts; export default { name: EnergyChart, props: { option: { type: Object, required: true }, height: { type: String, default: 300px } }, data() { return { chart: null }; }, watch: { option: { handler(newVal) { this.chart.setOption(newVal, true); }, deep: true } }, mounted() { this.initChart(); window.addEventListener(resize, this.handleResize); }, beforeDestroy() { window.removeEventListener(resize, this.handleResize); if (this.chart) { this.chart.dispose(); } }, methods: { initChart() { this.chart echarts.init(this.$refs.chartRef); this.chart.setOption(this.option); }, handleResize() { this.chart this.chart.resize(); } } }; /script style scoped .energy-chart { width: 100%; } /style几个参数说明option是父组件拼好的 ECharts 配置对象必须包含xAxis、yAxis、series等字段height控制图表高度默认 300px总览看板里会传不同的高度值来适配卡片布局。watch 里setOption(newVal, true)的第二个参数意为「不合并直接替换」这样切换楼栋或时间维度时图表不会残留上一份数据的痕迹。有个血泪经验是 ECharts 初始化必须在容器渲染完成后执行所以mounted里不能省this.$nextTick——如果容器被v-if控制初始化时 DOM 可能还不存在。这套代码里虽然直接在mounted里调用但前提是父组件保证v-if条件已满足如果你接手后改了父组件结构记得要先await this.$nextTick()再echarts.init。4.3 设备管理表格筛选、分页与状态切换一体的完整页面设备管理页面是所有业务页里最「管理后台」的页面它演示了 Element UI 表格 分页 表单筛选的组合用法。核心逻辑分三块查询条件、表格数据、分页行为。export default { data() { return { queryParams: { deviceType: , status: , keyword: , pageNum: 1, pageSize: 10 }, deviceList: [], total: 0, loading: false }; }, created() { this.fetchDeviceList(); }, methods: { async fetchDeviceList() { this.loading true; try { const data await getDeviceList(this.queryParams); this.deviceList data.records; this.total data.total; } finally { this.loading false; } }, handleSearch() { this.queryParams.pageNum 1; this.fetchDeviceList(); }, handleStatusChange(row) { // 切换设备启停状态后更新列表 updateDeviceStatus({ id: row.id, status: row.status }) .then(() { this.$message.success(状态更新成功); }) .catch(() { row.status row.status 1 ? 0 : 1; // 失败回滚 }); } } };分页参数pageNum和pageSize是后端标准分页约定handleSearch里把页码重置为 1是为了避免用户在第三页筛选时页面为空——这个细节新手经常漏掉。handleStatusChange里的「失败回滚」也值得学习前端先乐观更新状态开关如果接口报错就回滚到旧值而不是等接口返回后再刷新整页数据。在表格操作类场景下这种交互比「转圈等待」要快很多。4.4 告警中心轮询、声音提醒与批量处理的综合模块告警中心是这套系统里后端到前端反馈最实时的模块。由于项目初期没有接入 WebSocket告警数据采用了定时轮询的方式每 30 秒拉一次最新未处理告警有新告警时触发浏览器通知和列表刷新。轮询逻辑放在views/alarm/index.vue的created里created() { this.fetchAlarmList(); this.timer setInterval(() { this.fetchAlarmList(true); }, 30000); }, beforeDestroy() { clearInterval(this.timer); }fetchAlarmList(true)里的true参数表示静默刷新——不显示 loading不打断用户操作只更新列表和未读数量。如果发现后端返回的新告警 ID 不在当前列表里就追加到顶部并播放提示音。告警处理的操作是单条确认和批量确认批量确认通过记录当前勾选项的 ID 数组一次调用后端批量更新接口。这个模块的常踩坑点在组件销毁时忘记clearInterval导致页面切到其他路由后还在后台继续请求接口压力倒是小事主要是控制台会报「Avoid mutating a prop directly」之类的问题——因为定时器的回调作用域已经脱离了当前组件渲染上下文。所以我自己在写这类轮询时一定会同时在activated和deactivated里做开启和暂停配合keep-alive使用更严谨。5. 运行与避坑记录从 clone 到跑通的完整过程5.1 环境依赖与安装步骤这套项目的前端运行环境要求 Node 14 以上、npm 6 以上Vue 2.6 对 Node 版本不挑但建议 14 LTS 起步。拿到源码后按以下步骤执行# 1. 安装依赖建议用 npm不用 cnpm npm install # 2. 启动开发服务 npm run dev如果npm install过程中报 node-sass 相关的错大概率是 Node 版本太高或太低导致原生模块编译失败。这类问题在 Vue 2 项目里非常常见通常的解决方式是把node-sass换成sass或者在.npmrc文件里加一行sass_binary_sitehttps://npmmirror.com/mirrors/node-sass/重新安装即可。项目默认的.env.development配了一个本地代理转发VUE_APP_BASE_API值为/api开发服务器把/api前缀的请求转发到http://localhost:8080这个后端服务。前端和后端的开发端口约定是前端 9527后端 8080如果后端端口改了只需要同步修改vue.config.js里的devServer.proxy.target字段。5.2 五个最容易翻车的问题排查第一类问题ECharts 图表空白或只显示坐标轴不显示数据。现象是页面加载了但折线没有画出来。原因通常是传给option的数据为空数组或者容器的height为 0。排查方法是打开 DevTools 选中图表容器元素看 Computed 样式里的高度如果是auto说明样式没拉取成功。解决思路是先给容器写死高度比如styleheight: 400px再确认series.data里确实有数值。第二类问题路由切换后页面白屏控制台报错Cannot read property xxx of null。现象是首次进入页面正常切走后回来就崩了。原因多半是beforeDestroy里dispose了 ECharts 实例或clearInterval了定时器但组件缓存场景下activated没有重新初始化。解决方式是加activated钩子来检测实例状态如果this.chart不存在就重新initChart。第三类问题接口请求返回 404但后端接口明明能通。现象是前端请求路径是/api/energy/trend后端也暴露了/energy/trend。原因通常是devServer.proxy没有把/api前缀剥掉后端接口路径根本不含/api。解决方式是在vue.config.js的代理配置里加pathRewrite把/api替换成空字符串。第四类问题导出报表时下载下来的 CSV 用 Excel 打开中文乱码。原因很经典CSV 文件缺少 UTF-8 的 BOM 头Excel 默认按 ANSI 解析。解决方式是在生成 Blob 时前缀\uFEFF字符代码里是这样处理的const blob new Blob([\uFEFF csvContent], { type: text/csv;charsetutf-8; });第五类问题打包部署上线后刷新页面 404。原因是路由用的是history模式刷新时请求的路径没有对应的后端文件。解决方式有两种一是改成hash模式省事二是在 Nginx 配置try_files $uri $uri/ /index.html;做 fallback。这套项目默认是history模式所以部署文档里必须写清楚 Nginx 的配置不然上线后一刷新就翻车。5.3 开发环境联调的几个小技巧联调阶段我习惯在.env.development里直接配后端的完整地址而不是走代理——比如VUE_APP_BASE_API https://test-api.example.com。这样做的好处是后端同事不需要本机起服务也能看到前端请求是否到达网关坏处是会产生跨域所以需要在 axios 封装里允许携带凭证。另一个技巧是用浏览器的 Network 面板加一个「Fetch/XHR」筛选专门看/energy/前缀的接口耗时如果单次接口超过 3 秒就要提醒后端看慢查询了——能源系统的数据量通常不大超过 3 秒多半是 SQL 没走索引。6. 二次开发给这套 Vue 能源系统加一个新模块的完整思路6.1 改造一把 Mock 数据切换成真实后端数据如果你的手头没有现成的后端接口项目里确实内置了一份 mock 数据放在src/mock/目录。开发环境下通过mockjs拦截 axios 请求。当后端接口准备好后需要做两步切换第一步是注释掉main.js里的import ./mock这一步很多人会漏导致明明配了真实接口地址请求还是被 mock 拦掉第二步是确认每个 api 方法的url和后端Controller里的路径一致。我一般会在启动项目前先花 10 分钟把src/api/下每个函数的url和参数列表打印出来整理成一张表格发给后端同事对照——在这个项目里getEnergyTrend的参数是buildingId和periodgetDeviceList是deviceType/status/keyword/pageNum/pageSize提前对好能省下大量联调时来回扯皮的时间。6.2 改造二新增一个「碳排放概览」页面假设你要在这个系统里加一个碳排放概览页可以完全复用现有框架的套路。第一步在src/views/下新建carbon/目录页面结构参考dashboard/index.vue包含顶部卡片总排放量、减排量、碳强度和底部趋势图。第二步在src/api/下新建carbon.js写好三个接口函数。第三步在路由表里新增一条记录路由 name 取Carbonpath 取/carbon组件懒加载指向新页面菜单就会自动出现。如果是想给现有页面加图表更聪明的做法是直接复用EnergyChart.vue组件只需拼好option传入。拼option的代码可以抽成src/utils/chartOptions.js按图表类型导出工厂函数比如createLineOption({ xData, yData })、createPieOption({ data })这样图表配置不会散落在每个页面里后续统一调整配色或字体只需要改工厂函数。6.3 改造三多租户数据隔离的思路如果这套系统要卖给多个园区使用设备数据需要按租户隔离。常见做法是给每张表加一个tenant_id字段前端在 axios 请求拦截器里从 vuex 读取当前租户 ID统一加进请求头X-Tenant-Id后端通过过滤器解析后注入 SQL 查询条件。前端不需要每个页面都显式传这个参数改造量集中在拦截器和一个全局的下拉切换组件上。这种方案比「每个接口都加 tenantId 参数」要干净得多也不容易漏。夹带一个个人习惯每次改完这种跨页面联动的逻辑我都会强制走一遍回归流程——先在总览看板切换楼栋确认图表全部更新再切到告警中心确认没有因 store 更新导致报错最后刷新页面确认路由懒加载正常。这套项目涉及的状态分散在页面和 store 里不动状态管理就别动全部页面改状态管理就一定要回归所有依赖它的页面。从那以后我每次改能源系统的公共状态都会把「看板、趋势、告警」三个页面强制点一遍顺手看一眼 Network 面板里有没有红色请求。希望帮到你。本文还有配套的精品资源点击获取