【Flask】测试平台开发,产品管理功能UI重构-第九篇

发布于:2025-09-01 ⋅ 阅读:(30) ⋅ 点赞:(0)

 概述

在前面几篇文章中,我们实现了添加功能,编辑功能,删除功能,搜索功能,那么接下来我们是不是因该要实现一个分页功能,毕竟如果数据多了,是不是有个翻页的功能比较好,这里分页功能我就暂时不开发,不过我提供后端接口,你们试试,自己可不可以独立写完这个分页的前端页面,并且可以使用,这里我们重点事重构一下产品管理这块的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)

至此我们的产品管理功能页面所有功能已经开发完成了,看到这里的你不妨动手试试吧

 


网站公告

今日签到

点亮在社区的每一天
去签到