ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Vue2和ECharts的能源管理系统前端实战解析

基于Vue2和ECharts的能源管理系统前端实战解析

简介:能源管理系统Vue是基于Vue.js生态构建的能源管理前端项目,面向需要掌握企业级中后台开发的前端工程师与相关专业学生,解决能源数据实时采集、可视化展示与能耗分析等落地问题。资源包共477个文件,压缩后约21MB,其中138个vue组件构成核心页面逻辑,108个png与25个svg提供界面图标与视觉素材,92个css/scss负责样式布局,49个js实现交互与状态管理,另有配置文件、字体及测试样例等,整体结构完整,便于二次开发。目前已有1274人学习下载。从中可了解Vuex状态管理、ECharts图表接入、Axios请求封装、Vue CLI构建流程及Element UI组件库的综合运用,适合作为能源管理类系统开发的参考模板,也适合对照学习企业级Vue项目的目录组织与模块划分方式。上手后可基于现成模块扩展水电暖气等多类能源数据看板。

1. 能源管理系统 Vue 项目:把能耗数据从 Excel 报表搬到实时看板

在某个园区项目上,电工每天抄一次表,月底才能用 Excel 汇总出整栋楼的用电曲线,发现异常时已经过去半个多月。这套能源管理系统 Vue 前端项目解决的就是这个痛点:它把电、水、气、热等能耗数据接到统一的 Web 端,按小时、日、月维度自动聚合展示,支持大屏可视化、能耗趋势分析、设备告警和指标排名,管理员打开浏览器就能看到当前时刻的用能情况。项目本身是一套基于 Vue 2 全家桶的前端工程,配合 ECharts 做数据可视化,适合想快速搭起能源监控看板的前端开发者,也适合实施人员拿去做二次改造。下面我从架构、核心代码、运行步骤到改造点一层层拆开讲。

2. 项目架构与技术选型:Vue 全家桶加 ECharts 的工程化底座

2.1 技术栈:为什么选 Vue 2.6 而不是 Vue 3

这份源码拆下来看,主框架是 Vue 2.6,搭配 Vue Router 3、Vuex 3,UI 组件库用的 Element UI,图表这块是 ECharts 4.x,请求库是 Axios。时间处理用的 moment,样式预处理用的 sass。这套组合在能源、大屏这类后台管理项目里属于非常成熟的搭配,社区案例多,出了问题搜答案快,比 Vue 3 生态里部分组件还在磨合期要稳。

有人会问为什么不用 Vue 3。我的判断是这类项目往往要对接老旧的后台接口和第三方图表插件,Vue 2 的兼容面更宽,而且 Element UI 在 Vue 2 下功能齐全,表格、表单、树形控件都能直接用于设备管理和能耗报表。ECharts 4 的按需引入方式在 5.x 里也保留了大差不差的写法,后面要升级成本可控。如果你是从零起步且不需要兼容老浏览器,换 Vue 3 + Element Plus 也完全可以,但拿到这份源码时停留在 Vue 2.6 是合理的,跑通比追新更重要。

模块选型作用
框架Vue 2.6页面视图与组件化组织
路由Vue Router 3后台布局、大屏、分析页路由管理
状态Vuex 3跨组件共享能耗数据与筛选条件
UIElement UI表格、表单、弹窗、消息通知
图表ECharts 4折线图、柱状图、饼图、大屏可视化
请求Axios统一请求出口与拦截器
样式sass大屏布局与主题变量管理

2.2 目录结构:按业务域拆分的 src 组织

这份资源的目录结构延续了 vue-cli 的标准骨架,src 下面按业务域而不是按文件类型分包,这一点做得好。下面是我拆解后的目录:

src/ ├── api/ # 接口定义层,每个模块一个文件 │ ├── energy.js # 能耗数据相关接口 │ ├── device.js # 设备管理接口 │ └── alarm.js # 告警相关接口 ├── assets/ # 静态资源与全局样式 ├── components/ # 公共组件 │ ├── ChartCard.vue # 图表卡片封装 │ ├── ScreenWrapper.vue # 大屏自适应容器 │ └── TrendLine.vue # 趋势线图通用组件 ├── layout/ # 后台布局:侧边栏、顶栏、标签页 ├── mock/ # 本地 Mock 数据 ├── router/ # 路由配置 ├── store/ # Vuex 模块化状态 │ ├── modules/ │ │ ├── energy.js │ │ ├── device.js │ │ └── user.js │ └── index.js ├── utils/ # 工具函数 │ ├── request.js # Axios 封装 │ ├── chart.js # ECharts 按需引入与公共配置 │ └── format.js # 数字、单位、时间格式化 ├── views/ # 页面 │ ├── dashboard/ # 数据看板 │ ├── analysis/ # 能耗分析 │ ├── alarm/ # 告警中心 │ └── device/ # 设备管理 └── main.js

