Vue.js+Django构建全国空气质量实时可视化监测系统
简介一套面向环保部门的全国空气质量指数实时可视化监测系统采用前端Vue与后端Django框架协作构建实现从多源数据抓取、污染热力图渲染到历史趋势分析和多城市对比的完整闭环。压缩包共收录97个文件其中以36个JavaScript脚本、18个Vue组件为核心逻辑配合SCSS样式、SVG和PNG图标以及后端配置文件与静态资源整体仅1.34MB便于快速部署与二次开发。目前已有73人学习参考适合环保数据可视化、前后端分离项目实践及毕业设计等场景。资源中除完整可运行代码外还提供说明文件与附带文档能帮助使用者理解从API请求到前端渲染的数据流同时覆盖移动端适配、响应式布局等关键实现细节。对于想快速搭建空气质量监测平台或学习DjangoVue整合方案的开发者这是一份具有直接参考价值的落地资源。1. 全国空气质量实时可视化监测系统要解决的核心问题把全国 300 多个城市实时空气质量摆到一个屏幕上的难点不在于地图画得有多酷而在于代码得同时扛住三件事上游数据每小时的更新压力、前端渲染污染热力图时的视觉可读性、以及环保部门对“某城市最近 24 小时趋势”随时会提出来的查询。从 Vue.js Django 这对组合看这套空气质量实时可视化监测系统把职责分得非常干脆——Django 做空气质量数据采集、清洗、存储和聚合接口Vue.js 接接口渲染热力图、趋势线和城市对比图。这套架构在环境监测、数据可视化大屏和政务类看板里都是常见形态适合要对接真实空气监测数据、又不想把前端页面写死的团队。新手按这个结构把端到端链路跑通一遍基本就能摸清前后端分离项目的完整套路。2. Vue.js 与 Django 前后端分离的数据链路设计与工程骨架2.1 为什么选 Django 承接空气质量数据采集空气质量项目的核心不是接口写得花而是数据模型稳定、采集任务不丢数、聚合查询不卡。Django 自带 ORM、数据库迁移机制和 admin 管理后台这三个能力在一个会持续运行数月、还要接受环保部门查数的项目里就是刚需。另外 Python 的数据处理生态对上游数据源的清洗也友好requests 抓数、pandas 补空值、再用 Django ORM 批量写入链路比 Node 后端更顺。对比项Django DRFFlaskNode/Express模型建表与迁移内置 ORM迁移脚本自动生成需自行接 SQLAlchemy代码量大需手写 SQL 或另接 ORM后台管理admin 开箱即用需另配 flask-admin需 react-admin 等前端方案定时采集任务管理命令 cron 即可需 celery 或 APScheduler需 node-cron 或 pm2权限与多用户自带 User/Permission需扩展需另写中间件从表里能看出Django 在“有明确表单、权限、报表需求”的项目里效率最高。空气质量监测恰好就是这种项目——城市、测点、时间、污染物浓度字段天然规整查询维度固定Django 的 ORM 聚合函数直接覆盖需求不必引入额外组件。2.2 为什么用 Vue.js 做可视化前端Vue.js 在可视化项目里更友好的是组件化拆分方式污染热力图独立成一个地图组件历史数据趋势独立成一个折线组件多城市对比独立成一个柱状组件组件之间通过 props 传数据、通过 emit 通知父级刷新。相比直接在 HTML 里写 ECharts 初始化这种模式在页面图表增多后不会失控。响应式设计所需的断点控制和 resize 监听也能在 Vue 组件生命周期里统一管理。ECharts 实例在onMounted里创建在onUnmounted里销毁窗口变化时在resize事件里调用chart.resize()这套逻辑天然适合 Vue 的组件化写法。再加上 Vue Router 的页面切换和 Vite 的本地开发代理前后端分离联调时不用处理跨域问题开发体验比传统模板渲染顺不少。2.3 工程骨架与目录规划常见做法是先建 Django 后端再初始化 Vue 前端两边独立开发后用代理联通。下面是最小可运行的工程初始化步骤# 1. 新建 Django 项目骨架 django-admin startproject air_quality_platform cd air_quality_platform python manage.py startapp monitor # 2. 初始化 Vue3 前端工程Vite 脚手架 npm create vuelatest frontend cd frontend npm install后端里monitor这个 app 专门负责空气质量相关的模型和接口前端的src/views下放 Dashboard 主页面src/components下放地图、折线、柱状图三个图表组件。这么划分之后后端加接口、前端换图表互不干扰。Vite 默认端口是 5173Django 开发服务器默认 8000联调时让 Vite 把/api代理到http://127.0.0.1:8000即可。2.4 前后端分离的接口约定接口设计上不用过度设计三个接口就能覆盖一个看板页的数据需求接口路径作用关键参数GET /api/air-quality/latest/获取所有城市最新一条 AQI无GET /api/air-quality/history/获取单城市历史趋势city,hoursGET /api/air-quality/compare/多城市对比cities逗号分隔响应统一包一层{ code: 0, data: [...] }时间字段统一用 ISO 8601 字符串数值字段允许为null但不要缺 key。这套约定让前端拿到数据后不需要再做兜底解析直接把 payload 喂给 ECharts 的 series.data 就行。3. Django 空气质量数据采集、模型设计与查询接口实现3.1 空气质量数据表设计空气质量的存储核心是城市维度和时间维度。城市表保存名称与坐标用于前端地图定位空气质量表保存每个城市在不同时刻的 AQI、PM2.5、PM10 等字段时间字段加索引。模型代码如下# monitor/models.py from django.db import models class City(models.Model): name models.CharField(城市名, max_length50, uniqueTrue) province models.CharField(省份, max_length50, blankTrue) lng models.FloatField(经度) lat models.FloatField(纬度) def __str__(self): return self.name class AirQuality(models.Model): city models.ForeignKey(City, on_deletemodels.CASCADE, related_namerecords) aqi models.IntegerField(AQI指数) pm25 models.FloatField(PM2.5) pm10 models.FloatField(PM10, nullTrue) quality models.CharField(等级, max_length20, blankTrue) timestamp models.DateTimeField(采集时间, db_indexTrue) class Meta: ordering [-timestamp] unique_together [(city, timestamp)]模型里最关键的是unique_together [(city, timestamp)]它保证同一城市同一时刻只有一条数据。没有这个约束定时任务误跑两次就会产生重复记录前端趋势图会出现同一个时间点两条值折线直接画成锯齿。timestamp加db_indexTrue是因为历史趋势查询必然按时间过滤索引能明显提速。建表后把模型注册进 admin环保部门可以直接在后台按城市、时间过滤查看原始数据不用每人配一个数据库客户端# monitor/admin.py from django.contrib import admin from .models import City, AirQuality admin.register(City) class CityAdmin(admin.ModelAdmin): list_display [name, province, lng, lat] admin.register(AirQuality) class AirQualityAdmin(admin.ModelAdmin): list_display [city, aqi, pm25, quality, timestamp] list_filter [city, quality] date_hierarchy timestampdate_hierarchy timestamp会在后台列表页顶部生成一个按年月日逐级筛选的入口数据量大了之后查某一天的数据不需要手写过滤条件这是优化后台体验成本最低的一行配置。3.2 定时采集任务管理命令 cron空气质量数据采集适合用 Django 管理命令实现而不是写进 views 里。因为采集是后台任务不需要 HTTP 触发管理命令可以直接被 cron 调用也能手动执行验证。下面是采集命令的核心结构# monitor/management/commands/collect_air_quality.py import requests from django.core.management.base import BaseCommand from django.utils import timezone from monitor.models import City, AirQuality class Command(BaseCommand): help 采集全国各城市空气质量数据 def handle(self, *args, **options): # 对接数据服务商提供的统一查询接口 payload requests.get( https://data.example.com/api/aqi/realtime, params{key: your-token}, timeout10 ).json() city_map {c.name: c for c in City.objects.all()} bulk_list [] for item in payload[list]: city city_map.get(item[city]) if not city: continue bulk_list.append(AirQuality( citycity, aqiitem[aqi], pm25item.get(pm25) or 0, pm10item.get(pm10), qualityitem.get(quality, ), timestamptimezone.now().replace(second0, microsecond0) )) AirQuality.objects.bulk_create( bulk_list, batch_size500, ignore_conflictsTrue ) self.stdout.write(self.style.SUCCESS( f写入 {len(bulk_list)} 条记录 ))采集逻辑说明先用requests.get从数据服务商拉取实时数据timeout10防止上游接口挂起导致采集卡死然后一次性批量写入数据库。ignore_conflictsTrue配合模型里的唯一约束重复执行同一小时的任务时直接跳过已存在的记录不会报错也不会产生脏数据。定时调度用 crontab 即可每 15 分钟跑一次能保证看板上的“实时”体验*/15 * * * * cd /opt/air_quality_platform python manage.py collect_air_quality logs/collect.log 21这里有个容易忽略的坑Django 管理命令必须在该虚拟环境激活状态下执行直接用系统 Python 跑会报ModuleNotFoundError: No module named django。cron 里建议写全虚拟环境路径比如/opt/venv/air/bin/python避免环境变量不一致导致任务静默失败。3.3 查询接口最新数据与历史趋势查询接口用 Django REST Framework 的 APIView 来写结构清晰后续加权限、限流都方便。先定义序列化器# monitor/serializers.py from rest_framework import serializers from .models import AirQuality, City class AirQualitySerializer(serializers.ModelSerializer): city serializers.CharField(sourcecity.name) class Meta: model AirQuality fields [city, aqi, pm25, pm10, quality, timestamp]接口视图里最值得注意的是latest的查询写法。由于 AirQuality 是全量记录表每个城市每小时都有一条数据直接order_by(-timestamp)再切片会把全表参与排序毫无必要。最快的方式是用distinct加子查询取每个城市最新一条# monitor/views.py from django.db.models import Max from rest_framework.views import APIView from rest_framework.response import Response from .models import AirQuality, City class LatestAirQuality(APIView): def get(self, request): latest_time ( AirQuality.objects .values(city_id) .annotate(max_timeMax(timestamp)) ) records ( AirQuality.objects .select_related(city) .filter( timestamp__in[r[max_time] for r in latest_time] ) .order_by(city__name) ) serializer AirQualitySerializer(records, manyTrue) return Response({code: 0, data: serializer.data})annotate(max_timeMax(timestamp))先在数据库层面按城市分组算出每个城市最新采集时间再用timestamp__in精确取回对应的完整记录。这样即使采集任务某次漏跑latest返回的依然是每个城市各自的最新数据而不是统一时间点的数据。3.4 跨域配置与 admin 后台优化前后端分离开发时Vue 跑在 5173 端口Django 跑在 8000 端口浏览器直接跨域。开发阶段最简单的处理是用 django-cors-headerspip install django-cors-headers# settings.py INSTALLED_APPS [corsheaders] MIDDLEWARE.insert(0, corsheaders.middleware.CorsMiddleware) CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]生产环境不建议把CORS_ALLOW_ALL_ORIGINS设成 True。环保部门内部系统大多通过域名访问把域名列表写死在CORS_ALLOWED_ORIGINS里既避免安全隐患也便于之后加白名单时回溯改动记录。4. Vue.js 污染热力图、趋势分析与多城市对比图表的落地代码4.1 前端引入 ECharts 并准备地图数据可视化部分依赖 ECharts 5。安装方式和按需引入代码如下npm install echarts5// main.js import { createApp } from vue; import * as echarts from echarts; import App from ./App.vue; const app createApp(App); app.config.globalProperties.$echarts echarts; app.mount(#app);空气质量项目里用到的地图、折线、柱状图都属于高频组件直接全量引入 ECharts 不仅代码简单后续加散点图、仪表盘也不用再改引入逻辑。污染热力图还需要中国地图的地理坐标数据ECharts 5 默认不带中国地图需要单独引入注册import chinaGeoJson from /assets/china.json; echarts.registerMap(china, chinaGeoJson);4.2 全国污染热力图渲染热力图的本质是在地图上按经纬度散点标出颜色深浅。ECharts 里用series.type: map配合visualMap实现。完整组件如下!-- src/components/AirQualityMap.vue -- template div refchartRef classmap-chart/div /template script setup import { ref, onMounted, onUnmounted, watch } from vue; import * as echarts from echarts; const props defineProps({ cityList: { type: Array, required: true } }); const chartRef ref(null); let chart null; const renderChart () { const data props.cityList.map(item ({ name: item.city, value: [item.lng, item.lat, item.aqi] })); chart.setOption({ tooltip: { trigger: item, formatter: params ${params.name}br/AQI: ${params.value ? params.value[2] : -} }, visualMap: { min: 0, max: 300, dimension: 2, inRange: { color: [#00d4c9, #ffe173, #ff6b57] }, text: [高, 低], realtime: false }, geo: { map: china, roam: true, zoom: 1.2, itemStyle: { borderColor: #3a4a5a, areaColor: #1b2735 } }, series: [{ type: map, geoIndex: 0, data }] }); }; onMounted(() { chart echarts.init(chartRef.value); renderChart(); }); onUnmounted(() { chart chart.dispose(); }); watch(() props.cityList, renderChart, { deep: true }); /script组件逻辑说明visualMap的dimension: 2表示取数据数组中的第三项下标 2即 AQI 值作为颜色映射依据realtime: false让拖拽图例时只在松开鼠标后重绘避免频繁渲染造成卡顿geoIndex: 0表示系列复用geo里注册的地图配置这样 series 里不需要重复设置map相关样式。roam: true允许拖拽缩放地图。做可视化大屏时建议把zoom初始值设在 1.11.3避免南海诸岛的小图影响整体视觉比例。4.3 历史数据趋势折线图趋势分析组件接收后端返回的时间序列核心是把时间点映射到 x 轴!-- src/components/AqiTrendLine.vue -- script setup import { ref, onMounted, watch } from vue; import * as echarts from echarts; const props defineProps({ history: { type: Array, required: true } }); const chartRef ref(null); let chart null; const render () { const times props.history.map(r r.timestamp.slice(5, 16)); const values props.history.map(r r.aqi); chart.setOption({ xAxis: { type: category, data: times }, yAxis: { type: value, name: AQI }, series: [{ type: line, smooth: true, data: values, areaStyle: { opacity: 0.15 } }] }); }; onMounted(() { chart echarts.init(chartRef.value); render(); }); watch(() props.history, render, { deep: true }); /scriptslice(5, 16)是把2025-04-08 14:00:00截成04-08 14:00横轴不至于太长。趋势图组件不建议在内部做轮询数据刷新逻辑应放在父级 Dashboard 中统一控制子组件只负责响应数据变化重绘。4.4 多城市对比柱状图多城市对比最常见的做法是选中几个城市看同一时刻的 AQI 值并排对比。Vue 里用多选下拉框收集城市列表把数据转化为并列柱状图script setup const props defineProps({ records: { type: Array, required: true } }); const render () { const aqiMap {}; props.records.forEach(r { aqiMap[r.city] r.aqi; }); chart.setOption({ xAxis: { type: category, data: Object.keys(aqiMap) }, yAxis: { type: value }, series: [{ type: bar, data: Object.values(aqiMap), label: { show: true, position: top } }] }); }; /script多城市对比的图表选型有一个经验城市数量大于等于 5 个时并列柱状图会变得拥挤此时改成横向柱状图或雷达图可读性更好。三种图表的适用场景对比如下图表类型适合场景注意事项热力图全国城市整体分布数据量 300 城市时渲染流畅折线图单城市 24 小时趋势时间轴过密时开启sampling: lttb柱状图多城市横向对比城市多时改用横向条形图折线图数据超过 100 个点时ECharts 默认不会降采样会出现渲染卡顿。显式加上sampling: lttb可以在保留趋势特征的前提下压缩点数这是历史数据跨天查询时最容易忽略的性能参数。5. 移动端适配、响应式布局和联调阶段需要验证的 3 个细节5.1 看板页面响应式断点设计环保部门现在大量使用平板和大屏移动端适配不能只做缩放要按设备宽度重新排布。Dashboard 主布局用 CSS Grid在断点切换列数.dashboard-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } media (max-width: 768px) { .dashboard-grid { grid-template-columns: 1fr; } } .dashboard-title { font-size: clamp(18px, 4vw, 28px); }平板宽度下保留两列手机宽度下压成一列。clamp(18px, 4vw, 28px)让标题字号在 1828px 间随视口平滑变化避免单独写多套字号。5.2 ECharts resize 防抖处理移动端页面转屏、浏览器窗口缩放都会触发 resize 事件。图表没有自适应调用时会变形。标准写法是防抖监听import { debounce } from lodash-es; const onResize debounce(() { chart chart.resize(); }, 150); onMounted(() window.addEventListener(resize, onResize)); onUnmounted(() window.removeEventListener(resize, onResize));防抖时间设 150ms 比较合适既跟手又不会在连续拖拽窗口时密集触发重绘。另外移动端有横竖屏切换事件orientationchange在部分安卓机型上不会触发 window resize需要一并绑定window.addEventListener(orientationchange, onResize);5.3 联调时必做的接口验证前端代理配置好之后先用 curl 验证后端接口连通性再开页面。这个顺序能区分问题出在前端还是后端curl -s http://127.0.0.1:8000/api/air-quality/latest/ | head -c 300返回 JSON 里code为 0 且data数组非空说明接口正常。然后检查 Vue 代理// vite.config.js server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } }页面访问http://127.0.0.1:5173时请求/api/xxx会被 Vite 代理到 Django。手机真机调试时Vite 默认监听localhost需要用--host启动让手机通过局域网 IP 访问同时 Django 的ALLOWED_HOSTS要临时加入局域网 IP否则后端会拒绝请求。本文还有配套的精品资源点击获取