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"
:scroll="{ minWidth: '700px' }"
: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)"
>
<template #expression="{ record, rowIndex }">
@ -99,7 +99,7 @@
:columns="xPathColumns"
:scroll="{ minWidth: '700px' }"
: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)"
>
<template #expression="{ record, rowIndex }">
@ -193,7 +193,7 @@
:span-method="documentSpanMethod"
is-tree-table
@tree-delete="deleteAllParam"
@change="(data, isInit) => handleChange(data, !!isInit, 'document')"
@change="(data) => handleChange(data, 'document')"
>
<template #matchValueDelete="{ record }">
<icon-minus-circle
@ -234,7 +234,7 @@
:columns="xPathColumns"
:scroll="{ minWidth: '700px' }"
: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)"
>
<template #expression="{ record, rowIndex }">
@ -473,10 +473,7 @@
matchValue: '',
enable: true,
};
const handleChange = (data: any[], isInit: boolean, type: string) => {
if (isInit) {
return;
}
const handleChange = (data: any[], type: string) => {
if (type === 'jsonPath') {
innerParams.value.jsonPath = data;
} else if (type === 'xPath') {

View File

@ -20,7 +20,13 @@
@click="handleTabClick(tab)"
>
<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>
<MsButton
v-if="props.atLeastOne ? props.tabs.length > 1 && tab.closable : tab.closable !== false"

View File

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

View File

@ -10,7 +10,7 @@
</a-empty>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<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]">
<a-select
v-if="requestVModel.isNew"
@ -54,41 +54,77 @@
/>
</a-input-group>
</div>
<div class="ml-[16px]">
<a-dropdown-button
v-if="!requestVModel.executeLoading && hasAnyPermission([props.permissionMap.execute])"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
<div>
<a-radio-group
v-if="props.isDefinition"
v-model:model-value="requestVModel.mode"
type="button"
class="mr-[12px]"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template v-if="hasLocalExec" #icon>
<icon-down />
</template>
<template v-if="hasLocalExec" #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
<a-dropdown
v-if="props.isDefinition && hasAnyPermission([props.permissionMap.create, props.permissionMap.update])"
:loading="saveLoading || (isHttpProtocol && !requestVModel.url)"
@select="handleSelect"
<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-button type="secondary">
<a-dropdown-button
v-if="!requestVModel.executeLoading"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template v-if="hasLocalExec" #icon>
<icon-down />
</template>
<template v-if="hasLocalExec" #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-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
v-if="requestVModel.mode === 'debug'"
:loading="saveLoading || (isHttpProtocol && !requestVModel.url)"
@select="handleSelect"
>
<a-button type="secondary">
{{ t('common.save') }}
</a-button>
<template #content>
<a-doption value="save">{{ t('common.save') }}</a-doption>
<a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption>
</template>
</a-dropdown>
<!-- 接口定义-定义模式直接保存接口定义 -->
<a-button v-else type="primary" @click="() => handleSelect('save')">
{{ t('common.save') }}
</a-button>
<template #content>
<a-doption value="save">{{ t('common.save') }}</a-doption>
<a-doption value="saveAsCase">{{ t('apiTestManagement.saveAsCase') }}</a-doption>
</template>
</a-dropdown>
</template>
<!-- 接口调试支持快捷保存 -->
<a-button
v-else
v-permission="[props.permissionMap.create, props.permissionMap.update]"
v-else-if="
requestVModel.isNew
? hasAnyPermission([props.permissionMap.create])
: hasAnyPermission([props.permissionMap.update])
"
type="secondary"
:disabled="isHttpProtocol && !requestVModel.url"
:loading="saveLoading"
@ -102,7 +138,7 @@
</div>
</div>
</div>
<div ref="splitContainerRef" :class="splitContainerClass">
<div ref="splitContainerRef" class="h-[calc(100%-40px)]">
<MsSplitBox
ref="splitBoxRef"
v-model:size="splitBoxSize"
@ -138,7 +174,13 @@
v-model:api="fApi"
:rule="currentPluginScript"
:option="currentPluginOptions"
@change="handlePluginFormChange"
@change="
() => {
if (isInitPluginForm) {
handlePluginFormChange();
}
}
"
/>
</a-spin>
<httpHeader
@ -329,6 +371,8 @@
isNew: boolean;
protocol: string;
activeTab: RequestComposition;
mode?: 'definition' | 'debug';
executeLoading: boolean; // loading
}
export type RequestParam = ExecuteApiRequestFullParams & {
responseDefinition?: ResponseItem[];
@ -364,7 +408,7 @@
const loading = defineModel<boolean>('detailLoading', { default: false });
const requestVModel = defineModel<RequestParam>('request', { required: true });
requestVModel.value.executeLoading = false; // loading
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const temporaryResponseMap = {}; // websockettab
const isInitPluginForm = ref(false);
@ -392,7 +436,7 @@
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
// RequestComposition.ASSERTION, TODO:
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
@ -443,10 +487,20 @@
// tab
const contentTabList = computed(() => {
if (isHttpProtocol.value) {
if (props.isDefinition) {
//
return requestVModel.value.mode === 'debug'
? httpContentTabList
: httpContentTabList.filter((e) => !commonContentTabKey.includes(e.value));
}
//
return props.isDefinition
? httpContentTabList
: httpContentTabList.filter((e) => e.value !== RequestComposition.ASSERTION);
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))];
});
@ -536,6 +590,24 @@
handleActiveDebugChange();
}, 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];
if (tempForm || !requestVModel.value.isNew) {
//
fApi.value?.nextRefresh(() => {
fApi.value?.reload(currentPluginScript.value);
const formData = tempForm || requestVModel.value;
if (fApi.value) {
const formData = tempForm || requestVModel.value;
if (fApi.value) {
fApi.value.nextTick(() => {
const form = {};
if (props.isDefinition) {
// 使
pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields?.forEach((key) => {
form[key] = formData[key];
});
} else {
pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields?.forEach((key) => {
form[key] = formData[key];
});
}
controlPluginFormFields().forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(form);
}
});
nextTick(() => {
isInitPluginForm.value = true;
});
setTimeout(() => {
// 300ms handlePluginFormChange
isInitPluginForm.value = true;
}, 300);
});
}
} else {
// form-create tab
fApi.value?.nextTick(() => {
controlPluginFormFields();
});
nextTick(() => {
// form-create tab
fApi.value?.resetFields();
});
}
@ -582,6 +650,7 @@
return;
}
pluginError.value = false;
isInitPluginForm.value = false;
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
setPluginFormData();
//
@ -672,12 +741,6 @@
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0);
const splitContainerClass = computed(() => {
if (!showResponse.value) {
return 'h-full';
}
return 'h-[calc(100%-40px)]';
});
watch(
() => splitBoxSize.value,
@ -716,6 +779,17 @@
}
}
watch(
() => showResponse.value,
(val) => {
if (val) {
changeExpand(true);
} else {
changeExpand(false);
}
}
);
function handleActiveLayoutChange() {
isExpanded.value = true;
splitBoxSize.value = 0.6;
@ -950,6 +1024,7 @@
const res = await props.createApi({
...makeRequestParams(),
...saveModalForm.value,
path: isHttpProtocol.value ? saveModalForm.value.path : undefined,
...props.otherParams,
});
requestVModel.value.id = res.id;
@ -979,33 +1054,29 @@
*/
async function handleSaveShortcut() {
if (!requestVModel.value.isNew) {
if (hasAnyPermission([props.permissionMap.update])) {
//
updateDebug();
}
//
updateDebug();
return;
}
if (hasAnyPermission([props.permissionMap.create])) {
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
saveModalForm.value = {
name: requestVModel.value.name || '',
path: requestVModel.value.url || '',
moduleId: 'root',
};
saveModalVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
//
requestVModel.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
saveModalForm.value = {
name: requestVModel.value.name || '',
path: requestVModel.value.url || '',
moduleId: 'root',
};
saveModalVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
//
requestVModel.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
}

View File

@ -60,7 +60,7 @@
class="flex items-center justify-between gap-[24px]"
>
<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 }}
</div>
<template #content>
@ -225,6 +225,15 @@
function setActiveResponse(val: 'content' | 'result') {
activeResponseType.value = val;
}
watch(
() => props.requestTaskResult,
(task) => {
if (task) {
setActiveResponse('result');
}
}
);
</script>
<style lang="less">

