后端支持
运行这个文件,就启动了一个后端服务,提供了一些练手的本地接口。
from flask import Flask, request, jsonify
from flask_cors import CORS
app = Flask(__name__)
# 测试数据
user_info = {"username": 'ltx', 'password': '123456'}
project_data = {"code": "1",
"data": [{"title": "天地房产", "id": "1001"},
{"title": "智慧门户", "id": "1002"},
{"title": "京东生鲜", "id": "1003"}
],
"msg": "三个项目",
}
# 接口数据
interface_data = {
"1001": {"code": "1",
"data": [{"name": "天地房产登录1001"},
{"name": "天地房产注册1001"}],
"msg": "2个接口", },
"1002": {"code": "1",
"data": [{"name": "智慧门户-登录1002"},
{"name": "智慧门户-注册1002"},
{"name": "智慧门户-贷款1004"}, ],
"msg": "3个接口", },
"1003": {"code": "1",
"data": [{"name": "京东生鲜-登录1003"},
{"name": "京东生鲜-注册1003"},
{"name": "京东生鲜下单1003"}, ],
"msg": "3个接口", },
}
# 登录
@app.route('/api/user/login', methods=['post'])
def login():
"""
接口地址:http://127.0.0.1:5000/api/user/login
请求方法:post
"""
data = request.form or request.json
# 判断账号,密码是否正确
if user_info.get('username') == data.get('username') and user_info.get('password') == data.get('password'):
return jsonify({'code': "1", "data": None, "msg": "成功"})
else:
return jsonify({'code': "0", "data": None, "msg": "密码有误"})
# 获取项目列表
@app.route('/api/projects', methods=['get'])
def pro_list():
"""
接口地址:http://127.0.0.1:5000/api/projects
请求方法:get
参数:无
返回所有的项目
:return:
"""
return jsonify(project_data)
# 获取接口列表
@app.route('/api/interface', methods=['get'])
def interface():
"""
接口地址:http://127.0.0.1:5000/api/interface
请求方法:get
参数: id(项目的id)
参数类型:查询字符串
返回:该项目的所有接口
"""
inter_id = request.args.get('id')
if inter_id:
res_data = interface_data.get(inter_id)
if res_data:
return jsonify(res_data)
else:
return jsonify({"code": "0", "data": None, "msg": "没有该项目"})
else:
return jsonify({"code": "0", "data": None, "msg": "请求参数不能为空"})
if __name__ == '__main__':
cors = CORS(app)
app.run(debug=True)
创建组件(页面)
componets文件夹下:
1.创建登录页面:Login.vue
2.生成一个简单模版vue文件
3.创建Home.vue(首页)、Cases.vue(用例页)、Projects.vue(项目页)、Interface(接口页)
4.在每个页面都下些对应的内容来看显示是否正常,示例:
5.此时访问/login,没有显示内容,需要在App.vue中添加视图占位符
<router-view></router-view>
此时访问:
配置路由规则
router文件夹下,index.js中配置
在我们的设计中,Cases.vue(用例页)、Projects.vue(项目页)、Interface(接口页)都是在Home.vue(首页)中展示的,用子路由实现。
此时访问/cases、/projects、/interface,不会显示对应的内容,需要在Home.vue中添加视图占位符。
<router-view></router-view>
初步练手
登录页
1.在elementUI上找到相似的代码copy过来进行修改,登录页需要用户名、密码输入框、提交按钮,是一个表单形式。
点击显示代码,把html的代码copy到Login.vue的<template></template>
标签中,把代码中的<script>
copy到Login.vue的<script></script>
中
Login.vue
<template>
<div class="login_box" style="width: 600px;height: 400px;margin:50px auto;text-align: center;">
<el-card class="login_card" >
<el-form ref="form" :model="formLogin" label-width="80px">
<el-form-item label="账号">
<el-input v-model="formLogin.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="formLogin.password" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loginHandle">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
// 组件中的变量和方法要使用export default暴露出去
export default {
data() {
return {
formLogin: {
username: '',
password: ''
}
}
},
methods: {
loginHandle:
async function(){
const response = await request.post("/api/user/login",this.formLogin)
console.log(response.status)
console.log(response.data)
if (response.data.code == "1"){
alert("登录成功")
// token存到sessionStorage
window.sessionStorage.setItem('token',response.data.token)
// 跳转到home页
this.$router.push('/home')
}
else{
alert("登录失败,账号或密码错误")
}
}
}
}
</script>
<style>
</style>
首页
1.在elementUI上找到相似的代码copy过来进行修改,home页有导航栏对应子路由的测试用例页、项目页、接口页
Home.vue
<template>
<div>
<el-menu :router='rou' default-active="activeIndex" class="el-menu-demo" active-text-color="#008B8B" mode="horizontal" @select="handleSelect">
<el-menu-item index="/cases">用例列表</el-menu-item>
<el-menu-item index="/interface">接口列表</el-menu-item>
<el-menu-item index="/projects">项目列表</el-menu-item>
</el-menu>
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {
rou:true,//:router=true 开启路由
activeIndex: "/cases"
};
},
methods: {
handleSelect(key, keyPath) {
console.log(key, keyPath);
}
}
}
</script>
<style>
</style>
一些解释:
:router=true 开启路由
default-active="activeIndex" 当前激活菜单的 index
active-text-color="#008B8B" 激活时的文本颜色
mode="horizontal" 水平模式
路由导航守卫-访问权限
在index.js下添加
// 添加导航守卫
router.beforeEach((to,from,next)=>{
// 判断是否要去登录页面,如果是,可以直接访问
if (to.path=='/login'){
return next()
}
// 判断是否有token,如有,可以去next访问的页面
else if (window.sessionStorage.getItem("token")){
return next()
// 其他情况,返回登录页
}else {
return next('/login')
}
})
现在访问http://localhost:8081/#/home
就会跳转到/login
,因为没有登录,这就是导航守卫的作用。
调用登录接口,实现登录功能
此时通过页面登录,控制台会报错request找不到。
这个request是指Login.vue中的request,之前是在<script></script>
中单个文件中定义request。现在作为一个项目来说需要放在一个文件中定义,供各处使用。
一般,在src下新建api文件夹,放与接口调用相关的代码。
src/api/index.js:
import axios from 'axios'
//使用axiso创建请求对象
const request = axios.create({
validateStatus: function (status) {
return true;
},
baseURL:"http://127.0.0.1:5000",
})
// 添加请求拦截器,每次请求时都会自动调用
request.interceptors.request.use(function(config){
// 发送之前判断是否有token
if (window.sessionStorage.getItem("token")){
config.headers.Authorization='JWT'+window.sessionStorage.getItem("token")
}
console.log("请求头",config.headers)
return config;
})
// 将request暴露出去
export default request
在main.js下导入request,并与vue原型绑定。
//导入创建的请求对象 - request
import request from '../src/api/index.js'
// 将请求对象绑定到vue的原型上
Vue.prototype.$request = request
优化Login.vue的request变量:
现在就能通过你写在服务端的正确的用户名和密码进行登录了。登录状态就能去任何路径,token掉了之后,访问其他页面都只能去到login页。
登录成功或失败的提示是原生弹窗的,我们可以修改为elementUI的。
if (response.data.code == "1"){
this.$message({
message: '登录成功',
type: 'success'
});
// token存到sessionStorage
window.sessionStorage.setItem('token',response.data.token)
// 跳转到home页
this.$router.push('/home')
}
else{
this.$message({
message: '登录失败,账号或密码错误',
type: 'warning'
});
}
启动项目:npm run sever
,同vue ui中启动app
实战项目
1. 项目初始化
这个上一篇文章讲过,搬运过来。
初始化的项目页面如下,我们需要清掉它初始化的内容:
1.App.vue页面:
2.不使用views目录结构,删掉
3.删掉views对应的路由规则删掉
4.删掉自带的组件helloworld,componets文件夹下
这样就得到了一个干净的项目了。
2. 登录与退出
上文练手部分也到过,现在作为一个项目级别的做一些优化。
2.1 实现输入账号、密码登录功能
-
优化标题样式
样式放在
<style>
标签中 -
去掉文字"账号"、"密码",使用icon表示
去掉
<el-form-item label="账号">
中label添加icon,使用elementUI中的带icon的输入框方法
给输入框内添加默认值placeholder
2.2 实现表单输入校验
-
在el-form中绑定rules属性,指定校验规则对象
<el-form ref="form" :model="formLogin" :rules="loginRules" label-width="10px">
-
在data中绑定校验规则
loginRules:{//前端验证规则 username:[ // required 是否必填 ,message 错误提示 ,trigger 触发时机 { required: true, message: '账号不能为空', trigger: 'blur' }, { min: 1, max: 12, message: '长度在 1 到 12 个字符', trigger: 'blur' } ], password:[ { required: true, message: '密码不能为空', trigger: 'blur' }, ] }
-
在el-form-item中使用prop指定校验字段
<el-form-item prop='username'> <el-form-item prop='password'>
2.3 提交表单时预验证
输入框有校验,但是点击登录时还是会发送请求。
-
el-form标签通过ref属性,设置表单引用对象
-
在点击登录的处理函数中,通过
this.$refs.表单引用对象
获取表单引用对象,获取到引用对象使用validate方法进行校验methods: { loginHandle: function(){ // 验证表单,验证通过再发送请求 this.$refs.loginRef.validate(async (valid)=>{ console.log("表单验证的结果",valid) // 如果为false 就执行return // 如果为true 就继续执行下面的代码 if (!valid) { return } const response = await this.$request.post("/api/user/login",this.formLogin) console.log(response.status) console.log("data",response.data) if (response.data.code == "1"){ this.$message({ message: '登录成功', type: 'success' }); // token存到sessionStorage window.sessionStorage.setItem('token',response.data.token) // 跳转到home页 this.$router.push('/home') } else{ this.$message({ message: '登录失败,账号或密码错误', type: 'warning' }); } }) } }
2.4 实现记住账号功能
Sessio Storage 下次打开页面会被清空。Local Storage下次打开页面不会被清空。所以,账号存在Local Storage。
-
在表单中添加一个记录的开关
<el-form-item> <el-switch v-model="formLogin.status" active-text="记住账号"> </el-switch> </el-form-item>
formLogin: { username: '', password: '', status: false // 默认不记住账号 },
-
在登录前判断是否设置了记住账号,如果设置了将账号存在Local Storage中,没有设置则清空Local Storage中的账号
// 账号username的保存、删除 if (this.formLogin.status){ // 勾选保存到localstorage中 window.localStorage.setItem("username",this.formLogin.username) }else{ // 未勾选删除localstorage中的账号 window.localStorage.removeItem("username") }
-
在组件的生命函数中,获取Local Storage的账号,保存在data中
// 组件中的数据挂载到模版中之后,会触发这个生命周期钩子函数 mounted(){ // 获取localStorage中的账号 const username = window.localStorage.getItem("username") if (username){ this.formLogin.username=username this.formLogin.status=true } }
2.5 代码完整示例:Login.vue
<template>
<div class="login_box">
<el-card class="box_card" >
<div class="title">
自 动 化 测 试 平 台
</div>
<el-form :model="formLogin" :rules="loginRules" ref="loginRef" label-width="10px" >
<el-form-item prop='username'>
<el-input v-model="formLogin.username" prefix-icon="el-icon-user" placeholder="请输入账号"></el-input>
</el-form-item>
<el-form-item prop='password'>
<el-input v-model="formLogin.password" type="password" prefix-icon="el-icon-lock" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-switch v-model="formLogin.status" active-text="记住账号">
</el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loginHandle">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
// 组件中的变量和方法要使用export default暴露出去
export default {
data() {
return {
formLogin: {
username: '',
password: '',
status: false // 默认不记住账号
},
loginRules:{//前端验证规则
username:[
// required 是否必填 ,message 错误提示 ,trigger 触发时机
{ required: true, message: '账号不能为空', trigger: 'blur' },
{ min: 1, max: 12, message: '长度在 1 到 12 个字符', trigger: 'blur' }
],
password:[
{ required: true, message: '密码不能为空', trigger: 'blur' },
]
}
}
},
methods: {
loginHandle:
function(){
// 验证表单,验证通过再发送请求
this.$refs.loginRef.validate(async (valid)=>{
console.log("表单验证的结果",valid)
// 如果为false 就执行return
// 如果为true 就继续执行下面的代码
if (!valid) {
return
}
// 账号username的保存、删除
if (this.formLogin.status){
// 勾选保存到localstorage中
window.localStorage.setItem("username",this.formLogin.username)
}else{
// 未勾选删除localstorage中的账号
window.localStorage.removeItem("username")
}
const response = await this.$request.post("/api/user/login",this.formLogin)
console.log(response.status)
console.log("data",response.data)
if (response.data.code == "1"){
this.$message({
message: '登录成功',
type: 'success'
});
// token存到sessionStorage
window.sessionStorage.setItem('token',response.data.token)
// 跳转到home页
this.$router.push('/home')
}
else{
this.$message({
message: '登录失败,账号或密码错误',
type: 'warning'
});
}
})
}
},
// 组件中的数据挂载到模版中之后,会触发这个生命周期钩子函数
mounted(){
// 获取localStorage中的账号
const username = window.localStorage.getItem("username")
if (username){
this.formLogin.username=username
this.formLogin.status=true
}
}
}
</script>
<style scoped>
/* style中添加scoped属性,表示css样式只对该组件生效 */
.login_box {
width: 600px;
height: 400px;
margin: 200px auto;
text-align: center;
}
.title {
color: #409eff;
font: bold 28px "微软雅黑";
width: 100%;
text-align: center;
margin-bottom: 30px;
}
</style>
3. 主页菜单
3.1 主页布局
主页菜单使用这个布局:
-
把之前的导航栏 和视图占位符 分别放在侧边栏和主体内容中。
-
增加退出登录
-
优化样式
<template> <el-container> <!-- 顶部栏 --> <el-header> <div class="title"> 自 动 化 测 试 平 台 </div> <div class="logout"> 退出登录 </div> </el-header> <el-container> <!-- 侧边栏 --> <el-aside width="200px"> <el-menu :router='rou' default-active="activeIndex" class="el-menu-demo" active-text-color="#409eff" mode="vertical" @select="handleSelect"> <el-menu-item index="/cases">用例列表</el-menu-item> <el-menu-item index="/interface">接口列表</el-menu-item> <el-menu-item index="/projects">项目列表</el-menu-item> </el-menu> </el-aside> <!-- 主体内容 --> <el-main> <router-view></router-view> </el-main> </el-container> </el-container> </template>
<style> .title { color: #409eff; font: bold 28px/60px "微软雅黑"; width: 90%; text-align: center; float: left; } .logout { width: 60px; font:14px/60px "微软雅黑"; float: right; text-align: center; } .logout:hover{ background: #409eff; } </style>
3.2 实现退出登录功能
退出登录就是,清空SessioStorage中的token,然后重定向到/login
页
-
退出登录需要一个二次确认弹框
<el-header>
<div class="title">
自 动 化 测 试 平 台
</div>
<el-popconfirm title="是否确认退出?" @confirm="logout()">
<div class="logout" slot="reference">退出登录</div>
</el-popconfirm>
</el-header>
- 退出登录的方法
methods: {
// 点击退出登录按钮的处理函数,删掉token、将路由重定向到/login
logout(){
window.sessionStorage.removeItem("token")
this.$router.push("/login")
}
}
3.3 实现层级菜单
3.4 代码完整示例:Home.vue
<template>
<el-container>
<!-- 顶部栏 -->
<el-header>
<div class="title">
自 动 化 测 试 平 台
</div>
<el-popconfirm title="是否确认退出?" @confirm="logout">
<div class="logout" slot="reference">退出登录</div>
</el-popconfirm>
</el-header>
<!-- 分割线 -->
<el-divider></el-divider>
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">
<el-menu :router='rou' default-active="activeIndex" class="el-menu-demo" active-text-color="#409eff" mode="vertical" unique-opened="true">
<!-- 项目管理菜单 -->
<el-submenu index="1">
<template slot="title">
<i class="el-icon-menu"></i>
<span>项目管理</span>
</template>
<el-menu-item index="/projects">项目列表</el-menu-item>
</el-submenu>
<!-- 接口管理菜单 -->
<el-submenu index="2">
<template slot="title">
<i class="el-icon-menu"></i>
<span>接口管理</span>
</template>
<el-menu-item index="/interface">接口列表</el-menu-item>
</el-submenu>
<!-- 用例管理菜单 -->
<el-submenu index="3">
<template slot="title">
<i class="el-icon-menu"></i>
<span>用例管理</span>
</template>
<el-menu-item index="/cases">用例列表</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!-- 主体内容 -->
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
data() {
return {
rou:true,//:router=true 开启路由
activeIndex: "/cases"
};
},
methods: {
// 点击退出登录按钮的处理函数,删掉token、将路由重定向到/login
logout(){
window.sessionStorage.removeItem("token")
this.$router.push("/login")
}
}
}
</script>
<style scoped>
/* 页面header的样式 */
.title {
color: #409eff;
font: bold 28px/60px "微软雅黑";
width: 90%;
text-align: center;
float: left;
}
.logout {
width: 60px;
font:14px/60px "微软雅黑";
float: right;
text-align: center;
}
.logout:hover{
text-color: #409eff;
}
.el-menu {
height: 900px;
}
</style>
4. 项目管理
4.1 项目列表
-
修复一些bug
- 路由
'/'
未配置,访问时没内容 - 访问
/home
时应有个默认页
3. 默认页时侧边栏选中的菜单错误
- 路由
-
面包屑实现
<el-breadcrumb separator-class="el-icon-arrow-right"> <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item> <el-breadcrumb-item>项目管理</el-breadcrumb-item> <el-breadcrumb-item>项目列表</el-breadcrumb-item> </el-breadcrumb>
-
项目列表展示
由于简单的服务已经无法支持我们练手了。提供一个开源的本地部署接口项目。
本地部署教程: www.jianshu.com/p/353173aa2…
可以把前面部分涉及的接口配置等都修改为这个项目。
这部分使用查看用户列表的接口来实现 :
列表展示可以由斑马纹表格+有操作按钮的表格组成
<!-- 数据列表 --> <el-table :data="projectList" style="width: 100%;margin-bottom:20px ;" > <el-table-column type="index" width="50" label="序号"> </el-table-column> <el-table-column prop="id" label="用户ID" width="200"> </el-table-column> <el-table-column prop="role_name" label="角色" width="200"> </el-table-column> <el-table-column prop="username" label="用户名" width="200"> </el-table-column> <el-table-column prop="create_time" label="创建时间" width="200" sortable> </el-table-column> <el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button> </template> </el-table-column> </el-table>
在页面加载时就要返回数据
async mounted(){ // 请求用户列表接口 const response = await this.$request.get("api/private/v1/users",{ params:{ pagenum:this.currentPage, pagesize:this.size } }) if (response.data.meta.status!==200){ return this.$message({message: '服务异常',type: 'warning' }); } console.log("data",response.data) this.projectList = response.data.data.users this.total = response.data.data.total this.currentPage = response.data.data.pagenum }
-
分页功能实现
选择组件:
<div class="block"> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="size" layout="total, sizes, prev, pager, next, jumper" :total=total> </el-pagination> </div>
handleSizeChange 是切换页面size的方法,handleCurrentChange是切换页码时的方法,current-page是当前页码,
:page-sizes="[5, 10, 20, 50]"
是页面size选项,可以选择5条/页,10条/页,20条/页,50条/页,page-size是页面size由于切换页面、切换页码时,都需要请求
api/private/v1/users
接口,所以我们将这个接口封装成一个方法。<script> export default { data() { return { projectList: [''], total:0, currentPage:1, size:10 } }, methods:{ // 封装获取用户列表的接口,因为多处会使用到 async getUsers(){ // 请求项目列表接口 const response = await this.$request.get("api/private/v1/users",{ params:{ pagenum:this.currentPage, pagesize:this.size } }) if (response.data.meta.status!==200){ return this.$message({message: '服务异常',type: 'warning' }); } console.log("data",response.data) this.projectList = response.data.data.users this.total = response.data.data.total this.currentPage = response.data.data.pagenum }, handleSizeChange(size){ // 给size赋值为当前值 this.size =size this.getUsers() }, handleCurrentChange(currentPage){ this.currentPage =currentPage this.getUsers() }, handleDelete(){}, handleEdit(){} }, // 组件中的数据挂载到模版中之后,会触发这个生命周期钩子函数 async mounted(){ this.getUsers() } } </script>
-
删除功能
删除需要一个二次确认弹窗,和Home.vue的退出登录功能一样,使用气泡确认框。
handleDelete(scope.row.id)
删除方法scope.row
表示点击的这行,scope.row.id
表示点击的这行的id。<el-popconfirm @confirm="handleDelete(scope.row.id)" title="确定删除吗?"> <el-button size="mini" type="danger" slot="reference">删除</el-button> </el-popconfirm>
方法:
// 处理删除按钮 async handleDelete(id){ console.log("当前删除的数据id为:",id) // 请求删除用户的接口 const response = await this.$request.delete("api/private/v1/users/"+id+"/") console.log(response.data) if (response.data.meta.status ==200){ this.$message({ message: '删除成功', type: 'success', duration:1000 }); // 刷新最新的数据列表 this.getUsers() } else{ this.$message({ message: '删除失败', type: 'warning' }); } },
-
编辑
使用嵌套表格的Dialog对话框
visible
表示是否显示该对话框,默认为false不显示,当我们修改这个值为true时,就显示,所以,当我们点击编辑按钮时,将dialogEditVisible
的值修改为true,就有弹出弹窗的效果。 重点:使用this.formEditUser = {...value}
复制一个对象,如果不复制,数据是双向绑定,不用提交页面显示就会改变。html部分
<!-- 编辑弹窗 --> <el-dialog title="编辑" :visible.sync="dialogEditVisible"> <el-form :model="formEditUser" > <el-form-item label="用户ID" :label-width="formLabelWidth" prop='id' > <el-input v-model="formEditUser.id" :disabled="true"></el-input> </el-form-item> <el-form-item label="用户名" :label-width="formLabelWidth" prop='username'> <el-input v-model="formEditUser.username" autocomplete="off" :disabled="true"></el-input> </el-form-item> <el-form-item label="邮箱" :label-width="formLabelWidth" prop='email'> <el-input v-model="formEditUser.email" autocomplete="off"></el-input> </el-form-item> <el-form-item label="手机号" :label-width="formLabelWidth" prop='mobile'> <el-input v-model="formEditUser.mobile" autocomplete="off"></el-input> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogEditVisible = false">取 消</el-button> <el-button type="primary" @click="updateUser()">确 定</el-button> </div> </el-dialog>
方法部分
// 处理编辑用户 async handleEdit(value){ console.log("当前编辑的内容为:",value) this.dialogEditVisible =true // 复制一个对象,如果不复制,数据是双向绑定,不用提交页面显示就会改变 this.formEditUser = {...value} }, // 请求编辑用户接口 async updateUser(){ const response = await this.$request.put("api/private/v1/users/"+this.formEditUser.id+"/",this.formEditUser) if (response.data.meta.status ==200){ this.dialogEditVisible = false this.$message({ message: '更新成功', type: 'success', duration:1000 }); // 刷新最新的数据列表 this.getUsers() } else{ this.$message({ message: '更新失败', type: 'warning' }); } },
data部
data() { return { // 编辑框 dialogEditVisible: false, formEditUser: { },
-
新增
新增弹窗跟编辑弹窗类似。
新增弹窗对传入的参数有校验,用户名和密码为必传。用到Login.vue部分的前后端校验规则。
4.2 完整代码示例
<template>
<div>
<el-card class="box-card">
<div slot="header" class="clearfix">
<!-- 面包屑 -->
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>项目管理</el-breadcrumb-item>
<el-breadcrumb-item>项目列表</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
<!-- 数据列表 -->
<el-table :data="projectList" style="width: 100%;margin-bottom:20px ;" >
<el-table-column type="index" width="50" label="序号">
</el-table-column>
<el-table-column prop="id" label="用户ID" width="80">
</el-table-column>
<el-table-column prop="role_name" label="角色" width="150">
</el-table-column>
<el-table-column prop="username" label="用户名" width="150">
</el-table-column>
<el-table-column prop="mobile" label="手机号" width="150">
</el-table-column>
<el-table-column prop="email" label="邮箱" width="150">
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="150" sortable>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm @confirm="handleDelete(scope.row.id)"
title="确定删除吗?"
>
<el-button size="mini"
type="danger" slot="reference">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 翻页 -->
<div class="block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="size"
layout="total, sizes, prev, pager, next, jumper"
:total=total>
</el-pagination>
</div>
</el-card>
<!-- 编辑弹窗 -->
<el-dialog title="编辑" :visible.sync="dialogEditVisible">
<el-form :model="formEditUser" >
<el-form-item label="用户ID" :label-width="formLabelWidth" prop='id' >
<el-input v-model="formEditUser.id" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="用户名" :label-width="formLabelWidth" prop='username'>
<el-input v-model="formEditUser.username" autocomplete="off" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="邮箱" :label-width="formLabelWidth" prop='email'>
<el-input v-model="formEditUser.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="手机号" :label-width="formLabelWidth" prop='mobile'>
<el-input v-model="formEditUser.mobile" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogEditVisible = false">取 消</el-button>
<el-button type="primary" @click="updateUser()">确 定</el-button>
</div>
</el-dialog>
<!-- 新增弹窗 -->
<el-dialog title="新增" :visible.sync="dialogAddVisible">
<el-form :model="formAddUser" :rules="addUserRules" ref="addUserRef" >
<el-form-item label="用户名" :label-width="formLabelWidth" prop='username'>
<el-input v-model="formAddUser.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" :label-width="formLabelWidth" prop='password'>
<el-input v-model="formAddUser.password" autocomplete="off" ></el-input>
</el-form-item>
<el-form-item label="邮箱" :label-width="formLabelWidth" prop='email'>
<el-input v-model="formAddUser.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="手机号" :label-width="formLabelWidth" prop='mobile'>
<el-input v-model="formAddUser.mobile" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogAddVisible = false">取 消</el-button>
<el-button type="primary" @click="addUser()">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
projectList: [''],
total:0,
currentPage:1,
size:10,
// 编辑框
dialogEditVisible: false,
dialogAddVisible: false,
formEditUser: {
},
formAddUser: {
},
formLabelWidth: '120px',
addUserRules:{
username:[
{required: true,message: '用户名不能为空', trigger: 'blur'}
],
password:[
{required: true,message: '密码不能为空', trigger: 'blur'}
]
}
}
},
methods:{
// 封装获取用户列表的接口,因为多处会使用到
async getUsers(){
// 请求用户列表接口
const response = await this.$request.get("api/private/v1/users",{
params:{
pagenum:this.currentPage,
pagesize:this.size
}
})
if (response.data.meta.status!==200){
return this.$message({message: '服务异常',type: 'warning'
});
}
console.log("data",response.data)
this.projectList = response.data.data.users
this.total = response.data.data.total
this.currentPage = response.data.data.pagenum
},
// 处理切换页面size
handleSizeChange(size){
// 给size赋值为当前值
this.size =size
this.getUsers()
},
// 处理切换页码
handleCurrentChange(currentPage){
this.currentPage =currentPage
this.getUsers()
},
// 处理删除按钮
async handleDelete(id){
console.log("当前删除的数据id为:",id)
// 请求删除用户的接口
const response = await this.$request.delete("api/private/v1/users/"+id+"/")
console.log(response.data)
if (response.data.meta.status ==200){
this.$message({
message: '删除成功',
type: 'success',
duration:1000
});
// 刷新最新的数据列表
this.getUsers()
}
else{
this.$message({
message: '删除失败',
type: 'warning'
});
}
},
// 处理编辑用户
async handleEdit(value){
console.log("当前编辑的内容为:",value)
this.dialogEditVisible =true
// 复制一个对象,如果不复制,数据是双向绑定,不用提交页面显示就会改变
this.formEditUser = {...value}
},
// 请求编辑用户接口
async updateUser(){
const response = await this.$request.put("api/private/v1/users/"+this.formEditUser.id+"/",this.formEditUser)
if (response.data.meta.status ==200){
this.dialogEditVisible = false
this.$message({
message: '更新成功',
type: 'success',
duration:1000
});
// 刷新最新的数据列表
this.getUsers()
}
else{
this.$message({
message: '更新失败',
type: 'warning'
});
}
},
// 处理新增
handleAdd(){
this.dialogAddVisible = true
},
addUser() {
this.$refs.addUserRef.validate(async (valid) => {
console.log("表单验证的结果",valid)
if (!valid) {
return
}
else {
const response = await this.$request.post("api/private/v1/users/",this.formAddUser)
if (response.data.meta.status==201){
this.dialogAddVisible = false
this.$message({
message: '新增成功',
type: 'success',
duration:1000
});
// 刷新最新的数据列表
}
else{
this.$message({
message: response.data.meta.msg,
type: 'warning'
});
}
}
});
}},
// 组件中的数据挂载到模版中之后,会触发这个生命周期钩子函数
async mounted(){
this.getUsers();
}
}
</script>
<style>
</style>
5. 接口管理
主要是列表展示,跟项目管理页差不多,自行练手。
6. 用例管理
类似于postman的调用页面。
-
栅格布局组件
<el-row :gutter="24"> <el-col :span="4"> <el-select v-model="caseInfo.method" placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> </el-col> <el-col :span="8"> <el-input placeholder="请输入请求域名" v-model="caseInfo.host"> <template slot="prepend">Http://</template> </el-input> </el-col> <el-col :span="8"> <el-input placeholder="请输入请求路径" v-model="caseInfo.path"> <template slot="prepend">接口路径</template> </el-input> </el-col> <el-col :span="4"><el-button type="primary"><i class="el-icon-s-promotion"></i> 运行</el-button></el-col> </el-row>
-
选项卡组件
-
动态增加输入项
// 监听器 watch:{ // 对于对象中有多层值的 'caseInfo.headers':{ handler:function(value,oldval){ // 为0时给个空行 if (value.length==0){ this.caseInfo.headers.push({key:'',value:''}) } // 判断是否是最后一行 if ([value.length-1].key || value[value.length-1].value){ this.caseInfo.headers.push({key:'',value:''}) } }, deep:true } },
-
json格式数据展示
安装:
npm install vue2-ace-editor
或者vue ui中安装依赖导入为子组件:
// 导入依赖 import Editor from 'vue2-ace-editor' // 注册为子组件 components:{ Editor }
-
json数据编辑器
<!-- json编辑器 --> <el-tab-pane label="application/json" name="second"> <editor ref="aceEditor" v-model="caseInfo.json" @init="editorInit" width="1000px" height="600px" lang="'json'" :theme="theme" :options="{ enableBasicAutocompletion: true, enableSnippets: true, enableLiveAutocompletion: true, tabSize:2, fontSize:14, showPrintMargin:false, //去除编辑器里的竖线 }" > </editor> </el-tab-pane>
6.2 代码示例
<template>
<div>
<el-card class="box-card">
<div slot="header" class="clearfix">
<!-- 面包屑 -->
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>用例管理</el-breadcrumb-item>
<el-breadcrumb-item>用例编辑</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-divider content-position="left"><span style="color: #409eff;font-weight: bolder;">Api</span></el-divider>
<el-row :gutter="24">
<el-col :span="4">
<el-select v-model="caseInfo.method" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
<el-col :span="8">
<el-input placeholder="请输入请求域名" v-model="caseInfo.host">
<template slot="prepend">Http://</template>
</el-input>
</el-col>
<el-col :span="8">
<el-input placeholder="请输入请求路径" v-model="caseInfo.path">
<template slot="prepend">接口路径</template>
</el-input>
</el-col>
<el-col :span="4"><el-button type="primary"><i class="el-icon-s-promotion"></i> 运行</el-button></el-col>
</el-row>
<el-divider content-position="left"><span style="color: #409eff;font-weight: bolder;">Request</span></el-divider>
<el-tabs type="border-card">
<!-- 请求头 -->
<el-tab-pane label="请求头">
<el-row :gutter="24" v-for='header in caseInfo.headers' :key='header.key'>
<el-col :span="10">
<el-input v-model.lazy="header.key" placeholder="请输入请求头参数"></el-input>
</el-col>
<el-col :span="10">
<el-input v-model.lazy="header.value" placeholder="请输入请求头参数值"></el-input>
</el-col>
<el-col :span="4">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteRow(header)"></el-button>
</el-col>
</el-row>
</el-tab-pane>
<!-- 请求参数 -->
<el-tab-pane label="请求参数">
<el-tabs>
<el-tab-pane label="Params" name="first">类似请求头的操作</el-tab-pane>
<!-- json编辑器 -->
<el-tab-pane label="application/json" name="second">
<editor
ref="aceEditor"
v-model="caseInfo.json"
@init="editorInit"
width="1000px"
height="600px"
lang="'json'"
:theme="theme"
:options="{
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
tabSize:2,
fontSize:14,
showPrintMargin:false, //去除编辑器里的竖线
}"
></editor>
</el-tab-pane>
<el-tab-pane label="form" name="third">表单</el-tab-pane>
</el-tabs>
</el-tab-pane>
<el-tab-pane label="响应提取">响应提取</el-tab-pane>
<el-tab-pane label="用例断言">用例断言</el-tab-pane>
<el-tab-pane label="数据库校验">数据库校验</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
// 导入依赖
import Editor from 'vue2-ace-editor'
import "brace/theme/monokai";
export default {
data(){
return {
caseInfo:{
method:'GET',
host:'',
path:'',
headers:[
{key:'Content-Type',value:'application/json'},
{key:'User-Agent',value:'PostmanRuntime/7.29.0'},
{key:'',value:''}
],
params:[
{key:'',value:''}
],
data:[
{key:'',value:''}
],
json:'{}'
},
options:[{
value:"GET",
label:"GET"
},{
value:"POST",
label:"POST"
},{
value:"PUT",
label:"PUT"
}
],
input1:'',
input2:'',
theme:''
}
},
methods:{
deleteRow(header){
console.log(header)
// 一种方式 找索引
// hIndex=this.caseInfo.headers.findIndex(function(item,index){
// if (item==header) return
// })
// this.caseInfo.slice(hIndex)
// 另一种 过滤
const newHeaders = this.caseInfo.headers.filter(function(item,index){
return item != header
})
this.caseInfo.headers=newHeaders
},
editorInit() {
require('brace/ext/language_tools') //language extension prerequsite...
require('brace/mode/html')
require('brace/mode/json') //language
require('brace/mode/python')
require('brace/mode/less')
require('brace/theme/monokai')
require('brace/snippets/json') //snippet
}
},
// 监听器
watch:{
// 对于对象中有多层值的
'caseInfo.headers':{
handler:function(value,oldval){
// 为0时给个空行
if (value.length==0){
this.caseInfo.headers.push({key:'',value:''})
}
// 判断是否是最后一行
if ([value.length-1].key || value[value.length-1].value){
this.caseInfo.headers.push({key:'',value:''})
}
},
deep:true
}
},
// 注册为子组件
components:{
Editor
}
}
</script>
<style scope>
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.el-col {
border-radius: 4px;
}
</style>
7. 部署
7.1 build编译
方式一:通过vue的管理页-选择build
打包成功:
方式二:npm命令 npm run build
7.2 build的意义
当build成功后,在项目目录下会生成一个dist文件夹。vue是一个单文件网页,所以会有一个index.html。
build是将vue的代码编译为对应的html文件、css、js文件。
生产环境的部署,就是拿dist文件夹的内容去部署的,而不是拿你写的vue文件去部署。
7.3 部署
假设project_prd是生产环境的路径。
-
将dist文件copy到生产环境的路径project_prd下。
-
初始化包管理文件
进入到目录下:
cd /Users/leitianxiao/Documents/700_TestDev/project_prd
初始化命令:
npm init -y
会生成一个package.json的文件,但是此时文件中没有记录依赖。
-
安装express框架:
npm install express
会生成一个package-lock.json
-
创建一个app.js文件
作用是让项目跑起来。
// 导入框架express const express =require('express') const app =express() // 指定静态资源对象 app.use(express.static('./dist')) // 监听80端口 app.listen(80,()=>{ console.log("服务已启动,运行在127.0.0.1:80") })
-
使用命令
node app.js
启动服务 -
访问127.0.0.1:80
-
前端项目管理工具pm2
可以发现,如果关闭了终端,127.0.0.1:80会无法访问。
安装为全局工具
npm install pm2 -g
,-g表示全局,不是安装在这个项目下。这是比较传统的步骤,后面再介绍使用docker。
-
启动pm2
pm2 start app.js
此时关闭终端也可以访问127.0.0.1:80。
常用参数:
pm2 start xxx 启动 pm2 list 查看当前运行项目 pm2 delet xxx(id)通过id删除当前启动的项目 pm2 start xxx --name abc 启动项目并给项目命名为abc pm2 stop xxx 暂停项目的执行
-
nginx端口转发 (学后端时间讲)
指定监听端口,转发到前端监听端口。