概述
在前面几篇文章中,我们实现了添加功能,编辑功能,删除功能,搜索功能,那么接下来我们是不是因该要实现一个分页功能,毕竟如果数据多了,是不是有个翻页的功能比较好,这里分页功能我就暂时不开发,不过我提供后端接口,你们试试,自己可不可以独立写完这个分页的前端页面,并且可以使用,这里我们重点事重构一下产品管理这块的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-input
v-model="search.title"
placeholder="支持模糊查询"
class="search-input"
clearable
/>
</el-form-item>
<el-form-item label="关键词">
<el-input
v-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-button
v-if="dialogProductStatus === 'ADD'"
type="primary"
@click="submitForm('productForm', pCreate)"
:loading="loading">添 加</el-button>
<el-button
v-if="dialogProductStatus === 'UPDATE'"
type="primary"
@click="submitForm('productForm', pUpdate)"
:loading="loading">修 改</el-button>
</span>
</el-dialog>
<!-- 表格区域 -->
<el-table
:data="tableData"
v-loading="tableLoading"
border
class="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-button
size="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-button
size="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 = true
apiProductList().then(response => {
this.tableData = response.data
}).finally(() => {
this.tableLoading = false
})
},
searchProduct() {
this.tableLoading = true
apiProductSearch(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 = true
this.$nextTick(() => {
this.$refs.productForm && this.$refs.productForm.clearValidate()
})
},
submitForm(formName, callback) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
callback()
} else {
return false
}
})
},
pCreate() {
apiProductCreate(this.product).then(response => {
this.$notify({
title: '成功',
message: '项目或产品添加成功',
type: 'success',
duration: 2000
})
this.dialogProductShow = false
this.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 = false
this.getProductList()
}).finally(() => {
this.loading = false
})
},
pHardRemove(id) {
// 移除确认按钮的加载状态,仅保留表格加载状态
this.$confirm('此操作将永久删除该项目, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 只显示表格加载状态,不控制按钮状态
this.tableLoading = true
apiProductDelete(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 = true
apiProductRemove(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 PooledDB
app_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和keyCode
title = 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和keyCode
title = 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 body
body = 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"] = 20001
resp_data["message"] = "唯一编码keyCode已存在"
return resp_data
with 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": []
}
# 获取请求传递json
body = 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"] = 20001
resp_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():
# 返回的reponse
resp_data = {
"code": 20000,
"message": "success",
"data": []
}
# 方式1:通过params 获取id
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:
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. 返回被停用的ID
resp_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)
至此我们的产品管理功能页面所有功能已经开发完成了,看到这里的你不妨动手试试吧