简介这是一份基于 Vue.js 开发的开源智慧水务系统前端工程面向水务行业信息化开发者、前端工程师及正在学习 Vue 实战项目的读者。项目围绕水务监测、数据展示与后台管理场景提供了完整可运行的页面组件与构建配置适合直接部署体验也可作为二次开发或课程设计的基础框架。压缩包共包含229个文件以 Vue 单文件组件、CSS 样式表和 JavaScript 逻辑文件为主辅以 PNG/SVG 图标、JSON 配置及字体资源整体仅约 2MB结构轻量、便于快速下载和本地运行。当前已有 740 人学习下载。通过这份资源使用者可以获得一套较完整的 Vue 项目目录与模块划分思路了解页面组件、全局样式、路由相关配置和构建脚本的组织方式同时可利用内置的可视化图表与水务业务界面快速搭建原型或替换数据源进行功能扩展对掌握 Vue 工程化开发流程有直接帮助。1. 智慧水务用 Vue 做这套开源系统能解决什么适合谁上手智慧水务向来是 Java 后端的天下前端能看到的界面多半是「套壳」的。但这次拆的 vueLeakage 不一样——它是一个系统开源、前端完全用 Vue 写的智慧水务系统从大屏监控到工单管理全部覆盖。如果你手头正好要接水务、园区、管网的看板类项目或者想找一个能快速改成交付版的管理系统模板这套代码值得花一个下午过一遍。很多初学者第一次打开这种项目容易被几十个文件吓住实际上它拆开之后逻辑很清晰左侧菜单、中间大屏、右侧告警列表数据源全部走 Mock 加接口两层换数据非常方便。下面我把工程结构、核心模块和几个最容易翻车的地方一次说清楚。2. 技术栈与工程结构从路由到数据流先把骨架看明白2.1 为什么这代 Vue 项目选 Vue 2 组件生态先回答一个绕不开的问题这套项目用的是 Vue 2 而不是 Vue 3。原因很现实开源项目沉淀较早Element UI 和 vue-router 3、vuex 3 在 Vue 2 下配合最稳定。实际做水务这类偏传统的管理系统甲方看重的是稳定和可维护性团队里随便拉一个前端都能改代码Vue 2 的受众仍然最大。后续你自己二次开发不必强行升到 Vue 3。如果一定升工作量主要在组件库替换上。Element UI 的el-table、el-form在 Element Plus 里 props 和事件名改了不少表格的slot写法也变了属于牵一发动全身的改动。我一般建议保持 Vue 2 跑业务新写的模块可以单独用 Vue 3 微前端挂载两边互不干扰。2.2 目录结构与模块职责打开项目根目录第一件事不是跑npm install而是先读一遍目录。这个项目的结构是典型的「按业务域分包」不是按文件类型分包vueLeakage/ ├── public/ │ └── static/ # 静态地图瓦片、天气图标、大屏背景图 ├── src/ │ ├── api/ # 所有接口请求统一封装 │ │ ├── monitor.js # 实时监测接口 │ │ ├── manage.js # 设备管理接口 │ │ └── system.js # 用户与角色接口 │ ├── components/ # 通用组件 │ ├── layout/ # 框架布局侧边栏、顶栏、标签页 │ ├── mock/ # Mock 数据前端开发时不用等后端 │ ├── router/ # 路由表与权限控制 │ ├── store/ # Vuex 状态管理 │ ├── styles/ # 全局样式 │ ├── utils/ # 工具函数含 request.js │ └── views/ # 页面视图 │ ├── dashboard/ # 可视化大屏 │ ├── monitor/ # 实时监测 │ ├── device/ # 设备台账 │ ├── workbench/ # 工单处理 │ └── system/ # 系统设置 └── package.json这套分包的核心设计思想是「页面归页面接口归接口」。views里的组件不允许直接写axios必须去调用api目录下的方法。这样做的好处是一次数据源切换不用改页面逻辑。注意mock/这块它和utils/request.js配合实现了一套开关开发环境走本地 Mock生产环境走真实后端切换点只有一个环境变量。这是这套系统最值得抄的设计。2.3 路由与权限控制开源系统是怎么管菜单的水务系统对权限要求细致调度员只能看监测数据维修工只能处理分派给自己的工单。这套系统的做法是路由表加meta.roles字段配合 Vuex 里的用户角色做动态筛选。// router/index.js 关键逻辑 const routes [ { path: /monitor, name: Monitor, component: () import(/views/monitor/index.vue), meta: { title: 实时监测, roles: [admin, operator], // 允许访问的角色 icon: monitor } } ] // 全局前置守卫先查登录态再查角色 router.beforeEach((to, from, next) { const userInfo store.state.user.info if (!userInfo) { next({ path: /login, query: { redirect: to.fullPath } }) return } const roles to.meta.roles if (roles !roles.includes(userInfo.role)) { next({ path: /403 }) // 无权限跳转 return } next() })代码里两个点值得注意。第一路由全部用了动态import大屏页面文件体积大切到该路由时再加载首屏能快不少第二roles判断用的是includes而不是精确等于这样 admin 天然拥有全部权限不需要在每一条路由里把 admin 写一遍。实际部署时你会发现一个坑动态 import 在旧版构建工具下打包出来会生成非常多碎文件水务系统经常部署在用户内网的老服务器上HTTP 并发有限加载反而变慢。解决办法是在vue.config.js里调整chunkSize限制把过小的 chunk 合并掉这个在第 5 章细说。3. 可视化大屏复现用水量分析模块从模板到接口的完整走读3.1 大屏布局1920 设计稿的缩放适配大屏是智慧水务的脸面很多采购决策就是看大屏效果。这套系统的大屏设计稿按 1920x1080 做页面默认铺满整屏。布局方式是典型的「上中下 左右对称」顶部放标题和汇总指标中间是管网 GIS 或流向图左侧是分区用水排名右侧是告警列表和实时压力。先看最核心的适配代码它直接决定大屏在不同分辨率显示器上是否变形/* styles/dashboard.scss 核心片段 */ .dashboard-container { width: 1920px; height: 1080px; position: relative; transform-origin: left top; }// 大屏缩放适配逻辑 function handleScreenZoom() { const designWidth 1920 const designHeight 1080 const clientWidth window.innerWidth const clientHeight window.innerHeight const scaleX clientWidth / designWidth const scaleY clientHeight / designHeight const scale Math.min(scaleX, scaleY) // 取最小比例保证不出现滚动条 const dom document.querySelector(.dashboard-container) dom.style.transform scale(${scale}) // 水平居中缩放后原容器宽度仍是 1920减去实际显示宽度再除以 2 const remainWidth (clientWidth - designWidth * scale) / 2 dom.style.left remainWidth px } window.addEventListener(resize, handleScreenZoom)这段方案用的是整体缩放不是 rem 适配。原因是大屏里的 ECharts 图表尺寸依赖容器像素值rem 适配会导致字体和图表内部留白比例失衡整体缩放是最稳的做法。实践下来有一个体验问题如果显示屏比例不是 16:9比如客户用的是 4:3 拼接屏取Math.min后上下会留黑边。通常处理是把背景色做成深色黑边不突兀或者切成背景图按 100% 宽铺满内容区保持缩放视觉上会好很多。3.2 用水量趋势分析ECharts 配置的细节取舍大屏中间最常放的就是用水量趋势图反映辖区总用水量的日/周变化。这个模块的 option 配置有几个值得抠的细节// views/dashboard/components/WaterTrendChart.vue 核心配置 setChartOption() { const option { tooltip: { trigger: axis, axisPointer: { type: cross } }, grid: { top: 24, right: 16, bottom: 28, left: 40 }, xAxis: { type: category, data: this.hours, axisLine: { lineStyle: { color: rgba(255,255,255,0.25) } }, axisLabel: { color: #7eb6ff }, // 间隔自适应数据点超过 12 个就跳着显示 interval: this.hours.length 12 ? Math.ceil(this.hours.length / 12) : 0 }, yAxis: { type: value, name: 用水量 (m³), splitLine: { lineStyle: { color: rgba(255,255,255,0.1) } } }, series: [{ name: 本日用水量, type: line, smooth: true, areaStyle: { // 渐变填充透明到半透明蓝大屏才会有立体感 color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: rgba(38, 132, 255, 0.35) }, { offset: 1, color: rgba(38, 132, 255, 0) } ]) }, lineStyle: { width: 2, color: #2684ff }, itemStyle: { color: #2684ff }, data: this.waterData }] } }三个细节是很多人容易忽略的。第一axisLabel.interval如果不设置ECharts 默认使用auto数据点密集的时候它会自己挑但有时会同时显示相邻两个 label文字重叠非常难看手动设置跳显间隔最稳妥。第二areaStyle的渐变必须在setOption里用echarts.graphic.LinearGradient创建实例不能直接写 CSS 字符串否则部分版本会报警告。第三大屏背景色是深蓝色默认网格线和坐标轴标签颜色要在 option 里单独覆盖否则图表拉出来一片黑。3.3 图表与接口之间数据转换放哪个层初次接触这套系统的开发者最容易犯的错是直接在组件里写接口调用。组件里写axios.get项目后期接口字段一改组件、图表、甚至缓存逻辑全要跟着动。这套系统的做法是单独拆了一个transform函数放在 API 层// api/monitor.js 片段 import request from /utils/request // 每日用水量曲线 export function getDailyWaterTrend(params) { return request({ url: /monitor/water/trend/daily, method: get, params }).then(response { // 后端返回的是 [[00:00, 125.3], [01:00, 118.9], ...] // 组件需要的是 { hours: [...], values: [...] } const hours response.data.map(item item[0]) const values response.data.map(item Number(item[1]).toFixed(2)) return { hours, values } }) }组件里只需要这样调用const { hours, values } await getDailyWaterTrend({ stationId: this.stationId, date: this.date }) this.hours hours this.waterData values this.setChartOption()把数据格式化收敛在 API 层的收益是图表组件始终面对稳定结构。后端返回数组套数组节省带宽前端需要对象方便绑定两个结构在 API 层互相转换任何一侧调整都不必改动页面代码。这里要留意Number(item[1]).toFixed(2)的用法。字符串格式的数字直接做减法会得到预期结果但展示出来会带「.0000001」这类浮点尾数所以统一转一遍小数位。如果后端返回 null这行会报错稳妥的是加?? 0兜底。4. 实时监测模块Mock 仿真、WebSocket 替换与数据格式约定4.1 实时数据的双轨设计水务系统的实时监测涉及流量计、压力计、水质传感器。这套系统开发时还没有真实设备接入所以设计成双数据源——前端优先连 WebSocket连不上就自动回退到 Mock 定时器。这思路值得借鉴因为演示和真实环境共用一套页面代码。先看数据格式约定。不管是真接口还是 Mock推送的消息结构必须一致// 统一设备测点消息格式 { deviceId: DS-020312, // 设备编号 point: pressure, // 测点类型pressure / flow / quality value: 0.356, // 当前值 unit: MPa, // 单位MPa / m³/h / NTU ts: 1712345678901, // 采集时间戳毫秒 status: 0 // 0 正常1 预警2 报警 }三个字段是这套系统的关键约定。point用来区分同一设备的多种传感器一个设备可能有压力点、流量点、水质点ts是时间戳而非格式化字符串前端自己决定显示格式避免时区差异status字段让告警触发逻辑不在前端做而是在数据产生端就标好状态。4.2 Mock 推送实现随机游走模型别写太假Mock 最忌讳的是每次刷新数据都是随机数因为真实传感器数值是连续的压力不会从 0.3 瞬间跳到 0.8。这套系统用的是「随机游走」思路每次基于上一次的值加一个微小偏移。// mock/monitor.js 实时数据模拟核心逻辑 function createValueGenerator(initial, min, max, step) { let current initial return function next() { // 上下随机偏移控制在 step 范围内 const delta (Math.random() - 0.5) * step * 2 let nextValue current delta // 越界时回弹模拟量程保护 if (nextValue max) nextValue max - Math.abs(delta) if (nextValue min) nextValue min Math.abs(delta) current Number(nextValue.toFixed(3)) return current } } const pressureGen createValueGenerator(0.352, 0.2, 0.5, 0.008) const flowGen createValueGenerator(120.5, 50, 260, 1.5) // 定时器模拟 WebSocket 推送 setInterval(() { const payload { deviceId: DS-020312, point: pressure, value: pressureGen(), unit: MPa, ts: Date.now(), status: pressureGen() 0.45 ? 1 : 0 } MessageBus.$emit(realtime-data, payload) }, 3000)这里createValueGenerator的三个参数分别是初始值、最小值、最大值、单次步长。步长决定了曲线看起来多自然压力计步长 0.008 MPa 对应 1 到 2 秒内的真实波动流量计步长 1.5 m³/h因为流量本身波动幅度大步长太小会看不出变化太大又不真实。推送间隔 3000ms 是刻意选择。水务监测不是股票行情压力每 3 秒刷新已经足够。间隔太短图表会频繁重绘图表闪烁且 CPU 占用快速上涨。4.3 订阅更新Vuex 与 EventBus 的取舍推送数据到达前端的更新分发这套系统用的是 EventBus 而不是 Vuex。很多人认为所有数据都该进 Vuex这是误区。实时数据频率高、过期快、页面失活后也不需要保留放进 Vuex 反而让状态树不停抖动影响store里其他模块的响应。// 组件内订阅数据 created() { MessageBus.$on(realtime-data, this.handleRealtimeData) }, beforeDestroy() { // 组件销毁务必解绑否则事件回调里还在操作已销毁的 DOM MessageBus.$off(realtime-data, this.handleRealtimeData) }, methods: { handleRealtimeData(payload) { if (payload.deviceId this.currentDeviceId) { this.latestPressure payload.value this.status payload.status this.updateChart(payload) } this.pushToAlarmList(payload) } }注意beforeDestroy里必须$off。EventBus 最经典的问题是忘了解绑导致组件销毁后定时器还在触发回调消耗资源且可能报错。另外多组件同时监听同一个事件时需要靠deviceId过滤不能所有订阅者都傻乎乎地更新整屏。实际项目中我从这套系统学到的习惯是实时高频数据用 EventBus会话类数据用户信息、菜单权限放 Vuex这个边界别混。混了会出现一种尴尬情况——一个设备的数据变化触发大屏、列表、弹窗三条链路同时更新页面卡顿却找不到瓶颈。5. 避坑指南开源水务系统最常见的五个翻车点5.1 大屏缩放黑屏和比例失真现象客户会议室用的是 4:3 投影仪1920x1080 设计稿缩放后两侧有巨大黑边或者图表被拉伸变形。原因很多接手的人把transform: scale里的scaleX和scaleY分开设置不同比例屏幕下图表被压扁。解决统一使用const scale Math.min(scaleX, scaleY)并且不要只监听resize还要在进入全屏时手动调用一次handleScreenZoom因为全屏切换会改变window.innerHeight。5.2 ECharts 在 Tab 切换后宽度塌缩现象大屏或数据页里切换 Tab图表第一次显示正常切走再切回来图表宽度变成 100px 左右看起来像被挤扁。原因Tab 切走时容器display: noneECharts 初始化时拿不到正确的容器宽度切回来时实例没有感知到尺寸变化。解决在 Tab 切换回调里调用chart.resize()并配合this.$nextTick确保 DOM 已真正渲染如果部分场景仍失效用setTimeout(chart.resize, 200)兜底。5.3 实时推送导致 CPU 占用飙升、大屏掉帧现象连接真实 WebSocket 或 Mock 推送后客户电脑风扇狂转大屏切图卡顿任务管理器一看浏览器 CPU 占比超过 90%。原因每秒推送的数据都被Vue.set绑定进响应式数组图表随之整体重绘加上告警列表重新渲染多个高频操作叠加。解决高频数据不进 Vuex推送数据单独用非响应式字典存ECharts 用setOption只更新需要变化的 series不重建实例。还可以把图表刷频控制在 1 秒内最多两次忽略中间帧。5.4 Mock 数据和真实后端字段不一致现象开发阶段一切正常部署到客户环境后大屏所有图表空白控制台报Cannot read property map of undefined。原因后端接口返回字段名与 Mock 不一致例如后端返回water_usage而 Mock 里是value前端代码按 Mock 的字段取数。解决在 API 层统一加transform映射不要期望后端字段替你改变第二个办法是联调前先用后端真实返回背一份 JSON 存到 Mock 目录以真实数据格式作为 Mock 的唯一基准。5.5 打包后动态路由刷新页面直接 404现象用history模式的 Vue Router本地跑正常部署到水务公司内网服务器按 F5 刷新后白屏页面路径提示 404。原因history模式需要服务器把所有非文件请求重写回index.html。很多内网部署用的是老式 Apache 或 IIS没有配置重写规则。解决两个办法优先让运维配 URL 重写如果运维配合不了就在vue.config.js里把publicPath设为相对路径并改用hash模式。6. 二次开发技巧用环境变量一键切换演示数据与真实接口接手这套系统后第一个要改的就是数据源切换。演示环境没有后端生产环境要连真实设备网关。很多开发者直接在代码里写if (location.hostname xxx)这种分支判断散落在各个模块后期维护非常痛苦。这套系统的做法是借助.env系列文件加一个VUE_APP_API_MODE变量# .env.development VUE_APP_API_MODEmock # .env.production VUE_APP_API_MODEreal然后在 API 层统一判断// utils/request.js import axios from axios const mode process.env.VUE_APP_API_MODE const service axios.create({ baseURL: mode mock ? /mock // 走本地 Mock 数据 : http://192.168.31.216:8081, // 走真实后端网关 timeout: 15000 })关键点在于 axios 的拦截器里再做一次逻辑隔离。Mock 模式下所有请求返回一个Promise.resolve({ data: mockData })不让请求真的发出去service.interceptors.request.use(config { if (mode mock) { // 从本地 Mock 表里取数据模拟网络延迟 return new Promise(resolve { setTimeout(() { const url config.url.replace(/mock, ) const mockData MockTable[url] || { code: 404, msg: Mock 数据不存在 } config._mockData mockData resolve(config) }, 300 Math.random() * 500) }) } return config }) service.interceptors.response.use(response { if (mode mock) { return { data: response.config._mockData } } return response.data })这样做的好处是页面代码完全不用关心来源。切换演示和生产只要在打包时指定不同的环境文件代码零改动。我一般会把 Mock 的响应延迟做成 300 到 800 毫秒的随机数模拟真实网络抖动这样页面骨架屏和 loading 状态都能在演示时得到真实检验。最后一件事拿到这套系统后先别急着改页面第一件事是把环境变量跑通开发模式确认大屏出图再用生产模式打一次包确认路由、ECharts、Mock 三条链路都通再开始动业务代码。我从那次演示翻车之后每次接手开源项目都强制走一遍「环境切换冒烟测试」十分钟就能排除一大半环境类问题。希望帮到你。本文还有配套的精品资源点击获取