Skip to content

测试平台开发前端


需求

测试平台前端界面开发。需要实现登录界面、首页、测试用例页面、测试计划页面、测试记录页面。


实战步骤

uml diagram


初始化项目


  • 第一步:选择 vue
  • 第二步:选择 javascript
  • 第三步:
    • cd vite-project
    • npm install
    • npm run dev

使用 vscode 打开项目

  • 项目结构


  • vscode 安装以下插件
    • Chinese (Simplified),为 VS Code 提供本地化界面,按下“Ctrl+Shift+P”组合键以显示“命令面板”,然后键入“display”以筛选并显示“Configure Display Language”命令。按“Enter”,然后会按区域设置显示安装的语言列表,并突出显示当前语言设置
    • Vue.js Extension Pack,用于 vue3 的智能代码提示,语法高亮、智能感知、Emmet 等
    • Prettier - Code formatter,代码格式化
    • Auto Rename Tag,修改 html 标签,自动完成尾部闭合标签的同步修改
    • Auto Close Tag,自动闭合 HTML 标签
    • Path Intellisense,自动路径补全

安装依赖库

  • 1.安装接口请求工具 axios

    npm install axios
    
  • 2.安装路由 vue-router

    npm install vue-router@4
    
  • 3.安装组件库 element-plus
    npm install element-plus
    npm install @element-plus/icons-vue
    
  • 4.调整 html 细节
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试平台前端项目</title>
    <style>
      html,
      body,
      #app,
      .common-layout,
      .el-container {
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

封装接口请求

  • 新建 api 文件
  • 在 api 文件中新建 http.js,初始化 axios
// 完成http请求的基本配置
// 导入axios
import axios from "axios";

// 创建axios实例
var instance = axios.create({
  // 请求体
  headers: {
    "Content-Type": "application/json",
  },
  // 超时时间
  timeout: 2500,
  // 基础url,后端的接口服务地址
  // baseURL: 'https://dev-hogwarts-platform-backend.hogwarts.ceshiren.com'
  baseURL: "http://127.0.0.1:5000",
});

// 添加请求拦截器,在请求头中加入token
instance.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem("token");
    // console.log('token', token)
    if (token) {
      // 设置请求头中的 Authorization 字段
      config.headers.Authorization = `Bearer ${token}`;
      // console.log('token', config.headers.Authorization)
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default instance;

新建 api.js,封装接口请求

import instance from "./http";

const api = {
  // get方法
  get(url, params) {
    return instance({
      url: url,
      method: "get",
      params: params,
    });
  },
  // post方法
  post(url, data) {
    return instance({
      url: url,
      method: "post",
      data: data,
    });
  },
  // put方法
  put(url, data) {
    return instance({
      url: url,
      method: "put",
      data: data,
    });
  },
  // delete方法
  delete(url, data) {
    return instance({
      url: url,
      method: "delete",
      data: data,
    });
  },
};
export default api;

封装路由

  • 新建 router 文件
  • 在 router 文件中新建 index.js,封装路由映射
import { createRouter, createWebHashHistory } from "vue-router";

const routes = [
  {
    path: "/",
    redirect: "/index",
  },
  {
    path: "/index",
    name: "Index",
    component: () => import("../views/Index.vue"),
    children: [
      {
        path: "testcase",
        name: "Testcase",
        component: () => import("../views/Testcase.vue"),
      },
      {
        path: "plan",
        name: "Plan",
        component: () => import("../views/Plan.vue"),
      },
      {
        path: "record",
        name: "Record",
        component: () => import("../views/Record.vue"),
      },
    ],
  },
  {
    path: "/user",
    name: "User",
    component: () => import("../views/User.vue"),
  },
];

const router = createRouter({
  // 使用hash方式实现路由
  history: createWebHashHistory(),
  routes,
});

// 路由守卫
router.beforeEach((to, from, next) => {
  // 判断是否要去登录页面
  if (to.path == "/user") {
    // 放行
    next();
  } else {
    // 去其他路由页面,判断是否有登录的token
    const token = localStorage.getItem("token");
    // 如果没有token(未登录)
    if (!token) {
      // 强制进入登录页面
      next("/user");
    } else {
      // 放行
      next();
    }
  }
});

export default router;

使用依赖包

  • main.js 中引入依赖包并使用
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";

// 导入api
import api from "./api/api";
// 导入路由
import router from "./router";
// 导入element-plus
import ElementPlus from "element-plus";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";

// 注册api
window.$api = api;

// 初始化vue App
const app = createApp(App);

// 全局引入icon库
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}

// 全局引入路由router和element-plus
app.use(router).use(ElementPlus, { locale: zhCn }).mount("#app");

  • App.vue
<template>
  <router-view></router-view>
</template>

<style scoped>

</style>

初始化页面

  • 新建 views 文件,在 views 文件中新建以下 vue 文件
  • User.vue
    <template>
      <div>User</div>
    </template>
    
    <script setup></script>
    
    <style scoped></style>
    
  • Index.vue
    <template>
      <router-view></router-view>
    </template>
    
    <script setup></script>
    
    <style scoped></style>
    
  • Testcase.vue

    <template>
      <div>Testcase</div>
    </template>
    
    <script setup></script>
    
    <style scoped></style>
    
  • Plan.vue

    <template>
      <div>Plan</div>
    </template>
    
    <script setup></script>
    
    <style scoped></style>
    
  • Record.vue
    <template>
      <div>Record</div>
    </template>
    
    <script setup></script>
    
    <style scoped></style>
    

实现登录页面 User.vue

<template>
    <div class="main">
        <el-card class="box-card" style="margin-bottom: 10%;margin-right: 4%;">
            <template #header>
                <div class="card-header">
                    <span>登录表单</span>
                </div>
            </template>
            <el-form :model="form" label-width="80px">
                <el-form-item label="用户名">
                    <el-input v-model="form.username" />
                </el-form-item>
                <el-form-item label="密码">
                    <el-input type="password" v-model="form.password" />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="login">登录</el-button>
                    <el-button type="success" @click="register">注册</el-button>
                </el-form-item>
            </el-form>
        </el-card>

    </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'

const router = useRouter()

const form = ref({
    username: '',
    password: '',
})

// 登录
const login = async () => {
    const result = await $api.post('/user/login', form.value)
    const { code, data, msg } = result.data
    console.log(code, data, msg);
    // 登录成功时
    if (code == 0) {
        ElMessage.success(msg)
        localStorage.setItem('token', data.token)
        router.push('/index/testcase')
    } else {
        ElMessage.error(msg)
    }
}

// 注册
const register = async () => {
    const result = await $api.post('/user/register', form.value)
    const { code, data, msg } = result.data
    // 注册成功时
    if (code == 0) {
        ElMessage.success(msg)
        login()
    } else {
        ElMessage.error(msg)
    }
}
</script>

<style scoped>
.main {
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background-image: url('https://ceshiren.com/uploads/default/original/3X/2/8/289416e80d70439819bd6003883d8dfe4e2cd5f3.jpeg');
    background-size: 100% 100%;
}
</style>


实现顶部栏侧边栏 Index.vue

<template>
    <div class="common-layout">
        <el-container>
            <el-header style="border-bottom: 1px solid #ccc;">
                <div style="display: flex;justify-content:space-between;align-items: center;">
                    <h1>测试平台</h1>
                    <div
                        style="display: flex;flex-direction: row;gap: 10px;justify-content:center;align-items: center;">
                        <h2>{{ username }}</h2>
                        <el-button type="primary" @click="logout">退出</el-button>
                    </div>
                </div>
            </el-header>
            <el-container>
                <el-aside width="200px">
                    <el-menu router default-active="2" class="el-menu-vertical-demo" @open="handleOpen"
                        @close="handleClose">
                        <el-menu-item index="testcase">
                            <el-icon>
                                <document />
                            </el-icon>
                            <span>测试用例</span>
                        </el-menu-item>
                        <el-menu-item index="plan">
                            <el-icon><icon-menu /></el-icon>
                            <span>测试计划</span>
                        </el-menu-item>
                        <el-menu-item index="record">
                            <el-icon>
                                <setting />
                            </el-icon>
                            <span>测试记录</span>
                        </el-menu-item>
                    </el-menu>
                </el-aside>
                <el-main style="border-left: 1px solid #ccc;">
                    <!-- 三个界面用router-view占位 -->
                    <router-view></router-view>
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>

<script setup>
import { onMounted, ref } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"
import {
    Document,
    Menu as IconMenu,
    Setting,
} from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'

const router = useRouter()

const logout = () => {
    router.push('/user')
}

const username= ref('')
// 初始化数据
const initData = async () => {
    const result = await $api.get('/user/get_info')
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        username.value = data
    } else {
        ElMessage.error(msg)
    }
}