View File

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

View File

@ -6,21 +6,6 @@
{{ t('apiTestManagement.showSubdirectory') }}
</div>
<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
v-model:model-value="keyword"
:placeholder="t('apiTestManagement.searchPlaceholder')"
@ -251,7 +236,6 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
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 apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -392,14 +376,6 @@
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(
getDefinitionPage,
{
@ -758,6 +734,19 @@
function openApiTab(record: ApiDefinitionDetail) {
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>
<style lang="less" scoped>

View File

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

View File

@ -5,14 +5,13 @@
ref="apiRef"
:module-tree="props.moduleTree"
:active-module="props.activeModule"
:all-count="props.allCount"
:offspring-ids="props.offspringIds"
:protocol="protocol"
/>
</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="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>
<div class="flex items-center gap-[8px] pr-[24px]">
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]">
@ -42,7 +41,6 @@
import { ModuleTreeNode } from '@/models/common';
const props = defineProps<{
allCount: number;
activeModule: string;
offspringIds: string[];
protocol: string;

View File

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

View File

@ -4,6 +4,7 @@
<template #first>
<div class="p-[24px]">
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeApi?.id"
@init="handleModuleInit"
@new-api="newApi"
@ -40,7 +41,6 @@
<management
ref="managementRef"
:module-tree="folderTree"
:all-count="allCount"
:active-module="activeModule"
:offspring-ids="offspringIds"
:protocol="protocol"
@ -65,11 +65,11 @@
const activeModule = ref<string>('all');
const folderTree = ref<ModuleTreeNode[]>([]);
const allCount = ref(0);
const importDrawerVisible = ref(false);
const offspringIds = ref<string[]>([]);
const protocol = ref('HTTP');
const activeApi = ref<RequestParam>();
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const managementRef = ref<InstanceType<typeof management>>();
function handleModuleInit(tree, _protocol: string) {
@ -98,7 +98,13 @@
protocol.value = val;
}
function refreshModuleTree() {
moduleTreeRef.value?.refresh();
}
/** 向子孙组件提供方法 */
provide('setActiveApi', setActiveApi);
provide('refreshModuleTree', refreshModuleTree);
</script>
<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.apiType': '请求类型',
'apiTestManagement.apiStatus': '状态',
'apiTestManagement.responsiblePerson': '责任人',
'apiTestManagement.path': '路径',
'apiTestManagement.version': '版本',
'apiTestManagement.createTime': '创建时间',
@ -75,11 +74,14 @@ export default {
'apiTestManagement.customFrequency': '自定义频率',
'apiTestManagement.case': '用例',
'apiTestManagement.definition': '定义',
'apiTestManagement.addDependency': '添加依赖关系',
'apiTestManagement.preDependency': '前置依赖',
'apiTestManagement.debug': '调试',
'apiTestManagement.addDependency': '选择依赖关系用例',
'apiTestManagement.clearSelected': '清空已选用例',
'apiTestManagement.preDependency': '前置接口',
'apiTestManagement.addPreDependency': '添加前置依赖',
'apiTestManagement.postDependency': '后置依赖',
'apiTestManagement.postDependency': '后置接口',
'apiTestManagement.addPostDependency': '添加后置依赖',
'apiTestManagement.dependencyUnit': '条',
'apiTestManagement.saveAsCase': '保存为新用例',
'apiTestManagement.apiNamePlaceholder': '请输入接口名称',
'apiTestManagement.executeResult': '执行结果',