简介:vue3+Echarts智能养护工程监管平台项目资料包,适合前端开发者、可视化大屏学习者以及需要快速搭建公司监管看板的团队使用,既可作为业务实战参考,也能直接当作模板复用。资料以单个doc说明文档形式交付,整体仅636KB,文档内提供百度网盘链接,可获取完整源码、操作演示视频和项目效果截图,无需逐个下载资源,结构清晰、按需取用。目前已有73人浏览学习。通过学习可重点理解vue3组件化开发与Echarts图表配置在大屏业务场景中的联动方式,掌握养护工程监管数据的指标展示逻辑;拿到源码后可对照视频逐段调试,也可直接抽取图表配置与样式,快速改造成自己的可视化项目,尤其适合初中级前端做实战复盘或项目起步。
1. vue3+Echart养护工程监管平台到底在管什么
很多做公路、市政养护的团队,日常管理还停在「微信群报照片 + Excel 月底汇总」这个阶段,巡查记录散在每个人手机里,领导想看进度只能等人工拼表。vue3+Echart养护工程监管平台这类项目,本质就是把「巡查发现—病害上报—工单派发—处置复核—统计展示」这条业务链串起来:Vue3 负责搭界面和交互,Echarts 负责把养护里程、病害类型、资金执行率这些数据画成可以直观下钻的图表。它解决的不是单点记账问题,而是把分散的养护动作沉淀成一套可统计、可追溯的数据资产。适合三类人:想给团队做信息化的养护单位负责人、正在选型外包项目的项目经理、以及准备拿真实业务场景练 Vue3 + Echarts 中后台能力的前端开发者。这篇笔记会从业务建模讲到工程落地,再讲到图表封装和几个高频坑,全程给可复现的代码和参数。
2. 养护监管平台的业务建模:病害台账、工单闭环和统计口径
拿这一类平台前,第一件事不是建工程,而是把业务对象拆清楚。我见过不少项目,图表画得很漂亮,结果数据对不上,根本原因是业务口径没定死。监管平台最核心的对象只有三类:病害、工单、计划。所有图表几乎都是从这三类数据聚合出来的。
2.1 先拆业务:病害、工单和计划是平台的三根柱子
病害是巡查员在路面、桥梁、绿化带发现的破损或隐患,常见的有坑槽、裂缝、沉陷、护栏损坏、标志牌缺失。每条病害记录必须带位置、类型、严重程度、照片和时间,这是整个平台的原始数据。工单是病害被受理后形成的处置任务,记录派给谁、什么时候处理完、用了什么材料、验收是否合格。计划则是月度或季度的养护安排,比如某条路本月计划铣刨罩面多少公里,资金预算多少。
这三类数据之间有明确的流转关系:病害上报后可以转成工单,工单完成情况参与计划执行率统计,资金消耗又和计划预算对比。下面这个表是我在类似项目里常用的病害口径,你也可以直接拿去当数据库字典的起点:
| 病害类型 | 常见表现 | 统计口径建议 |
|---|---|---|
| 路面坑槽 | 面层破损、形成凹坑 | 按处统计,拍照必传 |
| 路面裂缝 | 横向/纵向裂缝、龟裂 | 按米统计,需标注走向 |
| 沉陷 | 路面局部下沉 | 按处统计,需测深度 |
| 附属设施损坏 | 护栏、标志牌、井盖 | 按件统计,关联设施编号 |
| 绿化缺失 | 死株、裸露 | 按平方米统计 |
工单状态我一般建议做五态:待派发、进行中、待验收、已完成、已退回。退回状态必须有原因字段,否则月底统计「维修及时率」时说不清是谁的责任。状态流转清晰之后,前端做筛选器、后端做聚合统计才有统一依据,不然你画出来的饼图连「已处理」和「已完成」都分不清楚。
2.2 统计模块的接口约定:前端怎么拿数据,后端怎么给数据
图表页面最怕的不是 Echarts 不会用,而是接口数据格式和图表配置对不上。我的常用做法是:后端单独提供一组/api/stats/*聚合接口,直接返回已经按时间、区域聚合好的结果,前端只负责渲染,不在页面里做 reduce 运算。这样做的好处有两个:口径统一由后端维护,前端不会因为排序或过滤条件不同算出两个版本的数字;前端图表组件也能保持简单,拿到数组直接塞给 series 就行。
一个典型的统计接口返回结构大概长这样:
{ "code": 0, "message": "ok", "data": { "trendStats": { "months": ["2025-01", "2025-02", "2025-03"], "plannedMileage": [120, 135, 150], "actualMileage": [98, 126, 132] }, "diseaseTypeStats": [ { "type": "坑槽", "count": 47 }, { "type": "裂缝", "count": 132 }, { "type": "沉陷", "count": 12 } ], "fundStats": { "budget": 860000, "spent": 523000, "categories": ["材料", "机械", "人工"], "values": [210000, 145000, 168000] } } }这里有几个约定值得注意:code: 0表示业务成功,HTTP 状态码不代表业务结果;trendStats用平行数组而不是对象数组,是因为折线图双 y 轴时这样最省事;diseaseTypeStats的type字段值和前端图例一一对应,后端别用拼音缩写,前端别自己改映射。只要这个结构定下来,折线图、饼图、柱状图基本都能直接吃。
2.3 拿到这类网盘资源后怎么快速跑起来:先看 package.json 和 mock
标题里有「源码+视频+截图」和网盘字样,说明是打包好的学习资源。这类资源拿到手,第一步别急着打开 IDE,先解压看目录里有没有 README,然后打开package.json看scripts字段。前端项目跑不起来的九成原因,不是代码问题,而是依赖版本和 Node 版本不匹配。
常规执行顺序是这样的:
# 1. 检查 node 版本,Vue3 + Vite 项目建议 Node 18 以上 node -v # 2. 安装依赖,推荐用 pnpm,遇到 node-sass 报错时优先换 npm 重试 pnpm install # 3. 启动开发服务 pnpm run dev # 4. 如果项目里有 mock 目录或 vite-plugin-mock 配置,说明数据是本地模拟的 # 先不要急着接后端,用 mock 数据把页面跑通更重要跑起来之后,先确认两件事:路由能不能进、图表有没有数据。如果页面白屏,大概率是路由模式问题,createWebHistory在开发服务器下正常,但直接双击 HTML 打开是空的,需要改成createWebHashHistory或走 Vite 的 dev server。如果图表有容器但没画面,别急着找 Echarts 的问题,切换到 Network 面板看接口通不通,mock 数据有没有被正确加载。视频和截图的作用是帮你确认「原始效果长什么样」,和本地跑出来有差异时,以代码实际行为为准,不要反过来硬改代码去凑截图效果。
3. vue3工程搭建:目录约定、axios封装和监管平台的权限路由
业务模型理清后,工程部分可以按一套固定的模板走。监管平台这类项目,页面数量一般在 10~20 个之间,团队规模不大,所以技术选型我倾向于克制:Vite + Vue3 组合式 API + Vue Router 4 + Pinia + Axios,不再额外引入重型组件库,图表统一走 Echarts。这套组合足够撑起一个监管后台,而且新手看源码时每一层都看得懂。
3.1 初始化项目:Vite、组合式API和目录结构
用 create-vite 初始化项目是最快的路径,注意在交互式命令行里选择 Vue 而不是 Vue-TS。监管平台这类业务,TS 可以用,但如果团队不熟,纯 JavaScript 能让交付节奏更快,源码也更容易被后来者接手。
npm create vite@latest maintenance-platform -- --template vue cd maintenance-platform pnpm install pnpm add vue-router@4 pinia axios echarts装完依赖后,我一般会把src下的目录结构整理成下面这样,这套结构也适用于大多数中后台项目:
src/ ├─ api/ # 按业务域拆接口:disease.js、workOrder.js、stats.js ├─ assets/ # 静态资源,地图 GeoJSON 也放这里 ├─ components/ # 通用组件,chart、table、upload 等 ├─ router/ # 路由表和守卫 ├─ store/ # Pinia 状态 ├─ utils/ # request 封装、日期格式化等 ├─ views/ # 页面级组件 └─ App.vue目录约定的价值在后期维护,尤其当你拿到一份网盘源码时,先看目录结构就能判断作者的水平。api和views一一对应,接口请求不散落在页面里,这是底线要求。
3.2 封装请求层:拦截器、超时和业务错误处理
监管平台的接口请求不能直接在页面里写 axios.get,否则 token 刷新、超时提示、错误弹窗会在每个页面重复一遍。二次封装一次,后面所有页面都走同一套逻辑:
// src/utils/request.js import axios from 'axios' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE, // 环境变量区分本地和线上 timeout: 15000 }) // 请求拦截:把登录后的 token 带上 request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截:把后端包裹的 code/data/message 拆开 request.interceptors.response.use( res => { // 约定 code === 0 表示业务成功,非 0 直接抛错 if (res.data.code !== 0) { return Promise.reject(new Error(res.data.message)) } return res.data.data }, err => { // 超时、断网、401 统一在这里兜底 console.error('request error:', err.message) return Promise.reject(err) } ) export default request几个参数按项目规模调:timeout建议 10~15 秒,太短统计大屏接口容易误报,太长用户等得烦躁;baseURL不要写死,用 Vite 的.env.development和.env.production分别配VITE_API_BASE。后端接口如果还没有统一返回code/data/message结构,你需要和后端对齐,这是最省事的格式,比直接返回裸数组好扩展。
3.3 权限路由的轻方案:meta记录角色,路由守卫拦一下
监管平台通常有管理员、巡查员、养护单位、领导四类角色。这类系统的菜单是固定的,不需要动态注册路由,在路由表的meta里声明roles,用全局前置守卫拦一下即可。这样实现成本最低,权限逻辑一眼能看到头。
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '监管总览', roles: ['admin', 'leader'] } }, { path: '/disease', component: () => import('@/views/DiseaseList.vue'), meta: { title: '病害台账', roles: ['admin', 'inspector'] } }, { path: '/work-order', component: () => import('@/views/WorkOrder.vue'), meta: { title: '工单管理', roles: ['admin', 'contractor'] } } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') return } const roles = JSON.parse(localStorage.getItem('roles') || '[]') if (to.meta.roles && !to.meta.roles.some(r => roles.includes(r))) { next('/403') return } next() }) export default router这里有个容易被忽略的点:roles判断要放在登录校验之后,否则未登录用户会被角色逻辑挡住,永远跳不到登录页。角色信息在登录接口返回时写入 localStorage 或 Pinia,刷新页面后从存储恢复。如果以后菜单需要按角色动态显示,再加一个数组 filter 对routes做过滤就行,不用推翻重来。
4. 把Echarts嵌进vue3:按需引入、图表封装和自适应重置
Echarts 接 Vue3 最大的门槛有两个:打包体积和实例生命周期。直接在组件里import * as echarts from 'echarts'会把所有图表和组件都打进包里,一个空图表页面就能吃掉几百 KB。更合理的做法是按需引入,再封装成通用组件,把 init、setOption、resize、dispose 全部收敛到一个地方。
4.1 按需引入:让首屏体积可接受
创建src/plugins/echarts.js,只注册会用到的模块。监管平台的图表类型通常固定:折线图、柱状图、饼图、散点图,偶尔有南丁格尔玫瑰图,这些都从echarts/charts里按名引入。
// src/plugins/echarts.js import * as echarts from 'echarts/core' import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts' import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent, GraphicComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' echarts.use([ BarChart, LineChart, PieChart, ScatterChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent, GraphicComponent ]) export default echarts注册后再从echarts/core导出的init就只包含你注册过的能力。CanvasRenderer 是默认渲染器,适合大部分统计图;如果页面里有大量图表同时刷新,可以考虑 SVGRenderer,但散点图数据量大时 Canvas 更稳。GraphicComponent 不是必选项,我注册它主要是用来做水印和自定义标记,监管平台对截图溯源有需求时很有用。
4.2 封装一个config驱动的chart组件
有了按需引入的 echarts 实例,下一步是封装 Chart 组件。这个组件只做四件事:创建实例、更新配置、监听窗口变化、销毁实例。所有页面共用它,页面里只关心 option 怎么写,不需要关心生命周期。
<!-- src/components/BaseChart.vue --> <template> <div ref="chartRef" class="chart-container" :style="{ height }"></div> </template> <script setup> import { onMounted, onBeforeUnmount, ref, watch } from 'vue' import { toRaw } from 'vue' import echarts from '@/plugins/echarts' const props = defineProps({ option: { type: Object, required: true }, height: { type: String, default: '320px' }, notMerge: { type: Boolean, default: false } }) const chartRef = ref(null) let chartInstance = null const render = () => { if (!chartInstance) { chartInstance = echarts.init(chartRef.value) } // toRaw 避开 vue3 响应式代理对 echarts 内部判断的干扰 chartInstance.setOption(toRaw(props.option), props.notMerge) } const resizeChart = () => { chartInstance && chartInstance.resize() } onMounted(() => { render() window.addEventListener('resize', resizeChart) }) watch( () => props.option, () => render(), { deep: true } ) onBeforeUnmount(() => { window.removeEventListener('resize', resizeChart) if (chartInstance) { chartInstance.dispose() chartInstance = null } }) </script> <style scoped> .chart-container { width: 100%; } </style>代码里两个细节要重点说明。toRaw()是为了避免把 Vue3 的响应式代理对象直接传给 Echarts,后面避坑章节会具体讲原因,这里先按防御性写法处理。notMerge参数默认是false,因为页面切换筛选条件时往往希望合并配置;如果从饼图切成折线图,旧 series 类型不匹配,就要显式传true强制替换。
4.3 三个必调的视觉参数:tooltip、dataZoom 和 grid
图表能显示数据只是第一步,监管平台的大屏场景里,有三个参数我几乎每次都调,不调就会在真机上翻车。
tooltip 的trigger直接影响交互:折线图、柱状图用axis模式,鼠标移动时整条轴线上的数据一起显示;饼图用item模式,指到哪块显示哪块。axisPointer设置为shadow在柱状图上会有背景阴影,视觉上更清楚。
dataZoom 决定一轮屏幕放多少数据。月度趋势图动辄 12 个月起步,全部挤在一个图里柱子会变成细线。我习惯配置一个inside和一个slider,滚轮缩放、拖动条滑动都能用,start和end控制在 40 到 100,把最近 60% 的时间段作为默认视野。
grid 的containLabel: true是我强烈建议开的,它让 y 轴刻度文字的宽度自动参与布局计算,否则左侧标签长了会溢出容器,在小屏显示器上尤其明显。一个比较稳妥的初始配置如下:
const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: 16, right: 24, top: 32, bottom: 20, containLabel: true }, dataZoom: [ { type: 'inside', start: 40, end: 100 }, { type: 'slider', height: 12, bottom: 4 } ], xAxis: { type: 'category', data: [] }, yAxis: { type: 'value' }, series: [{ type: 'line', data: [] }] }这套参数放在 BaseChart 的父组件里作为 option 的基底,具体页面往里填数据即可。注意grid.right不要给太大,右侧图例如果已经用legend配置,right: 24足够,留太多空间会让图表区显得局促。
5. 养护监管平台图表开发避坑:5个踩过的雷
图表开发里真正耗时间的不是写配置,是排查「看起来没问题却显示不出来」的问题。下面五个坑是我在类似监管平台项目里真实遇到过的,按现象、原因、解决三步写清楚。
5.1 图表区域一片空白:容器初始化时没有宽度
现象:图表组件挂在页面上,控制台没有报错,echarts 实例也创建成功了,但页面就是白板一块。最常见的原因是容器在初始化那一刻处于隐藏状态,比如弹窗还没打开、Tab 还没切换、或者父级用了v-if导致容器宽度为 0。
原因:Echarts 在init时会读取容器的宽高,宽高为 0 时它不会报错,只是画布尺寸就是 0,后续setOption也不会触发重绘。
解决:等容器真正可见后再初始化,或者在容器状态变化后手动调一次resize():
const openDialog = async () => { visible.value = true await nextTick() chartRef.value.getInstance().resize() }如果项目里 tab 切换很频繁,我更推荐直接给 BaseChart 加一个ResizeObserver,监听容器实际尺寸变化,而不是依赖 Tab 组件的事件回调。
5.2 vue3响应式Proxy对象让echarts行为异常
现象:页面上用reactive或ref存图表数据,打印出来明明有值,但图表不渲染,或者 tooltip 里显示的字段错位、变成[object Object]。
原因:Vue3 的响应式对象是 Proxy 包装的,Echarts 内部对数据结构做isArray、isObject判断时,Proxy 对象在某些场景会绕过这些类型检查,导致 series 数据没有被正确解析。
解决:传给setOption之前用toRaw还原普通对象,或者在组件里做一层浅拷贝:
import { toRaw } from 'vue' chartInstance.setOption(toRaw(option), notMerge)这不是每次都会触发,所以很多人会觉得「玄学」,但一旦出现,排查极其耗时。我的习惯是 BaseChart 内部统一toRaw,从源头切断这个问题。
5.3 隐藏的页面页签切回来之后图表白屏或错位
现象:用户在「病害台账」和「统计大屏」两个页签之间切换,切回统计大屏时,图表要么空白,要么只有半个图,拖动浏览器窗口才恢复正常。
原因:页签隐藏时容器display: none,Echarts 画布尺寸塌陷。切回可见后没有任何事件通知图表重新计算尺寸,除非浏览器窗口恰好触发 resize。
解决:在页签切换的回调里延迟调用 resize,或者用visibilitychange配合nextTick兜底:
const handleTabChange = async () => { await nextTick() setTimeout(() => { chartInstance && chartInstance.resize() }, 100) }延迟 100ms 是为了等容器完成布局。如果用了display: none切换,也可以用visibility: hidden替代,隐藏状态下容器保留尺寸,图表不会塌陷,不过这会牺牲一点渲染性能。
5.4 setOption多次调用后数据重叠、Y轴范围异常
现象:筛选条件从「2025年」切到「2026年」,折线图里新数据和旧数据叠在一起,Y 轴刻度范围变成 0 到 2000,数据分布完全失去参考意义。
原因:setOption的默认行为是增量合并,不是替换。如果前后两次 option 结构相同,Echarts 会保留旧 series 的数据,再叠加新数据,结果就是多条线重叠或堆积。
解决:在切换数据维度时显式传notMerge = true,让 Echarts 先清空再重绘:
chartInstance.setOption(toRaw(newOption), true)注意这个参数不是越多越好。如果你只想更新某个 series 的数据而保留图例、颜色、tooltip 配置,用默认的合并模式更合理。判断标准是:前后 option 的结构是否发生了实质变化,变了就true,没变就false。
5.5 打包部署后地图/资源404,图表只剩基本结构
现象:本地开发一切正常,npm run build部署到服务器子目录后,地图类的图表渲染不出来,控制台报 GeoJSON 文件 404,或者字体文件加载失败。
原因:Vite 默认base是/,资源路径按根域名拼接。部署到子路径时,所有静态资源都指向了错误的位置。地图 GeoJSON 如果用字符串拼接路径加载,也会踩同样的坑。
解决:修改vite.config.js里的base: './',让资源使用相对路径;GeoJSON 用 import 方式引入,让打包工具处理路径:
import geoJson from '../assets/map/china.json' echarts.registerMap('china', geoJson)用import引入 JSON 还有一个好处:文件会被打包进应用,不会因为服务器目录部署结构变化而失效。地图类的容器在注册地图前保持不可见状态,注册完成后渲染,顺序不能颠倒。
6. 一个值钱的小技巧:把监视图表做成配置驱动组件
如果你已经跑通了上述每一个步骤,最后一个值得投入的做法是:把统计大屏里的每张卡片抽象成配置驱动的组件。简单说,就是不再为每张图单独写一个页面文件,而是用一个configs数组描述「这张卡片是什么图、数据从哪个接口来、多久刷新一次」,组件接收配置后自己完成请求、渲染和轮询。
我常用的配置结构长这样:
const cardConfigs = [ { id: 'mileage-trend', title: '月度养护里程', chartType: 'line', api: '/api/stats/monthly-mileage', interval: 60 }, { id: 'fund-rate', title: '资金执行率', chartType: 'bar-line', api: '/api/stats/fund-rate', interval: 120 }, { id: 'disease-pie', title: '病害类型分布', chartType: 'pie', api: '/api/stats/disease-type', interval: false } ]组件内部用一个request(card.api)拿到数据,再根据chartType把数据映射成对应的 option。轮询用setInterval管理,但记得在页面document.hidden时暂停,切回时立即刷新一次,否则后台挂了几小时,回来一屏全是过期数据。
验证这种做法是否值得,有个很简单的自检方法:把同一条接口的数据同时渲染在两个页面上,一份用旧写法、一份用配置驱动写法,然后模拟后端把字段名从count改成total,看哪个方案改动的文件数少。配置驱动方案通常只需要改一个映射函数,旧写法可能要翻三四个页面文件。
以前我为了赶进度,总是一页一页复制图表的 setOption,页面多了以后,改一个字段要翻四个文件,把自己坑得不轻。现在习惯是先抽配置再写图,一开始多花半小时,后面每次改需求都省半天。这个方向如果你也想试,别一口气重构全部,先拿一张总览卡片改造成配置驱动,跑通一轮再说。希望帮到你。
本文还有配套的精品资源,点击获取