【Flask】测试平台开发,产品管理功能UI重构-第九篇
概述
在前面几篇文章中,我们实现了添加功能,编辑功能,删除功能,搜索功能,那么接下来我们是不是因该要实现一个分页功能,毕竟如果数据多了,是不是有个翻页的功能比较好,这里分页功能我就暂时不开发,不过我提供后端接口,你们试试,自己可不可以独立写完这个分页的前端页面,并且可以使用,这里我们重点事重构一下产品管理这块的UI
在之前的文章中我们虽然实现了功能,当你有没有发现,我们列表页面的布局和编辑,删除按钮部分整体看着比较low,不是很直观
原UI如下:
接下来我们重构页面的UI展示,以及操作部分
前端完整源码如下:
<template><div class="app-container"><div class="search-container"><div class="search-card"><el-form :inline="true" :model="search" class="search-form"><el-form-item label="名称"><el-inputv-model="search.title"placeholder="支持模糊查询"class="search-input"clearable/></el-form-item><el-form-item label="关键词"><el-inputv-model="search.keyCode"placeholder="支持模糊查询"class="search-input"clearable/></el-form-item><el-form-item class="search-btn-group"><el-button type="primary" plain @click="searchProduct()" icon="el-icon-search">搜索</el-button><el-button type="default" plain @click="resetSearch()" icon="el-icon-refresh-left">重置</el-button></el-form-item></el-form><el-button type="primary" icon="el-icon-plus" @click="dialogProduct()">新增</el-button></div></div><el-dialog:title="dialogProductStatus==='ADD'?'添加产品或项目':'修改产品或项目'":visible.sync="dialogProductShow":close-on-click-modal="false"width="600px"><!-- 对话框内容保持不变 --><el-form:model="product":rules="rules"ref="productForm"label-position="right"label-width="110px"><el-form-item v-if="dialogProductStatus==='UPDATE'" label="编号:" prop="id"><el-input v-model="product.id" disabled></el-input></el-form-item><el-form-item label="项目名称:" prop="title"><el-input v-model="product.title" placeholder="请填写中文名称"></el-input></el-form-item><el-form-item label="唯一标识符:" prop="keyCode"><el-input v-model="product.keyCode" placeholder="产品/项目唯一码"></el-input></el-form-item><el-form-item label="项目描述:" prop="desc"><el-input v-model="product.desc" type="textarea" placeholder="备注说明" :rows="3"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="dialogProductShow = false">取 消</el-button><el-buttonv-if="dialogProductStatus === 'ADD'"type="primary"@click="submitForm('productForm', pCreate)":loading="loading">添 加</el-button><el-buttonv-if="dialogProductStatus === 'UPDATE'"type="primary"@click="submitForm('productForm', pUpdate)":loading="loading">修 改</el-button></span></el-dialog><!-- 表格区域 --><el-table:data="tableData"v-loading="tableLoading"borderclass="table-container":cell-style="{padding: '12px 0'}":header-cell-style="{background: '#f5f7ff', color: '#303134'}"><el-table-column prop="id" label="编号" width="80" align="center"/><el-table-column prop="title" label="项目名称" min-width="100" align="center" /><el-table-column prop="keyCode" label="唯一标识符" width="100" align="center"/><el-table-column prop="desc" label="项目描述" show-overflow-tooltip min-width="100" align="center"/><el-table-column prop="operator" label="创建者" width="120" align="center"/><el-table-column :formatter="formatDate" prop="update" label="创建时间" width="160" align="center"/><el-table-column label="操作" width="280" fixed="right" align="center"><template slot-scope="scope"><div class="table-action-buttons"><el-buttonsize="mini"type="primary"icon="el-icon-edit"@click="dialogProductUpdate(scope.row)">编辑</el-button>
<!-- <el-button-->
<!-- size="mini"-->
<!-- type="warning"-->
<!-- icon="el-icon-remove-outline"-->
<!-- @click="pSoftRemove(scope.row.id)">停用</el-button>--><el-buttonsize="mini"type="danger"icon="el-icon-delete"@click="pHardRemove(scope.row.id)">删除</el-button></div></template></el-table-column></el-table></div>
</template><script>
import { apiProductList, apiProductCreate, apiProductUpdate, apiProductDelete, apiProductRemove, apiProductSearch } from '@/api/product'
import store from '@/store'
import moment from 'moment'export default {name: 'Product',data() {return {op_user: store.getters.name,search: {title: undefined,keyCode: undefined},product: {id: undefined,title: undefined,keyCode: undefined,desc: undefined,operator: this.op_user},dialogProductShow: false,dialogProductStatus: 'ADD',tableData: [],tableLoading: false,loading: false,rules: {title: [{ required: true, message: '请输入名称', trigger: 'blur' },{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }],keyCode: [{ required: true, message: '请输入唯一码', trigger: 'blur' },{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }],desc: [{ max: 200, message: '长度不能超过 200 个字符', trigger: 'blur' }]}}},created() {this.getProductList()},methods: {getProductList() {this.tableLoading = trueapiProductList().then(response => {this.tableData = response.data}).finally(() => {this.tableLoading = false})},searchProduct() {this.tableLoading = trueapiProductSearch(this.search).then(res => {this.tableData = res.data}).finally(() => {this.tableLoading = false})},resetSearch() {this.search = {title: undefined,keyCode: undefined}this.getProductList()},formatDate(row, column) {const date = row[column.property]if (!date) return ''return moment(date).utcOffset(0).format('YYYY-MM-DD HH:mm:ss')},dialogProduct() {this.product = {id: undefined,keyCode: '',title: '',desc: '',operator: this.op_user}this.dialogProductStatus = 'ADD'this.dialogProductShow = truethis.$nextTick(() => {this.$refs.productForm && this.$refs.productForm.clearValidate()})},submitForm(formName, callback) {this.$refs[formName].validate((valid) => {if (valid) {this.loading = truecallback()} else {return false}})},pCreate() {apiProductCreate(this.product).then(response => {this.$notify({title: '成功',message: '项目或产品添加成功',type: 'success',duration: 2000})this.dialogProductShow = falsethis.getProductList()}).finally(() => {this.loading = false})},dialogProductUpdate(row) {this.product = {id: row.id,keyCode: row.keyCode,title: row.title,desc: row.desc,operator: this.op_user}this.dialogProductStatus = 'UPDATE'this.dialogProductShow = true},pUpdate() {apiProductUpdate(this.product).then(res => {this.$notify({title: '成功',message: '项目或产品修改成功',type: 'success',duration: 2000})this.dialogProductShow = falsethis.getProductList()}).finally(() => {this.loading = false})},pHardRemove(id) {// 移除确认按钮的加载状态,仅保留表格加载状态this.$confirm('此操作将永久删除该项目, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {// 只显示表格加载状态,不控制按钮状态this.tableLoading = trueapiProductDelete(id).then(res => {this.$message({type: 'success',message: '删除成功!'})this.getProductList() // 该方法内部已处理tableLoading}).catch(() => {this.tableLoading = false})}).catch(() => {this.$message({type: 'info',message: '已取消删除'})})},pSoftRemove(id) {// 移除确认按钮的加载状态,仅保留表格加载状态this.$confirm('此操作将停用该项目, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {// 只显示表格加载状态,不控制按钮状态this.tableLoading = trueapiProductRemove(id).then(res => {this.$message({type: 'success',message: '停用成功!'})this.getProductList() // 该方法内部已处理tableLoading}).catch(() => {this.tableLoading = false})}).catch(() => {this.$message({type: 'info',message: '已取消停用'})})}}
}
</script><style scoped>
/* 样式保持不变 */
.search-container {padding: 16px;
}.search-card {background: linear-gradient(145deg, #f5f9ff, #e6ecf5);border-radius: 12px;padding: 24px;box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);display: flex;justify-content: space-between;align-items: center;
}.search-form {display: flex;align-items: center;flex-wrap: wrap;gap: 20px;
}.search-input {width: 260px;border-radius: 6px;
}.search-btn-group {display: flex;gap: 12px;
}.table-container {margin: 0 16px 16px;border-radius: 8px;overflow: hidden;
}.table-action-buttons {display: flex;gap: 12px;justify-content: center;padding: 0 10px;
}@media (max-width: 768px) {.search-card {flex-direction: column;align-items: stretch;gap: 16px;}.search-form {justify-content: center;}.search-input {width: 100%;}.table-action-buttons {flex-wrap: wrap;}
}
</style>
最终重构后的效果如下:
前后对比,是不是发现我们重构后的UI看着更加的舒服
后端完整的源码如下:
# -*- coding:utf-8 -*-from flask import Blueprint, jsonify
import pymysql.cursors
from flask import request
import json
from configs import config
from dbutils.pooled_db import PooledDBapp_product = Blueprint("app_product", __name__)# 使用用户名密码创建数据库链接
# PyMySQL使用文档 https://pymysql.readthedocs.io
def connectDB():connection = pymysql.connect(host=config.MYSQL_HOST, # 数据库IP地址或链接域名user=config.MYSQL_USER, # 设置的具有增改查权限的用户password=config.MYSQL_PASSWORD, # 用户对应的密码database=config.MYSQL_DATABASE,# 数据表charset='utf8mb4', # 字符编码cursorclass=pymysql.cursors.DictCursor) # 结果作为字典返回游标# 返回新的书库链接对象return connection# 搜索接口
@app_product.route("/api/product/search",methods=['GET'])
def product_search():# 获取title和keyCodetitle = request.args.get('title')keyCode = request.args.get('keyCode')# 基础语句定义sql = "SELECT * FROM `products` WHERE `status`=0"# 如果title不为空,拼接tilite的模糊查询if title is not None:sql = sql + " AND `title` LIKE '%{}%'".format(title)# 如果keyCode不为空,拼接tilite的模糊查询if keyCode is not None:sql = sql + " AND `keyCode` LIKE '%{}%'".format(keyCode)# 排序最后拼接(分页查询)sql = sql + " ORDER BY `update` DESC"connection = connectDB()# 使用python的with..as控制流语句(相当于简化的try except finally)with connection.cursor() as cursor:# 按照条件进行查询cursor.execute(sql)data = cursor.fetchall()# 按返回模版格式进行json结果返回resp_data = {"code": 20000,"data": data}return resp_data@app_product.route("/api/product/searchPage",methods=['GET'])
def product_search_page():# 获取title和keyCodetitle = request.args.get('title')keyCode = request.args.get('keyCode')# 新增页数和每页个数参数,空时候做默认处理,并注意前端传过来可能是字符串,需要做个强制转换pageSize = 10 if request.args.get('pageSize') is None else int(request.args.get('pageSize'))currentPage = 1 if request.args.get('currentPage') is None else int(request.args.get('currentPage'))sql = "SELECT * FROM `products` WHERE `status`=0"# 增加基础全量个数统计sqlCount = "SELECT COUNT(*) as `count` FROM `products` WHERE `status`=0"# 条件拼接全量统计也需要同步if title is not None:sql = sql + " AND `title` LIKE '%{}%'".format(title)sqlCount = sqlCount + " AND `title` LIKE '%{}%'".format(title)if keyCode is not None:sql = sql + " AND `keyCode` LIKE '%{}%'".format(keyCode)sqlCount = sqlCount + " AND `keyCode` LIKE '%{}%'".format(keyCode)# 排序最后拼接带分页查询sql = sql + ' ORDER BY `update` DESC LIMIT {},{}'.format((currentPage - 1) * pageSize, pageSize)connection = connectDB()# 使用python的with..as控制流语句(相当于简化的try except finally)with connection:# 先查询总数with connection.cursor() as cursor:cursor.execute(sqlCount)total = cursor.fetchall()# 执行查询分页查询with connection.cursor() as cursor:# 按照条件进行查询cursor.execute(sql)data = cursor.fetchall()# 带着分页查询结果和总条数返回,total注意是list字段需要下角标key取值resp_data = {"code": 20000,"message": "success","data": data,"total": total[0]['count']}return resp_data@app_product.route("/api/product/list", methods=['GET'])
def product_list():# 初始化数据库链接connection = connectDB()# 使用python的with..as控制流语句(相当于简化的try except finally)with connection.cursor() as cursor:# 查询产品信息表-按更新时间新旧排序sql = "SELECT * FROM `products` WHERE `status`=0 ORDER BY `update` DESC"cursor.execute(sql)data = cursor.fetchall()# 按返回模版格式进行json结果返回resp_data = {"code": 20000,"data": data}return resp_data# [POST方法]实现新建数据的数据库插入
@app_product.route("/api/product/create",methods=['POST'])
def product_create():# 初始化数据库链接connection = connectDB()# 定义默认返回结构体resp_data = {"code": 20000,"message": "success","data": []}# 获取请求传递json bodybody = request.get_data()body = json.loads(body)with connection:# 先做个查询,判断keyCode是否重复(这里的关键词最初定义为唯一项目编号或者为服务的应用名)with connection.cursor() as cursor:select = "SELECT * FROM `products` WHERE `keyCode`=%s AND `status`=0"cursor.execute(select, (body["keyCode"],))result = cursor.fetchall()# 有数据说明存在相同值,封装提示直接返回if len(result) > 0:resp_data["code"] = 20001resp_data["message"] = "唯一编码keyCode已存在"return resp_datawith connection.cursor() as cursor:# 拼接插入语句,并用参数化%s构造防止基本的SQL注入# 其中id为自增,插入数据默认数据设置的当前时间sql = "INSERT INTO `products` (`keyCode`,`title`,`desc`,`operator`) VALUES (%s,%s,%s,%s)"cursor.execute(sql, (body["keyCode"], body["title"], body["desc"], body["operator"]))# 提交执行保存插入数据connection.commit()# 按返回模版格式进行json结果返回return resp_data# [POST方法]根据项目ID进行信息更新
@app_product.route("/api/product/update",methods=['POST'])
def product_update():# 按返回模版格式进行json结果返回resp_data = {"code": 20000,"message": "success","data": []}# 获取请求传递jsonbody = request.get_data()body = json.loads(body)# 初始化数据库链接connection = connectDB()with connection:with connection.cursor() as cursor:# 查询需要过滤状态为有效的select = "SELECT * FROM `products` WHERE `keyCode`=%s AND `status`=0"cursor.execute(select, (body["keyCode"],))result = cursor.fetchall()# 有数据并且不等于本身则为重复,封装提示直接返回if len(result) > 0 and result[0]["id"] != body["id"]:resp_data["code"] = 20001resp_data["message"] = "唯一编码keyCode已存在"return resp_data# 如果没有重复,定义新的链接,进行更新操作with connection.cursor() as cursor:# 拼接更新语句,并用参数化%s构造防止基本的SQL注入# 条件为id,更新时间用数据库NOW()获取当前时间sql = "UPDATE `products` SET `keyCode`=%s, `title`=%s,`desc`=%s,`operator`=%s, `update`= NOW() WHERE id=%s"cursor.execute(sql, (body["keyCode"], body["title"], body["desc"], body["operator"], body['id']))# 提交执行保存更新数据connection.commit()return resp_data# [DELETE方法]根据id实际删除项目信息
@app_product.route("/api/product/delete", methods=['DELETE'])
def product_delete():# 返回的reponseresp_data = {"code": 20000,"message": "success","data": []}# 方式1:通过params 获取idID = request.args.get('id')# 做个参数必填校验if ID is None:resp_data["code"] = 20002resp_data["message"] = "请求id参数为空"return resp_data# 重新链接数据库connection = connectDB()with connection.cursor() as cursor:sql = "DELETE from `products` where id=%s"cursor.execute(sql, ID)connection.commit()return resp_data# [POST方法]根据id更新状态项目状态,做软删除
# @app_product.route("/api/product/remove", methods=['POST'])
# def product_remove():
# # 返回的reponse
# resp_data = {
# "code": 20000,
# "message": "success",
# "data": []
# }
# ID = request.args.get('id')
#
# # 做个参数必填校验
# if ID is None:
# resp_data["code"] = 20002
# resp_data["message"] = "请求id参数为空"
# return resp_data
#
# # 重新链接数据库
# connection = connectDB()
#
#
# with connection.cursor() as cursor:
# # 状态默认正常状态为0,删除状态为1
# # alter table products add status int default 0 not null comment '状态有效0,无效0' after `desc`;
# sql = "UPDATE `products` SET `status`=1 WHERE id=%s"
# cursor.execute(sql, ID)
# connection.commit()
#
# return resp_data@app_product.route("/api/product/remove", methods=['POST'])
def product_remove():# 统一使用JSON格式接收数据body = request.get_json()resp_data = {"code": 20000,"message": "success","data": None}# 参数校验if not body or 'id' not in body:resp_data.update({"code": 40001,"message": "缺少必要参数: id"})return jsonify(resp_data)try:connection = connectDB()# connection = pool.connection() # 使用连接池with connection:with connection.cursor() as cursor:# 1. 先检查记录是否存在cursor.execute("SELECT id FROM `products` WHERE id=%s AND status=0",(body['id'],))if not cursor.fetchone():resp_data.update({"code": 40401,"message": "指定记录不存在或已被删除"})return jsonify(resp_data)# 2. 执行逻辑删除cursor.execute("UPDATE `products` SET status=1, update_time=NOW() WHERE id=%s",(body['id'],))# 3. 返回被停用的IDresp_data['data'] = {"id": body['id']}return jsonify(resp_data)except Exception as e:# current_app.logger.error(f"停用失败: {str(e)}")resp_data.update({"code": 50001,"message": f"服务端错误: {str(e)}"})return jsonify(resp_data)
至此我们的产品管理功能页面所有功能已经开发完成了,看到这里的你不妨动手试试吧