feat(接口管理): 用例编辑&重构用例详情
This commit is contained in:
parent
f5dc90ffd8
commit
88a4e8b1e3
|
@ -55,6 +55,7 @@ import {
|
||||||
SortCaseUrl,
|
SortCaseUrl,
|
||||||
SortDefinitionUrl,
|
SortDefinitionUrl,
|
||||||
SwitchDefinitionScheduleUrl,
|
SwitchDefinitionScheduleUrl,
|
||||||
|
ToggleFollowCaseUrl,
|
||||||
ToggleFollowDefinitionUrl,
|
ToggleFollowDefinitionUrl,
|
||||||
TransferFileCaseUrl,
|
TransferFileCaseUrl,
|
||||||
TransferFileModuleOptionCaseUrl,
|
TransferFileModuleOptionCaseUrl,
|
||||||
|
@ -401,6 +402,11 @@ export function getCaseDetail(id: string) {
|
||||||
return MSR.get<ApiCaseDetail>({ url: GetCaseDetailUrl, params: id });
|
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 GetExecuteHistoryUrl = 'api/case/execute/page'; // 获取用的执行历史
|
||||||
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
|
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
|
||||||
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
|
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
|
||||||
|
export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接口用例回收站
|
* 接口用例回收站
|
||||||
|
|
|
@ -373,7 +373,7 @@ export interface ExecutePluginRequestParams {
|
||||||
// 执行接口调试入参
|
// 执行接口调试入参
|
||||||
export interface ExecuteRequestParams {
|
export interface ExecuteRequestParams {
|
||||||
id?: string;
|
id?: string;
|
||||||
reportId: string;
|
reportId?: string;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
uploadFileIds: string[];
|
uploadFileIds: string[];
|
||||||
linkFileIds: string[];
|
linkFileIds: string[];
|
||||||
|
|
|
@ -343,8 +343,10 @@ export interface AddApiCaseParams extends ExecuteRequestParams {
|
||||||
name: string;
|
name: string;
|
||||||
priority: string;
|
priority: string;
|
||||||
status: string;
|
status: string;
|
||||||
apiDefinitionId: string | number;
|
apiDefinitionId?: string | number;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
deleteFileIds?: string[];
|
||||||
|
unLinkFileIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRunModeRequest {
|
export interface ApiRunModeRequest {
|
||||||
|
|
|
@ -139,7 +139,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="request-params-tab px-[16px]">
|
<div class="px-[16px]">
|
||||||
<MsTab
|
<MsTab
|
||||||
v-model:active-key="requestVModel.activeTab"
|
v-model:active-key="requestVModel.activeTab"
|
||||||
:content-tab-list="contentTabList"
|
:content-tab-list="contentTabList"
|
||||||
|
@ -1528,9 +1528,6 @@
|
||||||
:deep(.arco-tabs-tab) {
|
:deep(.arco-tabs-tab) {
|
||||||
@apply leading-none;
|
@apply leading-none;
|
||||||
}
|
}
|
||||||
.request-params-tab :deep(.arco-tabs-nav-tab) {
|
|
||||||
border-bottom: 1px solid var(--color-text-n8) !important;
|
|
||||||
}
|
|
||||||
.hidden-second {
|
.hidden-second {
|
||||||
:deep(.arco-split-trigger) {
|
:deep(.arco-split-trigger) {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
|
|
|
@ -338,6 +338,7 @@
|
||||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
isCase?: boolean; // case详情
|
||||||
detail: RequestParam;
|
detail: RequestParam;
|
||||||
protocols: ProtocolItem[];
|
protocols: ProtocolItem[];
|
||||||
}>();
|
}>();
|
||||||
|
@ -409,6 +410,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!props.isCase) return;
|
||||||
|
// case编辑后需要刷新数据
|
||||||
|
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.detail.id,
|
() => props.detail.id,
|
||||||
() => {
|
() => {
|
||||||
|
|
|
@ -2,19 +2,6 @@
|
||||||
<div class="h-full w-full overflow-hidden">
|
<div class="h-full w-full overflow-hidden">
|
||||||
<div class="px-[18px] pt-[16px]">
|
<div class="px-[18px] pt-[16px]">
|
||||||
<MsDetailCard
|
<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}`"
|
:title="`【${previewDetail.num}】${previewDetail.name}`"
|
||||||
:description="description"
|
:description="description"
|
||||||
:simple-show-count="4"
|
:simple-show-count="4"
|
||||||
|
@ -78,8 +65,6 @@
|
||||||
|
|
||||||
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
||||||
import MsIcon from '@/components/pure/ms-icon-font/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 detailTab from './detail.vue';
|
||||||
import history from './history.vue';
|
import history from './history.vue';
|
||||||
import quote from './quote.vue';
|
import quote from './quote.vue';
|
||||||
|
@ -100,7 +85,6 @@
|
||||||
detail: RequestParam;
|
detail: RequestParam;
|
||||||
moduleTree: ModuleTreeNode[];
|
moduleTree: ModuleTreeNode[];
|
||||||
protocols: ProtocolItem[];
|
protocols: ProtocolItem[];
|
||||||
isCaseDetail?: boolean; // 在用例详情里显示
|
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits(['updateFollow']);
|
const emit = defineEmits(['updateFollow']);
|
||||||
|
|
||||||
|
@ -113,7 +97,6 @@
|
||||||
() => props.detail.id,
|
() => props.detail.id,
|
||||||
() => {
|
() => {
|
||||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||||
if (props.isCaseDetail) return;
|
|
||||||
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
||||||
previewDetail.value = {
|
previewDetail.value = {
|
||||||
...previewDetail.value,
|
...previewDetail.value,
|
||||||
|
@ -137,8 +120,7 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const description = computed(() => {
|
const description = computed(() => [
|
||||||
const commonDescription = [
|
|
||||||
{
|
{
|
||||||
key: 'type',
|
key: 'type',
|
||||||
locale: 'apiTestManagement.apiType',
|
locale: 'apiTestManagement.apiType',
|
||||||
|
@ -154,11 +136,6 @@
|
||||||
locale: 'common.tag',
|
locale: 'common.tag',
|
||||||
value: previewDetail.value.tags,
|
value: previewDetail.value.tags,
|
||||||
},
|
},
|
||||||
];
|
|
||||||
if (!props.isCaseDetail) {
|
|
||||||
return [
|
|
||||||
...commonDescription,
|
|
||||||
...[
|
|
||||||
{
|
{
|
||||||
key: 'description',
|
key: 'description',
|
||||||
locale: 'common.desc',
|
locale: 'common.desc',
|
||||||
|
@ -185,18 +162,7 @@
|
||||||
locale: 'apiTestManagement.updateTime',
|
locale: 'apiTestManagement.updateTime',
|
||||||
value: dayjs(previewDetail.value.updateTime).format('YYYY-MM-DD HH:mm:ss'),
|
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 followLoading = ref(false);
|
const followLoading = ref(false);
|
||||||
async function toggleFollowReview() {
|
async function toggleFollowReview() {
|
||||||
|
|
|
@ -1,33 +1,126 @@
|
||||||
<template>
|
<template>
|
||||||
<preview
|
<div class="h-full w-full overflow-hidden">
|
||||||
:detail="activeApiTab"
|
<a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load>
|
||||||
:module-tree="props.moduleTree"
|
<template #extra>
|
||||||
:protocols="protocols"
|
<div v-show="!props.isDrawer" class="flex gap-[12px]">
|
||||||
is-case-detail
|
<a-button type="primary">
|
||||||
@update-follow="activeApiTab.follow = !activeApiTab.follow"
|
{{ 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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { getProtocolList } from '@/api/modules/api-test/common';
|
||||||
|
import { toggleFollowCase } from '@/api/modules/api-test/management';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
import { ProtocolItem } from '@/models/apiTest/common';
|
import { ProtocolItem } from '@/models/apiTest/common';
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { RequestMethods } from '@/enums/apiEnum';
|
||||||
|
|
||||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
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 appStore = useAppStore();
|
||||||
|
|
||||||
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
|
const caseDetail = computed<RequestParam>(() => cloneDeep(props.detail)); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||||
required: true,
|
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[]>([]);
|
const protocols = ref<ProtocolItem[]>([]);
|
||||||
async function initProtocolList() {
|
async function initProtocolList() {
|
||||||
|
@ -42,4 +135,81 @@
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
initProtocolList();
|
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>
|
</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>
|
<template>
|
||||||
<div class="overflow-hidden p-[16px_22px]">
|
<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
|
<a-button
|
||||||
v-show="props.isApi"
|
v-show="props.isApi"
|
||||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
|
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
|
||||||
|
@ -37,7 +37,9 @@
|
||||||
@drag-change="handleDragChange"
|
@drag-change="handleDragChange"
|
||||||
>
|
>
|
||||||
<template #num="{ record }">
|
<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>
|
||||||
<template #caseLevel="{ record }">
|
<template #caseLevel="{ record }">
|
||||||
<a-select
|
<a-select
|
||||||
|
@ -236,12 +238,19 @@
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<createAndEditCaseDrawer
|
<createAndEditCaseDrawer
|
||||||
v-if="props.isApi"
|
|
||||||
ref="createAndEditCaseDrawerRef"
|
ref="createAndEditCaseDrawerRef"
|
||||||
:protocol="props.protocol"
|
:protocol="props.protocol"
|
||||||
:api-detail="apiDetail as RequestParam"
|
:api-detail="apiDetail"
|
||||||
@load-case="loadCaseListAndResetSelector()"
|
@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">
|
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('report.trigger.batch.execution') }}
|
{{ t('report.trigger.batch.execution') }}
|
||||||
|
@ -337,8 +346,10 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
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 MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
import useTable from '@/components/pure/ms-table/useTable';
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
@ -346,6 +357,7 @@
|
||||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||||
|
import caseDetailDrawer from './caseDetailDrawer.vue';
|
||||||
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
||||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||||
|
|
||||||
|
@ -356,6 +368,7 @@
|
||||||
deleteCase,
|
deleteCase,
|
||||||
dragSort,
|
dragSort,
|
||||||
executeCase,
|
executeCase,
|
||||||
|
getCaseDetail,
|
||||||
getCasePage,
|
getCasePage,
|
||||||
getEnvList,
|
getEnvList,
|
||||||
getPoolId,
|
getPoolId,
|
||||||
|
@ -376,6 +389,7 @@
|
||||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||||
|
|
||||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
|
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isApi: boolean; // 接口定义详情的case tab下
|
isApi: boolean; // 接口定义详情的case tab下
|
||||||
|
@ -949,15 +963,52 @@
|
||||||
|
|
||||||
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
|
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
|
||||||
function createCase() {
|
function createCase() {
|
||||||
createAndEditCaseDrawerRef.value?.open();
|
createAndEditCaseDrawerRef.value?.open(props.apiDetail?.id as string);
|
||||||
}
|
}
|
||||||
function copyCase(record: ApiCaseDetail) {
|
function copyCase(record: ApiCaseDetail) {
|
||||||
createAndEditCaseDrawerRef.value?.open(record, true);
|
createAndEditCaseDrawerRef.value?.open(record.apiDefinitionId, record, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCaseTab(record: ApiCaseDetail) {
|
function openCaseTab(record: ApiCaseDetail) {
|
||||||
emit('openCaseTab', record);
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<MsDrawer
|
<MsDrawer
|
||||||
v-model:visible="innerVisible"
|
v-model:visible="innerVisible"
|
||||||
:title="t('case.createCase')"
|
:title="isEdit ? t('case.updateCase') : t('case.createCase')"
|
||||||
:width="894"
|
:width="894"
|
||||||
no-content-padding
|
no-content-padding
|
||||||
:ok-text="t('common.create')"
|
:ok-text="isEdit ? 'common.update' : 'common.create'"
|
||||||
:ok-loading="drawerLoading"
|
:ok-loading="drawerLoading"
|
||||||
:save-continue-text="t('case.saveContinueText')"
|
:save-continue-text="t('case.saveContinueText')"
|
||||||
:show-continue="true"
|
:show-continue="!isEdit && !!props.apiDetail"
|
||||||
@confirm="handleDrawerConfirm"
|
@confirm="handleDrawerConfirm(false)"
|
||||||
@continue="handleDrawerConfirm(true)"
|
@continue="handleDrawerConfirm(true)"
|
||||||
@cancel="handleSaveCaseCancel"
|
@cancel="handleSaveCaseCancel"
|
||||||
>
|
>
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<div class="flex h-full flex-col overflow-hidden">
|
<div class="flex h-full flex-col overflow-hidden">
|
||||||
<div class="px-[16px] pt-[16px]">
|
<div class="px-[16px] pt-[16px]">
|
||||||
<MsDetailCard
|
<MsDetailCard
|
||||||
:title="`【${apiDataDetail.num}】${apiDataDetail.name}`"
|
:title="`【${apiDetailInfo.num}】${apiDetailInfo.name}`"
|
||||||
:description="description"
|
:description="description"
|
||||||
class="!flex-row justify-between"
|
class="!flex-row justify-between"
|
||||||
>
|
>
|
||||||
|
@ -26,11 +26,11 @@
|
||||||
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
|
||||||
</template>
|
</template>
|
||||||
</MsDetailCard>
|
</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') }]">
|
<a-form-item field="name" label="" :rules="[{ required: true, message: t('case.caseNameRequired') }]">
|
||||||
<div class="flex w-full items-center gap-[8px]">
|
<div class="flex w-full items-center gap-[8px]">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:model-value="caseModalForm.name"
|
v-model:model-value="detailForm.name"
|
||||||
:placeholder="t('case.caseNamePlaceholder')"
|
:placeholder="t('case.caseNamePlaceholder')"
|
||||||
allow-clear
|
allow-clear
|
||||||
:max-length="255"
|
:max-length="255"
|
||||||
|
@ -43,9 +43,9 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<div class="flex gap-[16px]">
|
<div class="flex gap-[16px]">
|
||||||
<a-form-item field="priority" :label="t('case.caseLevel')">
|
<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>
|
<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>
|
</template>
|
||||||
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
||||||
<caseLevel :case-level="item.label as CaseLevel" />
|
<caseLevel :case-level="item.label as CaseLevel" />
|
||||||
|
@ -53,9 +53,9 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item field="status" :label="t('apiTestManagement.apiStatus')">
|
<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>
|
<template #label>
|
||||||
<apiStatus :status="caseModalForm.status" />
|
<apiStatus :status="detailForm.status" />
|
||||||
</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" />
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item field="tags" :label="t('common.tag')">
|
<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>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
@ -72,11 +72,11 @@
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<requestComposition
|
<requestComposition
|
||||||
ref="requestCompositionRef"
|
ref="requestCompositionRef"
|
||||||
v-model:request="apiDataDetail"
|
v-model:request="detailForm"
|
||||||
:is-case="true"
|
:is-case="true"
|
||||||
hide-response-layout-switch
|
hide-response-layout-switch
|
||||||
:upload-temp-file-api="uploadTempFileCase"
|
: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-module-options-api="getTransferOptionsCase"
|
||||||
:file-save-as-api="transferFileCase"
|
:file-save-as-api="transferFileCase"
|
||||||
:current-env-config="currentEnvConfig"
|
:current-env-config="currentEnvConfig"
|
||||||
|
@ -103,41 +103,55 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addCase,
|
addCase,
|
||||||
|
getDefinitionDetail,
|
||||||
getTransferOptionsCase,
|
getTransferOptionsCase,
|
||||||
transferFileCase,
|
transferFileCase,
|
||||||
|
updateCase,
|
||||||
uploadTempFileCase,
|
uploadTempFileCase,
|
||||||
} 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 { ApiCaseDetail } from '@/models/apiTest/management';
|
import { AddApiCaseParams, ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||||
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
|
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
|
||||||
|
|
||||||
import { casePriorityOptions } from '@/views/api-test/components/config';
|
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||||
|
|
||||||
const props = defineProps<{
|
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 { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const innerVisible = ref(false);
|
const innerVisible = ref(false);
|
||||||
|
|
||||||
const drawerLoading = 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(() => [
|
const description = computed(() => [
|
||||||
{
|
{
|
||||||
key: 'type',
|
key: 'type',
|
||||||
locale: 'apiTestManagement.apiType',
|
locale: 'apiTestManagement.apiType',
|
||||||
value: apiDataDetail.value.method,
|
value: apiDetailInfo.value.method,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'path',
|
key: 'path',
|
||||||
locale: 'apiTestManagement.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 currentEnvConfig = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
|
||||||
|
|
||||||
const formRef = ref<FormInstance>();
|
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 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) {
|
function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
|
||||||
innerVisible.value = true;
|
apiDefinitionId.value = apiId;
|
||||||
if (isCopy) {
|
// 从api下的用例里打开抽屉有api信息,从case下直接复制没有api信息
|
||||||
caseModalForm.value.name = record?.name;
|
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() {
|
function handleSaveCaseCancel() {
|
||||||
|
drawerLoading.value = false;
|
||||||
|
isEdit.value = false;
|
||||||
innerVisible.value = false;
|
innerVisible.value = false;
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
caseModalForm.value = { ...initForm };
|
detailForm.value = cloneDeep(defaultDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrawerConfirm(isContinue: boolean) {
|
function handleDrawerConfirm(isContinue: boolean) {
|
||||||
formRef.value?.validate(async (errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
drawerLoading.value = true;
|
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 {
|
try {
|
||||||
await addCase(params);
|
if (isEdit.value) {
|
||||||
|
await updateCase(params);
|
||||||
Message.success(t('common.updateSuccess'));
|
Message.success(t('common.updateSuccess'));
|
||||||
|
} else {
|
||||||
|
await addCase(params);
|
||||||
|
Message.success(t('common.createSuccess'));
|
||||||
|
}
|
||||||
|
emit('loadCase', id as string);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
if (!isContinue) {
|
if (!isContinue) {
|
||||||
emit('loadCase');
|
|
||||||
handleSaveCaseCancel();
|
handleSaveCaseCancel();
|
||||||
}
|
}
|
||||||
caseModalForm.value = { ...initForm };
|
detailForm.value = cloneDeep(defaultDetail);
|
||||||
drawerLoading.value = false;
|
drawerLoading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,13 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,9 +30,7 @@
|
||||||
|
|
||||||
import { ApiCaseDetail } from '@/models/apiTest/management';
|
import { ApiCaseDetail } from '@/models/apiTest/management';
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
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 type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
|
@ -47,106 +51,37 @@
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initDefaultId = `case-${Date.now()}`;
|
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
|
||||||
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 loading = ref(false);
|
const loading = ref(false);
|
||||||
async function openCaseTab(apiInfo: ApiCaseDetail) {
|
async function openOrUpdateCaseTab(isOpen: boolean, id: string) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
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 集合收集,更新时以判断文件是否删除以及是否新上传的文件;
|
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件;
|
||||||
// if (res.protocol === 'HTTP') { // TODO: 后端没protocol字段,问一下
|
// if (res.protocol === 'HTTP') { // TODO: 后端没protocol字段,问一下
|
||||||
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||||
// }
|
// }
|
||||||
addTab({
|
const tabItemInfo = {
|
||||||
|
...cloneDeep(defaultCaseParams as RequestParam),
|
||||||
|
...({
|
||||||
...res.request,
|
...res.request,
|
||||||
...res,
|
...res,
|
||||||
response: cloneDeep(defaultResponse),
|
|
||||||
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: 后端没response字段,问一下
|
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: 后端没response字段,问一下
|
||||||
url: res.path,
|
url: res.path,
|
||||||
...parseRequestBodyResult,
|
...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(() => {
|
nextTick(() => {
|
||||||
loading.value = false; // 等待内容渲染出来再隐藏loading
|
loading.value = false; // 等待内容渲染出来再隐藏loading
|
||||||
});
|
});
|
||||||
|
@ -156,4 +91,16 @@
|
||||||
loading.value = false;
|
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>
|
</script>
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectOptionData } from '@arco-design/web-vue';
|
import { SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||||
import api from './api/index.vue';
|
import api from './api/index.vue';
|
||||||
|
@ -73,8 +74,17 @@
|
||||||
|
|
||||||
import { ModuleTreeNode } from '@/models/common';
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||||
|
import {
|
||||||
|
RequestAuthType,
|
||||||
|
RequestComposition,
|
||||||
|
RequestDefinitionStatus,
|
||||||
|
RequestMethods,
|
||||||
|
ResponseComposition,
|
||||||
|
} from '@/enums/apiEnum';
|
||||||
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
|
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
|
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
offspringIds: string[];
|
offspringIds: string[];
|
||||||
|
@ -113,6 +123,76 @@
|
||||||
]);
|
]);
|
||||||
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
|
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
|
// 监听模块树的激活节点变化,记录表格数据的模块 id
|
||||||
watch(
|
watch(
|
||||||
() => props.activeModule,
|
() => props.activeModule,
|
||||||
|
@ -236,6 +316,7 @@
|
||||||
|
|
||||||
/** 向孙组件提供属性 */
|
/** 向孙组件提供属性 */
|
||||||
provide('currentEnvConfig', readonly(currentEnvConfig));
|
provide('currentEnvConfig', readonly(currentEnvConfig));
|
||||||
|
provide('defaultCaseParams', readonly(defaultCaseParams));
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
newTab,
|
newTab,
|
||||||
|
|
|
@ -191,6 +191,7 @@ export default {
|
||||||
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
|
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
|
||||||
'case.recycle.confirmRecovery': 'Confirm recovery',
|
'case.recycle.confirmRecovery': 'Confirm recovery',
|
||||||
'case.createCase': 'Create Case',
|
'case.createCase': 'Create Case',
|
||||||
|
'case.updateCase': 'Update Case',
|
||||||
'case.saveContinueText': 'Save & continue',
|
'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.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',
|
'case.detail.noReminders': 'No longer remind',
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
|
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
|
||||||
'case.recycle.confirmRecovery': '确认恢复',
|
'case.recycle.confirmRecovery': '确认恢复',
|
||||||
'case.createCase': '创建用例',
|
'case.createCase': '创建用例',
|
||||||
|
'case.updateCase': '更新用例',
|
||||||
'case.saveContinueText': '保存并继续创建',
|
'case.saveContinueText': '保存并继续创建',
|
||||||
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
|
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
|
||||||
'case.detail.noReminders': '不再提醒',
|
'case.detail.noReminders': '不再提醒',
|
||||||
|
@ -191,5 +192,4 @@ export default {
|
||||||
'case.detail.operator': '操作人',
|
'case.detail.operator': '操作人',
|
||||||
'case.detail.tableColumnUpdateTime': '更新时间',
|
'case.detail.tableColumnUpdateTime': '更新时间',
|
||||||
'case.detail.execute.success': '执行成功',
|
'case.detail.execute.success': '执行成功',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue