简介vue3Echarts智能养护工程监管平台项目资料包适合前端开发者、可视化大屏学习者以及需要快速搭建公司监管看板的团队使用既可作为业务实战参考也能直接当作模板复用。资料以单个doc说明文档形式交付整体仅636KB文档内提供百度网盘链接可获取完整源码、操作演示视频和项目效果截图无需逐个下载资源结构清晰、按需取用。目前已有73人浏览学习。通过学习可重点理解vue3组件化开发与Echarts图表配置在大屏业务场景中的联动方式掌握养护工程监管数据的指标展示逻辑拿到源码后可对照视频逐段调试也可直接抽取图表配置与样式快速改造成自己的可视化项目尤其适合初中级前端做实战复盘或项目起步。1. vue3Echart养护工程监管平台到底在管什么很多做公路、市政养护的团队日常管理还停在「微信群报照片 Excel 月底汇总」这个阶段巡查记录散在每个人手机里领导想看进度只能等人工拼表。vue3Echart养护工程监管平台这类项目本质就是把「巡查发现—病害上报—工单派发—处置复核—统计展示」这条业务链串起来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封装和监管平台的权限路由业务模型理清后工程部分可以按一套固定的模板走。监管平台这类项目页面数量一般在 1020 个之间团队规模不大所以技术选型我倾向于克制Vite Vue3 组合式 API Vue Router 4 Pinia Axios不再额外引入重型组件库图表统一走 Echarts。这套组合足够撑起一个监管后台而且新手看源码时每一层都看得懂。3.1 初始化项目Vite、组合式API和目录结构用 create-vite 初始化项目是最快的路径注意在交互式命令行里选择 Vue 而不是 Vue-TS。监管平台这类业务TS 可以用但如果团队不熟纯 JavaScript 能让交付节奏更快源码也更容易被后来者接手。npm create vitelatest maintenance-platform -- --template vue cd maintenance-platform pnpm install pnpm add vue-router4 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建议 1015 秒太短统计大屏接口容易误报太长用户等得烦躁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 refchartRef classchart-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: noneEcharts 画布尺寸塌陷。切回可见后没有任何事件通知图表重新计算尺寸除非浏览器窗口恰好触发 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页面多了以后改一个字段要翻四个文件把自己坑得不轻。现在习惯是先抽配置再写图一开始多花半小时后面每次改需求都省半天。这个方向如果你也想试别一口气重构全部先拿一张总览卡片改造成配置驱动跑通一轮再说。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?