feat(接口管理): 接口定义优化

This commit is contained in:
baiqi 2024-03-06 16:53:09 +08:00 committed by 刘瑞斌
parent 2b8cdc5359
commit 1d0ef9a28a
13 changed files with 428 additions and 172 deletions

View File

@ -14,7 +14,7 @@
:columns="jsonPathColumns" :columns="jsonPathColumns"
:scroll="{ minWidth: '700px' }" :scroll="{ minWidth: '700px' }"
:default-param-item="jsonPathDefaultParamItem" :default-param-item="jsonPathDefaultParamItem"
@change="(data, isInit) => handleChange(data, !!isInit, 'jsonPath')" @change="(data) => handleChange(data, 'jsonPath')"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)" @more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
> >
<template #expression="{ record, rowIndex }"> <template #expression="{ record, rowIndex }">
@ -99,7 +99,7 @@
:columns="xPathColumns" :columns="xPathColumns"
:scroll="{ minWidth: '700px' }" :scroll="{ minWidth: '700px' }"
:default-param-item="xPathDefaultParamItem" :default-param-item="xPathDefaultParamItem"
@change="(data, isInit) => handleChange(data, !!isInit, 'xPath')" @change="(data) => handleChange(data, 'xPath')"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)" @more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
> >
<template #expression="{ record, rowIndex }"> <template #expression="{ record, rowIndex }">
@ -193,7 +193,7 @@
:span-method="documentSpanMethod" :span-method="documentSpanMethod"
is-tree-table is-tree-table
@tree-delete="deleteAllParam" @tree-delete="deleteAllParam"
@change="(data, isInit) => handleChange(data, !!isInit, 'document')" @change="(data) => handleChange(data, 'document')"
> >
<template #matchValueDelete="{ record }"> <template #matchValueDelete="{ record }">
<icon-minus-circle <icon-minus-circle
@ -234,7 +234,7 @@
:columns="xPathColumns" :columns="xPathColumns"
:scroll="{ minWidth: '700px' }" :scroll="{ minWidth: '700px' }"
:default-param-item="xPathDefaultParamItem" :default-param-item="xPathDefaultParamItem"
@change="(data, isInit) => handleChange(data, !!isInit, 'regular')" @change="(data) => handleChange(data, 'regular')"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)" @more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
> >
<template #expression="{ record, rowIndex }"> <template #expression="{ record, rowIndex }">
@ -473,10 +473,7 @@
matchValue: '', matchValue: '',
enable: true, enable: true,
}; };
const handleChange = (data: any[], isInit: boolean, type: string) => { const handleChange = (data: any[], type: string) => {
if (isInit) {
return;
}
if (type === 'jsonPath') { if (type === 'jsonPath') {
innerParams.value.jsonPath = data; innerParams.value.jsonPath = data;
} else if (type === 'xPath') { } else if (type === 'xPath') {

View File

@ -20,7 +20,13 @@
@click="handleTabClick(tab)" @click="handleTabClick(tab)"
> >
<div :draggable="!!tab.draggable" class="flex items-center"> <div :draggable="!!tab.draggable" class="flex items-center">
<slot name="label" :tab="tab">{{ tab.label }}</slot> <slot name="label" :tab="tab">
<a-tooltip :content="tab.label" :mouse-enter-delay="500">
<div class="one-line-text flex max-w-[144px] items-center">
{{ tab.label }}
</div>
</a-tooltip>
</slot>
<div v-if="tab.unSaved" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div> <div v-if="tab.unSaved" class="ml-[8px] h-[8px] w-[8px] rounded-full bg-[rgb(var(--primary-5))]"></div>
<MsButton <MsButton
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable !== false" v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable !== false"

View File

@ -551,7 +551,7 @@
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', data: any[], isInit?: boolean): void; // (e: 'change', data: any[]): void; //
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void; (e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
(e: 'projectChange', projectId: string): void; (e: 'projectChange', projectId: string): void;
(e: 'treeDelete', record: Record<string, any>): void; (e: 'treeDelete', record: Record<string, any>): void;
@ -583,6 +583,12 @@
showPagination: false, showPagination: false,
}); });
function emitChange(from: string, isInit?: boolean) {
if (!isInit) {
emit('change', propsRes.value.data);
}
}
const selectedKeys = computed(() => propsRes.value.data.filter((e) => e.enable).map((e) => e.id)); const selectedKeys = computed(() => propsRes.value.data.filter((e) => e.enable).map((e) => e.id));
propsEvent.value.rowSelectChange = (key: string) => { propsEvent.value.rowSelectChange = (key: string) => {
propsRes.value.data = propsRes.value.data.map((e) => { propsRes.value.data = propsRes.value.data.map((e) => {
@ -591,14 +597,14 @@
} }
return e; return e;
}); });
emit('change', propsRes.value.data); emitChange('rowSelectChange');
}; };
propsEvent.value.selectAllChange = (v: SelectAllEnum) => { propsEvent.value.selectAllChange = (v: SelectAllEnum) => {
propsRes.value.data = propsRes.value.data.map((e) => { propsRes.value.data = propsRes.value.data.map((e) => {
e.enable = v !== SelectAllEnum.NONE; e.enable = v !== SelectAllEnum.NONE;
return e; return e;
}); });
emit('change', propsRes.value.data); emitChange('selectAllChange');
}; };
watch( watch(
@ -623,7 +629,7 @@
return; return;
} }
propsRes.value.data.splice(rowIndex, 1); propsRes.value.data.splice(rowIndex, 1);
emit('change', propsRes.value.data); emitChange('deleteParam');
} }
/** 断言-文档-Begin */ /** 断言-文档-Begin */
@ -638,7 +644,7 @@
e.mustInclude = val; e.mustInclude = val;
}); });
propsRes.value.data = data; propsRes.value.data = data;
emit('change', propsRes.value.data); emitChange('handleMustIncludeChange');
}; };
const handleMustContainColChange = (notEmit?: boolean) => { const handleMustContainColChange = (notEmit?: boolean) => {
const { data } = propsRes.value; const { data } = propsRes.value;
@ -654,7 +660,7 @@
mustIncludeIndeterminate.value = true; mustIncludeIndeterminate.value = true;
} }
if (!notEmit) { if (!notEmit) {
emit('change', propsRes.value.data); emitChange('handleMustContainColChange');
} }
}; };
@ -668,7 +674,7 @@
e.typeChecking = val; e.typeChecking = val;
}); });
propsRes.value.data = data; propsRes.value.data = data;
emit('change', propsRes.value.data); emitChange('handleTypeCheckingChange');
}; };
const handleTypeCheckingColChange = (notEmit?: boolean) => { const handleTypeCheckingColChange = (notEmit?: boolean) => {
const { data } = propsRes.value; const { data } = propsRes.value;
@ -684,7 +690,7 @@
typeCheckingIndeterminate.value = true; typeCheckingIndeterminate.value = true;
} }
if (!notEmit) { if (!notEmit) {
emit('change', propsRes.value.data); emitChange('handleTypeCheckingColChange');
} }
}; };
/** 断言-文档-end */ /** 断言-文档-end */
@ -707,7 +713,7 @@
const handleEnvironment = (obj: Record<string, any>, record: Record<string, any>) => { const handleEnvironment = (obj: Record<string, any>, record: Record<string, any>) => {
record.domain = {}; record.domain = {};
emit('change', propsRes.value.data); emitChange('handleEnvironment');
}; };
const hostVisible = ref(false); const hostVisible = ref(false);
@ -751,7 +757,7 @@
* @param key 当前列的 key * @param key 当前列的 key
* @param isForce 是否强制添加 * @param isForce 是否强制添加
*/ */
function addTableLine(rowIndex: number, addLineDisabled?: boolean) { function addTableLine(rowIndex: number, addLineDisabled?: boolean, isInit?: boolean) {
if (addLineDisabled) { if (addLineDisabled) {
return; return;
} }
@ -763,7 +769,7 @@
...cloneDeep(props.defaultParamItem), // ...cloneDeep(props.defaultParamItem), //
enable: true, // enable: true, //
} as any); } as any);
emit('change', propsRes.value.data); emitChange('addTableLine', isInit);
} }
handleMustContainColChange(true); handleMustContainColChange(true);
handleTypeCheckingColChange(true); handleTypeCheckingColChange(true);
@ -786,7 +792,7 @@
return item; return item;
}); });
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) { if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
addTableLine(arr.length - 1); addTableLine(arr.length - 1, false, true);
} }
} else { } else {
const id = new Date().getTime().toString(); const id = new Date().getTime().toString();
@ -797,7 +803,7 @@
enable: true, // enable: true, //
}, },
] as any[]; ] as any[];
emit('change', propsRes.value.data, true); emitChange('watch props.params', true);
} }
}, },
{ {
@ -808,7 +814,7 @@
function toggleRequired(record: Record<string, any>, rowIndex: number) { function toggleRequired(record: Record<string, any>, rowIndex: number) {
record.required = !record.required; record.required = !record.required;
addTableLine(rowIndex); addTableLine(rowIndex);
emit('change', propsRes.value.data); emitChange('toggleRequired');
} }
async function handleFileChange( async function handleFileChange(
@ -842,7 +848,7 @@
})); }));
} }
addTableLine(rowIndex); addTableLine(rowIndex);
emit('change', propsRes.value.data); emitChange('handleFileChange');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -871,7 +877,7 @@
showQuickInputParam.value = false; showQuickInputParam.value = false;
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id)); addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id));
clearQuickInputParam(); clearQuickInputParam();
emit('change', propsRes.value.data); emitChange('applyQuickInputParam');
} }
const showQuickInputDesc = ref(false); const showQuickInputDesc = ref(false);
@ -893,11 +899,11 @@
showQuickInputDesc.value = false; showQuickInputDesc.value = false;
addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id)); addTableLine(propsRes.value.data.findIndex((e) => e.id === activeQuickInputRecord.value.id));
clearQuickInputDesc(); clearQuickInputDesc();
emit('change', propsRes.value.data); emitChange('applyQuickInputDesc');
} }
function handleDescChange() { function handleDescChange() {
emit('change', propsRes.value.data); emitChange('handleDescChange');
} }
function handleTypeChange( function handleTypeChange(
@ -917,7 +923,7 @@
record.contentType = RequestContentTypeEnum.TEXT; record.contentType = RequestContentTypeEnum.TEXT;
} }
} }
emit('change', propsRes.value.data); emitChange('handleTypeChange');
} }
/** /**

View File

@ -10,7 +10,7 @@
</a-empty> </a-empty>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col"> <div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<div class="px-[18px] pt-[8px]"> <div class="px-[18px] pt-[8px]">
<div class="flex items-center justify-between"> <div class="flex flex-wrap items-center justify-between gap-[12px]">
<div class="flex flex-1 items-center gap-[16px]"> <div class="flex flex-1 items-center gap-[16px]">
<a-select <a-select
v-if="requestVModel.isNew" v-if="requestVModel.isNew"
@ -54,9 +54,25 @@
/> />
</a-input-group> </a-input-group>
</div> </div>
<div class="ml-[16px]"> <div>
<a-radio-group
v-if="props.isDefinition"
v-model:model-value="requestVModel.mode"
type="button"
class="mr-[12px]"
>
<a-radio value="definition">{{ t('apiTestManagement.definition') }}</a-radio>
<a-radio value="debug">{{ t('apiTestManagement.debug') }}</a-radio>
</a-radio-group>
<!-- 接口定义-调试模式下可执行 -->
<template
v-if="
(!props.isDefinition || (props.isDefinition && requestVModel.mode === 'debug')) &&
hasAnyPermission([props.permissionMap.execute])
"
>
<a-dropdown-button <a-dropdown-button
v-if="!requestVModel.executeLoading && hasAnyPermission([props.permissionMap.execute])" v-if="!requestVModel.executeLoading"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)" :disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
class="exec-btn" class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')" @click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@ -73,8 +89,19 @@
</template> </template>
</a-dropdown-button> </a-dropdown-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button> <a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
</template>
<!-- 接口定义-且有保存或更新权限 -->
<template
v-if="
props.isDefinition &&
(requestVModel.isNew
? hasAnyPermission([props.permissionMap.create])
: hasAnyPermission([props.permissionMap.update]))
"
>
<!-- 接口定义-调试模式可保存或保存为新用例 -->
<a-dropdown <a-dropdown
v-if="props.isDefinition && hasAnyPermission([props.permissionMap.create, props.permissionMap.update])" v-if="requestVModel.mode === 'debug'"
:loading="saveLoading || (isHttpProtocol && !requestVModel.url)" :loading="saveLoading || (isHttpProtocol && !requestVModel.url)"
@select="handleSelect" @select="handleSelect"
> >
@ -86,9 +113,18 @@
<a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption> <a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption>
</template> </template>
</a-dropdown> </a-dropdown>
<!-- 接口定义-定义模式直接保存接口定义 -->
<a-button v-else type="primary" @click="() => handleSelect('save')">
{{ t('common.save') }}
</a-button>
</template>
<!-- 接口调试支持快捷保存 -->
<a-button <a-button
v-else v-else-if="
v-permission="[props.permissionMap.create, props.permissionMap.update]" requestVModel.isNew
? hasAnyPermission([props.permissionMap.create])
: hasAnyPermission([props.permissionMap.update])
"
type="secondary" type="secondary"
:disabled="isHttpProtocol && !requestVModel.url" :disabled="isHttpProtocol && !requestVModel.url"
:loading="saveLoading" :loading="saveLoading"
@ -102,7 +138,7 @@
</div> </div>
</div> </div>
</div> </div>
<div ref="splitContainerRef" :class="splitContainerClass"> <div ref="splitContainerRef" class="h-[calc(100%-40px)]">
<MsSplitBox <MsSplitBox
ref="splitBoxRef" ref="splitBoxRef"
v-model:size="splitBoxSize" v-model:size="splitBoxSize"
@ -138,7 +174,13 @@
v-model:api="fApi" v-model:api="fApi"
:rule="currentPluginScript" :rule="currentPluginScript"
:option="currentPluginOptions" :option="currentPluginOptions"
@change="handlePluginFormChange" @change="
() => {
if (isInitPluginForm) {
handlePluginFormChange();
}
}
"
/> />
</a-spin> </a-spin>
<httpHeader <httpHeader
@ -329,6 +371,8 @@
isNew: boolean; isNew: boolean;
protocol: string; protocol: string;
activeTab: RequestComposition; activeTab: RequestComposition;
mode?: 'definition' | 'debug';
executeLoading: boolean; // loading
} }
export type RequestParam = ExecuteApiRequestFullParams & { export type RequestParam = ExecuteApiRequestFullParams & {
responseDefinition?: ResponseItem[]; responseDefinition?: ResponseItem[];
@ -364,7 +408,7 @@
const loading = defineModel<boolean>('detailLoading', { default: false }); const loading = defineModel<boolean>('detailLoading', { default: false });
const requestVModel = defineModel<RequestParam>('request', { required: true }); const requestVModel = defineModel<RequestParam>('request', { required: true });
requestVModel.value.executeLoading = false; // loading
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP'); const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const temporaryResponseMap = {}; // websockettab const temporaryResponseMap = {}; // websockettab
const isInitPluginForm = ref(false); const isInitPluginForm = ref(false);
@ -392,7 +436,7 @@
const commonContentTabKey = [ const commonContentTabKey = [
RequestComposition.PRECONDITION, RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION, RequestComposition.POST_CONDITION,
// RequestComposition.ASSERTION, TODO: RequestComposition.ASSERTION,
]; ];
// tab // tab
const pluginContentTab = [ const pluginContentTab = [
@ -443,10 +487,20 @@
// tab // tab
const contentTabList = computed(() => { const contentTabList = computed(() => {
if (isHttpProtocol.value) { if (isHttpProtocol.value) {
// if (props.isDefinition) {
return props.isDefinition //
return requestVModel.value.mode === 'debug'
? httpContentTabList ? httpContentTabList
: httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION); : httpContentTabList.filter((e) => !commonContentTabKey.includes(e.value));
}
//
return httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
}
if (props.isDefinition) {
//
return requestVModel.value.mode === 'definition'
? pluginContentTab
: [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
} }
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))]; return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
}); });
@ -536,6 +590,24 @@
handleActiveDebugChange(); handleActiveDebugChange();
}, 300); }, 300);
/**
* 控制插件表单字段显示
*/
function controlPluginFormFields() {
const allFields = fApi.value?.fields();
let fields: string[] = [];
if (props.isDefinition) {
// 使
// apiDefinitionFields
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields || [];
} else {
// apiDebugFields
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
}
fApi.value?.hidden(true, allFields?.filter((e) => !fields.includes(e)) || []);
return fields;
}
/** /**
* 设置插件表单数据 * 设置插件表单数据
*/ */
@ -543,30 +615,26 @@
const tempForm = temporaryPluginFormMap[requestVModel.value.id]; const tempForm = temporaryPluginFormMap[requestVModel.value.id];
if (tempForm || !requestVModel.value.isNew) { if (tempForm || !requestVModel.value.isNew) {
// //
fApi.value?.nextRefresh(() => {
fApi.value?.reload(currentPluginScript.value);
const formData = tempForm || requestVModel.value; const formData = tempForm || requestVModel.value;
if (fApi.value) { if (fApi.value) {
fApi.value.nextTick(() => {
const form = {}; const form = {};
if (props.isDefinition) { controlPluginFormFields().forEach((key) => {
// 使
pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields?.forEach((key) => {
form[key] = formData[key]; form[key] = formData[key];
}); });
} else {
pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields?.forEach((key) => {
form[key] = formData[key];
});
}
fApi.value?.setValue(form); fApi.value?.setValue(form);
} setTimeout(() => {
}); // 300ms handlePluginFormChange
nextTick(() => {
isInitPluginForm.value = true; isInitPluginForm.value = true;
}, 300);
}); });
}
} else { } else {
// form-create tab fApi.value?.nextTick(() => {
controlPluginFormFields();
});
nextTick(() => { nextTick(() => {
// form-create tab
fApi.value?.resetFields(); fApi.value?.resetFields();
}); });
} }
@ -582,6 +650,7 @@
return; return;
} }
pluginError.value = false; pluginError.value = false;
isInitPluginForm.value = false;
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) { if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
setPluginFormData(); setPluginFormData();
// //
@ -672,12 +741,6 @@
const activeLayout = ref<'horizontal' | 'vertical'>('vertical'); const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>(); const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0); const secondBoxHeight = ref(0);
const splitContainerClass = computed(() => {
if (!showResponse.value) {
return 'h-full';
}
return 'h-[calc(100%-40px)]';
});
watch( watch(
() => splitBoxSize.value, () => splitBoxSize.value,
@ -716,6 +779,17 @@
} }
} }
watch(
() => showResponse.value,
(val) => {
if (val) {
changeExpand(true);
} else {
changeExpand(false);
}
}
);
function handleActiveLayoutChange() { function handleActiveLayoutChange() {
isExpanded.value = true; isExpanded.value = true;
splitBoxSize.value = 0.6; splitBoxSize.value = 0.6;
@ -950,6 +1024,7 @@
const res = await props.createApi({ const res = await props.createApi({
...makeRequestParams(), ...makeRequestParams(),
...saveModalForm.value, ...saveModalForm.value,
path: isHttpProtocol.value ? saveModalForm.value.path : undefined,
...props.otherParams, ...props.otherParams,
}); });
requestVModel.value.id = res.id; requestVModel.value.id = res.id;
@ -979,13 +1054,10 @@
*/ */
async function handleSaveShortcut() { async function handleSaveShortcut() {
if (!requestVModel.value.isNew) { if (!requestVModel.value.isNew) {
if (hasAnyPermission([props.permissionMap.update])) {
// //
updateDebug(); updateDebug();
}
return; return;
} }
if (hasAnyPermission([props.permissionMap.create])) {
try { try {
if (!isHttpProtocol.value) { if (!isHttpProtocol.value) {
// //
@ -1007,7 +1079,6 @@
}); });
} }
} }
}
const isUrlError = ref(false); const isUrlError = ref(false);
function handleSelect(value: string | number | Record<string, any> | undefined) { function handleSelect(value: string | number | Record<string, any> | undefined) {

View File

@ -60,7 +60,7 @@
class="flex items-center justify-between gap-[24px]" class="flex items-center justify-between gap-[24px]"
> >
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">
<div :style="{ color: statusCodeColor }"> <div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor }">
{{ props.requestTaskResult.requestResults[0].responseResult.responseCode }} {{ props.requestTaskResult.requestResults[0].responseResult.responseCode }}
</div> </div>
<template #content> <template #content>
@ -225,6 +225,15 @@
function setActiveResponse(val: 'content' | 'result') { function setActiveResponse(val: 'content' | 'result') {
activeResponseType.value = val; activeResponseType.value = val;
} }
watch(
() => props.requestTaskResult,
(task) => {
if (task) {
setActiveResponse('result');
}
}
);
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -249,6 +249,7 @@
responseActiveTab: ResponseComposition.BODY, responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse), response: cloneDeep(defaultResponse),
isNew: true, isNew: true,
executeLoading: false,
}; };
const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]); const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<RequestParam>(debugTabs.value[0]); const activeDebug = ref<RequestParam>(debugTabs.value[0]);

