feat(接口测试): 接口文档分享联调(不包含导出)

This commit is contained in:
xinxin.wu 2024-10-16 13:27:40 +08:00 committed by 刘瑞斌
parent 98e38ffefc
commit 1f23110358
16 changed files with 1081 additions and 145 deletions

View File

@ -7,6 +7,7 @@ import {
AddDefinitionUrl,
AddMockUrl,
AddModuleUrl,
AddShareUrl,
BatchCleanOutApiUrl,
BatchDeleteCaseUrl,
BatchDeleteDefinitionUrl,
@ -24,6 +25,7 @@ import {
CasePageUrl,
caseTableBatchSyncUrl,
CheckDefinitionScheduleUrl,
checkSharePsdUrl,
clearThisChangeUrl,
ConvertJsonSchemaToJsonUrl,
CopyMockUrl,
@ -39,6 +41,7 @@ import {
DeleteModuleUrl,
DeleteRecycleApiUrl,
DeleteRecycleCaseUrl,
DeleteShareUrl,
diffDataUrl,
ExecuteCaseUrl,
ExportDefinitionUrl,
@ -59,6 +62,7 @@ import {
GetModuleTreeUrl,
GetPoolId,
GetPoolOptionUrl,
GetSharePageUrl,
getSyncedCaseDetailUrl,
GetTrashModuleCountUrl,
GetTrashModuleTreeUrl,
@ -74,6 +78,9 @@ import {
RecycleCasePageUrl,
RunCaseUrl,
SaveOperationHistoryUrl,
shareDetailUrl,
shareModuleCountUrl,
shareModuleTreeUrl,
SortCaseUrl,
SortDefinitionUrl,
StopApiExportUrl,
@ -94,6 +101,7 @@ import {
UpdateMockStatusUrl,
UpdateMockUrl,
UpdateModuleUrl,
UpdateShareUrl,
UploadTempFileCaseUrl,
UploadTempFileUrl,
UploadTempMockFileUrl,
@ -127,6 +135,7 @@ import {
ApiDefinitionUpdateParams,
BatchRecoverApiParams,
CheckScheduleParams,
CheckSharePsdType,
CreateImportApiDefinitionScheduleParams,
DefinitionHistoryItem,
DefinitionHistoryPageParams,
@ -136,19 +145,22 @@ import {
ImportApiDefinitionParams,
mockParams,
RecoverDefinitionParams,
ShareDetail,
ShareDetailType,
shareItem,
UpdateScheduleParams,
} from '@/models/apiTest/management';
import type { BatchEditMockParams, MockDetail, MockParams, UpdateMockParams } from '@/models/apiTest/mock';
import {
import type {
AddModuleParams,
type BatchApiParams,
BatchApiParams,
CommonList,
DragSortParams,
ModuleTreeNode,
MoveModules,
TableQueryParams,
TransferFileParams,
} from '@/models/common';
import { TableQueryParams } from '@/models/common';
import { ResourcePoolItem } from '@/models/setting/resourcePool';
// 更新模块
@ -635,3 +647,40 @@ export function logCaseReportBatchExport(data: BatchApiParams) {
export function getCaseBatchExportParams(data: BatchApiParams) {
return MSR.post({ url: `${GetCaseBatchExportParamsUrl}`, data });
}
// 接口定义-接口文档
// 接口测试-接口管理-新增分享
export function addShare(data: ShareDetail) {
return MSR.post({ url: `${AddShareUrl}`, data });
}
// 接口测试-接口管理-更新分享
export function updateShare(data: ShareDetail) {
return MSR.post({ url: `${UpdateShareUrl}`, data });
}
// 接口测试-接口管理-删除分享
export function deleteShare(id: string) {
return MSR.get({ url: DeleteShareUrl, params: id });
}
// 接口测试-接口管理-分享列表
export function getSharePage(data: TableQueryParams) {
return MSR.post<CommonList<shareItem>>({ url: `${GetSharePageUrl}`, data });
}
// 接口测试-接口管理-分享详情
export function shareDetail(id: string) {
return MSR.get<ShareDetailType>({ url: shareDetailUrl, params: id });
}
// 接口测试-接口管理-校验分享密码
export function checkSharePsd(data: CheckSharePsdType) {
return MSR.post<CommonList<shareItem>>({ url: `${checkSharePsdUrl}`, data });
}
// 接口测试-接口管理-分享模块树
export function getShareModuleTree(data: ApiDefinitionGetModuleParams) {
return MSR.post<ModuleTreeNode[]>({ url: shareModuleTreeUrl, data });
}
// 接口测试-接口管理-分享模块数量
export function getShareModuleCount(data: ApiDefinitionGetModuleParams) {
return MSR.post({ url: shareModuleCountUrl, data });
}

View File

@ -112,3 +112,13 @@ export const AddCaseUrl = '/api/case/add'; // 添加用例
export const GetPoolOptionUrl = '/api/test/pool-option'; // 获取接口资源池
export const GetPoolId = '/api/test/get-pool/'; // 获取项目应用设置的资源池id
// 接口定义文档
export const AddShareUrl = '/api/doc/share/add'; // 接口测试-接口管理-新增分享
export const UpdateShareUrl = '/api/doc/share/update'; // 接口测试-接口管理-更新分享
export const DeleteShareUrl = '/api/doc/share/delete'; // 接口测试-接口管理-删除分享
export const GetSharePageUrl = '/api/doc/share/page'; // 接口测试-接口管理-分享列表
export const checkSharePsdUrl = '/api/doc/share/check'; // 接口测试-接口管理-校验分享密码
export const shareDetailUrl = '/api/doc/share/detail'; // 接口测试-接口管理-查看链接
export const shareModuleTreeUrl = '/api/doc/share/module/tree'; // 接口测试-接口管理-模块树
export const shareModuleCountUrl = '/api/doc/share/module/count'; // 接口测试-接口管理-模块数量

View File

@ -174,6 +174,7 @@ export default {
'common.refresh': 'Refresh',
'common.searchByIdName': 'Search by ID/name',
'common.searchByIDNameTag': 'Search by ID/name/tag',
'common.searchByName': 'Search by name',
'common.archive': 'archive',
'common.running': 'Running',
'common.unExecute': 'Pending',

View File

@ -174,6 +174,7 @@ export default {
'common.refresh': '刷新',
'common.searchByIdName': '通过 ID/名称搜索',
'common.searchByIDNameTag': '通过 ID/名称/标签搜索',
'common.searchByName': '通过名称搜索',
'common.archive': '归档',
'common.running': '执行中',
'common.unExecute': '未执行',

View File

@ -105,6 +105,7 @@ export interface ApiDefinitionGetModuleParams {
projectId: string;
versionId?: string;
refId?: string;
shareId?: string;
}
// 环境-选中的模块
@ -438,3 +439,38 @@ export interface diffSyncParams {
deleteRedundantParam: boolean; // 是否删除多余参数
apiCaseRequest: RequestParam; // 用例详情请求request
}
export type ApiRangeType = 'ALL' | 'MODULE' | 'PATH' | 'TAG';
// 接口定义-接口文档-分享
export interface ShareDetail {
id?: string;
name: string;
apiRange: ApiRangeType; // 接口范围;全部接口(ALL)、模块(MODULE)、路径(PATH)、标签(TAG)
rangeMatchSymbol: string; // 范围匹配符
rangeMatchVal: string; // 范围匹配值;eg: 选中路径范围时, 该值作为路径匹配
isPrivate: boolean; // 是否公开
password: string; // 访问密码
allowExport: boolean; // 允许导出
projectId: string;
invalidTime?: string; // 失效时间值
invalidUnit?: string; // 失效时间单位;小时(HOUR)、天(DAY)、月(MONTH)、年(YEAR)
}
// 分享列表
export interface shareItem extends ShareDetail {
createTime: number;
createUser: string;
invalid: boolean;
apiShareNum: number;
deadline: number;
}
export interface CheckSharePsdType {
docShareId: string;
password: string;
}
// 分享详情
export interface ShareDetailType {
invalid: boolean;
allowExport: boolean;
isPrivate: boolean;
}

View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia';
// 验证分享的文档是否校验过
const useDocShareCheckStore = defineStore('shareCheckStore', {
state: (): { verifiedDocs: string[] } => ({
verifiedDocs: [],
}),
actions: {
// 检查该 docShareId 和 userId 组合是否已经验证过
isDocVerified(docShareId: string, userId: string) {
const key: string = `verified_${docShareId}_${userId}`;
return this.verifiedDocs.includes(key) || localStorage.getItem(key) === 'true';
},
// 将 docShareId 和 userId 组合标记为已验证
markDocAsVerified(docShareId: string, userId: string) {
const key: string = `verified_${docShareId}_${userId}`;
if (!this.verifiedDocs.includes(key)) {
this.verifiedDocs.push(key);
localStorage.setItem(key, 'true');
}
},
},
});
export default useDocShareCheckStore;

View File

@ -0,0 +1,245 @@
<template>
<div class="h-[calc(100%-32px)]">
<ApiPreview
:detail="activeApiDetail"
:protocols="props.selectedProtocols"
@update-follow="activeApiDetail.follow = !activeApiDetail.follow"
/>
</div>
<div class="doc-toggle-footer">
<div v-if="props?.previousNode" class="doc-toggle" @click="toggleApiDetail('prev')">
<MsIcon
type="icon-icon_pull-left_outlined"
:class="` text-[var(--color-text-4)] ${props.previousNode ? 'hover:text-[rgb(var(--primary-5))]' : ''}`"
:size="16"
/>
<apiMethodName
:method="
props?.previousNode?.attachInfo.protocol === 'HTTP'
? props.previousNode?.attachInfo.method ?? ''
: props.previousNode?.attachInfo.protocol ?? ''
"
class="mr-[4px]"
/>
<a-tooltip :content="`${props.previousNode?.name}`" position="tl" :disabled="!props.previousNode?.name">
<div class="doc-toggle-name one-line-text">
{{ props.previousNode?.name }}
</div>
</a-tooltip>
</div>
<div v-else></div>
<div v-if="props?.nextNode" class="doc-toggle justify-end" @click="toggleApiDetail('next')">
<apiMethodName
:method="
props?.nextNode?.attachInfo.protocol === 'HTTP'
? props.nextNode?.attachInfo.method ?? ''
: props.nextNode?.attachInfo.protocol ?? ''
"
class="mr-[4px]"
/>
<a-tooltip :content="`${props?.nextNode?.name}`" position="tr">
<div class="doc-toggle-name one-line-text">
{{ props?.nextNode?.name }}
</div>
</a-tooltip>
<MsIcon
type="icon-icon_pull-right_outlined"
:class="` text-[var(--color-text-4)] ${props?.nextNode ? 'hover:text-[rgb(var(--primary-5))]' : ''}`"
:size="16"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import ApiPreview from '@/views/api-test/management/components/management/api/preview/index.vue';
import { getDefinitionDetail } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import {
ProtocolKeyEnum,
RequestAuthType,
RequestComposition,
RequestDefinitionStatus,
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 appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
apiInfo?: ModuleTreeNode | null;
previousNode?: ModuleTreeNode | null;
nextNode?: ModuleTreeNode | null;
selectedProtocols: ProtocolItem[];
}>();
const emit = defineEmits<{
(e: 'toggleDetail', type: string): void;
}>();
const localProtocol = localStorage.getItem(ProtocolKeyEnum.API_NEW_PROTOCOL);
const initDefaultId = `definition-${Date.now()}`;
const defaultDefinitionParams: RequestParam = {
type: 'api',
definitionActiveKey: 'definition',
id: initDefaultId,
moduleId: '',
protocol: localProtocol || 'HTTP',
tags: [],
status: RequestDefinitionStatus.PROCESSING,
description: '',
url: '',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSaved: false,
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: true,
assertions: [],
},
postProcessorConfig: {
enableGlobal: true,
processors: [],
},
preProcessorConfig: {
enableGlobal: true,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
mode: 'definition',
executeLoading: false,
preDependency: [], //
postDependency: [], //
errorMessageInfo: {},
};
const loading = ref(false);
const activeApiDetail = ref<RequestParam>(cloneDeep(defaultDefinitionParams));
async function initDetail() {
if (props.apiInfo && props.apiInfo.id) {
try {
appStore.showLoading();
loading.value = true;
const res = await getDefinitionDetail(props.apiInfo.id);
appStore.hideLoading();
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body, res.response); // id
}
const { request } = res;
const defaultProps: Partial<TabItem> = {
label: res.name,
...res,
...request,
name: res.name || '-',
num: res.num || '-',
response: cloneDeep(defaultResponse),
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
url: res.path,
definitionActiveKey: 'preview',
...parseRequestBodyResult,
};
activeApiDetail.value = {
...cloneDeep(defaultDefinitionParams),
...defaultProps,
};
nextTick(() => {
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
appStore.hideLoading();
}
}
}
// &
function toggleApiDetail(type: string) {
emit('toggleDetail', type);
}
watch(
() => props.apiInfo,
(val) => {
if (val) {
initDetail();
}
},
{ deep: true }
);
</script>
<style scoped lang="less">
.doc-toggle-footer {
position: absolute;
bottom: 0;
z-index: 99;
padding: 16px;
height: 22px;
@apply flex w-full items-center justify-between bg-white;
.doc-toggle {
@apply flex flex-1 cursor-pointer items-center gap-2;
.doc-toggle-name {
max-width: 300px;
}
}
}
</style>

View File

@ -284,8 +284,13 @@
</div>
</template>
</a-modal>
<CreateShareModal v-model:visible="showShareModal" :edit-id="editId" @close="cancelHandler" />
<ShareListDrawer v-model:visible="showShareListDrawer" @edit-or-create="editHandler" />
<CreateShareModal
v-model:visible="showShareModal"
:record="editRecord"
@close="cancelHandler"
@load-list="loadShareList"
/>
<ShareListDrawer ref="shareListRef" v-model:visible="showShareListDrawer" @edit-or-create="editHandler" />
</template>
<script setup lang="ts">
@ -332,6 +337,7 @@
import { hasAnyPermission } from '@/utils/permission';
import { ProtocolItem } from '@/models/apiTest/common';
import type { ShareDetail } from '@/models/apiTest/management';
import { ApiDefinitionDetail, ApiDefinitionGetModuleParams } from '@/models/apiTest/management';
import { DragSortParams, ModuleTreeNode } from '@/models/common';
import { FilterType, ViewTypeEnum } from '@/enums/advancedFilterEnum';
@ -1288,14 +1294,25 @@
showShareListDrawer.value = true;
}
const editId = ref<string>();
function editHandler(id?: string) {
editId.value = id;
const editRecord = ref<ShareDetail>();
//
function editHandler(record?: ShareDetail) {
editRecord.value = record;
showShareModal.value = true;
}
const shareListRef = ref<InstanceType<typeof ShareListDrawer>>();
function cancelHandler() {
editId.value = '';
showShareModal.value = false;
editRecord.value = undefined;
}
//
function loadShareList() {
if (!showShareListDrawer.value) {
showShareListDrawer.value = true;
} else {
shareListRef.value?.searchList();
}
}
watch(

View File

@ -1,7 +1,7 @@
<template>
<a-modal
v-model:visible="innerVisible"
:title="props.editId ? t('apiTestManagement.updateCreateShare') : t('apiTestManagement.newCreateShare')"
:title="props?.record?.id ? t('apiTestManagement.updateCreateShare') : t('apiTestManagement.newCreateShare')"
title-align="start"
class="ms-modal-form"
:cancel-button-props="{ disabled: confirmLoading }"
@ -20,54 +20,49 @@
</a-form-item>
<a-form-item field="interfaceRange" :label="t('apiTestManagement.interfaceRange')" asterisk-position="end">
<div class="flex w-full items-center gap-[8px]">
<a-select v-model="form.type" class="w-[120px]">
<a-select v-model="form.apiRange" class="w-[120px]">
<a-option v-for="item in shareTypeOptions" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
<a-tree-select
v-if="form.type === 'module'"
v-model:modelValue="form.moduleId"
<MsTreeSelect
v-if="form.apiRange === 'MODULE'"
v-model:model-value="moduleIds"
:data="moduleTree"
allow-clear
:multiple="true"
:tree-checkable="true"
:placeholder="t('common.pleaseSelect')"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:tree-props="{
virtualListProps: {
height: 200,
threshold: 200,
},
}"
:filter-tree-node="filterTreeNode"
allow-search
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[300px]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
<a-select v-if="form.type === 'tag'" v-model="form.operator" class="w-[120px]">
/>
<a-input
v-if="form.apiRange === 'PATH'"
v-model="form.rangeMatchVal"
class="w-full"
:max-length="255"
:placeholder="t('project.environmental.http.pathPlaceholder')"
/>
<a-select v-if="form.apiRange === 'TAG'" v-model="form.rangeMatchSymbol" class="w-[120px]">
<a-option v-for="item in tagOperators" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-option>
</a-select>
<MsTagsInput
v-if="form.type === 'tag'"
v-model:model-value="form.tags"
v-if="form.apiRange === 'TAG'"
v-model:model-value="tags"
class="flex-1"
placeholder="apiTestManagement.enterTheInputTag"
allow-clear
unique-value
empty-priority-highest
retain-input-value
/>
</div>
</a-form-item>
<a-form-item field="effectiveTime" :label="t('apiTestManagement.effectiveTime')" asterisk-position="end">
<MsTimeSelectorVue v-model="form.time" @change="handleTimeChange" />
<MsTimeSelectorVue v-model="invalidTimeValue" @change="handleTimeChange" />
</a-form-item>
<div class="mb-[16px] flex items-center">
<a-switch v-model:model-value="form.passwordAccess" class="mr-[8px]" size="small" />
<a-switch v-model:model-value="form.isPrivate" class="mr-[8px]" size="small" />
{{ t('apiTestManagement.passwordAccess') }}
</div>
<a-form-item
@ -77,7 +72,7 @@
hide-asterisk
hide-label
:validate-trigger="['blur']"
:rules="form.passwordAccess ? [{ validator: validatePassword }] : []"
:rules="form.isPrivate ? [{ validator: validatePassword }] : []"
>
<a-input
v-model:model-value="form.password"
@ -105,72 +100,82 @@
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import { CONTAINS, EQUAL } from '@/components/pure/ms-advance-filter/index';
import { CONTAINS } from '@/components/pure/ms-advance-filter/index';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsTimeSelectorVue from '@/components/pure/ms-time-selector/MsTimeSelector.vue';
import MsTreeSelect from '@/components/pure/ms-tree-select/index.vue';
import { addShare, getEnvModules, updateShare } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { filterTreeNode, TreeNode } from '@/utils';
import { useAppStore } from '@/store';
import { TreeNode } from '@/utils';
import type { ShareDetail } from '@/models/apiTest/management';
import type { ModuleTreeNode } from '@/models/common';
import { OperatorEnum } from '@/enums/advancedFilterEnum';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
editId?: string;
record?: ShareDetail;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'loadList'): void;
}>();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const initForm = {
id: '',
const initForm: ShareDetail = {
name: '',
type: 'allApi',
moduleId: '',
operator: OperatorEnum.CONTAINS,
tags: [],
time: '',
passwordAccess: false,
apiRange: 'ALL',
rangeMatchSymbol: OperatorEnum.CONTAINS,
rangeMatchVal: '',
invalidTime: '',
invalidUnit: '',
isPrivate: false,
password: '',
allowExport: false,
projectId: '',
};
const form = ref({ ...initForm });
const moduleTree = ref<TreeNode<ModuleTreeNode>[]>([]);
const tags = ref<string[]>([]);
const moduleIds = ref<string[]>([]);
const invalidTimeValue = ref('');
const form = ref<ShareDetail>({ ...initForm });
const shareTypeOptions = ref<SelectOptionData>([
{
label: t('apiTestManagement.allApi'),
value: 'allApi',
value: 'ALL',
},
{
label: t('apiTestManagement.module'),
value: 'module',
value: 'MODULE',
},
{
label: t('apiTestManagement.path'),
value: 'path',
value: 'PATH',
},
{
label: t('common.tag'),
value: 'tag',
value: 'TAG',
},
]);
const tagOperators = ref([CONTAINS, EQUAL]);
const tagOperators = ref([CONTAINS]);
function handleTimeChange(value: string) {
form.value.time = value;
invalidTimeValue.value = value;
}
const okText = computed(() => {
return props.editId ? t('common.update') : t('common.newCreate');
return props?.record?.id ? t('common.update') : t('common.newCreate');
});
const validatePassword = (value: string | undefined, callback: (error?: string) => void) => {
@ -187,12 +192,53 @@
const formRef = ref<FormInstance>();
function handleCancel() {
innerVisible.value = false;
formRef.value?.resetFields();
form.value = cloneDeep(initForm);
tags.value = [];
moduleIds.value = [];
emit('close');
}
const timeValueUnit = computed(() => {
let time: string | undefined;
let unit: string | undefined;
if (invalidTimeValue.value) {
// H, D, M, Y
const match = invalidTimeValue.value.match(/^(\d+)([HDMY])$/);
if (match) {
const [_, timeValue, symbol] = match;
time = timeValue; //
const unitSymbol = symbol; // (H, D, M, Y)
//
switch (unitSymbol) {
case 'H':
unit = 'HOUR';
break;
case 'D':
unit = 'DAY';
break;
case 'M':
unit = 'MONTH';
break;
case 'Y':
unit = 'YEAR';
break;
default:
unit = undefined; //
}
}
}
return {
time,
unit,
};
});
const confirmLoading = ref<boolean>(false);
function handleConfirm() {
@ -200,8 +246,29 @@
if (!errors) {
confirmLoading.value = true;
try {
//
const params: ShareDetail = {
...form.value,
invalidTime: timeValueUnit.value.time,
invalidUnit: timeValueUnit.value.unit,
projectId: appStore.currentProjectId,
};
if (form.value.apiRange === 'TAG') {
params.rangeMatchVal = tags.value.join(',');
}
if (form.value.apiRange === 'MODULE') {
params.rangeMatchVal = moduleIds.value.join(',');
}
if (props?.record?.id) {
await updateShare(params);
} else {
await addShare(params);
}
emit('loadList');
handleCancel();
Message.success(props?.record?.id ? t('common.updateSuccess') : t('common.createSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
@ -209,6 +276,60 @@
}
});
}
const moduleTree = ref<TreeNode<ModuleTreeNode>[]>([]);
async function initModuleTree() {
try {
const res = await getEnvModules({
projectId: appStore.currentProjectId,
});
moduleTree.value = res.moduleTree;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
function getOriginalUnit() {
switch (props.record?.invalidUnit) {
case 'HOUR':
return 'H';
case 'DAY':
return 'D';
case 'MONTH':
return 'M';
case 'YEAR':
return 'Y';
default:
return '';
}
}
function initDetail() {
if (props.record?.id) {
form.value = {
...props.record,
};
const { rangeMatchVal, invalidTime } = form.value;
if (form.value.apiRange === 'TAG') {
tags.value = rangeMatchVal.split(',');
}
if (form.value.apiRange === 'MODULE') {
moduleIds.value = rangeMatchVal.split(',');
}
invalidTimeValue.value = `${invalidTime}${getOriginalUnit()}`;
}
}
watch(
() => innerVisible.value,
(val) => {
if (val) {
initModuleTree();
initDetail();
}
}
);
</script>
<style scoped></style>

View File

@ -7,7 +7,7 @@
<apiMethodName :method="previewDetail.method as RequestMethods" tag-size="small" is-tag />
</template>
<template #titleAppend>
<a-tooltip :content="t('report.detail.api.copyLink')">
<a-tooltip v-if="!docShareId" :content="t('report.detail.api.copyLink')">
<MsIcon
type="icon-icon_copy_outlined"
class="cursor-pointer text-[var(--color-text-4)]"
@ -15,7 +15,7 @@
@click="share"
/>
</a-tooltip>
<a-tooltip :content="t(previewDetail.follow ? 'common.forked' : 'common.notForked')">
<a-tooltip v-if="!docShareId" :content="t(previewDetail.follow ? 'common.forked' : 'common.notForked')">
<MsIcon
v-permission="['PROJECT_API_DEFINITION:READ+UPDATE']"
:loading="followLoading"
@ -26,10 +26,22 @@
@click="toggleFollowReview"
/>
</a-tooltip>
<!-- 分享导出 TODO 联调 -->
<a-tooltip v-if="docShareId && shareDetailInfo?.allowExport" :content="t('common.export')">
<MsIcon
type="icon-icon_top-align_outlined"
class="cursor-pointer text-[var(--color-text-4)]"
:size="16"
@click="exportShare"
/>
</a-tooltip>
</template>
</MsDetailCard>
</div>
<a-tabs v-model:active-key="activeKey" animation lazy-load>
<div v-if="docShareId" class="px-[16px]">
<detailTab :detail="previewDetail" :protocols="props.protocols" />
</div>
<a-tabs v-else v-model:active-key="activeKey" animation lazy-load>
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]">
<detailTab :detail="previewDetail" :protocols="props.protocols" />
</a-tab-pane>
@ -64,6 +76,7 @@
import { toggleFollowDefinition } from '@/api/modules/api-test/management';
import { ProtocolItem } from '@/models/apiTest/common';
import { ShareDetailType } from '@/models/apiTest/management';
import { RequestMethods } from '@/enums/apiEnum';
import { getValidRequestTableParams } from '@/views/api-test/components/utils';
@ -78,6 +91,8 @@
const { t } = useI18n();
const previewDetail = ref<RequestParam>(cloneDeep(props.detail));
const docShareId: string | undefined = inject('docShareId');
const shareDetailInfo = inject<Ref<ShareDetailType>>('shareDetailInfo');
watch(
() => props.detail.id,
@ -174,6 +189,8 @@
}
const activeKey = ref('detail');
// TODO
function exportShare() {}
</script>
<style lang="less" scoped>

View File

@ -30,21 +30,21 @@
</div>
<div
v-for="item in shareList"
:key="item.value"
:class="[`share-option-item ${item.value === currentShare ? 'share-option-item-active' : ''} w-full`]"
:key="item.id"
:class="[`share-option-item ${item.id === currentShare ? 'share-option-item-active' : ''} w-full`]"
@click="changeShare(item)"
>
<div class="flex w-full items-center justify-between">
<a-tooltip :content="item.label">
<a-tooltip :content="item.name">
<div class="one-line-text max-w-[100px]">
{{ item.label }}
{{ item.name }}
</div>
</a-tooltip>
<MsIcon
type="icon-icon_copy_outlined"
class="cursor-pointer text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
:size="16"
@click="copyShareLink(item.value)"
@click="copyShareLink(item.id as string)"
/>
</div>
</div>
@ -55,9 +55,18 @@
<script setup lang="ts">
import { ref } from 'vue';
import { SelectOptionData } from '@arco-design/web-vue';
import { useClipboard } from '@vueuse/core';
import { Message, SelectOptionData } from '@arco-design/web-vue';
import { getSharePage } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { shareItem } from '@/models/apiTest/management';
const appStore = useAppStore();
const { copy, isSupported } = useClipboard({ legacy: true });
const { t } = useI18n();
@ -66,11 +75,6 @@
(e: 'showShareList'): void;
}>();
const isSelectedShare = ref(false);
function visibleChange(val: boolean) {
isSelectedShare.value = val;
}
const internalShare = ref([
{
label: t('apiTestManagement.shareList'),
@ -82,16 +86,7 @@
},
]);
const shareList = ref([
{
label: '001',
value: '001',
},
{
label: '002',
value: '002',
},
]);
const shareList = ref<shareItem[]>();
const currentShare = ref<string>('');
//
@ -104,6 +99,14 @@
emit('showShareList');
}
const isSelectedShare = ref(false);
function visibleChange(val: boolean) {
isSelectedShare.value = val;
if (!val) {
currentShare.value = '';
}
}
function changeShare(item: SelectOptionData) {
currentShare.value = item.value as string;
switch (item.value) {
@ -118,8 +121,44 @@
break;
}
}
//
function copyShareLink(value: string) {}
function copyShareLink(value: string) {
if (isSupported) {
// dId
const url = window.location.href;
const dIdParam = `&docShareId=${value}`;
copy(`${url}${dIdParam}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
async function initShareList() {
try {
const res = await getSharePage({
current: 1,
pageSize: 10,
sort: {},
combineSearch: {
searchMode: 'AND',
conditions: [],
},
projectId: appStore.currentProjectId,
filter: {},
});
shareList.value = res.list;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initShareList();
});
</script>
<style scoped lang="less">
@ -149,7 +188,7 @@
}
}
.share-option-item {
padding: 3px 8px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
@apply flex w-full items-center justify-between;

View File

@ -14,7 +14,7 @@
</a-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiScenario.params.searchPlaceholder')"
:placeholder="t('common.searchByName')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@ -23,19 +23,21 @@
/>
</div>
<MsBaseTable v-bind="propsRes" no-disable :row-class="getRowClass" v-on="propsEvent">
<template #accessRestriction="{ record }">
{{ record.accessRestriction ? t('apiTestManagement.passwordView') : t('apiTestManagement.publicityView') }}
<template #isPrivate="{ record }">
{{ record.isPrivate ? t('apiTestManagement.passwordView') : t('apiTestManagement.publicityView') }}
</template>
<template #operation="{ record }">
<MsButton class="!mx-0" @click="viewLink(record)">
{{ t('apiTestManagement.viewLink') }}
</MsButton>
<a-tooltip :disabled="!!record.apiShareNum" :content="t('apiTestManagement.apiShareNumberTip')">
<MsButton class="!mx-0" :disabled="!record.apiShareNum" @click="viewLink(record)">
{{ t('apiTestManagement.viewLink') }}
</MsButton>
</a-tooltip>
<a-divider direction="vertical" :margin="8" />
<MsButton class="!mx-0" @click="editShare(record.id)">
<MsButton class="!mx-0" @click="editShare(record)">
{{ t('common.edit') }}
</MsButton>
<a-divider direction="vertical" :margin="8" />
<MsButton class="!mx-0" @click="deleteShare(record)">
<MsButton class="!mx-0" @click="deleteHandler(record)">
{{ t('common.delete') }}
</MsButton>
</template>
@ -47,6 +49,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
@ -54,12 +57,14 @@
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { deleteShare, getSharePage } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { useTableStore } from '@/store';
import { useAppStore, useTableStore } from '@/store';
import { characterLimit } from '@/utils';
import type { ShareDetail, shareItem } from '@/models/apiTest/management';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
@ -70,9 +75,10 @@
const { t } = useI18n();
const tableStore = useTableStore();
const appStore = useAppStore();
const emit = defineEmits<{
(e: 'editOrCreate', id?: string): void;
(e: 'editOrCreate', record?: ShareDetail): void;
}>();
const innerVisible = defineModel<boolean>('visible', {
@ -90,8 +96,14 @@
},
{
title: 'apiTestManagement.accessRestriction',
slotName: 'accessRestriction',
dataIndex: 'accessRestriction',
slotName: 'isPrivate',
dataIndex: 'isPrivate',
showDrag: true,
},
{
title: 'apiTestManagement.apiShareNum',
slotName: 'apiShareNum',
dataIndex: 'apiShareNum',
showDrag: true,
},
{
@ -103,7 +115,7 @@
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
width: 200,
showDrag: true,
},
{
@ -129,30 +141,45 @@
},
];
const { propsRes, propsEvent, loadList, setKeyword } = useTable(undefined, {
tableKey: TableKeyEnum.SYSTEM_RESOURCE_POOL_CAPACITY,
scroll: { y: 'auto' },
selectable: false,
showSetting: true,
heightUsed: 310,
showSelectAll: false,
});
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getSharePage,
{
tableKey: TableKeyEnum.SYSTEM_RESOURCE_POOL_CAPACITY,
scroll: { x: '100%' },
selectable: false,
showSetting: true,
heightUsed: 310,
showSelectAll: false,
},
(item) => ({
...item,
deadline: item.deadline ? dayjs(item.deadline).format('YYYY-MM-DD HH:mm:ss') : '-',
})
);
//
function viewLink(record: any) {
function viewLink(record: shareItem) {
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
dId: record.id,
pId: record.projectId,
docShareId: record.id,
});
}
//
function editShare(id: string) {
emit('editOrCreate', id);
function editShare(record: ShareDetail) {
emit('editOrCreate', record);
}
const keyword = ref<string>('');
function searchList() {
setLoadListParams({
keyword: keyword.value,
projectId: appStore.currentProjectId,
});
loadList();
}
//
function deleteShare(record: any) {
function deleteHandler(record: shareItem) {
openModal({
type: 'error',
title: t('common.deleteConfirmTitle', { name: characterLimit(record.name) }),
@ -164,8 +191,13 @@
maskClosable: false,
onBeforeOk: async () => {
try {
Message.success(t('caseManagement.featureCase.deleteSuccess'));
if (record.id) {
await deleteShare(record.id);
Message.success(t('caseManagement.featureCase.deleteSuccess'));
searchList();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
@ -173,16 +205,21 @@
});
}
const keyword = ref<string>('');
function searchList() {}
function getRowClass(record: any) {
return record.expired ? 'grey-row-class' : '';
function getRowClass(record: shareItem) {
return record.invalid ? 'grey-row-class' : '';
}
onMounted(() => {
searchList();
watch(
() => innerVisible.value,
(val) => {
if (val) {
searchList();
}
}
);
defineExpose({
searchList,
});
await tableStore.initColumn(TableKeyEnum.SYSTEM_RESOURCE_POOL_CAPACITY, columns, 'drawer');

View File

@ -1,7 +1,7 @@
<template>
<div>
<template v-if="!props.isModal">
<div v-if="!props.readOnly && !props.trash" class="mb-[8px] flex items-center gap-[8px]">
<div v-if="!props.readOnly && !props.trash && !props.docShareId" class="mb-[8px] flex items-center gap-[8px]">
<a-button
v-permission="['PROJECT_API_DEFINITION:READ+ADD']"
type="primary"
@ -34,21 +34,26 @@
:folder-name="t('apiTestManagement.allApi')"
:all-count="allFileCount"
:active-folder="selectedKeys[0] as string"
:show-expand-api="!props.readOnly && !props.trash"
:show-expand-api="!props.readOnly && !props.trash && !props.docShareId"
@set-active-folder="setActiveFolder"
@change-api-expand="changeApiExpand"
@selected-protocols-change="selectedProtocolsChange"
>
<template #expandRight>
<popConfirm
v-if="hasAnyPermission(['PROJECT_API_DEFINITION:READ+ADD']) && !props.readOnly && !props.trash"
v-if="
hasAnyPermission(['PROJECT_API_DEFINITION:READ+ADD']) &&
!props.readOnly &&
!props.trash &&
!props.docShareId
"
mode="add"
:all-names="rootModulesName"
parent-id="NONE"
:add-module-api="addModule"
@add-finish="handleAddFinish"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsButton v-if="!props.docShareId" type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
@ -56,6 +61,11 @@
/>
</MsButton>
</popConfirm>
<a-tooltip v-if="props.docShareId && shareDetailInfo?.allowExport" :content="t('common.export')">
<MsButton type="icon" status="secondary" class="!mr-[4px] p-[4px]" @click="changeApiExpand">
<MsIcon type="icon-icon_top-align_outlined" :size="16" @click="exportShare" />
</MsButton>
</a-tooltip>
</template>
</TreeFolderAll>
</template>
@ -162,6 +172,8 @@
getModuleCount,
getModuleTree,
getModuleTreeOnlyModules,
getShareModuleCount,
getShareModuleTree,
getTrashModuleCount,
getTrashModuleTree,
moveModule,
@ -173,11 +185,11 @@
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { characterLimit, mapTree } from '@/utils';
import { characterLimit, filterTree, mapTree, TreeNode } from '@/utils';
import { getLocalStorage } from '@/utils/local-storage';
import { hasAnyPermission } from '@/utils/permission';
import { ApiDefinitionGetModuleParams } from '@/models/apiTest/management';
import { ApiDefinitionGetModuleParams, ShareDetailType } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
import { ProtocolKeyEnum } from '@/enums/apiEnum';
@ -189,6 +201,7 @@
activeNodeId?: string | number; // id
isModal?: boolean; //
trash?: boolean; //
docShareId?: string; //
}>(),
{
activeModule: 'all',
@ -207,6 +220,8 @@
'updateApiNode',
'deleteNode',
'execute',
'openCurrentNode',
'exportShare',
]);
const appStore = useAppStore();
@ -253,14 +268,14 @@
const virtualListProps = computed(() => {
if (props.readOnly || props.isModal) {
return {
height: 'calc(60vh - 190px)',
height: props.docShareId ? 'calc(60vh - 150px)' : 'calc(60vh - 190px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
};
}
return {
height: 'calc(100vh - 273px)',
height: props.docShareId ? 'calc(100vh - 233px)' : 'calc(100vh - 273px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
@ -268,7 +283,7 @@
});
const moduleKeyword = ref(''); //
const folderTree = ref<ModuleTreeNode[]>([]);
const folderTree = ref<TreeNode<ModuleTreeNode>[]>([]);
const focusNodeKey = ref<string | number>('');
const selectedKeys = ref<Array<string | number>>([props.activeModule]);
const loading = ref(false);
@ -340,6 +355,29 @@
protocols: selectedProtocols.value,
moduleIds: [],
});
//
const shareDetailInfo = inject<Ref<ShareDetailType>>('shareDetailInfo');
const apiNodes = ref<TreeNode<ModuleTreeNode>[]>([]);
const currentNode = ref<TreeNode<ModuleTreeNode> | null>(null);
//
const setCurrentNode = (id: string, isSelectedNode = false) => {
currentNode.value = apiNodes.value.find((node) => node.id === id) || null;
selectedKeys.value = [id];
emit('openCurrentNode', currentNode.value, apiNodes.value, isSelectedNode);
};
const getTreeNodeList = (nodes: TreeNode<ModuleTreeNode>[]) => {
nodes.forEach((node: TreeNode<ModuleTreeNode>) => {
if (node.type === 'API') {
apiNodes.value.push(node);
}
if (node.children) {
getTreeNodeList(node.children);
}
});
};
async function initModuleCount(params: ApiDefinitionGetModuleParams) {
try {
lastModuleCountParam.value = params;
@ -378,8 +416,45 @@
emit('folderNodeSelect', _selectedKeys, offspringIds);
} else if (node.type === 'API') {
emit('clickApiNode', node);
if (props.docShareId) {
setCurrentNode(node.id, true);
}
}
}
// count
async function initShareModuleCount(params: ApiDefinitionGetModuleParams) {
try {
modulesCount.value = await getShareModuleCount({
...params,
shareId: props.docShareId,
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function initShareModuleTree() {
await initShareModuleCount(lastModuleCountParam.value);
let res;
res = await getShareModuleTree({
keyword: '',
protocols: selectedProtocols.value,
projectId: appStore.currentProjectId,
moduleIds: [],
});
res = mapTree<ModuleTreeNode>(res, (node) => ({
...node,
count: modulesCount.value[node.id] || 0,
draggable: node.id !== 'root' && !(props.readOnly || props.isModal),
disabled: props.readOnly || props.isModal ? node.id === selectedKeys.value[0] : false,
hideMoreAction: node.id === 'root' || !!props.docShareId,
}));
// count0 MODULE
res = filterTree(res, (node) => !(node.count === 0 && node.type === 'MODULE'));
return res;
}
/**
* 初始化模块树
@ -389,7 +464,14 @@
try {
loading.value = true;
let res;
if (props.trash) {
if (props.docShareId) {
res = await initShareModuleTree();
folderTree.value = res;
getTreeNodeList(folderTree.value);
if (apiNodes.value.length) {
setCurrentNode(apiNodes.value[0].id);
}
} else if (props.trash) {
res = await getTrashModuleTree({
//
keyword: '',
@ -429,7 +511,7 @@
disabled: e.id === selectedKeys.value[0],
};
});
} else {
} else if (!props.docShareId) {
folderTree.value = mapTree<ModuleTreeNode>(res, (e, fullPath) => {
//
nodePathObj[e.id] = {
@ -452,7 +534,9 @@
console.log(error);
} finally {
loading.value = false;
initModuleCount(lastModuleCountParam.value);
if (!props.docShareId) {
initModuleCount(lastModuleCountParam.value);
}
}
}
@ -462,6 +546,36 @@
initModules();
}
// ID
const getPreviousApiId = () => {
if (!currentNode.value) return null;
const index = apiNodes.value.indexOf(currentNode.value);
return index > 0 ? apiNodes.value[index - 1].id : null;
};
// ID
const getNextApiId = () => {
if (!currentNode.value) return null;
const index = apiNodes.value.indexOf(currentNode.value);
return index < apiNodes.value.length - 1 ? apiNodes.value[index + 1].id : null;
};
//
const previousApi = () => {
const previousId = getPreviousApiId();
if (previousId) {
setCurrentNode(previousId);
}
};
//
const nextApi = () => {
const nextId = getNextApiId();
if (nextId) {
setCurrentNode(nextId);
}
};
watch(
() => props.isExpandAll,
(val) => {
@ -630,6 +744,11 @@
}
}
//
function exportShare() {
emit('exportShare');
}
onBeforeMount(() => {
initProtocolList();
});
@ -642,6 +761,9 @@
refresh,
initModuleCount,
setActiveFolder,
setCurrentNode,
previousApi,
nextApi,
});
</script>

View File

@ -7,6 +7,7 @@
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeNodeId"
:doc-share-id="docShareId"
@init="handleModuleInit"
@new-api="newApi"
@import="importDrawerVisible = true"
@ -16,9 +17,10 @@
@update-api-node="handleUpdateApiNode"
@delete-node="handleDeleteApiFromModuleTree"
@execute="handleExecute"
@open-current-node="openCurrentNode"
/>
</div>
<div class="flex-1">
<div v-if="!docShareId" class="flex-1">
<a-divider class="!my-0 !mb-0" />
<div class="case h-[40px] !px-[24px]" @click="setActiveFolder('recycle')">
<div class="flex items-center" :class="getActiveClass('recycle')">
@ -33,6 +35,7 @@
<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"
@ -46,6 +49,7 @@
/>
</div>
<management
v-if="!docShareId"
ref="managementRef"
:module-tree="folderTree"
:active-module="activeModule"
@ -54,9 +58,55 @@
@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"
/>
</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>
</MsCard>
</template>
@ -66,6 +116,7 @@
*/
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';
@ -73,12 +124,17 @@
import importApi from './components/import.vue';
import management from './components/management/index.vue';
import moduleTree from './components/moduleTree.vue';
import ApiSharePreview from '@/views/api-test/management/components/management/api/apiSharePreview.vue';
import { getTrashModuleCount } from '@/api/modules/api-test/management';
import { getProtocolList } from '@/api/modules/api-test/common';
import { checkSharePsd, getTrashModuleCount, 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 { ApiDefinitionGetModuleParams } from '@/models/apiTest/management';
import { ApiDefinitionGetModuleParams, ShareDetailType } from '@/models/apiTest/management';
import { ModuleTreeNode } from '@/models/common';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
@ -86,6 +142,8 @@
const route = useRoute();
const { t } = useI18n();
const router = useRouter();
const docCheckStore = useDocShareCheckStore();
const userStore = useUserStore();
const activeModule = ref<string>('all');
const folderTree = ref<ModuleTreeNode[]>([]);
@ -96,7 +154,6 @@
const activeNodeId = ref<string | number>('all');
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const managementRef = ref<InstanceType<typeof management>>();
function newApi() {
importDrawerVisible.value = false;
managementRef.value?.newTab();
@ -111,7 +168,6 @@
function handleApiNodeClick(node: ModuleTreeNode) {
managementRef.value?.newTab(node);
}
function setActiveApi(params: RequestParam) {
if (params.id === 'all') {
// tab api
@ -121,19 +177,36 @@
}
}
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;
}
const docShareId = ref<string>(route.query.docShareId as string);
const recycleModulesCount = ref(0);
async function selectRecycleCount() {
const res = await getTrashModuleCount({
projectId: appStore.currentProjectId,
keyword: '',
moduleIds: [],
protocols: selectedProtocols.value,
});
recycleModulesCount.value = res.all;
if (!docShareId.value) {
const res = await getTrashModuleCount({
projectId: appStore.currentProjectId,
keyword: '',
moduleIds: [],
protocols: selectedProtocols.value,
});
recycleModulesCount.value = res.all;
}
}
function handleModuleInit(tree: ModuleTreeNode[], _protocols: string[], pathMap: Record<string, any>) {
@ -198,11 +271,129 @@
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 shareDetailInfo = ref<ShareDetailType>();
const currentNode = ref();
//
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('setActiveApi', setActiveApi);
provide('refreshModuleTree', refreshModuleTree);
provide('refreshModuleTreeCount', refreshModuleTreeCount);
provide('folderTreePathMap', folderTreePathMap.value);
provide('docShareId', docShareId.value);
provide('shareDetailInfo', shareDetailInfo.value);
</script>
<style lang="less" scoped>
@ -258,4 +449,21 @@
}
}
}
.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

@ -180,6 +180,10 @@ export default {
'apiTestManagement.interfaceRange': 'Interface range',
'apiTestManagement.effectiveTime': 'Effective time',
'apiTestManagement.passwordAccess': 'Password access',
'apiTestManagement.sharePasswordPlaceholder': 'Please enter the share password',
'apiTestManagement.apiShareNum': 'Api number',
'apiTestManagement.apiShareNumberTip': 'The number of shared interfaces is 0, please check!',
'apiTestManagement.apiSharePsdError': 'Password error!',
'apiTestManagement.allowExport': 'Allow export',
'apiTestManagement.pleaseEnterName': 'Please enter name',
'apiTestManagement.module': 'Module',

View File

@ -173,6 +173,10 @@ export default {
'apiTestManagement.interfaceRange': '接口范围',
'apiTestManagement.effectiveTime': '有效时间',
'apiTestManagement.passwordAccess': '密码访问',
'apiTestManagement.sharePasswordPlaceholder': '请输入分享密码',
'apiTestManagement.apiShareNum': '接口数量',
'apiTestManagement.apiShareNumberTip': '分享的接口数量为0请检查',
'apiTestManagement.apiSharePsdError': '密码错误!',
'apiTestManagement.allowExport': '允许导出',
'apiTestManagement.pleaseEnterName': '请输入名称',
'apiTestManagement.module': '模块',