// 获取数据操作放在onMounted生命周期中
onMounted(() => {
    initData()
})
</script>

<style scoped></style>

实现测试用例页面 Testcase.vue

<template>
    <!-- 顶部栏按钮 -->
    <el-button type="success" @click="dialogPlan = true">新增计划</el-button>
    <el-button type="primary" @click="dialogAdd = true">新增用例</el-button>

    <!-- 表格 -->
    <el-table ref="tableRef" stripe border @selection-change="selectionChange" :data="tableData"
        style="margin-top: 10px;width: 100%">
        <el-table-column type="selection" width="40" />
        <el-table-column prop="id" label="用例Id" width="80" />
        <el-table-column prop="name" label="用例名称" />
        <el-table-column prop="step" label="用例步骤" />
        <el-table-column prop="method" label="用例方法" />
        <el-table-column prop="remark" label="备注" />
        <el-table-column prop="actions" label="操作" width="140">
            <template #default="scope">
                <el-button size="small" type="primary" @click="handleEdit(scope.$index)">修改</el-button>
                <el-button size="small" type="danger" @click="handleDelete(scope.$index)">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <!-- 新增计划弹框 -->
    <el-dialog v-model="dialogPlan"  title="新增计划">
        <!-- 内容区域 -->
        <el-form>
            <el-form-item label="计划名称">
                <el-input v-model="planName" />
            </el-form-item>
        </el-form>
        <!-- 底部插槽 -->
        <template #footer>
            <span class="dialog-footer">
                <el-button @click="dialogPlan = false">取消</el-button>
                <el-button type="primary" @click="addPlan"> 确认 </el-button>
            </span>
        </template>
    </el-dialog>

    <!-- 新增用例弹框 -->
    <el-dialog v-model="dialogAdd" title="新增用例">
        <!-- 内容区域 -->
        <el-form :model="addData">
            <el-form-item label="用例名称">
                <el-input v-model="addData.name" />
            </el-form-item>
            <el-form-item label="用例步骤">
                <el-input type="textarea" v-model="addData.step" />
            </el-form-item>
            <el-form-item label="用例方法">
                <el-input type="textarea" v-model="addData.method" />
            </el-form-item>
            <el-form-item label="用例备注">
                <el-input type="textarea" v-model="addData.remark" />
            </el-form-item>
        </el-form>
        <!-- 底部插槽 -->
        <template #footer>
            <span class="dialog-footer">
                <el-button @click="dialogAdd = false">取消</el-button>
                <el-button type="primary" @click="addTestcase"> 确认 </el-button>
            </span>
        </template>
    </el-dialog>

    <!-- 修改用例弹框 -->
    <el-dialog v-model="dialogPut" title="修改用例">
        <!-- 内容区域 -->
        <el-form :model="putData">
            <el-form-item label="用例ID">
                <el-input v-model="putData.id" disabled />
            </el-form-item>
            <el-form-item label="用例名称">
                <el-input v-model="putData.name" />
            </el-form-item>
            <el-form-item label="用例步骤">
                <el-input type="textarea" v-model="putData.step" />
            </el-form-item>
            <el-form-item label="用例方法">
                <el-input type="textarea" v-model="putData.method" />
            </el-form-item>
            <el-form-item label="用例备注">
                <el-input type="textarea" v-model="putData.remark" />
            </el-form-item>
        </el-form>
        <!-- 底部插槽 -->
        <template #footer>
            <span class="dialog-footer">
                <el-button @click="dialogPut = false">取消</el-button>
                <el-button type="primary" @click="putTestcase"> 确认 </el-button>
            </span>
        </template>
    </el-dialog>
