feat(接口调试): 接口调试交互优化&参数调整&问题修复

This commit is contained in:
baiqi 2024-02-22 11:57:05 +08:00 committed by Craftsman
parent 9072523eb5
commit 553627c755
19 changed files with 363 additions and 90 deletions

View File

@ -42,10 +42,15 @@
:class="props.inputClass"
placeholder=" "
:max-tag-count="1"
:size="props.inputSize"
readonly
>
<template #tag="{ data }">
<MsTag :closable="!data.label.includes('+')" class="m-0 p-0" @close="() => handleClose(data)">
<MsTag
:size="props.tagSize"
class="m-0 border-none p-0"
:self-style="{ backgroundColor: 'transparent !important' }"
>
{{ data.label }}
</MsTag>
</template>
@ -56,6 +61,7 @@
<a-input
v-model:model-value="inputFileName"
:class="props.inputClass"
:size="props.inputSize"
allow-clear
readonly
@clear="handleFileClear"
@ -78,7 +84,7 @@
import { useVModel } from '@vueuse/core';
import { TagData } from '@arco-design/web-vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
@ -99,7 +105,9 @@
fileList: MsFileItem[];
multiple?: boolean;
inputClass?: string;
fields: {
inputSize?: 'small' | 'medium' | 'large' | 'mini';
tagSize?: Size;
fields?: {
id: string;
name: string;
};
@ -225,4 +233,13 @@
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
:deep(.arco-input-tag-inner) {
@apply flex items-center;
.arco-input-tag-tag {
@apply !my-0 !bg-transparent;
max-width: calc(100% - 50px);
}
}
</style>

View File

@ -619,7 +619,6 @@
() => {
if (innerVisible.value) {
searchCase();
resetSelector();
initModules();
}
}

View File

@ -96,7 +96,7 @@
blockNode?: boolean; //
showLine?: boolean; // 线
defaultExpandAll?: boolean; //
selectable?: boolean; //
selectable?: boolean | ((node: MsTreeNodeData, info: { level: number; isLeaf: boolean }) => boolean); //
fieldNames?: MsTreeFieldNames; //
focusNodeKey?: string | number; // key
selectedKeys?: Array<string | number>; // key

View File

@ -11,6 +11,7 @@
:max-tag-count="props.maxTagCount"
:readonly="props.readonly"
:class="props.class"
:size="props.size"
@press-enter="tagInputEnter"
@blur="tagInputBlur"
@clear="emit('clear')"
@ -50,6 +51,7 @@
maxLength?: number;
readonly?: boolean;
class?: string;
size?: 'small' | 'large' | 'medium' | 'mini';
}>(),
{
retainInputValue: true,
@ -57,6 +59,7 @@
allowClear: true,
maxLength: 64,
class: '',
size: 'medium',
}
);
const emit = defineEmits(['update:modelValue', 'update:inputValue', 'change', 'clear']);

View File

@ -117,4 +117,5 @@ export default {
'common.validateSuccess': 'Validate success',
'common.to': 'To',
'common.tip': 'Tips',
'common.stay': 'Stay',
};

View File

@ -120,4 +120,5 @@ export default {
'common.validateSuccess': '验证成功',
'common.to': '至',
'common.tip': '温馨提示',
'common.stay': '留下',
};

View File

