feat(接口测试): 接口测试文档分享详情页面拆分不展示菜单

This commit is contained in:
xinxin.wu 2024-10-22 15:48:10 +08:00 committed by Craftsman
parent 946ed7de55
commit c0f4b89742
9 changed files with 353 additions and 258 deletions

View File

@ -108,6 +108,7 @@ export enum SettingRouteEnum {
export enum ShareEnum {
SHARE = 'share',
SHARE_REPORT_SCENARIO = 'shareReportScenario',
SHARE_DEFINITION_API = 'shareDefinitionApi',
SHARE_REPORT_CASE = 'shareReportCase',
SHARE_REPORT_TEST_PLAN = 'shareReportTestPlan',
}

View File

@ -21,6 +21,11 @@ export const WHITE_LIST = [
path: '/shareReportTestPlan',
children: [],
},
{
name: 'shareDefinitionApi',
path: '/shareDefinitionApi',
children: [],
},
],
},
{
@ -38,6 +43,11 @@ export const WHITE_LIST = [
path: '/shareReportTestPlan',
children: [],
},
{
name: 'shareDefinitionApi',
path: '/shareDefinitionApi',
children: [],
},
];
// 左侧菜单底部对齐的菜单数组数组项为一级路由的name

View File

@ -46,6 +46,17 @@ const ShareRoute: AppRouteRecordRaw = {
isTopMenu: false,
},
},
// 接口文档分享
{
path: 'shareDefinitionApi',
name: ShareEnum.SHARE_DEFINITION_API,
component: () => import('@/views/api-test/management/components/management/api/shareApiDocIndex.vue'),
meta: {
locale: '',
roles: ['*'],
isTopMenu: false,
},
},
],
};

View File

@ -1,5 +1,5 @@
<template>
<div class="h-[calc(100%-32px)]">
<div class="h-[calc(100%-64px)]">
<ApiPreview
:detail="activeApiDetail"
:protocols="props.selectedProtocols"
@ -238,7 +238,6 @@
bottom: 0;
z-index: 99;
padding: 16px;
height: 22px;
@apply flex w-full items-center justify-between bg-white;
.doc-toggle {

View File

@ -0,0 +1,320 @@
<template>
<MsCard simple no-content-padding auto-height>
<div class="h-[calc(100vh-32px)]">
<MsSplitBox :size="300" :max="0.5">
<template #first>
<div class="flex flex-col">
<div class="p-[16px]">
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeNodeId"
:doc-share-id="docShareId"
@init="handleModuleInit"
@folder-node-select="handleNodeSelect"
@change-protocol="handleProtocolChange"
@open-current-node="openCurrentNode"
@export-share="handleExportShare"
/>
</div>
</div>
</template>
<template #second>
<ApiSharePreview
:selected-protocols="protocols"
:api-info="currentNode"
:previous-node="previousNode"
:next-node="nextNode"
@toggle-detail="toggleDetail"
@export-share="handleExportShare"
/>
</template>
</MsSplitBox>
</div>
<!-- 分享密码校验 -->
<a-modal
v-model:visible="checkPsdModal"
:mask-closable="false"
:closable="false"
:mask="true"
title-align="start"
class="ms-modal-upload ms-modal-medium ms-modal-share"
:width="280"
unmount-on-close
@close="closeShareHandler"
>
<div class="no-resource-svg"></div>
<a-form ref="formRef" :rules="rules" :model="checkForm" layout="vertical">
<a-form-item
class="password-form mb-0"
field="password"
:label="t('apiTestManagement.effectiveTime')"
hide-asterisk
hide-label
:validate-trigger="['blur']"
>
<a-input-password
v-model="checkForm.password"
:max-length="6"
:placeholder="t('apiTestManagement.sharePasswordPlaceholder')"
allow-clear
autocomplete="new-password"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button type="primary" :loading="checkLoading" :disabled="!checkForm.password" @click="handleCheckPsd">
{{ t('common.confirm') }}
</a-button>
</template>
</a-modal>
<ApiExportModal
v-model:visible="showExportModal"
:batch-params="batchParams"
:condition-params="getConditionParams"
is-share
/>
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import ApiExportModal from '@/views/api-test/management/components/management/api/apiExportModal.vue';
import ApiSharePreview from '@/views/api-test/management/components/management/api/apiSharePreview.vue';
import moduleTree from '@/views/api-test/management/components/moduleTree.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { checkSharePsd, shareDetail } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { NOT_FOUND_RESOURCE } from '@/router/constants';
import { useUserStore } from '@/store';
import useDocShareCheckStore from '@/store/modules/api/docShareCheck';
import useAppStore from '@/store/modules/app';
import { ShareDetailType } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
const appStore = useAppStore();
const route = useRoute();
const { t } = useI18n();
const router = useRouter();
const docCheckStore = useDocShareCheckStore();
const userStore = useUserStore();
const activeNodeId = ref<string | number>('all');
const activeModule = ref<string>('all');
const offspringIds = ref<string[]>([]);
const docShareId = ref<string>(route.query.docShareId as string);
const folderTree = ref<ModuleTreeNode[]>([]);
const selectedProtocols = ref<string[]>([]);
const folderTreePathMap = ref<Record<string, any>>({});
function handleModuleInit(tree: ModuleTreeNode[], _protocols: string[], pathMap: Record<string, any>) {
folderTree.value = tree;
selectedProtocols.value = _protocols;
folderTreePathMap.value = pathMap;
}
function handleNodeSelect(keys: string[], _offspringIds: string[]) {
[activeModule.value] = keys;
offspringIds.value = _offspringIds;
}
function handleProtocolChange(val: string[]) {
selectedProtocols.value = val;
}
const checkLoading = ref<boolean>(false);
const checkPsdModal = ref<boolean>(false);
const checkForm = ref({
docShareId: route.query.docShareId as string,
password: '',
});
const validatePassword = (value: string | undefined, callback: (error?: string) => void) => {
const sixDigitRegex = /^\d{6}$/;
if (value === undefined || value === '') {
callback(t('apiTestManagement.enterPassword'));
} else if (!sixDigitRegex.test(value)) {
callback(t('apiTestManagement.enterPassword'));
} else {
callback();
}
};
const rules = {
password: [
{
required: true,
message: t('apiTestManagement.sharePasswordPlaceholder'),
},
{
validator: validatePassword,
},
],
};
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
// |
function toggleDetail(type: string) {
if (type === 'prev') {
moduleTreeRef.value?.previousApi();
} else {
moduleTreeRef.value?.nextApi();
}
}
const formRef = ref<FormInstance>();
//
function closeShareHandler() {
checkPsdModal.value = false;
formRef.value?.resetFields();
checkForm.value.password = '';
}
const protocols = ref<any[]>([]);
async function initProtocolList() {
try {
protocols.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initProtocolList();
});
const currentNode = ref();
const showExportModal = ref(false);
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
});
// |
function handleExportShare(all: boolean) {
batchParams.value.selectAll = all;
batchParams.value.selectedIds = all ? [] : [currentNode.value?.id];
showExportModal.value = true;
}
function getConditionParams() {
return {
condition: {
keyword: '',
filter: {},
viewId: '',
},
projectId: appStore.currentProjectId,
protocols: selectedProtocols.value,
moduleIds: [],
shareId: docShareId.value,
};
}
const shareDetailInfo = ref<ShareDetailType>({
invalid: false,
allowExport: false,
isPrivate: false,
});
//
async function getShareDetail() {
try {
shareDetailInfo.value = await shareDetail(docShareId.value);
//
if (shareDetailInfo.value.invalid) {
router.push({
name: NOT_FOUND_RESOURCE,
query: {
type: 'EXPIRED',
},
});
}
// 访
if (shareDetailInfo.value.isPrivate && !docCheckStore.isDocVerified(docShareId.value, userStore.id || '')) {
checkPsdModal.value = true;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const previousNode = ref<ModuleTreeNode | null>();
const nextNode = ref<ModuleTreeNode | null>();
//
function openCurrentNode(node: ModuleTreeNode, apiNodes: ModuleTreeNode[]) {
const index = apiNodes.indexOf(node);
currentNode.value = node;
previousNode.value = index > 0 ? apiNodes[index - 1] : null;
nextNode.value = index < apiNodes.length - 1 ? apiNodes[index + 1] : null;
}
//
function handleCheckPsd() {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
checkLoading.value = true;
const res = await checkSharePsd(checkForm.value);
if (res) {
closeShareHandler();
//
docCheckStore.markDocAsVerified(docShareId.value, userStore.id || '');
checkPsdModal.value = false;
} else {
Message.error(t('apiTestManagement.apiSharePsdError'));
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
checkLoading.value = false;
}
}
});
}
onBeforeMount(() => {
if (docShareId.value) {
getShareDetail();
}
});
provide('docShareId', docShareId.value);
provide('shareDetailInfo', shareDetailInfo);
</script>
<style scoped lang="less">
.no-resource-svg {
margin: 0 auto 24px;
width: 160px;
height: 98px;
background: url('@/assets/svg/no_resource.svg');
background-size: cover;
}
:deep(.ms-modal-share) {
.arco-modal-mask {
background: var(--color-text-1) !important;
}
}
:deep(.password-form) {
.arco-form-item-message {
margin-bottom: 0 !important;
}
}
</style>