</template>

<script setup>
import { onMounted, ref } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"

const tableRef = ref()
// 计划弹框
const dialogPlan = ref(false)
// 计划名称
const planName = ref('')
// 新增计划[6, 7]
const addPlan = async () => {
    let planData = { 'name': planName.value, 'testcase_ids': idList.value }
    const result = await $api.post('/plan/post', planData)
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        dialogPlan.value = false
        planName.value = ''
        tableRef.value.clearSelection()
    } else {
        ElMessage.error(msg)
    }
}

// 新增弹框
const dialogAdd = ref(false)
// 用例数据
const addData = ref({})
// 新增用例
const addTestcase = async () => {
    const result = await $api.post('/testcase/post', addData.value)
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        dialogAdd.value = false
        addData.value = {}
        initData()
    } else {
        ElMessage.error(msg)
    }
}

// 修改弹框
const dialogPut = ref(false)
// 点击修改
const handleEdit = (index) => {
    // 回传数据
    putData.value = tableData.value[index]
    dialogPut.value = true
}
// 用例数据
const putData = ref({})
// 修改用例
const putTestcase = async () => {
    const result = await $api.post('/testcase/put', putData.value)
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        dialogPut.value = false
        putData.value = {}
        initData()
    } else {
        ElMessage.error(msg)
    }
}

// 删除用例
const handleDelete = (index) => {
    ElMessageBox.alert('请确认是否删除,删除数据后无法找回', '警告!', {
        confirmButtonText: '确认',
        callback: async (action) => {
            console.log('action', action)
            if (action == 'confirm') {
                let deleteData = { 'id': tableData.value[index].id }
                const result = await $api.post('/testcase/delete', deleteData)
                const { code, data, msg } = result.data
                // 成功时
                if (code == 0) {
                    ElMessage.success(msg)
                    initData()
                } else {
                    ElMessage.error(msg)
                }
            }
        }
    })
}

