⚠️免责声明:本文项目仅供技术学习参考,严禁直接作为毕业设计或课程作业提交。请结合自身需求进行二次开发与创新,学术诚信从我做起 🙂
老师第一眼想看什么
毕设做到最后,功能和代码其实都差不多了。
真正让老师记住你的,往往是答辩那几分钟里他眼睛看到的东西。
老师进你后台的第一眼,看到的要么是一张枯燥的表格列表,要么是一块会动的数据大屏。这两个给人的感觉完全不一样。
图:这次要做的数据大屏——近14天订单趋势、商品销量TOP、订单状态分布、支付方式分布,四张图一屏展示
大屏不需要多复杂,四个图表就够了:销售额、订单趋势、销量排行、分类占比。
而且这东西投入产出比极高:后端四个统计接口,前端一个 ECharts,一两天就能搞定,但一眼看上去,像是个做完整了的项目。
这篇讲怎么加。
先倒推:老师想看什么
别一上来就写代码。先想清楚一件事:老师站在你旁边看演示,他的眼睛会往哪看?
大概率是这几个问题:
- 你这系统一共卖了多少?——总销售额
- 最近生意怎么样,是涨还是跌?——订单趋势
- 卖得最好的是哪个商品?——销量排行
- 哪个分类贡献最多?——分类占比
这四个问题,就是四个图表。
再加四个数字卡片放在最上面:总销售额、总订单数、今日订单、用户总数。
一块大屏的骨架就出来了。
一、后端:四个统计接口
先建一个后台首页的路由。
# admin/dashboard.pyfromflaskimportjsonify,render_templatefromsqlalchemyimportfuncfromdatetimeimportdatetime,timedeltafrommodelsimportdb,OrderInfo,OrderItem,Goods,Category,User@app.route('/admin/dashboard')@admin_requireddefdashboard():"""大屏页面"""returnrender_template('admin/dashboard.html')@app.route('/admin/dashboard/data')@admin_requireddefdashboard_data():"""大屏数据接口(前端 AJAX 拉取)"""# ========== 顶部四个数字卡片 ==========# 总销售额:只统计"已支付"的订单total_sales=db.session.query(func.coalesce(func.sum(OrderInfo.pay_amount),0)).filter(OrderInfo.status.in_([1,2,3])).scalar()total_orders=OrderInfo.query.count()total_users=User.query.count()today=datetime.now().date()today_orders=OrderInfo.query.filter(func.date(OrderInfo.created_at)==today).count()# ========== 图1:近7天订单趋势(折线图) ==========days,counts=[],[]foriinrange(6,-1,-1):day=today-timedelta(days=i)cnt=OrderInfo.query.filter(func.date(OrderInfo.created_at)==day).count()days.append(day.strftime('%m-%d'))counts.append(cnt)# ========== 图2:商品销量 Top5(柱状图) ==========top_goods=db.session.query(OrderItem.goods_name,func.sum(OrderItem.quantity).label('total_qty')).group_by(OrderItem.goods_name).order_by(func.sum(OrderItem.quantity).desc()).limit(5).all()# ========== 图3:分类销售占比(饼图) ==========category_sales=db.session.query(Category.name,func.sum(OrderItem.subtotal).label('amount')).join(Goods,Goods.category_id==Category.id)\.join(OrderItem,OrderItem.goods_id==Goods.id)\.group_by(Category.name).all()returnjsonify({'cards':{'total_sales':round(float(total_sales),2),'total_orders':total_orders,'today_orders':today_orders,'total_users':total_users,},'trend':{'days':days,'counts':counts,},'top_goods':{'names':[g.goods_nameforgintop_goods],'values':[int(g.total_qty)forgintop_goods],},'category':{'names':[c.nameforcincategory_sales],'values':[round(float(c.amount),2)forcincategory_sales],},})这段代码有两个地方要注意,答辩会问:
① 总销售额为什么只算status in [1, 2, 3]?
因为订单有 7 种状态,其中:
- 0 待支付 → 钱还没到,不算
- 1 待发货 / 2 待收货 / 3 已完成 → 钱已付,算
- 4 已取消 → 不算
- 5 退款中 / 6 已退款 → 钱退了,不算
如果直接SUM(pay_amount)把所有订单都加上,销售额就虚高了。这个细节说出来,老师会觉得你考虑周全。
② 为什么用func.coalesce(..., 0)?
因为一条订单都没有的时候,SUM返回的是NULL,前端拿到null会报错。coalesce的作用是:如果结果是NULL,就返回 0。
二、前端:一个页面搞定四个图
用 ECharts 的 CDN,不用装任何东西。
<!-- templates/admin/dashboard.html --><!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>数据大屏</title><scriptsrc="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script><style>*{margin:0;padding:0;box-sizing:border-box;}body{background:#0f1623;color:#e8eef7;font-family:"Microsoft YaHei",sans-serif;padding:20px;}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px;}.card{background:#1a2437;border-radius:10px;padding:20px;border-left:4px solid #2e75b6;}.card .label{font-size:14px;color:#8fa3bf;}.card .value{font-size:30px;font-weight:bold;margin-top:8px;}.charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;}.chart-box{background:#1a2437;border-radius:10px;padding:16px;}/* 关键:容器必须给高度,否则图表不显示 */.chart{width:100%;height:320px;}</style></head><body><divclass="cards"><divclass="card"><divclass="label">总销售额(元)</div><divclass="value"id="totalSales">-</div></div><divclass="card"><divclass="label">总订单数</div><divclass="value"id="totalOrders">-</div></div><divclass="card"><divclass="label">今日订单</div><divclass="value"id="todayOrders">-</div></div><divclass="card"><divclass="label">用户总数</div><divclass="value"id="totalUsers">-</div></div></div><divclass="charts"><divclass="chart-box"><divid="trendChart"class="chart"></div></div><divclass="chart-box"><divid="goodsChart"class="chart"></div></div><divclass="chart-box"><divid="categoryChart"class="chart"></div></div><divclass="chart-box"><divid="rankChart"class="chart"></div></div></div><script>// 1. 拉数据fetch('/admin/dashboard/data').then(res=>res.json()).then(data=>{// 数字卡片document.getElementById('totalSales').innerText=data.cards.total_sales;document.getElementById('totalOrders').innerText=data.cards.total_orders;document.getElementById('todayOrders').innerText=data.cards.today_orders;document.getElementById('totalUsers').innerText=data.cards.total_users;renderTrend(data.trend);renderGoods(data.top_goods);renderCategory(data.category);});// 2. 折线图:订单趋势functionrenderTrend(trend){constchart=echarts.init(document.getElementById('trendChart'));chart.setOption({title:{text:'近7天订单趋势',textStyle:{color:'#e8eef7',fontSize:16}},tooltip:{trigger:'axis'},xAxis:{type:'category',data:trend.days,axisLine:{lineStyle:{color:'#3a4a63'}}},yAxis:{type:'value',splitLine:{lineStyle:{color:'#243247'}}},series:[{type:'line',data:trend.counts,smooth:true,areaStyle:{opacity:0.2},itemStyle:{color:'#2e75b6'},}],});}// 3. 柱状图:商品销量 Top5functionrenderGoods(top){constchart=echarts.init(document.getElementById('goodsChart'));chart.setOption({title:{text:'商品销量 Top5',textStyle:{color:'#e8eef7',fontSize:16}},tooltip:{trigger:'axis'},xAxis:{type:'category',data:top.names,axisLabel:{interval:0,rotate:15}},yAxis:{type:'value',splitLine:{lineStyle:{color:'#243247'}}},series:[{type:'bar',data:top.values,barWidth:'45%',itemStyle:{color:'#4a9de0',borderRadius:[6,6,0,0]},}],});}// 4. 饼图:分类销售占比functionrenderCategory(cate){constchart=echarts.init(document.getElementById('categoryChart'));chart.setOption({title:{text:'分类销售占比',textStyle:{color:'#e8eef7',fontSize:16}},tooltip:{trigger:'item'},legend:{bottom:0,textStyle:{color:'#8fa3bf'}},series:[{type:'pie',radius:['40%','65%'],data:cate.names.map((name,i)=>({name:name,value:cate.values[i]})),}],});}// 5. 窗口缩放时图表自适应window.addEventListener('resize',()=>{['trendChart','goodsChart','categoryChart'].forEach(id=>{constinst=echarts.getInstanceByDom(document.getElementById(id));if(inst)inst.resize();});});</script></body></html>四个图分别是:折线(趋势)、柱状(排行)、饼图(占比),加上顶部四个数字卡片。
暗色背景 + 卡片式布局,看着就有"大屏"那味儿。
三、ECharts 最容易踩的三个坑
坑1:容器没有高度,图表不显示
这是新手 90% 的报错。
ECharts 的容器 div必须有明确的高度,否则它渲染出来是 0 高度,你什么都看不见,但控制台也不报错。
/* 错误:只有宽度没有高度 */.chart{width:100%;}/* 正确 */.chart{width:100%;height:320px;}坑2:DOM 还没加载就 init
如果你把<script>写在<head>里,DOM 还没生成,document.getElementById拿到的是null。
两个办法:
- 把
<script>放到</body>前面(推荐) - 或者用
window.onload = function() { ... }包起来
坑3:窗口变了图表不跟着变
ECharts 只在初始化时读一次容器尺寸。你拖动浏览器窗口,图表不会自动调整,就会变形。
解决:监听resize事件,调resize()方法。上面的代码最后那段就是干这个的。
四、想让效果更炸?加两个小东西
大屏基础版做完,如果还有时间,可以加两个:
① 数字滚动动画
销售额从 0 滚动到实际数值,比直接显示一个数字有感觉。用CountUp.js或手写一个setInterval就行。
② 自动刷新
setInterval(()=>{fetch('/admin/dashboard/data').then(r=>r.json()).then(updateAll);},30000);// 每30秒刷新一次答辩的时候挂着,数据会自己动,老师会觉得"这系统是活的"。
数据大屏,答辩会问什么
1. 这些统计数据是实时查数据库吗?
答:目前是前端 AJAX 请求时实时查库,对毕设这种数据量完全够用。如果数据量大了,可以做优化:一是加 Redis 缓存,统计结果缓存几分钟;二是用定时任务每小时预计算一次,把结果存到一张统计表里,前端直接读表,避免每次都做全表聚合。
2. 统计 SQL 为什么用聚合函数?
答:因为要的不是某一条记录,而是整体的汇总值。比如总销售额要SUM(pay_amount),销量排行要SUM(quantity) GROUP BY goods_name。聚合比把数据全部查出来在 Python 里循环相加效率高得多,数据量大的时候差距很明显。
3. 总销售额为什么排除了某些订单状态?
答:订单有 7 种状态,其中待支付的钱没到账、已取消和已退款的订单钱要退回去,都不该计入销售额。只有已支付状态的订单(待发货、待收货、已完成)才是真实收入。如果不做这个过滤,销售额就是虚的。
4. 这些图表数据怎么传给前端?
答:后端提供 JSON 接口,前端用 fetch 异步请求。后端只返回纯数据(数组和数字),不带任何图表逻辑;前端拿到数据后用 ECharts 渲染。前后端职责分离,后端改统计逻辑不用动前端。
5. 分类销售占比是怎么算的?
答:从订单明细表出发,关联商品表拿到分类 ID,再关联分类表拿到分类名,按分类分组对明细的小计金额求和。这样即使商品后来换了分类,历史订单的金额统计依然准确,因为用的是订单明细里的快照数据。
数据大屏这东西,技术含量不算高,但性价比高得离谱。
四个接口、一个页面,换来的是答辩现场第一眼的印象分。
而且它还有个隐藏好处:老师问"你的系统有什么亮点",你有一块能看的东西可以直接指。
梅雅达编程笔记原创 · 首发 CSDN · 转载请注明出处