feat(接口定义): 模块树操作与右侧数据同步

This commit is contained in:
baiqi 2024-03-13 20:37:44 +08:00 committed by 刘瑞斌
parent dac0d93608
commit 49090a2312
7 changed files with 184 additions and 47 deletions

View File

@ -78,7 +78,7 @@
updateApiNodeApi?: (params: { id: string; name: string }) => Promise<any>;
}>();
const emit = defineEmits(['update:visible', 'close', 'addFinish', 'renameFinish', 'updateDescFinish']);
const emit = defineEmits(['update:visible', 'close', 'addFinish', 'renameFinish']);
const appStore = useAppStore();
const { t } = useI18n();

View File

@ -262,6 +262,9 @@
...defaultProps,
});
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
if (!defaultProps) {
definitionActiveKey.value = 'definition';
}
}
const apiTableRef = ref<InstanceType<typeof apiTable>>();
@ -326,7 +329,6 @@
}
function handleAddDone() {
definitionActiveKey.value = 'preview'; //
if (typeof refreshModuleTree === 'function') {
refreshModuleTree();
}

View File

@ -92,9 +92,9 @@
const apiRef = ref<InstanceType<typeof api>>();
function newTab(apiInfo?: ModuleTreeNode | string) {
function newTab(apiInfo?: ModuleTreeNode | string, isCopy?: boolean, isExecute?: boolean) {
if (apiInfo) {
apiRef.value?.openApiTab(apiInfo);
apiRef.value?.openApiTab(apiInfo, isCopy, isExecute);
} else {
apiRef.value?.addApiTab();
}
@ -105,10 +105,22 @@
id: 'all',
label: t('apiTestManagement.allApi'),
closable: false,
} as RequestParam,
moduleId: 'root',
} as unknown as RequestParam,
]);
const activeApiTab = ref<RequestParam>(apiTabs.value[0] as RequestParam);
// id
watch(
() => props.activeModule,
(val) => {
const defaultTab = apiTabs.value.find((item) => item.id === 'all');
if (defaultTab) {
defaultTab.moduleId = val;
}
}
);
//
function currentTabChange(val: any) {
apiTabs.value[0].label = val === 'api' ? t('apiTestManagement.allApi') : t('case.allCase');
@ -117,7 +129,8 @@
watch(
() => activeApiTab.value.id,
() => {
if (typeof setActiveApi === 'function') {
if (typeof setActiveApi === 'function' && !activeApiTab.value.isNew) {
// tab tab
setActiveApi(activeApiTab.value);
}
}
@ -166,6 +179,53 @@
});
}
/**
* 同步模块树的接口信息更新操作
*/
function handleApiUpdateFromModuleTree(newInfo: { id: string; name: string; moduleId?: string; [key: string]: any }) {
apiTabs.value = apiTabs.value.map((item) => {
if (item.id === newInfo.id) {
item.label = newInfo.name;
item.name = newInfo.name;
if (newInfo.moduleId) {
item.moduleId = newInfo.moduleId;
}
}
return item;
});
if (activeApiTab.value.id === newInfo.id) {
activeApiTab.value.label = newInfo.name;
activeApiTab.value.name = newInfo.name;
if (newInfo.moduleId) {
activeApiTab.value.moduleId = newInfo.moduleId;
}
}
}
/**
* 同步模块树的接口信息删除操作
* @param id 接口 id
* @param isModule 是否是删除模块
*/
function handleDeleteApiFromModuleTree(id: string, isModule = false) {
if (isModule) {
//
apiTabs.value = apiTabs.value.filter((item) => {
if (activeApiTab.value.id === item.id) {
// tab, tab
[activeApiTab.value] = apiTabs.value;
}
return item.moduleId !== id || item.id === 'all';
});
} else {
// api
apiTabs.value = apiTabs.value.filter((item) => item.id !== id);
if (activeApiTab.value.id === id) {
[activeApiTab.value] = apiTabs.value;
}
}
}
onBeforeMount(() => {
initEnvList();
});
@ -176,6 +236,8 @@
defineExpose({
newTab,
refreshApiTable,
handleApiUpdateFromModuleTree,
handleDeleteApiFromModuleTree,
});
</script>