// id列表
const idList = ref([])
// 勾选后会自动触发
const selectionChange = (items) => {
    // [{id: 1, name: '', ..}, {id: 2, name: '', ..}, {id: 3, name: '', ..}]
    idList.value = items.map((value) => value.id)
}

// 表格数据
const tableData = ref([])

// 初始化数据
const initData = async () => {
    const result = await $api.get('/testcase/get')
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        tableData.value = data
    } else {
        ElMessage.error(msg)
    }
}

// 获取数据操作放在onMounted生命周期中
onMounted(() => {
    initData()
})
</script>

<style scoped></style>


实现测试计划页面 Plan.vue

<template>
    <!-- 表格 -->
    <el-table stripe border :data="tableData" style="width: 100%">
        <el-table-column prop="id" label="计划Id" width="80" />
        <el-table-column prop="name" label="计划名称" />
        <el-table-column prop="testcases" label="用例详情">
            <template #default="scope">
                <span v-for="item in scope.row.testcases">{{ item.name }}</span>
            </template>
        </el-table-column>
        <el-table-column prop="actions" label="操作" width="220">
            <template #default="scope">
                <el-button size="small" type="success" @click="handleBuild(scope.$index)">执行</el-button>
                <el-button size="small" type="primary" @click="getRecord(scope.$index)">历史记录</el-button>
                <el-button size="small" type="danger" @click="handleDelete(scope.$index)">删除</el-button>
            </template>
        </el-table-column>
    </el-table>

    <!-- 历史记录弹框 -->
    <el-dialog v-model="dialogRecord" title="历史记录">
        <!-- 内容区域 -->
        <el-table stripe border :data="recordData" style="width: 100%">
            <el-table-column prop="id" label="记录Id" width="80" />
            <el-table-column prop="plan_id" label="计划Id" width="80" />
            <el-table-column prop="report" label="用例详情">
                <template #default="scope">
                    <a :href="scope.row.report">{{ scope.row.report }}</a>
                </template>
            </el-table-column>
            <el-table-column prop="create_time" label="创建时间" />
        </el-table>
    </el-dialog>
</template>

<script setup>
import { onMounted, ref } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"

// 历史记录弹框
const dialogRecord = ref(false)
// 历史记录数据
const recordData = ref()
// 获取指定计划的报告
const getRecord = async (index) => {
    let getData = { 'plan_id': tableData.value[index].id }
    const result = await $api.get('/record/get', getData)
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        recordData.value = data
    } else {
        ElMessage.error(msg)
    }
    dialogRecord.value = true
}


// 执行
const handleBuild = async (index) => {
    let postData = { 'plan_id': tableData.value[index].id }
    const result = await $api.post('/record/post', postData)
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
    } else {
        ElMessage.error(msg)
    }
}

// 删除
const handleDelete = (index) => {
    ElMessageBox.alert('请确认是否删除,删除数据后无法找回', '警告!', {
        confirmButtonText: '确认',
        callback: async () => {
            let deleteData = { 'id': tableData.value[index].id }
            const result = await $api.post('/plan/delete', deleteData)
            const { code, data, msg } = result.data
            // 成功时
            if (code == 0) {
                ElMessage.success(msg)
                initData()
            } else {
                ElMessage.error(msg)
            }
        },
    })
}

// 表格数据
const tableData = ref([])

// 初始化数据
const initData = async () => {
    const result = await $api.get('/plan/get')
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        tableData.value = data
    } else {
        ElMessage.error(msg)
    }
}

// 获取数据操作放在onMounted生命周期中
onMounted(() => {
    initData()
})
</script>

<style scoped></style>


实现测试记录页面 Record.vue

<template>
    <!-- 表格 -->
    <el-table stripe border :data="tableData" style="width: 100%">
        <el-table-column prop="id" label="记录Id" width="180" />
        <el-table-column prop="plan_id" label="计划Id" width="180" />
        <el-table-column prop="report" label="用例详情">
            <template #default="scope">
                <a :href="scope.row.report">{{ scope.row.report }}</a>
            </template>
        </el-table-column>
        <el-table-column prop="create_time" label="创建时间" />
    </el-table>
</template>

<script setup>
import { onMounted, ref } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"

// 表格数据
const tableData = ref([])

// 初始化数据
const initData = async () => {
    const result = await $api.get('/record/get')
    const { code, data, msg } = result.data
    // 成功时
    if (code == 0) {
        ElMessage.success(msg)
        tableData.value = data
    } else {
        ElMessage.error(msg)
    }
}

// 获取数据操作放在onMounted生命周期中
onMounted(() => {
    initData()
})
</script>

<style scoped></style>


总结

  • 初始化测试平台前端项目
  • 开发测试平台前端页面