简介这一uni-app前端工程聚焦雾霾检测场景源自西安电子科技大学B测项目适合环境科学数据分析、小程序与跨端开发学习者参考。压缩包共73个文件体积10.51MB以41个JavaScript逻辑代码和4个Vue页面组件为主干辅以6个JSON配置、8个Markdown说明文档、ECharts可视化脚本以及多张空气质量相关图片素材结构清晰。工程内置axios请求封装、ECharts图表组件、页面路由与静态资源目录通过HBuilderX即可运行预览覆盖从后端数据接口适配到前端图表展示、交互切换的完整链路。预览中还可见针对不同颗粒物浓度绘制的图表页面与风力图标等素材能帮助快速理解雾霾检测数据可视化思路。已有513人学习对需要参考完整uni-app项目结构、学习ECharts整合或构建环境监测面板的开发者提供了可直接调试和二次开发的样例。1. 雾霾检测 B测真不是算法问题这份 uni-app 资源包先解决工程问题在雾霾检测项目的 B测阶段数据采集端已经能把 PM2.5、PM10 和 AQI 数值不断抛出来但真正让测试人员头疼的是这些数字怎么在手机端变成能看、能对比、能汇报的图表我拆过一份某高校的雾霾检测 B测资源包它用 uni-app 把请求封装、ECharts 可视化和页面流程串成了一个完整工程而不是只给了一堆算法代码。这份资源包解决的是「从数据到界面」那条最容易翻车的链路接口能不能拿到数据、图表组件选哪种引入方式、页面生命周期里数据来了之后组件是否已经准备好、真机上 canvas 会不会被原生层盖住。对新手来说它是一套能直接跑起来的雾霾检测 Demo对已经在做环境监测系统的人来说它最大的价值是封装方式和页面结构你可以把里面的模拟数据换成真实 AQI 接口半个小时内出一版可视化看板。所以这篇笔记我会按工程拆包的角度来讲先看目录和请求层再讲 ECharts 两个组件的差异和接入方式然后从 mock 数据到双端渲染跑通一整个 B测流程最后把我在还原过程中踩到的五个坑一一列出来。适合的人包括刚接触 uni-app 的开发者、做空气质量数据可视化的学生以及想快速验证明天要汇报的方案工程师。2. 拆工程骨架pages.json、manifest.json 与 uni-app 请求封装2.1 从文件列表反推项目结构一个标准 uni-app 页面工程拿到压缩包先别急着双击打开先看顶层文件列表能反推出很多东西。这个包里除了 node_modules 和 yarn.lock项目根目录下有 pages.json、manifest.json、main.js、App.vue、uni.scss这些是 uni-app 项目的标配文件。pages 目录里放着 index 页面components 目录下有两个以 echarts 命名的 Vue 组件js_sdk 目录下有一个封装好的请求模块images 目录里则放着 air.png、windmill.png 这类与空气可视化相关的图片资源。项目根目录/ ├── pages.json # 页面路由、tabBar、导航栏配置 ├── manifest.json # 应用标识、小程序/H5 配置 ├── main.js # Vue 实例入口挂载请求库和全局组件 ├── App.vue # 应用生命周期全局样式 ├── uni.scss # 全局 SCSS 变量 ├── pages/ │ └── index/ │ ├── index.vue # 雾霾检测主页面 │ └── index.js # 页面逻辑如有 ├── components/ │ ├── echarts.vue # 完整版 ECharts 封装组件 │ └── echarts-el.vue # 按需引入的 ECharts 封装组件 ├── js_sdk/ │ └── uni-axios/ # 基于 axios 适配 uni-app 的请求模块 │ ├── index.js │ └── adapter.js ├── images/ │ ├── air.png # 空气流动背景图 │ ├── windmill.png # 风车动效素材 │ └── banner.jpg # 首页顶部横幅 └── package.json这里有个容易被忽略的点文件名里带-master的目录说明它最初是从 Git 仓库导出的但 src 代码并没有放在二级子目录里根目录直接就是工程。所以导入项目时不需要调整路径直接选根目录就行。我一般会复制一份到本地工作区后再解压避免直接在下载目录里开发因为后续 npm install 会在项目目录里生成大量 node_modules放在系统下载目录容易被权限卡住。pages.json 是第一个要打开的文件它决定了这个雾霾检测应用有哪些页面、底部导航怎么分布、导航栏标题是什么。uni-app 框架靠它来编译到不同平台所以只要有新增页面需求就必须在这个文件里同步注册。这个资源包里的页面不算多主页面就是 index但 components 目录里的 echarts 组件会被多个页面复用所以把它放在全局组件声明里会更省事。2.2 请求封装uni.request、axios 适配器与超时参数在雾霾检测这种场景下页面需要轮询或者按天拉取 AQI 数据请求层做不好最直接的表现就是列表页面转圈、图表数据迟迟不刷新。这个资源包在 js_sdk 里放了一个基于 axios 的封装目的就是让 uni-app 里能像 Web 项目一样使用 axios 语法而不是到处写 uni.request 嵌套回调。// js_sdk/uni-axios/adapter.js import axios from axios export default function uniAdapter(config) { return new Promise((resolve, reject) { uni.request({ url: config.baseURL config.url, method: config.method || GET, data: config.data, header: config.headers, timeout: config.timeout || 10000, success: (res) { if (res.statusCode 200 res.statusCode 300) { resolve({ data: res.data, status: res.statusCode, headers: res.headers }) } else { reject(new Error(HTTP res.statusCode)) } }, fail: (err) reject(err) }) }) }这段代码的核心思路是axios 在浏览器环境下依赖 XMLHttpRequest而 uni-app 的小程序和移动端环境没有这个对象所以通过 adapter 把请求执行方式替换成 uni.request。config 对象里传进来的 baseURL、url、method、data、headers 全部透传给 uni.request返回结果统一包成 axios 风格的响应体。这样做的好处是业务代码里你可以用axios.get(...)原封不动地写不用每个页面重新封装。我在实际接这个项目时会把 timeout 单独提出来因为雾霾检测的数据接口有些是定时任务生成的服务端首次查询可能超过 10 秒。资源包里的默认值通常偏保守建议根据后端压测结果调整到 15000 或者更长。还有一个细节header 里如果后端要求传 token需要在这里统一加而不是在业务层每个请求手动拼否则轮询接口很容易出现个别请求带 token、个别不带的问题。// main.js 中挂载适配器 import axios from axios import uniAdapter from ./js_sdk/uni-axios/adapter.js axios.defaults.adapter uniAdapter axios.defaults.baseURL https://your-aqi-api.example.com axios.defaults.timeout 10000 axios.defaults.headers.common[Content-Type] application/json2.3 pages.json 路由配置tabBar、导航栏与页面传参雾霾检测应用一般会拆成「实时数据」和「历史趋势」两个入口但这个资源包把主要功能都放在了 index 页面里用 tab 切换组件来区分内容区域。pages.json 里的 tabBar 配置控制底部导航栏如果你要拆成真正的多页面需要在 pages 数组里追加路径并指定 tabBar 的 list。以下是典型配置{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 雾霾检测 } } ], tabBar: { color: #333333, selectedColor: #2E8B57, backgroundColor: #FFFFFF, list: [ { pagePath: pages/index/index, text: 实时监测, iconPath: static/tab/air.png, selectedIconPath: static/tab/air-active.png } ] }, globalStyle: { navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #F5F6FA } }注意 tabBar 的 pagePath 必须和 pages 数组里的 path 完全一致而且 iconPath 和 selectedIconPath 不能放在 images 目录下最好单独放一套 tab 图标。这是我之前翻车过的地方直接把 images/air.png 配到 tabBar 里结果在真机上底部图标不显示原因就是 tabBar 图标只支持静态资源目录下的文件需要在项目里创建static/tab文件夹。页面传参方面uni-app 用uni.navigateTo({ url: /pages/detail/detail?id stationId })在 detail 页面用onLoad(options)接收。如果是图表页面建议把 stationId 作为路由参数传过去这样同一个图表组件可以被不同监测站点复用。这个资源包里的 index 页面没有做多站点切换但参数结构已经预留二次开发时直接加一个下拉框就能扩展。3. 雾霾数据可视化ECharts 组件选型与数据刷新3.1 echarts.vue 与 echarts-el.vue完整包还是按需引入资源包的 components 目录里有两个相似组件echarts.vue和echarts-el.vue。前者通常引入的是完整版 echarts.min.js所有图表类型都能画但打出来的包体积会明显偏大后者是经过按需构建的版本只保留折线图、柱状图、散点图和基础坐标系组件体积能缩小一半以上。雾霾检测这类场景最常用的是折线趋势图、数值仪表盘和风向玫瑰图用 echarts-el 足够。我倾向于按需引入因为 uni-app 小程序对包体积很敏感超过 2MB 就会影响真机预览和分包加载。以下是 echarts-el.vue 的初始化骨架它接收一个 option 对象在 canvas 初始化后 setOption并通过 watch 监听外部数据变化template view classecharts-el canvas :idcanvasId :canvas-idcanvasId classchart-canvas / /view /template script import * as echarts from ../js_sdk/echarts-el/echarts.min.js export default { name: EchartsEl, props: { options: { type: Object, required: true }, canvasId: { type: String, default: chart } }, data() { return { chart: null } }, watch: { options: { deep: true, handler(nextOptions) { if (this.chart) { this.chart.setOption(nextOptions, true) } } } }, mounted() { this.initChart() }, beforeDestroy() { if (this.chart) { this.chart.dispose() this.chart null } }, methods: { initChart() { const ctx uni.createCanvasContext(this.canvasId, this) this.chart echarts.init(ctx, null, { width: 300, height: 200 }) this.chart.setOption(this.options) } } } /script style scoped .chart-canvas { width: 100%; height: 300px; } /style这段代码的关键在于echarts.init(ctx)传入的是uni.createCanvasContext返回的上下文不是浏览器里的 DOM 节点。因此数据变化时不能直接操作 DOM必须通过 setOption 更新。watch 里的deep: true保证嵌套对象变化也能触发重绘比如 PM2.5 实时数据从{ value: 45 }变成{ value: 78 }时图表能自动刷新。有个参数容易被忽略echarts.init的第三个参数里的 width 和 height。如果 canvas 组件样式中宽度是百分比初始化时屏幕尺寸还没计算完成图表可能只画一半。稳妥做法是在 onReady 或者 nextTick 后用uni.createSelectorQuery()获取到实际的像素宽高再初始化下面会继续展开。3.2 图表初始化时机onReady、nextTick 与数据竞态在 uni-app 中组件 mounted 生命周期里 canvas 不一定已准备好尤其是在小程序环境。比较可靠的做法是在父页面拿到数据后通过 v-if 控制图表组件出现然后在其 mounted 中初始化。但这样会有一个「数据竞态」接口返回很快时数据已经到了组件还没渲染完。资源包里的写法是用setTimeout延迟初始化这在 Demo 阶段能用但不适合生产。// pages/index/index.vue 片段 onLoad() { this.loadAirData() }, async loadAirData() { const res await axios.get(/aqi/current) this.airData res.data await this.$nextTick() this.$refs.chartEl.initChart(this.airData) }这里$nextTick的作用是等页面把旧内容更新完再执行图表初始化。如果你的 echarts 组件是放在v-if条件里的更建议在组件的 mounted 里直接接收optionsprop 初始化父组件不要手动调用子组件方法。因为 uni-app 的异步渲染在真机上可能比 Web 更滞后$nextTick并不能保证 canvas 上下文已经可用。我把这个项目放到小程序开发者工具里跑时发现一个典型问题echarts 组件在同一页面复用两次第二个组件初始化时 canvasId 不能重复。组件里用props.canvasId传入不同 id否则真机上会出现两个图表互相覆盖。资源包里默认的{ type: String, default: chart }对单图表场景没问题但多图表复用必须改。3.3 用 air.png 和 windmill.png 做采集场景动态背景雾霾检测页面通常不希望只是冷冰冰的数字原始资源包里放了 air.png 和 windmill.png我理解是用来做空气流动和风车旋转的视觉提示。在页面底部铺一张 air.png 作为背景再叠加 windmill.png 做 CSS 动画旋转能在不改动数据源的情况下让界面一眼看出「空气正在流动」。这不是性能消耗大的手段但能给测试汇报加分。.windmill { position: absolute; right: 30px; bottom: 40px; width: 80px; height: 80px; background: url(/static/images/windmill.png) no-repeat center / contain; animation: spin 6s linear infinite; } keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }把图片放到 static 目录下然后用 CSS 背景图方式引用比image标签更好控制动画矩阵。如果图片文件在 images 目录而非 staticH5 编译没问题但小程序真机上可能不显示因为小程序会把非 static 目录下的图片资源当普通文件处理路径解析容易出错。我一般会把这些背景图统一移动到static/images/下并在代码里用绝对路径/static/images/windmill.png省去相对路径的麻烦。4. 把 B测流程跑通从本地 mock 数据到 H5 和小程序双端渲染4.1 模拟雾霾数据字段设计与 mock 文件雾霾检测接口返回的数据一般包含监测站点、时间戳、PM2.5、PM10、AQI、SO2、NO2 等字段。这个资源包没有附带服务端所以请求层拿到的是 mock 数据。我自己会先在项目根目录建一个mock/目录放一个固定格式的 JSON 文件让前端可以先跑通流程等后端联调只需要改 baseURL。{ code: 0, data: { stationId: S1001, timestamp: 2025-06-16 09:00:00, aqi: 86, pm25: 58, pm10: 92, so2: 12, no2: 35, primaryPollutant: PM2.5, history: [ { time: 08:00, pm25: 45, pm10: 76 }, { time: 09:00, pm25: 58, pm10: 92 }, { time: 10:00, pm25: 67, pm10: 110 } ] } }字段里primaryPollutant是首要污染物很多新手会漏掉。页面展示时通常需要单独显示「首要污染物PM2.5」如果后端没给这个字段前端需要自己用规则判断。我会把 mock 数据和页面 UI 解耦写一个formatAirData函数专门做字段映射这样后端字段名变化时只改函数而不改模板。参数说明history数组是图表的数据源时间间隔需要均等否则 ECharts 横轴会出现不等距刻度这是很多人说图表「看起来很怪」的原因。4.2 请求时序onLoad 拉数据、onReady 渲染图表在 uni-app 页面生命周期中onLoad 时页面尚未完全渲染onReady 时组件已经挂载。正确的顺序是onLoad 里发起请求await 到数据后确保 ECharts 组件已经渲染完成再 setOption。这个资源包里用到了 axios 封装所以请求代码可以写成 async/await 形式不至于回调套三层。onLoad() { this.fetchAirQuality() }, async fetchAirQuality() { try { const { data } await axios.get(/api/current, { params: { stationId: S1001 } }) this.currentData data.data this.historyData data.data.history // 通过 v-if 控制图表组件挂载 this.chartReady true } catch (error) { console.error(雾霾数据获取失败, error) } }这里用chartReady控制图表的渲染时机模板里写成echarts-el v-ifchartReady :optionschartOptions /。这样数据到达后组件才开始初始化避免了空数据图表。chartOptions需要在 data 里定义为空对象然后在拿到数据后赋值。注意赋值时要重新构造对象而不是直接修改原对象的属性否则 watch 不一定触发。4.3 H5 和小程序环境下的编译运行步骤把这个压缩包在本地跑起来常规路径是解压后用 HBuilderX 导入项目在运行菜单中选择「运行到浏览器」或「运行到手机或模拟器」。如果是纯命令行开发者也可以先npm install然后用uni-cli方式编译到 H5。步骤我建议这样走。# 进入项目根目录安装依赖 npm install # 编译到 H5 npm run dev:h5 # 编译到微信小程序需要自己在 config 里配置 appid npm run dev:mp-weixin依赖安装时有个问题压缩包里既有 yarn.lock 又有 node_modules用 npm install 很可能造成依赖版本不一致。后面避坑章会专门讲。编译成功后H5 端直接打开 localhost:8080 就能看到雾霾检测页面小程序端则需要用对应的开发者工具导入项目根目录下生成的dist/dev/mp-weixin文件夹而不是导入源文件。5. 避坑与常见问题还原这个资源包时的五个翻车点5.1 依赖安装坑yarn.lock 与 npm 混用导致 ECharts 组件白屏现象项目导入后页面空白控制台报echarts is not defined或模块解析失败。原因资源包里已经有 node_modules 和 yarn.lock说明开发环境原本用 Yarn 装依赖。用 npm install 后node_modules 中部分依赖被替换但 yarn.lock 记录的版本锁并没有被 npm 使用导致 echarts 相关库版本对不上。尤其是带 peerDependencies 的插件版本冲突通常不会直接报错而是让组件默默挂掉。解决删除 node_modules、yarn.lock 和 package-lock.json重新强制安装一套。我一般会统一用 npm因为后续二次开发时同事不一定装了 Yarn。命令为rm -rf node_modules yarn.lock package-lock.json npm install。如果资源包里的 package.json 脚本依赖 Yarn 特性再手动改成 npm 脚本。5.2 canvas 层级问题ECharts 在小程序真机上被原生组件盖住现象图表在 H5 端显示正常在微信开发者工具里也偶尔正常但真机上图表被页面上方的一个悬浮按钮盖住触摸无响应。原因小程序的 canvas 是原生组件原生组件天然位于普通组件之上无法通过 z-index 来控制层级。如果页面里用了 picker、map、video 或悬浮按钮它们可能和 canvas 形成层级冲突。解决常见做法是用cover-view和cover-image覆盖在 canvas 上方但如果是自定义悬浮按钮直接改用cover-view也能解决。另一种办法是避开原生 canvas使用 ECharts 的canvas转temporaryFilePath导出图片展示但交互会打折。我习惯在真机调试时把可能冲突的原生组件先用v-if去掉定位到元凶后再用 cover-view 包裹。5.3 tabBar 图标不显示images 目录资源不等于静态资源现象配置完 tabBar 后H5 端底部图标消失小程序端直接编译报错。原因uni-app 的 tabBar 图标路径不能使用相对路径指向项目根目录下的 images 文件夹必须放在static目录下。这是因为编译器只把 static 目录内的文件原样拷贝到dist输出目录其他目录的图片需要经过 import 才能真正被打包引用。资源包里的 air.png 位于 images如果直接复制到 tabBar 配置里开发环境可能正常但发布后路径失效。解决在项目根目录创建static/tab/把 air.png 和 windmill.png 复制进去tabBar 图标引用改为/static/tab/air.png。注意图标大小限制在 40KB 以内超过会提示「tabBar 图标过大」需要先用工具压缩到 81x81 像素附近。5.4 图表 size 不在真机上自适应canvas 初始化宽度为 0现象图表初始化后只在左上角显示一个小方块H5 正常真机上宽度只有几十像素。原因ECharts 初始化时没有拿到 canvas 的实际布局尺寸。uni-app 小程序里canvas 组件宽度通常由 CSS 决定但uni.createSelectorQuery()在 onLoad 阶段返回的 width 可能为 0组件还没布局完成。解决在mounted里用uni.createSelectorQuery().select(# canvasId).boundingClientRect()获取宽高再传入echarts.init。如果拿到的 width 仍然为 0使用setTimeout延迟 200ms 重试。我通常会把初始化封装成一个 Promise确保拿到尺寸后再 setOption。async function getCanvasSize(canvasId) { return new Promise((resolve, reject) { uni.createSelectorQuery() .select(# canvasId) .boundingClientRect((rect) { if (rect rect.width 0) { resolve({ width: rect.width, height: rect.height }) } else { reject(new Error(canvas size is 0)) } }) .exec() }) }5.5 轮询定时器坑页面隐藏后图表数据还在飞现象雾霾检测页面每隔 5 秒请求一次数据退出页面后 console 里仍然有请求甚至回到页面时出现多个图表实例重叠。原因设置定时器后没有在页面卸载时清除页面 onUnload 里也没有 dispose ECharts 实例。uni-app 页面栈会缓存页面实例定时器继续跑就会持续触发 setOption下次进入时又创建一个新图表canvasId 不变导致覆盖。解决在页面onUnload中清除定时器并调用图表组件的 dispose 方法。我的习惯是统一用this.timer setInterval(...)卸载时clearInterval(this.timer)。如果资源包里的图表组件没有暴露 dispose在beforeDestroy里也调用一次chart.dispose()这是双保险。6. 二次开发与验证把这套雾霾检测模板改造成自己的数据看板B测资源包最大的意义不是运行起来看个 Demo而是让你在真实项目里能快速替换数据源、扩展站点和图表。我会先把接口切换到真实监测站然后在formatAirData函数里做字段兼容。具体做法是保留 mock 数据作为 fallback当接口请求失败时自动降级到 mock这样演示时即使断网也不会白屏。验证数据一致性也是一个很实用的技巧。雾霾检测最怕前后端定义的单位不一致比如接口返回的 PM2.5 可能是μg/m³页面展示了mg/m³这时候图表趋势虽然一致但数值会差 1000 倍。我习惯在formatAirData里加一个单位参数并写一个简单的断言if (value 500) value value / 1000这样至少能在测试阶段暴露单位异常。进阶一点可以在页面里加一个「最近 24 小时趋势」的 tab复用同一个 ECharts 组件传入不同的 history 字段。因为日期轮询时可能出现某几个时间点没有数据前端应该用connectNulls让折线图跳过空值。大部分时候后端会给 null如果没给前端需要自己补点。这个资源包里的图表组件已经支持传入 options你只需要在组件外面拼好完整的 option 对象就能扩展出气压、湿度、风向等多个指标。我现在的习惯是每次拿到一个 B测资源包先用五分钟跑通 H5 端再花十分钟在小程序真机上验证 canvas 和 tabBar最后才会看业务代码。因为这两个地方是环境差异最大的坑点真机过了就成功一半。希望这篇拆包笔记能帮你在做雾霾检测或类似空气质量可视化项目时少走弯路尤其是图表选型和请求封装这两块值得直接复用。本文还有配套的精品资源点击获取