feat(接口定义): 请求/响应头输入联想&部分高优先级 bug 修复

This commit is contained in:
baiqi 2024-03-14 15:19:23 +08:00 committed by Craftsman
parent a017474064
commit 263317e3c4
17 changed files with 222 additions and 154 deletions

View File

@ -17,6 +17,8 @@
import { statusCodeOptions } from '@/components/pure/ms-advance-filter/index';
import paramsTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { responseHeaderOption } from '@/config/apiTest';
import type { ExecuteAssertionItem } from '@/models/apiTest/common';
interface Param {
@ -41,21 +43,6 @@
enable: true,
};
const responseHeaderOption = [
{ label: 'Accept', value: 'accept' },
{ label: 'Accept-Encoding', value: 'acceptEncoding' },
{ label: 'Accept-Language', value: 'acceptLanguage' },
{ label: 'Cache-Control', value: 'cacheControl' },
{ label: 'Content-Type', value: 'contentType' },
{ label: 'Content-Length', value: 'contentLength' },
{ label: 'User-Agent', value: 'userAgent' },
{ label: 'Referer', value: 'referer' },
{ label: 'Cookie', value: 'cookie' },
{ label: 'Authorization', value: 'authorization' },
{ label: 'If-None-Match', value: 'ifNoneMatch' },
{ label: 'If-Modified-Since', value: 'ifModifiedSince' },
];
const columns: ParamTableColumn[] = [
{
title: 'ms.assertion.responseHeader', //

View File

@ -15,6 +15,8 @@
import { statusCodeOptions } from '@/components/pure/ms-advance-filter/index';
import paramsTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { responseHeaderOption } from '@/config/apiTest';
interface Param {
[key: string]: any;
variableAssertionItems: any[];
@ -37,21 +39,6 @@
enable: true,
};
const responseHeaderOption = [
{ label: 'Accept', value: 'accept' },
{ label: 'Accept-Encoding', value: 'acceptEncoding' },
{ label: 'Accept-Language', value: 'acceptLanguage' },
{ label: 'Cache-Control', value: 'cacheControl' },
{ label: 'Content-Type', value: 'contentType' },
{ label: 'Content-Length', value: 'contentLength' },
{ label: 'User-Agent', value: 'userAgent' },
{ label: 'Referer', value: 'referer' },
{ label: 'Cookie', value: 'cookie' },
{ label: 'Authorization', value: 'authorization' },
{ label: 'If-None-Match', value: 'ifNoneMatch' },
{ label: 'If-Modified-Since', value: 'ifModifiedSince' },
];
const columns: ParamTableColumn[] = [
{
title: 'ms.assertion.variableName', //

View File

@ -1,5 +1,5 @@
<template>
<a-breadcrumb v-if="appStore.breadcrumbList.length > 0" class="z-10 mb-[-8px] mt-[8px]">
<a-breadcrumb v-if="appStore.breadcrumbList.length > 0" class="z-10 mb-[-8px]">
<a-breadcrumb-item v-for="crumb of appStore.breadcrumbList" :key="crumb.name" @click="jumpTo(crumb)">
{{ isEdit ? t(crumb.editLocale || crumb.locale) : t(crumb.locale) }}
</a-breadcrumb-item>

View File

@ -10,6 +10,7 @@
props.autoHeight ? '' : 'min-h-[500px]',
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
props.noBottomRadius ? 'ms-card--noBottomRadius' : '',
!props.hideFooter && !props.simple ? 'pb-[80px]' : '',
]"
>
<a-scrollbar v-if="!props.simple" :style="{ overflow: 'auto' }">
@ -149,9 +150,9 @@
}
if (props.simple) {
//
return props.noContentPadding ? 76 + _specialHeight : 124 + _specialHeight;
return props.noContentPadding ? 66 + _specialHeight : 114 + _specialHeight;
}
return 190 + _specialHeight;
return 250 + _specialHeight;
});
const getComputedContentStyle = computed(() => {

View File

@ -88,6 +88,30 @@
v-model:model-value="record[item.dataIndex as string]"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<template v-else-if="item.inputType === 'autoComplete'">
<a-auto-complete
v-model:model-value="record[item.dataIndex as string]"
:data="item.autoCompleteParams?.filter((e) => e.isShow === true)"
class="ms-form-table-input"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
:filter-option="false"
size="small"
@search="(val) => handleSearchParams(val, item)"
@change="() => handleFormChange(record, rowIndex, item)"
@select="(val) => selectAutoComplete(val, record, item)"
>
<template #option="{ data: opt }">
<div class="w-[350px]">
{{ opt.raw.value }}
<a-tooltip :content="t(opt.raw.desc)" position="bl" :mouse-enter-delay="300">
<div class="one-line-text max-w-full text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t(opt.raw.desc) }}
</div>
</a-tooltip>
</div>
</template>
</a-auto-complete>
</template>
<template v-else-if="item.inputType === 'text'">
{{
typeof item.valueFormat === 'function' ? item.valueFormat(record) : record[item.dataIndex as string] || '-'
@ -122,7 +146,6 @@
</template>
<script setup lang="ts">
import { TableColumnData, TableData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -140,12 +163,14 @@
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
import { ActionsItem } from '../ms-table-more-action/types';
import type { SelectOptionData, TableColumnData, TableData } from '@arco-design/web-vue';
import { TableOperationColumn } from '@arco-design/web-vue/es/table/interface';
export interface FormTableColumn extends MsTableColumnData {
enable?: boolean; //
required?: boolean; //
inputType?: 'input' | 'select' | 'tags' | 'switch' | 'text' | 'checkbox'; //
inputType?: 'input' | 'select' | 'tags' | 'switch' | 'text' | 'checkbox' | 'autoComplete'; //
autoCompleteParams?: SelectOptionData[]; //
valueFormat?: (record: Record<string, any>) => string; // inputTypetext
[key: string]: any; //
}
@ -330,6 +355,21 @@
emitChange('deleteParam');
}
/**
* 搜索变量
* @param val 变量名
*/
function handleSearchParams(val: string, item: FormTableColumn) {
item.autoCompleteParams = item.autoCompleteParams?.map((e) => {
e.isShow = e.label?.includes(val);
return e;
});
}
function selectAutoComplete(val: string, record: Record<string, any>, item: FormTableColumn) {
record[item.dataIndex as string] = val;
}
await initColumns();
</script>
@ -381,6 +421,16 @@
}
}
}
:deep(.ms-form-table-input-trigger) {
width: 350px;
.arco-select-dropdown-list {
.arco-select-option {
@apply !h-auto;
padding: 2px 8px !important;
}
}
}
:deep(.ms-form-table-input-number) {
@apply bg-transparent pr-0;
.arco-input {

View File

@ -23,3 +23,18 @@ export const requestBodyTypeMap = {
[RequestBodyFormat.XML]: 'xml',
[RequestBodyFormat.NONE]: 'none',
};
// 请求/响应头选项
export const responseHeaderOption = [
{ label: 'Accept', value: 'accept' },
{ label: 'Accept-Encoding', value: 'acceptEncoding' },
{ label: 'Accept-Language', value: 'acceptLanguage' },
{ label: 'Cache-Control', value: 'cacheControl' },
{ label: 'Content-Type', value: 'contentType' },
{ label: 'Content-Length', value: 'contentLength' },
{ label: 'User-Agent', value: 'userAgent' },
{ label: 'Referer', value: 'referer' },
{ label: 'Cookie', value: 'cookie' },
{ label: 'Authorization', value: 'authorization' },
{ label: 'If-None-Match', value: 'ifNoneMatch' },
{ label: 'If-Modified-Since', value: 'ifModifiedSince' },
];

View File

@ -41,12 +41,7 @@
</a-drawer>
<a-layout class="layout-content" :style="paddingStyle">
<a-spin :loading="appStore.loading" :tip="appStore.loadingTip">
<a-scrollbar
:style="{
overflow: 'auto',
height: 'calc(100vh - 64px)',
}"
>
<a-scrollbar class="flex h-[calc(100vh-56px)] flex-col gap-[8px] overflow-auto">
<MsBreadCrumb />
<a-layout-content>
<slot name="page">
@ -211,7 +206,6 @@
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
.arco-layout-content {
padding: 8px 16px 0 0;
min-height: 500px;
}
}
.arco-layout-sider-light {

View File

@ -17,4 +17,10 @@
<script lang="ts" setup></script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.page-content {
@apply h-full overflow-y-auto;
min-height: 500px;
}
</style>

View File

@ -39,7 +39,7 @@ const ApiTest: AppRouteRecordRaw = {
component: () => import('@/views/api-test/management/index.vue'),
meta: {
locale: 'menu.apiTest.management',
roles: ['*'],
roles: ['PROJECT_API_DEFINITION:READ'],
isTopMenu: true,
},
},

View File

@ -1,11 +1,14 @@
import {
EnableKeyValueParam,
ExecuteBody,
ExecuteRequestCommonParam,
ExecuteRequestFormBodyFormValue,
KeyValueParam,
RequestTaskResult,
ResponseDefinition,
} from '@/models/apiTest/common';
import {
RequestBodyFormat,
RequestCaseStatus,
RequestContentTypeEnum,
RequestParamsType,
@ -81,6 +84,54 @@ export const defaultResponseItem: ResponseDefinition = {
},
};
// 请求的默认 body 参数
export const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
// 默认的响应内容结构
export const defaultResponse: RequestTaskResult = {
requestResults: [
{
body: '',
headers: '',
method: '',
url: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
sslHandshakeTime: 0,
},
},
],
console: '',
};
// 默认提取参数的 key-value 表格行的值
export const defaultKeyValueParamItem: KeyValueParam = {
key: '',

View File

@ -55,6 +55,7 @@
</div>
</template>
<!-- 表格列 slot -->
<!-- 参数名 or 请求/响应头联想输入 -->
<template #key="{ record, columnConfig, rowIndex }">
<a-popover
position="tl"
@ -69,7 +70,31 @@
{{ record[columnConfig.dataIndex as string] }}
</div>
</template>
<a-auto-complete
v-if="columnConfig.inputType === 'autoComplete'"
v-model:model-value="record[columnConfig.dataIndex as string]"
:data="columnConfig.autoCompleteParams?.filter((e) => e.isShow === true)"
class="ms-form-table-input"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
:filter-option="false"
size="mini"
@search="(val) => handleSearchParams(val, columnConfig)"
@change="() => addTableLine(rowIndex, columnConfig.addLineDisabled)"
@select="(val) => selectAutoComplete(val, record, columnConfig)"
>
<template #option="{ data: opt }">
<div class="w-[350px]">
{{ t(opt.raw.label) }}
<!-- <a-tooltip :content="t(opt.raw.label)" position="bl" :mouse-enter-delay="300">
<div class="one-line-text max-w-full">
{{ t(opt.raw.label) }}
</div>
</a-tooltip> -->
</div>
</template>
</a-auto-complete>
<a-input
v-else
v-model:model-value="record[columnConfig.dataIndex as string]"
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
class="ms-form-table-input"
@ -79,6 +104,7 @@
/>
</a-popover>
</template>
<!-- 参数类型 -->
<template #paramType="{ record, columnConfig, rowIndex }">
<a-tooltip
v-if="columnConfig.hasRequired"
@ -104,6 +130,7 @@
@change="(val) => handleTypeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
/>
</template>
<!-- 提取类型 -->
<template #extractType="{ record, columnConfig, rowIndex }">
<a-select
v-model:model-value="record.extractType"
@ -113,6 +140,7 @@
@change="() => addTableLine(rowIndex)"
/>
</template>
<!-- 变量类型 -->
<template #variableType="{ record, columnConfig, rowIndex }">
<a-select
v-model:model-value="record.variableType"
@ -122,6 +150,7 @@
@change="() => addTableLine(rowIndex)"
/>
</template>
<!-- 提取范围 -->
<template #extractScope="{ record, columnConfig, rowIndex }">
<a-select
v-model:model-value="record.extractScope"
@ -131,9 +160,11 @@
@change="() => addTableLine(rowIndex)"
/>
</template>
<!-- 表达式 -->
<template #expression="{ record, rowIndex, columnConfig }">
<slot name="expression" :record="record" :row-index="rowIndex" :column-config="columnConfig"></slot>
</template>
<!-- 参数值 -->
<template #value="{ record, columnConfig, rowIndex }">
<a-popover
v-if="columnConfig.isNormal"
@ -185,6 +216,7 @@
@apply="() => addTableLine(rowIndex)"
/>
</template>
<!-- 长度范围 -->
<template #lengthRange="{ record, rowIndex }">
<div class="flex items-center justify-between">
<a-input-number
@ -208,6 +240,7 @@
/>
</div>
</template>
<!-- 标签 -->
<template #tag="{ record, columnConfig, rowIndex }">
<a-popover
position="tl"
@ -232,6 +265,7 @@
/>
</a-popover>
</template>
<!-- 描述 -->
<template #description="{ record, columnConfig, rowIndex }">
<paramDescInput
v-model:desc="record[columnConfig.dataIndex as string]"
@ -241,6 +275,7 @@
@change="handleDescChange"
/>
</template>
<!-- 编码 -->
<template #encode="{ record, rowIndex }">
<a-switch
v-model:model-value="record.encode"
@ -250,12 +285,14 @@
@change="() => addTableLine(rowIndex)"
/>
</template>
<!-- 必须包含 -->
<template #mustContain="{ record, columnConfig }">
<a-checkbox
v-model:model-value="record[columnConfig.dataIndex as string]"
@change="handleMustContainColChange(false)"
/>
</template>
<!-- 类型检查 -->
<template #typeChecking="{ record, columnConfig }">
<a-checkbox
v-model:model-value="record[columnConfig.dataIndex as string]"
@ -296,6 +333,7 @@
</a-tooltip>
<a-input v-model="record.expectedValue" size="mini" class="ms-form-table-input" />
</template>
<!-- 项目选择 -->
<template #project="{ record, rowIndex }">
<a-select
v-model:model-value="record.projectId"
@ -320,6 +358,7 @@
</a-tooltip>
</a-select>
</template>
<!-- 环境选择 -->
<template #environment="{ record }">
<MsSelect
v-if="record.projectId"
@ -340,6 +379,7 @@
/>
<span v-else></span>
</template>
<!-- 域名 -->
<template #host="{ record }">
<MsTagsGroup
v-if="Array.isArray(record.domain)"
@ -349,6 +389,7 @@
/>
<div v-else class="text-[var(--color-text-1)]">{{ '-' }}</div>
</template>
<!-- 操作 -->
<template #operation="{ record, rowIndex, columnConfig }">
<div class="flex flex-row items-center" :class="{ 'justify-end': columnConfig.align === 'right' }">
<a-switch
@ -482,6 +523,7 @@
const MsParamsInput = defineAsyncComponent(() => import('@/components/business/ms-params-input/index.vue'));
export interface ParamTableColumn extends FormTableColumn {
isAutoComplete?: boolean; // key /
isNormal?: boolean; // value MsParamsInput
hasRequired?: boolean; // type required
typeOptions?: { label: string; value: string }[]; // type
@ -923,6 +965,21 @@
emitChange('handleFormTableChange');
}
/**
* 搜索变量
* @param val 变量名
*/
function handleSearchParams(val: string, item: FormTableColumn) {
item.autoCompleteParams = item.autoCompleteParams?.map((e) => {
e.isShow = (e.label || '').includes(val);
return e;
});
}
function selectAutoComplete(val: string, record: Record<string, any>, item: FormTableColumn) {
record[item.dataIndex as string] = val;
}
defineExpose({
addTableLine,
});

View File

@ -24,6 +24,8 @@
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { responseHeaderOption } from '@/config/apiTest';
import { EnableKeyValueParam } from '@/models/apiTest/common';
import { filterKeyValParams } from '../utils';
@ -47,6 +49,8 @@
title: 'apiTestDebug.paramName',
dataIndex: 'key',
slotName: 'key',
inputType: 'autoComplete',
autoCompleteParams: responseHeaderOption,
},
{
title: 'apiTestDebug.paramValue',

View File

@ -143,10 +143,10 @@
v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList"
:get-text-func="getTabBadge"
class="no-content relative mt-[8px]"
class="no-content relative mt-[8px] border-b"
/>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-97px)]">
<div ref="splitContainerRef" class="h-[calc(100%-87px)]">
<MsSplitBox
ref="horizontalSplitBoxRef"
:size="props.isDefinition ? 0.7 : 1"
@ -166,7 +166,7 @@
min="10px"
:direction="activeLayout"
second-container-class="!overflow-y-hidden"
:class="!showResponse ? 'hidden-second' : ''"
:class="!showResponse ? 'hidden-second' : 'show-second'"
@expand-change="handleVerticalExpandChange"
>
<template #first>
@ -545,6 +545,7 @@
defaultHeaderParamsItem,
defaultKeyValueParamItem,
defaultRequestParamsItem,
defaultResponse,
} from '@/views/api-test/components/config';
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
import type { Api } from '@form-create/arco-design';
@ -876,22 +877,16 @@
function handleUrlChange(val: string) {
const params = parseQueryParams(val.trim());
if (params.length > 0) {
requestVModel.value.query.splice(
0,
requestVModel.value.query.length - 2,
requestVModel.value.query = [
...params.map((e, i) => ({
id: (new Date().getTime() + i).toString(),
paramType: RequestParamsType.STRING,
description: '',
required: false,
maxLength: undefined,
minLength: undefined,
encode: false,
enable: true,
...defaultRequestParamsItem,
...e,
}))
);
})),
cloneDeep(defaultRequestParamsItem),
];
requestVModel.value.activeTab = RequestComposition.QUERY;
[requestVModel.value.url] = val.split('?');
}
handleActiveDebugChange();
}
@ -1151,6 +1146,7 @@
if (isHttpProtocol.value) {
try {
requestVModel.value.executeLoading = true;
requestVModel.value.response = cloneDeep(defaultResponse);
const res = await props.executeApi(makeRequestParams(executeType));
if (executeType === 'localExec') {
await props.localExecuteApi(localExecuteUrl.value, res);
@ -1166,6 +1162,7 @@
if (valid === true) {
try {
requestVModel.value.executeLoading = true;
requestVModel.value.response = cloneDeep(defaultResponse);
const res = await props.executeApi(makeRequestParams(executeType));
if (executeType === 'localExec') {
await props.localExecuteApi(localExecuteUrl.value, res);
@ -1518,4 +1515,9 @@
@apply hidden;
}
}
.show-second {
:deep(.arco-split-trigger) {
@apply block;
}
}
</style>

View File

@ -178,6 +178,7 @@
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import { responseHeaderOption } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -397,7 +398,8 @@
title: 'apiTestManagement.paramName',
dataIndex: 'key',
slotName: 'key',
inputType: 'input',
inputType: 'autoComplete',
autoCompleteParams: responseHeaderOption,
},
{
title: 'apiTestManagement.paramVal',

View File

@ -133,6 +133,7 @@
ResponseComposition,
} from '@/enums/apiEnum';
import { defaultBodyParams, defaultResponse } from '../components/config';
import { parseRequestBodyFiles } from '../components/utils';
const { t } = useI18n();
@ -150,50 +151,6 @@
}
const initDefaultId = `debug-${Date.now()}`;
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
const defaultResponse: RequestTaskResult = {
requestResults: [
{
body: '',
headers: '',
url: '',
method: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
sslHandshakeTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
},
},
],
console: '',
}; //
const defaultDebugParams: RequestParam = {
id: initDefaultId,
moduleId: 'root',

View File

@ -92,7 +92,7 @@
ResponseComposition,
} from '@/enums/apiEnum';
import { defaultResponseItem } from '@/views/api-test/components/config';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
// requestComposition
@ -138,51 +138,6 @@
});
const initDefaultId = `definition-${Date.now()}`;
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
//
const defaultResponse: RequestTaskResult = {
requestResults: [
{
body: '',
headers: '',
method: '',
url: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
sslHandshakeTime: 0,
},
},
],
console: '',
};
const defaultDefinitionParams: RequestParam = {
id: initDefaultId,
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,

View File

@ -313,10 +313,10 @@
label: 'apiTestManagement.share',
eventTag: 'share',
},
{
label: 'apiTestManagement.shareModule',
eventTag: 'shareModule',
},
// {
// label: 'apiTestManagement.shareModule',
// eventTag: 'shareModule',
// }, // TODO:
{
isDivider: true,
},