View File

@ -60,7 +60,7 @@
:all-names="rootModulesName"
parent-id="NONE"
:add-module-api="addModule"
@add-finish="initModules"
@add-finish="handleAddFinish"
>
<span id="addModulePopSpan"></span>
</popConfirm>
@ -122,7 +122,7 @@
:parent-id="nodeData.id"
:add-module-api="addModule"
@close="resetFocusNodeKey"
@add-finish="() => initModules()"
@add-finish="handleAddFinish"
>
<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)]" />
@ -139,7 +139,7 @@
:update-module-api="updateModule"
:update-api-node-api="updateDefinition"
@close="resetFocusNodeKey"
@rename-finish="initModules"
@rename-finish="handleRenameFinish"
>
<span :id="`renameSpan${nodeData.id}`" class="relative"></span>
</popConfirm>
@ -151,6 +151,7 @@
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useClipboard } from '@vueuse/core';
import { Message, SelectOptionData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -164,6 +165,7 @@
import { getProtocolList } from '@/api/modules/api-test/common';
import {
addModule,
deleteDefinition,
deleteModule,
getModuleCount,
getModuleTree,
@ -199,11 +201,22 @@
trash: false,
}
);
const emit = defineEmits(['init', 'newApi', 'import', 'folderNodeSelect', 'clickApiNode', 'changeProtocol']);
const emit = defineEmits([
'init',
'newApi',
'import',
'folderNodeSelect',
'clickApiNode',
'changeProtocol',
'updateApiNode',
'deleteNode',
'execute',
]);
const appStore = useAppStore();
const { t } = useI18n();
const { openModal } = useModal();
const { copy, isSupported } = useClipboard();
const moduleProtocol = ref('HTTP');
const moduleProtocolOptions = ref<SelectOptionData[]>([]);
@ -483,8 +496,11 @@
function deleteFolder(node: MsTreeNodeData) {
openModal({
type: 'error',
title: t('apiTestDebug.deleteFolderTipTitle', { name: node.name }),
content: t('apiTestDebug.deleteFolderTipContent'),
title:
node.type === 'API'
? t('apiTestDebug.deleteDebugTipTitle', { name: node.name })
: t('apiTestDebug.deleteFolderTipTitle', { name: node.name }),
content: node.type === 'API' ? t('apiTestDebug.deleteDebugTipContent') : t('apiTestDebug.deleteFolderTipContent'),
okText: t('apiTestDebug.deleteConfirm'),
okButtonProps: {
status: 'danger',
@ -492,8 +508,13 @@
maskClosable: false,
onBeforeOk: async () => {
try {
await deleteModule(node.id);
if (node.type === 'API') {
await deleteDefinition(node.id);
} else {
await deleteModule(node.id);
}
Message.success(t('apiTestDebug.deleteSuccess'));
emit('deleteNode', node.id, node.type === 'MODULE');
await initModules();
initModuleCount();
} catch (error) {
@ -514,6 +535,15 @@
renameFolderTitle.value = '';
}
function share(id: string) {
if (isSupported) {
copy(`${window.location.href}&dId=${id}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
/**
* 处理树节点更多按钮事件
* @param item
@ -529,6 +559,12 @@
renamePopVisible.value = true;
document.querySelector(`#renameSpan${node.id}`)?.dispatchEvent(new Event('click'));
break;
case 'share':
share(node.id);
break;
case 'execute':
emit('execute', node.id);
break;
default:
break;
}
@ -577,9 +613,10 @@
projectId: appStore.currentProjectId,
moveMode: dropPositionMap[dropPosition],
moveId: dragNode.id,
targetId: dropNode.type === 'MODULE' ? dragNode.id : dropNode.id,
targetId: dropNode.type === 'MODULE' ? dragNode.id : dropNode.id, // API id API id
moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id, // APIidid
});
emit('updateApiNode', { ...dragNode, moduleId: dropNode.type === 'API' ? dropNode.parentId : dropNode.id });
}
Message.success(t('apiTestDebug.moduleMoveSuccess'));
} catch (error) {
@ -592,6 +629,16 @@
}
}
async function handleAddFinish() {
await initModules();
initModuleCount();
}
function handleRenameFinish(newName: string, id: string) {
emit('updateApiNode', { name: newName, id });
initModules();
}
function moreActionsClose() {
if (!renamePopVisible.value) {
// key

View File

@ -2,24 +2,33 @@
<MsCard simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5">
<template #first>
<div class="p-[16px]">
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeApi?.id"
@init="handleModuleInit"
@new-api="newApi"
@import="importDrawerVisible = true"
@folder-node-select="handleNodeSelect"
@click-api-node="handleApiNodeClick"
@change-protocol="handleProtocolChange"
/>
</div>
<div class="w-full p-[8px]">
<a-divider class="!my-0 !mb-0" />
<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('caseManagement.featureCase.recycle') }}</div>
<div class="flex flex-col">
<div class="p-[16px]">
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeNodeId"
@init="handleModuleInit"
@new-api="newApi"
@import="importDrawerVisible = true"
@folder-node-select="handleNodeSelect"
@click-api-node="handleApiNodeClick"
@change-protocol="handleProtocolChange"
@update-api-node="handleUpdateApiNode"
@delete-node="handleDeleteApiFromModuleTree"
@execute="handleExecute"
/>
</div>
<div class="flex-1">
<a-divider class="!my-0 !mb-0" />
<div class="case">
<div
class="flex items-center px-[20px]"
:class="getActiveClass('recycle')"
@click="setActiveFolder('recycle')"
>
<MsIcon type="icon-icon_delete-trash_outlined" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.recycle') }}</div>
</div>
</div>
</div>
</div>
@ -79,7 +88,7 @@
const importDrawerVisible = ref(false);
const offspringIds = ref<string[]>([]);
const protocol = ref('HTTP');
const activeApi = ref<RequestParam>();
const activeNodeId = ref<string | number>('all');
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const managementRef = ref<InstanceType<typeof management>>();
@ -103,7 +112,12 @@
}
function setActiveApi(params: RequestParam) {
activeApi.value = params;
if (params.id === 'all') {
// tab api
activeNodeId.value = params.moduleId;
} else {
activeNodeId.value = params.id;
}
}
function handleProtocolChange(val: string) {
@ -119,6 +133,18 @@
managementRef.value?.refreshApiTable();
}
function handleUpdateApiNode(newInfo: { id: string; name: string; moduleId?: string; [key: string]: any }) {
managementRef.value?.handleApiUpdateFromModuleTree(newInfo);
}
function handleDeleteApiFromModuleTree(id: string, isModule?: boolean) {
managementRef.value?.handleDeleteApiFromModuleTree(id, isModule);
}
function handleExecute(id: string) {
managementRef.value?.newTab(id, false, true);
}
onMounted(() => {
if (route.query.dId) {
// dId tab

View File

@ -186,14 +186,14 @@
const virtualListProps = computed(() => {
if (props.readOnly) {
return {
height: 'calc(60vh - 343px)',
height: 'calc(60vh - 325px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
};
}
return {
height: 'calc(100vh - 343px)',
height: 'calc(100vh - 325px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding

View File

@ -20,23 +20,23 @@
<div v-if="activeApiTab.id === 'all'" class="pageWrap">
<MsSplitBox :size="300" :max="0.5">
<template #first>
<div class="p-[24px] pb-0">
<div class="feature-case h-[100%]">
<div class="flex h-full flex-col">
<div class="p-[16px]">
<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="redirectRecycle()">
<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 class="flex-1">
<a-divider margin="0" />
<div class="case">
<div class="flex items-center px-[20px]" :class="getActiveClass('recycle')" @click="redirectRecycle()">
<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>