【Flask】测试平台开发,数据看板开发-第二十一篇

概述:

在前面我们已经实现了我们的产品创建管理,应用管理管理,需求提测管理但是每周提测了多少需求,创建了哪些产品,我们是不是看着不是很直观,接下来我们就需要开发一个数据看板功能,实现能够看到产品下创建的需求,提测数据

先看看实现效果

后端接口源码:

# -*- coding:utf-8 -*-
# application.py
import datetime
import tracebackfrom flask import Blueprint, current_app
from dbutils.pooled_db import PooledDB
from apis.product import connectDB
from configs import config, format
from flask import request
import pymysql.cursors
import json
# from utils.jwt import login_required# 使用数据库连接池的方式链接数据库,提高资源利用率
pool = PooledDB(pymysql, mincached=2, maxcached=5, host=config.MYSQL_HOST, port=config.MYSQL_PORT,user=config.MYSQL_USER, passwd=config.MYSQL_PASSWORD, database=config.MYSQL_DATABASE,cursorclass=pymysql.cursors.DictCursor)test_dashboard = Blueprint("test_dashboard", __name__)@test_dashboard.route("/api/dashboard/stacked", methods=['POST'])
def get_request_stacked():connection = Nonetry:connection = pool.connection()with connection.cursor() as cursor:sql_select = '''SELECT DATE_FORMAT(request.createDate,"%Y%u") weeks, apps.note, COUNT(apps.id) counts FROM request LEFT JOIN apps ON request.appId = apps.id GROUP BY weeks, apps.note;'''cursor.execute(sql_select)table_data = cursor.fetchall()  # 数据库返回的结果,包含 note 为 NULL 的行# === 核心修复:处理 NULL 值 ===weeks = []notes = []key_value = {}for row in table_data:# 1. 处理 weeks 可能为 NULL 的情况(如 createDate 为 NULL 时)week = row['weeks'] or 'No_Week'  # 转为默认字符串# 2. 处理 note 为 NULL 的情况(关键修复!)note = row['note'] or 'No_App'    # 将 NULL 转为 'Unknown_App'counts = row['counts'] or 0            # 确保 counts 不为 NULL# 后续逻辑保持不变,但使用处理后的 week 和 noteif week not in weeks:weeks.append(week)if note not in notes:notes.append(note)# 使用处理后的 week 和 note 拼接键名,避免 NULL 导致的 TypeErrorkey_value[f"{week}_{note}"] = counts  # 建议用下划线分隔,避免歧义(如 week=202534, note=23 变为 20253423)weeks.sort()  # 排序周数# 生成 series 数据(保持不变,但 note 已无 NULL)series = {}for note in notes:series[note] = []for week in weeks:# 使用处理后的键名(带下划线)series[note].append(key_value.get(f"{week}_{note}", 0))resp_data = {'weeks': weeks,'notes': notes,  # 已包含处理后的 'Unknown_App''series': series}resp = format.resp_format_successresp['data'] = resp_datareturn respexcept Exception as e:# current_app.logger.error(f"Error in get_request_stacked: {str(e)}")  # 记录错误日志,方便调试resp = format.resp_format_errorresp['message'] = "Failed to process stacked data"return resp, 500finally:if connection:connection.close()  # 释放连接from datetime import datetime  # 正确的导入方式
@test_dashboard.route("/api/dashboard/metadata", methods=['POST'])
def get_request_stacked_metadata():connection = Nonetry:# === 1. 解析请求体 ===if not request.data:return {"code": 40001, "message": "Request body is empty", "data": [], "total": 0}, 400body = request.get_json()if body is None:return {"code": 40002, "message": "Invalid JSON format", "data": [], "total": 0}, 400current_app.logger.info(f"Request body: {body}")date_range = body.get('date', [])start_date_param = body.get('start_date')end_date_param = body.get('end_date')# 初始化变量start_date = Noneend_date = Nonevalid = False# === 2. 日期参数处理 ===if date_range and len(date_range) == 2:start_str, end_str = date_range[0], date_range[1]date_format = '%Y-%m-%d %H:%M:%S'try:# 使用正确的 datetime.datetime.strptimedatetime.strptime(start_str, date_format)datetime.strptime(end_str, date_format)if start_str <= end_str:start_date = start_strend_date = end_strvalid = Truecurrent_app.logger.info(f"Valid date range: {start_date} to {end_date}")except ValueError:current_app.logger.warning("Invalid date format in date_range")valid = Falseelif start_date_param and end_date_param:date_format = '%Y-%m-%d %H:%M:%S'try:datetime.strptime(start_date_param, date_format)datetime.strptime(end_date_param, date_format)if start_date_param <= end_date_param:start_date = start_date_paramend_date = end_date_paramvalid = Truecurrent_app.logger.info(f"Valid date params: {start_date} to {end_date}")except ValueError:current_app.logger.warning("Invalid date format in start_date/end_date")valid = Falseelse:current_app.logger.info("No date filter applied, querying all data")# === 3. 构建SQL查询 ===connection = pool.connection()with connection.cursor() as cursor:# 临时禁用ONLY_FULL_GROUP_BYtry:cursor.execute("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))")except Exception as mode_error:current_app.logger.warning(f"Could not modify SQL mode: {mode_error}")if valid and start_date and end_date:# 带日期过滤的查询sql = """SELECT CONCAT(YEAR(r.createDate), '-', LPAD(WEEK(r.createDate), 2, '0')) as week_key, \COALESCE(a.note, 'No_App')                                   AS app_name, \COUNT(*)                                                          as request_countFROM request rLEFT JOIN apps a ON r.appId = a.idWHERE r.createDate BETWEEN %s AND %sGROUP BY YEAR (r.createDate), WEEK(r.createDate), a.noteORDER BY week_key, app_name \"""current_app.logger.info(f"Executing filtered query: {start_date} to {end_date}")cursor.execute(sql, (start_date, end_date))else:# 查询所有数据的查询sql = """SELECT CONCAT(YEAR(r.createDate), '-', LPAD(WEEK(r.createDate), 2, '0')) as week_key, \COALESCE(a.note, 'No_App')                                   AS app_name, \COUNT(*)                                                          as request_countFROM request rLEFT JOIN apps a ON r.appId = a.idGROUP BY YEAR (r.createDate), WEEK(r.createDate), a.noteORDER BY week_key, app_name \"""current_app.logger.info("Executing full data query")cursor.execute(sql)table_data = cursor.fetchall()current_app.logger.info(f"Query returned {len(table_data)} rows")# === 4. 处理返回数据 ===cleaned_data = []for row in table_data:cleaned_data.append({'weeks': row.get('week_key', 'No_Week'),'note': row.get('app_name', 'No_App'),'counts': row.get('request_count', 0)})# 成功响应return {"code": 20000,"message": "success","data": cleaned_data,"total": len(cleaned_data)}except json.JSONDecodeError:return {"code": 40000, "message": "Invalid JSON format", "data": [], "total": 0}, 400except Exception as e:current_app.logger.error(f"Metadata API Error:\n"f"Error: {str(e)}\n"f"Stacktrace: {traceback.format_exc()}")return {"code": 50000,"message": "Internal server error","data": [],"total": 0}, 500finally:if connection:try:connection.close()except Exception:pass

