feat(接口管理): 用例编辑&重构用例详情
This commit is contained in:
parent
f5dc90ffd8
commit
88a4e8b1e3
|
@ -55,6 +55,7 @@ import {
|
|||
SortCaseUrl,
|
||||
SortDefinitionUrl,
|
||||
SwitchDefinitionScheduleUrl,
|
||||
ToggleFollowCaseUrl,
|
||||
ToggleFollowDefinitionUrl,
|
||||
TransferFileCaseUrl,
|
||||
TransferFileModuleOptionCaseUrl,
|
||||
|
@ -401,6 +402,11 @@ export function getCaseDetail(id: string) {
|
|||
return MSR.get<ApiCaseDetail>({ url: GetCaseDetailUrl, params: id });
|
||||
}
|
||||
|
||||
// 关注/取消关注接口用例
|
||||
export function toggleFollowCase(id: string | number) {
|
||||
return MSR.get({ url: ToggleFollowCaseUrl, params: id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口用例回收站
|
||||
*/
|
||||
|
|
|
@ -73,6 +73,7 @@ export const ExecuteCaseUrl = '/api/case/run/'; // 单独执行接口用例
|
|||
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'; // 接口定义-关注/取消关注
|
||||
|
||||
/**
|
||||
* 接口用例回收站
|
||||
|
|
|
@ -373,7 +373,7 @@ export interface ExecutePluginRequestParams {
|
|||
// 执行接口调试入参
|
||||
export interface ExecuteRequestParams {
|
||||
id?: string;
|
||||
reportId: string;
|
||||
reportId?: string;
|
||||
environmentId: string;
|
||||
uploadFileIds: string[];
|
||||
linkFileIds: string[];
|
||||
|
|
|
@ -343,8 +343,10 @@ export interface AddApiCaseParams extends ExecuteRequestParams {
|
|||
name: string;
|
||||
priority: string;
|
||||
status: string;
|
||||
apiDefinitionId: string | number;
|
||||
apiDefinitionId?: string | number;
|
||||
tags: string[];
|
||||
deleteFileIds?: string[];
|
||||
unLinkFileIds?: string[];
|
||||
}
|
||||
|
||||
export interface ApiRunModeRequest {
|
||||
|
|
|
@ -139,7 +139,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="request-params-tab px-[16px]">
|
||||
<div class="px-[16px]">
|
||||
<MsTab
|
||||
v-model:active-key="requestVModel.activeTab"
|
||||
:content-tab-list="contentTabList"
|
||||
|
@ -1528,9 +1528,6 @@
|
|||
:deep(.arco-tabs-tab) {
|
||||
@apply leading-none;
|
||||
}
|
||||
.request-params-tab :deep(.arco-tabs-nav-tab) {
|
||||
border-bottom: 1px solid var(--color-text-n8) !important;
|
||||
}
|
||||
.hidden-second {
|
||||
:deep(.arco-split-trigger) {
|
||||
@apply hidden;
|
||||
|
|
|
@ -338,6 +338,7 @@
|
|||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isCase?: boolean; // case详情
|
||||
detail: RequestParam;
|
||||
protocols: ProtocolItem[];
|
||||
}>();
|
||||
|
@ -409,6 +410,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (!props.isCase) return;
|
||||
// case编辑后需要刷新数据
|
||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.detail.id,
|
||||
() => {
|
||||
|
|
|
@ -2,19 +2,6 @@
|
|||
<div class="h-full w-full overflow-hidden">
|
||||
<div class="px-[18px] pt-[16px]">
|
||||
<MsDetailCard
|
||||
v-if="props.isCaseDetail"
|
||||
:title="`【${previewDetail.num}】${previewDetail.name}`"
|
||||
:description="description"
|
||||
>
|
||||
<template #type="{ value }">
|
||||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||
</template>
|
||||
<template #priority="{ value }">
|
||||
<caseLevel :case-level="value as CaseLevel" />
|
||||
</template>
|
||||
</MsDetailCard>
|
||||
<MsDetailCard
|
||||
v-else
|
||||
:title="`【${previewDetail.num}】${previewDetail.name}`"
|
||||
:description="description"
|
||||
:simple-show-count="4"
|
||||
|
@ -78,8 +65,6 @@
|
|||
|
||||
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||
import detailTab from './detail.vue';
|
||||
import history from './history.vue';
|
||||
import quote from './quote.vue';
|
||||
|
@ -100,7 +85,6 @@
|
|||
detail: RequestParam;
|
||||
moduleTree: ModuleTreeNode[];
|
||||
protocols: ProtocolItem[];
|
||||
isCaseDetail?: boolean; // 在用例详情里显示
|
||||
}>();
|
||||
const emit = defineEmits(['updateFollow']);
|
||||
|
||||
|
@ -113,7 +97,6 @@
|
|||
() => props.detail.id,
|
||||
() => {
|
||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
if (props.isCaseDetail) return;
|
||||
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
||||
previewDetail.value = {
|
||||
...previewDetail.value,
|
||||
|
@ -137,66 +120,49 @@
|
|||
}
|
||||
);
|
||||
|
||||
const description = computed(() => {
|
||||
const commonDescription = [
|
||||
{
|
||||
key: 'type',
|
||||
locale: 'apiTestManagement.apiType',
|
||||
value: previewDetail.value.method,
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
locale: 'apiTestManagement.path',
|
||||
value: previewDetail.value.url || previewDetail.value.path,
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
locale: 'common.tag',
|
||||
value: previewDetail.value.tags,
|
||||
},
|
||||
];
|
||||
if (!props.isCaseDetail) {
|
||||
return [
|
||||
...commonDescription,
|
||||
...[
|
||||
{
|
||||
key: 'description',
|
||||
locale: 'common.desc',
|
||||
value: previewDetail.value.description,
|
||||
width: '100%',
|
||||
},
|
||||
{
|
||||
key: 'belongModule',
|
||||
locale: 'apiTestManagement.belongModule',
|
||||
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path,
|
||||
},
|
||||
{
|
||||
key: 'creator',
|
||||
locale: 'common.creator',
|
||||
value: previewDetail.value.createUserName,
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
locale: 'apiTestManagement.createTime',
|
||||
value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
key: 'updateTime',
|
||||
locale: 'apiTestManagement.updateTime',
|
||||
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
// 处理用例详情的
|
||||
const caseDescription = commonDescription.slice();
|
||||
caseDescription.splice(1, 0, {
|
||||
key: 'priority',
|
||||
locale: 'case.caseLevel',
|
||||
value: previewDetail.value.priority,
|
||||
});
|
||||
return caseDescription;
|
||||
});
|
||||
const description = computed(() => [
|
||||
{
|
||||
key: 'type',
|
||||
locale: 'apiTestManagement.apiType',
|
||||
value: previewDetail.value.method,
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
locale: 'apiTestManagement.path',
|
||||
value: previewDetail.value.url || previewDetail.value.path,
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
locale: 'common.tag',
|
||||
value: previewDetail.value.tags,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
locale: 'common.desc',
|
||||
value: previewDetail.value.description,
|
||||
width: '100%',
|
||||
},
|
||||
{
|
||||
key: 'belongModule',
|
||||
locale: 'apiTestManagement.belongModule',
|
||||
value: findNodeByKey<ModuleTreeNode>(props.moduleTree, previewDetail.value.moduleId, 'id')?.path,
|
||||
},
|
||||
{
|
||||
key: 'creator',
|
||||
locale: 'common.creator',
|
||||
value: previewDetail.value.createUserName,
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
locale: 'apiTestManagement.createTime',
|
||||
value: dayjs(previewDetail.value.createTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
key: 'updateTime',
|
||||
locale: 'apiTestManagement.updateTime',
|
||||
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
]);
|
||||
|
||||
const followLoading = ref(false);
|
||||
async function toggleFollowReview() {
|
||||
|
|
|
@ -1,33 +1,126 @@
|
|||
<template>
|
||||
<preview
|
||||
:detail="activeApiTab"
|
||||
:module-tree="props.moduleTree"
|
||||
:protocols="protocols"
|
||||
is-case-detail
|
||||
@update-follow="activeApiTab.follow = !activeApiTab.follow"
|
||||
/>
|
||||
<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>
|
||||
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
|
||||
<a-button>{{ t('common.operation') }}</a-button>
|
||||
<template #content>
|
||||
<a-doption value="edit">
|
||||
<MsIcon type="icon-icon_edit_outlined" />
|
||||
{{ t('common.edit') }}
|
||||
</a-doption>
|
||||
<a-doption value="share">
|
||||
<MsIcon type="icon-icon_share1" />
|
||||
{{ t('common.share') }}
|
||||
</a-doption>
|
||||
<a-doption 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))]">
|
||||
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
|
||||
{{ t('common.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
<template #priority="{ value }">
|
||||
<caseLevel :case-level="value as CaseLevel" />
|
||||
</template>
|
||||
</MsDetailCard>
|
||||
<detailTab :detail="caseDetail" :protocols="protocols" is-case />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
|
||||
<quote :source-id="caseDetail.id" />
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
|
||||
</a-tab-pane> -->
|
||||
<a-tab-pane key="changeHistory" :title="t('apiTestManagement.changeHistory')" class="px-[18px] py-[16px]">
|
||||
<history :source-id="caseDetail.id" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" :protocol="props.protocol" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import preview from '@/views/api-test/management/components/management/api/preview/index.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
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 detailTab from '../api/preview/detail.vue';
|
||||
import history from '../api/preview/history.vue';
|
||||
import quote from '../api/preview/quote.vue';
|
||||
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
import { getProtocolList } from '@/api/modules/api-test/common';
|
||||
import { toggleFollowCase } from '@/api/modules/api-test/management';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ProtocolItem } from '@/models/apiTest/common';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
|
||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import { RequestMethods } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
isDrawer?: boolean; // 抽屉
|
||||
detail: RequestParam;
|
||||
protocol: string;
|
||||
}>();
|
||||
const emit = defineEmits(['updateFollow']);
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
|
||||
required: true,
|
||||
});
|
||||
const caseDetail = computed<RequestParam>(() => cloneDeep(props.detail)); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
const activeKey = ref('detail');
|
||||
|
||||
const description = computed(() => [
|
||||
{
|
||||
key: 'type',
|
||||
locale: 'apiTestManagement.apiType',
|
||||
value: caseDetail.value.method,
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
locale: 'case.caseLevel',
|
||||
value: caseDetail.value.priority,
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
locale: 'apiTestManagement.path',
|
||||
value: caseDetail.value.url || caseDetail.value.path,
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
locale: 'common.tag',
|
||||
value: caseDetail.value.tags,
|
||||
},
|
||||
]);
|
||||
|
||||
const protocols = ref<ProtocolItem[]>([]);
|
||||
async function initProtocolList() {
|
||||
|
@ -42,4 +135,81 @@
|
|||
onBeforeMount(() => {
|
||||
initProtocolList();
|
||||
});
|
||||
|
||||
const followLoading = ref(false);
|
||||
async function follow() {
|
||||
try {
|
||||
followLoading.value = true;
|
||||
await toggleFollowCase(caseDetail.value.id);
|
||||
Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
|
||||
emit('updateFollow');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
followLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function share() {
|
||||
if (isSupported) {
|
||||
copy(`${window.location.href}&dId=${caseDetail.value.id}`);
|
||||
Message.success(t('apiTestManagement.shareUrlCopied'));
|
||||
} else {
|
||||
Message.error(t('common.copyNotSupport'));
|
||||
}
|
||||
}
|
||||
|
||||
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
|
||||
function editCase() {
|
||||
createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value, false);
|
||||
}
|
||||
|
||||
function handleSelect(val: string | number | Record<string, any> | undefined) {
|
||||
switch (val) {
|
||||
case 'edit':
|
||||
editCase();
|
||||
break;
|
||||
case 'share':
|
||||
share();
|
||||
break;
|
||||
case 'fork':
|
||||
follow();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
editCase,
|
||||
share,
|
||||
follow,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-tabs-nav) {
|
||||
border-bottom: 1px solid var(--color-text-n8);
|
||||
}
|
||||
:deep(.arco-tabs-content) {
|
||||
@apply pt-0;
|
||||
.arco-tabs-content-item {
|
||||
@apply px-0;
|
||||
}
|
||||
}
|
||||
:deep(.ms-detail-card-desc) {
|
||||
gap: 16px;
|
||||
flex-wrap: nowrap !important;
|
||||
& > div:nth-of-type(n) {
|
||||
width: auto;
|
||||
max-width: 30%;
|
||||
}
|
||||
}
|
||||
.error-6 {
|
||||
color: rgb(var(--danger-6));
|
||||
&:hover {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="innerVisible"
|
||||
unmount-on-close
|
||||
:title="t('caseManagement.featureCase.caseDetail')"
|
||||
:width="894"
|
||||
:footer="false"
|
||||
no-content-padding
|
||||
>
|
||||
<template #headerLeft>
|
||||
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
|
||||
</template>
|
||||
<template #tbutton>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
@click="caseDerailRef?.editCase()"
|
||||
>
|
||||
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
|
||||
{{ t('common.edit') }}
|
||||
</MsButton>
|
||||
<MsButton type="icon" status="secondary" @click="caseDerailRef?.share()">
|
||||
<MsIcon type="icon-icon_share1" class="mr-[8px]" />
|
||||
{{ t('common.share') }}
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
@click="caseDerailRef?.follow()"
|
||||
>
|
||||
<MsIcon
|
||||
:type="props.detail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
|
||||
class="mr-[8px]"
|
||||
:class="[props.detail.follow ? 'text-[rgb(var(--warning-6))]' : '']"
|
||||
/>
|
||||
{{ t('common.fork') }}
|
||||
</MsButton>
|
||||
<MsButton type="icon" status="secondary">
|
||||
<a-dropdown position="br">
|
||||
<div>
|
||||
<icon-more class="mr-[8px]" />
|
||||
<span> {{ t('common.more') }}</span>
|
||||
</div>
|
||||
<template #content>
|
||||
<a-doption
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+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>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<caseDetail
|
||||
ref="caseDerailRef"
|
||||
is-drawer
|
||||
:detail="props.detail"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="props.apiDetail"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
|
||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
detail: RequestParam;
|
||||
protocol: string;
|
||||
apiDetail: RequestParam;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerVisible = defineModel<boolean>('visible', {
|
||||
required: true,
|
||||
});
|
||||
const caseDerailRef = ref<InstanceType<typeof caseDetail>>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.error-6 {
|
||||
color: rgb(var(--danger-6));
|
||||
&:hover {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="overflow-hidden p-[16px_22px]">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div :class="['mb-[16px]', 'flex', 'items-center', props.isApi ? 'justify-between' : 'justify-end']">
|
||||
<a-button
|
||||
v-show="props.isApi"
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
|
||||
|
@ -37,7 +37,9 @@
|
|||
@drag-change="handleDragChange"
|
||||
>
|
||||
<template #num="{ record }">
|
||||
<MsButton type="text" @click="openCaseTab(record)">{{ record.num }}</MsButton>
|
||||
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">{{
|
||||
record.num
|
||||
}}</MsButton>
|
||||
</template>
|
||||
<template #caseLevel="{ record }">
|
||||
<a-select
|
||||
|
@ -236,12 +238,19 @@
|
|||
</template>
|
||||
</a-modal>
|
||||
<createAndEditCaseDrawer
|
||||
v-if="props.isApi"
|
||||
ref="createAndEditCaseDrawerRef"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="apiDetail as RequestParam"
|
||||
: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)"
|
||||
/>
|
||||
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
|
||||
<template #title>
|
||||
{{ t('report.trigger.batch.execution') }}
|
||||
|
@ -337,8 +346,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
|
@ -346,6 +357,7 @@
|
|||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import caseDetailDrawer from './caseDetailDrawer.vue';
|
||||
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
|
||||
|
@ -356,6 +368,7 @@
|
|||
deleteCase,
|
||||
dragSort,
|
||||
executeCase,
|
||||
getCaseDetail,
|
||||
getCasePage,
|
||||
getEnvList,
|
||||
getPoolId,
|
||||
|
@ -376,6 +389,7 @@
|
|||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
isApi: boolean; // 接口定义详情的case tab下
|
||||
|
@ -949,15 +963,52 @@
|
|||
|
||||
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
|
||||
function createCase() {
|
||||
createAndEditCaseDrawerRef.value?.open();
|
||||
createAndEditCaseDrawerRef.value?.open(props.apiDetail?.id as string);
|
||||
}
|
||||
function copyCase(record: ApiCaseDetail) {
|
||||
createAndEditCaseDrawerRef.value?.open(record, true);
|
||||
createAndEditCaseDrawerRef.value?.open(record.apiDefinitionId, record, true);
|
||||
}
|
||||
|
||||
function openCaseTab(record: ApiCaseDetail) {
|
||||
emit('openCaseTab', record);
|
||||
}
|
||||
|
||||
const caseDetailDrawerVisible = ref(false);
|
||||
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
|
||||
const caseDetail = ref<Record<string, any>>({});
|
||||
|
||||
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 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
// }
|
||||
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>),
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
async function openCaseDetailDrawer(id: string) {
|
||||
await getCaseDetailInfo(id);
|
||||
caseDetailDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 在api下的用例里打开用例详情抽屉,点击编辑,编辑后在此刷新数据
|
||||
async function loadCase(id: string) {
|
||||
getCaseDetailInfo(id);
|
||||
loadCaseList();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="innerVisible"
|
||||
:title="t('case.createCase')"
|
||||
:title="isEdit ? t('case.updateCase') : t('case.createCase')"
|
||||
:width="894"
|
||||
no-content-padding
|
||||
:ok-text="t('common.create')"
|
||||
:ok-text="isEdit ? 'common.update' : 'common.create'"
|
||||
:ok-loading="drawerLoading"
|
||||
:save-continue-text="t('case.saveContinueText')"
|
||||
:show-continue="true"
|
||||
@confirm="handleDrawerConfirm"
|
||||
:show-continue="!isEdit && !!props.apiDetail"
|
||||
@confirm="handleDrawerConfirm(false)"
|
||||
@continue="handleDrawerConfirm(true)"
|
||||
@cancel="handleSaveCaseCancel"
|
||||
>
|
||||
|
@ -18,7 +18,7 @@
|
|||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<div class="px-[16px] pt-[16px]">
|
||||
<MsDetailCard
|
||||
:title="`【${apiDataDetail.num}】${apiDataDetail.name}`"
|
||||
:title="`【${apiDetailInfo.num}】${apiDetailInfo.name}`"
|
||||
:description="description"
|
||||
class="!flex-row justify-between"
|
||||
>
|
||||
|
@ -26,11 +26,11 @@
|
|||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||
</template>
|
||||
</MsDetailCard>
|
||||
<a-form ref="formRef" class="mt-[16px]" :model="caseModalForm" layout="vertical">
|
||||
<a-form ref="formRef" class="mt-[16px]" :model="detailForm" layout="vertical">
|
||||
<a-form-item field="name" label="" :rules="[{ required: true, message: t('case.caseNameRequired') }]">
|
||||
<div class="flex w-full items-center gap-[8px]">
|
||||
<a-input
|
||||
v-model:model-value="caseModalForm.name"
|
||||
v-model:model-value="detailForm.name"
|
||||
:placeholder="t('case.caseNamePlaceholder')"
|
||||
allow-clear
|
||||
:max-length="255"
|
||||
|
@ -43,9 +43,9 @@
|
|||
</a-form-item>
|
||||
<div class="flex gap-[16px]">
|
||||
<a-form-item field="priority" :label="t('case.caseLevel')">
|
||||
<a-select v-model:model-value="caseModalForm.priority" :placeholder="t('common.pleaseSelect')">
|
||||
<a-select v-model:model-value="detailForm.priority" :placeholder="t('common.pleaseSelect')">
|
||||
<template #label>
|
||||
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="caseModalForm.priority" /></span>
|
||||
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="detailForm.priority" /></span>
|
||||
</template>
|
||||
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
||||
<caseLevel :case-level="item.label as CaseLevel" />
|
||||
|
@ -53,9 +53,9 @@
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item field="status" :label="t('apiTestManagement.apiStatus')">
|
||||
<a-select v-model:model-value="caseModalForm.status" :placeholder="t('common.pleaseSelect')">
|
||||
<a-select v-model:model-value="detailForm.status" :placeholder="t('common.pleaseSelect')">
|
||||
<template #label>
|
||||
<apiStatus :status="caseModalForm.status" />
|
||||
<apiStatus :status="detailForm.status" />
|
||||
</template>
|
||||
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
|
||||
<apiStatus :status="item" />
|
||||
|
@ -63,7 +63,7 @@
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item field="tags" :label="t('common.tag')">
|
||||
<MsTagsInput v-model:model-value="caseModalForm.tags" />
|
||||
<MsTagsInput v-model:model-value="detailForm.tags" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
|
@ -72,11 +72,11 @@
|
|||
<div class="flex-1 overflow-hidden">
|
||||
<requestComposition
|
||||
ref="requestCompositionRef"
|
||||
v-model:request="apiDataDetail"
|
||||
v-model:request="detailForm"
|
||||
:is-case="true"
|
||||
hide-response-layout-switch
|
||||
:upload-temp-file-api="uploadTempFileCase"
|
||||
:file-save-as-source-id="apiDataDetail.id"
|
||||
:file-save-as-source-id="detailForm.id"
|
||||
:file-module-options-api="getTransferOptionsCase"
|
||||
:file-save-as-api="transferFileCase"
|
||||
:current-env-config="currentEnvConfig"
|
||||
|
@ -103,41 +103,55 @@
|
|||
|
||||
import {
|
||||
addCase,
|
||||
getDefinitionDetail,
|
||||
getTransferOptionsCase,
|
||||
transferFileCase,
|
||||
updateCase,
|
||||
uploadTempFileCase,
|
||||
} from '@/api/modules/api-test/management';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ApiCaseDetail } from '@/models/apiTest/management';
|
||||
import { AddApiCaseParams, ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
|
||||
|
||||
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||
|
||||
const props = defineProps<{
|
||||
apiDetail: RequestParam;
|
||||
apiDetail?: RequestParam | ApiDefinitionDetail;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'loadCase', id?: string): void;
|
||||
}>();
|
||||
const emit = defineEmits(['loadCase']);
|
||||
|
||||
const apiDataDetail = ref<RequestParam>(cloneDeep(props.apiDetail));
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const innerVisible = ref(false);
|
||||
|
||||
const drawerLoading = ref(false);
|
||||
|
||||
const apiDefinitionId = ref('');
|
||||
const apiDetailInfo = ref<Record<string, any>>({});
|
||||
async function getApiDetail() {
|
||||
try {
|
||||
apiDetailInfo.value = await getDefinitionDetail(apiDefinitionId.value);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const description = computed(() => [
|
||||
{
|
||||
key: 'type',
|
||||
locale: 'apiTestManagement.apiType',
|
||||
value: apiDataDetail.value.method,
|
||||
value: apiDetailInfo.value.method,
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
locale: 'apiTestManagement.path',
|
||||
value: apiDataDetail.value.url || apiDataDetail.value.path,
|
||||
value: apiDetailInfo.value.url || apiDetailInfo.value.path,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -145,47 +159,84 @@
|
|||
const currentEnvConfig = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const initForm: any = {
|
||||
apiDefinitionId: apiDataDetail.value.id as string,
|
||||
name: '',
|
||||
priority: 'P0',
|
||||
tags: [],
|
||||
status: RequestDefinitionStatus.PROCESSING,
|
||||
};
|
||||
const caseModalForm = ref({ ...initForm });
|
||||
|
||||
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 isEdit = ref(false);
|
||||
|
||||
function open(record?: ApiCaseDetail, isCopy?: boolean) {
|
||||
innerVisible.value = true;
|
||||
if (isCopy) {
|
||||
caseModalForm.value.name = record?.name;
|
||||
function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
|
||||
apiDefinitionId.value = apiId;
|
||||
// 从api下的用例里打开抽屉有api信息,从case下直接复制没有api信息
|
||||
if (props.apiDetail) {
|
||||
apiDetailInfo.value = props.apiDetail;
|
||||
} else {
|
||||
getApiDetail();
|
||||
}
|
||||
// 复制
|
||||
if (isCopy) {
|
||||
detailForm.value.name = `copy_${record?.name}`;
|
||||
}
|
||||
// 编辑
|
||||
if (!isCopy && record?.id) {
|
||||
isEdit.value = true;
|
||||
detailForm.value = cloneDeep(record as RequestParam);
|
||||
}
|
||||
innerVisible.value = true;
|
||||
}
|
||||
|
||||
function handleSaveCaseCancel() {
|
||||
drawerLoading.value = false;
|
||||
isEdit.value = false;
|
||||
innerVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
caseModalForm.value = { ...initForm };
|
||||
detailForm.value = cloneDeep(defaultDetail);
|
||||
}
|
||||
|
||||
function handleDrawerConfirm(isContinue: boolean) {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
drawerLoading.value = true;
|
||||
const params = { ...requestCompositionRef.value?.makeRequestParams(), ...caseModalForm.value };
|
||||
// 给后端传的参数
|
||||
if (!requestCompositionRef.value?.makeRequestParams()) return;
|
||||
const { linkFileIds, uploadFileIds, request, unLinkFileIds, deleteFileIds } =
|
||||
requestCompositionRef.value.makeRequestParams();
|
||||
const { name, priority, status, tags, id } = detailForm.value;
|
||||
const params: AddApiCaseParams = {
|
||||
projectId: appStore.currentProjectId,
|
||||
environmentId: currentEnvConfig.value?.id as string,
|
||||
apiDefinitionId: apiDefinitionId.value,
|
||||
linkFileIds,
|
||||
uploadFileIds,
|
||||
request,
|
||||
id: id as string,
|
||||
name,
|
||||
priority,
|
||||
status,
|
||||
tags,
|
||||
unLinkFileIds,
|
||||
deleteFileIds,
|
||||
};
|
||||
try {
|
||||
await addCase(params);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
if (isEdit.value) {
|
||||
await updateCase(params);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} else {
|
||||
await addCase(params);
|
||||
Message.success(t('common.createSuccess'));
|
||||
}
|
||||
emit('loadCase', id as string);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
if (!isContinue) {
|
||||
emit('loadCase');
|
||||
handleSaveCaseCancel();
|
||||
}
|
||||
caseModalForm.value = { ...initForm };
|
||||
detailForm.value = cloneDeep(defaultDetail);
|
||||
drawerLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,7 +9,13 @@
|
|||
/>
|
||||
</div>
|
||||
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
|
||||
<caseDetail :active-api-tab="activeApiTab" :module-tree="props.moduleTree" />
|
||||
<caseDetail
|
||||
:detail="activeApiTab"
|
||||
:module-tree="props.moduleTree"
|
||||
:protocol="props.protocol"
|
||||
@update-follow="activeApiTab.follow = !activeApiTab.follow"
|
||||
@load-case="(id: string) => openOrUpdateCaseTab(false, id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -24,9 +30,7 @@
|
|||
|
||||
import { ApiCaseDetail } from '@/models/apiTest/management';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { RequestAuthType, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
|
||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
|
||||
|
@ -47,106 +51,37 @@
|
|||
required: true,
|
||||
});
|
||||
|
||||
const initDefaultId = `case-${Date.now()}`;
|
||||
const defaultCaseParams: RequestParam = {
|
||||
type: 'case',
|
||||
id: initDefaultId,
|
||||
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
||||
protocol: 'HTTP',
|
||||
tags: [],
|
||||
description: '',
|
||||
url: '',
|
||||
activeTab: RequestComposition.HEADER,
|
||||
closable: true,
|
||||
method: RequestMethods.GET,
|
||||
headers: [],
|
||||
body: cloneDeep(defaultBodyParams),
|
||||
query: [],
|
||||
rest: [],
|
||||
polymorphicName: '',
|
||||
name: '',
|
||||
path: '',
|
||||
projectId: '',
|
||||
uploadFileIds: [],
|
||||
linkFileIds: [],
|
||||
authConfig: {
|
||||
authType: RequestAuthType.NONE,
|
||||
basicAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
digestAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
enableGlobal: false,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
processors: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
processors: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
otherConfig: {
|
||||
connectTimeout: 60000,
|
||||
responseTimeout: 60000,
|
||||
certificateAlias: '',
|
||||
followRedirects: true,
|
||||
autoRedirects: false,
|
||||
},
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
response: cloneDeep(defaultResponse),
|
||||
responseDefinition: [cloneDeep(defaultResponseItem)],
|
||||
isNew: true,
|
||||
unSaved: false,
|
||||
executeLoading: false,
|
||||
preDependency: [], // 前置依赖
|
||||
postDependency: [], // 后置依赖
|
||||
};
|
||||
|
||||
function addTab(defaultProps?: Partial<TabItem>) {
|
||||
apiTabs.value.push({
|
||||
...cloneDeep(defaultCaseParams),
|
||||
...defaultProps,
|
||||
});
|
||||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||
}
|
||||
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
|
||||
|
||||
const loading = ref(false);
|
||||
async function openCaseTab(apiInfo: ApiCaseDetail) {
|
||||
const isLoadedTabIndex = apiTabs.value.findIndex(
|
||||
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
|
||||
);
|
||||
if (isLoadedTabIndex > -1) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
|
||||
return;
|
||||
}
|
||||
async function openOrUpdateCaseTab(isOpen: boolean, id: string) {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getCaseDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
|
||||
const res = await getCaseDetail(id);
|
||||
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件;
|
||||
// if (res.protocol === 'HTTP') { // TODO: 后端没protocol字段,问一下
|
||||
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||
// }
|
||||
addTab({
|
||||
...res.request,
|
||||
...res,
|
||||
response: cloneDeep(defaultResponse),
|
||||
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: 后端没response字段,问一下
|
||||
url: res.path,
|
||||
...parseRequestBodyResult,
|
||||
});
|
||||
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>),
|
||||
};
|
||||
if (isOpen) {
|
||||
apiTabs.value.push(tabItemInfo);
|
||||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||
} else {
|
||||
// 更新数据
|
||||
const index = apiTabs.value.findIndex((item) => item.id === id);
|
||||
apiTabs.value[index] = tabItemInfo;
|
||||
activeApiTab.value = tabItemInfo;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
loading.value = false; // 等待内容渲染出来再隐藏loading
|
||||
});
|
||||
|
@ -156,4 +91,16 @@
|
|||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openCaseTab(apiInfo: ApiCaseDetail) {
|
||||
const isLoadedTabIndex = apiTabs.value.findIndex(
|
||||
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
|
||||
);
|
||||
if (isLoadedTabIndex > -1) {
|
||||
// 如果点击的请求在tab中已经存在,则直接切换到该tab
|
||||
activeApiTab.value = apiTabs.value[isLoadedTabIndex] as RequestParam;
|
||||
return;
|
||||
}
|
||||
await openOrUpdateCaseTab(true, typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { SelectOptionData } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
import api from './api/index.vue';
|
||||
|
@ -73,8 +74,17 @@
|
|||
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import {
|
||||
RequestAuthType,
|
||||
RequestComposition,
|
||||
RequestDefinitionStatus,
|
||||
RequestMethods,
|
||||
ResponseComposition,
|
||||
} from '@/enums/apiEnum';
|
||||
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
|
||||
|
||||
const props = defineProps<{
|
||||
activeModule: string;
|
||||
offspringIds: string[];
|
||||
|
@ -113,6 +123,76 @@
|
|||
]);
|
||||
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
|
||||
|
||||
// api下的创建用例弹窗也用到了defaultCaseParams
|
||||
const initDefaultId = `case-${Date.now()}`;
|
||||
const defaultCaseParams: RequestParam = {
|
||||
id: initDefaultId,
|
||||
type: 'case',
|
||||
moduleId: props.activeModule === 'all' ? 'root' : props.activeModule,
|
||||
protocol: 'HTTP',
|
||||
tags: [],
|
||||
description: '',
|
||||
priority: 'P0',
|
||||
status: RequestDefinitionStatus.PROCESSING,
|
||||
url: '',
|
||||
activeTab: RequestComposition.HEADER,
|
||||
closable: true,
|
||||
method: RequestMethods.GET,
|
||||
headers: [],
|
||||
body: cloneDeep(defaultBodyParams),
|
||||
query: [],
|
||||
rest: [],
|
||||
polymorphicName: '',
|
||||
name: '',
|
||||
path: '',
|
||||
projectId: '',
|
||||
uploadFileIds: [],
|
||||
linkFileIds: [],
|
||||
authConfig: {
|
||||
authType: RequestAuthType.NONE,
|
||||
basicAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
digestAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: {
|
||||
enableGlobal: false,
|
||||
assertions: [],
|
||||
},
|
||||
postProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
processors: [],
|
||||
},
|
||||
preProcessorConfig: {
|
||||
enableGlobal: false,
|
||||
processors: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
otherConfig: {
|
||||
connectTimeout: 60000,
|
||||
responseTimeout: 60000,
|
||||
certificateAlias: '',
|
||||
followRedirects: true,
|
||||
autoRedirects: false,
|
||||
},
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
response: cloneDeep(defaultResponse),
|
||||
responseDefinition: [cloneDeep(defaultResponseItem)],
|
||||
isNew: true,
|
||||
unSaved: false,
|
||||
executeLoading: false,
|
||||
preDependency: [], // 前置依赖
|
||||
postDependency: [], // 后置依赖
|
||||
};
|
||||
|
||||
// 监听模块树的激活节点变化,记录表格数据的模块 id
|
||||
watch(
|
||||
() => props.activeModule,
|
||||
|
@ -236,6 +316,7 @@
|
|||
|
||||
/** 向孙组件提供属性 */
|
||||
provide('currentEnvConfig', readonly(currentEnvConfig));
|
||||
provide('defaultCaseParams', readonly(defaultCaseParams));
|
||||
|
||||
defineExpose({
|
||||
newTab,
|
||||
|
|
|
@ -191,6 +191,7 @@ export default {
|
|||
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
|
||||
'case.recycle.confirmRecovery': 'Confirm recovery',
|
||||
'case.createCase': 'Create Case',
|
||||
'case.updateCase': 'Update Case',
|
||||
'case.saveContinueText': 'Save & continue',
|
||||
'case.detail.changeHistoryTip': `View and compare historical changes. According to the administrator's setting rules, historical changes will be automatically deleted`,
|
||||
'case.detail.noReminders': 'No longer remind',
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
|
||||
'case.recycle.confirmRecovery': '确认恢复',
|
||||
'case.createCase': '创建用例',
|
||||
'case.updateCase': '更新用例',
|
||||
'case.saveContinueText': '保存并继续创建',
|
||||
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
|
||||
'case.detail.noReminders': '不再提醒',
|
||||
|
@ -191,5 +192,4 @@ export default {
|
|||
'case.detail.operator': '操作人',
|
||||
'case.detail.tableColumnUpdateTime': '更新时间',
|
||||
'case.detail.execute.success': '执行成功',
|
||||
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue