feat(接口测试): 接口场景模块树开发
--task=1014597 --user=宋天阳 接口测试-接口场景-模块树(增,删,改,查,移动) https://www.tapd.cn/55049933/s/1473433
This commit is contained in:
parent
6857dd764c
commit
1253919add
|
@ -0,0 +1,47 @@
|
||||||
|
import MSR from '@/api/http/index';
|
||||||
|
import {
|
||||||
|
AddModuleUrl,
|
||||||
|
DeleteModuleUrl,
|
||||||
|
GetModuleCountUrl,
|
||||||
|
GetModuleTreeUrl,
|
||||||
|
MoveModuleUrl,
|
||||||
|
UpdateModuleUrl,
|
||||||
|
} from '@/api/requrls/api-test/scenario';
|
||||||
|
|
||||||
|
import { ApiScenarioGetModuleParams, ApiScenarioModuleUpdateParams } from '@/models/apiTest/scenario';
|
||||||
|
import {
|
||||||
|
AddModuleParams,
|
||||||
|
ModuleTreeNode,
|
||||||
|
MoveModules,
|
||||||
|
} from '@/models/common';
|
||||||
|
|
||||||
|
// 更新模块
|
||||||
|
export function updateModule(data: ApiScenarioModuleUpdateParams) {
|
||||||
|
return MSR.post({ url: UpdateModuleUrl, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模块树
|
||||||
|
export function getModuleTree(data: ApiScenarioGetModuleParams) {
|
||||||
|
return MSR.post<ModuleTreeNode[]>({ url: GetModuleTreeUrl, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动模块
|
||||||
|
export function moveModule(data: MoveModules) {
|
||||||
|
return MSR.post({ url: MoveModuleUrl, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取模块统计数量
|
||||||
|
export function getModuleCount(data: ApiScenarioGetModuleParams) {
|
||||||
|
return MSR.post({ url: GetModuleCountUrl, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加模块
|
||||||
|
export function addModule(data: AddModuleParams) {
|
||||||
|
return MSR.post({ url: AddModuleUrl, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除模块
|
||||||
|
export function deleteModule(id: string) {
|
||||||
|
return MSR.get({ url: DeleteModuleUrl, params: id });
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const UpdateModuleUrl = '/api/scenario/module/update'; // 更新模块
|
||||||
|
export const GetModuleTreeUrl = '/api/scenario/module/tree'; // 查找模块
|
||||||
|
export const MoveModuleUrl = '/api/scenario/module/move'; // 移动模块
|
||||||
|
export const GetModuleCountUrl = '/api/scenario/module/count'; // 获取模块统计数量
|
||||||
|
export const AddModuleUrl = '/api/scenario/module/add'; // 添加模块
|
||||||
|
export const DeleteModuleUrl = '/api/scenario/module/delete'; // 删除模块
|
||||||
|
// export const GetEnvModuleUrl = '/api/scenario/module/env/tree'; // 获取环境的模块树
|
||||||
|
// export const DefinitionPageUrl = '/api/scenario/page'; // 接口定义列表
|
||||||
|
// export const AddDefinitionUrl = '/api/scenario/add'; // 添加接口定义
|
||||||
|
// export const UpdateDefinitionUrl = '/api/scenario/update'; // 更新接口定义
|
||||||
|
// export const GetDefinitionDetailUrl = '/api/scenario/get-detail'; // 获取接口定义详情
|
||||||
|
// export const TransferFileUrl = '/api/scenario/transfer'; // 文件转存
|
||||||
|
// export const TransferFileModuleOptionUrl = '/api/scenario/transfer/options'; // 文件转存目录
|
||||||
|
// export const UploadTempFileUrl = '/api/scenario/upload/temp/file'; // 临时文件上传
|
||||||
|
// export const DefinitionMockPageUrl = '/api/scenario/mock/page'; // mock列表
|
||||||
|
// export const UpdateMockStatusUrl = '/api/scenario/mock/enable/'; // 更新mock状态
|
||||||
|
// export const DeleteMockUrl = '/api/scenario/mock/delete'; // 刪除mock
|
||||||
|
// export const DeleteDefinitionUrl = '/api/scenario/delete-to-gc'; // 删除接口定义
|
||||||
|
// export const ImportDefinitionUrl = '/api/scenario/import'; // 导入接口定义
|
||||||
|
// export const SortDefinitionUrl = '/api/scenario/edit/pos'; // 接口定义拖拽
|
||||||
|
// export const BatchUpdateDefinitionUrl = '/api/scenario/batch-update'; // 批量更新接口定义
|
||||||
|
// export const BatchMoveDefinitionUrl = '/api/scenario/batch-move'; // 批量移动接口定义
|
||||||
|
// export const BatchDeleteDefinitionUrl = '/api/scenario/batch/delete-to-gc'; // 批量删除接口定义
|
||||||
|
// export const UpdateDefinitionScheduleUrl = '/api/scenario/schedule/update'; // 接口定义-定时同步-更新
|
||||||
|
// export const CheckDefinitionScheduleUrl = '/api/scenario/schedule/check'; // 接口定义-定时同步-检查 url 是否存在
|
||||||
|
// export const AddDefinitionScheduleUrl = '/api/scenario/schedule/add'; // 接口定义-定时同步-添加
|
||||||
|
// export const SwitchDefinitionScheduleUrl = '/api/scenario/schedule/switch'; // 接口定义-定时同步-开启关闭
|
||||||
|
// export const GetDefinitionScheduleUrl = '/api/scenario/schedule/get'; // 接口定义-定时同步-查询
|
||||||
|
// export const DeleteDefinitionScheduleUrl = '/api/scenario/schedule/delete'; // 接口定义-定时同步-删除
|
||||||
|
// export const DebugDefinitionUrl = '/api/scenario/debug'; // 接口定义-调试
|
|
@ -3,6 +3,8 @@ export enum ApiTestRouteEnum {
|
||||||
API_TEST_DEBUG_MANAGEMENT = 'apiTestDebug',
|
API_TEST_DEBUG_MANAGEMENT = 'apiTestDebug',
|
||||||
API_TEST_MANAGEMENT = 'apiTestManagement',
|
API_TEST_MANAGEMENT = 'apiTestManagement',
|
||||||
API_TEST_MANAGEMENT_RECYCLE = 'apiTestManagementRecycle',
|
API_TEST_MANAGEMENT_RECYCLE = 'apiTestManagementRecycle',
|
||||||
|
API_TEST_SCENARIO = 'apiTestScenario',
|
||||||
|
API_TEST_SCENARIO_RECYCLE = 'apiTestScenarioRecycle',
|
||||||
API_TEST_REPORT = 'apiTestReport',
|
API_TEST_REPORT = 'apiTestReport',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default {
|
||||||
'menu.apiTest.debug': 'API debug',
|
'menu.apiTest.debug': 'API debug',
|
||||||
'menu.apiTest.debug.debug': 'Debug',
|
'menu.apiTest.debug.debug': 'Debug',
|
||||||
'menu.apiTest.management': 'API Management',
|
'menu.apiTest.management': 'API Management',
|
||||||
|
'menu.apiTest.scenario': 'API Scenario',
|
||||||
'menu.apiTest.report': 'API Report',
|
'menu.apiTest.report': 'API Report',
|
||||||
'menu.uiTest': 'UI Test',
|
'menu.uiTest': 'UI Test',
|
||||||
'menu.performanceTest': 'Performance Test',
|
'menu.performanceTest': 'Performance Test',
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default {
|
||||||
'menu.apiTest.debug.debug': '调试',
|
'menu.apiTest.debug.debug': '调试',
|
||||||
'menu.apiTest.management': '接口管理',
|
'menu.apiTest.management': '接口管理',
|
||||||
'menu.apiTest.api': 'API列表',
|
'menu.apiTest.api': 'API列表',
|
||||||
|
'menu.apiTest.scenario': '接口场景',
|
||||||
'menu.apiTest.report': '接口报告',
|
'menu.apiTest.report': '接口报告',
|
||||||
'menu.uiTest': 'UI测试',
|
'menu.uiTest': 'UI测试',
|
||||||
'menu.workstation': '工作台',
|
'menu.workstation': '工作台',
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { RequestDefinitionStatus, RequestImportFormat, RequestImportType } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
import { BatchApiParams, ModuleTreeNode, TableQueryParams } from '../common';
|
||||||
|
import { ExecuteRequestParams, ResponseDefinition } from './common';
|
||||||
|
|
||||||
|
|
||||||
|
// 场景-更新模块参数
|
||||||
|
export interface ApiScenarioModuleUpdateParams {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景-获取模块树参数
|
||||||
|
export interface ApiScenarioGetModuleParams {
|
||||||
|
keyword: string;
|
||||||
|
searchMode?: 'AND' | 'OR';
|
||||||
|
filter?: Record<string, any>;
|
||||||
|
combine?: Record<string, any>;
|
||||||
|
moduleIds: string[];
|
||||||
|
projectId: string;
|
||||||
|
versionId?: string;
|
||||||
|
refId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环境-选中的模块
|
||||||
|
export interface SelectedModule {
|
||||||
|
// 选中的模块
|
||||||
|
moduleId: string;
|
||||||
|
containChildModule: boolean; // 是否包含新增子模块
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环境-模块树
|
||||||
|
export interface EnvModule {
|
||||||
|
moduleTree: ModuleTreeNode[];
|
||||||
|
selectedModules: SelectedModule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义列表查询参数
|
||||||
|
export interface ApiScenarioPageParams extends TableQueryParams {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
protocol: string;
|
||||||
|
projectId: string;
|
||||||
|
versionId: string;
|
||||||
|
refId: string;
|
||||||
|
moduleIds: string[];
|
||||||
|
deleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface mockParams {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新定义参数
|
||||||
|
export interface ApiScenarioBatchUpdateParams extends BatchApiParams {
|
||||||
|
type?: string;
|
||||||
|
append?: boolean;
|
||||||
|
method?: string;
|
||||||
|
versionId?: string;
|
||||||
|
tags?: string[];
|
||||||
|
customField?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量移动定义参数
|
||||||
|
export interface ApiScenarioBatchMoveParams extends BatchApiParams {
|
||||||
|
moduleId: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除定义参数
|
||||||
|
export interface ApiScenarioBatchDeleteParams extends BatchApiParams {
|
||||||
|
deleteAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景-定时同步-更新参数
|
||||||
|
export interface UpdateScheduleParams {
|
||||||
|
id: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
|
@ -63,6 +63,28 @@ const ApiTest: AppRouteRecordRaw = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'scenario',
|
||||||
|
name: ApiTestRouteEnum.API_TEST_SCENARIO,
|
||||||
|
component: () => import('@/views/api-test/scenario/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.apiTest.scenario',
|
||||||
|
roles: ['*'],
|
||||||
|
isTopMenu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'scenarioRecycle',
|
||||||
|
name: ApiTestRouteEnum.API_TEST_SCENARIO_RECYCLE,
|
||||||
|
component: () => import('@/views/api-test/scenario/index.vue'),
|
||||||
|
meta: {
|
||||||
|
locale: 'menu.apiTest.scenario',
|
||||||
|
roles: ['*'],
|
||||||
|
isTopMenu: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'report',
|
path: 'report',
|
||||||
name: ApiTestRouteEnum.API_TEST_REPORT,
|
name: ApiTestRouteEnum.API_TEST_REPORT,
|
||||||
|
|
|
@ -0,0 +1,500 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-[8px] flex items-center gap-[8px]">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="moduleKeyword"
|
||||||
|
:placeholder="t('apiScenario.tree.selectorPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
<a-dropdown v-if="!props.readOnly" @select="handleSelect">
|
||||||
|
<a-button type="primary">{{ t('apiScenario.createScenario') }}</a-button>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="newScenario">{{ t('apiScenario.createScenario') }}</a-doption>
|
||||||
|
<a-doption value="import">
|
||||||
|
{{ t('apiScenario.importScenario') }}
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="folder" @click="setActiveFolder('all')">
|
||||||
|
<div :class="allFolderClass">
|
||||||
|
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||||
|
<div class="folder-name">{{ t('apiScenario.tree.folder.allScenario') }}</div>
|
||||||
|
<div class="folder-count">({{ allScenarioCount }})</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center">
|
||||||
|
<a-tooltip :content="isExpandAll ? t('common.collapseAll') : t('common.expandAll')">
|
||||||
|
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
|
||||||
|
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||||
|
</MsButton>
|
||||||
|
</a-tooltip>
|
||||||
|
<template v-if="!props.readOnly">
|
||||||
|
<a-dropdown @select="handleSelect">
|
||||||
|
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||||
|
<MsIcon
|
||||||
|
type="icon-icon_create_planarity"
|
||||||
|
size="18"
|
||||||
|
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
|
||||||
|
/>
|
||||||
|
</MsButton>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="newScenario">{{ t('apiScenario.createScenario') + '(暂未实现)' }}</a-doption>
|
||||||
|
<a-doption value="addModule">{{ t('apiScenario.createSubModule') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
<popConfirm
|
||||||
|
mode="add"
|
||||||
|
:all-names="rootModulesName"
|
||||||
|
parent-id="NONE"
|
||||||
|
:add-module-api="addModule"
|
||||||
|
@add-finish="initModules"
|
||||||
|
>
|
||||||
|
<span id="addModulePopSpan"></span>
|
||||||
|
</popConfirm>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-divider class="my-[8px]" />
|
||||||
|
|
||||||
|
<a-spin class="w-full" :loading="loading">
|
||||||
|
<MsTree
|
||||||
|
v-model:focus-node-key="focusNodeKey"
|
||||||
|
v-model:selected-keys="selectedKeys"
|
||||||
|
:data="folderTree"
|
||||||
|
:keyword="moduleKeyword"
|
||||||
|
:node-more-actions="folderMoreActions"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:expand-all="isExpandAll"
|
||||||
|
:empty-text="t('apiScenario.tree.noMatchModule')"
|
||||||
|
:virtual-list-props="virtualListProps"
|
||||||
|
:field-names="{
|
||||||
|
title: 'name',
|
||||||
|
key: 'id',
|
||||||
|
children: 'children',
|
||||||
|
count: 'count',
|
||||||
|
}"
|
||||||
|
:draggable="!props.readOnly"
|
||||||
|
block-node
|
||||||
|
title-tooltip-position="left"
|
||||||
|
@select="folderNodeSelect"
|
||||||
|
@more-action-select="handleFolderMoreSelect"
|
||||||
|
@more-actions-close="moreActionsClose"
|
||||||
|
@drop="handleDrop"
|
||||||
|
>
|
||||||
|
<template #title="nodeData">
|
||||||
|
<div :id="nodeData.id" class="inline-flex w-full">
|
||||||
|
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||||
|
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="!props.readOnly" #extra="nodeData">
|
||||||
|
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||||
|
<popConfirm
|
||||||
|
v-if="nodeData.id !== 'root' && nodeData.type === 'MODULE'"
|
||||||
|
mode="add"
|
||||||
|
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||||
|
:parent-id="nodeData.id"
|
||||||
|
:add-module-api="addModule"
|
||||||
|
@close="resetFocusNodeKey"
|
||||||
|
@add-finish="() => initModules()"
|
||||||
|
>
|
||||||
|
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusNodeKey(nodeData)">
|
||||||
|
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||||
|
</MsButton>
|
||||||
|
</popConfirm>
|
||||||
|
<popConfirm
|
||||||
|
v-if="nodeData.id !== 'root'"
|
||||||
|
mode="rename"
|
||||||
|
:node-type="nodeData.type"
|
||||||
|
:parent-id="nodeData.id"
|
||||||
|
:node-id="nodeData.id"
|
||||||
|
:field-config="{ field: renameFolderTitle }"
|
||||||
|
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||||
|
:update-module-api="updateModule"
|
||||||
|
@close="resetFocusNodeKey"
|
||||||
|
@rename-finish="initModules"
|
||||||
|
>
|
||||||
|
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
|
||||||
|
</popConfirm>
|
||||||
|
</template>
|
||||||
|
</MsTree>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addModule,
|
||||||
|
deleteModule,
|
||||||
|
getModuleCount,
|
||||||
|
getModuleTree,
|
||||||
|
moveModule,
|
||||||
|
updateModule,
|
||||||
|
} from '@/api/modules/api-test/scenario';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { mapTree } from '@/utils';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
isExpandAll?: boolean; // 是否展开所有节点
|
||||||
|
isShowScenario?: boolean; // 是否显示挂载的场景
|
||||||
|
activeModule?: string | number; // 选中的节点 key
|
||||||
|
readOnly?: boolean; // 是否是只读模式
|
||||||
|
activeNodeId?: string | number; // 当前选中节点 id
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
activeModule: 'all',
|
||||||
|
readOnly: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const emit = defineEmits(['init', 'newScenario', 'import', 'folderNodeSelect', 'clickScenario', 'changeProtocol']);
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
function handleSelect(value: string | number | Record<string, any> | undefined) {
|
||||||
|
switch (value) {
|
||||||
|
case 'newScenario':
|
||||||
|
emit('newScenario');
|
||||||
|
break;
|
||||||
|
case 'import':
|
||||||
|
emit('import');
|
||||||
|
break;
|
||||||
|
case 'addModule':
|
||||||
|
document.querySelector('#addModulePopSpan')?.dispatchEvent(new Event('click'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualListProps = computed(() => {
|
||||||
|
if (props.readOnly) {
|
||||||
|
return {
|
||||||
|
height: 'calc(60vh - 343px)',
|
||||||
|
threshold: 200,
|
||||||
|
fixedSize: true,
|
||||||
|
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
height: 'calc(100vh - 343px)',
|
||||||
|
threshold: 200,
|
||||||
|
fixedSize: true,
|
||||||
|
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleKeyword = ref('');
|
||||||
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
const focusNodeKey = ref<string | number>('');
|
||||||
|
const selectedKeys = ref<Array<string | number>>([props.activeModule]);
|
||||||
|
const allFolderClass = computed(() =>
|
||||||
|
selectedKeys.value[0] === 'all' ? 'folder-text folder-text--active' : 'folder-text'
|
||||||
|
);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
function setActiveFolder(id: string) {
|
||||||
|
selectedKeys.value = [id];
|
||||||
|
emit('folderNodeSelect', selectedKeys.value, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.activeNodeId,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
selectedKeys.value = [val];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function setFocusNodeKey(node: MsTreeNodeData) {
|
||||||
|
focusNodeKey.value = node.id || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderMoreActions: ActionsItem[] = [
|
||||||
|
{
|
||||||
|
label: 'common.rename',
|
||||||
|
eventTag: 'rename',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDivider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'common.delete',
|
||||||
|
eventTag: 'delete',
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const modulesCount = ref<Record<string, number>>({});
|
||||||
|
const allScenarioCount = computed(() => modulesCount.value.all || 0);
|
||||||
|
const isExpandAll = ref(props.isExpandAll);
|
||||||
|
const rootModulesName = ref<string[]>([]); // 根模块名称列表
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化模块树
|
||||||
|
* @param isSetDefaultKey 是否设置第一个节点为选中节点
|
||||||
|
*/
|
||||||
|
async function initModules(isSetDefaultKey = false) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getModuleTree({
|
||||||
|
keyword: moduleKeyword.value,
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
moduleIds: [],
|
||||||
|
});
|
||||||
|
const nodePathObj: Record<string, any> = {};
|
||||||
|
if (props.readOnly) {
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
|
||||||
|
// 拼接当前节点的完整路径
|
||||||
|
nodePathObj[e.id] = {
|
||||||
|
path: e.path,
|
||||||
|
fullPath,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
hideMoreAction: true,
|
||||||
|
draggable: false,
|
||||||
|
disabled: e.id === selectedKeys.value[0],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
|
||||||
|
// 拼接当前节点的完整路径
|
||||||
|
nodePathObj[e.id] = {
|
||||||
|
path: e.path,
|
||||||
|
fullPath,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
hideMoreAction: e.id === 'root',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isSetDefaultKey) {
|
||||||
|
selectedKeys.value = [folderTree.value[0].id];
|
||||||
|
}
|
||||||
|
emit('init', folderTree.value, nodePathObj);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initModuleCount() {
|
||||||
|
try {
|
||||||
|
const res = await getModuleCount({
|
||||||
|
keyword: moduleKeyword.value,
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
moduleIds: [],
|
||||||
|
});
|
||||||
|
modulesCount.value = res;
|
||||||
|
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
count: res[node.id] || 0,
|
||||||
|
draggable: !props.readOnly,
|
||||||
|
disabled: props.readOnly ? node.id === selectedKeys.value[0] : false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isExpandAll,
|
||||||
|
(val) => {
|
||||||
|
isExpandAll.value = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function changeExpand() {
|
||||||
|
isExpandAll.value = !isExpandAll.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件夹树节点选中事件
|
||||||
|
*/
|
||||||
|
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
|
||||||
|
const offspringIds: string[] = [];
|
||||||
|
if (props.isShowScenario) {
|
||||||
|
mapTree(node.children || [], (e) => {
|
||||||
|
offspringIds.push(e.id);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('folderNodeSelect', _selectedKeys, offspringIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件夹
|
||||||
|
* @param node 节点信息
|
||||||
|
*/
|
||||||
|
function deleteFolder(node: MsTreeNodeData) {
|
||||||
|
openModal({
|
||||||
|
type: 'error',
|
||||||
|
title: t('apiScenario.module.deleteTipTitle', { name: node.name }),
|
||||||
|
content: t('apiScenario.module.deleteTipContent'),
|
||||||
|
okText: t('apiScenario.deleteConfirm'),
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
maskClosable: false,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteModule(node.id);
|
||||||
|
Message.success(t('apiScenario.deleteSuccess'));
|
||||||
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const renamePopVisible = ref(false);
|
||||||
|
const renameFolderTitle = ref(''); // 重命名的文件夹名称
|
||||||
|
|
||||||
|
function resetFocusNodeKey() {
|
||||||
|
focusNodeKey.value = '';
|
||||||
|
renamePopVisible.value = false;
|
||||||
|
renameFolderTitle.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理树节点更多按钮事件
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
function handleFolderMoreSelect(item: ActionsItem, node: MsTreeNodeData) {
|
||||||
|
switch (item.eventTag) {
|
||||||
|
case 'delete':
|
||||||
|
deleteFolder(node);
|
||||||
|
resetFocusNodeKey();
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
renameFolderTitle.value = node.name || '';
|
||||||
|
renamePopVisible.value = true;
|
||||||
|
document.querySelector(`#renameSpan${node.id}`)?.dispatchEvent(new Event('click'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文件夹树节点拖拽事件
|
||||||
|
* @param tree 树数据
|
||||||
|
* @param dragNode 拖拽节点
|
||||||
|
* @param dropNode 释放节点
|
||||||
|
* @param dropPosition 释放位置
|
||||||
|
*/
|
||||||
|
async function handleDrop(
|
||||||
|
tree: MsTreeNodeData[],
|
||||||
|
dragNode: MsTreeNodeData,
|
||||||
|
dropNode: MsTreeNodeData,
|
||||||
|
dropPosition: number
|
||||||
|
) {
|
||||||
|
if (dragNode.id === 'root' || (dragNode.type === 'MODULE' && dropNode.id === 'root')) {
|
||||||
|
// 根节点不可拖拽;模块不可拖拽到根节点
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
await moveModule({
|
||||||
|
dragNodeId: dragNode.id as string,
|
||||||
|
dropNodeId: dropNode.id || '',
|
||||||
|
dropPosition,
|
||||||
|
});
|
||||||
|
Message.success(t('apiScenario.moveSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moreActionsClose() {
|
||||||
|
if (!renamePopVisible.value) {
|
||||||
|
// 当下拉菜单关闭时,若不是触发重命名气泡显示,则清空聚焦节点 key
|
||||||
|
resetFocusNodeKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
await initModules();
|
||||||
|
initModuleCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refresh,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.folder {
|
||||||
|
@apply flex cursor-pointer items-center justify-between;
|
||||||
|
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(var(--primary-1));
|
||||||
|
}
|
||||||
|
.folder-text {
|
||||||
|
@apply flex flex-1 cursor-pointer items-center;
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.folder-name {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
.folder-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.folder-text--active {
|
||||||
|
.folder-icon,
|
||||||
|
.folder-name,
|
||||||
|
.folder-count {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(#root ~ .arco-tree-node-drag-icon) {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<div class="rounded-2xl bg-white">
|
||||||
|
<div class="p-[24px] pb-[16px]">
|
||||||
|
<span>场景列表接口(标签页配置未实现)</span>
|
||||||
|
</div>
|
||||||
|
<a-divider class="!my-0" />
|
||||||
|
<div class="pageWrap">
|
||||||
|
<MsSplitBox :size="300" :max="0.5">
|
||||||
|
<template #first>
|
||||||
|
<div class="p-[24px] pb-0">
|
||||||
|
<div class="feature-case h-[100%]">
|
||||||
|
<scenarioModuleTree
|
||||||
|
ref="scenarioModuleTreeRef"
|
||||||
|
:is-show-scenario="isShowScenario"
|
||||||
|
@folder-node-select="handleNodeSelect"
|
||||||
|
@init="handleModuleInit"
|
||||||
|
></scenarioModuleTree>
|
||||||
|
<div class="b-0 absolute w-[88%]">
|
||||||
|
<a-divider class="!my-0 !mb-2" />
|
||||||
|
<div class="case h-[38px]">
|
||||||
|
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
|
||||||
|
<MsIcon type="icon-icon_delete-trash_outlined" class="folder-icon" />
|
||||||
|
<div class="folder-name mx-[4px]">{{ t('apiScenario.tree.recycleBin') }}</div>
|
||||||
|
<!-- <div class="folder-count">({{ recycleModulesCount.all || 0 }})</div></div-->
|
||||||
|
<div class="folder-count">({{ 0 }})</div></div
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #second>
|
||||||
|
<div class="p-[24px]">
|
||||||
|
<!-- <apiTable ref="apiTableRef" :active-module="activeFolder" :offspring-ids="offspringIds" />-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsSplitBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* @description 接口测试-接口场景主页
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
|
import scenarioModuleTree from './components/scenarioModuleTree.vue';
|
||||||
|
import ApiTable from '@/views/api-test/management/components/management/api/apiTable.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
import { ModuleTreeNode } from '@/models/common';
|
||||||
|
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||||
|
const folderTreePathMap = ref<Record<string, any>>({});
|
||||||
|
const activeFolder = ref<string>('all');
|
||||||
|
const activeModule = ref<string>('all');
|
||||||
|
const offspringIds = ref<string[]>([]);
|
||||||
|
const addSubVisible = ref(false);
|
||||||
|
|
||||||
|
const isShowScenario = ref(false);
|
||||||
|
|
||||||
|
// 获取激活用例类型样式
|
||||||
|
const getActiveClass = (type: string) => {
|
||||||
|
return activeFolder.value === type ? 'folder-text case-active' : 'folder-text';
|
||||||
|
};
|
||||||
|
|
||||||
|
const scenarioModuleTreeRef = ref();
|
||||||
|
|
||||||
|
function handleModuleInit(tree, _protocol: string, pathMap: Record<string, any>) {
|
||||||
|
folderTree.value = tree;
|
||||||
|
folderTreePathMap.value = pathMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeSelect(keys: string[], _offspringIds: string[]) {
|
||||||
|
[activeModule.value] = keys;
|
||||||
|
offspringIds.value = _offspringIds;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.pageWrap {
|
||||||
|
min-width: 1000px;
|
||||||
|
height: calc(100vh - 166px);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
@apply bg-white;
|
||||||
|
.case {
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
@apply flex cursor-pointer items-center justify-between;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(var(--primary-1));
|
||||||
|
}
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.folder-name {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
.folder-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.case-active {
|
||||||
|
.folder-icon,
|
||||||
|
.folder-name,
|
||||||
|
.folder-count {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
|
||||||
|
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
|
||||||
|
.arco-icon {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
@apply flex cursor-pointer items-center rounded-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.recycle {
|
||||||
|
@apply absolute bottom-0 bg-white pb-4;
|
||||||
|
:deep(.arco-divider-horizontal) {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.recycle-bin {
|
||||||
|
@apply bottom-0 flex items-center bg-white;
|
||||||
|
.recycle-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,20 @@
|
||||||
|
export default {
|
||||||
|
'apiScenario.createScenario': 'Create scenario',
|
||||||
|
'apiScenario.importScenario': 'Import scenario',
|
||||||
|
'apiScenario.tree.selectorPlaceholder': 'Please enter the module name',
|
||||||
|
'apiScenario.tree.folder.allScenario': 'All scenarios',
|
||||||
|
'apiScenario.tree.showLeafNodeScenario': 'Show subdirectory scenarios',
|
||||||
|
'apiScenario.tree.recycleBin': 'Recycle bin',
|
||||||
|
'apiScenario.tree.noMatchModule': 'No matching module/scene yet',
|
||||||
|
|
||||||
|
'apiScenario.createSubModule': 'Create sub-module',
|
||||||
|
|
||||||
|
'apiScenario.module.deleteTipTitle': 'Delete {name} module?',
|
||||||
|
'apiScenario.module.deleteTipContent':
|
||||||
|
'After deletion, all scenarios under the module will be deleted synchronously. Please operate with caution.',
|
||||||
|
|
||||||
|
'apiScenario.deleteConfirm': 'Confirm',
|
||||||
|
'apiScenario.deleteSuccess': 'Success',
|
||||||
|
|
||||||
|
'apiScenario.moveSuccess': 'Success',
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
export default {
|
||||||
|
'apiScenario.createScenario': '新建场景',
|
||||||
|
'apiScenario.importScenario': '导入场景',
|
||||||
|
'apiScenario.tree.selectorPlaceholder': '请输入模块名称',
|
||||||
|
'apiScenario.tree.folder.allScenario': '全部场景',
|
||||||
|
'apiScenario.tree.showLeafNodeScenario': '显示子目录场景',
|
||||||
|
'apiScenario.tree.recycleBin': '回收站',
|
||||||
|
'apiScenario.tree.noMatchModule': '暂无匹配的模块/场景',
|
||||||
|
|
||||||
|
'apiScenario.createSubModule': '新建子模块',
|
||||||
|
|
||||||
|
'apiScenario.module.deleteTipTitle': '是否删除 {name} 模块?',
|
||||||
|
'apiScenario.module.deleteTipContent': '删除后,会同步删除模块下的所有场景,请谨慎操作.',
|
||||||
|
|
||||||
|
'apiScenario.deleteConfirm': '确认删除',
|
||||||
|
'apiScenario.deleteSuccess': '删除成功',
|
||||||
|
|
||||||
|
'apiScenario.moveSuccess': '移动成功',
|
||||||
|
};
|
Loading…
Reference in New Issue