前端图标部分主要分别是 Echats G2Plot 组件,从个人使用上来讲前者应用更广、自定义开发更灵活,后者使用上更简单尤其是在数据绑的格式和方式上更友好,在我们使用 Element vue admin 集成分支项目中有关图表的例子基础就是Echats,比如其中的混合图表(柱形+折线)

对应源代码中代码位置依据可从 /views/chats 看到导入的是 echats 也就是说此组件的使用方式,同样是通过添加依赖和导入使用。

结合提测平台的后台数据,接下来就体验下 Echarts 的图表的如何使用。

VUE项目使用步骤

步骤一:项目进行依赖安装

npm install echarts --save

执行完成后可以在 package.json 的 dependencies 配置项目看到 "echarts": "^5.2.2"引依赖被添加。

步骤二:页面添加组件引用和定义一个容器

<template><div class="app-container"><div ref="pieChartDemo" style="width: 600px;height:400px;"></div></div>
</template><script>
import * as echarts from 'echarts'
...
</script>

步骤三:使用 echarts.init 方法初始化一个 echarts 实例并通过setOption方法生成一个简单饼图,余下配置代码如下(注意查看几处注解说明):

export default {name: 'EchartsDemo',// 使用mounted在页面控件加载在完成后mounted方法进行echart初始化非createdmounted() {this.initPieChart()},methods: {initPieChart() {// 采用的是vue ref的方式获取容器var chartDom = this.$refs['pieChartDemo']var myChart = echarts.init(chartDom)var option = {title: {text: '测试开发',subtext: '文章类型分布',left: 'center'},tooltip: {trigger: 'item'},legend: {orient: 'vertical',left: 'left'},series: [{name: 'Access From',type: 'pie',radius: '50%',data: [{ value: 20, name: '提测平台' },{ value: 2, name: '性能测试' },{ value: 1, name: '流量' },{ value: 3, name: '分享' },{ value: 5, name: '其他' }]}]}option && myChart.setOption(option);}}
}

