简介:本资源是一套基于 Python 的汽车数据分析大屏可视化系统完整项目源码,面向具备一定 Django 与 Vue 基础、希望练习前后端分离开发或搭建数据可视化大屏的开发者与学习者。项目采用 Vue 3 搭配 Vue-cli 构建前端界面,借助 Echart 与 DataV 实现折线图、柱状图、饼图等图表的动态刷新渲染,并通过 flexible 插件适配不同屏幕尺寸;后端由 Django 负责业务逻辑、路由管理与数据存储,整体结构清晰,便于二次开发与功能扩展。压缩包共约 2000 个文件,以 md 文档、js 脚本、json 配置、py 源码及少量 xml、html 为主,涵盖前端组件、图表封装与后端逻辑,整体约 73.95MB。目前已有 96 人学习下载,适合作为课程设计、毕业设计或可视化练手项目的参考,帮助读者快速理解大屏项目的目录组织、图表集成与前后端协作方式。
1. 汽车数据分析大屏:从 Django 后端到可视化前端的完整落地路径
手里有一份汽车销量、投诉量、故障率或者二手车残值数据,想把它做成一块能挂在办公室墙上、也能在浏览器里随时打开的大屏,这件事听起来像是前端工程师的活,但真正卡住大多数人的往往是后端——数据怎么存、接口怎么出、多维度聚合怎么算、大屏刷新时怎么不把数据库打崩。Django 在这个场景里其实非常合适:自带 ORM 能直接对汽车数据做分组聚合,DRF 能把聚合结果序列化成前端要的 JSON,模板和静态文件机制又能让大屏页面和后端接口跑在同一个工程里,省掉跨域和部署的麻烦。这篇笔记面向的是手上有真实汽车数据集、想用 Django 把「数据分析 + 大屏可视化」这条链路完整跑通的开发者,不管你是第一次做 Django 项目实战的新手,还是已经写过几个 Django Web 应用、想补上大屏这块拼图的老手,下面这套路径都能直接照着复现。
2. Django 汽车数据大屏的工程骨架:从建项目到数据落库
2.1 为什么选 Django 而不是 Flask 或 FastAPI 做大屏后端
大屏可视化的后端需求和普通 CRUD 接口不太一样。一块汽车数据分析大屏通常要同时展示十几个图表:月度销量趋势、品牌市占率、区域分布、价格段分布、投诉类型占比、故障率排名等等。这些图表背后是大量GROUP BY、COUNT、AVG、时间截断和条件过滤,如果后端没有一套成熟的 ORM 和查询表达式,光写 SQL 拼接就够折腾半天。Django ORM 的annotate、aggregate、TruncMonth、Case/When能覆盖大屏里九成以上的聚合需求,而且换数据库时基本不用改代码。
另一个现实原因是工程完整性。Django 自带 admin,汽车数据导入后可以直接在后台核对;自带模板和静态文件管理,大屏页面可以和后端接口放在同一个项目里;自带用户和权限,如果大屏需要区分「总部看全量、区域看本区」,权限体系现成可用。Flask 更轻,但这些东西要自己搭;FastAPI 异步性能好,但大屏场景的瓶颈通常在数据库聚合和前端渲染,不在 Web 框架的并发模型上。所以对「汽车数据分析大屏」这个具体场景,Django 是综合成本最低的选择。
2.2 建项目、建 app、连数据库的最小命令集
先确认 Python 和 Django 版本。Django 4.x 和 5.x 在 ORM 聚合 API 上差异不大,选你环境里能装上的稳定版即可。下面是建工程骨架的命令,每一步都说明它在干什么。
# 安装 Django 和数据库驱动,mysqlclient 用于连 MySQL pip install django mysqlclient # 创建工程,名字叫 car_dashboard django-admin startproject car_dashboard # 进入工程目录 cd car_dashboard # 创建数据 app,专门管汽车数据模型和接口 python manage.py startapp car_data # 创建大屏展示 app,管页面和前端资源 python manage.py startapp dashboardstartproject生成的是工程配置骨架,startapp生成的是业务模块骨架。把「数据」和「展示」拆成两个 app,是因为汽车数据的模型、导入脚本、聚合接口会越来越多,而大屏页面可能换好几版,拆开后互不干扰。建完 app 后,在car_dashboard/settings.py的INSTALLED_APPS里加上car_data和dashboard,然后配置数据库连接。
# car_dashboard/settings.py 片段 DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "car_dashboard", "USER": "root", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": {"charset": "utf8mb4"}, } }ENGINE换成django.db.backends.sqlite3也能跑,本地验证阶段用 SQLite 更快,但汽车数据量上到百万行、又要做多表关联聚合时,MySQL 或 PostgreSQL 更稳。utf8mb4是为了存品牌名里的特殊字符和中文注释。配置完执行python manage.py migrate建系统表,再python manage.py createsuperuser建管理员账号,后面导入数据后要在 admin 里核对。
2.3 汽车数据模型设计:字段、索引和聚合友好性
模型设计直接决定后面聚合查询写起来顺不顺。汽车数据分析常见的维度有时间、品牌、车系、车型、区域、价格段、能源类型,指标有销量、投诉量、故障数、评分。下面是一个能覆盖多数大屏需求的模型。
# car_data/models.py from django.db import models class CarSales(models.Model): """汽车销量明细表,一行代表某车型某月在某区域的销量""" stat_month = models.DateField(db_index=True, verbose_name="统计月份") brand = models.CharField(max_length=64, db_index=True, verbose_name="品牌") series = models.CharField(max_length=64, verbose_name="车系") model_name = models.CharField(max_length=128, verbose_name="车型") region = models.CharField(max_length=64, db_index=True, verbose_name="区域") energy_type = models.CharField(max_length=32, verbose_name="能源类型") price_range = models.CharField(max_length=32, verbose_name="价格段") sales_volume = models.IntegerField(default=0, verbose_name="销量") complaint_count = models.IntegerField(default=0, verbose_name="投诉量") fault_count = models.IntegerField(default=0, verbose_name="故障数") avg_score = models.FloatField(default=0.0, verbose_name="平均评分") class Meta: db_table = "car_sales" indexes = [ models.Index(fields=["stat_month", "brand"]), models.Index(fields=["region", "stat_month"]), ] verbose_name = "汽车销量明细" def __str__(self): return f"{self.stat_month} {self.brand} {self.model_name}"stat_month用DateField而不是字符串,是为了能用TruncMonth做月度聚合;brand、region加db_index=True,因为大屏最常见的筛选就是按品牌和区域;Meta.indexes里的复合索引对应「按月看品牌」和「按区域看月度」这两类高频查询。sales_volume、complaint_count用IntegerField,avg_score用FloatField,聚合时Sum和Avg都能直接用。模型建好后执行python manage.py makemigrations car_data和python manage.py migrate,表就建好了。
2.4 数据导入:从 CSV 到数据库的批量写入脚本
真实汽车数据通常来自 CSV 或 Excel。用 Django 的bulk_create批量写入,比逐条save()快一个数量级。下面是一个管理命令形式的导入脚本,放在car_data/management/commands/import_car_data.py。
# car_data/management/commands/import_car_data.py import csv from datetime import datetime from django.core.management.base import BaseCommand from car_data.models import CarSales class Command(BaseCommand): help = "从 CSV 导入汽车销量数据" def add_arguments(self, parser): parser.add_argument("csv_path", type=str, help="CSV 文件路径") def handle(self, *args, **options): csv_path = options["csv_path"] batch = [] batch_size = 2000 with open(csv_path, encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: batch.append(CarSales( stat_month=datetime.strptime(row["stat_month"], "%Y-%m-%d").date(), brand=row["brand"], series=row["series"], model_name=row["model_name"], region=row["region"], energy_type=row["energy_type"], price_range=row["price_range"], sales_volume=int(row["sales_volume"] or 0), complaint_count=int(row["complaint_count"] or 0), fault_count=int(row["fault_count"] or 0), avg_score=float(row["avg_score"] or 0), )) if len(batch) >= batch_size: CarSales.objects.bulk_create(batch) batch.clear() if batch: CarSales.objects.bulk_create(batch) self.stdout.write(self.style.SUCCESS("导入完成"))batch_size设 2000 是经验值,太小写入次数多,太大占内存且单次事务过长。bulk_create不会触发save()和信号,所以如果模型里有自动计算字段要提前算好。CSV 里的空值用or 0兜底,避免int("")报错。导入完成后去 admin 里抽查几条,确认月份、品牌、销量对得上,再开始写聚合接口。
3. 大屏数据接口:用 Django ORM 做多维聚合与 DRF 输出
3.1 大屏图表需要哪些聚合接口
一块汽车数据分析大屏的图表可以归成四类。第一类是趋势类,比如月度销量走势、投诉量走势,需要按TruncMonth分组求和。第二类是占比类,比如品牌市占率、能源类型占比、价格段分布,需要按维度分组求和再算百分比。第三类是排名类,比如销量 Top10 车型、故障率最高品牌,需要排序加截断。第四类是分布类,比如区域销量地图、评分分布直方图,需要按区域或分数区间分组。这四类接口如果每个都手写 SQL,维护起来很痛苦;用 ORM 的values+annotate组合,基本每个接口十行以内能写完。
接口输出格式要统一,前端 ECharts 或百度可视化大屏(现在多叫 DataV 类产品)通常吃两种结构:一种是{categories: [...], series: [{name, data}]},适合折线柱状;一种是[{name, value}],适合饼图和地图。后端统一返回{"code": 0, "data": ...}的外壳,前端只处理data部分,出错时code非零并带msg,这样大屏上的错误提示也好做。
3.2 用 annotate 和 TruncMonth 写月度销量趋势接口
月度趋势是大屏最核心的图。下面这个视图按月份聚合销量和投诉量,返回前端直接能用的结构。
# car_data/views.py from django.db.models import Sum from django.db.models.functions import TruncMonth from rest_framework.decorators import api_view from rest_framework.response import Response from car_data.models import CarSales @api_view(["GET"]) def monthly_trend(request): """月度销量与投诉量趋势,支持按品牌筛选""" brand = request.GET.get("brand") qs = CarSales.objects.all() if brand: qs = qs.filter(brand=brand) rows = ( qs.annotate(month=TruncMonth("stat_month")) .values("month") .annotate(total_sales=Sum("sales_volume"), total_complaint=Sum("complaint_count")) .order_by("month") ) categories = [r["month"].strftime("%Y-%m") for r in rows] sales_data = [r["total_sales"] for r in rows] complaint_data = [r["total_complaint"] for r in rows] return Response({ "code": 0, "data": { "categories": categories, "series": [ {"name": "销量", "data": sales_data}, {"name": "投诉量", "data": complaint_data}, ], }, })TruncMonth("stat_month")把日期截断到月初,values("month")按这个截断值分组,annotate里做Sum聚合。order_by("month")保证时间轴顺序,否则数据库返回顺序不保证,大屏折线会乱跳。brand参数是可选的,前端点品牌筛选时传进来,不传就是全量。注意TruncMonth在 MySQL 和 PostgreSQL 上行为一致,SQLite 也支持,所以本地和线上切换不用改代码。
3.3 品牌市占率和区域分布的聚合写法
市占率接口要算每个品牌销量占总销量的比例。可以在 Python 里算,也可以在一次查询里用Window函数算,但为了兼容性和可读性,我一般分两步:先取各品牌销量,再在 Python 里算百分比。
# car_data/views.py @api_view(["GET"]) def brand_share(request): """品牌市占率,返回饼图数据""" rows = ( CarSales.objects.values("brand") .annotate(total=Sum("sales_volume")) .order_by("-total") ) total_all = sum(r["total"] for r in rows) or 1 data = [ {"name": r["brand"], "value": r["total"], "percent": round(r["total"] / total_all * 100, 2)} for r in rows ] return Response({"code": 0, "data": data})total_all用or 1兜底,防止数据为空时除零。percent在 Python 里算,保留两位小数,前端饼图标签直接用。区域分布接口结构一样,把values("brand")换成values("region")即可。如果区域要对应地图,name字段要和地图 JSON 里的区域名完全一致,否则地图上会缺块,这是大屏地图最常见的翻车点。
3.4 排名类接口:Top N 与故障率计算
排名接口的坑在于「率」的计算。故障率不能简单用fault_count排序,要用fault_count / sales_volume。ORM 里可以用ExpressionWrapper做除法。
# car_data/views.py from django.db.models import F, FloatField, ExpressionWrapper @api_view(["GET"]) def fault_rate_rank(request): """故障率排名 Top10,故障率 = 故障数 / 销量""" rows = ( CarSales.objects.values("brand") .annotate( total_sales=Sum("sales_volume"), total_fault=Sum("fault_count"), ) .filter(total_sales__gt=0) .annotate( fault_rate=ExpressionWrapper( F("total_fault") * 1.0 / F("total_sales"), output_field=FloatField(), ) ) .order_by("-fault_rate")[:10] ) data = [ {"name": r["brand"], "value": round(r["fault_rate"] * 100, 3)} for r in rows ] return Response({"code": 0, "data": data})F("total_fault") * 1.0是为了把整数除法变成浮点除法,否则在部分数据库上会得到 0。filter(total_sales__gt=0)排除销量为零的品牌,避免除零。[:10]在 ORM 里对应 SQL 的LIMIT 10,但要注意它必须放在order_by之后。故障率乘 100 转成百分比,前端显示更直观。
3.5 接口性能:缓存和查询次数的控制
大屏通常每隔几十秒自动刷新一次,如果每次刷新都全表聚合,数据库压力会很大。Django 自带缓存框架,可以把聚合结果缓存 60 秒。
# car_data/views.py from django.core.cache import cache @api_view(["GET"]) def monthly_trend(request): brand = request.GET.get("brand", "all") cache_key = f"monthly_trend_{brand}" cached = cache.get(cache_key) if cached: return Response({"code": 0, "data": cached}) # ... 聚合逻辑同上 ... result = {"categories": categories, "series": [...]} cache.set(cache_key, result, 60) return Response({"code": 0, "data": result})缓存时间设 60 秒,是因为大屏刷新间隔一般不低于 30 秒,60 秒能挡住大部分重复查询,又不会让数据太旧。缓存 key 里带brand,保证不同筛选条件不串数据。如果数据是每天更新一次,缓存时间可以拉到几小时。另外,聚合接口要避免 N+1 查询,values+annotate本身就是单条 SQL,不要在里面循环调CarSales.objects.filter(...)。
4. 大屏前端:ECharts 布局、自适应与接口联调
4.1 大屏页面结构:栅格布局与图表容器
大屏页面用 Django 模板渲染一个 HTML 骨架,图表用 ECharts 在浏览器里画。布局用 CSS Grid 最省事,把屏幕分成若干行若干列,每个图表占一个格子。
<!-- dashboard/templates/dashboard/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>汽车数据分析大屏</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #0b1a2b; color: #fff; overflow: hidden; } .grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: 80px 1fr 1fr; gap: 12px; padding: 12px; height: 100vh; } .header { grid-column: 1 / -1; text-align: center; font-size: 28px; line-height: 80px; } .chart { background: rgba(255,255,255,0.04); border-radius: 8px; } </style> </head> <body> <div class="grid"> <div class="header">汽车数据分析大屏</div> <div class="chart" id="trend"></div> <div class="chart" id="brandShare"></div> <div class="chart" id="regionMap"></div> <div class="chart" id="faultRank"></div> </div> <script src="/static/dashboard/js/main.js"></script> </body> </html>grid-template-columns: repeat(4, 1fr)把宽度分成四等份,grid-template-rows第一行固定 80px 放标题,下面两行自适应。每个图表容器给一个id,JS 里按id初始化 ECharts 实例。height: 100vh加overflow: hidden保证大屏不出现滚动条,这是投屏场景的基本要求。
4.2 用 fetch 拉接口并渲染 ECharts
前端 JS 负责调后端接口、拿数据、塞进 ECharts 配置。下面以月度趋势和品牌市占率为例。
// dashboard/static/dashboard/js/main.js async function fetchJSON(url) { const resp = await fetch(url); if (!resp.ok) throw new Error("HTTP " + resp.status); const json = await resp.json(); if (json.code !== 0) throw new Error(json.msg || "接口返回异常"); return json.data; } async function renderTrend() { const chart = echarts.init(document.getElementById("trend")); const data = await fetchJSON("/api/monthly-trend/"); chart.setOption({ tooltip: { trigger: "axis" }, legend: { data: data.series.map(s => s.name), textStyle: { color: "#fff" } }, xAxis: { type: "category", data: data.categories, axisLabel: { color: "#9fb3c8" } }, yAxis: { type: "value", axisLabel: { color: "#9fb3c8" } }, series: data.series.map(s => ({ name: s.name, type: "line", smooth: true, data: s.data, })), }); return chart; } async function renderBrandShare() { const chart = echarts.init(document.getElementById("brandShare")); const data = await fetchJSON("/api/brand-share/"); chart.setOption({ tooltip: { trigger: "item" }, series: [{ type: "pie", radius: ["40%", "70%"], data: data.map(d => ({ name: d.name, value: d.value })), label: { color: "#fff" }, }], }); return chart; } const charts = []; Promise.all([renderTrend(), renderBrandShare()]).then(list => { charts.push(...list); window.addEventListener("resize", () => charts.forEach(c => c.resize())); });fetchJSON统一处理 HTTP 错误和业务错误码,避免每个图表重复写 try/catch。echarts.init要在容器有宽高之后调用,所以脚本放在 body 末尾。resize监听让图表跟随窗口变化,投屏分辨率切换时不会变形。如果图表多,可以把renderXxx统一注册到一个数组里,刷新时批量重绘。
4.3 自动刷新与接口联调的三个注意点
大屏一般要自动刷新。用setInterval定时重新拉数据并setOption,但要注意三点。第一,setOption默认是合并模式,如果新数据比旧数据短,旧的多余数据可能残留,加{ notMerge: true }可以整体替换。第二,刷新间隔不要小于后端缓存时间,否则缓存没命中,等于白做。第三,页面切到后台时浏览器会降频setInterval,如果大屏是常驻投屏,建议用visibilitychange事件在页面可见时立即刷新一次。
联调阶段最常见的问题是跨域。如果大屏页面和接口在同一个 Django 工程里,用模板渲染就不存在跨域;如果前端单独部署,需要装django-cors-headers并在settings.py里配置CORS_ALLOWED_ORIGINS。另一个问题是接口返回的日期格式,strftime("%Y-%m")在 Python 里没问题,但如果直接返回DateField,DRF 会序列化成 ISO 格式,前端要自己截断,不如后端统一格式化好。
5. 避坑与排查:汽车数据大屏上线前必须过的五道坎
5.1 聚合结果为空或月份缺失
现象:月度趋势图某些月份没有数据点,折线断开。原因:原始数据里某些月份确实没有记录,values("month")分组后这些月份不会出现。解决:如果业务要求连续月份,在后端生成完整月份序列,用字典映射补零;或者在前端 x 轴固定月份列表,数据按月份对齐。不要指望数据库补全空月份。
5.2 大屏地图区域名对不上导致缺块
现象:区域分布地图上部分省份是灰色,没有数据颜色。原因:接口返回的region名称和 ECharts 地图 JSON 里的名称不一致,比如「广西」对「广西壮族自治区」。解决:在数据库里存标准全称,或在接口层做名称映射字典,把简称转全称。上线前把地图 JSON 里的所有区域名导出来,和数据库distinct region结果做一次比对。
5.3 接口慢导致大屏加载转圈
现象:大屏打开后图表长时间空白,Network 里接口耗时几秒。原因:聚合查询没走索引,或者一次查了太多维度。解决:用explain看查询计划,确认stat_month、brand、region上的索引被命中;把多个图表合并成一个接口返回,减少请求数;加缓存。如果数据量特别大,考虑预聚合,每天定时把聚合结果写进一张汇总表,大屏只查汇总表。
5.4 前端图表容器宽高为零
现象:ECharts 初始化后不显示,控制台报容器宽高为 0。原因:echarts.init在容器还没布局完成时执行,或者容器父级display: none。解决:把初始化放在DOMContentLoaded之后,或者用setTimeout延迟;如果容器在 tab 里,切到该 tab 时再resize。CSS Grid 布局下,容器要有明确的height,不能只靠内容撑开。
5.5 数据导入后 admin 里中文乱码
现象:admin 列表里品牌名、区域名显示成问号或乱码。原因:数据库字符集不是utf8mb4,或者 CSV 文件编码不是 UTF-8。解决:建库时指定CHARACTER SET utf8mb4,settings.py的OPTIONS里加charset: utf8mb4;导入脚本里open(csv_path, encoding="utf-8"),如果 CSV 是 GBK 就改成gbk。导入前先用file -i或文本编辑器确认编码。
6. 进阶技巧:把大屏从「能看」做到「好用」的几个习惯
大屏做完能跑只是及格,真正拉开差距的是细节。第一个习惯是给每个接口加一个?debug=1参数,返回聚合前的行数和查询耗时,联调时不用去数据库里数。第二个习惯是把所有图表配置抽成一个chartOptions.js,颜色、字体、边距统一管理,换主题时只改一个文件。第三个习惯是给大屏加一个「数据更新时间」角标,从接口返回里带updated_at,让看的人知道数据新鲜度,避免拿旧数据做决策。
再进阶一点,可以把 Django 的聚合接口和定时任务结合。用django-crontab或系统 cron 每天凌晨跑一次预聚合,把月度趋势、品牌市占率、故障率排名写进CarSalesSummary表,大屏接口直接读汇总表,响应时间能从秒级降到毫秒级。预聚合表的字段和接口返回结构对齐,前端完全不用改。这个做法在数据量上千万行之后几乎是必选项。
最后一个习惯是关于验证。大屏上线前,我会用 Django shell 手动跑一遍每个聚合查询,把结果和 Excel 透视表对一遍,确认数字没错。曾经有一次品牌市占率少了两个点,查了半天发现是某个品牌的sales_volume有空值,Sum忽略空值导致总数偏小。从那以后,所有参与聚合的数值字段都设default=0且null=False,导入时强制转成整数。这个坑不踩一次不会长记性,希望帮到你。
本文还有配套的精品资源,点击获取