自述
各位大佬好,我是一名工作了九年半的测试练习生,应该算是个高级点工 + 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 '工单表';
应用初始化过程
请求处理过程
程序入口
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 启动文件、安装 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/#/
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
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/