api 层把后端接口定义集中到独立文件里,页面里不直接写 URL。后面如果后端路径调整,只需要改 api/energy.js 一个文件。views 按功能页分,dashboard 是大屏,analysis 是分析报表,alarm 是告警,device 是设备档案,基本覆盖能源管理系统的四个核心场景。

2.3 数据流设计:Axios 请求封装与 Vuex 模块化状态

这个项目的请求封装写在 utils/request.js,思路是统一创建 Axios 实例,设置 baseURL 和超时时间,然后在拦截器里注入 token 和错误提示。核心片段如下:

// 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: 15000 // 15 秒超时,大屏轮询接口不宜更长 }) // 请求拦截器:带上登录态 service.interceptors.request.use( (config) => { const token = store.getters.token if (token) { config.headers['X-Token'] = token } return config }, (error) => Promise.reject(error) ) // 响应拦截器:统一处理业务码和 HTTP 错误 service.interceptors.response.use( (response) => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') // 业务码 401 表示登录失效,清掉登录态跳转登录页 if (res.code === 401) { store.dispatch('resetToken') location.reload() } return Promise.reject(new Error(res.message)) } return res.data }, (error) => { Message.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

baseURL 由 VUE_APP_BASE_API 环境变量控制。开发环境代理到本机 Mock 端口,生产环境指向真实网关。timeout 设 15000 是给大屏轮询接口留的余量。响应拦截器里的 code 约定是后端返回的业务码,200 代表成功,401 代表登录失效,项目里所有接口都走这条统一出口。页面里调用接口时只关心业务数据本身,不用重复写错误提示。

页面数据一部分直接通过 api 层在组件内请求,一部分全局共享的数据放在 Vuex modules。energy.js 模块里维护当前选中时间粒度、刷新频率、能耗原始列表,供大屏多个图表同时消费;device.js 维护设备列表和筛选条件。这么做的好处是大屏上多个图表组件对同一份数据源取值,不会出现各组件自己请求、数据口径不一致的问题。

我拆代码时的一个感受是,很多同类项目喜欢把请求全堆在页面里,一到联调阶段改接口地址能找到人崩溃。这套源码把 api 单独成层,Vuex 只放跨组件共享的数据,单页私有的数据留在组件 data 里,职责划分比较清晰,二次开发时照着这个约定加模块就行。

提示:大屏项目的接口超时时间不建议设太短,能耗数据接口通常要聚合多张表,查询耗时比普通业务接口长,15 秒是这类项目里比较常见的取值。

3. 核心功能拆解:大屏看板、能耗分析与告警中心的完整链路

3.1 能耗大屏:ECharts 图表从数据请求到渲染的组装过程

大屏是这类系统的门面,views/dashboard/index.vue 页面整体用百分比加 flex 布局,分成顶部标题区、中间主图区、左右辅图区。每个图表都不是直接在页面里写 option,而是通过 ChartCard 包裹。ChartCard 负责创建 DOM 容器、初始化 echarts 实例、监听窗口 resize 并调用实例的 resize 方法。

核心代码在 components/ChartCard.vue 里,简化后关键逻辑如下:

