feat: 高级参数设置

This commit is contained in:
Captain.B 2020-08-06 18:01:16 +08:00
parent e8c682e402
commit 00ef9b7db9
7 changed files with 473 additions and 77 deletions

View File

@ -25,7 +25,11 @@
"vue-router": "^3.1.3",
"vuedraggable": "^2.23.2",
"vuex": "^3.1.2",
"vue-calendar-heatmap": "^0.8.4"
"vue-calendar-heatmap": "^0.8.4",
"mockjs": "^1.1.0",
"md5": "^2.3.0",
"sha.js": "^2.4.11",
"js-base64": "^3.4.4"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",

View File

@ -1,7 +1,7 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{description}}
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in items" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
@ -15,8 +15,18 @@
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:placeholder="valueText" show-word-limit/>
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="item.value"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
<i slot="suffix" class="el-input__icon el-icon-edit" style="cursor: pointer;" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
@ -24,96 +34,211 @@
</el-col>
</el-row>
</div>
<el-dialog :title="$t('api_test.request.parameters_advance')"
:visible.sync="itemValueVisible"
class="advanced-item-value"
width="50%">
<el-form>
<el-form-item>
<el-input :autosize="{ minRows: 2, maxRows: 4}" type="textarea" :placeholder="valueText"
v-model="itemValue"/>
</el-form-item>
</el-form>
<div>
<el-row type="flex" align="middle">
<el-col :span="3">
<el-button class="save-button" type="success" plain @click="showPreview(itemValue)">
{{ $t('api_test.request.parameters_preview') }}
</el-button>
</el-col>
<el-col>
<div> {{ itemValuePreview }}</div>
</el-col>
</el-row>
</div>
<div class="format-tip">
<div>
<p>{{ $t('api_test.request.parameters_filter') }}
<el-tag size="mini" v-for="func in funcs" :key="func" @click="appendFunc(func)"
style="margin-left: 2px;cursor: pointer;">
<span>{{ func }}</span>
</el-tag>
</p>
</div>
<div>
<span>{{ $t('api_test.request.parameters_filter_desc') }}
<el-link href="http://mockjs.com/examples.html" target="_blank">http://mockjs.com/examples.html</el-link>
</span>
<p>{{ $t('api_test.request.parameters_filter_example') }}@string(10) | md5 | substr: 1, 3</p>
<p>{{ $t('api_test.request.parameters_filter_example') }}@integer(1, 5) | concat:_metersphere</p>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import {KeyValue} from "../model/ScenarioModel";
import {KeyValue} from "../model/ScenarioModel";
import {MOCKJS_FUNC} from "@/common/js/constants";
import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter";
export default {
name: "MsApiKeyValue",
export default {
name: "MsApiKeyValue",
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
data() {
return {
itemValueVisible: false,
itemValue: null,
funcs: ["md5", "sha1", "sha224", "sha256", "sha384", "sha512", "base64",
"unbase64", "substr", "concat", "lconcat", "lower", "upper", "length", "number"],
itemValuePreview: null
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
methods: {
remove: function (index) {
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
});
if (isNeedCreate) {
this.items.push(new KeyValue());
//
isNeedCreate = false;
}
this.$emit('change', this.items);
// TODO key
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
},
created() {
if (this.items.length === 0) {
});
if (isNeedCreate) {
this.items.push(new KeyValue());
}
this.$emit('change', this.items);
// TODO key
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC;
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
showPreview(itemValue) {
if (!itemValue) {
return;
}
let funcs = itemValue.split("|");
let value = Mock.mock(funcs[0].trim());
if (funcs.length === 1) {
this.itemValuePreview = value;
return value;
}
for (let i = 1; i < funcs.length; i++) {
let func = funcs[i].trim();
let args = func.split(":");
let strings = [];
if (args[1]) {
strings = args[1].split(",");
}
value = funcFilters[args[0].trim()](value, ...strings);
}
this.itemValuePreview = value;
return value;
},
appendFunc(func) {
if (this.itemValue) {
this.itemValue += " | " + func;
} else {
this.$warning(this.$t("api_test.request.parameters_preview_warning"));
}
},
advanced(item) {
this.itemValueVisible = true;
this.itemValue = item.value;
},
},
created() {
if (this.items.length === 0) {
this.items.push(new KeyValue());
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-delete {
width: 60px;
}
.el-autocomplete {
width: 100%;
}
.el-autocomplete {
width: 100%;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.format-tip {
background: #EDEDED;
}
.format-tip {
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style>

View File

@ -57,3 +57,57 @@ export const REQUEST_HEADERS = [
{value: 'Via'},
{value: 'Warning'}
]
export const MOCKJS_FUNC = [
{name: '@boolean'},
{name: '@natural'},
{name: '@integer'},
{name: '@float'},
{name: '@character'},
{name: '@string'},
{name: '@range'},
{name: '@date'},
{name: '@time'},
{name: '@datetime'},
{name: '@now'},
{name: '@img'},
{name: '@dataImage'},
{name: '@color'},
{name: '@hex'},
{name: '@rgb'},
{name: '@rgba'},
{name: '@hsl'},
{name: '@paragraph'},
{name: '@sentence'},
{name: '@word'},
{name: '@title'},
{name: '@cparagraph'},
{name: '@csentence'},
{name: '@cword'},
{name: '@ctitle'},
{name: '@first'},
{name: '@last'},
{name: '@name'},
{name: '@cfirst'},
{name: '@clast'},
{name: '@cname'},
{name: '@url'},
{name: '@domain'},
{name: '@protocol'},
{name: '@tld'},
{name: '@email'},
{name: '@ip'},
{name: '@region'},
{name: '@province'},
{name: '@city'},
{name: '@county'},
{name: '@zip'},
{name: '@capitalize'},
{name: '@upper'},
{name: '@lower'},
{name: '@pick'},
{name: '@shuffle'},
{name: '@guid'},
{name: '@id'},
{name: '@increment'}
]

View File

@ -0,0 +1,199 @@
const aUniqueVerticalStringNotFoundInData = '___UNIQUE_VERTICAL___';
const aUniqueCommaStringNotFoundInData = '___UNIQUE_COMMA___';
const segmentSeparateChar = '|';
const methodAndArgsSeparateChar = ':';
const argsSeparateChar = ',';
const md5 = require('md5');
const sha = require('sha.js');
const Base64 = require('js-base64').Base64;
export const funcFilters = {
md5: function (str) {
return md5(str);
},
sha: function (str, arg) {
return sha(arg)
.update(str)
.digest('hex');
},
/**
* type: sha1 sha224 sha256 sha384 sha512
*/
sha1: function (str) {
return sha('sha1')
.update(str)
.digest('hex');
},
sha224: function (str) {
return sha('sha224')
.update(str)
.digest('hex');
},
sha256: function (str) {
return sha('sha256')
.update(str)
.digest('hex');
},
sha384: function (str) {
return sha('sha384')
.update(str)
.digest('hex');
},
sha512: function (str) {
return sha('sha512')
.update(str)
.digest('hex');
},
base64: function (str) {
return Base64.encode(str);
},
unbase64: function (str) {
return Base64.decode(str);
},
substr: function (str, ...args) {
return str.substr(...args);
},
concat: function (str, ...args) {
args.forEach(item => {
str += item;
});
return str;
},
lconcat: function (str, ...args) {
args.forEach(item => {
str = item + this._string;
});
return str;
},
lower: function (str) {
return str.toLowerCase();
},
upper: function (str) {
return str.toUpperCase();
},
length: function (str) {
return str.length;
},
number: function (str) {
return !isNaN(str) ? +str : str;
}
};
let handleValue = function (str) {
return str;
};
const _handleValue = function (str) {
if (str[0] === str[str.length - 1] && (str[0] === '"' || str[0] === "'")) {
str = str.substr(1, str.length - 2);
}
return handleValue(
str
.replace(new RegExp(aUniqueVerticalStringNotFoundInData, 'g'), segmentSeparateChar)
.replace(new RegExp(aUniqueCommaStringNotFoundInData, 'g'), argsSeparateChar)
);
};
class PowerString {
constructor(str) {
this._string = str;
}
toString() {
return this._string;
}
}
function addMethod(method, fn) {
PowerString.prototype[method] = function (...args) {
args.unshift(this._string + '');
this._string = fn.apply(this, args);
return this;
};
}
function importMethods(handles) {
for (let method in handles) {
addMethod(method, handles[method]);
}
}
importMethods(funcFilters);
function handleOriginStr(str, handleValueFn) {
if (!str) return str;
if (typeof handleValueFn === 'function') {
handleValue = handleValueFn;
}
str = str
.replace('\\' + segmentSeparateChar, aUniqueVerticalStringNotFoundInData)
.replace('\\' + argsSeparateChar, aUniqueCommaStringNotFoundInData)
.split(segmentSeparateChar)
.map(handleSegment)
.reduce(execute, null)
.toString();
return str;
}
function execute(str, curItem, index) {
if (index === 0) {
return new PowerString(curItem);
}
return str[curItem.method].apply(str, curItem.args);
}
function handleSegment(str, index) {
str = str.trim();
if (index === 0) {
return _handleValue(str);
}
let method,
args = [];
if (str.indexOf(methodAndArgsSeparateChar) > 0) {
str = str.split(methodAndArgsSeparateChar);
method = str[0].trim();
args = str[1].split(argsSeparateChar).map(item => _handleValue(item.trim()));
} else {
method = str;
}
if (typeof funcFilters[method] !== 'function') {
throw new Error(`This method name(${method}) is not exist.`);
}
return {
method,
args
};
}
// module.exports = {
// utils: stringHandles,
// PowerString,
// /**
// * 类似于 angularJs的 filter 功能
// * @params string
// * @params fn 处理参数值函数,默认是一个返回原有参数值函数
// *
// * @expamle
// * filter('string | substr: 1, 10 | md5 | concat: hello ')
// */
// filter: handleOriginStr
// };

View File

@ -386,6 +386,10 @@ export default {
url_description: "etc: https://fit2cloud.com",
path_description: "etc/login",
parameters: "Query parameters",
parameters_filter_example: "Example",
parameters_advance: "Advanced parameter settings",
parameters_preview: "Preview",
parameters_preview_warning: "Please enter the template first",
parameters_desc: "Parameters will be appended to the URL e.g. https://fit2cloud.com?Name=Value&Name2=Value2",
headers: "Headers",
body: "Body",

View File

@ -384,6 +384,12 @@ export default {
path_description: "例如:/login",
url_invalid: "URL无效",
parameters: "请求参数",
parameters_filter: "内置函数",
parameters_filter_desc: "使用方法",
parameters_filter_example: "示例",
parameters_advance: "高级参数设置",
parameters_preview: "预览",
parameters_preview_warning: "请先输入模版",
parameters_desc: "参数追加到URL例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
headers: "请求头",
body: "请求内容",
@ -493,9 +499,9 @@ export default {
actual_result: ": 实际结果为空",
case: {
input_test_case:'请输入关联用例名称',
test_name:'测试名称',
other:"--其他--",
input_test_case: '请输入关联用例名称',
test_name: '测试名称',
other: "--其他--",
test_case: "测试用例",
move: "移动用例",
case_list: "用例列表",

View File

@ -385,6 +385,10 @@ export default {
path_description: "例如:/login",
url_invalid: "URL無效",
parameters: "請求參數",
parameters_filter_example: "示例",
parameters_advance: "高級參數設置",
parameters_preview: "預覽",
parameters_preview_warning: "請先輸入模版",
parameters_desc: "參數追加到URL,例如https://fit2cloud.com/entrieskey1=Value1&amp;Key2=Value2",
headers: "請求頭",
body: "請求內容",