View File

@ -67,11 +67,11 @@
import { useAppStore } from '@/store';
import type { shareItem } from '@/models/apiTest/management';
import { RouteEnum } from '@/enums/routeEnum';
const appStore = useAppStore();
const { copy, isSupported } = useClipboard({ legacy: true });
const { t } = useI18n();
const emit = defineEmits<{
@ -133,9 +133,9 @@
}
if (isSupported) {
// dId
const url = window.location.href;
const dIdParam = `&docShareId=${item.id}`;
copy(`${url}${dIdParam}`);
const shareLinkUrl = `${origin}/#/${RouteEnum.SHARE}/${RouteEnum.SHARE_DEFINITION_API}`;
const dIdParam = `?orgId=${appStore.currentOrgId}&pId=${appStore.currentProjectId}&docShareId=${item.id}`;
copy(`${shareLinkUrl}${dIdParam}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));

View File

@ -78,7 +78,7 @@
import { hasAnyPermission } from '@/utils/permission';
import type { ShareDetail, shareItem } from '@/models/apiTest/management';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { ShareEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterRemoteMethodsEnum } from '@/enums/tableFilterEnum';
@ -189,7 +189,7 @@
//
function viewLink(record: shareItem) {
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
openNewPage(ShareEnum.SHARE_DEFINITION_API, {
docShareId: record.id,
});
}

View File

@ -268,14 +268,14 @@
const virtualListProps = computed(() => {
if (props.readOnly || props.isModal) {
return {
height: props.docShareId ? 'calc(60vh - 150px)' : 'calc(60vh - 190px)',
height: 'calc(60vh - 190px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
};
}
return {
height: props.docShareId ? 'calc(100vh - 233px)' : 'calc(100vh - 273px)',
height: props.docShareId ? 'calc(100vh - 150px)' : 'calc(100vh - 273px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding

View File

@ -7,7 +7,6 @@
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeNodeId"
:doc-share-id="docShareId"
@init="handleModuleInit"
@new-api="newApi"
@import="importDrawerVisible = true"
@ -17,8 +16,6 @@
@update-api-node="handleUpdateApiNode"
@delete-node="handleDeleteApiFromModuleTree"
@execute="handleExecute"
@open-current-node="openCurrentNode"
@export-share="handleExportShare"
/>
</div>
<div v-if="!docShareId" class="flex-1">
@ -36,7 +33,6 @@
<template #second>
<div class="relative flex h-full flex-col">
<div
v-if="!docShareId"
id="managementContainer"
:class="['absolute z-[102] h-full w-full', importDrawerVisible ? '' : 'invisible']"
style="transition: all 0.3s"
@ -51,7 +47,6 @@
/>
</div>
<management
v-if="!docShareId"
ref="managementRef"
:module-tree="folderTree"
:active-module="activeModule"
@ -60,62 +55,9 @@
@import="importDrawerVisible = true"
@handle-adv-search="handleAdvSearch"
/>
<ApiSharePreview
v-if="docShareId"
:selected-protocols="protocols"
:api-info="currentNode"
:previous-node="previousNode"
:next-node="nextNode"
@toggle-detail="toggleDetail"
@export-share="handleExportShare"
/>
</div>
</template>
</MsSplitBox>
<!-- 分享密码校验 -->
<a-modal
v-model:visible="checkPsdModal"
:mask-closable="false"
:closable="false"
:mask="true"
title-align="start"
class="ms-modal-upload ms-modal-medium ms-modal-share"
:width="280"
unmount-on-close
@close="closeShareHandler"
>
<div class="no-resource-svg"></div>
<a-form ref="formRef" :rules="rules" :model="checkForm" layout="vertical">
<a-form-item
class="password-form mb-0"
field="password"
:label="t('apiTestManagement.effectiveTime')"
hide-asterisk
hide-label
:validate-trigger="['blur']"
>
<a-input-password
v-model="checkForm.password"
:max-length="6"
:placeholder="t('apiTestManagement.sharePasswordPlaceholder')"
allow-clear
autocomplete="new-password"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button type="primary" :loading="checkLoading" :disabled="!checkForm.password" @click="handleCheckPsd">
{{ t('common.confirm') }}
</a-button>
</template>
</a-modal>
<ApiExportModal
v-model:visible="showExportModal"
:batch-params="batchParams"
:condition-params="getConditionParams"
is-share
/>
</MsCard>
<importTaskDrawer v-model:visible="taskDrawerVisible" />
</template>
@ -126,28 +68,20 @@
*/
import { provide } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { RequestParam } from '../components/requestComposition/index.vue';
import importApi from './components/import.vue';
import importTaskDrawer from './components/importTaskDrawer.vue';
import management from './components/management/index.vue';
import moduleTree from './components/moduleTree.vue';
import ApiExportModal from '@/views/api-test/management/components/management/api/apiExportModal.vue';
import ApiSharePreview from '@/views/api-test/management/components/management/api/apiSharePreview.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { checkSharePsd, getTrashModuleCount, shareDetail } from '@/api/modules/api-test/management';
import { getTrashModuleCount } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { NOT_FOUND_RESOURCE } from '@/router/constants';
import { useUserStore } from '@/store';
import useDocShareCheckStore from '@/store/modules/api/docShareCheck';
import useAppStore from '@/store/modules/app';
import { ApiDefinitionGetModuleParams, ShareDetailType } from '@/models/apiTest/management';
import { ApiDefinitionGetModuleParams } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
@ -155,8 +89,6 @@
const route = useRoute();
const { t } = useI18n();
const router = useRouter();
const docCheckStore = useDocShareCheckStore();
const userStore = useUserStore();
const activeModule = ref<string>('all');
const folderTree = ref<ModuleTreeNode[]>([]);
@ -190,20 +122,6 @@
}
}
const protocols = ref<any[]>([]);
async function initProtocolList() {
try {
protocols.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initProtocolList();
});
function handleProtocolChange(val: string[]) {
selectedProtocols.value = val;
}
@ -284,153 +202,8 @@
moduleTreeRef.value?.setActiveFolder('all');
}
const checkLoading = ref<boolean>(false);
const checkPsdModal = ref<boolean>(false);
const checkForm = ref({
docShareId: route.query.docShareId as string,
password: '',
});
const validatePassword = (value: string | undefined, callback: (error?: string) => void) => {
const sixDigitRegex = /^\d{6}$/;
if (value === undefined || value === '') {
callback(t('apiTestManagement.enterPassword'));
} else if (!sixDigitRegex.test(value)) {
callback(t('apiTestManagement.enterPassword'));
} else {
callback();
}
};
const rules = {
password: [
{
required: true,
message: t('apiTestManagement.sharePasswordPlaceholder'),
},
{
validator: validatePassword,
},
],
};
// |
function toggleDetail(type: string) {
if (type === 'prev') {
moduleTreeRef.value?.previousApi();
} else {
moduleTreeRef.value?.nextApi();
}
}
const formRef = ref<FormInstance>();
//
function closeShareHandler() {
checkPsdModal.value = false;
formRef.value?.resetFields();
checkForm.value.password = '';
}
const currentNode = ref();
const showExportModal = ref(false);
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
selectAll: false,
excludeIds: [],
});
// |
function handleExportShare(all: boolean) {
batchParams.value.selectAll = all;
batchParams.value.selectedIds = all ? [] : [currentNode.value?.id];
showExportModal.value = true;
}
function getConditionParams() {
return {
condition: {
keyword: '',
filter: {},
viewId: '',
},
projectId: appStore.currentProjectId,
protocols: selectedProtocols.value,
moduleIds: [],
shareId: docShareId.value,
};
}
const shareDetailInfo = ref<ShareDetailType>({
invalid: false,
allowExport: false,
isPrivate: false,
});
//
async function getShareDetail() {
try {
shareDetailInfo.value = await shareDetail(docShareId.value);
//
if (shareDetailInfo.value.invalid) {
router.push({
name: NOT_FOUND_RESOURCE,
query: {
type: 'EXPIRED',
},
});
}
// 访
if (shareDetailInfo.value.isPrivate && !docCheckStore.isDocVerified(docShareId.value, userStore.id || '')) {
checkPsdModal.value = true;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const previousNode = ref<ModuleTreeNode | null>();
const nextNode = ref<ModuleTreeNode | null>();
//
function openCurrentNode(node: ModuleTreeNode, apiNodes: ModuleTreeNode[]) {
const index = apiNodes.indexOf(node);
currentNode.value = node;
previousNode.value = index > 0 ? apiNodes[index - 1] : null;
nextNode.value = index < apiNodes.length - 1 ? apiNodes[index + 1] : null;
}
//
function handleCheckPsd() {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
checkLoading.value = true;
const res = await checkSharePsd(checkForm.value);
if (res) {
closeShareHandler();
//
docCheckStore.markDocAsVerified(docShareId.value, userStore.id || '');
checkPsdModal.value = false;
} else {
Message.error(t('apiTestManagement.apiSharePsdError'));
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
checkLoading.value = false;
}
}
});
}
const taskDrawerVisible = ref(false);
onBeforeMount(() => {
if (docShareId.value) {
getShareDetail();
}
if (route.query.taskDrawer) {
taskDrawerVisible.value = true;
}
@ -441,8 +214,6 @@
provide('refreshModuleTree', refreshModuleTree);
provide('refreshModuleTreeCount', refreshModuleTreeCount);
provide('folderTreePathMap', folderTreePathMap.value);
provide('docShareId', docShareId.value);
provide('shareDetailInfo', shareDetailInfo);
</script>
<style lang="less" scoped>
@ -498,21 +269,4 @@
}
}
}
.no-resource-svg {
margin: 0 auto 24px;
width: 160px;
height: 98px;
background: url('@/assets/svg/no_resource.svg');
background-size: cover;
}
:deep(.ms-modal-share) {
.arco-modal-mask {
background: var(--color-text-1) !important;
}
}
:deep(.password-form) {
.arco-form-item-message {
margin-bottom: 0 !important;
}
}
</style>