feat(接口调试): 接口调试交互优化&参数调整&问题修复
This commit is contained in:
parent
9072523eb5
commit
553627c755
|
@ -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>
|
||||
|
|
|
@ -619,7 +619,6 @@
|
|||
() => {
|
||||
if (innerVisible.value) {
|
||||
searchCase();
|
||||
resetSelector();
|
||||
initModules();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -117,4 +117,5 @@ export default {
|
|||
'common.validateSuccess': 'Validate success',
|
||||
'common.to': 'To',
|
||||
'common.tip': 'Tips',
|
||||
'common.stay': 'Stay',
|
||||
};
|
||||
|
|
|
@ -120,4 +120,5 @@ export default {
|
|||
'common.validateSuccess': '验证成功',
|
||||
'common.to': '至',
|
||||
'common.tip': '温馨提示',
|
||||
'common.stay': '留下',
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
password: string;
|
||||
userName: string;
|
||||
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 {
|
||||
|
|
|
@ -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<{
|
||||
method: RequestMethods;
|
||||
isTag?: boolean; // 是否展示为标签
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
method: RequestMethods;
|
||||
isTag?: boolean;
|
||||
tagSize?: Size;
|
||||
tagBackgroundColor?: string;
|
||||
tagTextColor?: string;
|
||||
}>(),
|
||||
{
|
||||
isTag: false,
|
||||
tagSize: 'medium',
|
||||
}
|
||||
);
|
||||
|
||||
const colorMaps = [
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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)"
|
||||
/>
|
||||
<apiMethodName
|
||||
v-else
|
||||
:method="(requestVModel.protocol as RequestMethods)"
|
||||
class="mr-[16px] flex h-[30px] items-center"
|
||||
/>
|
||||
<div v-else class="flex items-center gap-[4px]">
|
||||
<apiMethodName
|
||||
:method="(requestVModel.protocol as RequestMethods)"
|
||||
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;
|
||||
saveLoading.value = false;
|
||||
saveModalVisible.value = false;
|
||||
done(true);
|
||||
emit('addDone');
|
||||
} else {
|
||||
updateDebug();
|
||||
}
|
||||
saveLoading.value = false;
|
||||
saveModalVisible.value = false;
|
||||
done(true);
|
||||
emit('addDone');
|
||||
} catch (error) {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
|
|
|
@ -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 集合
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,8 +188,14 @@
|
|||
linkFileIds: [],
|
||||
authConfig: {
|
||||
authType: RequestAuthType.NONE,
|
||||
userName: '',
|
||||
password: '',
|
||||
basicAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
digestAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
@ -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, // request里面还有个name但是是null
|
||||
...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) {
|
||||
// 如果查看的tab被删除了,则切换到第一个tab
|
||||
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>
|
||||
|
|
|
@ -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?',
|
||||
};
|
||||
|
|
|
@ -173,4 +173,7 @@ export default {
|
|||
'apiTestDebug.closeOther': '关闭其他请求',
|
||||
'apiTestDebug.importByCURL': '导入 cURL',
|
||||
'apiTestDebug.importByCURLTip': '支持快速导入 Chrome、Charles 或 Fiddler 等工具中的抓包数据',
|
||||
'apiTestDebug.noPluginTip': '该插件已卸载,无法查看插件详情',
|
||||
'apiTestDebug.noPlugin': '插件已卸载',
|
||||
'apiTestDebug.unsavedLeave': '有标签页的内容未保存,离开后后未保存的内容将丢失,确定要离开吗?',
|
||||
};
|
||||
|
|
|
@ -192,8 +192,14 @@
|
|||
linkFileIds: [],
|
||||
authConfig: {
|
||||
authType: RequestAuthType.NONE,
|
||||
userName: '',
|
||||
password: '',
|
||||
basicAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
digestAuth: {
|
||||
userName: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue