测开-Vue开发实战part3

119 阅读7分钟

后端支持

运行这个文件,就启动了一个后端服务,提供了一些练手的本地接口。

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

image.png

2.生成一个简单模版vue文件

image.png

3.创建Home.vue(首页)、Cases.vue(用例页)、Projects.vue(项目页)、Interface(接口页)

4.在每个页面都下些对应的内容来看显示是否正常,示例:

image.png

5.此时访问/login,没有显示内容,需要在App.vue中添加视图占位符 <router-view></router-view>

image.png

此时访问:

image.png

配置路由规则

router文件夹下,index.js中配置

在我们的设计中,Cases.vue(用例页)、Projects.vue(项目页)、Interface(接口页)都是在Home.vue(首页)中展示的,用子路由实现。

image.png

此时访问/cases、/projects、/interface,不会显示对应的内容,需要在Home.vue中添加视图占位符。 <router-view></router-view>

image.png

image.png

初步练手

登录页

1.在elementUI上找到相似的代码copy过来进行修改,登录页需要用户名、密码输入框、提交按钮,是一个表单形式。

点击显示代码,把html的代码copy到Login.vue的<template></template>标签中,把代码中的<script>copy到Login.vue的<script></script>

image.png

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页有导航栏对应子路由的测试用例页、项目页、接口页

image.png

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下添加

image.png

// 添加导航守卫
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找不到。

image.png

这个request是指Login.vue中的request,之前是在<script></script>中单个文件中定义request。现在作为一个项目来说需要放在一个文件中定义,供各处使用。

image.png

一般,在src下新建api文件夹,放与接口调用相关的代码。

image.png

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原型绑定。

image.png

//导入创建的请求对象 - request
import request from '../src/api/index.js'

// 将请求对象绑定到vue的原型上
Vue.prototype.$request = request

优化Login.vue的request变量:

image.png

现在就能通过你写在服务端的正确的用户名和密码进行登录了。登录状态就能去任何路径,token掉了之后,访问其他页面都只能去到login页。

登录成功或失败的提示是原生弹窗的,我们可以修改为elementUI的。

image.png

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'
                });
}

image.png

启动项目:npm run sever,同vue ui中启动app

实战项目

1. 项目初始化

这个上一篇文章讲过,搬运过来。

初始化的项目页面如下,我们需要清掉它初始化的内容:

image.png

1.App.vue页面:

image.png

2.不使用views目录结构,删掉

3.删掉views对应的路由规则删掉

image.png

4.删掉自带的组件helloworld,componets文件夹下

这样就得到了一个干净的项目了。

2. 登录与退出

上文练手部分也到过,现在作为一个项目级别的做一些优化。

2.1 实现输入账号、密码登录功能
  • 优化标题样式

    样式放在<style>标签中

  • 去掉文字"账号"、"密码",使用icon表示

    去掉<el-form-item label="账号">中label

    添加icon,使用elementUI中的带icon的输入框方法

    image.png

    image.png

    给输入框内添加默认值placeholder

2.2 实现表单输入校验

image.png

  • 在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 提交表单时预验证

输入框有校验,但是点击登录时还是会发送请求。

image.png

  • 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 主页布局

主页菜单使用这个布局:

image.png

image.png

  • 把之前的导航栏 和视图占位符 分别放在侧边栏和主体内容中。

  • 增加退出登录

  • 优化样式

    <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

  • 退出登录需要一个二次确认弹框

    image.png

<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 实现层级菜单

image.png

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

    1. 路由'/'未配置,访问时没内容
    2. 访问/home时应有个默认页

    image.png 3. 默认页时侧边栏选中的菜单错误

    image.png

  • 面包屑实现

    image.png

    <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…

    可以把前面部分涉及的接口配置等都修改为这个项目。

    这部分使用查看用户列表的接口来实现 :

    image.png

    列表展示可以由斑马纹表格+有操作按钮的表格组成

    image.png

    image.png

    <!-- 数据列表 -->
    <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
    }
    
  • 分页功能实现

    选择组件:

    image.png

    <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对话框

    image.png

    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的调用页面。

image.png

  • 栅格布局组件

    image.png

    <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>
    
  • 选项卡组件

    image.png

  • 动态增加输入项

     // 监听器
     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

image.png

打包成功:

image.png

方式二:npm命令 npm run build

image.png

image.png

7.2 build的意义

当build成功后,在项目目录下会生成一个dist文件夹。vue是一个单文件网页,所以会有一个index.html。

build是将vue的代码编译为对应的html文件、css、js文件。

生产环境的部署,就是拿dist文件夹的内容去部署的,而不是拿你写的vue文件去部署。

image.png

7.3 部署

假设project_prd是生产环境的路径。

  1. 将dist文件copy到生产环境的路径project_prd下。

  2. 初始化包管理文件

    进入到目录下:

    cd /Users/leitianxiao/Documents/700_TestDev/project_prd

    初始化命令:

    npm init -y

    image.png

    会生成一个package.json的文件,但是此时文件中没有记录依赖。

  3. 安装express框架:

    npm install express

    image.png

    会生成一个package-lock.json

  4. 创建一个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")
    })
    
  5. 使用命令node app.js启动服务

  6. 访问127.0.0.1:80

  7. 前端项目管理工具pm2

    可以发现,如果关闭了终端,127.0.0.1:80会无法访问。

    安装为全局工具 npm install pm2 -g,-g表示全局,不是安装在这个项目下。

    这是比较传统的步骤,后面再介绍使用docker。

  8. 启动pm2

    pm2 start app.js

    image.png

    此时关闭终端也可以访问127.0.0.1:80。

    常用参数:

    pm2 start xxx 启动
    pm2 list 查看当前运行项目
    pm2 delet xxx(id)通过id删除当前启动的项目
    pm2 start xxx --name abc 启动项目并给项目命名为abc
    pm2 stop xxx 暂停项目的执行
    
  9. nginx端口转发 (学后端时间讲)

    指定监听端口,转发到前端监听端口。

8. 基于开源项目进行修改

gitee.com/lin-xin/vue…