@ -64,6 +64,8 @@ export type ExecuteRequestFormBodyFormValue = ExecuteRequestCommonParam & {
fileId: string;
fileName: string;
local: boolean; // 是否是本地上传的文件
fileAlias: string; // 文件别名
delete: boolean; // 是否删除
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
}[];
contentType?: RequestContentTypeEnum & string;
@ -258,8 +260,14 @@ export interface ExecuteCommonChild {
// 执行请求-认证配置
export interface ExecuteAuthConfig {
authType: RequestAuthType;
basicAuth: {
password: string;
userName: string;
};
digestAuth: {
password: string;
userName: string;
};
}
// 执行请求- body 配置-文本格式的 body
export interface ExecuteValueBody {
@ -321,7 +329,7 @@ export interface SaveDebugParams {
export interface UpdateDebugParams extends Partial<SaveDebugParams> {
id: string;
deleteFileIds?: string[];
unLinkRefIds?: string[];
unLinkFileIds?: string[];
}
// 更新模块入参
export interface UpdateDebugModule {

View File

@ -1,7 +1,12 @@
<template>
<MsTag
v-if="props.isTag"
:self-style="{ border: `1px solid ${methodColor}`, color: methodColor, backgroundColor: 'white' }"
:self-style="{
border: `1px solid ${props.tagBackgroundColor || methodColor}`,
color: props.tagTextColor || methodColor,
backgroundColor: props.tagBackgroundColor || 'white',
}"
:size="props.tagSize"
>
{{ props.method }}
</MsTag>
@ -9,14 +14,23 @@
</template>
<script setup lang="ts">
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import { RequestMethods } from '@/enums/apiEnum';
const props = defineProps<{
const props = withDefaults(
defineProps<{
method: RequestMethods;
isTag?: boolean; //
}>();
isTag?: boolean;
tagSize?: Size;
tagBackgroundColor?: string;
tagTextColor?: string;
}>(),
{
isTag: false,
tagSize: 'medium',
}
);
const colorMaps = [
{

View File

@ -133,7 +133,9 @@
id: 'fileId',
name: 'fileName',
}"
input-class="param-input"
input-class="param-input h-[24px]"
input-size="small"
tag-size="small"
@change="(files) => handleFileChange(files, record)"
/>
<MsParamsInput

View File

@ -1,15 +1,15 @@
<template>
<div class="h-full rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]">
<div class="mb-[8px]">{{ t('apiTestDebug.authType') }}</div>
<a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]" @change="authTypeChange">
<a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]">
<a-radio :value="RequestAuthType.NONE">No Auth</a-radio>
<a-radio :value="RequestAuthType.BASIC">Basic Auth</a-radio>
<a-radio :value="RequestAuthType.DIGEST">Digest Auth</a-radio>
</a-radio-group>
<a-form v-if="authForm.authType !== 'NONE'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form v-if="authForm.authType === 'BASIC'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form-item :label="t('apiTestDebug.username')">
<a-input
v-model:model-value="authForm.userName"
v-model:model-value="authForm.basicAuth.userName"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
:max-length="255"
@ -17,7 +17,25 @@
</a-form-item>
<a-form-item :label="t('apiTestDebug.password')">
<a-input-password
v-model:model-value="authForm.password"
v-model:model-value="authForm.basicAuth.password"
autocomplete="new-password"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
/>
</a-form-item>
</a-form>
<a-form v-else-if="authForm.authType == 'DIGEST'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form-item :label="t('apiTestDebug.username')">
<a-input
v-model:model-value="authForm.digestAuth.userName"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
:max-length="255"
/>
</a-form-item>
<a-form-item :label="t('apiTestDebug.password')">
<a-input-password
v-model:model-value="authForm.digestAuth.password"
autocomplete="new-password"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
@ -55,13 +73,6 @@
},
{ deep: true }
);
function authTypeChange(val: string | number | boolean) {
if (val === 'none') {
authForm.value.userName = '';
authForm.value.password = '';
}
}
</script>
<style lang="less" scoped></style>

View File

