feat(接口管理): 创建用例&用例详情
This commit is contained in:
parent
7e6edf8352
commit
1f1b43f108
|
@ -16,6 +16,7 @@ import {
|
|||
BatchUpdateDefinitionUrl,
|
||||
CasePageUrl,
|
||||
CheckDefinitionScheduleUrl,
|
||||
DebugCaseUrl,
|
||||
DebugDefinitionUrl,
|
||||
DefinitionMockPageUrl,
|
||||
DefinitionPageUrl,
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
DeleteRecycleApiUrl,
|
||||
DeleteRecycleCaseUrl,
|
||||
ExecuteCaseUrl,
|
||||
GetCaseDetailUrl,
|
||||
GetChangeHistoryUrl,
|
||||
GetDefinitionDetailUrl,
|
||||
GetDefinitionScheduleUrl,
|
||||
|
@ -52,14 +54,18 @@ import {
|
|||
SortDefinitionUrl,
|
||||
SwitchDefinitionScheduleUrl,
|
||||
ToggleFollowDefinitionUrl,
|
||||
TransferFileCaseUrl,
|
||||
TransferFileModuleOptionCaseUrl,
|
||||
TransferFileModuleOptionUrl,
|
||||
TransferFileUrl,
|
||||
UpdateCasePriorityUrl,
|
||||
UpdateCaseStatusUrl,
|
||||
UpdateCaseUrl,
|
||||
UpdateDefinitionScheduleUrl,
|
||||
UpdateDefinitionUrl,
|
||||
UpdateMockStatusUrl,
|
||||
UpdateModuleUrl,
|
||||
UploadTempFileCaseUrl,
|
||||
UploadTempFileUrl,
|
||||
} from '@/api/requrls/api-test/management';
|
||||
|
||||
|
@ -68,8 +74,11 @@ import {
|
|||
AddApiCaseParams,
|
||||
ApiCaseBatchEditParams,
|
||||
ApiCaseBatchExecuteParams,
|
||||
ApiCaseBatchParams, ApiCaseChangeHistoryParams, ApiCaseDependencyParams,
|
||||
ApiCaseDetail, ApiCaseExecuteHistoryParams,
|
||||
ApiCaseBatchParams,
|
||||
ApiCaseChangeHistoryParams,
|
||||
ApiCaseDependencyParams,
|
||||
ApiCaseDetail,
|
||||
ApiCaseExecuteHistoryParams,
|
||||
ApiCasePageParams,
|
||||
ApiDefinitionBatchDeleteParams,
|
||||
ApiDefinitionBatchMoveParams,
|
||||
|
@ -102,7 +111,8 @@ import {
|
|||
CommonList,
|
||||
DragSortParams,
|
||||
ModuleTreeNode,
|
||||
MoveModules, TableQueryParams,
|
||||
MoveModules,
|
||||
TableQueryParams,
|
||||
TransferFileParams,
|
||||
} from '@/models/common';
|
||||
|
||||
|
@ -358,6 +368,36 @@ export function dragSort(data: DragSortParams) {
|
|||
return MSR.post({ url: SortCaseUrl, data });
|
||||
}
|
||||
|
||||
// 更新接口用例
|
||||
export function updateCase(data: AddApiCaseParams) {
|
||||
return MSR.post({ url: UpdateCaseUrl, data });
|
||||
}
|
||||
|
||||
// 接口用例调试
|
||||
export function debugCase(data: ExecuteRequestParams) {
|
||||
return MSR.post({ url: DebugCaseUrl, data });
|
||||
}
|
||||
|
||||
// 文件转存
|
||||
export function transferFileCase(data: TransferFileParams) {
|
||||
return MSR.post({ url: TransferFileCaseUrl, data });
|
||||
}
|
||||
|
||||
// 文件转存目录
|
||||
export function getTransferOptionsCase(projectId: string) {
|
||||
return MSR.get<ModuleTreeNode[]>({ url: TransferFileModuleOptionCaseUrl, params: projectId });
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
export function uploadTempFileCase(file: File) {
|
||||
return MSR.uploadFile({ url: UploadTempFileCaseUrl }, { fileList: [file] }, 'file');
|
||||
}
|
||||
|
||||
// 获取接口用例详情
|
||||
export function getCaseDetail(id: string) {
|
||||
return MSR.get<ApiCaseDetail>({ url: GetCaseDetailUrl, params: id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口用例回收站
|
||||
*/
|
||||
|
@ -419,4 +459,4 @@ export function getApiCaseChangeHistory(data: ApiCaseChangeHistoryParams) {
|
|||
// 获取接口用例-依赖关系
|
||||
export function getApiCaseDependency(data: ApiCaseDependencyParams) {
|
||||
return MSR.post({ url: GetDependencyUrl, data });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,12 +55,18 @@ export const GetTrashModuleCountUrl = '/api/definition/module/trash/count'; //
|
|||
|
||||
// --------------------用例
|
||||
export const CasePageUrl = '/api/case/page'; // 接口用例列表
|
||||
export const UpdateCaseUrl = '/api/case/update'; // 接口用例更新
|
||||
export const UpdateCaseStatusUrl = '/api/case/update-status'; // 接口用例更新状态
|
||||
export const UpdateCasePriorityUrl = '/api/case/update-priority'; // 接口用例更新等级
|
||||
export const DeleteCaseUrl = '/api/case/delete-to-gc'; // 删除接口用例
|
||||
export const BatchDeleteCaseUrl = '/api/case/batch/delete-to-gc'; // 批量删除接口用例
|
||||
export const BatchEditCaseUrl = '/api/case/batch/edit'; // 批量编辑接口用例
|
||||
export const SortCaseUrl = '/api/case/edit/pos'; // 接口用例拖拽
|
||||
export const DebugCaseUrl = '/api/case/debug'; // 接口用例调试
|
||||
export const TransferFileCaseUrl = '/api/case/transfer'; // 文件转存
|
||||
export const TransferFileModuleOptionCaseUrl = '/api/case/transfer/options'; // 文件转存目录
|
||||
export const UploadTempFileCaseUrl = '/api/case/upload/temp/file'; // 临时文件上传
|
||||
export const GetCaseDetailUrl = '/api/case/get-detail'; // 获取接口用例详情
|
||||
export const GetEnvListUrl = '/api/test/env-list'; // 接口测试-环境列表
|
||||
export const BatchExecuteCaseUrl = '/api/case/batch/run'; // 批量执行接口用例
|
||||
export const ExecuteCaseUrl = '/api/case/run/'; // 单独执行接口用例
|
||||
|
@ -68,8 +74,6 @@ export const GetExecuteHistoryUrl = 'api/case/execute/page'; // 获取用的执
|
|||
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
|
||||
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 接口用例回收站
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="ms-detail-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="ms-detail-card-title flex items-center justify-between">
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<a-tooltip :content="t(props.title)">
|
||||
<div class="one-line-text flex-1 font-medium text-[var(--color-text-1)]">
|
||||
|
|
|
@ -294,8 +294,8 @@ export interface ApiCasePageParams extends TableQueryParams {
|
|||
moduleIds?: string[];
|
||||
apiDefinitionId?: string;
|
||||
}
|
||||
// 用例列表
|
||||
export interface ApiCaseDetail {
|
||||
// 用例列表和用例详情
|
||||
export interface ApiCaseDetail extends ExecuteRequestParams {
|
||||
id: string;
|
||||
name: string;
|
||||
priority: string;
|
||||
|
@ -327,7 +327,7 @@ export interface ApiCaseDetail {
|
|||
// 批量操作参数
|
||||
export interface ApiCaseBatchParams extends BatchApiParams {
|
||||
protocol: string;
|
||||
apiDefinitionId?: string[];
|
||||
apiDefinitionId?: string;
|
||||
versionId?: string;
|
||||
}
|
||||
// 用例批量编辑参数
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</template>
|
||||
</a-empty>
|
||||
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
|
||||
<div class="px-[18px] pt-[8px]">
|
||||
<div v-if="!props.isCase" class="px-[18px] pt-[8px]">
|
||||
<div class="flex flex-wrap items-center justify-between gap-[12px]">
|
||||
<div class="flex flex-1 items-center gap-[16px]">
|
||||
<a-select
|
||||
|
@ -68,6 +68,7 @@
|
|||
<template
|
||||
v-if="
|
||||
(!props.isDefinition || (props.isDefinition && requestVModel.mode === 'debug')) &&
|
||||
props.permissionMap &&
|
||||
hasAnyPermission([props.permissionMap.execute])
|
||||
"
|
||||
>
|
||||
|
@ -95,8 +96,8 @@
|
|||
v-if="
|
||||
props.isDefinition &&
|
||||
(requestVModel.isNew
|
||||
? hasAnyPermission([props.permissionMap.create])
|
||||
: hasAnyPermission([props.permissionMap.update]))
|
||||
? props.permissionMap && hasAnyPermission([props.permissionMap.create])
|
||||
: props.permissionMap && hasAnyPermission([props.permissionMap.update]))
|
||||
"
|
||||
>
|
||||
<!-- 接口定义-调试模式,可保存或保存为新用例 -->
|
||||
|
@ -122,8 +123,8 @@
|
|||
<a-button
|
||||
v-else-if="
|
||||
requestVModel.isNew
|
||||
? hasAnyPermission([props.permissionMap.create])
|
||||
: hasAnyPermission([props.permissionMap.update])
|
||||
? props.permissionMap && hasAnyPermission([props.permissionMap.create])
|
||||
: props.permissionMap && hasAnyPermission([props.permissionMap.update])
|
||||
"
|
||||
type="secondary"
|
||||
:disabled="isHttpProtocol && !requestVModel.url"
|
||||
|
@ -138,7 +139,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-[16px]">
|
||||
<div class="request-params-tab px-[16px]">
|
||||
<MsTab
|
||||
v-model:active-key="requestVModel.activeTab"
|
||||
:content-tab-list="contentTabList"
|
||||
|
@ -146,15 +147,15 @@
|
|||
class="no-content relative mt-[8px] border-b"
|
||||
/>
|
||||
</div>
|
||||
<div ref="splitContainerRef" class="h-[calc(100%-87px)]">
|
||||
<div ref="splitContainerRef" class="request-and-response h-[calc(100%-87px)]">
|
||||
<MsSplitBox
|
||||
ref="horizontalSplitBoxRef"
|
||||
:size="props.isDefinition ? 0.7 : 1"
|
||||
:max="props.isDefinition ? 0.9 : 1"
|
||||
:min="props.isDefinition ? 0.7 : 1"
|
||||
:disabled="!props.isDefinition"
|
||||
:class="!props.isDefinition ? 'hidden-second' : ''"
|
||||
:first-container-class="!props.isDefinition ? 'border-r-0' : ''"
|
||||
:size="!props.isCase && props.isDefinition ? 0.7 : 1"
|
||||
:max="props.isDefinition && !props.isCase ? 0.9 : 1"
|
||||
:min="props.isDefinition && !props.isCase ? 0.7 : 1"
|
||||
:disabled="props.isCase && !props.isDefinition"
|
||||
:class="props.isCase && !props.isDefinition ? 'hidden-second' : ''"
|
||||
:first-container-class="props.isCase && !props.isDefinition ? 'border-r-0' : ''"
|
||||
direction="horizontal"
|
||||
expand-direction="right"
|
||||
>
|
||||
|
@ -286,7 +287,7 @@
|
|||
</template>
|
||||
</MsSplitBox>
|
||||
</template>
|
||||
<template v-if="props.isDefinition" #second>
|
||||
<template v-if="!props.isCase && props.isDefinition" #second>
|
||||
<div class="p-[16px]">
|
||||
<!-- TODO:第一版没有模板 -->
|
||||
<!-- <MsFormCreate v-model:api="fApi" :rule="currentApiTemplateRules" :option="options" /> -->
|
||||
|
@ -308,7 +309,7 @@
|
|||
<a-form-item :label="t('apiTestManagement.belongModule')" class="mb-[16px]">
|
||||
<a-tree-select
|
||||
v-model:modelValue="requestVModel.moduleId"
|
||||
:data="selectTree"
|
||||
:data="selectTree as ModuleTreeNode[]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
|
@ -395,6 +396,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<a-modal
|
||||
v-if="!isCase"
|
||||
v-model:visible="saveModalVisible"
|
||||
:title="t('common.save')"
|
||||
:ok-loading="saveLoading"
|
||||
|
@ -433,7 +435,7 @@
|
|||
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
|
||||
<a-tree-select
|
||||
v-model:modelValue="saveModalForm.moduleId"
|
||||
:data="selectTree"
|
||||
:data="selectTree as ModuleTreeNode[]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
|
@ -447,6 +449,7 @@
|
|||
</a-form>
|
||||
</a-modal>
|
||||
<a-modal
|
||||
v-if="!isCase"
|
||||
v-model:visible="saveCaseModalVisible"
|
||||
:title="t('common.save')"
|
||||
:ok-loading="saveCaseLoading"
|
||||
|
@ -563,7 +566,7 @@
|
|||
isNew: boolean;
|
||||
protocol: string;
|
||||
activeTab: RequestComposition;
|
||||
mode?: 'definition' | 'debug';
|
||||
mode?: 'definition' | 'debug' | 'case';
|
||||
executeLoading: boolean; // 执行中loading
|
||||
isCopy?: boolean; // 是否是复制
|
||||
isExecute?: boolean; // 是否是执行
|
||||
|
@ -576,21 +579,22 @@
|
|||
|
||||
const props = defineProps<{
|
||||
request: RequestParam; // 请求参数集合
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
moduleTree?: ModuleTreeNode[]; // 模块树
|
||||
isCase?: boolean; // 是否是用例引用的组件
|
||||
detailLoading?: boolean; // 详情加载状态
|
||||
isDefinition?: boolean; // 是否是接口定义模式
|
||||
hideResponseLayoutSwitch?: boolean; // 是否隐藏响应体的布局切换
|
||||
otherParams?: Record<string, any>; // 保存请求时的其他参数
|
||||
currentEnvConfig?: EnvConfig;
|
||||
executeApi: (params: ExecuteRequestParams) => Promise<any>; // 执行接口
|
||||
localExecuteApi: (url: string, params: ExecuteRequestParams) => Promise<any>; // 本地执行接口
|
||||
createApi: (...args) => Promise<any>; // 创建接口
|
||||
updateApi: (...args) => Promise<any>; // 更新接口
|
||||
executeApi?: (params: ExecuteRequestParams) => Promise<any>; // 执行接口
|
||||
localExecuteApi?: (url: string, params: ExecuteRequestParams) => Promise<any>; // 本地执行接口
|
||||
createApi?: (...args) => Promise<any>; // 创建接口
|
||||
updateApi?: (...args) => Promise<any>; // 更新接口
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||
fileModuleOptionsApi?: (projectId: string) => Promise<ModuleTreeNode[]>; // 文件转存目录下拉框接口
|
||||
permissionMap: {
|
||||
permissionMap?: {
|
||||
execute: string;
|
||||
create: string;
|
||||
update: string;
|
||||
|
@ -1145,10 +1149,11 @@
|
|||
async function execute(executeType?: 'localExec' | 'serverExec') {
|
||||
if (isHttpProtocol.value) {
|
||||
try {
|
||||
if (!props.executeApi) return;
|
||||
requestVModel.value.executeLoading = true;
|
||||
requestVModel.value.response = cloneDeep(defaultResponse);
|
||||
const res = await props.executeApi(makeRequestParams(executeType));
|
||||
if (executeType === 'localExec') {
|
||||
if (executeType === 'localExec' && props.localExecuteApi) {
|
||||
await props.localExecuteApi(localExecuteUrl.value, res);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -1161,10 +1166,11 @@
|
|||
fApi.value?.validate(async (valid) => {
|
||||
if (valid === true) {
|
||||
try {
|
||||
if (!props.executeApi) return;
|
||||
requestVModel.value.executeLoading = true;
|
||||
requestVModel.value.response = cloneDeep(defaultResponse);
|
||||
const res = await props.executeApi(makeRequestParams(executeType));
|
||||
if (executeType === 'localExec') {
|
||||
if (executeType === 'localExec' && props.localExecuteApi) {
|
||||
await props.localExecuteApi(localExecuteUrl.value, res);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -1217,6 +1223,7 @@
|
|||
|
||||
async function updateRequest() {
|
||||
try {
|
||||
if (!props.updateApi) return;
|
||||
saveLoading.value = true;
|
||||
await props.updateApi({
|
||||
...makeRequestParams(),
|
||||
|
@ -1238,6 +1245,7 @@
|
|||
*/
|
||||
async function realSave(fullParams?: Record<string, any>, silence?: boolean) {
|
||||
try {
|
||||
if (!props.createApi) return;
|
||||
if (!silence) {
|
||||
saveLoading.value = true;
|
||||
}
|
||||
|
@ -1485,6 +1493,10 @@
|
|||
removeCatchSaveShortcut(handleSaveShortcut);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
makeRequestParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -1513,6 +1525,9 @@
|
|||
: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;
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-select
|
||||
v-model:model-value="currentEnv"
|
||||
:options="envOptions"
|
||||
class="!w-[200px] pl-0 pr-[8px]"
|
||||
:loading="envLoading"
|
||||
allow-search
|
||||
@change="initEnvironment"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="flex cursor-pointer p-[8px]" @click.stop="goEnv">
|
||||
<icon-location class="text-[var(--color-text-4)]" />
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
import { getEnvironment, getEnvList } from '@/api/modules/api-test/common';
|
||||
import router from '@/router';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const currentEnv = ref('');
|
||||
|
||||
const currentEnvConfig = ref<EnvConfig>();
|
||||
const envLoading = ref(false);
|
||||
const envOptions = ref<SelectOptionData[]>([]);
|
||||
|
||||
async function initEnvironment() {
|
||||
try {
|
||||
currentEnvConfig.value = await getEnvironment(currentEnv.value);
|
||||
currentEnvConfig.value.id = currentEnv.value;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function initEnvList() {
|
||||
try {
|
||||
envLoading.value = true;
|
||||
const res = await getEnvList(appStore.currentProjectId);
|
||||
envOptions.value = res.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
currentEnv.value = res[0]?.id || '';
|
||||
initEnvironment();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
envLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goEnv() {
|
||||
router.push({
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT,
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initEnvList();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
currentEnvConfig,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ms-input-group--prepend();
|
||||
:deep(.arco-select-view-prefix) {
|
||||
margin-right: 8px;
|
||||
padding-right: 0;
|
||||
border-right: 1px solid var(--color-text-input-border);
|
||||
}
|
||||
</style>
|
|
@ -51,6 +51,12 @@
|
|||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane">
|
||||
<caseTable
|
||||
:is-api="true"
|
||||
:active-module="props.activeModule"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="activeApiTab"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane v-if="!activeApiTab.isNew" key="mock" title="MOCK" class="ms-api-tab-pane"> </a-tab-pane> -->
|
||||
</a-tabs>
|
||||
|
@ -63,6 +69,7 @@
|
|||
|
||||
// import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import caseTable from '../case/caseTable.vue';
|
||||
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
|
||||
import apiTable from './apiTable.vue';
|
||||
|
||||
|
|
|
@ -2,6 +2,19 @@
|
|||
<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"
|
||||
|
@ -65,6 +78,8 @@
|
|||
|
||||
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';
|
||||
|
@ -85,6 +100,7 @@
|
|||
detail: RequestParam;
|
||||
moduleTree: ModuleTreeNode[];
|
||||
protocols: ProtocolItem[];
|
||||
isCaseDetail?: boolean; // 在用例详情里显示
|
||||
}>();
|
||||
const emit = defineEmits(['updateFollow']);
|
||||
|
||||
|
@ -95,6 +111,7 @@
|
|||
|
||||
watchEffect(() => {
|
||||
previewDetail.value = cloneDeep(props.detail); // props.detail是嵌套的引用类型,防止不必要的修改来源影响props.detail的数据
|
||||
if (props.isCaseDetail) return;
|
||||
const tableParam = getValidRequestTableParams(previewDetail.value); // 在编辑props.detail时,参数表格会多出一行默认数据,需要去除
|
||||
previewDetail.value = {
|
||||
...previewDetail.value,
|
||||
|
@ -114,49 +131,66 @@
|
|||
};
|
||||
});
|
||||
|
||||
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 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 followLoading = ref(false);
|
||||
async function toggleFollowReview() {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<preview
|
||||
:detail="activeApiTab"
|
||||
:module-tree="props.moduleTree"
|
||||
:protocols="protocols"
|
||||
is-case-detail
|
||||
@update-follow="activeApiTab.follow = !activeApiTab.follow"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getProtocolList } from '@/api/modules/api-test/common';
|
||||
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';
|
||||
|
||||
const preview = defineAsyncComponent(() => import('../api/preview/index.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const protocols = ref<ProtocolItem[]>([]);
|
||||
async function initProtocolList() {
|
||||
try {
|
||||
protocols.value = await getProtocolList(appStore.currentOrgId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initProtocolList();
|
||||
});
|
||||
</script>
|
|
@ -1,19 +1,29 @@
|
|||
<template>
|
||||
<div class="p-[16px_22px]">
|
||||
<div class="mb-[16px] flex items-center gap-[8px]">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiTestManagement.searchPlaceholder')"
|
||||
allow-clear
|
||||
class="mr-[8px] w-[240px]"
|
||||
@search="loadCaseList"
|
||||
@press-enter="loadCaseList"
|
||||
/>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]" @click="loadCaseList">
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
<div class="overflow-hidden p-[16px_22px]">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<a-button
|
||||
v-show="props.isApi"
|
||||
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
|
||||
type="primary"
|
||||
@click="createCase"
|
||||
>
|
||||
{{ t('caseManagement.featureCase.creatingCase') }}
|
||||
</a-button>
|
||||
<div class="flex gap-[8px]">
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiTestManagement.searchPlaceholder')"
|
||||
allow-clear
|
||||
class="mr-[8px] w-[240px]"
|
||||
@search="loadCaseList"
|
||||
@press-enter="loadCaseList"
|
||||
/>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary !p-[8px]" @click="loadCaseList">
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<ms-base-table
|
||||
v-bind="propsRes"
|
||||
|
@ -27,13 +37,14 @@
|
|||
@drag-change="handleDragChange"
|
||||
>
|
||||
<template #num="{ record }">
|
||||
<MsButton type="text">{{ record.num }}</MsButton>
|
||||
<MsButton type="text" @click="openCaseTab(record)">{{ record.num }}</MsButton>
|
||||
</template>
|
||||
<template #caseLevel="{ record }">
|
||||
<a-select
|
||||
v-model:model-value="record.priority"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
size="mini"
|
||||
@change="() => handleCaseLevelChange(record)"
|
||||
>
|
||||
<template #label>
|
||||
|
@ -68,13 +79,14 @@
|
|||
v-model:model-value="record.status"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
size="mini"
|
||||
@change="() => handleStatusChange(record)"
|
||||
>
|
||||
<template #label>
|
||||
<apiStatus :status="record.status" />
|
||||
<apiStatus :status="record.status" size="small" />
|
||||
</template>
|
||||
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
|
||||
<apiStatus :status="item" />
|
||||
<apiStatus :status="item" size="small" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
@ -144,7 +156,7 @@
|
|||
{{ t('apiTestManagement.execute') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
<MsButton type="text" class="!mr-0">
|
||||
<MsButton type="text" class="!mr-0" @click="copyCase(record)">
|
||||
{{ t('common.copy') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
|
@ -223,6 +235,13 @@
|
|||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
<createAndEditCaseDrawer
|
||||
v-if="props.isApi"
|
||||
ref="createAndEditCaseDrawerRef"
|
||||
:protocol="props.protocol"
|
||||
:api-detail="apiDetail as RequestParam"
|
||||
@load-case="loadCaseListAndResetSelector()"
|
||||
/>
|
||||
<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') }}
|
||||
|
@ -327,6 +346,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 createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
|
||||
import {
|
||||
|
@ -354,9 +374,17 @@
|
|||
import { RequestDefinitionStatus } from '@/enums/apiEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isApi: boolean; // 接口定义详情的case tab下
|
||||
activeModule: string;
|
||||
protocol: string; // 查看的协议类型
|
||||
apiDetail?: RequestParam;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'openCaseTab', record: ApiCaseDetail): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
@ -378,7 +406,9 @@
|
|||
sorter: true,
|
||||
},
|
||||
fixed: 'left',
|
||||
width: 100,
|
||||
width: 130,
|
||||
ellipsis: true,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'case.caseName',
|
||||
|
@ -500,6 +530,7 @@
|
|||
selectable: true,
|
||||
showSelectAll: true,
|
||||
draggable: { type: 'handle', width: 32 },
|
||||
heightUsed: 308,
|
||||
});
|
||||
const batchActions = {
|
||||
baseAction: [
|
||||
|
@ -551,6 +582,7 @@
|
|||
});
|
||||
function loadCaseList() {
|
||||
const params = {
|
||||
apiDefinitionId: props.apiDetail?.id,
|
||||
keyword: keyword.value,
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: moduleIds.value,
|
||||
|
@ -673,6 +705,7 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
protocol: props.protocol,
|
||||
moduleIds: moduleIds.value,
|
||||
apiDefinitionId: props.apiDetail?.id as string,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -909,6 +942,18 @@
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
|
||||
function createCase() {
|
||||
createAndEditCaseDrawerRef.value?.open();
|
||||
}
|
||||
function copyCase(record: ApiCaseDetail) {
|
||||
createAndEditCaseDrawerRef.value?.open(record, true);
|
||||
}
|
||||
|
||||
function openCaseTab(record: ApiCaseDetail) {
|
||||
emit('openCaseTab', record);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="innerVisible"
|
||||
:title="t('case.createCase')"
|
||||
:width="894"
|
||||
no-content-padding
|
||||
:ok-text="t('common.create')"
|
||||
:ok-loading="drawerLoading"
|
||||
:save-continue-text="t('case.saveContinueText')"
|
||||
:show-continue="true"
|
||||
@confirm="handleDrawerConfirm"
|
||||
@continue="handleDrawerConfirm(true)"
|
||||
@cancel="handleSaveCaseCancel"
|
||||
>
|
||||
<template #headerLeft>
|
||||
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
|
||||
</template>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<div class="px-[16px] pt-[16px]">
|
||||
<MsDetailCard
|
||||
:title="`【${apiDataDetail.num}】${apiDataDetail.name}`"
|
||||
:description="description"
|
||||
class="!flex-row justify-between"
|
||||
>
|
||||
<template #type="{ value }">
|
||||
<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-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"
|
||||
:placeholder="t('case.caseNamePlaceholder')"
|
||||
allow-clear
|
||||
:max-length="255"
|
||||
show-word-limit
|
||||
/>
|
||||
<a-button type="primary">
|
||||
{{ t('apiTestManagement.execute') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</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')">
|
||||
<template #label>
|
||||
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="caseModalForm.priority" /></span>
|
||||
</template>
|
||||
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
|
||||
<caseLevel :case-level="item.label as CaseLevel" />
|
||||
</a-option>
|
||||
</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')">
|
||||
<template #label>
|
||||
<apiStatus :status="caseModalForm.status" />
|
||||
</template>
|
||||
<a-option v-for="item of Object.values(RequestDefinitionStatus)" :key="item" :value="item">
|
||||
<apiStatus :status="item" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item field="tags" :label="t('common.tag')">
|
||||
<MsTagsInput v-model:model-value="caseModalForm.tags" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="px-[16px] font-medium">{{ t('apiTestManagement.requestParams') }}</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<requestComposition
|
||||
ref="requestCompositionRef"
|
||||
v-model:request="apiDataDetail"
|
||||
:is-case="true"
|
||||
hide-response-layout-switch
|
||||
:upload-temp-file-api="uploadTempFileCase"
|
||||
:file-save-as-source-id="apiDataDetail.id"
|
||||
:file-module-options-api="getTransferOptionsCase"
|
||||
:file-save-as-api="transferFileCase"
|
||||
:current-env-config="currentEnvConfig"
|
||||
:is-definition="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||
import environmentSelect from '../../environmentSelect.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
import requestComposition, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
import {
|
||||
addCase,
|
||||
getTransferOptionsCase,
|
||||
transferFileCase,
|
||||
uploadTempFileCase,
|
||||
} from '@/api/modules/api-test/management';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ApiCaseDetail } 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;
|
||||
}>();
|
||||
const emit = defineEmits(['loadCase']);
|
||||
|
||||
const apiDataDetail = ref<RequestParam>(cloneDeep(props.apiDetail));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerVisible = ref(false);
|
||||
|
||||
const drawerLoading = ref(false);
|
||||
|
||||
const description = computed(() => [
|
||||
{
|
||||
key: 'type',
|
||||
locale: 'apiTestManagement.apiType',
|
||||
value: apiDataDetail.value.method,
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
locale: 'apiTestManagement.path',
|
||||
value: apiDataDetail.value.url || apiDataDetail.value.path,
|
||||
},
|
||||
]);
|
||||
|
||||
const environmentSelectRef = ref<InstanceType<typeof environmentSelect>>();
|
||||
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>>();
|
||||
|
||||
function open(record?: ApiCaseDetail, isCopy?: boolean) {
|
||||
innerVisible.value = true;
|
||||
if (isCopy) {
|
||||
caseModalForm.value.name = record?.name;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveCaseCancel() {
|
||||
innerVisible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
caseModalForm.value = { ...initForm };
|
||||
}
|
||||
|
||||
function handleDrawerConfirm(isContinue: boolean) {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
drawerLoading.value = true;
|
||||
const params = { ...requestCompositionRef.value?.makeRequestParams(), ...caseModalForm.value };
|
||||
try {
|
||||
await addCase(params);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
if (!isContinue) {
|
||||
emit('loadCase');
|
||||
handleSaveCaseCancel();
|
||||
}
|
||||
caseModalForm.value = { ...initForm };
|
||||
drawerLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-select-view-value) {
|
||||
font-weight: 400;
|
||||
}
|
||||
:deep(.ms-detail-card-title) {
|
||||
width: 50%;
|
||||
}
|
||||
:deep(.ms-detail-card-desc) {
|
||||
gap: 16px;
|
||||
& > div {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
:deep(.arco-form > .arco-form-item):nth-child(1) .arco-form-item-label-col {
|
||||
display: none;
|
||||
}
|
||||
:deep(.request-and-response) {
|
||||
height: calc(100% - 56px);
|
||||
}
|
||||
</style>
|
|
@ -1,22 +1,156 @@
|
|||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div v-show="activeApiTab.id === 'all'" class="flex-1">
|
||||
<caseTable :active-module="props.activeModule" :protocol="props.protocol" />
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div v-show="activeApiTab.id === 'all'" class="flex-1 overflow-hidden">
|
||||
<caseTable
|
||||
:is-api="false"
|
||||
:active-module="props.activeModule"
|
||||
:protocol="props.protocol"
|
||||
@open-case-tab="openCaseTab"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
|
||||
<caseDetail :active-api-tab="activeApiTab" :module-tree="props.moduleTree" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import caseDetail from './caseDetail.vue';
|
||||
import caseTable from './caseTable.vue';
|
||||
|
||||
import { getCaseDetail } from '@/api/modules/api-test/management';
|
||||
|
||||
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';
|
||||
|
||||
const props = defineProps<{
|
||||
activeModule: string;
|
||||
protocol: string;
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
|
||||
const apiTabs = defineModel<RequestParam[]>('apiTabs', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const activeApiTab = defineModel<RequestParam>('activeApiTab', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const initDefaultId = `case-${Date.now()}`;
|
||||
const defaultCaseParams: RequestParam = {
|
||||
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,
|
||||
mode: 'case',
|
||||
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);
|
||||
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;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getCaseDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.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,
|
||||
});
|
||||
nextTick(() => {
|
||||
loading.value = false; // 等待内容渲染出来再隐藏loading
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
v-model:active-tab="activeApiTab"
|
||||
v-model:tabs="apiTabs"
|
||||
class="flex-1 overflow-hidden"
|
||||
:show-add="currentTab === 'api'"
|
||||
@add="newTab"
|
||||
>
|
||||
<template #label="{ tab }">
|
||||
|
@ -36,7 +37,7 @@
|
|||
</a-select>
|
||||
</div>
|
||||
<api
|
||||
v-if="currentTab === 'api'"
|
||||
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.mode === 'definition'"
|
||||
ref="apiRef"
|
||||
v-model:active-api-tab="activeApiTab"
|
||||
v-model:api-tabs="apiTabs"
|
||||
|
@ -46,10 +47,12 @@
|
|||
:module-tree="props.moduleTree"
|
||||
/>
|
||||
<apiCase
|
||||
v-show="currentTab === 'case'"
|
||||
v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.mode === 'case'"
|
||||
v-model:api-tabs="apiTabs"
|
||||
v-model:active-api-tab="activeApiTab"
|
||||
:active-module="props.activeModule"
|
||||
:protocol="props.protocol"
|
||||
:module-tree="props.moduleTree"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -124,6 +127,7 @@
|
|||
// 下拉框切换
|
||||
function currentTabChange(val: any) {
|
||||
apiTabs.value[0].label = val === 'api' ? t('apiTestManagement.allApi') : t('case.allCase');
|
||||
activeApiTab.value = apiTabs.value[0] as RequestParam;
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
|
@ -190,6 +190,8 @@ export default {
|
|||
'case.batchRecoverCaseTip': 'Are you sure you want to recover {count} selected cases?',
|
||||
'case.recycle.recoverCaseTip': 'When restoring the case, the deleted API will be restored simultaneously.',
|
||||
'case.recycle.confirmRecovery': 'Confirm recovery',
|
||||
'case.createCase': 'Create 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',
|
||||
'case.detail.changeNumber': 'Change sequence',
|
||||
|
|
|
@ -182,6 +182,8 @@ export default {
|
|||
'case.batchRecoverCaseTip': '确认恢复已选中的 {count} 个用例吗?',
|
||||
'case.recycle.recoverCaseTip': '恢复case时会同步恢复被删除的api',
|
||||
'case.recycle.confirmRecovery': '确认恢复',
|
||||
'case.createCase': '创建用例',
|
||||
'case.saveContinueText': '保存并继续创建',
|
||||
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
|
||||
'case.detail.noReminders': '不再提醒',
|
||||
'case.detail.changeNumber': '变更序号',
|
||||
|
|
Loading…
Reference in New Issue