前言

自述
各位大佬好,我是一名工作了九年半的测试练习生,应该算是个高级点工 + py 自动化测试工程师。近期经历了 7.5 个月 Gap,上岸成功,下周入职新单位,开启我的第一份 995 工作,不知道我顶不顶得住 - -,
PS:给同在 Gap 期间的同学一些建议,保持与工作时差不多的作息,多运动,身体永远是第一位的。编写类似于日报的东西,包含每天要做的事和次日计划等,这样每天会过的相对充实一些,不会过度焦虑。
测试平台思考
我不是专业的测试开发工程师,所以给不出专业方面的建议,但我认为好的平台需要依托于现有业务,通过技术手段,去解决业务中遇到的问题,还需要考虑平台搭建的成本和回报率等等,框架编写前的思考大于框架的实现。这套造数平台框架应用于上一份工作,因为业务流程较长,上游依赖数据构造繁琐,为了提高造数效率,造数功能可供团队共享使用,出现了此平台。
分享目的
首先是为了对这些天的学习与平台搭建做一个总结,其次是给想写测试平台的开同学一些 demo 案例和学习思路。平台是上一份工作现有的,我通过学习重新搭建了此平台,后端学习和搭建用了 10 天,前端学习搭建加联调用了 13 天,感觉每天都过得很快,忘却待业焦虑 = =,

平台展示

功能比较简单,一个功能用于数据构造,另一个功能用于对数据进行查询、编辑和删除。平台使用思路是,造数据的表单页,可根据业务扩展新的页面,布局相同,只是表单项有所不同。查询编辑删除页,可用于维护一些动态的环境信息,或者构造数据的历史记录等。







技术栈与资料

后端
Python 语言,Flask Web 应用框架,部署: wsl(Windows Sub-system Linux)+ uwsgi + nginx
前端
Vue3 框架,组件库 Element-Plus,部署: wsl + nginx
数据库
Mysql
相关学习资料
Flask(学习框架使用与源码讲解):https://www.bilibili.com/video/BV1bC4y1a7FA
Vue 基础(Vue2):https://www.bilibili.com/video/BV1rH4y1K7o1
其实,习惯看文字的更推荐去 Vue 官网进行学习(Vue3)https://cn.vuejs.org/guide/introduction.html
前端项目(学习项目创建和布局思路):https://www.bilibili.com/video/BV1L24y1n7tB/

后端

环境准备

项目创建
我们使用 conda 虚拟环境
conda 安装:https://www.anaconda.com/download/success

切换 Python 版本和安装依赖
Pycharm windows 终端进入虚拟环境

activate api_backend_for_testhome


查看当前依赖包

conda list


切换 Python 版本

conda install python=3.10


安装依赖

# Flask模块
pip install flask
# 解决请求跨域
pip install flask_cors
# 用于请求session
pip install flask-session
# 数据库连接池
pip install DButils
# mysql数据库操作
pip install pymysql
# redis操作
pip install redis
# 发送请求模块
pip install requests
# json提取
pip install jsonpath
# 模拟数据
pip install faker
# jwt生成
pip install pyjwt
# yaml读取
pip install pyyaml

安装 wsl
Windows Sub-system Linux 即 Windows 下的 Linux 子系统
cmd 命令行

wsl --install

安装后重启电脑,会自动打开终端,输入用户和名密码,完成初始化

在终端通过 wsl 即可进入 linux 子系统

wsl 中安装和配置 mysql

# 安装mysql
sudo apt-get install mysql-server 
# 安装mysql安全脚本,设置安全选项 # 一直y即可
sudo mysql_secure_installation

# root连接数据库,提示输入密码,直接回车 # 进入mysql命令行
sudo mysql -u root -p
# 查看密码策略
SHOW VARIABLES LIKE 'validate_password%'; 
# 设置密码策略
set global validate_password.policy=LOW;
set global validate_password.length=4;
# 新建用户设置密码用于windows连接
CREATE USER 'xpcs'@'%' IDENTIFIED BY 'xpcs';
# 设置xpcs用户权限
 GRANT ALL PRIVILEGES ON *.* TO 'xpcs'@'%' WITH GRANT OPTION;
 # 刷新权限
 FLUSH PRIVILEGES;
# 查看mysql端口 # 3306
SHOW GLOBAL VARIABLES LIKE 'port';
 # 退出mysql命令行
 exit

 # xpcs 登录 mysql命令行
 mysql -u xpcs -p
 xpcs
 # 创建数据库
 create database api_backend_for_testhome;
# 退出mysql命令行
 exit

# 获取wls 服务器ip # 找到 eth0 对应地址 # 172.20.95.252
sudo apt install net-tools # 安装ifconfig 
ifconfig

#### 想让mysql外部windows访问,需注释掉这行 # bind-address = 127.0.0.1 ###
sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
# 重启mysql
service mysql restart

Pycharm 配置 Mysql 连接



Pycharm 执行建表

use api_backend_for_testhome;

create table flask_city (
  id        int         not null primary key auto_increment
  comment '自增主键',
  city_name varchar(20) not null
  comment '城市名称'
)
  ENGINE = InnoDB
  COMMENT '城市表';

insert into flask_city (city_name)
values ('烟台');
insert into flask_city (city_name)
values ('北京');

create table flask_area (
  id        int         not null primary key auto_increment
  comment '自增主键',
  area_name varchar(20) not null
  comment '地区名称',
  city_id   int         not null
  comment '所属城市id'
)
  ENGINE = InnoDB
  COMMENT '地区表';

insert into flask_area (area_name, city_id)
values ('牟平区', 1);
insert into flask_area (area_name, city_id)
values ('芝罘区', 1);
insert into flask_area (area_name, city_id)
values ('莱山区', 1);
insert into flask_area (area_name, city_id)
values ('福山区', 1);
insert into flask_area (area_name, city_id)
values ('蓬莱区', 1);
insert into flask_area (area_name, city_id)
values ('朝阳区', 2);
insert into flask_area (area_name, city_id)
values ('海淀区', 2);
insert into flask_area (area_name, city_id)
values ('东城区', 2);
insert into flask_area (area_name, city_id)
values ('西城区', 2);
insert into flask_area (area_name, city_id)
values ('丰台区', 2);
insert into flask_area (area_name, city_id)
values ('石景山区', 2);

create table flask_work_order (
  id            int primary key auto_increment comment '自增主键',
  work_order_id varchar(16) not null comment '工单编号',
  city_id       int         not null comment '城市id',
  city_name     varchar(10) not null comment '城市名称',
  area_id       int         not null comment '区域id',
  area_name     varchar(10) not null comment '区域名称',
  phone_number  varchar(11) not null comment '车主手机号',
  listen_time datetime not null default current_timestamp comment '上牌时间',
  order_type  int not null default 1 comment '工单类型',
  remark varchar(128) default '' comment '备注信息',
  created_at datetime DEFAULT current_timestamp , # 默认为当前时间
  updated_at datetime DEFAULT current_timestamp ON UPDATE current_timestamp # 更新字段自动更新
) engine InnoDB comment '工单表';

目录划分与调用过程

应用初始化过程

请求处理过程

后端代码 Demo

程序入口

app 实例
创建 app

加载配置

初始化日志

注册错误处理

初始化数据库连接池

自动查找并注册蓝图

操作数据表构造数据
view 层 - 请求接受

service 层 - 请求业务逻辑处理

model 层 - 请求操作入库

执行 sql 公共方法,构造数据

返回统一响应结果

调用开发接口构造数据
view 层 - 请求接受

request 层 - 调用开发接口

执行 request 公共方法,构造数据

def make_request_body(server_name, data_dict: dict):
    """
    :param server_name: 服务名, str类型, 在settings中配置的服务域名
    :param data_dict: 接口的请求参数字典,dict类型
    # 请求传参通过 params、data、json 区分使用 url 传参、from key-value传参、json传参
    :return: 返回 request body
    """
    # 获取配置中的服务器域名,拼接path
    url = current_app.config[server_name] + data_dict.get("path", "")
    method = data_dict.get("method", "get")
    headers = data_dict.get("headers", {})
    params = data_dict.get("params", {})
    data = data_dict.get("data", {})
    json = data_dict.get("json", {})

    request_body = {
        "url": url,
        "method": method,
        "headers": headers,
        "params": params,
        "data": data,
        "json": json
    }
    return request_body

def send_request(request_body, **kwargs):
    """
    :param request_body 请求数据
    :param kwargs: 扩展支持 files 上传文件、proxy 代理等
    :return:
    """
    url = request_body["url"]
    method = request_body["method"]
    headers = request_body["headers"]
    params = request_body["params"]
    data = request_body["data"]
    json = request_body["json"]

    if not url.startswith("http://") and not url.startswith("https://"):
        raise HttpError("请求url缺少协议名")
    if method.lower() not in ("get", "post", "put", "delete"):
        raise HttpError(f"暂不支持请求方法 - {method} - 可后续扩展")

    data_log = ""
    if params:
        data_log = f"params: {params}"
    if data:
        data_log = f"data: {data}"
    if json:
        data_log = f"json: {json}"
    if kwargs:
        data_log += f"\nkwargs: {kwargs}"

    current_app.logger.info("\n----------   request  info  ----------\n"
                            f"url: {url}\n"
                            f"method: {method}\n"
                            f"headers: {headers}\n"
                            f"{data_log}"
                            )

    try:
        response = requests.request(**request_body, timeout=30, **kwargs)
    except Exception as e:
        current_app.logger.warning(f"请求发生异常!!!")
        raise HttpError(e)

    if response.status_code == 200:
        current_app.logger.info("\n----------   response  info  ----------\n"
                                f"status: {response.status_code}\n"
                                f"headers: {response.headers}\n"
                                f"body: {response.text}")
    else:
        current_app.logger.warning(f"请求失败!!! 响应码不为200, 状态码为: {response.status_code}")
        current_app.logger.warning("\n----------   response  info  ----------\n"
                                   f"text: {response.text}\n"
                                   f"raw: {response.raw}")
        raise HttpError("响应码不为200")
    try:
        # 返回为字典类型
        return response.json()
    except requests.exceptions.JSONDecodeError:
        current_app.logger.warning("响应参数不为json,返回响应 response对象")
        return response

本地运行

windows 本地代码运行,对外暴露 http 协议,通过本机地址加端口访问


http://192.168.124.12:8090/api/partner/phone

部署(wsl+uwsgi+nginx)

需要将代码上传到 wsl 服务器、安装项目依赖包、配置 uwsgi 启动文件、安装 uwsgi 服务器、安装和配置 nginx ,完成部署,配置 host 进行访问。
上传代码
因为我们使用 wsl,所以省去上传代码步骤,直接从 Pycharm Windows 终端输入 wsl,就可进入到 wsl 中项目对应目录

安装依赖

# 依赖包安装
# Pycharm Windows终端执行
pip freeze > requirements.txt 导出依赖
# 进入wsl
wsl
pip install -r  requirements.txt 安装依赖

配置启动文件 uwsgi.ini

[uwsgi]
# 使用http协议,对外暴露端口8090 # 默认IP地址为服务器地址
# http = :8090
# 对外暴露socket与nginx交互
socket = :8090
# 项目地址
chdir = /mnt/d/2024/pythonCode/gitee/api_backend
# 启动模块名和APP
module = app:app
# 主进程
master = true
# 单进程单线程情况下,一个请求未处理完,另一个请求进来会阻塞,所以开启多进程多线程
# 进程数
processes = 2
# 线程数
threads = 2

安装 uwsgi

# pycharm 命令行输入 wsl 进入 linux
sudo apt-get update # 更新系统包
pip install uwsgi  # 安装uwsgi

安装 nginx

# 安装 nginx
sudo apt install nginx
 # 启动nginx
sudo systemctl start nginx
# 设置开机启动
sudo systemctl enable nginx

配置 nginx
nginx 请求转发:监听 80 端口,监听到请求后,根据请求头的 Location 匹配配置中的 server_name,匹配到服务后,再根据请求中的 path 匹配 location 定位/api/,uwsgi_pass,是将请求通过 socket 协议转发到 uwsgi 服务 8090 端口

cd /etc/nginx
sudo vi nginx.conf
#### 在http中添加如下server
http {
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

        server {
                listen 80;
                server_name "api.cn";
                location /api/ {
                        include uwsgi_params;
                        uwsgi_pass localhost:8090;
                        }
                }

}

配置好后,重启或者重新载入 nginx

# 停止nginx
sudo systemctl stop nginx
# 启动nginx
sudo systemctl start nginx
# 修改了配置文件,重新加载nginx
sudo systemctl reload nginx

启动服务

# 进入项目根目录,启动Flask服务
uwsgi --ini uwsgi.ini &



配置 host 访问后台
想要 nginx 匹配到 Flask 后台服务,需要请求域名是 api.cn,那就要配置 Windows 本地 host,映射 wsl 服务器地址 172.20.95.252

http://api.cn/api/partner/phone

前端

环境准备

Vue 项目创建

# vscode终端执行
npm create vue@latest
# 输入项目名
api-front-for-testhome
# 项目使用Vue-Router选择是,其余全部选择否

# vscode终端执行
cd api-front-for-testhome
# 安装依赖包
npm install
# 本地运行
npm run dev

http://localhost:5173/

删除自动生成内容
App.vue 保留如下

清空 assets 和 components 目录,删除 views 目录下的 HomeView 和 AboutView

创建布局和配置路由
views 下创建目录 layout 和 home,layout 下创建布局组件 Layout.vue

修改路由配置如下,使用 createWebHashHistory

安装和引入 Element-Plus 组件库

# vscode终端执行
npm install element-plus --save


安装 scss 和 axios

npm install sass --save
npm install --save axios

环境准备完毕
http://localhost:5173/#/

目录划分与调用过程


axios 封装

import axios from "axios"

// 创建请求实例
const instance = axios.create({
    // 生产使用
    // baseURL: "http://api.cn/api",

    // 本地调试使用
    baseURL: "http://192.168.124.12:8090/api",
    timeout: 5000
})

const request = {
    get(url, params) {
        // { params: params } 等价于 { params }
        return instance.get(url, { params })
    },
    // 默认为json请求头 "Content-Type": "application/json"
    postJson(url, data) {
        return instance.post(url, data);
    },
    postForm(url, data) {
        return instance.post(url, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
    },
    // url传参
    delete(url, params) {
        return instance.delete(url, { params })
    },
    // json传参
    deleteJson(url, data) {
        return instance.delete(url, { data });
    },
    put(url, data) {
        return instance.put(url, data);
    }
}

// 默认导出
export default request

前端页面 Demo





Order.vue

<template>
  <!-- 顶部卡片已经提取为公共组件通过descInfo传递描述信息 -->
  <header-desc :descInfo=descInfo></header-desc>

  <!-- 中间表单 -->
  <el-card style="height: 350px">
    <el-form ref="formRef" style="max-width: 600px" label-width="auto" label-position="right" size="small"
      :model=myForm>
      <!-- 城市区域下拉框 -->
      <el-form-item label="工单城市" prop="cityId" :rules="[{ required: true, message: '请选择城市', trigger: 'change' }]">
        <el-select v-model.number="myForm.cityId" filterable placeholder="请选择城市" style="width: 200px">
          <el-option v-for="item in myForm.cityList" :key="item.id" :label="item.city_name" :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="工单区域" prop="areaId" :rules="[{ required: true, message: '请选择区域', trigger: 'change' }]">
        <!-- v-model.number 修饰符会转换输入字符串为数字 -->
        <el-select v-model.number="myForm.areaId" filterable placeholder="请选择区域" style="width: 200px">
          <el-option v-for="item in myForm.areaList" :key="item.id" :label="item.area_name" :value="item.id" />
        </el-select>
      </el-form-item>
      <!-- 手机号数字输入框模拟生成按钮 -->
      <el-form-item label="车主手机号" prop="phone" :rules="[{ required: true, message: '请输入手机号', trigger: 'change' }]">
        <!-- clearable 输入数据可点击X清除  spellcheck取消语法检测否则输入内容会有红色下滑波浪线 -->
        <el-input style="width: 200px;" placeholder="请输入手机号" clearable v-model="myForm.phone" spellcheck="false" />
        <span style="padding-left: 15px; "><el-button type="primary" @click="clickPhoneHandler">模拟生成</el-button></span>
      </el-form-item>
      <!-- 日期时间选择框 -->
      <el-form-item label="上牌时间" prop="dateTime">
        <el-date-picker v-model="myForm.dateTime" style="width: 200px" type="datetime" placeholder="请选择日期和时间"
          value-format="YYYY-MM-DD HH:mm:ss" />
      </el-form-item>
      <!-- 工单类型单选按钮 -->
      <el-form-item label="工单类型" prop="orderType">
        <!-- 单选按钮value 需要是字符串所以不能使用 v-model.number修饰符否则无法点击 -->
        <el-radio-group v-model="myForm.orderType" class="ml-4">
          <el-radio value="1" size="small">优质工单</el-radio>
          <el-radio value="0" size="small">普通工单</el-radio>
        </el-radio-group>
      </el-form-item>
      <!-- 备注信息输入框 -->
      <el-form-item label="备注信息" prop="remark">
        <el-input style="width: 200px" :rows="2" type="textarea" placeholder="工单备注信息" clearable spellcheck="false"
          v-model="myForm.remark" />
      </el-form-item>

      <div style="height: 10px;">
        <!-- 占位符增加按钮与表单的距离 # 我们验证了子传父使用 emit + watch 但点击重置清空子组件内容未实现from内暂不做嵌套 -->
      </div>

      <!-- 提交重置按钮 -->
      <el-col :offset=1>
        <el-button type="primary" @click="submitForm(formRef)">提交</el-button>
        <el-button @click="resetForm(formRef)">重置</el-button>
      </el-col>
    </el-form>
  </el-card>

  <!-- 底部结果输出 通过myResponse传递结果-->
  <bottom-result :myResponse="myResponse"></bottom-result>
</template>

<script setup>
import { onMounted, reactive, ref, watch } from 'vue'
import HeaderDesc from '@/components/header/HeaderDesc.vue'
import BottomResult from '@/components/bottom/BottomResult.vue'
import request from '@/utils/request'
import { ElMessage } from 'element-plus'


// 顶部卡片,描述信息,传递给子组件
const descInfo = reactive({
  title: '构造工单',
  desc: '构造合伙人源线索工单,选择城市区域,输入手机号,选择上牌时间(可选)、工单类型,输入备注信息(可选)即可成单',
  person: 'xpcs',
  remark: '上牌时间不选择,默认为当前时间,备注不输入,默认为空'
})

// ref关联from
const formRef = ref()

// 中间卡片,from表单数据
const myForm = reactive({
  cityList: [],
  areaList: [],
  cityId: '',
  areaId: '',
  phone: '',
  dateTime: '',
  orderType: "1",
  remark: ''
})

// 提交表单,返回数据
const myResponse = reactive({
  code: '',
  msg: '',
  data: ''
})

// 获取城市列表
onMounted(async () => {
  await request.get('/partner/cities').then(res => {
    myForm.cityList = res.data.data
  }).catch(() => {
    ElMessage({
      type: 'error',
      message: '服务内部错误!',
    })
  })
})

// 模拟生成手机号事件
const clickPhoneHandler = async () => {
  await request.get('/partner/phone').then(res => {
    myForm.phone = res.data.data
  }).catch(() => {
    ElMessage({
      // 响应码大于300、请求超时异常、then内部异常走到此分支
      type: 'error',
      message: '服务内部错误!',
    })
  })
}

// from表单提交事件
const submitForm = (formEl) => {
  if (!formEl) return
  // 必填项校验
  formEl.validate(async (valid) => {
    if (valid) {
      // console.log('submit!')
      let data = {
        city_id: myForm.cityId,
        area_id: myForm.areaId,
        phone_number: myForm.phone,
        listen_time: myForm.dateTime,
        order_type: myForm.orderType,
        remark: myForm.remark
      }
      // console.log(data)
      await request.postJson('/partner/order', data).then(res => {
        // console.log(res.data)
        myResponse.code = res.data.code
        myResponse.msg = res.data.msg
        myResponse.data = res.data.data
      }).catch(() => {
        ElMessage({
          type: 'error',
          message: '服务内部错误!',
        })
      })
    }
  })
}

// form表单重置、必填项校验,需要form配置ref和model,表单项配置prop,rule
const resetForm = (formEl) => {
  if (!formEl) return
  formEl.resetFields()
}

// 侦听cityId发生变化,重新拉取区域列表
// reactive的属性需要使用 () => myFrom.cityId
watch(() => myForm.cityId, async (newValue) => {
  let data = { city_id: newValue }
  await request.get('/partner/areas', data).then(res => {
    // res.data 响应参数
    myForm.areaList = res.data.data
    myForm.areaId = ''
  }).catch(() => {
    ElMessage({
      type: 'error',
      message: '服务内部错误!',
    })
  })
})
</script>

View.vue

<template>
  <!-- 顶部查询栏 -->
  <el-card style="height: 60px">
    <el-form ref="formRef" :model="myForm" :inline="true">
      <el-form-item label="工单编号" size="small" prop="workOrderId">
        <el-input v-model="myForm.workOrderId" placeholder="请输入工单编号" style="width: 150px;" clearable
          spellcheck="false" />
      </el-form-item>
      <el-form-item label="车主手机号" size="small" prop="phoneNumber">
        <el-input v-model="myForm.phoneNumber" placeholder="请输入车主手机号" style="width: 150px;" clearable
          spellcheck="false" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit(formRef)" size="small">查询</el-button>
        <el-button @click="resetForm(formRef)" size="small">重置</el-button>
      </el-form-item>
    </el-form>
  </el-card>

  <!-- 中间数据展示表格 -->
  <el-card style="height: 550px">
    <el-table :data="myResponse.tableData" stripe height="500" style="width: 100%">
      <!-- <el-table-column prop="id" label="id" min-width="50" align="center" /> -->
      <!-- min-width 宽度为170px 当行内有多余的空位按比例增加 -->
      <el-table-column prop="work_order_id" label="工单编号" min-width="170" align="center" />
      <el-table-column prop="order_type" label="工单类型" width="90" align="center" />
      <el-table-column prop="city_name" label="城市" width="90" align="center" />
      <el-table-column prop="area_name" label="区域" width="90" align="center" />
      <el-table-column prop="phone_number" label="车主手机号" min-width="110" align="center" />
      <el-table-column prop="listen_time" label="上牌时间" min-width="165" align="center" />
      <!-- 超过字段长度的内容显示为 ...  hover展示全部内容 -->
      <el-table-column prop="remark" label="备注信息" min-width="165" show-overflow-tooltip align="center" />
      <el-table-column label="操作" align="center" min-width="170">
        <!-- 表格列插槽 -->
        <template #default="scope">
          <el-button size="small" type="primary" @click="handleEdit(scope.row)" text style="padding:0">
            编辑
          </el-button>
          <el-button size="small" type="danger" @click="handleDelete(scope.row)" text style="padding:0">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-card>
  <!-- 底部分页栏 -->
  <el-card style="height: 60px;">
    <el-pagination layout="total, prev, pager, next, jumper" :total=myResponse.count :small="true" background
      v-model:current-page="myForm.pageNum" @current-change="handleCurrentChange" />
  </el-card>
  <!-- 编辑弹框摆放任意位置即可会居中展示 -->
  <el-dialog v-model="dialogFormVisible" title="编辑工单" width=30% draggable>
    <el-form :model="dialogFrom" size="small" label-position="right" label-width="auto">
      <el-form-item label="工单编号">
        <!-- disabled 不可编辑 -->
        <el-input v-model="dialogFrom.workOrderId" disabled style="width: 160px" />
      </el-form-item>
      <el-form-item label="城市">
        <el-input v-model="dialogFrom.cityName" disabled style="width: 160px" />
      </el-form-item>
      <el-form-item label="区域">
        <el-input v-model="dialogFrom.areaName" disabled style="width: 160px" />
      </el-form-item>
      <el-form-item label="工单类型">
        <el-select v-model="dialogFrom.orderType" style="width: 160px;" disabled>
          <el-option label="优质工单" value=1 />
          <el-option label="普通工单" value=0 />
        </el-select>
      </el-form-item>
      <el-form-item label="车主手机号" prop="phoneNumber"
        :rules="[{ required: true, message: '请输入手机号', trigger: 'change' }]">
        <el-input v-model="dialogFrom.phoneNumber" style="width: 160px" />
      </el-form-item>
      <el-form-item label="备注信息">
        <el-input style="width: 200px" :rows="3" type="textarea" placeholder="工单备注信息" clearable spellcheck="false"
          v-model="dialogFrom.remark" />
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取消</el-button>
        <el-button type="primary" @click="dialogFromSubmit" :disabled="dialogSubmitDisable">提交</el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup>
import { onMounted, reactive, ref, watch } from 'vue'
import request from '@/utils/request'
// 引入消息提示、消息弹出框、消息通知
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
// 引入删除消息弹框中使用的垃圾箱图标
import { markRaw } from 'vue'
import { Delete } from '@element-plus/icons-vue'


// ref 关联表单
const formRef = ref()

// 查询提交表单数据
const myForm = reactive({
  pageNum: '',
  pageSize: 10,
  phoneNumber: '',
  workOrderId: ''
})

// 查询返回数据
const myResponse = reactive({
  count: 0,
  tableData: []
})
// 工单类型映射字典
const orderTypeObj = reactive({ 0: "普通工单", 1: "优质工单" })

// 对话展示标志
const dialogFormVisible = ref(false)

// 对话框内表单数据
const dialogFrom = reactive({
  workOrderId: '',
  cityName: '',
  areaName: '',
  orderType: '',
  phoneNumber: '',
  remark: '',
  originPhoneNumber: '',
  originRemark: ''
})
// 对话框内提交不可用标识
const dialogSubmitDisable = ref(true)

// 进入页面默认触发查询
onMounted(async () => {
  // async和await组合使用,发送请求后返回promise对象,res.data为响应参数
  await request.get("/partner/order").then((res) => {
    myResponse.count = res.data.count
    let data = res.data.data
    data.forEach(ele => {
      // 工单类型,数字转换文字
      ele['order_type'] = orderTypeObj[ele['order_type']]
      myResponse.tableData.push(ele)
    })
  }).catch(() => {
    ElNotification({
      title: '失败',
      message: '服务内部错误!',
      type: 'error',
    })
  })
})

// 点击分页触发查询
const handleCurrentChange = async () => {
  myResponse.tableData = []
  let data = {
    page_num: myForm.pageNum,
    page_size: myForm.pageSize,
    work_order_id: myForm.workOrderId,
    phone_number: myForm.phoneNumber
  }
  await request.get("/partner/order", data).then((res) => {
    myResponse.count = res.data.count
    data = res.data.data
    data.forEach(ele => {
      ele['order_type'] = orderTypeObj[ele['order_type']]
      myResponse.tableData.push(ele)
    })
  }).catch(() => {
    ElNotification({
      title: '失败',
      message: '服务内部错误!',
      type: 'error',
    })
  })
}

// 查询提交
const onSubmit = (formEl) => {
  if (!formEl) return
  // 必填项校验
  formEl.validate(async (valid) => {
    if (valid) {
      // 清空表格,分页定到首页
      myResponse.tableData = []
      myForm.pageNum = 1
      let data = {
        page_num: myForm.pageNum,
        page_size: myForm.pageSize,
        work_order_id: myForm.workOrderId,
        phone_number: myForm.phoneNumber
      }
      await request.get("/partner/order", data).then((res) => {
        myResponse.count = res.data.count
        let response = res.data.data
        response.forEach(ele => {
          ele['order_type'] = orderTypeObj[ele['order_type']]
          myResponse.tableData.push(ele)
        })
      }).catch(() => {
        ElNotification({
          title: '失败',
          message: '服务内部错误!',
          type: 'error',
        })
      })
    }
  })
}

// 查询重置
const resetForm = (formEl) => {
  if (!formEl) return
  formEl.resetFields()
}


// 删除工单
const handleDelete = (row) => {
  // 消息弹框
  ElMessageBox.confirm(
    '确认删除此工单  【' + row.work_order_id + '',
    '提示',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      icon: markRaw(Delete),
      draggable: true
    }
  )
    // 确认提交操作
    .then(async () => {
      // 调用删除接口
      await request.delete('/partner/order', { id: row.id })
        // 接口响应成功,响应码200
        .then(res => {
          // 进一步判断业务,code 0 成功 code -1 失败
          if (res.data.code == 0) {
            ElNotification({
              title: '成功',
              message: '工单已删除',
              type: 'success',
            })
            // 重新发起查询,调用分页方法最合适,不会定位到首页
            handleCurrentChange()
          } else {
            ElNotification({
              title: '警告',
              // 工单不存在
              message: res.data.msg,
              type: 'warning'
            })
          }
          // 接口响应失败,响应码大于300,如服务挂了 nginx 502 // 接口超时 // then分支内部报错
        }).catch(() => {
          ElNotification({
            title: '失败',
            message: '服务内部错误!',
            type: 'error'
          })
        })
    })
    // 取消操作
    .catch(() => {
      // 取消操作不做任何提示
    })
}

// 编辑工单弹框
const handleEdit = (row) => {
  dialogFormVisible.value = true
  dialogFrom.id = row.id
  dialogFrom.workOrderId = row.work_order_id
  dialogFrom.orderType = row.order_type
  dialogFrom.cityName = row.city_name
  dialogFrom.areaName = row.area_name
  dialogFrom.phoneNumber = row.phone_number
  dialogFrom.remark = row.remark
  dialogFrom.originPhoneNumber = row.phone_number,
    dialogFrom.originRemark = row.remark
}

// 编辑工单提交
const dialogFromSubmit = async () => {
  // 发送请求编辑请求
  let data = {
    id: dialogFrom.id,
    phone_number: dialogFrom.phoneNumber,
    remark: dialogFrom.remark
  }
  await request.put('/partner/order', data)
    // 接口响应成功,响应码200
    .then((res) => {
      // 进一步判断业务
      if (res.data.code == 0) {
        ElNotification({
          title: '成功',
          message: '工单编辑成功',
          type: 'success'
        })
        // 重新发起查询,调用分页方法最合适,不会定位到首页
        handleCurrentChange()
        // 将提交按钮至为不可用
        dialogSubmitDisable.value = true
        // 关闭弹框
        dialogFormVisible.value = false
      } else {
        ElNotification({
          title: '警告',
          // 工单不存在
          message: res.data.msg,
          type: 'warning'
        })
      }
      // 接口响应失败,响应码大于300,如服务挂了 nginx 502 // 接口超时 // then分支内部报错
    }).catch(() => {
      ElNotification({
        title: '失败',
        message: '服务内部错误!',
        type: 'error',
      })
    })
}

// 监听编辑对话框,手机号和备注是否发生变化,如果发生变化则可提交,否则不可提交
watch(() => dialogFrom.phoneNumber, (newValue) => {
  if (newValue == dialogFrom.originPhoneNumber && dialogFrom.remark == dialogFrom.originRemark) {
    dialogSubmitDisable.value = true
  } else {
    dialogSubmitDisable.value = false
  }
})
watch(() => dialogFrom.remark, (newValue) => {
  if (newValue == dialogFrom.originRemark && dialogFrom.phoneNumber == dialogFrom.originPhoneNumber) {
    dialogSubmitDisable.value = true
  } else {
    dialogSubmitDisable.value = false
  }
})
</script>

打包部署

打包

# vscode终端执行
npm run build
# 生成dist打包目录

nginx 部署

# vscode终端执行
# 进入wsl
wsl 
# 修改nginx配置文件
cd /etc/nginx
sudo vi nginx.conf

include mime.types;
types
 {
  application/javascipt mjs;
}

# vue项目打包dist目录
root /mnt/d/2024/VueCode/api-front/dist;
index index.html;

# 重新加载nginx配置
sudo systemctl reload nginx

访问系统
http://api.cn/


↙↙↙阅读原文可查看相关链接,并与作者交流