堆叠面积图

在掌握了Echar表的渲染方法和接口数据格式化的充分准备之后,就可以直接上在 src/views/dashboard/index.vue 编写代码,注意实现里有个额外的 series 数据处理,已经标注在代码注解里了。

<template><div class="dashboard-container"><div ref="LineChartBoard" style="width: 95%;height:500px;"></div></div>
</template><script>
import * as echarts from 'echarts'
import { requestStacked } from '@/api/board'export default {name: 'Dashboard',mounted() {this.getApList()},methods: {getApList() {requestStacked().then(resp => {this.initStackedChart(resp.data)})},initStackedChart(data) {const chartDom = this.$refs['LineChartBoard']const myChart = echarts.init(chartDom)const series = []// 唯一处理需要额外逻辑处理的地方,根据接口数据动态生成series数据for (var key in data.series) {series.push({name: key,type: 'line',stack: 'Total',areaStyle: {},emphasis: {focus: 'series'},data: data.series[key]})}var option = {title: {text: '周需求提测趋势'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#6a7985'}}},legend: {// 数据标题展示data: data.note},toolbox: {feature: {saveAsImage: {}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{type: 'category',boundaryGap: false,data: data.weeks}],yAxis: [{type: 'value'}],series: series}option && myChart.setOption(option)}}
}
</script>

完整的vue前端源码

<template><div class="dashboard-container"><div class="filter-container"><el-form :inline="true" :model="searchValue"><el-form-item label="日期选择"><el-date-pickerv-model="searchValue.date"type="daterange"value-format="yyyy-MM-dd HH:mm:ss"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"></el-date-picker></el-form-item><el-form-item><el-button type="primary" @click="searchBoard">刷新查询</el-button></el-form-item><el-form-item><el-switchv-model="stackedColumnMode"@change="changeBoardMode"active-text="分组模式"inactive-text="累积模式"></el-switch></el-form-item></el-form></div><el-card class="box-card"><div slot="header" class="clearfix"><span>周需求分组量</span></div><div id="ColumnBoard" style="width: 95%;height:360px;" /></el-card><br><el-card class="box-card"><div ref="LineChartBoard" style="width: 95%;height:500px;" /></el-card></div>
</template><script>
import * as echarts from 'echarts'
import { Column } from '@antv/g2plot'import { requestStacked, requestMetaData } from '@/api/board'export default {name: 'Dashboard',created() {this.getAppList()this.getMetaDate()},mounted() {this.stackedColumnPlot = new Column('ColumnBoard', {data: this.stackedColumnData,xField: 'weeks',yField: 'counts',seriesField: 'note',isGroup: this.stackedColumnMode ? 'true' : 'false',columnStyle: {radius: [20, 20, 0, 0]}})this.stackedColumnPlot.render()},data() {return {stackedColumnPlot: undefined,stackedColumnData: [],stackedColumnMode: true,searchValue: {date: []}}},methods: {getAppList() {requestStacked().then(resp => {this.initStackedChart(resp.data)})},getMetaDate() {const params = {date: this.searchValue.date}requestMetaData(params).then(resp => {this.stackedColumnData = resp.datathis.stackedColumnPlot.changeData(this.stackedColumnData)this.initStackedColumn(resp.data)})},// initStackedColumn(data) {//   const stackedColumnPlot = new Column('ColumnBoard', {//     data,//     xField: 'weeks',//     yField: 'counts',//     seriesField: 'note',//     isGroup: 'true',//     columnStyle: {//       radius: [20, 20, 0, 0]//     }//   })//   stackedColumnPlot.render()// },initStackedChart(data) {const chartDom = this.$refs['LineChartBoard']const myChart = echarts.init(chartDom)const series = []// 唯一处理需要额外逻辑处理的地方,根据接口数据动态生成series数据for (var key in data.series) {series.push({name: key,type: 'line',stack: 'Total',areaStyle: {},emphasis: {focus: 'series'},data: data.series[key]})}var option = {title: {text: '周需求提测趋势'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#6a7985'}}},legend: {data: data.note},toolbox: {feature: {saveAsImage: {}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{type: 'category',boundaryGap: false,data: data.weeks}],yAxis: [{type: 'value'}],series: series}option && myChart.setOption(option)},searchBoard() {this.getMetaDate()},// 更改显示类型changeBoardMode() {const options = {isGroup: this.stackedColumnMode}this.stackedColumnPlot.update(options)}}
}
</script><style lang="scss" scoped>
.dashboard {&-container {margin: 30px;}&-text {font-size: 30px;line-height: 46px;}
}
</style>

最终实现后就是我们一开始截图后的实现效果

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/96133.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/96133.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我是程序员,不是程序猿:请别把我当猴耍——拒绝被低估,用专业赢得尊重

摘要 本文旨在深度剖析“程序员”与“程序猿”一字之差背后所反映的职业尊严与身份认同问题。我们生活在一个技术驱动的时代&#xff0c;但对技术创造者的认知却常常被“程序猿”、“码农”等标签简单化、甚至矮化。本文将从正名开始&#xff0c;辨析“程序员”的专业内涵&…

C++中vector删除操作的安全隐患与最佳实践

std::vector 是C标准模板库&#xff08;STL&#xff09;中最常用的动态数组容器&#xff0c;提供了高效的随机访问和动态扩容能力。然而&#xff0c;其删除操作如果使用不当&#xff0c;会引入严重的安全隐患&#xff0c;包括未定义行为、内存泄漏和数据竞争等问题。本文将深入…

Unix/Linux 系统中的 `writev` 系统调用

<摘要> 本文对 Unix/Linux 系统中的 writev 系统调用进行了全面深入的解析。内容涵盖了其产生的背景&#xff08;从传统 write 的局限性到分散/聚集 I/O 概念的引入&#xff09;、核心概念&#xff08;如 struct iovec、系统调用流程&#xff09;。重点剖析了其设计意图&…

深入理解 Android targetSdkVersion:从 Google Play 政策到依赖冲突

深入理解 Android targetSdkVersion&#xff1a;从 Google Play 政策到依赖冲突 作为 Android 开发者&#xff0c;你很可能在 Android Studio 中见过这条提示&#xff1a;Google Play requires that apps target API level 33 or higher。它像一个尽职的提醒者&#xff0c;时常…

灰匣(GrayBox)1.0.0 发布【提升系统权限APP】

灰匣是一个提升系统权限的工具&#xff0c;可以配合Root、三方软件&#xff08;Shizuku&#xff09;以及【设备管理员】&#xff08;设备所有者&#xff09;实现一些高级功能及底层接口&#xff0c;可以自动隔离&#xff08;冻结/禁用&#xff09;不必要的应用&#xff0c;如某…

PAT 1104 Sum of Number Segments

这一题的大意就是找一个数组中的所有子数组&#xff0c;它们的累加和为多少&#xff0c; 题目上给出的数据范围是O(n^5)那么只能遍历一次&#xff0c;不能用暴力的方法求出。 看到这一题我有两个思路&#xff1a; 1.试图用双指针和滑动窗口来把O&#xff08;n^2)的时间复杂度降…

[万字长文]AJAX入门-常用请求方法和数据提交、HTTP协议-报文、接口文档、案例实战

本系列可作为前端学习系列的笔记&#xff0c;代码的运行环境是在VS code中&#xff0c;小编会将代码复制下来&#xff0c;大家复制下来就可以练习了&#xff0c;方便大家学习。 HTML、CSS、JavaScript系列文章 已经收录在前端专栏&#xff0c;有需要的宝宝们可以点击前端专栏查…

Codesy中的UDP发送信息

Codesy UDP通讯 概述 CAA Net Base Services UDP通讯的建立 发送UDP 状态控制 效果 概述 Codesys中默认安装的通讯支持很多,不安装其他的软件也可以实现TCP通讯。但是,在使用UDP通讯时,因为我们的PLC有两个网卡,一般我们把第一个网口做编程和HMI用,把的个网口做外部通讯,…

神经网络之深入理解偏置

&#x1f50d; 1. 表达能力&#xff1a;无偏模型不能表示全体函数族 ✔ 有偏线性变换&#xff1a; yWxb&#xff08;仿射变换&#xff09; y Wx b \quad \text{&#xff08;仿射变换&#xff09;} yWxb&#xff08;仿射变换&#xff09; 能表示任意线性函数 平移是仿射空间的…

小白必看:AI智能体零基础搭建全攻略!

写在前面&#xff1a;别怕&#xff0c;真的不需要技术背景&#xff01; 你是不是经常听到"AI智能体"、"大模型"这些高大上的词&#xff0c;总觉得那是技术大牛的专利&#xff1f;别担心&#xff0c;这篇教程就是为你准备的&#xff01;我们将用最通俗的语…

React state在setInterval里未获取最新值的问题

目录 一、问题描述 二、解决方案 方案一&#xff0c;使用函数式更新 方案二&#xff0c;使用 useRef 保存最新值 一、问题描述 在 React 中&#xff0c;当在 setInterval或setTimeout 中使用 setState 时&#xff0c;经常会遇到状态不是最新值的问题。这是因为闭包导致的&a…

x86 架构 Docker 镜像迁移至 ARM 环境的详细指南

目录 一、问题背景与分析 二、解决步骤 &#xff08;一&#xff09;检查 docker-compose 版本 &#xff08;二&#xff09;升级 docker-compose 1. 对于 Linux 系统 2. 对于 Windows 系统 &#xff08;三&#xff09;验证升级 &#xff08;四&#xff09;重新运行 dock…

零代码部署工业数据平台:TRAE + TDengine IDMP 实践

对于编程初学者来说&#xff0c;软件开发流程中的开发环境配置、安装异常或报错往往需要花费大量时间查阅资料和反复试错&#xff0c;才能正常安装和启动某些软件工具。现在&#xff0c;在 TRAE 的帮助下&#xff0c;即使完全没有接触过编程&#xff0c;也能通过自然语言直接表…

史上最全Flink面试题(完整版)

1、简单介绍一下 FlinkFlink 是一个框架和分布式处理引擎&#xff0c;用于对无界和有界数据流进行有状态计算。并且 Flink 提供了数据分布、容错机制以及资源管理等核心功能。Flink提供了诸多高抽象层的API以便用户编写分布式任务&#xff1a;DataSet API&#xff0c; 对静态数…

C# .NET中使用log4Net日志框架指南

C# .NET中使用log4Net日志框架指南 log4Net是Apache基金会开发的一款高效、灵活的日志记录框架&#xff0c;广泛应用于.NET生态系统中。它支持多种日志输出目标&#xff08;如文件、数据库、控制台&#xff09;&#xff0c;并提供细粒度的日志级别控制&#xff0c;帮助开发者监…

每日算法刷题Day68:9.10:leetcode 最短路6道题,用时2h30min

一. 单源最短路&#xff1a;Dijkstra 算法 1.套路 1.Dijkstra 算法介绍 (1)定义 g[i][j] 表示节点 i 到节点 j 这条边的边权。如果没有 i 到 j 的边&#xff0c;则 g[i][j]∞。 (2)定义 dis[i] 表示起点 k 到节点 i 的最短路长度&#xff0c;一开始 dis[k]0&#xff0c;其余 …

Spring Boot + Apache Tika 从文件或文件流中提取文本内容

应用效果&#xff1a;1、安装 Apache Tika 依赖pom.xml<!-- Apache Tika 从文件中提取结构化文本和元数据 --><dependency><groupId>org.apache.tika</groupId><artifactId>tika-core</artifactId><version>2.9.2</version>&l…

qqq数据结构补充

1.绪论1.存储方式顺序存储&#xff1a;逻辑相邻&#xff0c;物理相邻链式存储&#xff1a;逻辑相邻&#xff0c;物理不一定相邻2.线性表1.顺序表1.不可扩容数组写一个顺序表1.在头文件中应有#pragam once&#xff0c;防止头文件多次编译&#xff1b;如果头文件多次编译&#x…

Anaconda与Jupyter 安装和使用

Anaconda内部集成了很多科学计算包&#xff0c;并且可以实现环境隔离 1. 安装Anaconda 定义&#xff1a;Anaconda是一个集成的Python发行版&#xff0c;专为数据科学、机器学习和AI开发而设计。它包含了常用的Python库、包管理工具&#xff08;Conda&#xff09;和Jupyter No…

5.后台运行设置和包设计与实现

程序的入口点(想让其后台默认.exe进程运行)也可以不通过vs设置也可以通过定义预处理设置第三种就是没有窗口的变成后台运行的了 处理client传来的数据包 第一步&#xff1a;咱们怎么设计一种包呢&#xff1f;FEFF在网络环境里面出现的概率低所以就采用这个 自己数据包截断了&am…