feat(接口管理): 用例执行&用例详情删除&部分细节修改
This commit is contained in:
parent
7791b800a4
commit
efa5387cf1
|
@ -53,6 +53,7 @@ import {
|
|||
RecoverDefinitionUrl,
|
||||
RecoverOperationHistoryUrl,
|
||||
RecycleCasePageUrl,
|
||||
RunCaseUrl,
|
||||
SaveOperationHistoryUrl,
|
||||
SortCaseUrl,
|
||||
SortDefinitionUrl,
|
||||
|
@ -409,6 +410,11 @@ export function toggleFollowCase(id: string | number) {
|
|||
return MSR.get({ url: ToggleFollowCaseUrl, params: id });
|
||||
}
|
||||
|
||||
// 用例执行,传请求详情执行
|
||||
export function runCase(data: ExecuteRequestParams) {
|
||||
return MSR.post({ url: RunCaseUrl, data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口用例回收站
|
||||
*/
|
||||
|
|
|
@ -74,6 +74,7 @@ export const GetExecuteHistoryUrl = '/api/case/execute/page'; // 获取用的执
|
|||
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
|
||||
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
|
||||
export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注
|
||||
export const RunCaseUrl = '/api/case/run'; // 执行接口用例
|
||||
export const GetCaseReportByIdUrl = '/api/report/case/get/'; // 接口用例报告获取
|
||||
export const GetCaseReportDetailUrl = '/api/report/case/get/detail/'; // 接口用例报告获取
|
||||
|
||||
|
|
|
@ -301,6 +301,7 @@ export interface ApiCaseDetail extends ExecuteRequestParams {
|
|||
priority: string;
|
||||
num: number;
|
||||
status: string;
|
||||
protocol: string;
|
||||
lastReportStatus: string;
|
||||
lastReportId: string;
|
||||
projectId: string;
|
||||
|
|
|
@ -587,6 +587,7 @@
|
|||
request: RequestParam; // 请求参数集合
|
||||
moduleTree?: ModuleTreeNode[]; // 模块树
|
||||
isCase?: boolean; // 是否是用例引用的组件
|
||||
apiDetail?: RequestParam; // 用例引用的时候需要接口定义的数据
|
||||
detailLoading?: boolean; // 详情加载状态
|
||||
isDefinition?: boolean; // 是否是接口定义模式
|
||||
hideResponseLayoutSwitch?: boolean; // 是否隐藏响应体的布局切换
|
||||
|
@ -677,10 +678,32 @@
|
|||
label: t('apiTestDebug.setting'),
|
||||
},
|
||||
];
|
||||
const restNumApi = computed(
|
||||
() =>
|
||||
filterKeyValParams(props.apiDetail?.rest ?? props.apiDetail?.request.rest, defaultRequestParamsItem).validParams
|
||||
.length
|
||||
);
|
||||
const queryNumApi = computed(
|
||||
() =>
|
||||
filterKeyValParams(props.apiDetail?.query ?? props.apiDetail?.request.query, defaultRequestParamsItem).validParams
|
||||
.length
|
||||
);
|
||||
const bodyTabBadgeApi = computed(() =>
|
||||
props.apiDetail?.request.body?.bodyType !== RequestBodyFormat.NONE ? '1' : ''
|
||||
);
|
||||
// 根据协议类型获取请求内容tab
|
||||
const contentTabList = computed(() => {
|
||||
// HTTP 协议 tabs
|
||||
if (isHttpProtocol.value) {
|
||||
if (props.isCase) {
|
||||
// 定义没有参数BODY/QUERY/REST的,用例对应tab不显示
|
||||
return httpContentTabList.filter(
|
||||
(e) =>
|
||||
!(!restNumApi.value && e.value === RequestComposition.REST) &&
|
||||
!(!queryNumApi.value && e.value === RequestComposition.QUERY) &&
|
||||
!(!bodyTabBadgeApi.value?.length && e.value === RequestComposition.BODY)
|
||||
);
|
||||
}
|
||||
if (props.isDefinition) {
|
||||
// 接口定义,定义模式隐藏前后置、断言
|
||||
return requestVModel.value.mode === 'debug'
|
||||
|
@ -1214,6 +1237,14 @@
|
|||
} else if (protocolOptions.value.length === 0) {
|
||||
await initProtocolList();
|
||||
}
|
||||
if (
|
||||
props.isCase &&
|
||||
requestVModel.value.protocol === 'HTTP' &&
|
||||
(restNumApi.value || queryNumApi.value || bodyTabBadgeApi.value?.length)
|
||||
) {
|
||||
// 如果定义有参数BODY/QUERY/REST,用例默认tab是参数tab
|
||||
requestVModel.value.activeTab = contentTabList.value[1].value;
|
||||
}
|
||||
if (props.request.isExecute && !requestVModel.value.executeLoading) {
|
||||
// 如果是执行操作打开接口详情,且该接口不在执行状态中,则立即执行
|
||||
execute(isPriorityLocalExec.value ? 'localExec' : 'serverExec');
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
<caseTable
|
||||
:is-api="true"
|
||||
:active-module="props.activeModule"
|
||||
:protocol="props.protocol"
|
||||
:protocol="activeApiTab.protocol"
|
||||
:api-detail="activeApiTab"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
|
|
@ -234,85 +234,87 @@
|
|||
</div>
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
<a-collapse-item
|
||||
v-if="
|
||||
previewDetail.responseDefinition &&
|
||||
previewDetail.responseDefinition.length > 0 &&
|
||||
props.detail.protocol === 'HTTP'
|
||||
"
|
||||
key="response"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<div v-if="activeDetailKey.includes('response')" class="down-icon">
|
||||
<icon-down :size="10" class="block" />
|
||||
</div>
|
||||
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]">
|
||||
<icon-right :size="10" class="block" />
|
||||
</div>
|
||||
<div class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeResponse"
|
||||
:tabs="previewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
|
||||
hide-more-action
|
||||
readonly
|
||||
class="my-[8px]"
|
||||
<a-spin :loading="previewDetail.executeLoading" class="w-full">
|
||||
<a-collapse-item
|
||||
v-if="
|
||||
previewDetail.responseDefinition &&
|
||||
previewDetail.responseDefinition.length > 0 &&
|
||||
props.detail.protocol === 'HTTP'
|
||||
"
|
||||
key="response"
|
||||
>
|
||||
<template #label="{ tab }">
|
||||
<div class="response-tab">
|
||||
<div v-if="tab.defaultFlag" class="response-tab-default-icon"></div>
|
||||
{{ t(tab.label || tab.name) }}({{ tab.statusCode }})
|
||||
<template #header>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<div v-if="activeDetailKey.includes('response')" class="down-icon">
|
||||
<icon-down :size="10" class="block" />
|
||||
</div>
|
||||
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]">
|
||||
<icon-right :size="10" class="block" />
|
||||
</div>
|
||||
<div class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
<div class="detail-item !pt-0">
|
||||
<div class="detail-item-title">
|
||||
<div class="detail-item-title-text">
|
||||
{{ `${t('apiTestDebug.responseBody')}-${activeResponse?.body.bodyType}` }}
|
||||
</div>
|
||||
</div>
|
||||
<MsFormTable
|
||||
v-if="activeResponse?.body.bodyType === ResponseBodyFormat.BINARY"
|
||||
:columns="responseBodyColumns"
|
||||
:data="responseBodyTableData"
|
||||
:selectable="false"
|
||||
/>
|
||||
<MsCodeEditor
|
||||
v-else
|
||||
:model-value="responseCode"
|
||||
class="flex-1"
|
||||
theme="vs"
|
||||
height="200px"
|
||||
:language="responseCodeLanguage"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
read-only
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeResponse"
|
||||
:tabs="previewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
|
||||
hide-more-action
|
||||
readonly
|
||||
class="my-[8px]"
|
||||
>
|
||||
<template #rightTitle>
|
||||
<a-button
|
||||
type="outline"
|
||||
class="arco-btn-outline--secondary p-[0_8px]"
|
||||
size="mini"
|
||||
@click="copyScript(responseCode || '')"
|
||||
>
|
||||
<template #icon>
|
||||
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
|
||||
</template>
|
||||
</a-button>
|
||||
<template #label="{ tab }">
|
||||
<div class="response-tab">
|
||||
<div v-if="tab.defaultFlag" class="response-tab-default-icon"></div>
|
||||
{{ t(tab.label || tab.name) }}({{ tab.statusCode }})
|
||||
</div>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
<div v-if="activeResponse?.headers && activeResponse?.headers.length > 0" class="detail-item">
|
||||
<div class="detail-item-title">
|
||||
<div class="detail-item-title-text">
|
||||
{{ t('apiTestDebug.responseHeader') }}
|
||||
</MsEditableTab>
|
||||
<div class="detail-item !pt-0">
|
||||
<div class="detail-item-title">
|
||||
<div class="detail-item-title-text">
|
||||
{{ `${t('apiTestDebug.responseBody')}-${activeResponse?.body.bodyType}` }}
|
||||
</div>
|
||||
</div>
|
||||
<MsFormTable
|
||||
v-if="activeResponse?.body.bodyType === ResponseBodyFormat.BINARY"
|
||||
:columns="responseBodyColumns"
|
||||
:data="responseBodyTableData"
|
||||
:selectable="false"
|
||||
/>
|
||||
<MsCodeEditor
|
||||
v-else
|
||||
:model-value="responseCode"
|
||||
class="flex-1"
|
||||
theme="vs"
|
||||
height="200px"
|
||||
:language="responseCodeLanguage"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
read-only
|
||||
>
|
||||
<template #rightTitle>
|
||||
<a-button
|
||||
type="outline"
|
||||
class="arco-btn-outline--secondary p-[0_8px]"
|
||||
size="mini"
|
||||
@click="copyScript(responseCode || '')"
|
||||
>
|
||||
<template #icon>
|
||||
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
<MsFormTable :columns="responseHeaderColumns" :data="activeResponse?.headers || []" :selectable="false" />
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
<div v-if="activeResponse?.headers && activeResponse?.headers.length > 0" class="detail-item">
|
||||
<div class="detail-item-title">
|
||||
<div class="detail-item-title-text">
|
||||
{{ t('apiTestDebug.responseHeader') }}
|
||||
</div>
|
||||
</div>
|
||||
<MsFormTable :columns="responseHeaderColumns" :data="activeResponse?.headers || []" :selectable="false" />
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
</a-spin>
|
||||
</a-collapse>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
<div class="h-full w-full overflow-hidden">
|
||||
<a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load>
|
||||
<template #extra>
|
||||
<div v-show="!props.isDrawer" class="flex gap-[12px]">
|
||||
<a-button type="primary">
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</a-button>
|
||||
<div class="flex gap-[12px]">
|
||||
<environmentSelect v-if="props.isDrawer" ref="environmentSelectRef" />
|
||||
<execute v-model:detail="caseDetail" :environment-id="environmentId as string" />
|
||||
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
|
||||
<a-button>{{ t('common.operation') }}</a-button>
|
||||
<a-button v-if="!props.isDrawer">{{ t('common.operation') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption value="edit">
|
||||
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="edit">
|
||||
<MsIcon type="icon-icon_edit_outlined" />
|
||||
{{ t('common.edit') }}
|
||||
</a-doption>
|
||||
|
@ -17,15 +16,19 @@
|
|||
<MsIcon type="icon-icon_share1" />
|
||||
{{ t('common.share') }}
|
||||
</a-doption>
|
||||
<a-doption value="fork">
|
||||
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="fork">
|
||||
<MsIcon
|
||||
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`"
|
||||
/>
|
||||
{{ t('common.fork') }}
|
||||
</a-doption>
|
||||
<a-divider margin="4px" />
|
||||
<a-doption class="error-6 text-[rgb(var(--danger-6))]">
|
||||
<a-divider v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']" margin="4px" />
|
||||
<a-doption
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
|
||||
value="delete"
|
||||
class="error-6 text-[rgb(var(--danger-6))]"
|
||||
>
|
||||
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
|
||||
{{ t('common.delete') }}
|
||||
</a-doption>
|
||||
|
@ -35,11 +38,6 @@
|
|||
</template>
|
||||
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]">
|
||||
<MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]">
|
||||
<template #titleAppend>
|
||||
<a-button v-show="props.isDrawer" type="primary" size="mini">
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template #type="{ value }">
|
||||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||
</template>
|
||||
|
@ -47,13 +45,13 @@
|
|||
<caseLevel :case-level="value as CaseLevel" />
|
||||
</template>
|
||||
</MsDetailCard>
|
||||
<detailTab :detail="caseDetail" :protocols="protocols" is-case />
|
||||
<detailTab :detail="caseDetail" :protocols="protocols as ProtocolItem[]" is-case />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
|
||||
<tab-case-dependency :source-id="caseDetail.id" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="executeHistory" :title="t('apiTestManagement.executeHistory')" class="px-[18px] py-[16px]">
|
||||
<tab-case-execute-history :source-id="caseDetail.id" module-type="API_REPORT" :protocol="props.protocol" />
|
||||
<tab-case-execute-history :source-id="caseDetail.id" module-type="API_REPORT" :protocol="caseDetail.protocol" />
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
|
||||
</a-tab-pane> -->
|
||||
|
@ -62,7 +60,7 @@
|
|||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" :protocol="props.protocol" v-bind="$attrs" />
|
||||
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -74,33 +72,41 @@
|
|||
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||
import environmentSelect from '../../environmentSelect.vue';
|
||||
import detailTab from '../api/preview/detail.vue';
|
||||
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
||||
import execute from './execute.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import TabCaseChangeHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseChangeHistory.vue';
|
||||
import TabCaseDependency from '@/views/api-test/management/components/management/case/tabContent/tabCaseDependency.vue';
|
||||
import TabCaseExecuteHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseExecuteHistory.vue';
|
||||
|
||||
import { getProtocolList } from '@/api/modules/api-test/common';
|
||||
import { toggleFollowCase } from '@/api/modules/api-test/management';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { deleteCase, toggleFollowCase } from '@/api/modules/api-test/management';
|
||||
import useModal from '@/hooks/useModal';
|
||||
|
||||
import { ProtocolItem } from '@/models/apiTest/common';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import { RequestMethods } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
isDrawer?: boolean; // 抽屉
|
||||
detail: RequestParam;
|
||||
protocol: string;
|
||||
}>();
|
||||
const emit = defineEmits(['updateFollow']);
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateFollow'): void;
|
||||
(e: 'deleteCase', id: string): void;
|
||||
}>();
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const caseDetail = ref<RequestParam>(cloneDeep(props.detail)); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
watchEffect(() => {
|
||||
caseDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
});
|
||||
|
||||
const caseDetail = computed<RequestParam>(() => cloneDeep(props.detail)); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
const activeKey = ref('detail');
|
||||
|
||||
const description = computed(() => [
|
||||
|
@ -126,20 +132,6 @@
|
|||
},
|
||||
]);
|
||||
|
||||
const protocols = ref<ProtocolItem[]>([]);
|
||||
async function initProtocolList() {
|
||||
try {
|
||||
protocols.value = await getProtocolList(appStore.currentOrgId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initProtocolList();
|
||||
});
|
||||
|
||||
const followLoading = ref(false);
|
||||
async function follow() {
|
||||
try {
|
||||
|
@ -157,7 +149,7 @@
|
|||
|
||||
function share() {
|
||||
if (isSupported) {
|
||||
copy(`${window.location.href}&dId=${caseDetail.value.id}`);
|
||||
copy(`${window.location.href}&cId=${caseDetail.value.id}`);
|
||||
Message.success(t('apiTestManagement.shareUrlCopied'));
|
||||
} else {
|
||||
Message.error(t('common.copyNotSupport'));
|
||||
|
@ -169,6 +161,30 @@
|
|||
createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value, false);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('apiTestManagement.deleteApiTipTitle', { name: caseDetail.value.name }),
|
||||
content: t('case.deleteCaseTip'),
|
||||
okText: t('common.confirmDelete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
maskClosable: false,
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
await deleteCase(caseDetail.value.id as string);
|
||||
emit('deleteCase', caseDetail.value.id as string);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSelect(val: string | number | Record<string, any> | undefined) {
|
||||
switch (val) {
|
||||
case 'edit':
|
||||
|
@ -180,15 +196,28 @@
|
|||
case 'fork':
|
||||
follow();
|
||||
break;
|
||||
case 'delete':
|
||||
handleDelete();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const protocols = inject<Ref<ProtocolItem[]>>('protocols');
|
||||
|
||||
const environmentSelectRef = ref<InstanceType<typeof environmentSelect>>();
|
||||
const currentEnvConfigByDrawer = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
|
||||
const currentEnvConfigByInject = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
const environmentId = computed(() =>
|
||||
props.isDrawer ? currentEnvConfigByDrawer.value?.id : currentEnvConfigByInject?.value?.id
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
editCase,
|
||||
share,
|
||||
follow,
|
||||
handleDelete,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -196,6 +225,9 @@
|
|||
:deep(.arco-tabs-nav) {
|
||||
border-bottom: 1px solid var(--color-text-n8);
|
||||
}
|
||||
:deep(.arco-tabs-nav-extra) {
|
||||
line-height: 32px;
|
||||
}
|
||||
:deep(.arco-tabs-content) {
|
||||
@apply pt-0;
|
||||
.arco-tabs-content-item {
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
:footer="false"
|
||||
no-content-padding
|
||||
>
|
||||
<template #headerLeft>
|
||||
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
|
||||
</template>
|
||||
<template #tbutton>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<MsButton
|
||||
|
@ -39,7 +36,7 @@
|
|||
{{ t('common.fork') }}
|
||||
</MsButton>
|
||||
<MsButton type="icon" status="secondary">
|
||||
<a-dropdown position="br">
|
||||
<a-dropdown position="br" @select="handleSelect">
|
||||
<div>
|
||||
<icon-more class="mr-[8px]" />
|
||||
<span> {{ t('common.more') }}</span>
|
||||
|
@ -47,6 +44,7 @@
|
|||
<template #content>
|
||||
<a-doption
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
|
||||
value="delete"
|
||||
class="error-6 text-[rgb(var(--danger-6))]"
|
||||
>
|
||||
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
|
||||
|
@ -57,14 +55,7 @@
|
|||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<caseDetail
|
||||
ref="caseDerailRef"
|
||||
is-drawer
|
||||
:detail="props.detail"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="props.apiDetail"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<caseDetail ref="caseDerailRef" is-drawer :detail="props.detail" :api-detail="props.apiDetail" v-bind="$attrs" />
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
|
@ -72,7 +63,6 @@
|
|||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import environmentSelect from '../../environmentSelect.vue';
|
||||
import caseDetail from './caseDetail.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
@ -81,7 +71,6 @@
|
|||
|
||||
const props = defineProps<{
|
||||
detail: RequestParam;
|
||||
protocol: string;
|
||||
apiDetail: RequestParam;
|
||||
}>();
|
||||
|
||||
|
@ -91,6 +80,16 @@
|
|||
required: true,
|
||||
});
|
||||
const caseDerailRef = ref<InstanceType<typeof caseDetail>>();
|
||||
|
||||
function handleSelect(val: string | number | Record<string, any> | undefined) {
|
||||
switch (val) {
|
||||
case 'delete':
|
||||
caseDerailRef.value?.handleDelete();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
</template>
|
||||
<template #caseLevel="{ record }">
|
||||
<a-select
|
||||
v-if="hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ+UPDATE'])"
|
||||
v-model:model-value="record.priority"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
|
@ -56,6 +57,7 @@
|
|||
<caseLevel :case-level="item.text" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
<span v-else class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.priority" /></span>
|
||||
</template>
|
||||
<template #caseLevelFilter="{ columnConfig }">
|
||||
<a-trigger v-model:popup-visible="caseFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
|
||||
|
@ -78,6 +80,7 @@
|
|||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-select
|
||||
v-if="hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ+UPDATE'])"
|
||||
v-model:model-value="record.status"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
|
@ -91,6 +94,7 @@
|
|||
<apiStatus :status="item" size="small" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
<apiStatus v-else :status="record.status" size="small" />
|
||||
</template>
|
||||
<template #statusFilter="{ columnConfig }">
|
||||
<a-trigger
|
||||
|
@ -153,12 +157,26 @@
|
|||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<MsButton type="text" class="!mr-0" @click="onExecute(record.id)">
|
||||
<template #operation="{ record }">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
|
||||
type="text"
|
||||
class="!mr-0"
|
||||
@click="onExecute(record.id)"
|
||||
>
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
<MsButton type="text" class="!mr-0" @click="copyCase(record)">
|
||||
<a-divider
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
|
||||
direction="vertical"
|
||||
:margin="8"
|
||||
></a-divider>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
|
||||
type="text"
|
||||
class="!mr-0"
|
||||
@click="copyCase(record)"
|
||||
>
|
||||
{{ t('common.copy') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
|
@ -239,17 +257,16 @@
|
|||
</a-modal>
|
||||
<createAndEditCaseDrawer
|
||||
ref="createAndEditCaseDrawerRef"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="apiDetail"
|
||||
@load-case="loadCaseListAndResetSelector()"
|
||||
/>
|
||||
<caseDetailDrawer
|
||||
v-model:visible="caseDetailDrawerVisible"
|
||||
:detail="caseDetail as RequestParam"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="apiDetail as RequestParam"
|
||||
@update-follow="caseDetail.follow = !caseDetail.follow"
|
||||
@load-case="(id: string) => loadCase(id)"
|
||||
@delete-case="deleteCaseByDetail"
|
||||
/>
|
||||
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
|
||||
<template #title>
|
||||
|
@ -381,6 +398,7 @@
|
|||
import useModal from '@/hooks/useModal';
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
|
||||
import { ApiCaseDetail, Environment } from '@/models/apiTest/management';
|
||||
import { DragSortParams } from '@/models/common';
|
||||
|
@ -410,6 +428,13 @@
|
|||
const keyword = ref('');
|
||||
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||
|
||||
const hasOperationPermission = computed(() =>
|
||||
hasAnyPermission([
|
||||
'PROJECT_API_DEFINITION_CASE:READ+DELETE',
|
||||
'PROJECT_API_DEFINITION_CASE:READ+ADD',
|
||||
'PROJECT_API_DEFINITION_CASE:READ+EXECUTE',
|
||||
])
|
||||
);
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
|
@ -529,14 +554,13 @@
|
|||
width: 180,
|
||||
},
|
||||
{
|
||||
title: 'common.operation',
|
||||
slotName: 'action',
|
||||
title: hasOperationPermission.value ? 'common.operation' : '',
|
||||
slotName: 'operation',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
width: hasOperationPermission.value ? 150 : 50,
|
||||
},
|
||||
];
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_CASE, columns, 'drawer');
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
|
@ -666,6 +690,7 @@
|
|||
watch(
|
||||
() => props.protocol,
|
||||
() => {
|
||||
if (props.isApi) return;
|
||||
loadCaseListAndResetSelector();
|
||||
}
|
||||
);
|
||||
|
@ -980,16 +1005,15 @@
|
|||
async function getCaseDetailInfo(id: string) {
|
||||
try {
|
||||
const res = await getCaseDetail(id);
|
||||
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件;
|
||||
// if (res.protocol === 'HTTP') { // TODO: 后端没protocol字段,问一下
|
||||
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
// }
|
||||
let parseRequestBodyResult;
|
||||
if (res.protocol === 'HTTP') {
|
||||
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
}
|
||||
caseDetail.value = {
|
||||
...cloneDeep(defaultCaseParams as RequestParam),
|
||||
...({
|
||||
...res.request,
|
||||
...res,
|
||||
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: 后端没response字段,问一下
|
||||
url: res.path,
|
||||
...parseRequestBodyResult,
|
||||
} as Partial<TabItem>),
|
||||
|
@ -1004,11 +1028,22 @@
|
|||
caseDetailDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
function deleteCaseByDetail() {
|
||||
caseDetailDrawerVisible.value = false;
|
||||
loadCaseList();
|
||||
}
|
||||
|
||||
// 在api下的用例里打开用例详情抽屉,点击编辑,编辑后在此刷新数据
|
||||
async function loadCase(id: string) {
|
||||
function loadCase(id: string) {
|
||||
getCaseDetailInfo(id);
|
||||
loadCaseList();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
loadCaseList,
|
||||
});
|
||||
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_CASE, columns, 'drawer');
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
:title="isEdit ? t('case.updateCase') : t('case.createCase')"
|
||||
:width="894"
|
||||
no-content-padding
|
||||
unmount-on-close
|
||||
:ok-text="isEdit ? 'common.update' : 'common.create'"
|
||||
:ok-loading="drawerLoading"
|
||||
:save-continue-text="t('case.saveContinueText')"
|
||||
|
@ -12,9 +13,6 @@
|
|||
@continue="handleDrawerConfirm(true)"
|
||||
@cancel="handleSaveCaseCancel"
|
||||
>
|
||||
<template #headerLeft>
|
||||
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
|
||||
</template>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<div class="px-[16px] pt-[16px]">
|
||||
<MsDetailCard
|
||||
|
@ -36,9 +34,12 @@
|
|||
:max-length="255"
|
||||
show-word-limit
|
||||
/>
|
||||
<a-button type="primary">
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</a-button>
|
||||
<environmentSelect ref="environmentSelectRef" />
|
||||
<execute
|
||||
v-model:detail="detailForm"
|
||||
:environment-id="currentEnvConfig?.id as string"
|
||||
:request="requestCompositionRef?.makeRequestParams"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<div class="flex gap-[16px]">
|
||||
|
@ -74,6 +75,7 @@
|
|||
ref="requestCompositionRef"
|
||||
v-model:request="detailForm"
|
||||
:is-case="true"
|
||||
:api-detail="apiDetailInfo as RequestParam"
|
||||
hide-response-layout-switch
|
||||
:upload-temp-file-api="uploadTempFileCase"
|
||||
:file-save-as-source-id="detailForm.id"
|
||||
|
@ -97,6 +99,7 @@
|
|||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||
import environmentSelect from '../../environmentSelect.vue';
|
||||
import execute from './execute.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
import requestComposition, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
@ -161,21 +164,32 @@
|
|||
const formRef = ref<FormInstance>();
|
||||
const requestCompositionRef = ref<InstanceType<typeof requestComposition>>();
|
||||
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
|
||||
const defaultDetail: RequestParam = {
|
||||
apiDefinitionId: apiDefinitionId.value,
|
||||
...(defaultCaseParams as RequestParam),
|
||||
};
|
||||
const detailForm = ref(cloneDeep(defaultDetail));
|
||||
const defaultDetail = computed<RequestParam>(() => {
|
||||
return {
|
||||
...(defaultCaseParams as RequestParam),
|
||||
apiDefinitionId: apiDefinitionId.value,
|
||||
protocol: apiDetailInfo.value.protocol,
|
||||
};
|
||||
});
|
||||
const detailForm = ref(cloneDeep(defaultDetail.value));
|
||||
const isEdit = ref(false);
|
||||
|
||||
function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
|
||||
async function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
|
||||
apiDefinitionId.value = apiId;
|
||||
// 从api下的用例里打开抽屉有api信息,从case下直接复制没有api信息
|
||||
if (props.apiDetail) {
|
||||
apiDetailInfo.value = props.apiDetail;
|
||||
apiDetailInfo.value = cloneDeep(props.apiDetail);
|
||||
} else {
|
||||
getApiDetail();
|
||||
await getApiDetail();
|
||||
}
|
||||
// 创建或者复制的时候,请求参数为接口定义的请求参数
|
||||
detailForm.value = {
|
||||
...cloneDeep(defaultDetail.value),
|
||||
headers: apiDetailInfo.value.headers ?? apiDetailInfo.value.request.headers,
|
||||
body: apiDetailInfo.value.body ?? apiDetailInfo.value.request.body,
|
||||
rest: apiDetailInfo.value.rest ?? apiDetailInfo.value.request.rest,
|
||||
query: apiDetailInfo.value.query ?? apiDetailInfo.value.request.query,
|
||||
};
|
||||
// 复制
|
||||
if (isCopy) {
|
||||
detailForm.value.name = `copy_${record?.name}`;
|
||||
|
@ -184,6 +198,7 @@
|
|||
if (!isCopy && record?.id) {
|
||||
isEdit.value = true;
|
||||
detailForm.value = cloneDeep(record as RequestParam);
|
||||
detailForm.value.isNew = false;
|
||||
}
|
||||
innerVisible.value = true;
|
||||
}
|
||||
|
@ -193,7 +208,6 @@
|
|||
isEdit.value = false;
|
||||
innerVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
detailForm.value = cloneDeep(defaultDetail);
|
||||
}
|
||||
|
||||
function handleDrawerConfirm(isContinue: boolean) {
|
||||
|
@ -236,7 +250,7 @@
|
|||
if (!isContinue) {
|
||||
handleSaveCaseCancel();
|
||||
}
|
||||
detailForm.value = cloneDeep(defaultDetail);
|
||||
detailForm.value = cloneDeep(defaultDetail.value);
|
||||
drawerLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<a-dropdown-button
|
||||
v-if="!caseDetail.executeLoading"
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
|
||||
class="exec-btn"
|
||||
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||
@select="execute"
|
||||
>
|
||||
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
|
||||
<template v-if="hasLocalExec" #icon>
|
||||
<icon-down />
|
||||
</template>
|
||||
<template v-if="hasLocalExec" #content>
|
||||
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
|
||||
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
<a-button v-else type="primary" @click="stopDebug">{{ t('common.stop') }}</a-button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
|
||||
import { debugCase, runCase } from '@/api/modules/api-test/management';
|
||||
import { getSocket } from '@/api/modules/project-management/commonScript';
|
||||
import { getLocalConfig } from '@/api/modules/user/index';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getGenerateId } from '@/utils';
|
||||
|
||||
import { defaultResponse } from '@/views/api-test/components/config';
|
||||
|
||||
const props = defineProps<{
|
||||
environmentId: string;
|
||||
request?: (...args) => Record<string, any>;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const caseDetail = defineModel<RequestParam>('detail', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const hasLocalExec = ref(false); // 是否配置了api本地执行
|
||||
const isPriorityLocalExec = ref(false); // 是否优先本地执行
|
||||
const localExecuteUrl = ref('');
|
||||
const reportId = ref('');
|
||||
const websocket = ref<WebSocket>();
|
||||
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
|
||||
async function initLocalConfig() {
|
||||
if (hasLocalExec.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getLocalConfig(); // TODO: 会报错
|
||||
const apiLocalExec = res.find((e) => e.type === 'API');
|
||||
if (apiLocalExec) {
|
||||
hasLocalExec.value = true;
|
||||
isPriorityLocalExec.value = apiLocalExec.enable || false;
|
||||
localExecuteUrl.value = apiLocalExec.userUrl || '';
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(executeType?: 'localExec' | 'serverExec') {
|
||||
websocket.value = getSocket(
|
||||
reportId.value,
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl.value : ''
|
||||
);
|
||||
websocket.value.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.msgType === 'EXEC_RESULT') {
|
||||
if (caseDetail.value.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
// TODO: 渲染出用例详情的响应数据
|
||||
caseDetail.value.response = data.taskResult; // 渲染出创建用例抽屉的响应数据
|
||||
caseDetail.value.executeLoading = false;
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
temporaryResponseMap[data.reportId] = data.taskResult;
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
caseDetail.value.executeLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function execute(executeType?: 'localExec' | 'serverExec') {
|
||||
try {
|
||||
caseDetail.value.executeLoading = true;
|
||||
caseDetail.value.response = cloneDeep(defaultResponse);
|
||||
const makeRequestParams = props.request && props.request(executeType); // 写在reportId之前,防止覆盖reportId
|
||||
reportId.value = getGenerateId();
|
||||
caseDetail.value.reportId = reportId.value; // 存储报告ID
|
||||
let res;
|
||||
const params = {
|
||||
environmentId: props.environmentId as string,
|
||||
frontendDebug: executeType === 'localExec',
|
||||
reportId: reportId.value,
|
||||
};
|
||||
debugSocket(executeType); // 开启websocket
|
||||
if ((caseDetail.value.id as string).startsWith('c')) {
|
||||
// 还没创建
|
||||
res = await debugCase({
|
||||
request: makeRequestParams?.request,
|
||||
linkFileIds: makeRequestParams?.linkFileIds,
|
||||
uploadFileIds: makeRequestParams?.uploadFileIds,
|
||||
id: `case-${Date.now()}`,
|
||||
projectId: appStore.currentProjectId,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
res = await runCase({
|
||||
request: caseDetail.value.request,
|
||||
id: caseDetail.value.id as string,
|
||||
projectId: caseDetail.value.projectId,
|
||||
linkFileIds: caseDetail.value.linkFileIds,
|
||||
uploadFileIds: caseDetail.value.uploadFileIds,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
if (executeType === 'localExec') {
|
||||
await localExecuteApiDebug(localExecuteUrl.value, res); // TODO: 会报错
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
caseDetail.value.executeLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopDebug() {
|
||||
websocket.value?.close();
|
||||
caseDetail.value.executeLoading = false;
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initLocalConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.exec-btn :deep(.arco-btn) {
|
||||
color: white !important;
|
||||
background-color: rgb(var(--primary-5)) !important;
|
||||
.btn-base-primary-hover();
|
||||
.btn-base-primary-active();
|
||||
.btn-base-primary-disabled();
|
||||
}
|
||||
</style>
|
|
@ -2,6 +2,7 @@
|
|||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div v-show="activeApiTab.id === 'all'" class="flex-1 overflow-hidden">
|
||||
<caseTable
|
||||
ref="caseTableRef"
|
||||
:is-api="false"
|
||||
:active-module="props.activeModule"
|
||||
:protocol="props.protocol"
|
||||
|
@ -12,7 +13,7 @@
|
|||
<caseDetail
|
||||
:detail="activeApiTab"
|
||||
:module-tree="props.moduleTree"
|
||||
:protocol="props.protocol"
|
||||
@delete-case="deleteCase"
|
||||
@update-follow="activeApiTab.follow = !activeApiTab.follow"
|
||||
@load-case="(id: string) => openOrUpdateCaseTab(false, id)"
|
||||
/>
|
||||
|
@ -42,6 +43,9 @@
|
|||
protocol: string;
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'deleteCase', id: string): void;
|
||||
}>();
|
||||
|
||||
const apiTabs = defineModel<RequestParam[]>('apiTabs', {
|
||||
required: true,
|
||||
|
@ -58,16 +62,15 @@
|
|||
try {
|
||||
loading.value = true;
|
||||
const res = await getCaseDetail(id);
|
||||
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件;
|
||||
// if (res.protocol === 'HTTP') { // TODO: 后端没protocol字段,问一下
|
||||
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
// }
|
||||
let parseRequestBodyResult;
|
||||
if (res.protocol === 'HTTP') {
|
||||
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
}
|
||||
const tabItemInfo = {
|
||||
...cloneDeep(defaultCaseParams as RequestParam),
|
||||
...({
|
||||
...res.request,
|
||||
...res,
|
||||
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: 后端没response字段,问一下
|
||||
url: res.path,
|
||||
...parseRequestBodyResult,
|
||||
} as Partial<TabItem>),
|
||||
|
@ -92,7 +95,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function openCaseTab(apiInfo: ApiCaseDetail) {
|
||||
async function openCaseTab(apiInfo: ApiCaseDetail | string) {
|
||||
const isLoadedTabIndex = apiTabs.value.findIndex(
|
||||
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
|
||||
);
|
||||
|
@ -103,4 +106,14 @@
|
|||
}
|
||||
await openOrUpdateCaseTab(true, typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
|
||||
}
|
||||
|
||||
const caseTableRef = ref<InstanceType<typeof caseTable>>();
|
||||
function deleteCase(id: string) {
|
||||
emit('deleteCase', id);
|
||||
caseTableRef.value?.loadCaseList();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openCaseTab,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -47,12 +47,14 @@
|
|||
:module-tree="props.moduleTree"
|
||||
/>
|
||||
<apiCase
|
||||
v-if="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'"
|
||||
v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'"
|
||||
ref="caseRef"
|
||||
v-model:api-tabs="apiTabs"
|
||||
v-model:active-api-tab="activeApiTab"
|
||||
:active-module="props.activeModule"
|
||||
:protocol="props.protocol"
|
||||
:module-tree="props.moduleTree"
|
||||
@delete-case="(id) => handleDeleteApiFromModuleTree(id)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -67,11 +69,12 @@
|
|||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
// import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
|
||||
import { getEnvironment, getEnvList } from '@/api/modules/api-test/common';
|
||||
import { getEnvironment, getEnvList, getProtocolList } from '@/api/modules/api-test/common';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import router from '@/router';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ProtocolItem } from '@/models/apiTest/common';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import {
|
||||
|
@ -104,6 +107,7 @@
|
|||
];
|
||||
|
||||
const apiRef = ref<InstanceType<typeof api>>();
|
||||
const caseRef = ref<InstanceType<typeof apiCase>>();
|
||||
|
||||
function newTab(apiInfo?: ModuleTreeNode | string, isCopy?: boolean, isExecute?: boolean) {
|
||||
if (apiInfo) {
|
||||
|
@ -113,6 +117,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
function newCaseTab(id: string) {
|
||||
caseRef.value?.openCaseTab(id);
|
||||
}
|
||||
|
||||
const apiTabs = ref<RequestParam[]>([
|
||||
{
|
||||
id: 'all',
|
||||
|
@ -310,16 +318,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
const protocols = ref<ProtocolItem[]>([]);
|
||||
async function initProtocolList() {
|
||||
try {
|
||||
protocols.value = await getProtocolList(appStore.currentOrgId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initEnvList();
|
||||
initProtocolList();
|
||||
});
|
||||
|
||||
/** 向孙组件提供属性 */
|
||||
provide('currentEnvConfig', readonly(currentEnvConfig));
|
||||
provide('defaultCaseParams', readonly(defaultCaseParams));
|
||||
provide('protocols', readonly(protocols));
|
||||
|
||||
defineExpose({
|
||||
newTab,
|
||||
newCaseTab,
|
||||
refreshApiTable,
|
||||
handleApiUpdateFromModuleTree,
|
||||
handleDeleteApiFromModuleTree,
|
||||
|
|
|
@ -149,6 +149,9 @@
|
|||
if (route.query.dId) {
|
||||
// 携带 dId 参数,自动打开接口定义详情 tab
|
||||
managementRef.value?.newTab(route.query.dId as string);
|
||||
} else if (route.query.cId) {
|
||||
// 携带 cId 参数,自动打开接口用例详情 tab
|
||||
managementRef.value?.newCaseTab(route.query.cId as string);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue