refactor(接口管理): 接口定义-布局重构&另存为用例

This commit is contained in:
baiqi 2024-03-13 12:00:49 +08:00 committed by 刘瑞斌
parent ad99fd8f92
commit 63d905dc60
37 changed files with 996 additions and 765 deletions

View File

@ -1,5 +1,6 @@
import MSR from '@/api/http/index';
import {
AddCaseUrl,
AddDefinitionScheduleUrl,
AddDefinitionUrl,
AddModuleUrl,
@ -53,6 +54,7 @@ import {
import { ExecuteRequestParams } from '@/models/apiTest/common';
import {
AddApiCaseParams,
ApiCaseBatchEditParams,
ApiCaseBatchParams,
ApiCaseDetail,
@ -342,3 +344,8 @@ export function batchEditCase(data: ApiCaseBatchEditParams) {
export function dragSort(data: DragSortParams) {
return MSR.post({ url: SortCaseUrl, data });
}
// 添加接口用例
export function addCase(data: AddApiCaseParams) {
return MSR.post({ url: AddCaseUrl, data });
}

View File

@ -34,6 +34,7 @@ export const ToggleFollowDefinitionUrl = '/api/definition/follow'; // 接口定
export const OperationHistoryUrl = '/api/definition/operation-history'; // 接口定义-变更历史
export const SaveOperationHistoryUrl = '/api/definition/operation-history/save'; // 接口定义-另存变更历史为指定版本
export const RecoverOperationHistoryUrl = '/api/definition/operation-history/recover'; // 接口定义-变更历史恢复
export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取接口引用关系
/**
* Mock
@ -42,11 +43,6 @@ export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表
export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态
export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock
/**
*
*/
export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取接口引用关系
/**
* api回收站
*/
@ -65,3 +61,4 @@ export const DeleteCaseUrl = '/api/case/delete'; // 删除接口用例
export const BatchDeleteCaseUrl = '/api/case/batch/delete'; // 批量删除接口用例
export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例
export const SortCaseUrl = '/api/case/edit/pos'; // 接口用例拖拽
export const AddCaseUrl = '/api/case/add'; // 添加用例

View File

@ -243,9 +243,6 @@
});
const buttonDropDownVisible = ref(false);
watchEffect(() => {
console.log('innerFileList', innerFileList.value);
});
onBeforeMount(() => {
//
const defaultFiles = innerFileList.value.filter((item) => item) || [];

View File

@ -193,9 +193,6 @@
<div class="ms-params-popover-title !mb-[8px]">
{{ t('ms.paramsInput.value') }}
</div>
<div class="ms-params-popover-subtitle">
{{ t('ms.paramsInput.value') }}
</div>
<div class="ms-params-popover-value mb-[8px]">
{{ innerValue }}
</div>
@ -702,7 +699,7 @@
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
color: var(--color-text-1);
}
.ms-params-popover-value {
min-width: 100px;

View File

@ -21,7 +21,10 @@
:style="{ width: item.width }"
>
<div class="text-[var(--color-text-4)]">{{ t(item.locale) }}</div>
<MsTagGroup v-if="Array.isArray(item.value)" :tag-list="item.value" size="small" is-string-tag />
<div v-if="Array.isArray(item.value)">
<MsTagGroup v-if="item.value.length > 0" :tag-list="item.value" size="small" is-string-tag />
<div v-else>-</div>
</div>
<slot v-else :name="item.key" :value="item.value">
<a-tooltip :content="item.value" :disabled="isEmpty(item.value)">
<div class="text-[var(--color-text-1)]">{{ item.value || '-' }}</div>

View File

@ -21,13 +21,13 @@
>
<div
v-if="props.direction === 'horizontal' && props.expandDirection === 'right' && !props.disabled"
class="absolute right-0 h-full w-[16px]"
class="absolute right-0 h-full w-[12px]"
>
<div class="expand-icon expand-icon--left" @click="() => changeExpand()">
<MsIcon
:type="isExpanded ? 'icon-icon_up-left_outlined' : 'icon-icon_down-right_outlined'"
class="text-[var(--color-text-brand)]"
size="12"
class="!w-auto text-[var(--color-text-brand)]"
size="9"
/>
</div>
</div>
@ -43,13 +43,13 @@
<div class="ms-split-box-second">
<div
v-if="props.direction === 'horizontal' && props.expandDirection === 'left' && !props.disabled"
class="absolute h-full w-[16px]"
class="absolute h-full w-[12px]"
>
<div class="expand-icon" @click="() => changeExpand()">
<MsIcon
:type="isExpanded ? 'icon-icon_up-left_outlined' : 'icon-icon_down-right_outlined'"
class="text-[var(--color-text-brand)]"
size="12"
class="!w-auto text-[var(--color-text-brand)]"
size="9"
/>
</div>
</div>

View File

@ -207,3 +207,9 @@ export enum RequestExtractResultMatchingRule {
RANDOM = 'RANDOM', // 随机匹配
SPECIFIC = 'SPECIFIC', // 指定匹配
}
// 接口用例状态
export enum RequestCaseStatus {
DEPRECATED = 'DEPRECATED',
PROCESSING = 'PROCESSING',
DONE = 'DONE',
}

View File

@ -131,4 +131,5 @@ export default {
'common.unFollowSuccess': 'Unfollow successfully',
'common.share': 'Share',
'common.notRemind': `Don't remind again`,
'common.status': 'Status',
};

View File

@ -134,4 +134,5 @@ export default {
'common.unFollowSuccess': '取消关注成功',
'common.share': '分享',
'common.notRemind': '不再提醒',
'common.status': '状态',
};

View File

@ -331,3 +331,11 @@ export interface ApiCaseBatchEditParams extends ApiCaseBatchParams {
environmentId?: string;
type: string;
}
// 添加用例参数
export interface AddApiCaseParams extends ExecuteRequestParams {
name: string;
priority: string;
status: string;
apiDefinitionId: string | number;
tags: string[];
}

View File

@ -51,6 +51,7 @@ export interface CommonParams {
[key: string]: any;
}
export interface EnvConfig {
id?: string;
commonParams?: CommonParams;
commonVariables: EnvConfigItem[];
httpConfig: EnvConfigItem[];

View File

@ -273,7 +273,7 @@
</a-tooltip>
</div>
<paramTable
v-model:params="condition.extractParams"
:params="condition.extractParams"
:columns="sqlSourceColumns"
:selectable="false"
:default-param-item="defaultKeyValueParamItem"
@ -301,6 +301,7 @@
mode="button"
:step="100"
:min="0"
:precision="0"
class="w-[160px]"
model-event="input"
/>
@ -309,7 +310,7 @@
<div v-else-if="condition.processorType === RequestConditionProcessor.EXTRACT">
<paramTable
ref="extractParamsTableRef"
v-model:params="condition.extractors"
:params="condition.extractors"
:default-param-item="defaultExtractParamItem"
:columns="extractParamsColumns"
:selectable="false"
@ -842,8 +843,10 @@ if (!result){
const protocolList = ref<ProtocolItem[]>([]);
onBeforeMount(async () => {
try {
// TODO:
protocolList.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
});
@ -899,12 +902,6 @@ if (!result){
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;

View File

@ -5,7 +5,13 @@ import {
KeyValueParam,
ResponseDefinition,
} from '@/models/apiTest/common';
import { RequestContentTypeEnum, RequestParamsType, ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
import {
RequestCaseStatus,
RequestContentTypeEnum,
RequestParamsType,
ResponseBodyFormat,
ResponseComposition,
} from '@/enums/apiEnum';
// 请求 body 参数表格默认行的值
export const defaultBodyParamsItem: ExecuteRequestFormBodyFormValue = {
@ -83,3 +89,18 @@ export const defaultKeyValueParamItem: KeyValueParam = {
// 请求的响应 response 的响应状态码集合
export const statusCodes = [200, 201, 202, 203, 204, 205, 400, 401, 402, 403, 404, 405, 500, 501, 502, 503, 504, 505];
// 用例等级选项
export const casePriorityOptions = [
{ label: 'P0', value: 'P0' },
{ label: 'P1', value: 'P1' },
{ label: 'P2', value: 'P2' },
{ label: 'P3', value: 'P3' },
];
// 用例状态选项
export const caseStatusOptions = [
{ label: 'apiTestManagement.processing', value: RequestCaseStatus.PROCESSING },
{ label: 'apiTestManagement.deprecate', value: RequestCaseStatus.DEPRECATED },
{ label: 'apiTestManagement.done', value: RequestCaseStatus.DONE },
];

View File

@ -66,12 +66,6 @@
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;

View File

@ -1,5 +1,5 @@
<template>
<MsFormTable v-bind="props" :data="paramsData">
<MsFormTable v-bind="props" :data="paramsData" @change="handleFormTableChange">
<!-- 展开行-->
<template #expand-icon="{ record }">
<div class="flex flex-row items-center gap-[2px] text-[var(--color-text-4)]">
@ -341,7 +341,7 @@
<span v-else></span>
</template>
<template #host="{ record }">
<MsTagGroup
<MsTagsGroup
v-if="Array.isArray(record.domain)"
:tag-list="getDomain(record.domain)"
size="small"
@ -459,7 +459,6 @@
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
@ -495,7 +494,7 @@
const props = withDefaults(
defineProps<{
params?: any[];
params?: Record<string, any>[];
defaultParamItem?: Record<string, any>; //
columns: ParamTableColumn[];
scroll?: {
@ -548,7 +547,7 @@
}
);
const emit = defineEmits<{
(e: 'change', data: any[]): void; //
(e: 'change', data: Record<string, any>[], isInit?: boolean): void; //
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
(e: 'projectChange', projectId: string): void;
(e: 'treeDelete', record: Record<string, any>): void;
@ -557,12 +556,10 @@
const appStore = useAppStore();
const { t } = useI18n();
const paramsData = ref<any[]>(props.params);
const paramsData = ref<Record<string, any>[]>([]);
function emitChange(from: string, isInit?: boolean) {
if (!isInit) {
emit('change', paramsData.value);
}
emit('change', paramsData.value, isInit);
}
const paramsLength = computed(() => paramsData.value.length);
@ -659,28 +656,28 @@
const hostVisible = ref(false);
const hostData = ref<any[]>([]);
const hostColumn = [
{
title: t('project.environmental.http.host'),
dataIndex: 'host',
showTooltip: true,
width: 300,
},
{
title: t('project.environmental.http.desc'),
dataIndex: 'description',
},
];
// const hostColumn = [
// {
// title: t('project.environmental.http.host'),
// dataIndex: 'host',
// showTooltip: true,
// width: 300,
// },
// {
// title: t('project.environmental.http.desc'),
// dataIndex: 'description',
// },
// ];
const showHostModal = (record: Record<string, any>) => {
hostVisible.value = true;
hostData.value = record.domain || [];
};
const hostModalClose = () => {
hostVisible.value = false;
hostData.value = [];
};
// const hostModalClose = () => {
// hostVisible.value = false;
// hostData.value = [];
// };
watchEffect(() => {
if (props.columns.some((e) => e.dataIndex === 'projectId')) {
@ -698,6 +695,7 @@
*/
function addTableLine(rowIndex: number, addLineDisabled?: boolean, isInit?: boolean) {
if (addLineDisabled) {
emitChange('addTableLine addLineDisabled', isInit);
return;
}
if (rowIndex === paramsData.value.length - 1) {
@ -708,8 +706,8 @@
...cloneDeep(props.defaultParamItem), //
enable: true, //
} as any);
emitChange('addTableLine', isInit);
}
emitChange('addTableLine', isInit);
handleMustContainColChange(true);
handleTypeCheckingColChange(true);
}
@ -920,6 +918,11 @@
});
}
function handleFormTableChange(data: any[]) {
paramsData.value = [...data];
emitChange('handleFormTableChange');
}
defineExpose({
addTableLine,
});
@ -942,12 +945,6 @@
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;

View File

@ -25,7 +25,7 @@
</div>
<paramTable
v-else-if="innerParams.bodyType === RequestBodyFormat.FORM_DATA"
v-model:params="currentTableParams"
:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
:height-used="heightUsed"
@ -40,7 +40,7 @@
/>
<paramTable
v-else-if="innerParams.bodyType === RequestBodyFormat.WWW_FORM"
v-model:params="currentTableParams"
:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
:height-used="heightUsed"
@ -115,6 +115,7 @@
import { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { filterKeyValParams } from '../utils';
import { defaultBodyParamsItem } from '@/views/api-test/components/config';
const props = defineProps<{
@ -315,14 +316,15 @@
*/
function handleBatchParamApply(resultArr: any[]) {
const files = currentTableParams.value.filter((item) => item.paramType === RequestParamsType.FILE);
if (resultArr.length < currentTableParams.value.length) {
currentTableParams.value.splice(0, currentTableParams.value.length - 1, ...files, ...resultArr);
} else {
const filterResult = filterKeyValParams(currentTableParams.value, defaultBodyParamsItem);
if (filterResult.lastDataIsDefault) {
currentTableParams.value = [
...files,
...resultArr,
currentTableParams.value[currentTableParams.value.length - 1],
].filter(Boolean);
} else {
currentTableParams.value = [...files, ...resultArr].filter(Boolean);
}
emit('change');
}

View File

@ -24,10 +24,9 @@
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { useI18n } from '@/hooks/useI18n';
import { EnableKeyValueParam } from '@/models/apiTest/common';
import { filterKeyValParams } from '../utils';
import { defaultHeaderParamsItem } from '@/views/api-test/components/config';
const props = defineProps<{
@ -41,8 +40,6 @@
(e: 'change'): void; //
}>();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const columns: ParamTableColumn[] = [
@ -80,10 +77,11 @@
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
const filterResult = filterKeyValParams(innerParams.value, defaultHeaderParamsItem);
if (filterResult.lastDataIsDefault) {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]].filter(Boolean);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
innerParams.value = resultArr.filter(Boolean);
}
emit('change');
}

View File

@ -114,7 +114,7 @@
</template>
</a-dropdown>
<!-- 接口定义-定义模式直接保存接口定义 -->
<a-button v-else type="primary" @click="() => handleSelect('save')">
<a-button v-else type="primary" :loading="saveLoading" @click="() => handleSelect('save')">
{{ t('common.save') }}
</a-button>
</template>
@ -138,32 +138,44 @@
</div>
</div>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-40px)]">
<div class="px-[16px]">
<MsTab
v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList"
:get-text-func="getTabBadge"
class="no-content relative mt-[8px]"
/>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-97px)]">
<MsSplitBox
ref="splitBoxRef"
ref="horizontalSplitBoxRef"
:size="props.isDefinition ? 0.7 : 1"
:max="props.isDefinition ? 0.9 : 1"
:min="props.isDefinition ? 0.7 : 1"
:disabled="!props.isDefinition"
:class="!props.isDefinition ? 'hidden-second' : ''"
:first-container-class="!props.isDefinition ? 'border-r-0' : ''"
direction="horizontal"
expand-direction="right"
>
<template #first>
<MsSplitBox
ref="verticalSplitBoxRef"
v-model:size="splitBoxSize"
:max="!showResponse ? 1 : 0.98"
min="10px"
:direction="activeLayout"
second-container-class="!overflow-y-hidden"
:class="!showResponse ? 'hidden-second' : ''"
@expand-change="handleExpandChange"
@expand-change="handleVerticalExpandChange"
>
<template #first>
<a-spin class="block h-full w-full" :loading="requestVModel.executeLoading || loading">
<div
:class="`flex h-full min-w-[800px] flex-col px-[18px] pb-[16px] ${
:class="`flex h-full min-w-[800px] flex-col p-[16px] ${
activeLayout === 'horizontal' ? ' pr-[16px]' : ''
}`"
>
<div>
<MsTab
v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList"
:get-text-func="getTabBadge"
class="no-content relative mb-[8px] border-b border-[var(--color-text-n8)]"
/>
</div>
<div class="tab-pane-container">
<a-spin
v-if="requestVModel.activeTab === RequestComposition.PLUGIN"
@ -253,19 +265,126 @@
:is-http-protocol="isHttpProtocol"
:is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url"
:is-expanded="isExpanded"
:is-expanded="isVerticalExpanded"
:hide-layout-switch="props.hideResponseLayoutSwitch"
:request-task-result="requestVModel.response"
:is-edit="props.isDefinition && isHttpProtocol"
:upload-temp-file-api="props.uploadTempFileApi"
:loading="requestVModel.executeLoading || loading"
@change-expand="changeExpand"
@change-expand="changeVerticalExpand"
@change-layout="handleActiveLayoutChange"
@change="handleActiveDebugChange"
@execute="execute"
/>
</template>
</MsSplitBox>
</template>
<template v-if="props.isDefinition" #second>
<div class="p-[16px]">
<!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<a-form ref="activeApiTabFormRef" :model="requestVModel" layout="vertical">
<a-form-item
field="name"
:label="t('apiTestManagement.apiName')"
class="mb-[16px]"
:rules="[{ required: true, message: t('apiTestManagement.apiNameRequired') }]"
>
<a-input
v-model:model-value="requestVModel.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
allow-clear
@change="handleActiveApiChange"
/>
</a-form-item>
<a-form-item :label="t('apiTestManagement.belongModule')" class="mb-[16px]">
<a-tree-select
v-model:modelValue="requestVModel.moduleId"
:data="selectTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:tree-props="{
virtualListProps: {
height: 200,
threshold: 200,
},
}"
allow-search
@change="handleActiveApiChange"
/>
</a-form-item>
<a-form-item :label="t('apiTestManagement.apiStatus')" class="mb-[16px]">
<a-select
v-model:model-value="requestVModel.status"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full"
@change="handleActiveApiChange"
>
<template #label>
<apiStatus :status="requestVModel.status" />
</template>
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
<apiStatus :status="item" />
</a-option>
</a-select>
</a-form-item>
<a-form-item :label="t('common.tag')" class="mb-[16px]">
<MsTagsInput v-model:model-value="requestVModel.tags" @change="handleActiveApiChange" />
</a-form-item>
<a-form-item :label="t('common.desc')" class="mb-[16px]">
<a-textarea
v-model:model-value="requestVModel.description"
:max-length="1000"
@change="handleActiveApiChange"
/>
</a-form-item>
</a-form>
<!-- TODO:第一版先不做依赖 -->
<!-- <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="requestVModel.preDependency.length === 0 && requestVModel.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))]">
{{ requestVModel.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))]">
{{ requestVModel.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>
</div>
</div>
<a-modal
@ -285,7 +404,11 @@
:rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" />
<a-input
v-model:model-value="saveModalForm.name"
:max-length="255"
:placeholder="t('apiTestDebug.requestNamePlaceholder')"
/>
</a-form-item>
<a-form-item
v-if="isHttpProtocol"
@ -294,7 +417,11 @@
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.path" :placeholder="t('apiTestDebug.commonPlaceholder')" />
<a-input
v-model:model-value="saveModalForm.path"
:max-length="255"
:placeholder="t('apiTestDebug.commonPlaceholder')"
/>
</a-form-item>
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select
@ -312,6 +439,43 @@
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:visible="saveCaseModalVisible"
:title="t('common.save')"
:ok-loading="saveCaseLoading"
class="ms-modal-form"
title-align="start"
body-class="!p-0"
@before-ok="saveAsCase"
@cancel="handleSaveCaseCancel"
>
<a-form ref="saveCaseModalFormRef" :model="saveCaseModalForm" layout="vertical">
<a-form-item
field="name"
:label="t('case.caseName')"
:rules="[{ required: true, message: t('case.caseNameRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveCaseModalForm.name" :placeholder="t('case.caseNamePlaceholder')" />
</a-form-item>
<a-form-item field="priority" :label="t('case.caseLevel')">
<a-select v-model:model-value="saveCaseModalForm.priority" :options="casePriorityOptions"></a-select>
</a-form-item>
<a-form-item field="status" :label="t('common.status')">
<a-select v-model:model-value="saveCaseModalForm.status" :options="caseStatusOptions"></a-select>
</a-form-item>
<a-form-item field="tags" :label="t('common.tag')">
<MsTagsInput
v-model:model-value="saveCaseModalForm.tags"
placeholder="common.tagsInputPlaceholder"
allow-clear
unique-value
retain-input-value
/>
</a-form-item>
</a-form>
</a-modal>
<addDependencyDrawer v-if="props.isDefinition" v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" />
</template>
<script setup lang="ts">
@ -323,6 +487,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import auth from './auth.vue';
import postcondition from './postcondition.vue';
import precondition from './precondition.vue';
@ -330,8 +495,10 @@
import setting from './setting.vue';
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';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
import { addCase } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
@ -348,18 +515,24 @@
PluginConfig,
RequestTaskResult,
} from '@/models/apiTest/common';
import { AddApiCaseParams } from '@/models/apiTest/management';
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
RequestAuthType,
RequestBodyFormat,
RequestCaseStatus,
RequestComposition,
RequestConditionProcessor,
RequestDefinitionStatus,
RequestMethods,
RequestParamsType,
} from '@/enums/apiEnum';
import type { ResponseItem } from './response/edit.vue';
import {
casePriorityOptions,
caseStatusOptions,
defaultBodyParamsItem,
defaultHeaderParamsItem,
defaultKeyValueParamItem,
@ -373,6 +546,9 @@
const httpBody = defineAsyncComponent(() => import('./body.vue'));
const httpQuery = defineAsyncComponent(() => import('./query.vue'));
const httpRest = defineAsyncComponent(() => import('./rest.vue'));
const addDependencyDrawer = defineAsyncComponent(
() => import('@/views/api-test/management/components/addDependencyDrawer.vue')
);
export interface RequestCustomAttr {
isNew: boolean;
@ -395,6 +571,7 @@
isDefinition?: boolean; //
hideResponseLayoutSwitch?: boolean; //
otherParams?: Record<string, any>; //
currentEnvConfig?: EnvConfig;
executeApi: (params: ExecuteRequestParams) => Promise<any>; //
localExecuteApi: (url: string, params: ExecuteRequestParams) => Promise<any>; //
createApi: (...args) => Promise<any>; //
@ -409,7 +586,7 @@
update: string;
};
}>();
const emit = defineEmits(['addDone', 'save', 'saveAsCase']);
const emit = defineEmits(['addDone']);
const appStore = useAppStore();
const { t } = useI18n();
@ -768,18 +945,18 @@
}
);
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isExpanded = ref(true);
function handleExpandChange(val: boolean) {
isExpanded.value = val;
const horizontalSplitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const verticalSplitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isVerticalExpanded = ref(true);
function handleVerticalExpandChange(val: boolean) {
isVerticalExpanded.value = val;
}
function changeExpand(val: boolean) {
isExpanded.value = val;
function changeVerticalExpand(val: boolean) {
isVerticalExpanded.value = val;
if (val) {
splitBoxRef.value?.expand(0.6);
verticalSplitBoxRef.value?.expand(0.6);
} else {
splitBoxRef.value?.collapse(
verticalSplitBoxRef.value?.collapse(
splitContainerRef.value
? `${splitContainerRef.value.clientHeight - (props.hideResponseLayoutSwitch ? 37 : 42)}px`
: 0
@ -791,17 +968,23 @@
() => showResponse.value,
(val) => {
if (val) {
changeExpand(true);
changeVerticalExpand(true);
} else {
changeExpand(false);
changeVerticalExpand(false);
}
}
);
function handleActiveLayoutChange() {
isExpanded.value = true;
isVerticalExpanded.value = true;
splitBoxSize.value = 0.6;
splitBoxRef.value?.expand(0.6);
verticalSplitBoxRef.value?.expand(0.6);
}
function handleActiveApiChange() {
if (requestVModel.value) {
requestVModel.value.unSaved = true;
}
}
const reportId = ref('');
@ -876,6 +1059,7 @@
* @param executeType 执行类型执行时传入
*/
function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
const isExecute = executeType === 'localExec' || executeType === 'serverExec';
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol
@ -883,8 +1067,16 @@
let parseRequestBodyResult;
let requestParams;
if (isHttpProtocol.value) {
const realFormDataBodyValues = filterKeyValParams(formDataBody.formValues, defaultBodyParamsItem).validParams;
const realWwwFormBodyValues = filterKeyValParams(wwwFormBody.formValues, defaultBodyParamsItem).validParams;
const realFormDataBodyValues = filterKeyValParams(
formDataBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
const realWwwFormBodyValues = filterKeyValParams(
wwwFormBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.body,
requestVModel.value.uploadFileIds, //
@ -901,12 +1093,12 @@
formValues: realWwwFormBodyValues,
},
},
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem).validParams,
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
method: requestVModel.value.method,
otherConfig: requestVModel.value.otherConfig,
path: requestVModel.value.url || requestVModel.value.path,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem).validParams,
rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem).validParams,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem, isExecute).validParams,
url: requestVModel.value.url,
polymorphicName,
};
@ -932,7 +1124,7 @@
status: requestVModel.value.status,
response: requestVModel.value.responseDefinition?.map((e) => ({
...e,
headers: filterKeyValParams(e.headers, defaultKeyValueParamItem).validParams,
headers: filterKeyValParams(e.headers, defaultKeyValueParamItem, isExecute).validParams,
})),
};
} else {
@ -942,7 +1134,7 @@
return {
id: requestVModel.value.id.toString(),
reportId: reportId.value,
environmentId: '',
environmentId: props.currentEnvConfig?.id || '',
name: requestName,
moduleId: requestModuleId,
...apiDefinitionParams,
@ -1018,7 +1210,7 @@
requestVModel.value.executeLoading = false;
}
async function updateDebug() {
async function updateRequest() {
try {
saveLoading.value = true;
await props.updateApi({
@ -1039,38 +1231,58 @@
/**
* 保存请求
*/
async function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
async function realSave(fullParams?: Record<string, any>, silence?: boolean) {
try {
if (!silence) {
saveLoading.value = true;
if (requestVModel.value.isNew) {
//
const res = await props.createApi({
...makeRequestParams(),
}
let params;
if (props.isDefinition) {
params = {
...(fullParams || makeRequestParams()),
...props.otherParams,
};
} else {
params = {
...(fullParams || makeRequestParams()),
...saveModalForm.value,
path: isHttpProtocol.value ? saveModalForm.value.path : undefined,
...props.otherParams,
});
};
}
const res = await props.createApi(params);
if (!silence) {
Message.success(t('common.saveSuccess'));
}
requestVModel.value.id = res.id;
requestVModel.value.num = res.num;
requestVModel.value.isNew = false;
Message.success(t('common.saveSuccess'));
requestVModel.value.unSaved = false;
requestVModel.value.name = saveModalForm.value.name;
requestVModel.value.label = saveModalForm.value.name;
requestVModel.value.url = saveModalForm.value.path;
saveLoading.value = false;
requestVModel.value.name = res.name;
requestVModel.value.label = res.name;
requestVModel.value.url = res.path;
requestVModel.value.path = res.path;
console.log('requestVModel.value', requestVModel.value);
if (!props.isDefinition) {
saveModalVisible.value = false;
done(true);
}
if (!silence) {
saveLoading.value = false;
emit('addDone');
} else {
updateDebug();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
saveLoading.value = false;
}
}
function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
await realSave();
done(true);
}
});
done(false);
}
@ -1079,22 +1291,27 @@
* 保存快捷键处理
*/
async function handleSaveShortcut() {
if (!requestVModel.value.isNew) {
//
updateDebug();
return;
}
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
if (!requestVModel.value.isNew) {
//
updateRequest();
return;
}
if (!props.isDefinition) {
//
saveModalForm.value = {
name: requestVModel.value.name || '',
path: requestVModel.value.url || '',
moduleId: 'root',
};
saveModalVisible.value = true;
} else {
realSave();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -1106,6 +1323,78 @@
}
}
const saveCaseModalVisible = ref(false);
const saveCaseLoading = ref(false);
const saveCaseModalForm = ref({
name: '',
priority: 'P0',
status: RequestCaseStatus.PROCESSING,
tags: [],
});
const saveCaseModalFormRef = ref<FormInstance>();
function handleSaveCaseCancel() {
saveCaseModalForm.value = {
name: '',
priority: 'P0',
status: RequestCaseStatus.PROCESSING,
tags: [],
};
saveCaseModalVisible.value = false;
}
//
function saveAsCase() {
saveCaseModalFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
saveCaseLoading.value = true;
const definitionParams = makeRequestParams();
if (requestVModel.value.isNew) {
//
await realSave(definitionParams, true);
}
const params: AddApiCaseParams = {
...definitionParams,
...saveCaseModalForm.value,
projectId: appStore.currentProjectId,
environmentId: props.currentEnvConfig?.id || '',
apiDefinitionId: requestVModel.value.id,
};
await addCase(params);
emit('addDone');
Message.success(t('common.saveSuccess'));
saveCaseModalVisible.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
saveCaseLoading.value = false;
}
}
});
}
//
async function saveNewDefinition() {
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
saveCaseModalVisible.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' });
});
}
}
const activeApiTabFormRef = ref<FormInstance>();
const isUrlError = ref(false);
function handleSelect(value: string | number | Record<string, any> | undefined) {
if (requestVModel.value.url === '' && requestVModel.value.protocol === 'HTTP') {
@ -1113,18 +1402,62 @@
return;
}
isUrlError.value = false;
activeApiTabFormRef.value?.validate(async (errors) => {
if (errors) {
horizontalSplitBoxRef.value?.expand();
} else {
switch (value) {
case 'save':
emit('save', makeRequestParams());
handleSaveShortcut();
break;
case 'saveAsCase':
emit('saveAsCase', makeRequestParams());
saveNewDefinition();
break;
default:
break;
}
}
});
}
// const fApi = ref();
// const options = {
// form: {
// layout: 'vertical',
// labelPosition: 'right',
// size: 'small',
// labelWidth: '00px',
// hideRequiredAsterisk: false,
// showMessage: true,
// inlineMessage: false,
// scrollToFirstError: true,
// },
// submitBtn: false,
// resetBtn: false,
// };
// const currentApiTemplateRules = [];
const showAddDependencyDrawer = ref(false);
const addDependencyMode = ref<'pre' | 'post'>('pre');
// function handleDddDependency(value: string | number | Record<string, any> | undefined) {
// switch (value) {
// case 'pre':
// addDependencyMode.value = 'pre';
// showAddDependencyDrawer.value = true;
// break;
// case 'post':
// addDependencyMode.value = 'post';
// showAddDependencyDrawer.value = true;
// break;
// default:
// break;
// }
// }
// function clearAllDependency() {
// activeApiTab.value.preDependency = [];
// activeApiTab.value.postDependency = [];
// }
function handleCancel() {
saveModalFormRef.value?.resetFields();

View File

@ -16,7 +16,7 @@
/>
</div>
<paramTable
v-model:params="innerParams"
:params="innerParams"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
@ -36,6 +36,7 @@
import { ExecuteRequestCommonParam } from '@/models/apiTest/common';
import { RequestParamsType } from '@/enums/apiEnum';
import { filterKeyValParams } from '../utils';
import { defaultRequestParamsItem } from '@/views/api-test/components/config';
const props = defineProps<{
@ -130,10 +131,11 @@
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
const filterResult = filterKeyValParams(innerParams.value, defaultRequestParamsItem);
if (filterResult.lastDataIsDefault) {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]].filter(Boolean);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
innerParams.value = resultArr.filter(Boolean);
}
emit('change');
}

View File

@ -146,7 +146,7 @@
</template>
<paramTable
v-else-if="activeResponse.responseActiveTab === ResponseComposition.HEADER"
v-model:params="activeResponse.headers"
:params="activeResponse.headers"
:columns="columns"
:default-param-item="defaultKeyValueParamItem"
:selectable="false"

View File

@ -153,7 +153,7 @@
activeTab: ResponseComposition;
isExpanded: boolean;
isPriorityLocalExec: boolean;
requestUrl: string;
requestUrl?: string;
isHttpProtocol: boolean;
activeLayout?: Direction;
responseDefinition?: ResponseItem[];

View File

@ -102,7 +102,7 @@
requestResult?: RequestResult;
console?: string;
isPriorityLocalExec: boolean;
requestUrl: string;
requestUrl?: string;
isHttpProtocol: boolean;
}>();
const emit = defineEmits(['execute']);

View File

@ -16,7 +16,7 @@
/>
</div>
<paramTable
v-model:params="innerParams"
:params="innerParams"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
@ -36,6 +36,7 @@
import { ExecuteRequestCommonParam } from '@/models/apiTest/common';
import { RequestParamsType } from '@/enums/apiEnum';
import { filterKeyValParams } from '../utils';
import { defaultRequestParamsItem } from '@/views/api-test/components/config';
const props = defineProps<{
@ -131,10 +132,11 @@
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
const filterResult = filterKeyValParams(innerParams.value, defaultRequestParamsItem);
if (filterResult.lastDataIsDefault) {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]].filter(Boolean);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
innerParams.value = resultArr.filter(Boolean);
}
emit('change');
}

View File

@ -115,8 +115,13 @@ export function parseRequestBodyFiles(
*
* @param params
* @param defaultParamItem
* @param filterEnable enable false
*/
export function filterKeyValParams<T>(params: (T & Record<string, any>)[], defaultParamItem: Record<string, any>) {
export function filterKeyValParams<T>(
params: (T & Record<string, any>)[],
defaultParamItem: Record<string, any>,
filterEnable = false
) {
const lastData = cloneDeep(params[params.length - 1]);
const defaultParam = cloneDeep(defaultParamItem);
if (!lastData || !defaultParam) {
@ -138,6 +143,9 @@ export function filterKeyValParams<T>(params: (T & Record<string, any>)[], defau
} else {
validParams = params;
}
if (filterEnable) {
validParams = validParams.filter((e) => e.enable === true);
}
return {
lastDataIsDefault,
validParams,

View File

@ -36,7 +36,7 @@
:add-module-api="addDebugModule"
@add-finish="initModules"
>
<MsButton v-permission="['PROJECT_API_DEBUG:READ+ADD']" type="icon" class="!mr-0 p-[2px]">
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"

View File

@ -72,6 +72,8 @@ export default {
'apiTestDebug.preconditionScriptName': 'Pre-script name',
'apiTestDebug.preconditionAssociatedSceneResult': 'Associated scene result',
'apiTestDebug.preconditionScriptNamePlaceholder': 'Please enter the pre-script name',
'apiTestDebug.postConditionScriptName': 'Postscript name',
'apiTestDebug.postConditionScriptNamePlaceholder': 'Please enter the postscript name',
'apiTestDebug.preconditionAssociateResultDesc':
'Counted in the scene execution result as a custom script step. Execution error will affect the final scene execution result',
'apiTestDebug.manual': 'Manual entry',

View File

@ -68,6 +68,8 @@ export default {
'apiTestDebug.preconditionScriptName': '前置脚本名称',
'apiTestDebug.preconditionAssociatedSceneResult': '关联场景结果',
'apiTestDebug.preconditionScriptNamePlaceholder': '请输入前置脚本名称',
'apiTestDebug.postConditionScriptName': '后置脚本名称',
'apiTestDebug.postConditionScriptNamePlaceholder': '请输入后置脚本名称',
'apiTestDebug.preconditionAssociateResultDesc':
'当作自定义脚本步骤统计到场景执行结果中,执行报错时会影响场景的最终执行结果',
'apiTestDebug.manual': '手动录入',

View File

@ -27,7 +27,7 @@
<script setup lang="ts">
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import apiTable from './apiTable.vue';
import apiTable from './management/api/apiTable.vue';
import moduleTree from '@/views/api-test/management/components/moduleTree.vue';
import { useI18n } from '@/hooks/useI18n';

View File

@ -1,5 +1,5 @@
<template>
<div :class="['p-[16px_22px]', props.class]">
<div :class="['p-[0_16px_16px_16px]', props.class]">
<div class="mb-[16px] flex items-center justify-between">
<div class="flex items-center gap-[8px]">
<a-input-search
@ -26,6 +26,7 @@
v-on="propsEvent"
@selected-change="handleTableSelect"
@batch-action="handleTableBatch"
@drag-change="handleTableDragSort"
>
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
<a-trigger
@ -81,28 +82,30 @@
v-if="props.protocol === 'HTTP'"
v-model:model-value="record.method"
class="param-input w-full"
size="mini"
@change="() => handleMethodChange(record)"
>
<template #label>
<apiMethodName :method="record.method" is-tag />
<apiMethodName :method="record.method" tag-size="small" is-tag />
</template>
<a-option v-for="item of Object.values(RequestMethods)" :key="item" :value="item">
<apiMethodName :method="item" is-tag />
<apiMethodName :method="item" tag-size="small" is-tag />
</a-option>
</a-select>
<apiMethodName v-else :method="record.method" is-tag />
<apiMethodName v-else :method="record.method" tag-size="small" is-tag />
</template>
<template #status="{ record }">
<a-select
v-model:model-value="record.status"
class="param-input w-full"
size="mini"
@change="() => handleStatusChange(record)"
>
<template #label>
<apiStatus :status="record.status" />
<apiStatus :status="record.status" size="small" />
</template>
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
<apiStatus :status="item" />
<apiStatus :status="item" size="small" />
</a-option>
</a-select>
</template>
@ -253,6 +256,7 @@
batchUpdateDefinition,
deleteDefinition,
getDefinitionPage,
sortDefinition,
updateDefinition,
} from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
@ -261,6 +265,7 @@
import useAppStore from '@/store/modules/app';
import { ApiDefinitionDetail } from '@/models/apiTest/management';
import { DragSortParams } from '@/models/common';
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
@ -375,7 +380,7 @@
selectable: true,
showSelectAll: !props.readOnly,
draggable: props.readOnly ? undefined : { type: 'handle', width: 32 },
heightUsed: 374,
heightUsed: 308,
},
(item) => ({
...item,
@ -754,6 +759,20 @@
emit('openCopyApiTab', record);
}
//
async function handleTableDragSort(params: DragSortParams) {
try {
await sortDefinition({
...params,
moduleId: moduleIds.value[0] || '',
});
Message.success(t('caseManagement.featureCase.sortSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
defineExpose({
loadApiList,
});

View File

@ -1,27 +1,6 @@
<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"
@change="handleActiveTabChange"
>
<template #label="{ tab }">
<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">
<div class="flex flex-1 flex-col overflow-hidden">
<div v-show="activeApiTab.id === 'all'" class="flex-1 pt-[16px]">
<apiTable
ref="apiTableRef"
:active-module="props.activeModule"
@ -48,15 +27,6 @@
/>
</a-tab-pane>
<a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-tab-pane">
<MsSplitBox
ref="splitBoxRef"
:size="0.7"
:max="0.9"
:min="0.7"
direction="horizontal"
expand-direction="right"
>
<template #first>
<requestComposition
v-model:detail-loading="loading"
v-model:request="activeApiTab"
@ -75,118 +45,10 @@
:file-save-as-source-id="activeApiTab.id"
:file-module-options-api="getTransferOptions"
:file-save-as-api="transferFile"
:current-env-config="currentEnvConfig"
is-definition
@add-done="emit('addDone')"
@save="handleSave"
@save-as-case="handleSaveAsCase"
@add-done="handleAddDone"
/>
</template>
<template #second>
<div class="p-[18px]">
<!-- TODO:第一版没有模板 -->
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
<a-form ref="activeApiTabFormRef" :model="activeApiTab" layout="vertical">
<a-form-item
field="name"
:label="t('apiTestManagement.apiName')"
class="mb-[16px]"
:rules="[{ required: true, message: t('apiTestManagement.apiNameRequired') }]"
>
<a-input
v-model:model-value="activeApiTab.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
allow-clear
@change="handleActiveApiChange"
/>
</a-form-item>
<a-form-item :label="t('apiTestManagement.belongModule')" class="mb-[16px]">
<a-tree-select
v-model:modelValue="activeApiTab.moduleId"
:data="selectTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:tree-props="{
virtualListProps: {
height: 200,
threshold: 200,
},
}"
allow-search
@change="handleActiveApiChange"
/>
</a-form-item>
<a-form-item :label="t('apiTestManagement.apiStatus')" class="mb-[16px]">
<a-select
v-model:model-value="activeApiTab.status"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full"
@change="handleActiveApiChange"
>
<template #label>
<apiStatus :status="activeApiTab.status" />
</template>
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
<apiStatus :status="item" />
</a-option>
</a-select>
</a-form-item>
<a-form-item :label="t('common.tag')" class="mb-[16px]">
<MsTagsInput v-model:model-value="activeApiTab.tags" @change="handleActiveApiChange" />
</a-form-item>
<a-form-item :label="t('common.desc')" class="mb-[16px]">
<a-textarea
v-model:model-value="activeApiTab.description"
:max-length="1000"
@change="handleActiveApiChange"
/>
</a-form-item>
</a-form>
<!-- TODO:第一版先不做依赖 -->
<!-- <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-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>
</a-tab-pane>
<a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane">
</a-tab-pane>
@ -194,23 +56,15 @@
</a-tabs>
</div>
</div>
<addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" />
</template>
<script setup lang="ts">
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';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import addDependencyDrawer from './addDependencyDrawer.vue';
import apiTable from './apiTable.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { getProtocolList, localExecuteApiDebug } from '@/api/modules/api-test/common';
import {
@ -224,15 +78,11 @@
} from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { filterTree } from '@/utils';
import { ExecuteBody, ProtocolItem, RequestTaskResult } from '@/models/apiTest/common';
import {
ApiDefinitionCreateParams,
ApiDefinitionDetail,
ApiDefinitionUpdateParams,
} from '@/models/apiTest/management';
import { ApiDefinitionDetail } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
RequestAuthType,
RequestBodyFormat,
@ -258,13 +108,22 @@
protocol: string;
}>();
const emit = defineEmits(['addDone']);
const definitionActiveKey = ref('definition');
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const definitionActiveKey = ref('definition');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const appStore = useAppStore();
const { t } = useI18n();
const apiTabs = defineModel<RequestParam[]>('apiTabs', {
required: true,
});
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
required: true,
});
const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() {
try {
@ -279,37 +138,6 @@
initProtocolList();
});
const apiTabs = ref<RequestParam[]>([
{
id: 'all',
label: t('apiTestManagement.allApi'),
closable: false,
} as RequestParam,
]);
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
function handleActiveApiChange() {
if (activeApiTab.value) {
activeApiTab.value.unSaved = true;
}
}
watch(
() => activeApiTab.value.id,
() => {
if (typeof setActiveApi === 'function') {
setActiveApi(activeApiTab.value);
}
}
);
const selectTree = computed(() =>
filterTree(cloneDeep(props.moduleTree), (e) => {
e.draggable = false;
return e.type === 'MODULE';
})
);
const initDefaultId = `definition-${Date.now()}`;
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
@ -438,11 +266,14 @@
const apiTableRef = ref<InstanceType<typeof apiTable>>();
function handleActiveTabChange(item: TabItem) {
if (item.id === 'all') {
watch(
() => activeApiTab.value.id,
(id) => {
if (id === 'all') {
apiTableRef.value?.loadApiList();
}
}
);
const loading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false) {
@ -487,84 +318,11 @@
}
}
// const fApi = ref();
// const options = {
// form: {
// layout: 'vertical',
// labelPosition: 'right',
// size: 'small',
// labelWidth: '00px',
// hideRequiredAsterisk: false,
// showMessage: true,
// inlineMessage: false,
// scrollToFirstError: true,
// },
// submitBtn: false,
// resetBtn: false,
// };
// const currentApiTemplateRules = [];
const showAddDependencyDrawer = ref(false);
const addDependencyMode = ref<'pre' | 'post'>('pre');
// function handleDddDependency(value: string | number | Record<string, any> | undefined) {
// switch (value) {
// case 'pre':
// addDependencyMode.value = 'pre';
// showAddDependencyDrawer.value = true;
// break;
// case 'post':
// addDependencyMode.value = 'post';
// showAddDependencyDrawer.value = true;
// break;
// default:
// break;
// }
// }
// function clearAllDependency() {
// activeApiTab.value.preDependency = [];
// activeApiTab.value.postDependency = [];
// }
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const activeApiTabFormRef = ref<FormInstance>();
function handleSave(params: ApiDefinitionCreateParams) {
activeApiTabFormRef.value?.validate(async (errors) => {
if (errors) {
splitBoxRef.value?.expand();
} else {
try {
appStore.showLoading();
let res;
params.versionId = 'v1.0';
if (params.isNew) {
res = await addDefinition(params);
} else {
res = await updateDefinition(params as ApiDefinitionUpdateParams);
}
activeApiTab.value.id = res.id;
activeApiTab.value.isNew = false;
Message.success(t('common.saveSuccess'));
activeApiTab.value.unSaved = false;
activeApiTab.value.name = res.name;
activeApiTab.value.label = res.name;
activeApiTab.value.url = res.path;
function handleAddDone() {
definitionActiveKey.value = 'preview'; //
if (typeof refreshModuleTree === 'function') {
refreshModuleTree();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
}
});
}
async function handleSaveAsCase(params: ApiDefinitionCreateParams) {
console.log(params);
}
function refreshTable() {
@ -579,12 +337,15 @@
</script>
<style lang="less" scoped>
.ms-api-tab-nav {
:deep(.ms-api-tab-nav) {
@apply h-full;
:deep(.arco-tabs-content) {
.arco-tabs-nav-tab {
border-bottom: 1px solid var(--color-text-n8);
}
.arco-tabs-content {
@apply pt-0;
height: calc(100% - 51px);
height: calc(100% - 48px);
.arco-tabs-content-list {
@apply h-full;
.arco-tabs-pane {

View File

@ -14,7 +14,7 @@
</template>
<div class="detail-collapse-item">
<template v-if="props.detail.protocol === 'HTTP'">
<div v-if="preivewDetail.headers.length > 0" class="detail-item">
<div v-if="previewDetail.headers.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">{{ t('apiTestManagement.requestHeader') }}</div>
<a-radio-group v-model:model-value="headerShowType" type="button" size="mini">
@ -25,7 +25,7 @@
<MsFormTable
v-show="headerShowType === 'table'"
:columns="headerColumns"
:data="preivewDetail.headers || []"
:data="previewDetail.headers || []"
:selectable="false"
/>
<MsCodeEditor
@ -53,7 +53,7 @@
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
<div v-if="preivewDetail.query.length > 0" class="detail-item">
<div v-if="previewDetail.query.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">Query</div>
<a-radio-group v-model:model-value="queryShowType" type="button" size="mini">
@ -64,7 +64,7 @@
<MsFormTable
v-show="queryShowType === 'table'"
:columns="queryRestColumns"
:data="preivewDetail.query || []"
:data="previewDetail.query || []"
:selectable="false"
/>
<MsCodeEditor
@ -92,7 +92,7 @@
</MsCodeEditor>
<a-divider type="dashed" :margin="0" class="!mt-[16px] border-[var(--color-text-n8)]" />
</div>
<div v-if="preivewDetail.rest.length > 0" class="detail-item">
<div v-if="previewDetail.rest.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">Rest</div>
<a-radio-group v-model:model-value="restShowType" type="button" size="mini">
@ -103,7 +103,7 @@
<MsFormTable
v-show="restShowType === 'table'"
:columns="queryRestColumns"
:data="preivewDetail.rest || []"
:data="previewDetail.rest || []"
:selectable="false"
/>
<MsCodeEditor
@ -134,10 +134,10 @@
<div class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ `${t('apiTestManagement.requestBody')}-${preivewDetail.body.bodyType}` }}
{{ `${t('apiTestManagement.requestBody')}-${previewDetail.body.bodyType}` }}
</div>
<!-- <a-radio-group
v-if="preivewDetail.body.bodyType !== RequestBodyFormat.NONE"
v-if="previewDetail.body.bodyType !== RequestBodyFormat.NONE"
v-model:model-value="bodyShowType"
type="button"
size="mini"
@ -147,15 +147,16 @@
</a-radio-group> -->
</div>
<div
v-if="preivewDetail.body.bodyType === RequestBodyFormat.NONE"
v-if="previewDetail.body.bodyType === RequestBodyFormat.NONE"
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
>
{{ t('apiTestDebug.noneBody') }}
</div>
<MsFormTable
v-else-if="
preivewDetail.body.bodyType === RequestBodyFormat.FORM_DATA ||
preivewDetail.body.bodyType === RequestBodyFormat.WWW_FORM
previewDetail.body.bodyType === RequestBodyFormat.FORM_DATA ||
previewDetail.body.bodyType === RequestBodyFormat.WWW_FORM ||
previewDetail.body.bodyType === RequestBodyFormat.BINARY
"
:columns="bodyColumns"
:data="bodyTableData"
@ -164,7 +165,7 @@
<MsCodeEditor
v-else-if="
[RequestBodyFormat.JSON, RequestBodyFormat.RAW, RequestBodyFormat.XML].includes(
preivewDetail.body.bodyType
previewDetail.body.bodyType
)
"
:model-value="bodyCode"
@ -235,8 +236,8 @@
</a-collapse-item>
<a-collapse-item
v-if="
preivewDetail.responseDefinition &&
preivewDetail.responseDefinition.length > 0 &&
previewDetail.responseDefinition &&
previewDetail.responseDefinition.length > 0 &&
props.detail.protocol === 'HTTP'
"
key="response"
@ -254,7 +255,7 @@
</template>
<MsEditableTab
v-model:active-tab="activeResponse"
:tabs="preivewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
:tabs="previewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
hide-more-action
readonly
class="my-[8px]"
@ -272,8 +273,14 @@
{{ `${t('apiTestDebug.responseBody')}-${activeResponse?.body.bodyType}` }}
</div>
</div>
<MsFormTable
v-if="activeResponse?.body.bodyType === ResponseBodyFormat.BINARY"
:columns="responseBodyColumns"
:data="responseBodyTableData"
:selectable="false"
/>
<MsCodeEditor
v-if="activeResponse?.body.bodyType !== ResponseBodyFormat.BINARY"
v-else
:model-value="responseCode"
class="flex-1"
theme="vs"
@ -329,7 +336,6 @@
import { RequestBodyFormat, RequestParamsType, ResponseBodyFormat } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { getValidRequestTableParams } from '@/views/api-test/components/utils';
const props = defineProps<{
detail: RequestParam;
@ -339,7 +345,7 @@
const { t } = useI18n();
const { copy, isSupported } = useClipboard();
const preivewDetail = ref<RequestParam>(cloneDeep(props.detail));
const previewDetail = ref<RequestParam>(props.detail);
const activeResponse = ref<TabItem & ResponseItem>();
const pluginLoading = ref(false);
@ -358,21 +364,21 @@
},
];
const pluginTableData = computed(() => {
if (pluginScriptMap.value[preivewDetail.value.protocol]) {
if (pluginScriptMap.value[previewDetail.value.protocol]) {
return (
pluginScriptMap.value[preivewDetail.value.protocol].apiDefinitionFields?.map((e) => ({
pluginScriptMap.value[previewDetail.value.protocol].apiDefinitionFields?.map((e) => ({
key: e,
value: preivewDetail.value[e],
value: previewDetail.value[e],
})) || []
);
}
return [];
});
const pluginRawCode = computed(() => {
if (pluginScriptMap.value[preivewDetail.value.protocol]) {
if (pluginScriptMap.value[previewDetail.value.protocol]) {
return (
pluginScriptMap.value[preivewDetail.value.protocol].apiDefinitionFields
?.map((e) => `${e}:${preivewDetail.value[e]}`)
pluginScriptMap.value[previewDetail.value.protocol].apiDefinitionFields
?.map((e) => `${e}:${previewDetail.value[e]}`)
.join('\n') || ''
);
}
@ -404,28 +410,11 @@
}
watchEffect(() => {
preivewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
const tableParam = getValidRequestTableParams(preivewDetail.value); // props.detail
preivewDetail.value = {
...preivewDetail.value,
body: {
...preivewDetail.value.body,
formDataBody: {
formValues: tableParam.formDataBodyTableParams,
},
wwwFormBody: {
formValues: tableParam.wwwFormBodyTableParams,
},
},
headers: tableParam.headers,
rest: tableParam.rest,
query: tableParam.query,
responseDefinition: tableParam.response,
};
[activeResponse.value] = tableParam.response;
if (preivewDetail.value.protocol !== 'HTTP') {
previewDetail.value = cloneDeep(props.detail); // props.detailprops.detail
[activeResponse.value] = previewDetail.value.responseDefinition || [];
if (previewDetail.value.protocol !== 'HTTP') {
//
initPluginScript(preivewDetail.value.protocol);
initPluginScript(previewDetail.value.protocol);
}
});
@ -463,7 +452,7 @@
];
const headerShowType = ref('table');
const headerRawCode = computed(() => {
return preivewDetail.value.headers?.map((item) => `${item.key}:${item.value}`).join('\n');
return previewDetail.value.headers?.map((item) => `${item.key}:${item.value}`).join('\n');
});
/**
@ -523,17 +512,19 @@
];
const queryShowType = ref('table');
const queryRawCode = computed(() => {
return preivewDetail.value.query?.map((item) => `${item.key}:${item.value}`).join('\n');
return previewDetail.value.query?.map((item) => `${item.key}:${item.value}`).join('\n');
});
const restShowType = ref('table');
const restRawCode = computed(() => {
return preivewDetail.value.rest?.map((item) => `${item.key}:${item.value}`).join('\n');
return previewDetail.value.rest?.map((item) => `${item.key}:${item.value}`).join('\n');
});
/**
* 请求体
*/
const bodyColumns: FormTableColumn[] = [
const bodyColumns = computed<FormTableColumn[]>(() => {
if ([RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(previewDetail.value.body.bodyType)) {
return [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
@ -587,41 +578,64 @@
width: 100,
},
];
}
return [
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
showTooltip: true,
},
];
});
// const bodyShowType = ref('table');
const bodyTableData = computed(() => {
switch (preivewDetail.value.body.bodyType) {
switch (previewDetail.value.body.bodyType) {
case RequestBodyFormat.FORM_DATA:
return (preivewDetail.value.body.formDataBody?.formValues || []).map((e) => ({
return (previewDetail.value.body.formDataBody?.formValues || []).map((e) => ({
...e,
value: e.paramType === RequestParamsType.FILE ? e.files?.map((file) => file.fileName).join('\n') : e.value,
}));
case RequestBodyFormat.WWW_FORM:
return preivewDetail.value.body.wwwFormBody?.formValues || [];
return previewDetail.value.body.wwwFormBody?.formValues || [];
case RequestBodyFormat.BINARY:
return [
{
description: previewDetail.value.body.binaryBody.description,
value: previewDetail.value.body.binaryBody.file?.fileName,
},
];
default:
return [];
}
});
const bodyCode = computed(() => {
switch (preivewDetail.value.body.bodyType) {
switch (previewDetail.value.body.bodyType) {
case RequestBodyFormat.FORM_DATA:
return preivewDetail.value.body.formDataBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
return previewDetail.value.body.formDataBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.WWW_FORM:
return preivewDetail.value.body.wwwFormBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
return previewDetail.value.body.wwwFormBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.RAW:
return preivewDetail.value.body.rawBody?.value;
return previewDetail.value.body.rawBody?.value;
case RequestBodyFormat.JSON:
return preivewDetail.value.body.jsonBody?.jsonValue;
return previewDetail.value.body.jsonBody?.jsonValue;
case RequestBodyFormat.XML:
return preivewDetail.value.body.xmlBody?.value;
return previewDetail.value.body.xmlBody?.value;
default:
return '';
}
});
const bodyCodeLanguage = computed(() => {
if (preivewDetail.value.body.bodyType === RequestBodyFormat.JSON) {
if (previewDetail.value.body.bodyType === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (preivewDetail.value.body.bodyType === RequestBodyFormat.XML) {
if (previewDetail.value.body.bodyType === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
@ -663,6 +677,30 @@
inputType: 'text',
},
];
const responseBodyColumns: FormTableColumn[] = [
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
showTooltip: true,
},
];
const responseBodyTableData = computed(() => {
return activeResponse.value?.body.bodyType === ResponseBodyFormat.BINARY
? [
{
description: activeResponse.value?.body.binaryBody.description,
value: activeResponse.value?.body.binaryBody.file?.fileName,
},
]
: [];
});
</script>
<style lang="less" scoped>

View File

@ -123,7 +123,7 @@
{
key: 'path',
locale: 'apiTestManagement.path',
value: previewDetail.value.path,
value: previewDetail.value.url || previewDetail.value.path,
},
{
key: 'tags',

View File

@ -1,61 +1,69 @@
<template>
<a-tabs v-model:active-key="activeTab" animation lazy-load class="ms-api-tab-nav">
<a-tab-pane key="api" title="API" class="ms-api-tab-pane">
<api
ref="apiRef"
:module-tree="props.moduleTree"
:active-module="props.activeModule"
:offspring-ids="props.offspringIds"
:protocol="protocol"
<div class="flex gap-[8px] px-[16px] pt-[16px]">
<a-select v-model:model-value="currentTab" class="w-[80px]" :options="tabOptions" />
<MsEditableTab
v-model:active-tab="activeApiTab"
v-model:tabs="apiTabs"
class="flex-1 overflow-hidden"
@add="newTab"
>
<template #label="{ tab }">
<apiMethodName
v-if="tab.id !== 'all'"
:method="tab.protocol === 'HTTP' ? tab.method : tab.protocol"
class="mr-[4px]"
/>
</a-tab-pane>
<a-tab-pane key="case" title="CASE" class="ms-api-tab-pane">
<apiCase :active-module="props.activeModule" :offspring-ids="props.offspringIds" :protocol="protocol" />
</a-tab-pane>
<!-- <a-tab-pane key="mock" title="MOCK" class="ms-api-tab-pane">
<mock-table
ref="mockRef"
:module-tree="props.moduleTree"
:active-module="props.activeModule"
:offspring-ids="props.offspringIds"
:protocol="protocol"
/>
</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]">
<template #icon>
<icon-location class="text-[var(--color-text-4)]" />
<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>
</a-button>
<MsSelect
</MsEditableTab>
<a-select
v-model:model-value="currentEnv"
mode="static"
:options="envOptions"
class="!w-[150px]"
:search-keys="['label']"
class="!w-[200px] pl-0 pr-[8px]"
:loading="envLoading"
allow-search
@change="initEnvironment"
/>
>
<template #prefix>
<div class="flex cursor-pointer p-[8px]" @click.stop="goEnv">
<icon-location class="text-[var(--color-text-4)]" />
</div>
</template>
</a-tabs>
</a-select>
</div>
<api
v-if="currentTab === 'api'"
ref="apiRef"
v-model:active-api-tab="activeApiTab"
v-model:api-tabs="apiTabs"
:active-module="props.activeModule"
:offspring-ids="props.offspringIds"
:protocol="props.protocol"
:module-tree="props.moduleTree"
/>
</template>
<script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue';
import MsSelect from '@/components/business/ms-select';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import api from './api/index.vue';
import apiCase from './case/index.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
// import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { getEnvironment, getEnvList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n';
import router from '@/router';
import useAppStore from '@/store/modules/app';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{
activeModule: string;
@ -65,8 +73,16 @@
}>();
const appStore = useAppStore();
const { t } = useI18n();
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
const currentTab = ref('api');
const tabOptions = [
{ label: 'API', value: 'api' },
{ label: 'CASE', value: 'case' },
];
const activeTab = ref('api');
const apiRef = ref<InstanceType<typeof api>>();
function newTab(apiInfo?: ModuleTreeNode | string) {
@ -77,14 +93,33 @@
}
}
const apiTabs = ref<RequestParam[]>([
{
id: 'all',
label: t('apiTestManagement.allApi'),
closable: false,
} as RequestParam,
]);
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
watch(
() => activeApiTab.value.id,
() => {
if (typeof setActiveApi === 'function') {
setActiveApi(activeApiTab.value);
}
}
);
const currentEnv = ref('');
const currentEnvConfig = ref({});
const currentEnvConfig = ref<EnvConfig>();
const envLoading = ref(false);
const envOptions = ref<SelectOptionData[]>([]);
async function initEnvironment() {
try {
currentEnvConfig.value = await getEnvironment(currentEnv.value);
currentEnvConfig.value.id = currentEnv.value;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -113,6 +148,12 @@
apiRef.value?.refreshTable();
}
function goEnv() {
router.push({
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT,
});
}
onBeforeMount(() => {
initEnvList();
});
@ -127,19 +168,10 @@
</script>
<style lang="less" scoped>
.ms-api-tab-nav {
@apply h-full;
:deep(.arco-tabs-content) {
height: calc(100% - 51px);
.arco-tabs-content-list {
@apply h-full;
.arco-tabs-pane {
@apply h-full;
}
}
}
:deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-text-n8);
}
.ms-input-group--prepend();
:deep(.arco-select-view-prefix) {
margin-right: 8px;
padding-right: 0;
border-right: 1px solid var(--color-text-input-border);
}
</style>

View File

@ -32,7 +32,8 @@
:content="isExpandApi ? t('apiTestManagement.collapseApi') : t('apiTestManagement.expandApi')"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeApiExpand">
<MsIcon :type="isExpandApi ? 'icon-icon_collapse_interface' : 'icon-icon_expand_interface'" />
<icon-eye-invisible v-if="isExpandApi" />
<icon-eye v-else />
</MsButton>
</a-tooltip>
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">

View File

@ -1,8 +1,8 @@
<template>
<MsCard :min-width="1180" simple no-content-padding>
<MsCard simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5">
<template #first>
<div class="p-[9px]">
<div class="p-[16px]">
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeApi?.id"

View File

@ -168,4 +168,6 @@ export default {
'case.batchDeleteCaseTip': 'Are you sure you want to delete {count} selected cases?',
'case.deleteCaseTip':
'Deleting an case will result in the execution failure of the test task that references the use case. Please be cautious!',
'apiTestManagement.click': 'Click',
'apiTestManagement.getResponse': 'Get response content',
};

View File

@ -149,6 +149,8 @@ export default {
'apiTestManagement.getResponse': '获取响应内容',
'case.allCase': '全部CASE',
'case.caseName': '用例名称',
'case.caseNameRequired': '用例名称不能为空',
'case.caseNamePlaceholder': '请输入用例名称',
'case.caseLevel': '用例等级',
'case.caseEnvironment': '用例环境',
'case.tableColumnCreateUser': '创建人',