<template>
|
<div class="loading-container">
|
<div v-loading="loading">
|
<div>
|
<el-dialog
|
title="全局变量"
|
:visible.sync="showGlobalVariables"
|
width="60%"
|
>
|
<el-input
|
v-model="searchText"
|
placeholder="请输入变量名、变量值或描述进行搜索"
|
style="margin-bottom: 20px"
|
clearable
|
></el-input>
|
<el-table
|
:data="filteredVariables"
|
height="400"
|
border
|
style="width: 100%"
|
>
|
<el-table-column
|
prop="key"
|
label="变量名"
|
width="180"
|
></el-table-column>
|
<el-table-column
|
prop="value"
|
label="变量值"
|
></el-table-column>
|
<el-table-column
|
prop="description"
|
label="描述"
|
></el-table-column>
|
<el-table-column label="操作" width="120">
|
<template slot-scope="scope">
|
<el-button
|
type="primary"
|
icon="el-icon-document-copy"
|
title="复制变量名"
|
circle
|
size="mini"
|
@click="copyVariableName(scope.row.key)"
|
></el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-dialog>
|
<div style="display: flex;">
|
<el-input
|
clearable
|
style="width: 600px"
|
placeholder="请输入接口名称"
|
v-model="name"
|
>
|
<template slot="prepend">接口信息录入</template>
|
</el-input>
|
<el-button
|
style="margin-left: 10px"
|
type="primary"
|
@click="reverseStatus"
|
:loading="loading"
|
:disabled="loading"
|
>发送</el-button
|
>
|
<el-button
|
slot="append"
|
type="primary"
|
:title="
|
userName === creator || isSuperuser || !isSaveAs
|
? '保存'
|
: '只有创建者才能修改'
|
"
|
:disabled="
|
userName !== creator && !isSuperuser && isSaveAs
|
"
|
@click="save = !save"
|
>保存</el-button
|
>
|
<el-button
|
slot="append"
|
type="success"
|
:title="'另存为'"
|
@click="handleSaveAs"
|
>另存为</el-button
|
>
|
<el-button
|
slot="append"
|
type="info"
|
icon="el-icon-view"
|
@click="showGlobalVariables = true"
|
>全局变量</el-button
|
>
|
</div>
|
<div>
|
<el-input
|
style="width: 760px; margin-top: 10px"
|
placeholder="请输入接口路径或完整的接口地址"
|
v-model.trim="url"
|
clearable
|
>
|
<el-select
|
style="width: 100px"
|
slot="prepend"
|
v-model="method"
|
>
|
<el-option
|
v-for="item of httpOptions"
|
:label="item.label"
|
:value="item.label"
|
:key="item.value"
|
></el-option>
|
</el-select>
|
<template slot="prepend">
|
<span style="margin-left: 20px">{{
|
config.base_url
|
}}</span>
|
</template>
|
</el-input>
|
|
<el-tooltip
|
effect="dark"
|
content="循环次数"
|
placement="bottom"
|
>
|
<el-input-number
|
v-model="times"
|
controls-position="right"
|
:min="1"
|
:max="100"
|
style="width: 120px"
|
></el-input-number>
|
</el-tooltip>
|
|
<div style="color: #409EFF; font-size: 15px; margin-top: 5px;">
|
引用函数方式为:${函数名称(a,b,c)},引用变量方式为:$变量名,可复合使用:${函数名称($a,$b,$c)}。例如:引用函数:${get_random_date(2025-01-01, 2025-04-01)},引用变量:$random_date,请注意格式!
|
</div>
|
|
</div>
|
</div>
|
<div class="request">
|
<el-dialog
|
v-if="dialogTableVisible"
|
:visible.sync="dialogTableVisible"
|
width="70%"
|
>
|
<report :summary="summary"></report>
|
</el-dialog>
|
|
<el-tabs style="margin-left: 20px" v-model="activeTag">
|
<el-tab-pane label="请求头" name="first">
|
<span slot="label">
|
请求头
|
<el-badge
|
slot="label"
|
:value="
|
handleBadgeValue(
|
response ? response.body.header : [],
|
'key'
|
)
|
"
|
></el-badge>
|
</span>
|
<headers
|
:save="save"
|
@header="handleHeader"
|
@dataChanged="handleHeaderDataChange"
|
:header="response ? response.body.header : []"
|
></headers>
|
</el-tab-pane>
|
|
<el-tab-pane label="请求体" name="second">
|
<request
|
:save="save"
|
:resetDataType="resetFlag"
|
@request="handleRequest"
|
@dataChanged="handleRequestDataChange"
|
:request="response ? response.body.request : []"
|
></request>
|
</el-tab-pane>
|
|
<el-tab-pane label="提取器" name="third">
|
<span slot="label">
|
提取器
|
<el-badge
|
slot="label"
|
:value="
|
handleBadgeValue(
|
response ? response.body.extract : [],
|
'key'
|
)
|
"
|
></el-badge>
|
</span>
|
<extract
|
:save="save"
|
@extract="handleExtract"
|
@dataChanged="handleExtractDataChange"
|
:extract="response ? response.body.extract : []"
|
></extract>
|
</el-tab-pane>
|
|
<el-tab-pane label="校验器" name="fourth">
|
<span slot="label">
|
校验器
|
<el-badge
|
slot="label"
|
:value="
|
handleBadgeValue(
|
response ? response.body.validate : [],
|
'actual'
|
)
|
"
|
></el-badge>
|
</span>
|
<validate
|
:save="save"
|
@validate="handleValidate"
|
@dataChanged="handleValidateDataChange"
|
:validate="response ? response.body.validate : []"
|
></validate>
|
</el-tab-pane>
|
|
<el-tab-pane label="变量" name="five">
|
<span slot="label">
|
变量
|
<el-badge
|
slot="label"
|
:value="
|
handleBadgeValue(
|
response ? response.body.variables : [],
|
'key'
|
)
|
"
|
></el-badge>
|
</span>
|
<variables
|
:save="save"
|
@variables="handleVariables"
|
@dataChanged="handleVariablesDataChange"
|
:variables="response ? response.body.variables : []"
|
></variables>
|
</el-tab-pane>
|
|
<el-tab-pane label="钩子函数" name="six">
|
<span slot="label">
|
钩子
|
<el-badge
|
slot="label"
|
:value="
|
handleHooksBadge(
|
response ? response.body.hooks : []
|
)
|
"
|
></el-badge>
|
</span>
|
<hooks
|
:save="save"
|
@hooks="handleHooks"
|
@dataChanged="handleHooksDataChange"
|
:hooks="response ? response.body.hooks : []"
|
></hooks>
|
</el-tab-pane>
|
</el-tabs>
|
</div>
|
</div>
|
<el-button
|
v-if="showCancel"
|
@click="cancelRequest"
|
class="custom-button"
|
size="mini"
|
>取消</el-button
|
>
|
</div>
|
</template>
|
|
<script>
|
import Headers from "@/pages/httprunner/components/Headers";
|
import Request from "@/pages/httprunner/components/Request";
|
import Extract from "@/pages/httprunner/components/Extract";
|
import Validate from "@/pages/httprunner/components/Validate";
|
import Variables from "@/pages/httprunner/components/Variables";
|
import { variablesList } from "@/restful/api";
|
import Hooks from "@/pages/httprunner/components/Hooks";
|
import Report from "@/pages/reports/DebugReport";
|
import axios from "axios";
|
import { isEqual } from "lodash";
|
|
export default {
|
name: "ApiBody",
|
components: {
|
Headers,
|
Request,
|
Extract,
|
Validate,
|
Variables,
|
Hooks,
|
Report
|
},
|
props: {
|
nodeId: {
|
required: false
|
},
|
project: {
|
required: false
|
},
|
config: {
|
required: false
|
},
|
response: {
|
required: false
|
},
|
isSaveAs: Boolean
|
},
|
data() {
|
return {
|
originalValues: {
|
name: "",
|
url: "",
|
method: "",
|
times: 1
|
},
|
isSuperuser: this.$store.state.is_superuser,
|
userName: this.$store.state.name,
|
loading: false,
|
showCancel: false, // 用于控制取消按钮的显示
|
times: 1,
|
name: "",
|
url: "",
|
id: "",
|
creator: "",
|
header: [],
|
request: [],
|
extract: [],
|
validate: [],
|
variables: [],
|
showGlobalVariables: false,
|
globalVariables: [],
|
searchText: '',
|
hooks: [],
|
method: "GET",
|
dialogTableVisible: false,
|
resetFlag: false, // 重置子组件dataType
|
save: false,
|
run: false,
|
summary: {},
|
activeTag: "second",
|
httpOptions: [
|
{
|
label: "GET",
|
value: 1
|
},
|
{
|
label: "POST",
|
value: 2
|
},
|
{
|
label: "PUT",
|
value: 3
|
},
|
{
|
label: "DELETE",
|
value: 4
|
},
|
{
|
label: "PATCH",
|
value: 5
|
},
|
{
|
label: "HEAD",
|
value: 6
|
},
|
{
|
label: "OPTIONS",
|
value: 7
|
}
|
]
|
};
|
},
|
watch: {
|
name() {
|
this.checkForChanges();
|
},
|
url() {
|
this.checkForChanges();
|
},
|
method() {
|
this.checkForChanges();
|
},
|
times() {
|
this.checkForChanges();
|
},
|
response: {
|
deep: true,
|
handler() {
|
this.setOriginalValues();
|
this.getGlobalVariables();
|
this.id = this.response.id;
|
this.name = this.response.body.name;
|
this.method = this.response.body.method;
|
this.url = this.response.body.url;
|
this.times = this.response.body.times;
|
this.creator = this.response.creator;
|
}
|
}
|
},
|
methods: {
|
handleHeaderDataChange(hasChanged) {
|
this.$emit("headerContentChanged", hasChanged);
|
},
|
handleRequestDataChange(hasChanged) {
|
this.$emit("requestContentChanged", hasChanged);
|
},
|
handleExtractDataChange(hasChanged) {
|
this.$emit("extractContentChanged", hasChanged);
|
},
|
handleValidateDataChange(hasChanged) {
|
this.$emit("validateContentChanged", hasChanged);
|
},
|
handleVariablesDataChange(hasChanged) {
|
this.$emit("variablesContentChanged", hasChanged);
|
},
|
async getGlobalVariables() {
|
try {
|
const response = await variablesList({
|
params: {
|
project: this.$route.params.id
|
}
|
});
|
this.globalVariables = response.results;
|
} catch (error) {
|
this.$message.error("获取全局变量失败");
|
}
|
},
|
|
handleCopyVariable(variable) {
|
this.$copyText(variable.key).then(
|
() => {
|
this.$notify.success({
|
title: "复制成功",
|
message: `已复制变量名: ${variable.key}`,
|
duration: 2000
|
});
|
},
|
e => {
|
this.$notify.error({
|
title: "复制失败",
|
message: e,
|
duration: 2000
|
});
|
}
|
);
|
},
|
|
copyVariableName(key) {
|
this.handleCopyVariable({ key });
|
},
|
handleHooksDataChange(hasChanged) {
|
this.$emit("hooksContentChanged", hasChanged);
|
},
|
setOriginalValues() {
|
if (this.response && this.response.body) {
|
this.originalValues = {
|
name: this.response.body.name,
|
url: this.response.body.url,
|
method: this.response.body.method,
|
times: this.response.body.times
|
};
|
}
|
},
|
checkForChanges() {
|
const currentValues = {
|
name: this.name,
|
url: this.url,
|
method: this.method,
|
times: this.times
|
};
|
const hasChanged = !isEqual(this.originalValues, currentValues);
|
this.$emit("otherContentChanged", hasChanged);
|
},
|
resetTabsState() {
|
this.activeTag = "second";
|
},
|
reverseStatus() {
|
this.save = !this.save;
|
this.run = true;
|
},
|
handleHeader(header) {
|
this.header = header;
|
},
|
handleRequest(request) {
|
this.request = request;
|
},
|
handleValidate(validate) {
|
this.validate = validate;
|
},
|
handleExtract(extract) {
|
this.extract = extract;
|
},
|
handleVariables(variables) {
|
this.variables = variables;
|
},
|
// 计算标记的数值
|
handleBadgeValue(arr, countKey) {
|
let res = 0;
|
for (const v of arr) {
|
if (v[countKey]) {
|
res += 1;
|
}
|
}
|
return res;
|
},
|
// 计算hooks的数值
|
handleHooksBadge(hook) {
|
let res = 0;
|
for (const hookElement of hook) {
|
if (hookElement.setup) {
|
res += 1;
|
}
|
if (hookElement.teardown) {
|
res += 1;
|
}
|
}
|
return res;
|
},
|
// 当save值变化时触发
|
handleHooks(hooks) {
|
this.hooks = hooks;
|
if (!this.run) {
|
if (this.id === "") {
|
this.addAPI();
|
} else {
|
this.updateAPI();
|
}
|
} else {
|
this.runAPI();
|
this.run = false;
|
}
|
},
|
handleSaveAs() {
|
this.save = !this.save;
|
this.id = "";
|
},
|
addAPI() {
|
if (this.validateData()) {
|
this.$api
|
.addAPI({
|
header: this.header,
|
request: this.request,
|
extract: this.extract,
|
validate: this.validate,
|
variables: this.variables,
|
hooks: this.hooks,
|
name: this.name,
|
url: this.url,
|
method: this.method,
|
times: this.times,
|
// 另存为时,使用response的值
|
nodeId: this.response.relation || this.nodeId,
|
project: this.response.project || this.project
|
})
|
.then(resp => {
|
if (resp.success) {
|
this.$emit("addSuccess");
|
this.$emit("refreshTree");
|
this.$message.success(resp.msg);
|
} else {
|
this.$message.error({
|
message: resp.msg,
|
duration: this.$store.state.duration
|
});
|
}
|
this.resetTabsState();
|
this.resetFlag = !this.resetFlag;
|
});
|
}
|
},
|
updateAPI() {
|
if (this.validateData()) {
|
this.$api
|
.updateAPI(this.id, {
|
header: this.header,
|
request: this.request,
|
extract: this.extract,
|
validate: this.validate,
|
variables: this.variables,
|
hooks: this.hooks,
|
name: this.name,
|
url: this.url,
|
method: this.method,
|
times: this.times
|
})
|
.then(resp => {
|
if (resp.success) {
|
this.$emit("addSuccess");
|
this.$message.success(resp.msg);
|
} else {
|
this.$message.error({
|
message: resp.msg,
|
duration: this.$store.state.duration
|
});
|
}
|
this.resetTabsState();
|
this.resetFlag = !this.resetFlag;
|
});
|
}
|
},
|
runAPI() {
|
if (this.validateData()) {
|
const isUrlValid =
|
this.url.startsWith("http://") ||
|
this.url.startsWith("https://");
|
if (!isUrlValid && !this.config.name) {
|
this.$message({
|
type: "warning",
|
message: "请先选择配置"
|
});
|
return false;
|
} else {
|
this.loading = true;
|
this.showCancel = true;
|
|
// 创建 cancel token
|
this.cancelTokenSource = axios.CancelToken.source();
|
|
// 设置一个定时器,2分钟后执行
|
const timeout = setTimeout(() => {
|
this.apiRunning = false;
|
this.cancelTokenSource.cancel("Request timed out");
|
}, 120000); // 120000ms equals to 2 minutes
|
|
this.$api
|
.runSingleAPI(
|
{
|
header: this.header,
|
request: this.request,
|
extract: this.extract,
|
validate: this.validate,
|
variables: this.variables,
|
hooks: this.hooks,
|
name: this.name,
|
url: this.url,
|
method: this.method,
|
times: this.times,
|
project: this.project,
|
config: this.config.name,
|
},
|
this.cancelTokenSource.token
|
)
|
.then(resp => {
|
clearTimeout(timeout); // 清除定时器
|
this.summary = resp;
|
this.dialogTableVisible = true;
|
this.loading = false;
|
this.showCancel = false; // 请求成功完成,隐藏‘取消请求’按钮
|
})
|
.catch(err => {
|
clearTimeout(timeout); // 清除定时器
|
if (!axios.isCancel(err)) {
|
// 如果错误不是由取消请求引起的,则处理错误
|
this.loading = false;
|
this.$message.error(err);
|
}
|
this.showCancel = false; // 请求失败,隐藏‘取消请求’按钮
|
});
|
}
|
}
|
},
|
cancelRequest() {
|
this.loading = false; // 关闭Loading
|
this.showCancel = false; // 隐藏‘取消请求’按钮
|
this.cancelTokenSource.cancel("User cancelled the request"); // 取消请求
|
},
|
validateData() {
|
if (this.name === "") {
|
this.$message({
|
type: "warning",
|
message: "接口名称不能为空"
|
});
|
return false;
|
}
|
if (this.url === "") {
|
this.$message({
|
type: "warning",
|
message: "接口地址不能为空"
|
});
|
return false;
|
}
|
return true;
|
}
|
},
|
computed: {
|
filteredVariables() {
|
if (!this.searchText) return this.globalVariables;
|
return this.globalVariables.filter(item => {
|
return (
|
item.key.toLowerCase().includes(this.searchText.toLowerCase()) ||
|
item.value.toLowerCase().includes(this.searchText.toLowerCase()) ||
|
(item.description && item.description.toLowerCase().includes(this.searchText.toLowerCase()))
|
);
|
});
|
}
|
},
|
mounted() {
|
this.setOriginalValues();
|
this.getGlobalVariables();
|
}
|
};
|
</script>
|
<style scoped>
|
.request {
|
margin-top: 15px;
|
border: 1px solid #ddd;
|
}
|
|
.custom-button {
|
position: absolute; /* 使用绝对定位 */
|
top: calc(50% + 40px); /* 从容器的顶部开始,向下移动50% + 20px */
|
left: 50%; /* 从容器的左边开始,向右移动50% */
|
transform: translate(-50%, -50%); /* 使用 transform 居中按钮 */
|
z-index: 2000; /* 确保按钮在 Loading 动画之上 */
|
}
|
|
.loading-container {
|
position: relative; /* 添加这个让子元素可以相对于此容器定位 */
|
margin-left: 10px;
|
}
|
</style>
|