View File

@ -6,21 +6,6 @@
{{ t('apiTestManagement.showSubdirectory') }} {{ t('apiTestManagement.showSubdirectory') }}
</div> </div>
<div class="flex items-center gap-[8px]"> <div class="flex items-center gap-[8px]">
<template v-if="!props.readOnly">
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
<template #icon>
<icon-location class="text-[var(--color-text-4)]" />
</template>
</a-button>
<MsSelect
v-model:model-value="checkedEnv"
mode="static"
:options="envOptions"
class="!w-[150px]"
:search-keys="['label']"
allow-search
/>
</template>
<a-input-search <a-input-search
v-model:model-value="keyword" v-model:model-value="keyword"
:placeholder="t('apiTestManagement.searchPlaceholder')" :placeholder="t('apiTestManagement.searchPlaceholder')"
@ -251,7 +236,6 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsSelect from '@/components/business/ms-select';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -392,14 +376,6 @@
width: 150, width: 150,
}, },
]; ];
if (!props.readOnly) {
const tableStore = useTableStore();
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
} else {
columns = columns.filter(
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
);
}
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getDefinitionPage, getDefinitionPage,
{ {
@ -758,6 +734,19 @@
function openApiTab(record: ApiDefinitionDetail) { function openApiTab(record: ApiDefinitionDetail) {
emit('openApiTab', record); emit('openApiTab', record);
} }
defineExpose({
loadApiList,
});
if (!props.readOnly) {
const tableStore = useTableStore();
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
} else {
columns = columns.filter(
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
);
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,15 +1,29 @@
<template> <template>
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]"> <div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]">
<MsEditableTab v-model:active-tab="activeApiTab" v-model:tabs="apiTabs" @add="addApiTab"> <MsEditableTab
v-model:active-tab="activeApiTab"
v-model:tabs="apiTabs"
@add="addApiTab"
@change="handleActiveTabChange"
>
<template #label="{ tab }"> <template #label="{ tab }">
<apiMethodName v-if="tab.id !== 'all'" :method="tab.method" class="mr-[4px]" /> <apiMethodName
v-if="tab.id !== 'all'"
:method="tab.protocol === 'HTTP' ? tab.method : tab.protocol"
class="mr-[4px]"
/>
<a-tooltip :content="tab.name || tab.label" :mouse-enter-delay="500">
<div class="one-line-text max-w-[144px]">
{{ tab.name || tab.label }} {{ tab.name || tab.label }}
</div>
</a-tooltip>
</template> </template>
</MsEditableTab> </MsEditableTab>
</div> </div>
<div v-show="activeApiTab.id === 'all'" class="flex-1"> <div v-show="activeApiTab.id === 'all'" class="flex-1">
<apiTable <apiTable
ref="apiTableRef"
:active-module="props.activeModule" :active-module="props.activeModule"
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:protocol="props.protocol" :protocol="props.protocol"
@ -112,18 +126,48 @@
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-dropdown @select="handleSelect"> <div class="mb-[8px] flex items-center">
<a-button type="outline"> <div class="text-[var(--color-text-2)]">
<div class="flex items-center gap-[8px]">
<icon-plus />
{{ t('apiTestManagement.addDependency') }} {{ t('apiTestManagement.addDependency') }}
</div> </div>
</a-button> <a-divider margin="4px" direction="vertical" />
<template #content> <MsButton
<a-doption value="pre">{{ t('apiTestManagement.preDependency') }}</a-doption> type="text"
<a-doption value="post">{{ t('apiTestManagement.postDependency') }}</a-doption> class="font-medium"
</template> :disabled="activeApiTab.preDependency.length === 0 && activeApiTab.postDependency.length === 0"
</a-dropdown> @click="clearAllDependency"
>
{{ t('apiTestManagement.clearSelected') }}
</MsButton>
</div>
<div class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<div class="flex items-center">
<div class="flex items-center gap-[4px] text-[var(--color-text-2)]">
{{ t('apiTestManagement.preDependency') }}
<div class="text-[rgb(var(--primary-5))]">
{{ activeApiTab.preDependency.length }}
</div>
{{ t('apiTestManagement.dependencyUnit') }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="handleDddDependency('pre')">
{{ t('apiTestManagement.addPreDependency') }}
</MsButton>
</div>
<div class="mt-[8px] flex items-center">
<div class="flex items-center gap-[4px] text-[var(--color-text-2)]">
{{ t('apiTestManagement.postDependency') }}
<div class="text-[rgb(var(--primary-5))]">
{{ activeApiTab.postDependency.length }}
</div>
{{ t('apiTestManagement.dependencyUnit') }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="handleDddDependency('post')">
{{ t('apiTestManagement.addPostDependency') }}
</MsButton>
</div>
</div>
</div> </div>
</template> </template>
</MsSplitBox> </MsSplitBox>
@ -141,6 +185,7 @@
import { FormInstance, Message } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue'; import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue'; // import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
@ -188,13 +233,14 @@
); );
const props = defineProps<{ const props = defineProps<{
allCount: number;
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
moduleTree: ModuleTreeNode[]; // moduleTree: ModuleTreeNode[]; //
protocol: string; protocol: string;
}>(); }>();
const emit = defineEmits(['addDone']); const emit = defineEmits(['addDone']);
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
@ -202,7 +248,7 @@
const apiTabs = ref<RequestParam[]>([ const apiTabs = ref<RequestParam[]>([
{ {
id: 'all', id: 'all',
label: `${t('apiTestManagement.allApi')}(${props.allCount})`, label: t('apiTestManagement.allApi'),
closable: false, closable: false,
} as RequestParam, } as RequestParam,
]); ]);
@ -214,7 +260,6 @@
} }
} }
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
watch( watch(
() => activeApiTab.value.id, () => activeApiTab.value.id,
() => { () => {
@ -338,6 +383,10 @@
response: cloneDeep(defaultResponse), response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)], responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true, isNew: true,
mode: 'definition',
executeLoading: false,
preDependency: [], //
postDependency: [], //
}; };
function addApiTab(defaultProps?: Partial<TabItem>) { function addApiTab(defaultProps?: Partial<TabItem>) {
@ -345,7 +394,7 @@
apiTabs.value.push({ apiTabs.value.push({
...cloneDeep(defaultDefinitionParams), ...cloneDeep(defaultDefinitionParams),
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule, moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
label: t('apiTestDebug.newApi'), label: t('apiTestManagement.newApi'),
id, id,
isNew: !defaultProps?.id, // tabidid isNew: !defaultProps?.id, // tabidid
...defaultProps, ...defaultProps,
@ -353,6 +402,14 @@
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam; activeApiTab.value = apiTabs.value[apiTabs.value.length - 1] as RequestParam;
} }
const apiTableRef = ref<InstanceType<typeof apiTable>>();
function handleActiveTabChange(item: TabItem) {
if (item.id === 'all') {
apiTableRef.value?.loadApiList();
}
}
const loading = ref(false); const loading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail) { async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail) {
const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id); const isLoadedTabIndex = apiTabs.value.findIndex((e) => e.id === apiInfo.id);
@ -403,7 +460,7 @@
const showAddDependencyDrawer = ref(false); const showAddDependencyDrawer = ref(false);
const addDependencyMode = ref<'pre' | 'post'>('pre'); const addDependencyMode = ref<'pre' | 'post'>('pre');
function handleSelect(value: string | number | Record<string, any> | undefined) { function handleDddDependency(value: string | number | Record<string, any> | undefined) {
switch (value) { switch (value) {
case 'pre': case 'pre':
addDependencyMode.value = 'pre'; addDependencyMode.value = 'pre';
@ -418,6 +475,11 @@
} }
} }
function clearAllDependency() {
activeApiTab.value.preDependency = [];
activeApiTab.value.postDependency = [];
}
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>(); const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const activeApiTabFormRef = ref<FormInstance>(); const activeApiTabFormRef = ref<FormInstance>();
@ -442,6 +504,9 @@
activeApiTab.value.name = res.name; activeApiTab.value.name = res.name;
activeApiTab.value.label = res.name; activeApiTab.value.label = res.name;
activeApiTab.value.url = res.path; activeApiTab.value.url = res.path;
if (typeof refreshModuleTree === 'function') {
refreshModuleTree();
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

@ -5,14 +5,13 @@
ref="apiRef" ref="apiRef"
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
:active-module="props.activeModule" :active-module="props.activeModule"
:all-count="props.allCount"
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:protocol="protocol" :protocol="protocol"
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane> <a-tab-pane key="case" title="CASE" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane> <a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane>
<a-tab-pane key="doc" :title="t('apiTestManagement.doc')" class="ms-api-tab-pane"> </a-tab-pane> <!-- <a-tab-pane key="doc" title="API Docs" class="ms-api-tab-pane"> </a-tab-pane> -->
<template #extra> <template #extra>
<div class="flex items-center gap-[8px] pr-[24px]"> <div class="flex items-center gap-[8px] pr-[24px]">
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]"> <a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
@ -42,7 +41,6 @@
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
const props = defineProps<{ const props = defineProps<{
allCount: number;
activeModule: string; activeModule: string;
offspringIds: string[]; offspringIds: string[];
protocol: string; protocol: string;

View File

@ -526,9 +526,13 @@
initModuleCount(); initModuleCount();
}); });
async function refresh() {
await initModules();
initModuleCount();
}
defineExpose({ defineExpose({
initModules, refresh,
initModuleCount,
}); });
</script> </script>

View File

@ -4,6 +4,7 @@
<template #first> <template #first>
<div class="p-[24px]"> <div class="p-[24px]">
<moduleTree <moduleTree
ref="moduleTreeRef"
:active-node-id="activeApi?.id" :active-node-id="activeApi?.id"
@init="handleModuleInit" @init="handleModuleInit"
@new-api="newApi" @new-api="newApi"
@ -40,7 +41,6 @@
<management <management
ref="managementRef" ref="managementRef"
:module-tree="folderTree" :module-tree="folderTree"
:all-count="allCount"
:active-module="activeModule" :active-module="activeModule"
:offspring-ids="offspringIds" :offspring-ids="offspringIds"
:protocol="protocol" :protocol="protocol"
@ -65,11 +65,11 @@
const activeModule = ref<string>('all'); const activeModule = ref<string>('all');
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
const allCount = ref(0);
const importDrawerVisible = ref(false); const importDrawerVisible = ref(false);
const offspringIds = ref<string[]>([]); const offspringIds = ref<string[]>([]);
const protocol = ref('HTTP'); const protocol = ref('HTTP');
const activeApi = ref<RequestParam>(); const activeApi = ref<RequestParam>();
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const managementRef = ref<InstanceType<typeof management>>(); const managementRef = ref<InstanceType<typeof management>>();
function handleModuleInit(tree, _protocol: string) { function handleModuleInit(tree, _protocol: string) {
@ -98,7 +98,13 @@
protocol.value = val; protocol.value = val;
} }
function refreshModuleTree() {
moduleTreeRef.value?.refresh();
}
/** 向子孙组件提供方法 */
provide('setActiveApi', setActiveApi); provide('setActiveApi', setActiveApi);
provide('refreshModuleTree', refreshModuleTree);
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -1 +1,103 @@
export default {}; export default {
'apiTestManagement.newApi': 'Create api',
'apiTestManagement.importApi': 'Import api',
'apiTestManagement.fileImport': 'Import file',
'apiTestManagement.timeImport': 'Scheduled import',
'apiTestManagement.addSubModule': 'Add submodule',
'apiTestManagement.allApi': 'All api',
'apiTestManagement.searchTip': 'Please enter module/api name',
'apiTestManagement.moveSearchTip': 'Please enter the module name to search',
'apiTestManagement.noMatchModule': 'No matching module/api yet',
'apiTestManagement.execute': 'Execute',
'apiTestManagement.share': 'Share API',
'apiTestManagement.shareModule': 'Share module',
'apiTestManagement.doc': 'Document',
'apiTestManagement.closeAll': 'Close all tabs',
'apiTestManagement.closeOther': 'Close other tabs',
'apiTestManagement.showSubdirectory': 'Show subdirectory use case',
'apiTestManagement.searchPlaceholder': 'Enter ID/name/api path search',
'apiTestManagement.apiName': 'Api name',
'apiTestManagement.apiType': 'Api type',
'apiTestManagement.apiStatus': 'Status',
'apiTestManagement.path': 'Path',
'apiTestManagement.version': 'Version',
'apiTestManagement.createTime': 'Creation time',
'apiTestManagement.updateTime': 'Update time',
'apiTestManagement.deprecate': 'Deprecated',
'apiTestManagement.processing': 'Processing',
'apiTestManagement.debugging': 'Debugging',
'apiTestManagement.done': 'Completed',
'apiTestManagement.deleteApiTipTitle': 'Are you sure you want to delete {name}?',
'apiTestManagement.deleteApiTip':
'After deletion, the interface will be placed in the recycle bin, where data recovery can be performed',
'apiTestManagement.batchDeleteApiTip': 'Are you sure you want to delete {count} selected interfaces?',
'apiTestManagement.batchModalSubTitle': '({count} interfaces selected)',
'apiTestManagement.chooseAttr': 'Select properties',
'apiTestManagement.attrRequired': 'Property cannot be empty',
'apiTestManagement.batchUpdate': 'Batch update to',
'apiTestManagement.valueRequired': 'Attribute value cannot be empty',
'apiTestManagement.batchMoveConfirm': 'Move to selected module',
'apiTestManagement.belongModule': 'Belonging module',
'apiTestManagement.importMode': 'Import mode',
'apiTestManagement.importModeTip1': 'Cover:',
'apiTestManagement.importModeTip2':
'1.If the request type + path is the same for the same interface, if the request parameter content is inconsistent, it will be overwritten.',
'apiTestManagement.importModeTip3':
'2.The same interface request type + path are the same, and the request parameter content is the same and does not change.',
'apiTestManagement.importModeTip4':
'3.If the interface is not the same and the request type + path is the same, add it',
'apiTestManagement.importModeTip5': 'Not covered:',
'apiTestManagement.importModeTip6':
'1.If the same interface request type + path are the same, no changes will be made.',
'apiTestManagement.importModeTip7': '2.If the request type + path are not the same for the same interface, add a new',
'apiTestManagement.cover': 'Cover',
'apiTestManagement.uncover': 'Not covered',
'apiTestManagement.moreSetting': 'More settings',
'apiTestManagement.importType': 'Import method',
'apiTestManagement.urlImport': 'URL import',
'apiTestManagement.syncImportCase': 'Synchronous import interface use case',
'apiTestManagement.syncUpdateDirectory': 'Synchronously update the directory where the interface is located',
'apiTestManagement.importSwaggerFileTip1': 'Supports json files of Swagger 3.0 version,',
'apiTestManagement.importSwaggerFileTip2':
'It is recommended to convert 2.0 files to 3.0 on the official website and then import them.',
'apiTestManagement.importSwaggerFileTip3': ', the size does not exceed 50M',
'apiTestManagement.urlImportPlaceholder': 'Please enter the OpenAPI/Swagger URL',
'apiTestManagement.swaggerURLRequired': 'SwaggerURL cannot be empty',
'apiTestManagement.basicAuth': 'Basic Authentication',
'apiTestManagement.account': 'Account',
'apiTestManagement.accountRequired': 'Account cannot be empty',
'apiTestManagement.password': 'Password',
'apiTestManagement.passwordRequired': 'Password can not be blank',
'apiTestManagement.taskName': 'Task name',
'apiTestManagement.taskNamePlaceholder': 'Please enter a task name',
'apiTestManagement.taskNameRequired': 'Task name cannot be empty',
'apiTestManagement.syncFrequency': 'Sync frequency',
'apiTestManagement.timeTaskList': 'Scheduled task list',
'apiTestManagement.timeTaskHour': '(per hour)',
'apiTestManagement.timeTaskSixHour': '(every 6 hours)',
'apiTestManagement.timeTaskTwelveHour': '(every 12 hours)',
'apiTestManagement.timeTaskDay': '(every day)',
'apiTestManagement.customFrequency': 'Custom frequency',
'apiTestManagement.case': 'Case',
'apiTestManagement.definition': 'Definition',
'apiTestManagement.debug': 'Debug',
'apiTestManagement.addDependency': 'Select dependency use case',
'apiTestManagement.clearSelected': 'Clear selected use cases',
'apiTestManagement.preDependency': 'Front interface',
'apiTestManagement.addPreDependency': 'Add pre-dependency',
'apiTestManagement.postDependency': 'Rear interface',
'apiTestManagement.addPostDependency': 'Add post-dependency',
'apiTestManagement.dependencyUnit': 'item',
'apiTestManagement.saveAsCase': 'Save as new use case',
'apiTestManagement.apiNamePlaceholder': 'Please enter the interface name',
'apiTestManagement.executeResult': 'Execution result',
'apiTestManagement.setDefault': 'Set as Default',
'apiTestManagement.confirmDelete': 'Are you sure you want to delete {name}?',
'apiTestManagement.response': 'Response{count}',
'apiTestManagement.responseCode': 'Response code',
'apiTestManagement.dynamicConversion': 'Dynamic conversion',
'apiTestManagement.expandApi': 'Show all requests',
'apiTestManagement.collapseApi': 'Hide all requests',
'apiTestManagement.paramName': 'Parameter name',
'apiTestManagement.paramVal': 'Parameter value',
};

View File

@ -19,7 +19,6 @@ export default {
'apiTestManagement.apiName': '接口名称', 'apiTestManagement.apiName': '接口名称',
'apiTestManagement.apiType': '请求类型', 'apiTestManagement.apiType': '请求类型',
'apiTestManagement.apiStatus': '状态', 'apiTestManagement.apiStatus': '状态',
'apiTestManagement.responsiblePerson': '责任人',
'apiTestManagement.path': '路径', 'apiTestManagement.path': '路径',
'apiTestManagement.version': '版本', 'apiTestManagement.version': '版本',
'apiTestManagement.createTime': '创建时间', 'apiTestManagement.createTime': '创建时间',
@ -75,11 +74,14 @@ export default {
'apiTestManagement.customFrequency': '自定义频率', 'apiTestManagement.customFrequency': '自定义频率',
'apiTestManagement.case': '用例', 'apiTestManagement.case': '用例',
'apiTestManagement.definition': '定义', 'apiTestManagement.definition': '定义',
'apiTestManagement.addDependency': '添加依赖关系', 'apiTestManagement.debug': '调试',
'apiTestManagement.preDependency': '前置依赖', 'apiTestManagement.addDependency': '选择依赖关系用例',
'apiTestManagement.clearSelected': '清空已选用例',
'apiTestManagement.preDependency': '前置接口',
'apiTestManagement.addPreDependency': '添加前置依赖', 'apiTestManagement.addPreDependency': '添加前置依赖',
'apiTestManagement.postDependency': '后置依赖', 'apiTestManagement.postDependency': '后置接口',
'apiTestManagement.addPostDependency': '添加后置依赖', 'apiTestManagement.addPostDependency': '添加后置依赖',
'apiTestManagement.dependencyUnit': '条',
'apiTestManagement.saveAsCase': '保存为新用例', 'apiTestManagement.saveAsCase': '保存为新用例',
'apiTestManagement.apiNamePlaceholder': '请输入接口名称', 'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
'apiTestManagement.executeResult': '执行结果', 'apiTestManagement.executeResult': '执行结果',