<template> <div ref="chartRef" class="chart-card"></div> </template> <script> import * as echarts from 'echarts' export default { props: { option: { type: Object, required: true }, // 父组件传入的 echarts option loading: { type: Boolean, default: false } }, data() { return { chart: null } }, watch: { option: { deep: true, handler(val) { // option 变化后整体替换渲染,避免旧数据残留 this.chart.setOption(val, true) } } }, mounted() { this.initChart() window.addEventListener('resize', this.handleResize) }, beforeDestroy() { window.removeEventListener('resize', this.handleResize) this.chart.dispose() // 关键:销毁实例,避免内存泄漏 }, methods: { initChart() { this.chart = echarts.init(this.$refs.chartRef) this.chart.setOption(this.option) }, handleResize() { this.chart && this.chart.resize() } } } </script>

这里有两个容易被忽略的细节。一是 props 里 option 加 deep 监听,大屏上数据轮询回来后父组件修改 option 的 series.data,子组件自动 setOption 更新;二是 setOption 第二个参数传 true,表示不再用 merge 方式合并,而是整体替换。beforeDestroy 里 dispose 是血泪经验换来的,大屏页面来回切换如果不销毁 echarts 实例,内存会一路涨上去,这个问题我在第 5 章专门讲。

图表数据来源是 api/energy.js 里的 fetchEnergyTrend,页面在 mounted 里请求一次,然后开一个 setInterval 按刷新频率定时请求。轮询的典型写法:

// views/dashboard/index.vue 片段 mounted() { this.loadTrend() this.startPolling() }, methods: { async loadTrend() { const params = { granularity: this.granularity, // hour / day / month startTime: this.range[0], endTime: this.range[1] } const data = await this.$store.dispatch('energy/fetchTrend', params) // 把返回结果组装成 echarts 需要的 xAxis 和 series 格式 this.trendOption = this.buildTrendOption(data) }, startPolling() { this.timer = setInterval(this.loadTrend, this.refreshInterval * 1000) } }

注意 startPolling 里的定时器要在 beforeDestroy 里 clearInterval。buildTrendOption 是把接口返回的扁平数组映射成 xAxis 的 time 字段和 series 的 value 字段,属于纯函数,方便单元测试。这个函数里做了数据降噪,比如把 null 转成连接符,保证折线图在断点处断开而不是直接归零。

3.2 能耗分析:时间聚合、同环比与单位指标计算

analysis 页面承担能耗分析的三大任务:趋势图、排名表、同环比指标卡。它和 dashboard 的区别在于分析页更看重筛选条件,比如按区域、按能源类型、按时间范围筛选,然后展示多维对比。

同环比的算法在 utils/format.js 里,是独立函数,代码不长但口径容易翻车:

// utils/format.js export function calcCompare(current, previous) { if (!previous) { return { rate: null, direction: 'up' } } const rate = ((current - previous) / previous) * 100 const direction = rate >= 0 ? 'up' : 'down' return { rate: Math.abs(rate).toFixed(2), direction } }

调用方拿到当前值、上期值,然后计算 (current - previous) / previous。之所以要返回 direction 字段,是因为前端展示时上升和下降要分别用不同颜色显示,能源场景里耗能上升是坏消息,所以往往用绿色代表下降、红色代表上升,与常规涨跌配色相反。previous 为 0 时直接返回 null,避免除零错误,这是我在实际项目里踩过的坑——某个月某栋楼确实没有用能数据,计算直接炸出 Infinity。

时间聚合逻辑在 store/modules/energy.js 里。granularity 为 hour 时返回当日 24 个点,day 时返回当月每天,month 时返回全年 12 个月。前端拿到后按 xAxis 展示。这里要注意后端返回的时间字段格式必须统一,我见过同一个接口有的返回时间戳,有的返回字符串,前端 formatTime 里就得做兼容判断。

分析页还有两个单位换算函数也放在 format.js 里:能耗数值大于 10000 时转成万 kWh,小于 1 时转成 kWh 并保留两位小数。这类函数看着简单,但如果不集中管理,每个页面自己写一套,最后会出现同一份数据在大屏显示 12.5,在报表页显示 12500 的口径混乱。

3.3 设备告警:阈值判断与轮询通知机制

alarm 模块负责设备超限告警。设备表里每个设备配置了能耗上限阈值,前端定时轮询告警接口,发现新告警后推送 Element UI 的 Notification 消息,并在顶部铃铛图标上显示未读数。告警列表页的核心是一个带 tab 的表格,tab 区分未处理、已处理、全部,状态切换调 updateAlarmStatus 接口,把告警标记为已处理。

轮询逻辑单独抽成了 mixin,避免大屏和分析页重复写定时器代码:

// mixins/polling.js export default { data() { return { pollTimer: null, pollInterval: 30000 // 默认 30 秒一次 } }, mounted() { this.startPoll() }, beforeDestroy() { this.stopPoll() }, methods: { startPoll() { this.stopPoll() this.pollTimer = setInterval(this.pollHandler, this.pollInterval) }, stopPoll() { if (this.pollTimer) { clearInterval(this.pollTimer) this.pollTimer = null } } } }

mixin 里 mounted 启动、beforeDestroy 清理,子组件只要实现 pollHandler 方法。这个约定比每个页面自己写一遍定时器干净。告警轮询间隔 30 秒,如果对接真实物联网平台,更合适的做法是改用 WebSocket 推送,源码里保留了切换点,把 startPoll 换成 socket 连接即可,不用改业务组件。

pollHandler 里还有个细节:新告警数量通过 store 里的 alarmCount 维护,顶部铃铛用这个值显示红点。读取告警接口时把上一次拉取的时间戳带过去,后端只返回这个时间点之后新增的告警,前端再决定要不要弹 Notification。这样避免每次轮询都把历史告警重新弹一遍。

4. 本地运行与接口联调:从 npm install 到生产构建的落地步骤

4.1 环境准备:Node 版本与依赖安装

项目基于 vue-cli 4 构建,依赖在 package.json 里有锁定版本。跑起来之前先确认本机 Node 版本。vue-cli 4 配 Node 10 到 12 比较稳妥,如果你本机是 Node 14 以上,node-sass 大概率会在 npm install 阶段编译失败,这事我在第 5 章单独说。

安装命令就是常规两步:

# 安装依赖,建议全程用 npm,不要混用 yarn 的 lock 文件 npm install # 启动开发服务,默认端口 8080 npm run dev

npm install 如果报 node-gyp 相关的错,先检查本机 Python 环境,Windows 上还需要 Visual Studio Build Tools。macOS 上常见的是 node-sass 二进制下载失败,可以用镜像源重新装。依赖装完后看到 node_modules 里有 element-ui 和 echarts 目录,基本就没问题。

4.2 本地开发:dev server 与 Mock 数据切换

项目在根目录建了 mock 目录,里面按模块放了 JSON 文件。开发时有两种跑法:一种是直接用 vue.config.js 里配置的 devServer 代理到后端联调环境,另一种是纯前端场景下用 Mock 数据顶替。我拆这个项目的习惯是先看 mock 目录和 src/api 的映射关系,Mock 通了再切真实接口。

vue.config.js 里代理配置大致是这样:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/energy-api': { target: 'http://192.168.1.100:9000', // 后端联调地址 changeOrigin: true, pathRewrite: { '^/energy-api': '' } } } } }

pathRewrite 把 /energy-api 前缀去掉。后端接口如果自带 /api 前缀,需要自己调整这里的正则。changeOrigin 必须为 true,否则后端拿到请求的 Host 头不对,部分网关会直接拒掉。

Mock 数据和真实接口切换的开关在 .env.development 文件里,内容类似这样:

# .env.development VUE_APP_BASE_API=/energy-api VUE_APP_USE_MOCK=true

VUE_APP_USE_MOCK 为 true 时,api 层请求走到 mock 适配器;设为 false 时走真实代理。切换后需要重新 npm run dev 让环境变量生效。Mock 数据的 JSON 结构和后端返回结构必须保持一致,前端代码不需要感知数据来源。

4.3 生产构建:打包参数与部署注意事项

生产构建命令:

npm run build

产物在 dist 目录,打包后的资源默认带 hash。部署时有两个常见注意点:接口域名要在 .env.production 里配成线上网关;路由如果用的 history 模式,nginx 需要加 try_files 回退,否则二级页面刷新直接 404,这个在第 5 章展开。

如果打包体积过大,建议看打包分析。这个项目大屏图表多,echarts 按需引入能明显减包,改好的部分在 src/utils/chart.js 里维护 echarts 组件的按需加载列表。只需要折线图、柱状图、饼图、仪表盘时,注册这几个组件就够,能省下近一半的 echarts 包体。

5. 排查与避坑:白屏、图表空白、跨域和内存泄漏实录

5.1 现象一:npm install 时 node-sass 编译失败

现象:npm install 进行到 node-sass 阶段报错,提示 failed at the node-sass@4.13.1 postinstall script,终端红字,依赖装不上,项目根本起不来。

原因:node-sass 依赖本机 Python 和 C++ 编译环境,安装时会下载对应平台的二进制文件;国内网络下载源不稳定,加上部分开发者本机缺编译工具链,导致 postinstall 脚本执行失败。

解决:先把 npm 镜像源切到国内镜像再重装。如果还失败,可以把依赖里的 node-sass 整体换成 sass,package.json 里改掉声明,代码里 import 路径不变。也可以直接升级 Node 到 14 以上并改用项目自带的 sass 版本,前提是样式代码没有用到 node-sass 独有语法。换完后删除 node_modules 和 package-lock.json 重新 install 一次。

5.2 现象二:ECharts 图表只显示空白容器

现象:页面渲染出来了,卡片区域是空白,控制台没有报错或者只有一个警告,图表的 DOM 元素没画出来。

原因:最常见的是 echarts.init 执行时容器的宽高为 0。ChartCard 组件在父组件布局还没稳定时 mounted,容器宽度可能是 0;另一种情况是 ECharts 升级后,按需引入没注册完整的图表类型,比如折线图忘了注册 LineChart。

解决:先确认容器父级有明确的高度,大屏页面常见做法是给 ref 容器设置 width: 100% 和 height: 100%,父级也要有高度;再检查 echarts 引入方式,全量引入可以临时换成import * as echarts from 'echarts'排掉按需注册的问题。图表初始化后调用一次 resize 也能兜住布局变化造成的空白。实在排查不出,打开 ECharts 官方调试面板看一下 series 数据里有没有 NaN 或 Infinity。

5.3 现象三:接口跨域和 history 路由刷新 404

现象:开发环境页面能打开,但请求接口时浏览器控制台报跨域,access-control-allow-origin 错误;部署上线后,在某个路由下刷新页面直接 404。

原因:跨域是浏览器同源策略导致的,开发环境 vue.config.js 的 proxy 没生效或 target 配错;刷新 404 是 nginx 没有配置 history 路由回退,请求落到了磁盘文件上找不到对应路径。

解决:跨域先确认请求 URL 以代理前缀开头,比如 /energy-api,再确认 vue.config.js 里 changeOrigin 为 true。线上 404 在 nginx 的 location 里加 try_files:

location / { try_files $uri $uri/ /index.html; }

加上之后刷新非根路径,nginx 找不到对应文件就回退到 index.html,前端路由再接管渲染。注意修改 nginx 配置后要 reload 生效,不要只改文件不重载。

5.4 现象四:大屏长时间运行内存只增不减

现象:大屏挂一天,浏览器内存逐渐涨到几 GB,页面开始卡顿掉帧,最后浏览器崩溃。

原因:定时器没有清掉,或者 echarts 实例没有 dispose。大屏页面切到别的路由后,原本的 setInterval 还在继续跑,每次轮询都生成新的 option、新的 DOM 节点;echarts 实例绑定的事件监听也一直挂在 window 上,页面销毁了实例还活着。

解决:所有定时器统一在 beforeDestroy 里 clearInterval,项目里的做法是把轮询逻辑收进 mixin,销毁钩子里统一 stop;echarts 实例在组件销毁时 dispose。组件级别再补一个 visibilitychange 监听,页面切到后台时暂停轮询,回到前台再恢复。这算是我做这类项目最深的教训之一,后来所有图表组件都强制写 beforeDestroy,没有例外。

6. 二次开发:从自定义图表到新增能耗模块的完整改造路径

6.1 新增大屏图表的四个固定步骤

想在能源大屏上加一个新图表时,我一般按四步走。第一步在 views/dashboard 下新增一个图表组件,或者直接复用 components/TrendLine.vue;第二步调用 api 层新接口或复用现有接口拉数据,拿到原始能耗数组;第三步把接口返回组装成 echarts 需要的 option 结构,交给 ChartCard 渲染;第四步在 dashboard 页面里引入组件,放入对应的布局区域。大屏的轮询刷新机制不用动,各子组件的 load 方法会被统一触发,新图表自然跟着刷新。

6.2 新增一个能耗指标模块的改动清单

假设要加一个"分时分室能耗对比"模块,改动文件按顺序大概是:api/energy.js 加一个接口函数;mock 目录加对应 JSON;store/modules/energy.js 加一个 action;views 下新增页面;router 里加一条路由并配好菜单 title 和图标。需要注意 store 里 action 名和 api 函数名保持一致,页面里 dispatch 时不容易记混。菜单权限如果用了路由守卫,还要确认新路由的角色字段,否则配好了菜单没权限也进不去。

6.3 验证方法与收尾

改完建议做两个验证:Mock 模式下先跑通数据链路,确认接口字段映射正确,图表能出数;再切真实后端联调,重点看跨域和超时。从那以后我每次给图表组件加轮询都会先检查销毁逻辑有没有写全,再确认 option 的 deep watch 生效,这两步能省掉后面排查内存问题的力气。这份源码的结构对二次开发足够友好,照着 api、store、views 的链路往下加模块就行。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进