@ -57,7 +57,10 @@
v-model:file-list="fileList"
mode="input"
:multiple="false"
:default-file-list="[innerParams.binaryBody.file]"
:fields="{
id: 'fileId',
name: 'fileName',
}"
@change="handleFileChange"
/>
</div>
@ -111,6 +114,7 @@
import { requestBodyTypeMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ExecuteBody, ExecuteRequestFormBodyFormValue } from '@/models/apiTest/debug';
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
@ -127,6 +131,7 @@
(e: 'change'): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
@ -155,6 +160,7 @@
if (!props.uploadTempFileApi) return;
try {
if (fileList.value[0]?.local) {
appStore.showLoading();
const res = await props.uploadTempFileApi(fileList.value[0].file);
innerParams.value.binaryBody.file = {
...fileList.value[0],
@ -162,6 +168,7 @@
fileName: fileList.value[0]?.name || '',
local: true,
};
appStore.hideLoading();
} else {
innerParams.value.binaryBody.file = {
...fileList.value[0],

View File

@ -1,8 +1,17 @@
<template>
<div class="flex h-full flex-col">
<a-empty
v-if="pluginError && !isHttpProtocol"
:description="t('apiTestDebug.noPlugin')"
class="h-[200px] items-center justify-center"
>
<template #image>
<MsIcon type="icon-icon_plugin_outlined" size="48" />
</template>
</a-empty>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<div class="px-[24px] pt-[16px]">
<div class="mb-[8px] flex items-center justify-between">
<div class="flex flex-1">
<div class="flex flex-1 items-center gap-[16px]">
<a-select
v-if="requestVModel.isNew"
v-model:model-value="requestVModel.protocol"
@ -11,11 +20,18 @@
class="mr-[4px] w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)"
/>
<div v-else class="flex items-center gap-[4px]">
<apiMethodName
v-else
:method="(requestVModel.protocol as RequestMethods)"
class="mr-[16px] flex h-[30px] items-center"
tag-background-color="rgb(var(--link-7))"
tag-text-color="white"
is-tag
class="flex items-center"
/>
<div v-if="!isHttpProtocol">
{{ requestVModel.label }}
</div>
</div>
<a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect
v-model:model-value="requestVModel.method"
@ -223,6 +239,7 @@
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import debugAuth from './auth.vue';
import postcondition from './postcondition.vue';
@ -246,6 +263,7 @@
import { ModuleTreeNode } from '@/models/common';
import { RequestComposition, RequestMethods, RequestParamsType } from '@/enums/apiEnum';
import { parseRequestBodyFiles } from '../utils';
import { Api } from '@form-create/arco-design';
// Http
@ -440,7 +458,16 @@
}
}
const pluginError = ref(false);
async function initPluginScript() {
const pluginId = protocolOptions.value.find((e) => e.value === requestVModel.value.protocol)?.pluginId;
if (!pluginId) {
Message.warning(t('apiTestDebug.noPluginTip'));
pluginError.value = true;
return;
}
pluginError.value = false;
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
setPluginFormData();
//
@ -448,9 +475,7 @@
}
try {
pluginLoading.value = true;
const res = await getPluginScript(
protocolOptions.value.find((e) => e.value === requestVModel.value.protocol)?.pluginId || ''
);
const res = await getPluginScript(pluginId);
pluginScriptMap.value[requestVModel.value.protocol] = res;
setPluginFormData();
} catch (error) {
@ -578,6 +603,7 @@
temporaryResponseMap[data.reportId] = data.taskResult;
}
}
websocket.value?.close();
});
}
@ -606,39 +632,20 @@
);
function makeRequestParams() {
const { formDataBody, wwwFormBody, binaryBody } = requestVModel.value.body;
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol
)?.polymorphicName; //
const realFormDataBodyValues = formDataBody.formValues.filter((e, i) => i !== formDataBody.formValues.length - 1); //
const realWwwFormBodyValues = wwwFormBody.formValues.filter((e, i) => i !== wwwFormBody.formValues.length - 1); //
const uploadFileIds: string[] = [];
const linkFileIds: string[] = [];
//
for (let i = 0; i < formDataBody.formValues.length; i++) {
const item = formDataBody.formValues[i];
if (item.paramType === RequestParamsType.FILE) {
if (item.files) {
for (let j = 0; j < item.files.length; j++) {
const file = item.files[j];
if (file.isLocal) {
uploadFileIds.push(file.fileId);
} else {
linkFileIds.push(file.fileId);
}
}
}
}
}
if (binaryBody) {
if (binaryBody.file?.isLocal) {
uploadFileIds.push(binaryBody.file.fileId);
} else if (binaryBody.file?.fileId) {
linkFileIds.push(binaryBody.file.fileId);
}
}
let parseRequestBodyResult;
let requestParams;
if (isHttpProtocol.value) {
const realFormDataBodyValues = formDataBody.formValues.filter((e, i) => i !== formDataBody.formValues.length - 1); //
const realWwwFormBodyValues = wwwFormBody.formValues.filter((e, i) => i !== wwwFormBody.formValues.length - 1); //
parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.body,
requestVModel.value.uploadFileIds,
requestVModel.value.linkFileIds
);
requestParams = {
authConfig: requestVModel.value.authConfig,
body: {
@ -672,10 +679,11 @@
id: requestVModel.value.id.toString(),
reportId: reportId.value,
environmentId: '',
name: saveModalForm.value.name || requestVModel.value.name,
name: requestVModel.value.isNew ? saveModalForm.value.name : requestVModel.value.name,
moduleId: requestVModel.value.isNew ? saveModalForm.value.moduleId : requestVModel.value.moduleId,
request: {
...requestParams,
name: saveModalForm.value.name || requestVModel.value.name,
name: requestVModel.value.isNew ? saveModalForm.value.name : requestVModel.value.name,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
@ -689,8 +697,7 @@
},
],
},
uploadFileIds,
linkFileIds,
...parseRequestBodyResult,
projectId: appStore.currentProjectId,
};
}
@ -709,8 +716,6 @@
// eslint-disable-next-line no-console
console.log(error);
requestVModel.value.executeLoading = false;
} finally {
websocket.value?.close();
}
} else {
//
@ -723,8 +728,6 @@
// eslint-disable-next-line no-console
console.log(error);
requestVModel.value.executeLoading = false;
} finally {
websocket.value?.close();
}
} else {
requestVModel.value.activeTab = RequestComposition.PLUGIN;
@ -741,7 +744,6 @@
saveLoading.value = true;
await props.updateApi({
...makeRequestParams(),
...saveModalForm.value,
protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
deleteFileIds: [], // TODO:
@ -749,8 +751,7 @@
});
Message.success(t('common.updateSuccess'));
requestVModel.value.unSaved = false;
requestVModel.value.name = saveModalForm.value.name;
requestVModel.value.label = saveModalForm.value.name;
emit('addDone');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -778,13 +779,13 @@
requestVModel.value.unSaved = false;
requestVModel.value.name = saveModalForm.value.name;
requestVModel.value.label = saveModalForm.value.name;
} else {
updateDebug();
}
saveLoading.value = false;
saveModalVisible.value = false;
done(true);
emit('addDone');
} else {
updateDebug();
}
} catch (error) {
saveLoading.value = false;
}

View File

@ -0,0 +1,102 @@
import { ExecuteBody } from '@/models/apiTest/debug';
import { RequestParamsType } from '@/enums/apiEnum';
export default {};
export interface ParseResult {
uploadFileIds: string[];
linkFileIds: string[];
deleteFileIds: string[];
unLinkFileIds: string[];
}
/**
* body
* @param body body
*/
export function parseRequestBodyFiles(
body: ExecuteBody,
saveUploadFileIds?: string[],
saveLinkFileIds?: string[]
): ParseResult {
const { formDataBody, binaryBody } = body;
const uploadFileIds = new Set<string>(); // 存储本地上传的文件 id 集合
const linkFileIds = new Set<string>(); // 存储关联文件 id 集合
const tempSaveUploadFileIds = new Set<string>(); // 临时存储 body 内已保存的上传文件 id 集合,用于对比 saveUploadFileIds 以判断有哪些文件被删除
const tempSaveLinkFileIds = new Set<string>(); // 临时存储 body 内已保存的关联文件 id 集合,用于对比 saveLinkFileIds 以判断有哪些文件被取消关联
// 获取上传文件和关联文件
for (let i = 0; i < formDataBody.formValues.length; i++) {
const item = formDataBody.formValues[i];
if (item.paramType === RequestParamsType.FILE) {
if (item.files) {
for (let j = 0; j < item.files.length; j++) {
const file = item.files[j];
if (file.local) {
// 本地上传的文件
if (saveUploadFileIds) {
// 如果有已保存的上传文件id集合
if (saveUploadFileIds.includes(file.fileId)) {
// 当前文件是已保存的文件,存入 tempSaveUploadFileIds
tempSaveUploadFileIds.add(file.fileId);
} else {
// 当前文件不是已保存的文件,存入 uploadFileIds
uploadFileIds.add(file.fileId);
}
} else {
// 没有已保存的文件id集合直接存入 uploadFileIds
uploadFileIds.add(file.fileId);
}
} else if (saveLinkFileIds) {
// 如果有已保存的关联文件id集合
if (saveLinkFileIds.includes(file.fileId)) {
// 当前文件是已保存的文件,存入
tempSaveLinkFileIds.add(file.fileId);
} else {
// 当前文件不是已保存的文件,存入 uploadFileIds
linkFileIds.add(file.fileId);
}
} else {
// 关联的文件
linkFileIds.add(file.fileId);
}
}
}
}
}
if (binaryBody && binaryBody.file) {
const { fileId } = binaryBody.file;
if (binaryBody.file?.local) {
if (saveUploadFileIds) {
// 如果有已保存的上传文件id集合
if (saveUploadFileIds.includes(fileId)) {
// 当前文件是已保存的文件,存入 tempSaveUploadFileIds
tempSaveUploadFileIds.add(fileId);
} else {
// 当前文件不是已保存的文件,存入 uploadFileIds
uploadFileIds.add(fileId);
}
} else {
// 没有已保存的文件id集合直接存入 uploadFileIds
uploadFileIds.add(fileId);
}
} else if (saveLinkFileIds) {
// 如果有已保存的关联文件id集合
if (saveLinkFileIds.includes(fileId)) {
// 当前文件是已保存的文件,存入
tempSaveLinkFileIds.add(fileId);
} else {
// 当前文件不是已保存的文件,存入 uploadFileIds
linkFileIds.add(fileId);
}
} else {
// 关联的文件
linkFileIds.add(fileId);
}
}
return {
uploadFileIds: Array.from(uploadFileIds),
linkFileIds: Array.from(linkFileIds),
deleteFileIds: saveUploadFileIds?.filter((id) => !tempSaveUploadFileIds.has(id)) || [], // 存储对比已保存的文件后,需要删除的文件 id 集合
unLinkFileIds: saveLinkFileIds?.filter((id) => !tempSaveLinkFileIds.has(id)) || [], // 存储对比已保存的文件后,需要取消关联的文件 id 集合
};
}

View File

@ -36,6 +36,7 @@
<a-divider class="my-[8px]" />
<a-spin class="h-[calc(100%-98px)] w-full" :loading="loading">
<MsTree
v-model:selected-keys="selectedKeys"
v-model:focus-node-key="focusNodeKey"
:data="folderTree"
:keyword="moduleKeyword"
@ -56,7 +57,7 @@
count: 'count',
}"
:draggable="true"
:selectable="false"
:selectable="nodeSelectable"
block-node
title-tooltip-position="left"
:allow-drop="allowDrop"
@ -141,8 +142,9 @@
const props = defineProps<{
isExpandAll?: boolean; //
activeNodeId?: string | number; // id
}>();
const emit = defineEmits(['init', 'clickApiNode', 'newApi', 'import', 'renameFinish']);
const emit = defineEmits(['init', 'clickApiNode', 'newApi', 'import', 'renameFinish', 'deleteFinish']);
const appStore = useAppStore();
const { t } = useI18n();
@ -178,9 +180,24 @@
const moduleKeyword = ref('');
const folderTree = ref<ModuleTreeNode[]>([]);
const selectedKeys = ref<(string | number)[]>([]);
const focusNodeKey = ref<string | number>('');
const loading = ref(false);
watch(
() => props.activeNodeId,
(val) => {
if (val) {
selectedKeys.value = [val];
}
}
);
function nodeSelectable(node: MsTreeNodeData) {
// api
return node.type === 'API';
}
function setFocusNodeKey(node: MsTreeNodeData) {
focusNodeKey.value = node.id || '';
}
@ -261,7 +278,9 @@
try {
await deleteDebugModule(node.id);
Message.success(t('apiTestDebug.deleteSuccess'));
initModules();
emit('deleteFinish', node);
await initModules();
initModuleCount();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -297,7 +316,9 @@
try {
await deleteDebug(node.id);
Message.success(t('apiTestDebug.deleteSuccess'));
initModules();
emit('deleteFinish', node);
await initModules();
initModuleCount();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -5,11 +5,13 @@
<template #first>
<moduleTree
ref="moduleTreeRef"
:active-node-id="activeDebug.id"
@init="(val) => (folderTree = val)"
@new-api="addDebugTab"
@click-api-node="openApiTab"
@import="importDrawerVisible = true"
@rename-finish="handleRenameFinish"
@delete-finish="handleDeleteFinish"
/>
</template>
<template #second>
@ -77,6 +79,7 @@
</template>
<script lang="ts" setup>
import { onBeforeRouteLeave } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue';
@ -91,6 +94,7 @@
import { addDebug, executeDebug, getDebugDetail, updateDebug, uploadTempFile } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { parseCurlScript } from '@/utils';
import { ExecuteBody } from '@/models/apiTest/debug';
@ -105,7 +109,10 @@
ResponseComposition,
} from '@/enums/apiEnum';
import { parseRequestBodyFiles } from '../components/utils';
const { t } = useI18n();
const { openModal } = useModal();
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const folderTree = ref<ModuleTreeNode[]>([]);
@ -181,9 +188,15 @@
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
@ -243,6 +256,10 @@
try {
loading.value = true;
const res = await getDebugDetail(apiInfo.id);
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body);
}
addDebugTab({
label: apiInfo.name,
...res,
@ -250,6 +267,7 @@
...res.request,
url: res.path,
name: res.name, // requestnamenull
...parseRequestBodyResult,
});
nextTick(() => {
// loading
@ -299,6 +317,53 @@
return tab;
});
}
function handleDeleteFinish(node: ModuleTreeNode) {
let index;
if (node.type === 'API') {
//
index = debugTabs.value.findIndex((tab) => tab.id === node.id);
} else {
//
index = debugTabs.value.findIndex((tab) => tab.moduleId === node.id);
}
if (index > -1) {
debugTabs.value.splice(index, 1);
if (activeDebug.value.id === node.id) {
// tabtab
if (debugTabs.value.length > 0) {
[activeDebug.value] = debugTabs.value;
} else {
addDebugTab();
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let isLeaving = false;
onBeforeRouteLeave((to, from, next) => {
if (!isLeaving && debugTabs.value.some((tab) => tab.unSaved)) {
isLeaving = true;
//
openModal({
type: 'warning',
title: t('common.tip'),
content: t('apiTestDebug.unsavedLeave'),
hideCancel: false,
cancelText: t('common.stay'),
okText: t('common.leave'),
onBeforeOk: async () => {
next();
},
onCancel: () => {
isLeaving = false;
},
});
} else {
next();
}
});
</script>
<style lang="less" scoped></style>

View File

@ -184,4 +184,8 @@ export default {
'apiTestDebug.importByCURL': 'Import cURL',
'apiTestDebug.importByCURLTip':
'Supports quick import of packet capture data from tools such as Chrome, Charles or Fiddler',
'apiTestDebug.noPluginTip': 'The plug-in has been uninstalled and plug-in details cannot be viewed.',
'apiTestDebug.noPlugin': 'Plugin has been uninstalled',
'apiTestDebug.unsavedLeave':
'The content of some tabs has not been saved. The unsaved content will be lost after leaving. Are you sure you want to leave?',
};

View File

@ -173,4 +173,7 @@ export default {
'apiTestDebug.closeOther': '关闭其他请求',
'apiTestDebug.importByCURL': '导入 cURL',
'apiTestDebug.importByCURLTip': '支持快速导入 Chrome、Charles 或 Fiddler 等工具中的抓包数据',
'apiTestDebug.noPluginTip': '该插件已卸载,无法查看插件详情',
'apiTestDebug.noPlugin': '插件已卸载',
'apiTestDebug.unsavedLeave': '有标签页的内容未保存,离开后后未保存的内容将丢失,确定要离开吗?',
};

View File

@ -192,9 +192,15 @@
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement

View File

@ -48,7 +48,7 @@
</template>
<template #receiver="{ record, dataIndex }">
<MsSelect
v-if="!record.children"
v-if="!record.children && hasAnyPermission(['PROJECT_MESSAGE:READ+ADD'])"
v-model:model-value="record.receivers"
v-model:loading="record.loading"
class="w-full"
@ -74,6 +74,12 @@
@remove="changeMessageReceivers(false, record, dataIndex as string)"
@popup-visible-change="changeMessageReceivers($event, record, dataIndex as string)"
/>
<MsTagGroup
v-else-if="!record.children && hasAnyPermission(['PROJECT_MESSAGE:READ'])"
is-string-tag
:tag-list="record.receivers?.map((e) => e.name) || []"
theme="outline"
/>
<span v-else></span>
</template>
<template #robot="{ record, dataIndex }">
@ -122,6 +128,7 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsSelect from '@/components/business/ms-select';
import MessagePreview from './messagePreview.vue';
@ -133,6 +140,7 @@
} from '@/api/modules/project-management/messageManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import type { MessageItem, ProjectRobotConfig, Receiver, RobotItem } from '@/models/projectManagement/message';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';