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 MSR from '@/api/http/index';
import { import {
AddCaseUrl,
AddDefinitionScheduleUrl, AddDefinitionScheduleUrl,
AddDefinitionUrl, AddDefinitionUrl,
AddModuleUrl, AddModuleUrl,
@ -53,6 +54,7 @@ import {
import { ExecuteRequestParams } from '@/models/apiTest/common'; import { ExecuteRequestParams } from '@/models/apiTest/common';
import { import {
AddApiCaseParams,
ApiCaseBatchEditParams, ApiCaseBatchEditParams,
ApiCaseBatchParams, ApiCaseBatchParams,
ApiCaseDetail, ApiCaseDetail,
@ -342,3 +344,8 @@ export function batchEditCase(data: ApiCaseBatchEditParams) {
export function dragSort(data: DragSortParams) { export function dragSort(data: DragSortParams) {
return MSR.post({ url: SortCaseUrl, data }); 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 OperationHistoryUrl = '/api/definition/operation-history'; // 接口定义-变更历史
export const SaveOperationHistoryUrl = '/api/definition/operation-history/save'; // 接口定义-另存变更历史为指定版本 export const SaveOperationHistoryUrl = '/api/definition/operation-history/save'; // 接口定义-另存变更历史为指定版本
export const RecoverOperationHistoryUrl = '/api/definition/operation-history/recover'; // 接口定义-变更历史恢复 export const RecoverOperationHistoryUrl = '/api/definition/operation-history/recover'; // 接口定义-变更历史恢复
export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取接口引用关系
/** /**
* Mock * Mock
@ -42,11 +43,6 @@ export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表
export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态 export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态
export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock
/**
*
*/
export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取接口引用关系
/** /**
* api回收站 * api回收站
*/ */
@ -65,3 +61,4 @@ export const DeleteCaseUrl = '/api/case/delete'; // 删除接口用例
export const BatchDeleteCaseUrl = '/api/case/batch/delete'; // 批量删除接口用例 export const BatchDeleteCaseUrl = '/api/case/batch/delete'; // 批量删除接口用例
export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例 export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例
export const SortCaseUrl = '/api/case/edit/pos'; // 接口用例拖拽 export const SortCaseUrl = '/api/case/edit/pos'; // 接口用例拖拽
export const AddCaseUrl = '/api/case/add'; // 添加用例

View File

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

View File

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

View File

@ -21,7 +21,10 @@
:style="{ width: item.width }" :style="{ width: item.width }"
> >
<div class="text-[var(--color-text-4)]">{{ t(item.locale) }}</div> <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"> <slot v-else :name="item.key" :value="item.value">
<a-tooltip :content="item.value" :disabled="isEmpty(item.value)"> <a-tooltip :content="item.value" :disabled="isEmpty(item.value)">
<div class="text-[var(--color-text-1)]">{{ item.value || '-' }}</div> <div class="text-[var(--color-text-1)]">{{ item.value || '-' }}</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -331,3 +331,11 @@ export interface ApiCaseBatchEditParams extends ApiCaseBatchParams {
environmentId?: string; environmentId?: string;
type: 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; [key: string]: any;
} }
export interface EnvConfig { export interface EnvConfig {
id?: string;
commonParams?: CommonParams; commonParams?: CommonParams;
commonVariables: EnvConfigItem[]; commonVariables: EnvConfigItem[];
httpConfig: EnvConfigItem[]; httpConfig: EnvConfigItem[];

View File

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

View File

@ -5,7 +5,13 @@ import {
KeyValueParam, KeyValueParam,
ResponseDefinition, ResponseDefinition,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { RequestContentTypeEnum, RequestParamsType, ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum'; import {
RequestCaseStatus,
RequestContentTypeEnum,
RequestParamsType,
ResponseBodyFormat,
ResponseComposition,
} from '@/enums/apiEnum';
// 请求 body 参数表格默认行的值 // 请求 body 参数表格默认行的值
export const defaultBodyParamsItem: ExecuteRequestFormBodyFormValue = { export const defaultBodyParamsItem: ExecuteRequestFormBodyFormValue = {
@ -83,3 +89,18 @@ export const defaultKeyValueParamItem: KeyValueParam = {
// 请求的响应 response 的响应状态码集合 // 请求的响应 response 的响应状态码集合
export const statusCodes = [200, 201, 202, 203, 204, 205, 400, 401, 402, 403, 404, 405, 500, 501, 502, 503, 504, 505]; 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; line-height: 16px;
color: var(--color-text-1); 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 { .param-popover-value {
min-width: 100px; min-width: 100px;
max-width: 280px; max-width: 280px;

View File

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

View File

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

View File

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

View File

@ -114,7 +114,7 @@
</template> </template>
</a-dropdown> </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') }} {{ t('common.save') }}
</a-button> </a-button>
</template> </template>
@ -138,32 +138,44 @@
</div> </div>
</div> </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 <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" v-model:size="splitBoxSize"
:max="!showResponse ? 1 : 0.98" :max="!showResponse ? 1 : 0.98"
min="10px" min="10px"
:direction="activeLayout" :direction="activeLayout"
second-container-class="!overflow-y-hidden" second-container-class="!overflow-y-hidden"
:class="!showResponse ? 'hidden-second' : ''" :class="!showResponse ? 'hidden-second' : ''"
@expand-change="handleExpandChange" @expand-change="handleVerticalExpandChange"
> >
<template #first> <template #first>
<a-spin class="block h-full w-full" :loading="requestVModel.executeLoading || loading"> <a-spin class="block h-full w-full" :loading="requestVModel.executeLoading || loading">
<div <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]' : '' 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"> <div class="tab-pane-container">
<a-spin <a-spin
v-if="requestVModel.activeTab === RequestComposition.PLUGIN" v-if="requestVModel.activeTab === RequestComposition.PLUGIN"
@ -253,19 +265,126 @@
:is-http-protocol="isHttpProtocol" :is-http-protocol="isHttpProtocol"
:is-priority-local-exec="isPriorityLocalExec" :is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url" :request-url="requestVModel.url"
:is-expanded="isExpanded" :is-expanded="isVerticalExpanded"
:hide-layout-switch="props.hideResponseLayoutSwitch" :hide-layout-switch="props.hideResponseLayoutSwitch"
:request-task-result="requestVModel.response" :request-task-result="requestVModel.response"
:is-edit="props.isDefinition && isHttpProtocol" :is-edit="props.isDefinition && isHttpProtocol"
:upload-temp-file-api="props.uploadTempFileApi" :upload-temp-file-api="props.uploadTempFileApi"
:loading="requestVModel.executeLoading || loading" :loading="requestVModel.executeLoading || loading"
@change-expand="changeExpand" @change-expand="changeVerticalExpand"
@change-layout="handleActiveLayoutChange" @change-layout="handleActiveLayoutChange"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
@execute="execute" @execute="execute"
/> />
</template> </template>
</MsSplitBox> </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>
</div> </div>
<a-modal <a-modal
@ -285,7 +404,11 @@
:rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]" :rules="[{ required: true, message: t('apiTestDebug.requestNameRequired') }]"
asterisk-position="end" 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>
<a-form-item <a-form-item
v-if="isHttpProtocol" v-if="isHttpProtocol"
@ -294,7 +417,11 @@
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]" :rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
asterisk-position="end" 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>
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0"> <a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select <a-tree-select
@ -312,6 +439,43 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -323,6 +487,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTab from '@/components/pure/ms-tab/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 auth from './auth.vue';
import postcondition from './postcondition.vue'; import postcondition from './postcondition.vue';
import precondition from './precondition.vue'; import precondition from './precondition.vue';
@ -330,8 +495,10 @@
import setting from './setting.vue'; import setting from './setting.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common'; 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 { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index'; import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -348,18 +515,24 @@
PluginConfig, PluginConfig,
RequestTaskResult, RequestTaskResult,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { AddApiCaseParams } from '@/models/apiTest/management';
import { ModuleTreeNode, TransferFileParams } from '@/models/common'; import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { import {
RequestAuthType, RequestAuthType,
RequestBodyFormat, RequestBodyFormat,
RequestCaseStatus,
RequestComposition, RequestComposition,
RequestConditionProcessor, RequestConditionProcessor,
RequestDefinitionStatus,
RequestMethods, RequestMethods,
RequestParamsType, RequestParamsType,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
import type { ResponseItem } from './response/edit.vue'; import type { ResponseItem } from './response/edit.vue';
import { import {
casePriorityOptions,
caseStatusOptions,
defaultBodyParamsItem, defaultBodyParamsItem,
defaultHeaderParamsItem, defaultHeaderParamsItem,
defaultKeyValueParamItem, defaultKeyValueParamItem,
@ -373,6 +546,9 @@
const httpBody = defineAsyncComponent(() => import('./body.vue')); const httpBody = defineAsyncComponent(() => import('./body.vue'));
const httpQuery = defineAsyncComponent(() => import('./query.vue')); const httpQuery = defineAsyncComponent(() => import('./query.vue'));
const httpRest = defineAsyncComponent(() => import('./rest.vue')); const httpRest = defineAsyncComponent(() => import('./rest.vue'));
const addDependencyDrawer = defineAsyncComponent(
() => import('@/views/api-test/management/components/addDependencyDrawer.vue')
);
export interface RequestCustomAttr { export interface RequestCustomAttr {
isNew: boolean; isNew: boolean;
@ -395,6 +571,7 @@
isDefinition?: boolean; // isDefinition?: boolean; //
hideResponseLayoutSwitch?: boolean; // hideResponseLayoutSwitch?: boolean; //
otherParams?: Record<string, any>; // otherParams?: Record<string, any>; //
currentEnvConfig?: EnvConfig;
executeApi: (params: ExecuteRequestParams) => Promise<any>; // executeApi: (params: ExecuteRequestParams) => Promise<any>; //
localExecuteApi: (url: string, params: ExecuteRequestParams) => Promise<any>; // localExecuteApi: (url: string, params: ExecuteRequestParams) => Promise<any>; //
createApi: (...args) => Promise<any>; // createApi: (...args) => Promise<any>; //
@ -409,7 +586,7 @@
update: string; update: string;
}; };
}>(); }>();
const emit = defineEmits(['addDone', 'save', 'saveAsCase']); const emit = defineEmits(['addDone']);
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
@ -768,18 +945,18 @@
} }
); );
const splitBoxRef = ref<InstanceType<typeof MsSplitBox>>(); const horizontalSplitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isExpanded = ref(true); const verticalSplitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isVerticalExpanded = ref(true);
function handleExpandChange(val: boolean) { function handleVerticalExpandChange(val: boolean) {
isExpanded.value = val; isVerticalExpanded.value = val;
} }
function changeExpand(val: boolean) { function changeVerticalExpand(val: boolean) {
isExpanded.value = val; isVerticalExpanded.value = val;
if (val) { if (val) {
splitBoxRef.value?.expand(0.6); verticalSplitBoxRef.value?.expand(0.6);
} else { } else {
splitBoxRef.value?.collapse( verticalSplitBoxRef.value?.collapse(
splitContainerRef.value splitContainerRef.value
? `${splitContainerRef.value.clientHeight - (props.hideResponseLayoutSwitch ? 37 : 42)}px` ? `${splitContainerRef.value.clientHeight - (props.hideResponseLayoutSwitch ? 37 : 42)}px`
: 0 : 0
@ -791,17 +968,23 @@
() => showResponse.value, () => showResponse.value,
(val) => { (val) => {
if (val) { if (val) {
changeExpand(true); changeVerticalExpand(true);
} else { } else {
changeExpand(false); changeVerticalExpand(false);
} }
} }
); );
function handleActiveLayoutChange() { function handleActiveLayoutChange() {
isExpanded.value = true; isVerticalExpanded.value = true;
splitBoxSize.value = 0.6; 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(''); const reportId = ref('');
@ -876,6 +1059,7 @@
* @param executeType 执行类型执行时传入 * @param executeType 执行类型执行时传入
*/ */
function makeRequestParams(executeType?: 'localExec' | 'serverExec') { function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
const isExecute = executeType === 'localExec' || executeType === 'serverExec';
const { formDataBody, wwwFormBody } = requestVModel.value.body; const { formDataBody, wwwFormBody } = requestVModel.value.body;
const polymorphicName = protocolOptions.value.find( const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol (e) => e.value === requestVModel.value.protocol
@ -883,8 +1067,16 @@
let parseRequestBodyResult; let parseRequestBodyResult;
let requestParams; let requestParams;
if (isHttpProtocol.value) { if (isHttpProtocol.value) {
const realFormDataBodyValues = filterKeyValParams(formDataBody.formValues, defaultBodyParamsItem).validParams; const realFormDataBodyValues = filterKeyValParams(
const realWwwFormBodyValues = filterKeyValParams(wwwFormBody.formValues, defaultBodyParamsItem).validParams; formDataBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
const realWwwFormBodyValues = filterKeyValParams(
wwwFormBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
parseRequestBodyResult = parseRequestBodyFiles( parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.body, requestVModel.value.body,
requestVModel.value.uploadFileIds, // requestVModel.value.uploadFileIds, //
@ -901,12 +1093,12 @@
formValues: realWwwFormBodyValues, formValues: realWwwFormBodyValues,
}, },
}, },
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem).validParams, headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
method: requestVModel.value.method, method: requestVModel.value.method,
otherConfig: requestVModel.value.otherConfig, otherConfig: requestVModel.value.otherConfig,
path: requestVModel.value.url || requestVModel.value.path, path: requestVModel.value.url || requestVModel.value.path,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem).validParams, query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem).validParams, rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem, isExecute).validParams,
url: requestVModel.value.url, url: requestVModel.value.url,
polymorphicName, polymorphicName,
}; };
@ -932,7 +1124,7 @@
status: requestVModel.value.status, status: requestVModel.value.status,
response: requestVModel.value.responseDefinition?.map((e) => ({ response: requestVModel.value.responseDefinition?.map((e) => ({
...e, ...e,
headers: filterKeyValParams(e.headers, defaultKeyValueParamItem).validParams, headers: filterKeyValParams(e.headers, defaultKeyValueParamItem, isExecute).validParams,
})), })),
}; };
} else { } else {
@ -942,7 +1134,7 @@
return { return {
id: requestVModel.value.id.toString(), id: requestVModel.value.id.toString(),
reportId: reportId.value, reportId: reportId.value,
environmentId: '', environmentId: props.currentEnvConfig?.id || '',
name: requestName, name: requestName,
moduleId: requestModuleId, moduleId: requestModuleId,
...apiDefinitionParams, ...apiDefinitionParams,
@ -1018,7 +1210,7 @@
requestVModel.value.executeLoading = false; requestVModel.value.executeLoading = false;
} }
async function updateDebug() { async function updateRequest() {
try { try {
saveLoading.value = true; saveLoading.value = true;
await props.updateApi({ await props.updateApi({
@ -1039,38 +1231,58 @@
/** /**
* 保存请求 * 保存请求
*/ */
async function handleSave(done: (closed: boolean) => void) { async function realSave(fullParams?: Record<string, any>, silence?: boolean) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
try { try {
if (!silence) {
saveLoading.value = true; saveLoading.value = true;
if (requestVModel.value.isNew) { }
// let params;
const res = await props.createApi({ if (props.isDefinition) {
...makeRequestParams(), params = {
...(fullParams || makeRequestParams()),
...props.otherParams,
};
} else {
params = {
...(fullParams || makeRequestParams()),
...saveModalForm.value, ...saveModalForm.value,
path: isHttpProtocol.value ? saveModalForm.value.path : undefined, path: isHttpProtocol.value ? saveModalForm.value.path : undefined,
...props.otherParams, ...props.otherParams,
}); };
}
const res = await props.createApi(params);
if (!silence) {
Message.success(t('common.saveSuccess'));
}
requestVModel.value.id = res.id; requestVModel.value.id = res.id;
requestVModel.value.num = res.num; requestVModel.value.num = res.num;
requestVModel.value.isNew = false; requestVModel.value.isNew = false;
Message.success(t('common.saveSuccess'));
requestVModel.value.unSaved = false; requestVModel.value.unSaved = false;
requestVModel.value.name = saveModalForm.value.name; requestVModel.value.name = res.name;
requestVModel.value.label = saveModalForm.value.name; requestVModel.value.label = res.name;
requestVModel.value.url = saveModalForm.value.path; requestVModel.value.url = res.path;
saveLoading.value = false; requestVModel.value.path = res.path;
console.log('requestVModel.value', requestVModel.value);
if (!props.isDefinition) {
saveModalVisible.value = false; saveModalVisible.value = false;
done(true); }
if (!silence) {
saveLoading.value = false;
emit('addDone'); emit('addDone');
} else {
updateDebug();
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error);
saveLoading.value = false; saveLoading.value = false;
} }
} }
function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
await realSave();
done(true);
}
}); });
done(false); done(false);
} }
@ -1079,22 +1291,27 @@
* 保存快捷键处理 * 保存快捷键处理
*/ */
async function handleSaveShortcut() { async function handleSaveShortcut() {
if (!requestVModel.value.isNew) {
//
updateDebug();
return;
}
try { try {
if (!isHttpProtocol.value) { if (!isHttpProtocol.value) {
// //
await fApi.value?.validate(); await fApi.value?.validate();
} }
if (!requestVModel.value.isNew) {
//
updateRequest();
return;
}
if (!props.isDefinition) {
//
saveModalForm.value = { saveModalForm.value = {
name: requestVModel.value.name || '', name: requestVModel.value.name || '',
path: requestVModel.value.url || '', path: requestVModel.value.url || '',
moduleId: 'root', moduleId: 'root',
}; };
saveModalVisible.value = true; saveModalVisible.value = true;
} else {
realSave();
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); 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); const isUrlError = ref(false);
function handleSelect(value: string | number | Record<string, any> | undefined) { function handleSelect(value: string | number | Record<string, any> | undefined) {
if (requestVModel.value.url === '' && requestVModel.value.protocol === 'HTTP') { if (requestVModel.value.url === '' && requestVModel.value.protocol === 'HTTP') {
@ -1113,18 +1402,62 @@
return; return;
} }
isUrlError.value = false; isUrlError.value = false;
activeApiTabFormRef.value?.validate(async (errors) => {
if (errors) {
horizontalSplitBoxRef.value?.expand();
} else {
switch (value) { switch (value) {
case 'save': case 'save':
emit('save', makeRequestParams()); handleSaveShortcut();
break; break;
case 'saveAsCase': case 'saveAsCase':
emit('saveAsCase', makeRequestParams()); saveNewDefinition();
break; break;
default: default:
break; 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() { function handleCancel() {
saveModalFormRef.value?.resetFields(); saveModalFormRef.value?.resetFields();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@
:add-module-api="addDebugModule" :add-module-api="addDebugModule"
@add-finish="initModules" @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 <MsIcon
type="icon-icon_create_planarity" type="icon-icon_create_planarity"
size="18" size="18"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +1,6 @@
<template> <template>
<div class="flex h-full flex-col"> <div class="flex flex-1 flex-col overflow-hidden">
<div class="border-b border-[var(--color-text-n8)] px-[22px] pb-[16px]"> <div v-show="activeApiTab.id === 'all'" class="flex-1 pt-[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">
<apiTable <apiTable
ref="apiTableRef" ref="apiTableRef"
:active-module="props.activeModule" :active-module="props.activeModule"
@ -48,15 +27,6 @@
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="definition" :title="t('apiTestManagement.definition')" class="ms-api-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 <requestComposition
v-model:detail-loading="loading" v-model:detail-loading="loading"
v-model:request="activeApiTab" v-model:request="activeApiTab"
@ -75,118 +45,10 @@
:file-save-as-source-id="activeApiTab.id" :file-save-as-source-id="activeApiTab.id"
:file-module-options-api="getTransferOptions" :file-module-options-api="getTransferOptions"
:file-save-as-api="transferFile" :file-save-as-api="transferFile"
:current-env-config="currentEnvConfig"
is-definition is-definition
@add-done="emit('addDone')" @add-done="handleAddDone"
@save="handleSave"
@save-as-case="handleSaveAsCase"
/> />
</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>
<a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane"> <a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane">
</a-tab-pane> </a-tab-pane>
@ -194,23 +56,15 @@
</a-tabs> </a-tabs>
</div> </div>
</div> </div>
<addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
// import MsButton from '@/components/pure/ms-button/index.vue'; // import 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 { TabItem } from '@/components/pure/ms-editable-tab/types';
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue'; // import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
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 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 { getProtocolList, localExecuteApiDebug } from '@/api/modules/api-test/common';
import { import {
@ -224,15 +78,11 @@
} from '@/api/modules/api-test/management'; } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { filterTree } from '@/utils';
import { ExecuteBody, ProtocolItem, RequestTaskResult } from '@/models/apiTest/common'; import { ExecuteBody, ProtocolItem, RequestTaskResult } from '@/models/apiTest/common';
import { import { ApiDefinitionDetail } from '@/models/apiTest/management';
ApiDefinitionCreateParams,
ApiDefinitionDetail,
ApiDefinitionUpdateParams,
} from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { import {
RequestAuthType, RequestAuthType,
RequestBodyFormat, RequestBodyFormat,
@ -258,13 +108,22 @@
protocol: string; protocol: string;
}>(); }>();
const emit = defineEmits(['addDone']); const emit = defineEmits(['addDone']);
const definitionActiveKey = ref('definition');
const setActiveApi: ((params: RequestParam) => void) | undefined = inject('setActiveApi');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree'); const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const definitionActiveKey = ref('definition');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const apiTabs = defineModel<RequestParam[]>('apiTabs', {
required: true,
});
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
required: true,
});
const protocols = ref<ProtocolItem[]>([]); const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() { async function initProtocolList() {
try { try {
@ -279,37 +138,6 @@
initProtocolList(); 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 initDefaultId = `definition-${Date.now()}`;
const defaultBodyParams: ExecuteBody = { const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE, bodyType: RequestBodyFormat.NONE,
@ -438,11 +266,14 @@
const apiTableRef = ref<InstanceType<typeof apiTable>>(); const apiTableRef = ref<InstanceType<typeof apiTable>>();
function handleActiveTabChange(item: TabItem) { watch(
if (item.id === 'all') { () => activeApiTab.value.id,
(id) => {
if (id === 'all') {
apiTableRef.value?.loadApiList(); apiTableRef.value?.loadApiList();
} }
} }
);
const loading = ref(false); const loading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false) { async function openApiTab(apiInfo: ModuleTreeNode | ApiDefinitionDetail | string, isCopy = false) {
@ -487,84 +318,11 @@
} }
} }
// const fApi = ref(); function handleAddDone() {
// const options = { definitionActiveKey.value = 'preview'; //
// 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;
if (typeof refreshModuleTree === 'function') { if (typeof refreshModuleTree === 'function') {
refreshModuleTree(); 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() { function refreshTable() {
@ -579,12 +337,15 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.ms-api-tab-nav { :deep(.ms-api-tab-nav) {
@apply h-full; @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; @apply pt-0;
height: calc(100% - 51px); height: calc(100% - 48px);
.arco-tabs-content-list { .arco-tabs-content-list {
@apply h-full; @apply h-full;
.arco-tabs-pane { .arco-tabs-pane {

View File

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

View File

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

View File

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

View File

@ -32,7 +32,8 @@
:content="isExpandApi ? t('apiTestManagement.collapseApi') : t('apiTestManagement.expandApi')" :content="isExpandApi ? t('apiTestManagement.collapseApi') : t('apiTestManagement.expandApi')"
> >
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeApiExpand"> <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> </MsButton>
</a-tooltip> </a-tooltip>
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')"> <a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">

View File

@ -1,8 +1,8 @@
<template> <template>
<MsCard :min-width="1180" simple no-content-padding> <MsCard simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5"> <MsSplitBox :size="0.25" :max="0.5">
<template #first> <template #first>
<div class="p-[9px]"> <div class="p-[16px]">
<moduleTree <moduleTree
ref="moduleTreeRef" ref="moduleTreeRef"
:active-node-id="activeApi?.id" :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.batchDeleteCaseTip': 'Are you sure you want to delete {count} selected cases?',
'case.deleteCaseTip': 'case.deleteCaseTip':
'Deleting an case will result in the execution failure of the test task that references the use case. Please be cautious!', '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': '获取响应内容', 'apiTestManagement.getResponse': '获取响应内容',
'case.allCase': '全部CASE', 'case.allCase': '全部CASE',
'case.caseName': '用例名称', 'case.caseName': '用例名称',
'case.caseNameRequired': '用例名称不能为空',
'case.caseNamePlaceholder': '请输入用例名称',
'case.caseLevel': '用例等级', 'case.caseLevel': '用例等级',
'case.caseEnvironment': '用例环境', 'case.caseEnvironment': '用例环境',
'case.tableColumnCreateUser': '创建人', 'case.tableColumnCreateUser': '创建人',