├── .env ├── babel.config.js ├── public ├── favicon.ico └── index.html ├── src ├── assets │ ├── image │ │ ├── logo.png │ │ ├── favicon.png │ │ ├── login-bg.png │ │ ├── teprunner.png │ │ ├── user-icon.png │ │ └── file-template.png │ ├── fonts │ │ ├── fontawesome-webfont.674f50d2.eot │ │ ├── fontawesome-webfont.b06871f2.ttf │ │ ├── fontawesome-webfont.af7ae505.woff2 │ │ └── fontawesome-webfont.fee66e71.woff │ ├── js │ │ └── highlight.js │ ├── iconfont │ │ └── iconfont.js │ └── css │ │ └── common.scss ├── views │ ├── console │ │ ├── index.vue │ │ ├── ProjectManagement.vue │ │ ├── AddProject.vue │ │ ├── userManagement.vue │ │ └── addUser.vue │ ├── teprunner │ │ ├── index.vue │ │ ├── case │ │ │ ├── CaseView.vue │ │ │ ├── WriteDown.vue │ │ │ ├── CaseEditor.vue │ │ │ ├── CaseResult.vue │ │ │ └── CaseManagement.vue │ │ ├── AddEnvVar.vue │ │ ├── plan │ │ │ ├── CaseResult.vue │ │ │ ├── PlanResult.vue │ │ │ ├── PlanEditor.vue │ │ │ ├── CaseList.vue │ │ │ └── PlanManagement.vue │ │ ├── FixtureEditor.vue │ │ ├── Fixture.vue │ │ ├── EnvVar.vue │ │ ├── Grammar.vue │ │ └── GitSync.vue │ ├── login │ │ └── index.vue │ └── home │ │ └── index.vue ├── components │ ├── IconFont.vue │ ├── Pagination.vue │ ├── WrapComponent.vue │ ├── NavLeft.vue │ ├── ProjectEnv.vue │ └── SelectionPanel.vue ├── App.vue ├── main.js ├── utils │ ├── const.js │ └── commonMethods.js ├── service │ └── axios.js └── router │ └── index.js ├── README ├── image-20210306090248863.png └── image-20210821103333142.png ├── deploy ├── Dockerfile ├── nginx.conf └── build.sh ├── .gitignore ├── README.md ├── vue.config.js └── package.json /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_apiServer=http://127.0.0.1:8000 2 | VUE_APP_wsServer=ws://127.0.0.1:8000 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/image/logo.png -------------------------------------------------------------------------------- /src/assets/image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/image/favicon.png -------------------------------------------------------------------------------- /src/assets/image/login-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/image/login-bg.png -------------------------------------------------------------------------------- /src/assets/image/teprunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/image/teprunner.png -------------------------------------------------------------------------------- /src/assets/image/user-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/image/user-icon.png -------------------------------------------------------------------------------- /README/image-20210306090248863.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/README/image-20210306090248863.png -------------------------------------------------------------------------------- /README/image-20210821103333142.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/README/image-20210821103333142.png -------------------------------------------------------------------------------- /src/assets/image/file-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/image/file-template.png -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | WORKDIR /usr/share/nginx/html 3 | COPY ./dist ./ 4 | COPY ./deploy/nginx.conf /etc/nginx/conf.d/default.conf 5 | -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.674f50d2.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/fonts/fontawesome-webfont.674f50d2.eot -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.b06871f2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/fonts/fontawesome-webfont.b06871f2.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.af7ae505.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/fonts/fontawesome-webfont.af7ae505.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.fee66e71.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongfanger/teprunner-frontend/HEAD/src/assets/fonts/fontawesome-webfont.fee66e71.woff -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | location / { 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | } 7 | location /api { 8 | proxy_pass http://172.16.25.131:8099; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/views/console/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 24 | -------------------------------------------------------------------------------- /src/components/IconFont.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # teprunner-frontend 2 | 3 | 视频教程:https://space.bilibili.com/396344573/channel/series 4 | 5 | ## 下载源码 6 | 7 | 方式①: 8 | 9 | ``` 10 | git clone git@github.com:dongfanger/teprunner-frontend.git 11 | ``` 12 | 13 | 方式②:下载zip压缩包后解压。 14 | 15 | ![image-20210821103333142](README/image-20210821103333142.png) 16 | 17 | ## 安装依赖包 18 | 19 | 在项目目录打开cmd,执行命令: 20 | 21 | ``` 22 | npm install 23 | ``` 24 | 25 | ## 启动服务 26 | 27 | 等待依赖包安装完成后,启动前端服务: 28 | 29 | ``` 30 | npm run serve 31 | ``` 32 | 33 | ## 访问系统 34 | 35 | 打开浏览器,输入`localhost:8080`: 36 | 37 | ![image2](README/image-20210306090248863.png) 38 | 39 | 用户名`admin`,密码`qa123456`。此时还无法登陆,需要部署[后端服务]()。 -------------------------------------------------------------------------------- /deploy/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PkgName='teprunner-frontend' 3 | 4 | Dockerfile='./Dockerfile' 5 | DockerContext=../ 6 | 7 | BUILDER_IMAGE='node:latest' 8 | echo "Start compiling code..." 9 | docker run --rm -v $(pwd)/../:/data/src -v /root/.npm/_logs:/root/.npm/_logs -w /data/src/ $BUILDER_IMAGE /bin/sh -c "npm install && npm run build" 10 | [ $? -ne 0 ] && exit 2 11 | echo "Compile complete" 12 | 13 | echo "Start buiding image..." 14 | docker build -f $Dockerfile -t $PkgName $DockerContext 15 | if [ $? -eq 0 ] 16 | then 17 | echo "Build docker image success" 18 | exit 0 19 | else 20 | echo "Build docker image failed" 21 | exit 1 22 | fi 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 30 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | function resolve(dir) { 4 | return path.join(__dirname, dir); 5 | } 6 | 7 | module.exports = { 8 | publicPath: process.env.NODE_ENV === "development" ? "./" : "/", 9 | chainWebpack: config => { 10 | config.resolve.alias.set("@", resolve("src")).set("src", resolve("src")); 11 | config.plugin("html").tap(args => { 12 | args[0].title = "teprunner"; 13 | return args; 14 | }); 15 | }, 16 | devServer: { 17 | publicPath: process.env.NODE_ENV === "development" ? "/" : "./", 18 | proxy: { 19 | "/api": { 20 | // target: "https://bf80bb80-b543-422e-b05c-28350a90c0aa.mock.pstmn.io", 21 | target: process.env.VUE_APP_apiServer, 22 | changeOrigin: true, 23 | pathRewrite: { 24 | "^/api": "/api", 25 | }, 26 | }, 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/assets/js/highlight.js: -------------------------------------------------------------------------------- 1 | // highlight.js 代码高亮指令 2 | import Hljs from "highlight.js"; 3 | import "highlight.js/styles/monokai-sublime.css"; 4 | 5 | let Highlight = {}; 6 | Highlight.install = function(Vue) { 7 | // 先有数据再绑定,调用highlightA 8 | Vue.directive("highlightA", { 9 | inserted: function(el) { 10 | let blocks = el.querySelectorAll("pre code"); 11 | for (let i = 0; i < blocks.length; i++) { 12 | const item = blocks[i]; 13 | Hljs.highlightBlock(item); 14 | } 15 | }, 16 | }); 17 | 18 | // 先绑定,后面会有数据更新,调用highlightB 19 | Vue.directive("highlightB", { 20 | componentUpdated: function(el) { 21 | let blocks = el.querySelectorAll("pre code"); 22 | for (let i = 0; i < blocks.length; i++) { 23 | const item = blocks[i]; 24 | Hljs.highlightBlock(item); 25 | } 26 | }, 27 | }); 28 | }; 29 | 30 | export default Highlight; 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 20 | 21 | 22 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import axios from "./service/axios"; 5 | import Element from "element-ui"; 6 | 7 | import "./assets/css/common.scss"; 8 | import "./assets/css/font.css"; 9 | 10 | import IconFont from "@/components/IconFont.vue"; 11 | import "@/assets/iconfont/iconfont"; 12 | Vue.component("icon-font", IconFont); 13 | 14 | import { onResponse, notifyMessage } from "./utils/commonMethods"; 15 | Vue.prototype.$notifyMessage = notifyMessage; 16 | Vue.prototype.$handleResponese = onResponse; 17 | 18 | import Pagination from "@/components/Pagination"; 19 | Vue.component("vue-pagination", Pagination); 20 | 21 | import Highlight from "@/assets/js/highlight.js"; 22 | Vue.use(Highlight); 23 | 24 | Vue.config.productionTip = false; 25 | 26 | Vue.use(Element); 27 | Vue.use(axios); 28 | 29 | new Vue({ 30 | router, 31 | render: h => h(App), 32 | }).$mount("#app"); 33 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/utils/const.js: -------------------------------------------------------------------------------- 1 | export const pwdReg = /^(?![\d]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{8,30}$/; 2 | export const pwdRegText = "密码支持数字、字母、特殊字符,两种及以上类型组合,长度限制8-30之间"; 3 | 4 | export const blankSpaceReg = /\s+/; 5 | 6 | export const chineseReg = /[\u4e00-\u9fa5]+/; 7 | 8 | export const specialCodeReg = new RegExp("[`~!@#$^&*()=|{}':;',\\[\\]<>/?~!@#¥……&*()——|{}【】‘;:”“’。,、?]+"); 9 | export const nameValidator = (rule, value, callback) => { 10 | if (value) { 11 | value = value.trim(); 12 | if (value.length < 2 || value.length > 30) { 13 | callback("名称长度限制在2~30之间"); 14 | } else if (specialCodeReg.test(value)) { 15 | callback("名称不能包含特殊字符"); 16 | } 17 | } 18 | callback(); 19 | }; 20 | 21 | export const pwdValidator = (rule, value, callback) => { 22 | if (value) { 23 | if (value.length < 8 || value.length > 30) { 24 | callback("密码长度限制8~30之间"); 25 | } else if (!pwdReg.test(value)) { 26 | callback("你的密码复杂度太低, 请重新输入"); 27 | } 28 | } 29 | callback(); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/WrapComponent.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teprunner-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.2", 12 | "brace": "^0.11.1", 13 | "core-js": "^3.6.5", 14 | "element-ui": "^2.13.1", 15 | "highlight.js": "^9.12.0", 16 | "vue": "^2.6.11", 17 | "vue-monaco-editor": "0.0.19", 18 | "vue-router": "^3.0.1", 19 | "vue2-ace-editor": "0.0.13" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "~4.5.0", 23 | "@vue/cli-plugin-eslint": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "@vue/eslint-config-prettier": "^6.0.0", 26 | "babel-eslint": "^10.1.0", 27 | "eslint": "^6.7.2", 28 | "eslint-plugin-vue": "^6.2.2", 29 | "eslint-plugin-prettier": "^3.1.1", 30 | "node-sass": "^4.12.0", 31 | "prettier": "^1.19.1", 32 | "sass-loader": "^8.0.2", 33 | "vue-template-compiler": "^2.6.11" 34 | }, 35 | "eslintConfig": { 36 | "root": true, 37 | "env": { 38 | "node": true 39 | }, 40 | "extends": [ 41 | "plugin:vue/essential", 42 | "eslint:recommended", 43 | "@vue/prettier" 44 | ], 45 | "parserOptions": { 46 | "parser": "babel-eslint" 47 | }, 48 | "rules": { 49 | "prettier/prettier": [ 50 | "warn", 51 | { 52 | "htmlWhitespaceSensitivity": "ignore", 53 | "printWidth": 120, 54 | "tabWidth": 2, 55 | "arrowParens": "avoid", 56 | "trailingComma": "all", 57 | "prettier": "^1.19.1" 58 | } 59 | ] 60 | } 61 | }, 62 | "browserslist": [ 63 | "> 1%", 64 | "last 2 versions", 65 | "not dead" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/service/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from "@/router"; 3 | import { Notification } from "element-ui"; 4 | import { isJson } from "@/utils/commonMethods"; 5 | 6 | const clearCache = backCode => { 7 | let msg = backCode === 401 ? "登录失效,请重新登录" : "暂无权限,请重新登录"; 8 | Notification({ 9 | title: "请求失败", 10 | message: msg, 11 | type: "error", 12 | }); 13 | localStorage.clear(); 14 | router.push("/login"); 15 | }; 16 | 17 | export const axiosInstance = axios.create({ 18 | baseURL: "/api", 19 | timeout: 30000, 20 | responseType: "json", 21 | withCredentials: true, 22 | headers: { 23 | "content-type": "application/json;charset=UTF-8", 24 | }, 25 | }); 26 | 27 | axiosInstance.interceptors.request.use(config => { 28 | if (config.headers.Authorization !== "none") { 29 | const token = localStorage.getItem("token"); 30 | if (token) { 31 | config.headers.common["Authorization"] = "Bearer " + token; 32 | } else { 33 | clearCache(401); 34 | } 35 | } else { 36 | delete config.headers.Authorization; 37 | } 38 | return config; 39 | }); 40 | 41 | axiosInstance.interceptors.response.use( 42 | res => { 43 | return res; 44 | }, 45 | err => { 46 | if (err.response) { 47 | let { 48 | response: { status, data }, 49 | } = err; 50 | data = data || {}; 51 | if ([401, 403].includes(status)) { 52 | setTimeout(() => { 53 | clearCache(status); 54 | }, 1000); 55 | } else { 56 | let msg = data.message || data.msg; 57 | if (isJson(data) && JSON.stringify(data) !== "{}") { 58 | msg = msg || data; 59 | } 60 | Notification({ 61 | title: "提示", 62 | type: "error", 63 | message: msg || "服务器内部错误", 64 | }); 65 | } 66 | } 67 | return Promise.reject(err); 68 | }, 69 | ); 70 | 71 | const install = Vue => { 72 | Object.defineProperty(Vue.prototype, "$http", { value: axiosInstance }); 73 | }; 74 | 75 | export default install; 76 | -------------------------------------------------------------------------------- /src/components/NavLeft.vue: -------------------------------------------------------------------------------- 1 | 38 | 67 | 79 | -------------------------------------------------------------------------------- /src/views/teprunner/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 66 | -------------------------------------------------------------------------------- /src/utils/commonMethods.js: -------------------------------------------------------------------------------- 1 | import { Notification, MessageBox } from "element-ui"; 2 | 3 | export function isType(type) { 4 | return function(obj) { 5 | return Object.prototype.toString.call(obj) === `[object ${type}]`; 6 | }; 7 | } 8 | 9 | export function isJson(data) { 10 | return ( 11 | typeof data == "object" && Object.prototype.toString.call(data).toLowerCase() === "[object object]" && !data.length 12 | ); 13 | } 14 | 15 | export function filterNullValue(data) { 16 | if (isType("Object")(data)) { 17 | let resultObj = {}; 18 | Object.entries(data).forEach(([key, value]) => { 19 | if (value) { 20 | resultObj[key] = value; 21 | } 22 | }); 23 | return resultObj; 24 | } else if (isType("Array")(data)) { 25 | return data.filter(value => value); 26 | } 27 | } 28 | 29 | export function onResponse({ status, statusText, data }, callback) { 30 | if ([200, 201, 204].includes(status)) { 31 | callback(data); 32 | } else { 33 | Notification({ 34 | title: "提示", 35 | type: "error", 36 | message: `${status} ${statusText}`, 37 | }); 38 | } 39 | } 40 | 41 | export function delConfirm(message, confirmCallback, options = {}) { 42 | const config = Object.assign( 43 | {}, 44 | { 45 | confirmButtonText: "删除", 46 | cancelButtonText: "取消", 47 | confirmButtonClass: "el-button--danger", 48 | type: "warning", 49 | }, 50 | options, 51 | ); 52 | 53 | if (isType("Function")(confirmCallback)) { 54 | MessageBox.confirm(message, "提示", config) 55 | .then(confirmCallback) 56 | .catch(() => {}); 57 | } else { 58 | MessageBox.confirm(message, "提示", config).catch(() => {}); 59 | } 60 | } 61 | 62 | export function notifyMessage(message, options = {}) { 63 | Notification({ 64 | title: "提示", 65 | type: "error", 66 | message, 67 | ...options, 68 | }); 69 | } 70 | 71 | export function resultColor(res) { 72 | if (res.indexOf("error") !== -1) { 73 | return "red"; 74 | } else if (res.indexOf("failed") !== -1) { 75 | return "rgb(236,76,71)"; 76 | } else if (res.indexOf("passed") !== -1) { 77 | return "rgb(0,153,117)"; 78 | } 79 | } 80 | 81 | export function isProjectExisted() { 82 | let projectEnvList = JSON.parse(localStorage.getItem("projectEnvList")); 83 | return !!projectEnvList.length; 84 | } 85 | 86 | export function trimStr(str) { 87 | return str.replace(/(^\s*)|(\s*$)/g, ""); 88 | } 89 | -------------------------------------------------------------------------------- /src/views/teprunner/case/CaseView.vue: -------------------------------------------------------------------------------- 1 | 33 | 76 | 86 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.js: -------------------------------------------------------------------------------- 1 | !(function(a) { 2 | var e, 3 | d = 4 | '', 5 | t = (e = document.getElementsByTagName("script"))[e.length - 1].getAttribute("data-injectcss"); 6 | if (t && !a.__iconfont__svg__cssinject__) { 7 | a.__iconfont__svg__cssinject__ = !0; 8 | try { 9 | document.write( 10 | "", 11 | ); 12 | } catch (e) { 13 | console && console.log(e); 14 | } 15 | } 16 | !(function(e) { 17 | if (document.addEventListener) 18 | if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) setTimeout(e, 0); 19 | else { 20 | var t = function() { 21 | document.removeEventListener("DOMContentLoaded", t, !1), e(); 22 | }; 23 | document.addEventListener("DOMContentLoaded", t, !1); 24 | } 25 | else 26 | document.attachEvent && 27 | ((n = e), 28 | (o = a.document), 29 | (i = !1), 30 | (c = function() { 31 | i || ((i = !0), n()); 32 | }), 33 | (d = function() { 34 | try { 35 | o.documentElement.doScroll("left"); 36 | } catch (e) { 37 | return void setTimeout(d, 50); 38 | } 39 | c(); 40 | })(), 41 | (o.onreadystatechange = function() { 42 | "complete" === o.readyState && ((o.onreadystatechange = null), c()); 43 | })); 44 | var n, o, i, c, d; 45 | })(function() { 46 | var e, t, n, o, i, c; 47 | ((e = document.createElement("div")).innerHTML = d), 48 | (d = null), 49 | (t = e.getElementsByTagName("svg")[0]) && 50 | (t.setAttribute("aria-hidden", "true"), 51 | (t.style.position = "absolute"), 52 | (t.style.width = 0), 53 | (t.style.height = 0), 54 | (t.style.overflow = "hidden"), 55 | (n = t), 56 | (o = document.body).firstChild 57 | ? ((i = n), (c = o.firstChild).parentNode.insertBefore(i, c)) 58 | : o.appendChild(n)); 59 | }); 60 | })(window); 61 | -------------------------------------------------------------------------------- /src/views/teprunner/case/WriteDown.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 98 | 99 | 115 | -------------------------------------------------------------------------------- /src/components/ProjectEnv.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/views/teprunner/AddEnvVar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 124 | 125 | 132 | -------------------------------------------------------------------------------- /src/views/teprunner/plan/CaseResult.vue: -------------------------------------------------------------------------------- 1 | 49 | 98 | 160 | -------------------------------------------------------------------------------- /src/views/teprunner/FixtureEditor.vue: -------------------------------------------------------------------------------- 1 | 49 | 144 | 154 | -------------------------------------------------------------------------------- /src/views/teprunner/case/CaseEditor.vue: -------------------------------------------------------------------------------- 1 | 38 | 145 | 155 | -------------------------------------------------------------------------------- /src/views/console/ProjectManagement.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 149 | -------------------------------------------------------------------------------- /src/views/console/AddProject.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 148 | 149 | 156 | -------------------------------------------------------------------------------- /src/views/teprunner/Fixture.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/views/teprunner/EnvVar.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 141 | 142 | 210 | -------------------------------------------------------------------------------- /src/views/teprunner/Grammar.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 216 | 217 | 223 | -------------------------------------------------------------------------------- /src/views/teprunner/plan/PlanResult.vue: -------------------------------------------------------------------------------- 1 | 90 | 166 | 220 | -------------------------------------------------------------------------------- /src/views/teprunner/GitSync.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 178 | 179 | 224 | -------------------------------------------------------------------------------- /src/views/teprunner/plan/PlanEditor.vue: -------------------------------------------------------------------------------- 1 | 76 | 185 | 195 | -------------------------------------------------------------------------------- /src/views/console/userManagement.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 200 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import login from "@/views/login"; 4 | import home from "@/views/home"; 5 | import console from "@/views/console"; 6 | import teprunner from "@/views/teprunner"; 7 | 8 | Vue.use(VueRouter); 9 | 10 | const routes = [ 11 | { 12 | path: "/login", 13 | meta: { 14 | title: "测试平台登录", 15 | }, 16 | component: login, 17 | }, 18 | { 19 | path: "/", 20 | name: "home", 21 | meta: { 22 | requireAuth: true, 23 | }, 24 | component: home, 25 | redirect: "teprunner/grammar", 26 | children: [ 27 | { 28 | path: "teprunner", 29 | name: "teprunner", 30 | meta: { 31 | title: "接口自动化", 32 | }, 33 | component: teprunner, 34 | redirect: "teprunner/grammar", 35 | children: [ 36 | { 37 | path: "grammar", 38 | meta: { 39 | title: "语法说明", 40 | }, 41 | component: () => import("@/views/teprunner/Grammar.vue"), 42 | }, 43 | { 44 | path: "envVar", 45 | meta: { 46 | title: "环境变量", 47 | }, 48 | component: () => import("@/views/teprunner/EnvVar.vue"), 49 | }, 50 | { 51 | path: "fixture", 52 | name: "fixture", 53 | meta: { 54 | title: "fixtures", 55 | }, 56 | component: () => import("@/views/teprunner/Fixture.vue"), 57 | children: [ 58 | { 59 | path: "addFixture", 60 | name: "addFixture", 61 | meta: { 62 | title: "新增 fixture", 63 | }, 64 | component: () => import("@/views/teprunner/FixtureEditor"), 65 | }, 66 | { 67 | path: "editFixture", 68 | name: "editFixture", 69 | meta: { 70 | title: "编辑 fixture", 71 | }, 72 | component: () => import("@/views/teprunner/FixtureEditor"), 73 | }, 74 | ], 75 | }, 76 | { 77 | path: "case", 78 | name: "case", 79 | meta: { 80 | title: "用例管理", 81 | }, 82 | component: () => import("@/views/teprunner/case/CaseManagement.vue"), 83 | children: [ 84 | { 85 | path: "addCase", 86 | name: "addCase", 87 | meta: { 88 | title: "新增用例", 89 | }, 90 | component: () => import("@/views/teprunner/case/CaseEditor"), 91 | }, 92 | { 93 | path: "editCase", 94 | name: "editCase", 95 | meta: { 96 | title: "编辑用例", 97 | }, 98 | component: () => import("@/views/teprunner/case/CaseEditor"), 99 | }, 100 | { 101 | path: "caseView", 102 | name: "caseView", 103 | meta: { 104 | title: "查看用例", 105 | }, 106 | component: () => import("@/views/teprunner/case/CaseView"), 107 | }, 108 | { 109 | path: "caseResult", 110 | name: "case.caseResult", 111 | meta: { 112 | title: "用例运行结果", 113 | }, 114 | component: () => import("@/views/teprunner/case/CaseResult"), 115 | }, 116 | ], 117 | }, 118 | { 119 | path: "plan", 120 | name: "plan", 121 | meta: { 122 | title: "测试计划", 123 | }, 124 | component: () => import("@/views/teprunner/plan/PlanManagement.vue"), 125 | children: [ 126 | { 127 | path: "addPlan", 128 | name: "addPlan", 129 | meta: { 130 | title: "添加计划", 131 | }, 132 | component: () => import("@/views/teprunner/plan/PlanEditor"), 133 | }, 134 | { 135 | path: "editPlan", 136 | name: "editPlan", 137 | meta: { 138 | title: "编辑计划", 139 | }, 140 | component: () => import("@/views/teprunner/plan/PlanEditor"), 141 | }, 142 | { 143 | path: "caseList", 144 | name: "caseList", 145 | meta: { 146 | title: "用例列表", 147 | }, 148 | component: () => import("@/views/teprunner/plan/CaseList"), 149 | }, 150 | { 151 | path: "planResult", 152 | name: "planResult", 153 | meta: { 154 | title: "计划运行结果", 155 | }, 156 | component: () => import("@/views/teprunner/plan/PlanResult"), 157 | children: [ 158 | { 159 | path: "caseResult", 160 | name: "plan.caseResult", 161 | meta: { 162 | title: "用例运行结果", 163 | }, 164 | component: () => import("@/views/teprunner/plan/CaseResult"), 165 | }, 166 | ], 167 | }, 168 | ], 169 | }, 170 | { 171 | path: "gitSync", 172 | meta: { 173 | title: "Git同步", 174 | }, 175 | component: () => import("@/views/teprunner/GitSync.vue"), 176 | }, 177 | ], 178 | }, 179 | { 180 | path: "console", 181 | name: "console", 182 | meta: { 183 | title: "后台管理", 184 | }, 185 | component: console, 186 | redirect: "console/userManagement", 187 | children: [ 188 | { 189 | path: "userManagement", 190 | meta: { 191 | title: "用户管理", 192 | }, 193 | component: () => import("@/views/console/userManagement.vue"), 194 | }, 195 | { 196 | path: "projectManagement", 197 | meta: { 198 | title: "项目管理", 199 | }, 200 | component: () => import("@/views/console/ProjectManagement.vue"), 201 | }, 202 | ], 203 | }, 204 | ], 205 | }, 206 | ]; 207 | 208 | const router = new VueRouter({ 209 | routes, 210 | }); 211 | 212 | router.beforeEach((to, from, next) => { 213 | if (to.matched.some(auth => auth.meta.requireAuth)) { 214 | let token = localStorage.getItem("token"); 215 | if (token) { 216 | next(); 217 | } else { 218 | next({ 219 | path: "/login", 220 | }); 221 | } 222 | } else { 223 | next(); 224 | } 225 | }); 226 | export default router; 227 | -------------------------------------------------------------------------------- /src/views/teprunner/case/CaseResult.vue: -------------------------------------------------------------------------------- 1 | 50 | 174 | 236 | -------------------------------------------------------------------------------- /src/assets/css/common.scss: -------------------------------------------------------------------------------- 1 | html { 2 | background: #f2f2f4; 3 | } 4 | .self-left { 5 | float: left; 6 | } 7 | .self-right { 8 | float: right; 9 | } 10 | .clear::after { 11 | content: ""; 12 | display: block; 13 | clear: both; 14 | } 15 | 16 | .control-list { 17 | margin-bottom: 24px; 18 | } 19 | 20 | .content-info { 21 | background: #fff; 22 | padding-top: 16px; 23 | border-radius: 4px; 24 | overflow: hidden; 25 | .content-header { 26 | padding-left: 24px; 27 | padding-right: 24px; 28 | .info-name { 29 | padding-left: 12px; 30 | font-weight: 600; 31 | font-size: 16px; 32 | color: rgba(0, 0, 0, 0.85); 33 | line-height: 40px; 34 | position: relative; 35 | &::after { 36 | content: ""; 37 | display: block; 38 | width: 4px; 39 | height: 16px; 40 | background: #4fda5e; 41 | position: absolute; 42 | left: 0; 43 | top: 50%; 44 | margin-top: -8px; 45 | } 46 | } 47 | } 48 | } 49 | 50 | .content-table { 51 | // margin: 16px 20px; 52 | } 53 | 54 | .content-footer { 55 | height: 64px; 56 | line-height: 64px; 57 | border-top: 1px solid #e6e6ea; 58 | padding: 0 24px; 59 | .el-button + .el-button { 60 | margin-left: 16px; 61 | } 62 | .page-list { 63 | & > div { 64 | height: 28px; 65 | margin-top: 18px; 66 | } 67 | } 68 | .el-pagination { 69 | font-weight: 100 !important; 70 | color: #606266; 71 | } 72 | } 73 | 74 | .td-pop { 75 | p { 76 | margin-bottom: 23px; 77 | } 78 | } 79 | 80 | .title-search-form { 81 | /deep/.el-form-item { 82 | margin-bottom: 16px; 83 | } 84 | } 85 | 86 | .search-form { 87 | padding-left: 24px; 88 | padding-right: 24px; 89 | .el-button + .el-button { 90 | margin-left: 16px; 91 | } 92 | + .content-table { 93 | margin-top: 0; 94 | } 95 | margin-top: 16px; 96 | /deep/.el-form-item { 97 | margin-right: 16px; 98 | } 99 | /deep/ .el-form-item { 100 | margin-bottom: 16px; 101 | } 102 | /deep/ .custom-size .el-form-item__content, 103 | .custom-size .el-select, 104 | .custom-size .el-autocomplete, 105 | .custom-size .el-select > .el-input { 106 | width: 224px; 107 | } 108 | } 109 | 110 | .person-name { 111 | font-size: 14px; 112 | line-height: 32px; 113 | margin-left: 8px; 114 | } 115 | .has-tag { 116 | margin-bottom: 0; 117 | .choose-person-btn.el-button { 118 | height: 40px; 119 | margin-bottom: 24px; 120 | overflow: hidden; 121 | } 122 | .el-tag { 123 | height: 40px; 124 | line-height: 40px; 125 | margin-right: 16px; 126 | // margin-bottom: 16px; 127 | font-size: 14px; 128 | .el-icon-close { 129 | top: 0; 130 | } 131 | } 132 | } 133 | 134 | .person-list { 135 | margin-bottom: 0; 136 | .el-tag { 137 | font-size: 0; 138 | margin-bottom: 5px; 139 | .el-icon-close { 140 | top: -4px; 141 | } 142 | .tag-name { 143 | font-size: 14px; 144 | } 145 | } 146 | } 147 | 148 | .input-number { 149 | width: 100px; 150 | margin-right: 10px; 151 | } 152 | 153 | .flex-center { 154 | display: flex; 155 | align-items: center; 156 | } 157 | 158 | .input-380 { 159 | width: 380px !important; 160 | } 161 | 162 | .el-table__header th { 163 | color: rgba(0, 0, 0, 0.65); 164 | } 165 | 166 | .pri-add-btn { 167 | .el-icon-circle-plus { 168 | margin-right: -5px; 169 | font-size: 14px; 170 | } 171 | } 172 | 173 | $--color-danger: #fd4848; 174 | $--color-info: #0091ff; 175 | $--color-primary: #3642ff; 176 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 177 | @import "~element-ui/packages/theme-chalk/src/index"; 178 | 179 | .el-button--danger.is-plain, 180 | .el-button--info.is-plain { 181 | background: #fff; 182 | } 183 | 184 | .el-menu-vertical-demo:not(.el-menu--collapse) { 185 | width: 250px; 186 | } 187 | 188 | .el-submenu__title { 189 | height: 64px; 190 | line-height: 64px; 191 | } 192 | .el-menu-item:hover, 193 | .el-submenu__title:hover { 194 | outline: none; 195 | background: rgba(54, 66, 255, 0.04) !important; 196 | } 197 | 198 | .el-menu-item.is-active { 199 | background: linear-gradient(270deg, rgba(54, 66, 255, 0.08) 0%, rgba(54, 66, 255, 0) 100%); 200 | position: relative; 201 | &::after { 202 | content: ""; 203 | display: block; 204 | position: absolute; 205 | top: 8px; 206 | right: 0; 207 | width: 4px; 208 | height: 32px; 209 | background: rgba(54, 66, 255, 1); 210 | } 211 | } 212 | 213 | .el-table-column--selection .cell { 214 | padding-left: 10px !important; 215 | padding-right: 10px !important; 216 | } 217 | 218 | .el-form-item__label { 219 | padding-right: 16px; 220 | } 221 | 222 | .el-form-item { 223 | margin-bottom: 24px; 224 | } 225 | 226 | .el-breadcrumb__inner.is-link { 227 | color: $--color-text-regular; 228 | font-weight: normal; 229 | } 230 | 231 | .el-breadcrumb__item:last-child .el-breadcrumb__inner { 232 | color: $--color-text-primary; 233 | font-weight: bold; 234 | } 235 | 236 | .el-breadcrumb__item:last-child .el-breadcrumb__inner:hover { 237 | color: $--color-text-primary; 238 | font-weight: bold; 239 | } 240 | 241 | .el-table { 242 | .el-table__header-wrapper { 243 | background: rgba(144, 147, 153, 0.06); 244 | } 245 | th { 246 | padding: 0 !important; 247 | height: 64px; 248 | line-height: 64px; 249 | &:first-child { 250 | padding-left: 14px !important; 251 | } 252 | } 253 | td { 254 | height: 48px; 255 | line-height: 48px; 256 | padding: 0 !important; 257 | border-bottom: 1px solid #e6e6ea; 258 | &:first-child { 259 | border-bottom: 0 !important; 260 | padding-left: 14px !important; 261 | position: relative; 262 | &::after { 263 | content: ""; 264 | display: block; 265 | position: absolute; 266 | bottom: 0; 267 | left: 24px; 268 | right: 0; 269 | height: 0; 270 | z-index: 1; 271 | border-bottom: 1px solid #e6e6ea; 272 | } 273 | } 274 | &:last-child { 275 | border-bottom: 0 !important; 276 | position: relative; 277 | &::after { 278 | content: ""; 279 | display: block; 280 | position: absolute; 281 | bottom: 0; 282 | left: 0; 283 | right: 24px; 284 | height: 0; 285 | z-index: 1; 286 | border-bottom: 1px solid #e6e6ea; 287 | } 288 | } 289 | } 290 | tr:last-child { 291 | td:first-child, 292 | td:last-child { 293 | &::after { 294 | border-bottom: 0; 295 | } 296 | } 297 | } 298 | th.is-leaf { 299 | border-bottom: 0 !important; 300 | } 301 | &::before { 302 | background-color: #fff; 303 | } 304 | tbody { 305 | position: relative; 306 | &::after { 307 | content: ""; 308 | display: block; 309 | position: absolute; 310 | bottom: 0; 311 | left: 24px; 312 | right: 24px; 313 | height: 0; 314 | z-index: 1; 315 | // border-bottom: 1px solid #e6e6ea; 316 | } 317 | } 318 | .el-table__fixed-right::before { 319 | background: #fff; 320 | } 321 | } 322 | 323 | .el-dialog__body { 324 | padding: 24px 24px 0; 325 | } 326 | 327 | .el-textarea__inner { 328 | padding: 8px 15px; 329 | font-size: 14px; 330 | } 331 | 332 | .el-textarea textarea::-webkit-input-placeholder { 333 | font-size: 14px; 334 | color: rgba(192, 196, 204, 1); 335 | } 336 | 337 | .cursor-pointer { 338 | cursor: pointer; 339 | } 340 | 341 | .flex-center { 342 | display: flex; 343 | align-items: center; 344 | } 345 | 346 | .text-ellipsis { 347 | overflow: hidden; 348 | text-overflow: ellipsis; 349 | white-space: nowrap; 350 | } 351 | 352 | .el-tooltip__popper { 353 | max-width: 95%; 354 | line-height: 16px; 355 | } 356 | -------------------------------------------------------------------------------- /src/components/SelectionPanel.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 239 | 298 | -------------------------------------------------------------------------------- /src/views/console/addUser.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 268 | 269 | 276 | -------------------------------------------------------------------------------- /src/views/teprunner/plan/CaseList.vue: -------------------------------------------------------------------------------- 1 | 103 | 293 | 362 | -------------------------------------------------------------------------------- /src/views/teprunner/plan/PlanManagement.vue: -------------------------------------------------------------------------------- 1 | 173 | 336 | 344 | -------------------------------------------------------------------------------- /src/views/teprunner/case/CaseManagement.vue: -------------------------------------------------------------------------------- 1 | 157 | 158 | 361 | 362 | 363 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 114 | 257 | 322 | 491 | --------------------------------------------------------------------------------