feat(上传附件组件): 新增文件转存功能&部分高优先级bug解决
This commit is contained in:
parent
e3caf65b33
commit
569ed4c933
|
@ -64,7 +64,7 @@ module.exports = {
|
|||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-debugger': 2,
|
||||
'no-param-reassign': 0,
|
||||
'prefer-regex-literals': 0,
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
LocalExecuteApiDebugUrl,
|
||||
MoveDebugModuleUrl,
|
||||
TestMockUrl,
|
||||
TransferFileUrl,
|
||||
TransferOptionsUrl,
|
||||
UpdateApiDebugUrl,
|
||||
UpdateDebugModuleUrl,
|
||||
UploadTempFileUrl,
|
||||
|
@ -25,7 +27,7 @@ import {
|
|||
UpdateDebugModule,
|
||||
UpdateDebugParams,
|
||||
} from '@/models/apiTest/debug';
|
||||
import { DragSortParams, ModuleTreeNode, MoveModules } from '@/models/common';
|
||||
import { DragSortParams, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common';
|
||||
|
||||
// 获取模块树
|
||||
export function getDebugModules() {
|
||||
|
@ -101,3 +103,13 @@ export function testMock(key: string) {
|
|||
export function uploadTempFile(file: File) {
|
||||
return MSR.uploadFile({ url: UploadTempFileUrl }, { fileList: [file] }, 'file');
|
||||
}
|
||||
|
||||
// 文件转存
|
||||
export function transferFile(data: TransferFileParams) {
|
||||
return MSR.post({ url: TransferFileUrl, data });
|
||||
}
|
||||
|
||||
// 文件转存目录
|
||||
export function getTransferOptions(projectId: string) {
|
||||
return MSR.get({ url: TransferOptionsUrl, params: projectId });
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
RegisterByInviteUrl,
|
||||
ResetPasswordUrl,
|
||||
UpdateUserUrl,
|
||||
ValidInviteUrl,
|
||||
} from '@/api/requrls/setting/user';
|
||||
|
||||
import type { CommonList, TableQueryParams } from '@/models/common';
|
||||
|
@ -118,3 +119,8 @@ export function inviteUser(data: InviteUserParams) {
|
|||
export function registerByInvite(data: RegisterByInviteParams) {
|
||||
return MSR.post({ url: RegisterByInviteUrl, data });
|
||||
}
|
||||
|
||||
// 检查邀请链接是否过期
|
||||
export function validInvite(id: string) {
|
||||
return MSR.get({ url: ValidInviteUrl, params: id });
|
||||
}
|
||||
|
|
|
@ -13,3 +13,5 @@ export const GetDebugModulesUrl = '/api/debug/module/tree'; // 查询模块树
|
|||
export const DeleteDebugModuleUrl = '/api/debug/module/delete'; // 删除模块
|
||||
export const DragDebugUrl = '/api/debug/edit/pos'; // 拖拽调试节点
|
||||
export const UploadTempFileUrl = '/api/debug/upload/temp/file'; // 上传文件
|
||||
export const TransferOptionsUrl = '/api/debug/transfer/options'; // 文件转存目录
|
||||
export const TransferFileUrl = '/api/debug/transfer'; // 文件转存
|
||||
|
|
|
@ -30,3 +30,5 @@ export const GetProjectsUrl = '/system/user/get/project';
|
|||
export const RegisterByInviteUrl = '/system/user/register-by-invite';
|
||||
// 邀请用户
|
||||
export const InviteUserUrl = '/system/user/invite';
|
||||
// 检查邀请链接是否过期
|
||||
export const ValidInviteUrl = '/system/user/check-invite';
|
||||
|
|
|
@ -49,12 +49,13 @@
|
|||
const { t } = useI18n();
|
||||
|
||||
const innerFileList = defineModel<MsFileItem[]>('fileList', {
|
||||
required: true,
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const dropdownVisible = ref(false);
|
||||
|
||||
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
|
||||
fileItem.local = true;
|
||||
emit('change', _fileList, fileItem);
|
||||
nextTick(() => {
|
||||
// emit 完文件之后再关闭菜单
|
||||
|
|
|
@ -9,20 +9,21 @@
|
|||
{{ t('system.orgTemplate.addAttachment') }}
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
<MsUpload
|
||||
v-model:file-list="innerFileList"
|
||||
:limit="50"
|
||||
accept="none"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:limit="50"
|
||||
size-unit="MB"
|
||||
:multiple="props.multiple"
|
||||
class="w-full"
|
||||
@change="handleChange"
|
||||
>
|
||||
<template #upload-button>
|
||||
<a-button type="text" class="arco-dropdown-option !text-[var(--color-text-1)]">
|
||||
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
<a-button type="text" class="arco-dropdown-option !text-[var(--color-text-1)]">
|
||||
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}
|
||||
</a-button>
|
||||
</MsUpload>
|
||||
<a-button type="text" class="arco-dropdown-option !text-[var(--color-text-1)]" @click="associatedFile">
|
||||
<MsIcon type="icon-icon_link-copy_outlined" size="16" />
|
||||
{{ t('caseManagement.featureCase.associatedFile') }}
|
||||
|
@ -36,13 +37,26 @@
|
|||
</div>
|
||||
</a-form-item>
|
||||
<template v-else>
|
||||
<div v-if="props.multiple" class="flex w-full items-center gap-[4px]">
|
||||
<dropdownMenu v-model:file-list="innerFileList" @link-file="associatedFile" @change="handleChange" />
|
||||
<div v-if="props.multiple" class="flex w-full items-center">
|
||||
<dropdownMenu @link-file="associatedFile" @change="handleChange" />
|
||||
<saveAsFilePopover
|
||||
v-if="props.fileSaveAsSourceId"
|
||||
v-model:visible="saveFilePopoverVisible"
|
||||
:saving-file="savingFile"
|
||||
:file-id-key="props.fields.id"
|
||||
:file-save-as-api="props.fileSaveAsApi"
|
||||
:file-save-as-source-id="props.fileSaveAsSourceId"
|
||||
:file-module-options-api="props.fileModuleOptionsApi"
|
||||
@finish="handleSaveFileFinish"
|
||||
/>
|
||||
<a-popover
|
||||
v-model:popup-visible="inputFilesPopoverVisible"
|
||||
trigger="click"
|
||||
position="bottom"
|
||||
position="bl"
|
||||
:disabled="inputFiles.length === 0"
|
||||
content-class="ms-add-attachment-files-popover"
|
||||
arrow-class="hidden"
|
||||
:popup-offset="0"
|
||||
>
|
||||
<MsTagsInput
|
||||
v-model:model-value="inputFiles"
|
||||
|
@ -104,7 +118,7 @@
|
|||
</a-tooltip>
|
||||
<div v-if="file.local === true" class="flex items-center">
|
||||
<a-tooltip :content="t('ms.add.attachment.saveAs')">
|
||||
<MsButton type="text" status="secondary" class="!mr-0" @click="handleClose(file)">
|
||||
<MsButton type="text" status="secondary" class="!mr-0" @click="handleOpenSaveAs(file)">
|
||||
<MsIcon type="icon-icon_unloading" class="hover:text-[rgb(var(--primary-5))]" size="16" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
|
@ -132,7 +146,7 @@
|
|||
</a-popover>
|
||||
</div>
|
||||
<div v-else class="flex w-full items-center gap-[4px]">
|
||||
<dropdownMenu v-model:file-list="innerFileList" @link-file="associatedFile" @change="handleChange" />
|
||||
<dropdownMenu @link-file="associatedFile" @change="handleChange" />
|
||||
<a-input
|
||||
v-model:model-value="inputFileName"
|
||||
:class="props.inputClass"
|
||||
|
@ -163,22 +177,24 @@
|
|||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||
import type { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||
import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
|
||||
import dropdownMenu from './dropdownMenu.vue';
|
||||
import saveAsFilePopover from './saveAsFilePopover.vue';
|
||||
|
||||
import { getAssociatedFileListUrl } from '@/api/modules/case-management/featureCase';
|
||||
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { AssociatedList } from '@/models/caseManagement/featureCase';
|
||||
import { TableQueryParams } from '@/models/common';
|
||||
import { TableQueryParams, TransferFileParams } from '@/models/common';
|
||||
|
||||
import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mode: 'button' | 'input';
|
||||
mode?: 'button' | 'input';
|
||||
fileList: MsFileItem[]; // TODO:这里的文件含有组件内部定义的属性,应该继承MsFileItem类型并扩展声明组件定义的类型属性
|
||||
multiple?: boolean;
|
||||
inputClass?: string;
|
||||
|
@ -188,6 +204,9 @@
|
|||
id: string; // 自定义文件的 id 字段名,用于详情展示,接口返回的字段名
|
||||
name: string;
|
||||
};
|
||||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||
fileModuleOptionsApi?: (...args) => Promise<any>; // 文件转存目录下拉框接口
|
||||
}>(),
|
||||
{
|
||||
mode: 'button',
|
||||
|
@ -243,7 +262,6 @@
|
|||
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
|
||||
// 校验本地文件是否重复
|
||||
const isRepeat = _fileList.filter((item) => item.name === fileItem.name).length > 1;
|
||||
debugger;
|
||||
if (isRepeat) {
|
||||
Message.error(t('ms.add.attachment.repeatFileTip'));
|
||||
innerFileList.value = _fileList.reduce((prev: MsFileItem[], current: MsFileItem) => {
|
||||
|
@ -253,24 +271,21 @@
|
|||
}
|
||||
return prev;
|
||||
}, []);
|
||||
} else {
|
||||
innerFileList.value = _fileList.map((item) => ({ ...item, local: true }));
|
||||
if (props.multiple) {
|
||||
inputFiles.value = _fileList.map((item) => ({
|
||||
...item,
|
||||
value: item?.uid || '',
|
||||
label: item?.name || '',
|
||||
local: true,
|
||||
}));
|
||||
} else {
|
||||
inputFileName.value = fileItem.name || '';
|
||||
}
|
||||
emit('change', _fileList, { ...fileItem, local: true });
|
||||
nextTick(() => {
|
||||
// 在 emit 文件上去之后再关闭菜单
|
||||
buttonDropDownVisible.value = false;
|
||||
} else if (props.multiple) {
|
||||
innerFileList.value.push(fileItem);
|
||||
inputFiles.value.push({
|
||||
...fileItem,
|
||||
value: fileItem[props.fields.id] || fileItem.uid || '',
|
||||
label: fileItem[props.fields.name] || fileItem.name || '',
|
||||
});
|
||||
} else {
|
||||
inputFileName.value = fileItem.name || '';
|
||||
}
|
||||
emit('change', _fileList, { ...fileItem, local: true });
|
||||
nextTick(() => {
|
||||
// 在 emit 文件上去之后再关闭菜单
|
||||
buttonDropDownVisible.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function associatedFile() {
|
||||
|
@ -309,8 +324,9 @@
|
|||
);
|
||||
} else {
|
||||
// 单选文件
|
||||
innerFileList.value = fileResultList;
|
||||
inputFileName.value = fileResultList[0].name || '';
|
||||
const file = fileResultList[0];
|
||||
innerFileList.value = [{ ...file, fileId: file.uid || '', fileName: file.name || '' }];
|
||||
inputFileName.value = file.name || '';
|
||||
}
|
||||
emit('change', innerFileList.value);
|
||||
}
|
||||
|
@ -344,8 +360,38 @@
|
|||
innerFileList.value = [];
|
||||
emit('change', []);
|
||||
}
|
||||
|
||||
const saveFilePopoverVisible = ref(false);
|
||||
const savingFile = ref<MsFileItem>();
|
||||
|
||||
/**
|
||||
* 打开文件转存弹窗
|
||||
* @param item 点击转存的文件标签项
|
||||
*/
|
||||
function handleOpenSaveAs(item: TagData) {
|
||||
inputFilesPopoverVisible.value = false;
|
||||
// 这里先判定 uid 是否存在,存在则是刚上传的文件;否则是已保存过后的详情文件
|
||||
savingFile.value = innerFileList.value.find((file) => (file.uid || file[props.fields.id]) === item.value);
|
||||
saveFilePopoverVisible.value = true;
|
||||
}
|
||||
|
||||
function handleSaveFileFinish(fileId: string) {
|
||||
if (savingFile.value) {
|
||||
savingFile.value.fileId = fileId;
|
||||
savingFile.value.local = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ms-add-attachment-files-popover {
|
||||
padding: 16px;
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-list {
|
||||
@apply flex flex-col overflow-y-auto overflow-x-hidden;
|
||||
|
|
|
@ -8,4 +8,8 @@ export default {
|
|||
'ms.add.attachment.cancelAssociate': 'Disassociate',
|
||||
'ms.add.attachment.saveAs': 'Save',
|
||||
'ms.add.attachment.repeatFileTip': 'File already exists.',
|
||||
'ms.add.attachment.saveAsTitle': 'Please select the transfer directory',
|
||||
'ms.add.attachment.saveAsNamePlaceholder': 'Please enter file name',
|
||||
'ms.add.attachment.saveAsModulePlaceholder': 'Please select the transfer directory',
|
||||
'ms.add.attachment.saveAsSuccess': 'File transfer successful',
|
||||
};
|
||||
|
|
|
@ -8,4 +8,8 @@ export default {
|
|||
'ms.add.attachment.cancelAssociate': '取消关联',
|
||||
'ms.add.attachment.saveAs': '转存',
|
||||
'ms.add.attachment.repeatFileTip': '文件重复',
|
||||
'ms.add.attachment.saveAsTitle': '请选择转存目录',
|
||||
'ms.add.attachment.saveAsNamePlaceholder': '请输入文件名称',
|
||||
'ms.add.attachment.saveAsModulePlaceholder': '请选择转存目录',
|
||||
'ms.add.attachment.saveAsSuccess': '文件转存成功',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<a-popover
|
||||
v-model:popup-visible="saveFilePopoverVisible"
|
||||
trigger="click"
|
||||
position="bl"
|
||||
content-class="ms-add-attachment-save-file-popover"
|
||||
arrow-class="hidden"
|
||||
:popup-offset="12"
|
||||
@popup-visible-change="
|
||||
(val) => {
|
||||
if (!val) handleSaveFileCancel();
|
||||
}
|
||||
"
|
||||
>
|
||||
<span class="mx-[2px]"></span>
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-[16px] text-[14px]">
|
||||
<div class="font-semibold text-[var(--color-text-1)]">
|
||||
{{ t('ms.add.attachment.saveAsTitle') }}
|
||||
</div>
|
||||
<a-input
|
||||
v-model:model-value="saveFileForm.name"
|
||||
:placeholder="t('ms.add.attachment.saveAsNamePlaceholder')"
|
||||
></a-input>
|
||||
<a-tree-select
|
||||
v-model:modelValue="saveFileForm.moduleId"
|
||||
:data="moduleTree"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:placeholder="t('ms.add.attachment.saveAsModulePlaceholder')"
|
||||
:loading="moduleTreeLoading"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
height: 200,
|
||||
threshold: 200,
|
||||
},
|
||||
}"
|
||||
allow-search
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-[12px]">
|
||||
<a-button type="secondary" @click="handleSaveFileCancel">{{ t('common.cancel') }}</a-button>
|
||||
<a-button type="primary" :loading="saveLoading" @click="handleSaveFileConfirm">
|
||||
{{ t('common.confirm') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
savingFile?: MsFileItem;
|
||||
fileSaveAsSourceId: string | number;
|
||||
fileIdKey?: string;
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>;
|
||||
fileModuleOptionsApi?: (projectId: string) => Promise<any[]>;
|
||||
}>(),
|
||||
{
|
||||
fileIdKey: 'fileId',
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'finish', fileId: string): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const saveFilePopoverVisible = defineModel<boolean>('visible', {
|
||||
required: true,
|
||||
});
|
||||
const saveFileForm = ref({
|
||||
name: '',
|
||||
moduleId: '',
|
||||
});
|
||||
const saveLoading = ref(false);
|
||||
const moduleTree = ref<ModuleTreeNode[]>([]);
|
||||
const moduleTreeLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 初始化文件转存目录下拉框选项
|
||||
*/
|
||||
async function initModuleOptions() {
|
||||
try {
|
||||
if (props.fileModuleOptionsApi && moduleTree.value.length === 0) {
|
||||
// 只初始化一次
|
||||
moduleTreeLoading.value = true;
|
||||
moduleTree.value = await props.fileModuleOptionsApi(appStore.currentProjectId);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
moduleTreeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => saveFilePopoverVisible.value,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
initModuleOptions();
|
||||
console.log('visible', props.savingFile, props.fileIdKey);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 关闭文件转存弹窗,清理数据
|
||||
*/
|
||||
function handleSaveFileCancel() {
|
||||
saveFileForm.value = {
|
||||
name: '',
|
||||
moduleId: '',
|
||||
};
|
||||
saveFilePopoverVisible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认文件转存,转存成功后将本地文件改成关联文件类型
|
||||
*/
|
||||
async function handleSaveFileConfirm() {
|
||||
try {
|
||||
if (props.fileSaveAsApi && props.savingFile) {
|
||||
saveLoading.value = true;
|
||||
const res = await props.fileSaveAsApi({
|
||||
projectId: appStore.currentProjectId,
|
||||
sourceId: props.fileSaveAsSourceId || '',
|
||||
fileId: props.savingFile[props.fileIdKey] || props.savingFile.uid,
|
||||
local: true,
|
||||
moduleId: saveFileForm.value.moduleId,
|
||||
name: saveFileForm.value.name,
|
||||
});
|
||||
emit('finish', res);
|
||||
Message.success(t('ms.add.attachment.saveAsSuccess'));
|
||||
handleSaveFileCancel();
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ms-add-attachment-save-file-popover {
|
||||
padding: 24px;
|
||||
width: 300px;
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -19,31 +19,29 @@
|
|||
</MsTag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.showType === 'commonScript'" class="flex bg-[var(--color-bg-3)]">
|
||||
<div class="relative w-full">
|
||||
<MsCodeEditor
|
||||
ref="codeEditorRef"
|
||||
v-model:model-value="innerCodeValue"
|
||||
title=""
|
||||
:width="expandMenu ? '100%' : '68%'"
|
||||
height="460px"
|
||||
theme="vs"
|
||||
:language="innerLanguagesType"
|
||||
:read-only="false"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
>
|
||||
<template #rightBox>
|
||||
<MsScriptMenu
|
||||
v-model:expand="expandMenu"
|
||||
v-model:languagesType="innerLanguagesType"
|
||||
@insert="insertHandler"
|
||||
@form-api-import="formApiImport"
|
||||
@insert-common-script="insertCommonScript"
|
||||
/>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
<div v-if="props.showType === 'commonScript'" class="relative flex w-full">
|
||||
<MsCodeEditor
|
||||
ref="codeEditorRef"
|
||||
v-model:model-value="innerCodeValue"
|
||||
title=""
|
||||
:width="expandMenu ? '100%' : '68%'"
|
||||
height="460px"
|
||||
theme="vs"
|
||||
:language="innerLanguagesType"
|
||||
:read-only="false"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
>
|
||||
<template #rightBox>
|
||||
<MsScriptMenu
|
||||
v-model:expand="expandMenu"
|
||||
v-model:languagesType="innerLanguagesType"
|
||||
@insert="insertHandler"
|
||||
@form-api-import="formApiImport"
|
||||
@insert-common-script="insertCommonScript"
|
||||
/>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
<MsCodeEditor
|
||||
v-else
|
||||
|
|
|
@ -7,7 +7,7 @@ export default {
|
|||
'ms.personal.apiKey': 'APIKEY',
|
||||
'ms.personal.tripartite': '三方平台账号',
|
||||
'ms.personal.changeAvatar': '更换头像',
|
||||
'ms.personal.name': '用户姓名',
|
||||
'ms.personal.name': '用户名称',
|
||||
'ms.personal.namePlaceholder': '请输入用户名称',
|
||||
'ms.personal.nameRequired': '用户名称不能为空',
|
||||
'ms.personal.email': '邮箱',
|
||||
|
|
|
@ -173,7 +173,7 @@ export default defineComponent(
|
|||
if (e[key]?.toLowerCase().includes(val.toLowerCase())) {
|
||||
// 是否匹配
|
||||
hasMatch = true;
|
||||
item[props.labelKey || 'label'] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
||||
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
>
|
||||
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
|
||||
<MsIcon v-else type="icon-icon_magnify_outlined" />
|
||||
{{ t('msCodeEditor.fullScreen') }}
|
||||
{{ t(isFullScreen ? 'common.offFullScreen' : 'common.fullScreen') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.subHeader" class="basis-full">
|
||||
|
@ -88,7 +88,7 @@
|
|||
hideFooter: boolean; // 隐藏底部栏
|
||||
loading: boolean; // 卡片 loading 状态
|
||||
isEdit: boolean; // 是否编辑状态
|
||||
specialHeight: number; // 特殊高度,例如某些页面有面包屑
|
||||
specialHeight: number; // 特殊高度,例如某些页面有面包屑,autoHeight 时无效
|
||||
hideBack: boolean; // 隐藏返回按钮
|
||||
autoHeight: boolean; // 内容区域高度是否自适应
|
||||
otherWidth: number; // 该宽度为卡片外部同级容器的宽度
|
||||
|
@ -145,32 +145,17 @@
|
|||
|
||||
const cardOverHeight = computed(() => {
|
||||
if (isFullScreen.value) {
|
||||
if (props.hideFooter) {
|
||||
// 隐藏底部
|
||||
return 62;
|
||||
}
|
||||
return 142;
|
||||
return 106;
|
||||
}
|
||||
if (props.simple) {
|
||||
// 简单模式没有标题、没有底部
|
||||
return props.noContentPadding ? 76 + _specialHeight : 124 + _specialHeight;
|
||||
}
|
||||
if (props.hideFooter) {
|
||||
// 隐藏底部
|
||||
return props.noContentPadding ? 140 + _specialHeight : 180 + _specialHeight;
|
||||
}
|
||||
return 264 + _specialHeight;
|
||||
return 190 + _specialHeight;
|
||||
});
|
||||
|
||||
const getComputedContentStyle = computed(() => {
|
||||
if (props.isFullscreen || isFullScreen.value) {
|
||||
return {
|
||||
overflow: 'auto',
|
||||
width: 'auto',
|
||||
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight.value}px)`,
|
||||
};
|
||||
}
|
||||
if (props.noContentPadding) {
|
||||
if (props.isFullscreen || isFullScreen.value || props.noContentPadding) {
|
||||
return {
|
||||
overflow: 'auto',
|
||||
width: 'auto',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
ref="fullRef"
|
||||
class="h-full overflow-hidden rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[12px]"
|
||||
class="flex h-full flex-col rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[12px]"
|
||||
>
|
||||
<div v-if="showTitleLine" class="mb-[8px] flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-[4px]">
|
||||
|
@ -33,11 +33,11 @@
|
|||
<span class="flex items-center gap-[4px] font-medium">{{ title }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
<div>
|
||||
<div class="ml-auto flex items-center gap-[8px]">
|
||||
<slot name="rightTitle"> </slot>
|
||||
<div
|
||||
v-if="showFullScreen"
|
||||
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
|
||||
class="cursor-pointer text-right !text-[var(--color-text-4)]"
|
||||
@click="toggleFullScreen"
|
||||
>
|
||||
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
|
||||
|
@ -46,12 +46,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 这里的 36px 是顶部标题的 36px -->
|
||||
<div
|
||||
:class="`flex ${
|
||||
showTitleLine ? 'h-[calc(100%-32px)]' : 'h-full'
|
||||
} w-full flex-row overflow-hidden rounded-[var(--border-radius-small)]`"
|
||||
>
|
||||
<!-- 这里的 32px 是顶部标题的 32px -->
|
||||
<div class="flex w-full flex-1 flex-row rounded-[var(--border-radius-small)]">
|
||||
<div
|
||||
ref="codeContainerRef"
|
||||
:class="['ms-code-editor', isFullScreen ? 'ms-code-editor-full-screen' : '', currentTheme]"
|
||||
|
@ -327,7 +323,7 @@
|
|||
|
||||
<style lang="less" scoped>
|
||||
.ms-code-editor {
|
||||
@apply z-10 overflow-hidden;
|
||||
@apply z-10;
|
||||
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
|
@ -336,6 +332,9 @@
|
|||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
:deep(.overflowingContentWidgets) {
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
.ms-code-editor-full-screen {
|
||||
height: calc(100vh - 66px);
|
||||
|
|
|
@ -112,6 +112,6 @@ export const editorProps = {
|
|||
// 是否显示主题切换
|
||||
showThemeChange: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<script setup lang="ts">
|
||||
import { hasAllPermission } from '@/utils/permission';
|
||||
|
||||
interface MenuItem {
|
||||
export interface MenuItem {
|
||||
title: string;
|
||||
level: number;
|
||||
name: string;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
<slot>
|
||||
<div :class="['ms-more-action-trigger-content', visible ? 'ms-more-action-trigger-content--focus' : '']">
|
||||
<MsButton type="text" size="mini" class="more-icon-btn">
|
||||
<MsButton type="text" size="mini" class="more-icon-btn" @click="visible = !visible">
|
||||
<MsIcon type="icon-icon_more_outlined" size="16" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<a-upload
|
||||
v-if="showDropArea"
|
||||
v-bind="{ ...props }"
|
||||
v-model:file-list="fileList"
|
||||
v-model:file-list="innerFileList"
|
||||
:accept="
|
||||
[UploadAcceptEnum.none, UploadAcceptEnum.unknown].includes(UploadAcceptEnum[props.accept])
|
||||
? '*'
|
||||
|
@ -30,7 +30,7 @@
|
|||
<div v-else class="ms-upload-icon ms-upload-icon--default"></div>
|
||||
</div>
|
||||
<!-- 支持多文件上传时,不需要展示选择文件后的信息,已选的文件使用文件列表搭配展示 -->
|
||||
<template v-if="fileList.length === 0 || props.multiple">
|
||||
<template v-if="innerFileList.length === 0 || props.multiple">
|
||||
<div class="ms-upload-main-text">
|
||||
{{ t(props.mainText || 'ms.upload.importModalDragText') }}
|
||||
</div>
|
||||
|
@ -47,11 +47,11 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<div class="ms-upload-main-text w-full">
|
||||
<a-tooltip :content="fileList[0]?.name">
|
||||
<span class="one-line-text w-[80%] text-center"> {{ fileList[0]?.name }}</span>
|
||||
<a-tooltip :content="innerFileList[0]?.name">
|
||||
<span class="one-line-text w-[80%] text-center"> {{ innerFileList[0]?.name }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="ms-upload-sub-text">{{ formatFileSize(fileList[0]?.file?.size || 0) }}</div>
|
||||
<div class="ms-upload-sub-text">{{ formatFileSize(innerFileList[0]?.file?.size || 0) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</slot>
|
||||
|
@ -77,6 +77,7 @@
|
|||
|
||||
// 上传 组件 props
|
||||
type UploadProps = Partial<{
|
||||
fileList: MsFileItem[];
|
||||
mainText: string; // 主要文案
|
||||
subText: string; // 次要文案
|
||||
showSubText: boolean; // 是否显示次要文案
|
||||
|
@ -96,7 +97,6 @@
|
|||
limit: number; // 限制上传文件数量
|
||||
}> & {
|
||||
accept: UploadType;
|
||||
fileList: MsFileItem[];
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<UploadProps>(), {
|
||||
|
@ -110,35 +110,23 @@
|
|||
|
||||
const defaultMaxSize = 50;
|
||||
|
||||
const fileList = ref<MsFileItem[]>(props.fileList);
|
||||
|
||||
watch(
|
||||
() => props.fileList,
|
||||
(val) => {
|
||||
fileList.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => fileList.value,
|
||||
(val) => {
|
||||
emit('update:fileList', val);
|
||||
}
|
||||
);
|
||||
const innerFileList = defineModel<MsFileItem[]>('fileList', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const fileIconType = computed(() => {
|
||||
// 单选并且选了文件,按文件类型展示图标(单选文件选择后直接展示绿色图标)
|
||||
if (fileList.value.length > 0 && !props.multiple) {
|
||||
return getFileIcon(fileList.value[0], UploadStatus.done);
|
||||
if (innerFileList.value.length > 0 && !props.multiple) {
|
||||
return getFileIcon(innerFileList.value[0], UploadStatus.done);
|
||||
}
|
||||
// 多选直接按照类型展示
|
||||
return FileIconMap[props.accept][UploadStatus.init];
|
||||
});
|
||||
|
||||
async function beforeUpload(file: File) {
|
||||
if (!props.multiple && fileList.value.length > 0) {
|
||||
if (!props.multiple && innerFileList.value.length > 0) {
|
||||
// 单文件上传时,清空之前的文件
|
||||
fileList.value = [];
|
||||
innerFileList.value = [];
|
||||
}
|
||||
const maxSize = props.maxSize || defaultMaxSize;
|
||||
const _maxSize = props.sizeUnit === 'MB' ? maxSize * 1024 * 1024 : maxSize * 1024;
|
||||
|
|
|
@ -32,7 +32,7 @@ export enum RequestBodyFormat {
|
|||
RAW = 'RAW',
|
||||
BINARY = 'BINARY',
|
||||
}
|
||||
// 接口响应体格式
|
||||
// 接口响应头格式
|
||||
export enum RequestContentTypeEnum {
|
||||
JSON = 'application/json',
|
||||
TEXT = 'application/text',
|
||||
|
@ -52,6 +52,14 @@ export enum ResponseComposition {
|
|||
CONSOLE = 'CONSOLE',
|
||||
EXTRACT = 'EXTRACT',
|
||||
ASSERTION = 'ASSERTION',
|
||||
CODE = 'CODE',
|
||||
}
|
||||
// 接口响应体格式
|
||||
export enum ResponseBodyFormat {
|
||||
JSON = 'JSON',
|
||||
XML = 'XML',
|
||||
RAW = 'RAW',
|
||||
BINARY = 'BINARY',
|
||||
}
|
||||
// 接口定义状态
|
||||
export enum RequestDefinitionStatus {
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function useFullScreen(
|
|||
originalStyle.value = dom.getAttribute('style') || '';
|
||||
mergeStyles(
|
||||
dom,
|
||||
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100; width: 100%; height: 100%;'
|
||||
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; width: 100%; height: 100%;'
|
||||
);
|
||||
isFullScreen.value = true;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { ResponseBodyFormat } from '@/enums/apiEnum';
|
||||
|
||||
import { EnableKeyValueParam, ExecuteBinaryBody, ExecuteJsonBody, ExecuteValueBody } from './debug';
|
||||
|
||||
// 获取插件表单选项参数
|
||||
export interface GetPluginOptionsParams {
|
||||
orgId: string;
|
||||
|
@ -23,7 +27,8 @@ export interface PluginConfig {
|
|||
options: Record<string, any>;
|
||||
script: Record<string, any>[];
|
||||
scriptType: string;
|
||||
fields?: string[]; // 插件脚本内配置的全部字段集合
|
||||
apiDebugFields?: string[]; // 接口调试脚本内配置的全部字段集合
|
||||
apiDefinitionFields?: string[]; // 接口定义脚本内配置的全部字段集合
|
||||
}
|
||||
// 响应结果
|
||||
export interface ResponseResult {
|
||||
|
@ -48,3 +53,49 @@ export interface ResponseResult {
|
|||
}[]; // 请求结果
|
||||
console: string;
|
||||
}
|
||||
// 响应定义-body
|
||||
export interface ResponseDefinitionBody {
|
||||
bodyType: ResponseBodyFormat;
|
||||
jsonBody: ExecuteJsonBody;
|
||||
xmlBody: ExecuteValueBody;
|
||||
rawBody: ExecuteValueBody;
|
||||
binaryBody: ExecuteBinaryBody;
|
||||
}
|
||||
export interface ResponseDefinitionHeader extends EnableKeyValueParam {
|
||||
notBlankValue: boolean;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
// 响应定义
|
||||
export interface ResponseDefinition {
|
||||
id: string | number;
|
||||
statusCode: string | number;
|
||||
defaultFlag: boolean; // 默认响应标志
|
||||
name: string; // 响应名称
|
||||
headers: ResponseDefinitionHeader[];
|
||||
body: ResponseDefinitionBody;
|
||||
}
|
||||
// 接口定义-JsonSchema
|
||||
export interface JsonSchema {
|
||||
example: Record<string, any>;
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
items: string;
|
||||
mock: Record<string, any>;
|
||||
properties: Record<string, any>;
|
||||
additionalProperties: string;
|
||||
required: string[];
|
||||
pattern: string;
|
||||
maxLength: number;
|
||||
minLength: number;
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
schema: string;
|
||||
format: string;
|
||||
enumString: string[];
|
||||
enumInteger: number[];
|
||||
enumNumber: number[];
|
||||
extensions: Record<string, any>;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
ResponseBodyXPathAssertionFormat,
|
||||
} from '@/enums/apiEnum';
|
||||
|
||||
import { JsonSchema } from './common';
|
||||
|
||||
// 条件操作类型
|
||||
export type ConditionType = RequestConditionProcessor;
|
||||
// 断言-匹配条件规则
|
||||
|
@ -78,15 +80,18 @@ export interface ExecuteBinaryBody {
|
|||
file?: {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileAlias: string; // 文件别名
|
||||
local: boolean; // 是否是本地上传的文件
|
||||
delete?: boolean; // 关联文件是否被删除
|
||||
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
|
||||
};
|
||||
sendAsBody?: boolean; // 是否作为正文发送,只有 mock 有此字段
|
||||
}
|
||||
// 接口请求json-body参数集合信息
|
||||
export interface ExecuteJsonBody {
|
||||
enableJsonSchema?: boolean;
|
||||
enableTransition?: boolean;
|
||||
jsonSchema?: string;
|
||||
jsonSchema?: JsonSchema;
|
||||
jsonValue: string;
|
||||
}
|
||||
// 执行请求配置
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { ResponseDefinition } from './common';
|
||||
import { ExecuteRequestParams } from './debug';
|
||||
|
||||
export interface ApiDefinitionCustomField {
|
||||
apiId: string;
|
||||
fieldId: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ApiDefinitionCreateParams extends ExecuteRequestParams {
|
||||
response: ResponseDefinition;
|
||||
customFields: ApiDefinitionCustomField[];
|
||||
}
|
||||
|
||||
export interface ApiDefinitionCustomFieldDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
scene: string;
|
||||
type: string;
|
||||
remark: string;
|
||||
internal: boolean;
|
||||
scopeType: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
createUser: string;
|
||||
refId: string;
|
||||
enableOptionKey: boolean;
|
||||
scopeId: string;
|
||||
value: string;
|
||||
apiId: string;
|
||||
fieldId: string;
|
||||
}
|
||||
|
||||
export interface ApiDefinitionDetail extends ApiDefinitionCreateParams {
|
||||
id: string;
|
||||
name: string;
|
||||
protocol: string;
|
||||
method: string;
|
||||
path: string;
|
||||
status: string;
|
||||
num: number;
|
||||
tags: string[];
|
||||
pos: number;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
latest: boolean;
|
||||
versionId: string;
|
||||
refId: string;
|
||||
description: string;
|
||||
createTime: number;
|
||||
createUser: string;
|
||||
updateTime: number;
|
||||
updateUser: string;
|
||||
deleteUser: string;
|
||||
deleteTime: number;
|
||||
deleted: boolean;
|
||||
createUserName: string;
|
||||
updateUserName: string;
|
||||
deleteUserName: string;
|
||||
versionName: string;
|
||||
caseTotal: number;
|
||||
casePassRate: string;
|
||||
caseStatus: string;
|
||||
follow: boolean;
|
||||
customFields: ApiDefinitionCustomFieldDetail[];
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import { key } from 'localforage';
|
||||
|
||||
import { TableQueryParams } from '@/models/common';
|
||||
import { StatusType } from '@/enums/caseEnum';
|
||||
|
||||
|
|
|
@ -75,3 +75,12 @@ export interface DragSortParams {
|
|||
moveId: string;
|
||||
moduleId?: string;
|
||||
}
|
||||
// 文件转存入参
|
||||
export interface TransferFileParams {
|
||||
projectId: string;
|
||||
sourceId: string | number;
|
||||
name?: string;
|
||||
fileId: string;
|
||||
local: true;
|
||||
moduleId: string;
|
||||
}
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
<div class="condition-content">
|
||||
<!-- 脚本操作 -->
|
||||
<template v-if="condition.processorType === RequestConditionProcessor.SCRIPT">
|
||||
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[16px]">
|
||||
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[8px]">
|
||||
<a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio>
|
||||
<a-radio :value="true">{{ t('apiTestDebug.quote') }}</a-radio>
|
||||
</a-radio-group>
|
||||
<div
|
||||
v-if="!condition.enableCommonScript"
|
||||
class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
|
||||
class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)]"
|
||||
>
|
||||
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
|
||||
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] top-[12px] z-10 w-[calc(100%-24px)]">
|
||||
<a-input
|
||||
ref="scriptNameInputRef"
|
||||
v-model:model-value="condition.scriptName"
|
||||
|
@ -21,12 +21,12 @@
|
|||
@blur="isShowEditScriptNameInput = false"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between px-[12px] pt-[12px]">
|
||||
<div class="flex items-center">
|
||||
<a-tooltip :content="condition.commonScriptInfo?.name">
|
||||
<a-tooltip :content="condition.scriptName">
|
||||
<div class="script-name-container">
|
||||
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
|
||||
{{ condition.commonScriptInfo?.name }}
|
||||
{{ condition.scriptName }}
|
||||
</div>
|
||||
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
|
||||
</div>
|
||||
|
@ -741,7 +741,7 @@ if (!result){
|
|||
@apply flex flex-1 flex-col overflow-y-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-small);
|
||||
.script-name-container {
|
||||
|
|
|
@ -174,6 +174,9 @@
|
|||
id: 'fileId',
|
||||
name: 'fileName',
|
||||
}"
|
||||
:file-save-as-source-id="props.fileSaveAsSourceId"
|
||||
:file-save-as-api="props.fileSaveAsApi"
|
||||
:file-module-options-api="props.fileModuleOptionsApi"
|
||||
input-class="param-input h-[24px]"
|
||||
input-size="small"
|
||||
tag-size="small"
|
||||
|
@ -469,6 +472,7 @@
|
|||
import useTableStore from '@/hooks/useTableStore';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { TransferFileParams } from '@/models/common';
|
||||
import { ProjectOptionItem } from '@/models/projectManagement/environmental';
|
||||
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
|
||||
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
@ -518,6 +522,9 @@
|
|||
columnIndex: number;
|
||||
}) => { rowspan?: number; colspan?: number } | void;
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||
fileModuleOptionsApi?: (...args) => Promise<any>; // 文件转存目录下拉框接口
|
||||
}>(),
|
||||
{
|
||||
params: () => [],
|
||||
|
@ -771,9 +778,11 @@
|
|||
() => props.params,
|
||||
(arr) => {
|
||||
if (arr.length > 0) {
|
||||
// 后台存储无id,渲染时需要手动添加一次
|
||||
let hasNoIdItem = false; // 是否有没有id的项,用以判断是否是后台数据初始化表格
|
||||
propsRes.value.data = arr.map((item, i) => {
|
||||
if (!item.id) {
|
||||
// 后台存储无id,渲染时需要手动添加一次
|
||||
hasNoIdItem = true;
|
||||
return {
|
||||
...item,
|
||||
id: new Date().getTime() + i,
|
||||
|
@ -781,7 +790,7 @@
|
|||
}
|
||||
return item;
|
||||
});
|
||||
if (!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
|
||||
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
|
||||
addTableLine(arr.length - 1);
|
||||
}
|
||||
} else {
|
||||
|
@ -816,18 +825,21 @@
|
|||
try {
|
||||
if (props.uploadTempFileApi && file?.local) {
|
||||
// 本地上传单次只能选一个文件
|
||||
const fileItem = files[0];
|
||||
appStore.showLoading();
|
||||
const res = await props.uploadTempFileApi(fileItem.file);
|
||||
record.files = [
|
||||
{
|
||||
...fileItem,
|
||||
fileId: res.data,
|
||||
fileName: fileItem.name || '',
|
||||
local: true,
|
||||
},
|
||||
];
|
||||
const res = await props.uploadTempFileApi(file.file);
|
||||
for (let i = 0; i < record.files.length; i++) {
|
||||
const item = record.files[i];
|
||||
if ([item.fileId, item.uid].includes(file.uid)) {
|
||||
record.files[i] = {
|
||||
...file,
|
||||
fileId: res.data,
|
||||
fileName: file.name || '',
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 关联文件可选多个文件
|
||||
record.files = files.map((e) => ({
|
||||
...e,
|
||||
fileId: e.uid || e.fileId || '',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="beforeConfirm"
|
||||
:popup-container="props.popupContainer || 'body'"
|
||||
:popup-offset="props.popupOffset"
|
||||
@popup-visible-change="reset"
|
||||
>
|
||||
<template #content>
|
||||
|
@ -72,6 +73,7 @@
|
|||
fieldConfig?: FieldConfig;
|
||||
parentId?: string; // 父节点 id
|
||||
nodeId?: string; // 节点 id
|
||||
popupOffset?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible', 'close', 'addFinish', 'renameFinish', 'updateDescFinish']);
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_DATA"
|
||||
:default-param-item="defaultBodyParamsItem"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
:file-save-as-source-id="props.fileSaveAsSourceId"
|
||||
:file-save-as-api="props.fileSaveAsApi"
|
||||
:file-module-options-api="props.fileModuleOptionsApi"
|
||||
@change="handleParamTableChange"
|
||||
/>
|
||||
<paramTable
|
||||
|
@ -79,22 +82,16 @@
|
|||
</a-tooltip>
|
||||
</div> -->
|
||||
</div>
|
||||
<div v-else class="flex h-[calc(100%-100px)]">
|
||||
<div v-else class="flex h-[calc(100%-34px)]">
|
||||
<MsCodeEditor
|
||||
v-model:model-value="currentBodyCode"
|
||||
class="flex-1"
|
||||
theme="MS-text"
|
||||
height="100%"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
:language="currentCodeLanguage"
|
||||
>
|
||||
<template #rightTitle>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('apiTestDebug.batchAddParamsTip1') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -114,6 +111,7 @@
|
|||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ExecuteBody } from '@/models/apiTest/debug';
|
||||
import { TransferFileParams } from '@/models/common';
|
||||
import { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
|
@ -124,6 +122,9 @@
|
|||
layout: 'horizontal' | 'vertical';
|
||||
secondBoxHeight: number;
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||
fileModuleOptionsApi?: (...args) => Promise<any>; // 文件转存目录下拉框接口
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:params', value: any[]): void;
|
||||
|
|
|
@ -160,6 +160,9 @@
|
|||
:layout="activeLayout"
|
||||
:second-box-height="secondBoxHeight"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
:file-save-as-source-id="props.fileSaveAsSourceId"
|
||||
:file-save-as-api="props.fileSaveAsApi"
|
||||
:file-module-options-api="props.fileModuleOptionsApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<debugQuery
|
||||
|
@ -208,11 +211,11 @@
|
|||
v-model:active-layout="activeLayout"
|
||||
v-model:active-tab="requestVModel.responseActiveTab"
|
||||
:is-expanded="isExpanded"
|
||||
:response="requestVModel.response"
|
||||
:response-definition="requestVModel.responseDefinition"
|
||||
:hide-layout-switch="props.hideResponseLayoutSwitch"
|
||||
:request="requestVModel"
|
||||
:loading="requestVModel.executeLoading"
|
||||
:is-edit="props.isDefinition"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
@change-expand="changeExpand"
|
||||
@change-layout="handleActiveLayoutChange"
|
||||
@change="handleActiveDebugChange"
|
||||
|
@ -294,9 +297,9 @@
|
|||
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
|
||||
import { PluginConfig } from '@/models/apiTest/common';
|
||||
import { PluginConfig, ResponseDefinition } from '@/models/apiTest/common';
|
||||
import { ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
||||
import {
|
||||
RequestAuthType,
|
||||
RequestBodyFormat,
|
||||
|
@ -324,7 +327,10 @@
|
|||
protocol: string;
|
||||
activeTab: RequestComposition;
|
||||
}
|
||||
export type RequestParam = ExecuteHTTPRequestFullParams & RequestCustomAttr & TabItem;
|
||||
export type RequestParam = ExecuteHTTPRequestFullParams & {
|
||||
responseDefinition?: ResponseDefinition;
|
||||
} & RequestCustomAttr &
|
||||
TabItem;
|
||||
|
||||
const props = defineProps<{
|
||||
request: RequestParam; // 请求参数集合
|
||||
|
@ -337,6 +343,9 @@
|
|||
createApi: (...args) => Promise<any>; // 创建接口
|
||||
updateApi: (...args) => Promise<any>; // 更新接口
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||
fileModuleOptionsApi?: (...args) => Promise<any>; // 文件转存目录下拉框接口
|
||||
permissionMap: {
|
||||
execute: string;
|
||||
create: string;
|
||||
|
@ -528,7 +537,7 @@
|
|||
const formData = tempForm || requestVModel.value;
|
||||
if (fApi.value) {
|
||||
const form = {};
|
||||
pluginScriptMap.value[requestVModel.value.protocol].fields?.forEach((key) => {
|
||||
pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields?.forEach((key) => {
|
||||
form[key] = formData[key];
|
||||
});
|
||||
fApi.value?.setValue(form);
|
||||
|
|
|
@ -1,7 +1,454 @@
|
|||
<template>
|
||||
<div> </div>
|
||||
<div class="mt-[8px] w-full">
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeResponse"
|
||||
v-model:tabs="responseTabs"
|
||||
at-least-one
|
||||
hide-more-action
|
||||
@add="addResponseTab"
|
||||
>
|
||||
<template #label="{ tab }">
|
||||
<div class="response-tab">
|
||||
<div v-if="tab.isDefault" class="response-tab-default-icon"></div>
|
||||
{{ tab.label }}({{ tab.code }})
|
||||
<MsMoreAction
|
||||
:list="
|
||||
tab.isDefault
|
||||
? tabMoreActionList.filter((e) => e.eventTag !== 'setDefault' && e.eventTag !== 'delete')
|
||||
: tabMoreActionList
|
||||
"
|
||||
class="response-more-action"
|
||||
@select="(e) => handleMoreActionSelect(e, tab as ResponseItem)"
|
||||
/>
|
||||
<popConfirm
|
||||
v-model:visible="tab.showRenamePopConfirm"
|
||||
mode="tabRename"
|
||||
:field-config="{ field: tab.label }"
|
||||
:all-names="responseTabs.map((e) => e.label)"
|
||||
:popup-offset="20"
|
||||
@rename-finish="(val) => (tab.label = val)"
|
||||
>
|
||||
<span :id="`renameSpan${tab.id}`" class="relative"></span>
|
||||
</popConfirm>
|
||||
<a-popconfirm
|
||||
v-model:popup-visible="tab.showPopConfirm"
|
||||
position="bottom"
|
||||
content-class="w-[300px]"
|
||||
:ok-text="t('common.confirm')"
|
||||
:popup-offset="20"
|
||||
@ok="() => handleDeleteResponseTab(tab.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-exclamation-circle-fill class="!text-[rgb(var(--danger-6))]" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="font-semibold text-[var(--color-text-1)]">
|
||||
{{ t('apiTestManagement.confirmDelete', { name: tab.label }) }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="relative"></div>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
</div>
|
||||
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]">
|
||||
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
|
||||
</a-tabs>
|
||||
<div class="response-container">
|
||||
<div class="mb-[8px] flex items-center justify-between">
|
||||
<a-radio-group
|
||||
v-model:model-value="innerResponse.body.bodyType"
|
||||
type="button"
|
||||
size="small"
|
||||
@change="(val) => changeBodyFormat(val as ResponseBodyFormat)"
|
||||
>
|
||||
<a-radio v-for="item of ResponseBodyFormat" :key="item" :value="item">
|
||||
{{ ResponseBodyFormat[item].toLowerCase() }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="flex items-center gap-[24px]">
|
||||
<a-radio-group size="mini" @change="(val) => changeJsonBodyFormat(val as ResponseBodyFormat)">
|
||||
<a-radio value="Json" :default-checked="!innerResponse.body.jsonBody.enableJsonSchema">Json</a-radio>
|
||||
<a-radio class="mr-0" value="JsonSchema" :default-checked="innerResponse.body.jsonBody.enableJsonSchema">
|
||||
Json Schema
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<a-switch v-model:model-value="innerResponse.body.jsonBody.enableTransition" size="small" type="line" />
|
||||
{{ t('apiTestManagement.dynamicConversion') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="activeTab === ResponseComposition.BODY">
|
||||
<div
|
||||
v-if="
|
||||
[ResponseBodyFormat.JSON, ResponseBodyFormat.XML, ResponseBodyFormat.RAW].includes(
|
||||
innerResponse.body.bodyType
|
||||
)
|
||||
"
|
||||
class="h-[calc(100%-35px)]"
|
||||
>
|
||||
<MsCodeEditor
|
||||
ref="responseEditorRef"
|
||||
v-model:model-value="currentBodyCode"
|
||||
:language="currentCodeLanguage"
|
||||
theme="vs"
|
||||
height="100%"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
:show-language-change="false"
|
||||
:show-charset-change="false"
|
||||
read-only
|
||||
>
|
||||
<template #rightTitle>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
|
||||
<template #icon>
|
||||
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
|
||||
<a-input
|
||||
v-model:model-value="innerResponse.body.binaryBody.description"
|
||||
:placeholder="t('common.desc')"
|
||||
:max-length="255"
|
||||
/>
|
||||
<MsAddAttachment
|
||||
v-model:file-list="fileList"
|
||||
mode="input"
|
||||
:multiple="false"
|
||||
:fields="{
|
||||
id: 'fileId',
|
||||
name: 'fileName',
|
||||
}"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
|
||||
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import { ResponseDefinition } from '@/models/apiTest/common';
|
||||
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
response: ResponseDefinition;
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change'): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
export interface ResponseItem extends TabItem {
|
||||
isDefault?: boolean; // 是否是默认tab
|
||||
code: number; // 状态码
|
||||
showPopConfirm?: boolean; // 是否显示确认弹窗
|
||||
showRenamePopConfirm?: boolean; // 是否显示重命名确认弹窗
|
||||
}
|
||||
const activeTab = defineModel<ResponseComposition>('activeTab', {
|
||||
required: true,
|
||||
default: ResponseComposition.BODY,
|
||||
});
|
||||
const innerResponse = defineModel<ResponseDefinition>('response', {
|
||||
required: true,
|
||||
});
|
||||
const responseTabs = ref<ResponseItem[]>([
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
label: t('apiTestManagement.response'),
|
||||
closable: false,
|
||||
code: 200,
|
||||
isDefault: true,
|
||||
showPopConfirm: false,
|
||||
showRenamePopConfirm: false,
|
||||
},
|
||||
]);
|
||||
const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
|
||||
|
||||
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
|
||||
responseTabs.value.push({
|
||||
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
|
||||
code: 200,
|
||||
...defaultProps,
|
||||
id: new Date().getTime(),
|
||||
isDefault: false,
|
||||
showPopConfirm: false,
|
||||
showRenamePopConfirm: false,
|
||||
});
|
||||
activeResponse.value = responseTabs.value[responseTabs.value.length - 1];
|
||||
emit('change');
|
||||
}
|
||||
|
||||
const tabMoreActionList: ActionsItem[] = [
|
||||
{
|
||||
label: t('apiTestManagement.setDefault'),
|
||||
eventTag: 'setDefault',
|
||||
},
|
||||
{
|
||||
label: t('common.rename'),
|
||||
eventTag: 'rename',
|
||||
},
|
||||
{
|
||||
label: t('common.copy'),
|
||||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
const renameValue = ref('');
|
||||
|
||||
function handleMoreActionSelect(e: ActionsItem, _tab: ResponseItem) {
|
||||
switch (e.eventTag) {
|
||||
case 'setDefault':
|
||||
responseTabs.value = responseTabs.value.map((tab) => {
|
||||
tab.isDefault = _tab.id === tab.id;
|
||||
return tab;
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
renameValue.value = _tab.label || '';
|
||||
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
|
||||
break;
|
||||
case 'copy':
|
||||
addResponseTab({ ..._tab, label: `${_tab.label}-Copy` });
|
||||
break;
|
||||
case 'delete':
|
||||
_tab.showPopConfirm = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteResponseTab(id: number | string) {
|
||||
responseTabs.value = responseTabs.value.filter((tab) => tab.id !== id);
|
||||
if (id === activeResponse.value.id) {
|
||||
[activeResponse.value] = responseTabs.value;
|
||||
}
|
||||
}
|
||||
|
||||
const responseCompositionTabList = [
|
||||
{
|
||||
label: t('apiTestDebug.responseBody'),
|
||||
value: ResponseComposition.BODY,
|
||||
},
|
||||
{
|
||||
label: t('apiTestDebug.responseHeader'),
|
||||
value: ResponseComposition.HEADER,
|
||||
},
|
||||
{
|
||||
label: t('apiTestManagement.responseCode'),
|
||||
value: ResponseComposition.CODE,
|
||||
},
|
||||
];
|
||||
|
||||
function changeBodyFormat(val: ResponseBodyFormat) {
|
||||
innerResponse.value.body.bodyType = val;
|
||||
emit('change');
|
||||
}
|
||||
|
||||
function changeJsonBodyFormat(val: string) {
|
||||
innerResponse.value.body.jsonBody.enableJsonSchema = val === 'JsonSchema';
|
||||
emit('change');
|
||||
}
|
||||
|
||||
// 当前显示的代码
|
||||
const currentBodyCode = computed({
|
||||
get() {
|
||||
if (innerResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
|
||||
return innerResponse.value.body.jsonBody.jsonValue;
|
||||
}
|
||||
if (innerResponse.value.body.bodyType === ResponseBodyFormat.XML) {
|
||||
return innerResponse.value.body.xmlBody.value;
|
||||
}
|
||||
return innerResponse.value.body.rawBody.value;
|
||||
},
|
||||
set(val) {
|
||||
if (innerResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
|
||||
innerResponse.value.body.jsonBody.jsonValue = val;
|
||||
} else if (innerResponse.value.body.bodyType === ResponseBodyFormat.XML) {
|
||||
innerResponse.value.body.xmlBody.value = val;
|
||||
} else {
|
||||
innerResponse.value.body.rawBody.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
// 当前代码编辑器的语言
|
||||
const currentCodeLanguage = computed(() => {
|
||||
if (innerResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
|
||||
return LanguageEnum.JSON;
|
||||
}
|
||||
if (innerResponse.value.body.bodyType === ResponseBodyFormat.XML) {
|
||||
return LanguageEnum.XML;
|
||||
}
|
||||
return LanguageEnum.PLAINTEXT;
|
||||
});
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
|
||||
function copyScript() {
|
||||
if (isSupported) {
|
||||
copy(currentBodyCode.value);
|
||||
Message.success(t('common.copySuccess'));
|
||||
} else {
|
||||
Message.warning(t('apiTestDebug.copyNotSupport'));
|
||||
}
|
||||
}
|
||||
|
||||
const fileList = ref<any[]>(
|
||||
innerResponse.value.body.binaryBody && innerResponse.value.body.binaryBody.file
|
||||
? [innerResponse.value.body.binaryBody.file]
|
||||
: []
|
||||
);
|
||||
|
||||
async function handleFileChange(files: MsFileItem[]) {
|
||||
if (files.length === 0) {
|
||||
innerResponse.value.body.binaryBody.file = undefined;
|
||||
return;
|
||||
}
|
||||
if (!props.uploadTempFileApi) return;
|
||||
try {
|
||||
if (fileList.value[0]?.local) {
|
||||
appStore.showLoading();
|
||||
const res = await props.uploadTempFileApi(fileList.value[0].file);
|
||||
innerResponse.value.body.binaryBody.file = {
|
||||
...fileList.value[0],
|
||||
fileId: res.data,
|
||||
fileName: fileList.value[0]?.name || '',
|
||||
fileAlias: fileList.value[0]?.name || '',
|
||||
local: true,
|
||||
};
|
||||
appStore.hideLoading();
|
||||
} else {
|
||||
innerResponse.value.body.binaryBody.file = {
|
||||
...fileList.value[0],
|
||||
fileId: fileList.value[0].uid,
|
||||
fileName: fileList.value[0]?.originalName || '',
|
||||
fileAlias: fileList.value[0]?.name || '',
|
||||
local: false,
|
||||
};
|
||||
}
|
||||
emit('change');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'apiTestDebug.content',
|
||||
dataIndex: 'content',
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'apiTestDebug.status',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'desc',
|
||||
showTooltip: true,
|
||||
},
|
||||
];
|
||||
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
|
||||
scroll: { x: '100%' },
|
||||
columns,
|
||||
});
|
||||
propsRes.value.data = [
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
content: 'Response Code equals: 200',
|
||||
status: 1,
|
||||
desc: '',
|
||||
},
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
content: '$.users[1].age REGEX: 31',
|
||||
status: 0,
|
||||
desc: `Value expected to match regexp '31', but it did not match: '30' match: '30'`,
|
||||
},
|
||||
] as any;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.response-container {
|
||||
margin-top: 8px;
|
||||
height: calc(100% - 88px);
|
||||
.response-header-pre {
|
||||
@apply h-full overflow-auto bg-white;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
}
|
||||
:deep(.arco-table-th) {
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
:deep(.arco-tabs-tab) {
|
||||
@apply leading-none;
|
||||
}
|
||||
.response-tab {
|
||||
@apply flex items-center;
|
||||
.response-tab-default-icon {
|
||||
@apply rounded-full;
|
||||
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('@/assets/svg/icons/default.svg') no-repeat;
|
||||
background-size: contain;
|
||||
box-shadow: 0 0 7px 0 rgb(15 0 78 / 9%);
|
||||
}
|
||||
:deep(.response-more-action) {
|
||||
margin-left: 4px;
|
||||
.more-icon-btn {
|
||||
@apply invisible;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
:deep(.response-more-action) {
|
||||
.more-icon-btn {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</MsButton>
|
||||
</template>
|
||||
<div
|
||||
v-if="props.isEdit && props.response.requestResults[0]?.responseResult?.responseCode"
|
||||
v-if="props.isEdit && props.request.response.requestResults[0]?.responseResult?.responseCode"
|
||||
class="ml-[4px] flex items-center"
|
||||
>
|
||||
<MsButton
|
||||
|
@ -56,31 +56,31 @@
|
|||
</a-radio-group>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.response.requestResults[0]?.responseResult?.responseCode"
|
||||
v-if="props.request.response.requestResults[0]?.responseResult?.responseCode"
|
||||
class="flex items-center justify-between gap-[24px]"
|
||||
>
|
||||
<a-popover position="left" content-class="response-popover-content">
|
||||
<div :style="{ color: statusCodeColor }">
|
||||
{{ props.response.requestResults[0].responseResult.responseCode }}
|
||||
{{ props.request.response.requestResults[0].responseResult.responseCode }}
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
|
||||
<div :style="{ color: statusCodeColor }">
|
||||
{{ props.response.requestResults[0].responseResult.responseCode }}
|
||||
{{ props.request.response.requestResults[0].responseResult.responseCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<a-popover position="left" content-class="w-[400px]">
|
||||
<div class="one-line-text text-[rgb(var(--success-7))]">
|
||||
{{ props.response.requestResults[0].responseResult.responseTime }} ms
|
||||
{{ props.request.response.requestResults[0].responseResult.responseTime }} ms
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="mb-[8px] flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div>
|
||||
<div class="text-[rgb(var(--success-7))]">
|
||||
{{ props.response.requestResults[0].responseResult.responseTime }} ms
|
||||
{{ props.request.response.requestResults[0].responseResult.responseTime }} ms
|
||||
</div>
|
||||
</div>
|
||||
<responseTimeLine :response-timing="timingInfo" />
|
||||
|
@ -88,94 +88,48 @@
|
|||
</a-popover>
|
||||
<a-popover position="left" content-class="response-popover-content">
|
||||
<div class="one-line-text text-[rgb(var(--success-7))]">
|
||||
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
|
||||
{{ props.request.response.requestResults[0].responseResult.responseSize }} bytes
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
|
||||
<div class="one-line-text text-[rgb(var(--success-7))]">
|
||||
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
|
||||
{{ props.request.response.requestResults[0].responseResult.responseSize }} bytes
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<!-- <a-popover position="left" content-class="response-popover-content">
|
||||
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ props.request.response.env }}</div>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.runningEnv') }}</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ props.request.response.env }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<a-popover position="left" content-class="response-popover-content">
|
||||
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ props.request.response.resource }}</div>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.resourcePool') }}</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
|
||||
<div class="text-[var(--color-text-1)]">{{ props.request.response.resource }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover> -->
|
||||
</div>
|
||||
</div>
|
||||
<a-spin :loading="props.loading" class="h-[calc(100%-42px)] w-full px-[18px] pb-[18px]">
|
||||
<div v-if="props.isEdit" class="my-[8px] w-full">
|
||||
<MsEditableTab
|
||||
v-model:active-tab="activeResponse"
|
||||
v-model:tabs="responseTabs"
|
||||
at-least-one
|
||||
hide-more-action
|
||||
@add="addResponseTab"
|
||||
>
|
||||
<template #label="{ tab }">
|
||||
<div class="response-tab">
|
||||
<div v-if="tab.isDefault" class="response-tab-default-icon"></div>
|
||||
{{ tab.label }}({{ tab.code }})
|
||||
<MsMoreAction
|
||||
:list="
|
||||
tab.isDefault
|
||||
? tabMoreActionList.filter((e) => e.eventTag !== 'setDefault' && e.eventTag !== 'delete')
|
||||
: tabMoreActionList
|
||||
"
|
||||
class="response-more-action"
|
||||
@select="(e) => handleMoreActionSelect(e, tab as ResponseItem)"
|
||||
/>
|
||||
<popConfirm
|
||||
v-model:visible="tab.showRenamePopConfirm"
|
||||
mode="tabRename"
|
||||
:field-config="{ field: tab.label }"
|
||||
:all-names="responseTabs.map((e) => e.label)"
|
||||
@rename-finish="(val) => (tab.label = val)"
|
||||
>
|
||||
<span :id="`renameSpan${tab.id}`" class="relative"></span>
|
||||
</popConfirm>
|
||||
<a-popconfirm
|
||||
v-model:popup-visible="tab.showPopConfirm"
|
||||
position="bottom"
|
||||
content-class="w-[300px]"
|
||||
:ok-text="t('common.confirm')"
|
||||
:popup-offset="20"
|
||||
@ok="() => handleDeleteResponseTab(tab.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-exclamation-circle-fill class="!text-[rgb(var(--danger-6))]" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="font-semibold text-[var(--color-text-1)]">
|
||||
{{ t('apiTestManagement.confirmDelete', { name: tab.label }) }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="relative"></div>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
</div>
|
||||
<result
|
||||
v-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
|
||||
<a-spin :loading="props.loading" class="h-[calc(100%-35px)] w-full px-[18px] pb-[18px]">
|
||||
<edit
|
||||
v-if="props.isEdit && activeResponseType === 'content' && props.responseDefinition"
|
||||
v-model:activeTab="activeTab"
|
||||
:response="props.responseDefinition"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
@change="handleResponseChange"
|
||||
/>
|
||||
<result
|
||||
v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
|
||||
v-model:activeTab="activeTab"
|
||||
:response="props.response"
|
||||
:request="props.request"
|
||||
/>
|
||||
</a-spin>
|
||||
|
@ -186,32 +140,29 @@
|
|||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||
import type { Direction } from '@/components/pure/ms-split-box/index.vue';
|
||||
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import edit from './edit.vue';
|
||||
import result from './result.vue';
|
||||
import popConfirm from '@/views/api-test/components/popConfirm.vue';
|
||||
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ResponseResult } from '@/models/apiTest/common';
|
||||
import { ResponseDefinition } from '@/models/apiTest/common';
|
||||
import { ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
import type { RequestParam } from '../index.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activeTab: keyof typeof ResponseComposition;
|
||||
activeTab: ResponseComposition;
|
||||
activeLayout?: Direction;
|
||||
isExpanded: boolean;
|
||||
response: ResponseResult;
|
||||
request?: RequestParam;
|
||||
responseDefinition?: ResponseDefinition;
|
||||
request: RequestParam;
|
||||
hideLayoutSwitch?: boolean; // 隐藏布局切换
|
||||
loading?: boolean;
|
||||
isEdit?: boolean; // 是否可编辑
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
}>(),
|
||||
{
|
||||
activeLayout: 'vertical',
|
||||
|
@ -220,7 +171,7 @@
|
|||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:activeLayout', value: Direction): void;
|
||||
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
|
||||
(e: 'update:activeTab', value: ResponseComposition): void;
|
||||
(e: 'changeExpand', value: boolean): void;
|
||||
(e: 'changeLayout', value: Direction): void;
|
||||
(e: 'change'): void;
|
||||
|
@ -241,7 +192,7 @@
|
|||
sslHandshakeTime,
|
||||
tcpHandshakeTime,
|
||||
transferStartTime,
|
||||
} = props.response.requestResults[0].responseResult;
|
||||
} = props.request.response.requestResults[0].responseResult;
|
||||
return {
|
||||
dnsLookupTime,
|
||||
tcpHandshakeTime,
|
||||
|
@ -255,7 +206,7 @@
|
|||
});
|
||||
// 响应状态码对应颜色
|
||||
const statusCodeColor = computed(() => {
|
||||
const code = props.response.requestResults[0].responseResult.responseCode;
|
||||
const code = props.request.response.requestResults[0].responseResult.responseCode;
|
||||
if (code >= 200 && code < 300) {
|
||||
return 'rgb(var(--success-7)';
|
||||
}
|
||||
|
@ -265,13 +216,8 @@
|
|||
return 'rgb(var(--danger-7)';
|
||||
});
|
||||
|
||||
/** 响应内容编辑状态逻辑 */
|
||||
|
||||
export interface ResponseItem extends TabItem {
|
||||
isDefault?: boolean; // 是否是默认tab
|
||||
code: number; // 状态码
|
||||
showPopConfirm?: boolean; // 是否显示确认弹窗
|
||||
showRenamePopConfirm?: boolean; // 是否显示重命名确认弹窗
|
||||
function handleResponseChange() {
|
||||
emit('change');
|
||||
}
|
||||
|
||||
const activeResponseType = ref<'content' | 'result'>('content');
|
||||
|
@ -279,87 +225,6 @@
|
|||
function setActiveResponse(val: 'content' | 'result') {
|
||||
activeResponseType.value = val;
|
||||
}
|
||||
|
||||
const responseTabs = ref<ResponseItem[]>([
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
label: t('apiTestManagement.response'),
|
||||
closable: false,
|
||||
code: 200,
|
||||
isDefault: true,
|
||||
showPopConfirm: false,
|
||||
showRenamePopConfirm: false,
|
||||
},
|
||||
]);
|
||||
const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
|
||||
|
||||
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
|
||||
responseTabs.value.push({
|
||||
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
|
||||
code: 200,
|
||||
...defaultProps,
|
||||
id: new Date().getTime(),
|
||||
isDefault: false,
|
||||
showPopConfirm: false,
|
||||
showRenamePopConfirm: false,
|
||||
});
|
||||
activeResponse.value = responseTabs.value[responseTabs.value.length - 1];
|
||||
emit('change');
|
||||
}
|
||||
|
||||
const tabMoreActionList: ActionsItem[] = [
|
||||
{
|
||||
label: t('apiTestManagement.setDefault'),
|
||||
eventTag: 'setDefault',
|
||||
},
|
||||
{
|
||||
label: t('common.rename'),
|
||||
eventTag: 'rename',
|
||||
},
|
||||
{
|
||||
label: t('common.copy'),
|
||||
eventTag: 'copy',
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
const renameValue = ref('');
|
||||
|
||||
function handleMoreActionSelect(e: ActionsItem, _tab: ResponseItem) {
|
||||
switch (e.eventTag) {
|
||||
case 'setDefault':
|
||||
responseTabs.value = responseTabs.value.map((tab) => {
|
||||
tab.isDefault = _tab.id === tab.id;
|
||||
return tab;
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
renameValue.value = _tab.label || '';
|
||||
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
|
||||
break;
|
||||
case 'copy':
|
||||
addResponseTab({ ..._tab, label: `${_tab.label}-Copy` });
|
||||
break;
|
||||
case 'delete':
|
||||
_tab.showPopConfirm = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteResponseTab(id: number | string) {
|
||||
responseTabs.value = responseTabs.value.filter((tab) => tab.id !== id);
|
||||
if (id === activeResponse.value.id) {
|
||||
[activeResponse.value] = responseTabs.value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
@ -382,30 +247,4 @@
|
|||
border-color: var(--color-text-n8);
|
||||
gap: 8px;
|
||||
}
|
||||
.response-tab {
|
||||
@apply flex items-center;
|
||||
.response-tab-default-icon {
|
||||
@apply rounded-full;
|
||||
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('@/assets/svg/icons/default.svg') no-repeat;
|
||||
background-size: contain;
|
||||
box-shadow: 0 0 7px 0 rgb(15 0 78 / 9%);
|
||||
}
|
||||
:deep(.response-more-action) {
|
||||
margin-left: 4px;
|
||||
.more-icon-btn {
|
||||
@apply invisible;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
:deep(.response-more-action) {
|
||||
.more-icon-btn {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<MsCodeEditor
|
||||
v-if="activeTab === ResponseComposition.BODY"
|
||||
ref="responseEditorRef"
|
||||
:model-value="props.response.requestResults[0].responseResult?.body"
|
||||
:model-value="props.request.response.requestResults[0].responseResult?.body"
|
||||
:language="responseLanguage"
|
||||
theme="vs"
|
||||
height="100%"
|
||||
|
@ -27,7 +27,7 @@
|
|||
</MsCodeEditor>
|
||||
<MsCodeEditor
|
||||
v-else-if="activeTab === ResponseComposition.CONSOLE"
|
||||
:model-value="props.response.console.trim()"
|
||||
:model-value="props.request.response.console.trim()"
|
||||
:language="LanguageEnum.PLAINTEXT"
|
||||
theme="MS-text"
|
||||
height="100%"
|
||||
|
@ -72,14 +72,12 @@
|
|||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ResponseResult } from '@/models/apiTest/common';
|
||||
import { ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
response: ResponseResult;
|
||||
request?: RequestParam;
|
||||
request: RequestParam;
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -109,13 +107,14 @@
|
|||
// value: ResponseComposition.ASSERTION,
|
||||
// }, // TODO:断言暂时没加
|
||||
];
|
||||
const activeTab = defineModel<keyof typeof ResponseComposition>('activeTab', {
|
||||
const activeTab = defineModel<ResponseComposition>('activeTab', {
|
||||
required: true,
|
||||
default: ResponseComposition.BODY,
|
||||
});
|
||||
|
||||
// 响应体语言类型
|
||||
const responseLanguage = computed(() => {
|
||||
const { contentType } = props.response.requestResults[0].responseResult;
|
||||
const { contentType } = props.request.response.requestResults[0].responseResult;
|
||||
if (contentType.includes('json')) {
|
||||
return LanguageEnum.JSON;
|
||||
}
|
||||
|
@ -132,7 +131,7 @@
|
|||
|
||||
function copyScript() {
|
||||
if (isSupported) {
|
||||
copy(props.response.requestResults[0].responseResult.body);
|
||||
copy(props.request.response.requestResults[0].responseResult.body);
|
||||
Message.success(t('common.copySuccess'));
|
||||
} else {
|
||||
Message.warning(t('apiTestDebug.copyNotSupport'));
|
||||
|
@ -142,16 +141,16 @@
|
|||
function getResponsePreContent(type: keyof typeof ResponseComposition) {
|
||||
switch (type) {
|
||||
case ResponseComposition.HEADER:
|
||||
return props.response.requestResults[0].responseResult?.headers.trim();
|
||||
return props.request.response.requestResults[0].responseResult?.headers.trim();
|
||||
case ResponseComposition.REAL_REQUEST:
|
||||
return props.response.requestResults[0].body
|
||||
return props.request.response.requestResults[0].body
|
||||
? `${t('apiTestDebug.requestUrl')}:\n${props.request?.url}\n${t('apiTestDebug.header')}:\n${
|
||||
props.response.requestResults[0].headers
|
||||
}\nBody:\n${props.response.requestResults[0].body.trim()}`
|
||||
props.request.response.requestResults[0].headers
|
||||
}\nBody:\n${props.request.response.requestResults[0].body.trim()}`
|
||||
: '';
|
||||
// case ResponseComposition.EXTRACT:
|
||||
// return Object.keys(props.response.extract)
|
||||
// .map((e) => `${e}: ${props.response.extract[e]}`)
|
||||
// return Object.keys(props.request.response.extract)
|
||||
// .map((e) => `${e}: ${props.request.response.extract[e]}`)
|
||||
// .join('\n'); // TODO:断言暂时没加
|
||||
default:
|
||||
return '';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
|
||||
import { ExecuteBody } from '@/models/apiTest/debug';
|
||||
import { RequestParamsType } from '@/enums/apiEnum';
|
||||
|
@ -122,7 +122,7 @@ export function filterKeyValParams(params: Record<string, any>[], defaultParamIt
|
|||
delete lastData.enable;
|
||||
delete defaultParam.id;
|
||||
delete defaultParam.enable;
|
||||
const lastDataIsDefault = JSON.stringify(lastData) === JSON.stringify(defaultParam);
|
||||
const lastDataIsDefault = isEqual(lastData, defaultParam);
|
||||
let validParams: Record<string, any>[] = [];
|
||||
if (lastDataIsDefault) {
|
||||
// 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个
|
||||
|
|
|
@ -28,7 +28,13 @@
|
|||
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
|
||||
<popConfirm
|
||||
v-if="hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD'])"
|
||||
mode="add"
|
||||
:all-names="rootModulesName"
|
||||
parent-id="NONE"
|
||||
@add-finish="initModules"
|
||||
>
|
||||
<MsButton v-permission="['PROJECT_API_DEBUG:READ+ADD']" type="icon" class="!mr-0 p-[2px]">
|
||||
<MsIcon
|
||||
type="icon-icon_create_planarity"
|
||||
|
@ -87,8 +93,7 @@
|
|||
</template>
|
||||
<template #extra="nodeData">
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
v-permission="['PROJECT_API_DEBUG:READ+UPDATE']"
|
||||
v-if="nodeData.id !== 'root' && hasAnyPermission(['PROJECT_API_DEBUG:READ+UPDATE'])"
|
||||
mode="rename"
|
||||
:parent-id="nodeData.id"
|
||||
:node-id="nodeData.id"
|
||||
|
@ -102,8 +107,7 @@
|
|||
</popConfirm>
|
||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块;API不可添加子模块 -->
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root' && nodeData.type !== 'API'"
|
||||
v-permission="['PROJECT_API_DEBUG:READ+ADD']"
|
||||
v-if="nodeData.id !== 'root' && nodeData.type !== 'API' && hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD'])"
|
||||
mode="add"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:parent-id="nodeData.id"
|
||||
|
|
|
@ -41,6 +41,9 @@
|
|||
:execute-api="executeDebug"
|
||||
:local-execute-api="localExecuteApiDebug"
|
||||
:upload-temp-file-api="uploadTempFile"
|
||||
:file-save-as-source-id="activeDebug.id"
|
||||
:file-save-as-api="transferFile"
|
||||
:file-module-options-api="getTransferOptions"
|
||||
:permission-map="{
|
||||
execute: 'PROJECT_API_DEBUG:READ+EXECUTE',
|
||||
update: 'PROJECT_API_DEBUG:READ+UPDATE',
|
||||
|
@ -104,7 +107,9 @@
|
|||
addDebug,
|
||||
executeDebug,
|
||||
getDebugDetail,
|
||||
getTransferOptions,
|
||||
localExecuteApiDebug,
|
||||
transferFile,
|
||||
updateDebug,
|
||||
uploadTempFile,
|
||||
} from '@/api/modules/api-test/debug';
|
||||
|
|
|
@ -107,6 +107,7 @@
|
|||
RequestBodyFormat,
|
||||
RequestComposition,
|
||||
RequestMethods,
|
||||
ResponseBodyFormat,
|
||||
ResponseComposition,
|
||||
} from '@/enums/apiEnum';
|
||||
|
||||
|
@ -250,6 +251,25 @@
|
|||
},
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
response: cloneDeep(defaultResponse),
|
||||
responseDefinition: {
|
||||
id: 'default',
|
||||
defaultFlag: true,
|
||||
name: t('common.success'),
|
||||
headers: [],
|
||||
body: {
|
||||
bodyType: ResponseBodyFormat.JSON,
|
||||
jsonBody: {
|
||||
jsonValue: '',
|
||||
},
|
||||
xmlBody: { value: '' },
|
||||
binaryBody: {
|
||||
description: '',
|
||||
file: undefined,
|
||||
},
|
||||
rawBody: { value: '' },
|
||||
},
|
||||
statusCode: 200,
|
||||
},
|
||||
isNew: true,
|
||||
};
|
||||
|
||||
|
|
|
@ -86,4 +86,6 @@ export default {
|
|||
'apiTestManagement.setDefault': '设为默认',
|
||||
'apiTestManagement.confirmDelete': '确认删除 {name} 吗?',
|
||||
'apiTestManagement.response': '响应{count}',
|
||||
'apiTestManagement.responseCode': '响应码',
|
||||
'apiTestManagement.dynamicConversion': '动态转换',
|
||||
};
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<template>
|
||||
<div class="invite-page">
|
||||
<div class="form-box w-1/3 rounded-[12px] bg-white">
|
||||
<a-empty v-if="isInviteOverTime" :description="t('invite.overTime')" class="h-[400px] w-full">
|
||||
<template #image>
|
||||
<icon-close-circle-fill :size="68" />
|
||||
</template>
|
||||
</a-empty>
|
||||
<div v-else class="form-box w-1/3 rounded-[12px] bg-white">
|
||||
<div class="form-box-title">{{ t('invite.title') }}</div>
|
||||
<a-form
|
||||
ref="registerFormRef"
|
||||
|
@ -38,13 +43,15 @@
|
|||
|
||||
import MsPasswordInput from '@/components/pure/ms-password-input/index.vue';
|
||||
|
||||
import { registerByInvite } from '@/api/modules/setting/user';
|
||||
import { registerByInvite, validInvite } from '@/api/modules/setting/user';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { encrypted, sleep } from '@/utils';
|
||||
import { validatePasswordLength, validateWordPassword } from '@/utils/validate';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const form = ref({
|
||||
|
@ -86,10 +93,19 @@
|
|||
],
|
||||
};
|
||||
|
||||
function validatePsw(value: string) {
|
||||
pswValidateRes.value = validateWordPassword(value);
|
||||
pswLengthValidateRes.value = validatePasswordLength(value);
|
||||
}
|
||||
const isInviteOverTime = ref(false); // 邀请是否过期
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
appStore.showLoading();
|
||||
await validInvite(route.query.inviteId as string);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
isInviteOverTime.value = true;
|
||||
} finally {
|
||||
appStore.hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
function confirmInvite() {
|
||||
registerFormRef.value?.validate(async (errors) => {
|
||||
|
@ -160,4 +176,7 @@
|
|||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
:deep(.arco-empty-description) {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
|||
'invite.passwordLengthRule': 'The length is 8-32 digits',
|
||||
'invite.passwordWordRule': 'Must contain numbers and letters, Chinese or spaces are not allowed',
|
||||
'invite.success': 'Registered successfully',
|
||||
'invite.overTime': 'The invitation link has expired',
|
||||
'personal.info': 'My Info',
|
||||
'personal.switchOrg': 'Switch Org',
|
||||
'personal.searchOrgPlaceholder': 'Please enter organization name',
|
||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
|||
'invite.passwordLengthRule': '长度为8-32位',
|
||||
'invite.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
|
||||
'invite.success': '注册成功',
|
||||
'invite.overTime': '邀请链接已过期',
|
||||
'personal.info': '个人信息',
|
||||
'personal.switchOrg': '切换组织',
|
||||
'personal.searchOrgPlaceholder': '请输入组织名称',
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
:upload-image="handleUploadImage"
|
||||
/>
|
||||
</a-form-item>
|
||||
<AddAttachment v-model:file-list="fileList" @change="handleChange" @link-file="associatedFile" />
|
||||
<AddAttachment v-model:file-list="fileList" multiple @change="handleChange" @link-file="associatedFile" />
|
||||
</a-form>
|
||||
<!-- 文件列表开始 -->
|
||||
<div class="w-[90%]">
|
||||
|
|
|
@ -105,6 +105,7 @@
|
|||
</a-tooltip>
|
||||
<template v-if="item.key === 'name'">
|
||||
<popConfirm
|
||||
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
|
||||
mode="fileRename"
|
||||
:field-config="{
|
||||
field: detail.name,
|
||||
|
@ -114,9 +115,9 @@
|
|||
:all-names="[]"
|
||||
@rename-finish="detailDrawerRef?.initDetail"
|
||||
>
|
||||
<MsButton v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']" class="!mr-0 ml-[8px]">{{
|
||||
t('common.rename')
|
||||
}}</MsButton>
|
||||
<MsButton class="!mr-0 ml-[8px]">
|
||||
{{ t('common.rename') }}
|
||||
</MsButton>
|
||||
</popConfirm>
|
||||
<template v-if="UploadAcceptEnum.image.includes(fileType)">
|
||||
<a-divider
|
||||
|
@ -130,6 +131,7 @@
|
|||
</template>
|
||||
<template v-if="item.key === 'desc'">
|
||||
<popConfirm
|
||||
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
|
||||
mode="fileUpdateDesc"
|
||||
:title="t('project.fileManagement.desc')"
|
||||
:field-config="{
|
||||
|
@ -142,9 +144,7 @@
|
|||
:all-names="[]"
|
||||
@update-desc-finish="detailDrawerRef?.initDetail"
|
||||
>
|
||||
<MsButton v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']" class="ml-[8px]"
|
||||
><MsIcon type="icon-icon_edit_outlined"></MsIcon
|
||||
></MsButton>
|
||||
<MsButton class="ml-[8px]"> <MsIcon type="icon-icon_edit_outlined" /></MsButton>
|
||||
</popConfirm>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -39,8 +39,7 @@
|
|||
<template v-if="!props.isModal" #extra="nodeData">
|
||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']"
|
||||
v-if="nodeData.id !== 'root' && hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"
|
||||
mode="add"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:parent-id="nodeData.id"
|
||||
|
@ -52,8 +51,7 @@
|
|||
</MsButton>
|
||||
</popConfirm>
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']"
|
||||
v-if="nodeData.id !== 'root' && hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
|
||||
mode="rename"
|
||||
:parent-id="nodeData.id"
|
||||
:node-id="nodeData.id"
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
</template>
|
||||
<template #itemAction="{ item }">
|
||||
<popConfirm
|
||||
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
|
||||
mode="repositoryRename"
|
||||
:node-id="item.id"
|
||||
:field-config="{ field: renameStorageTitle }"
|
||||
|
@ -161,6 +162,7 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
import { validateGitUrl } from '@/utils/validate';
|
||||
|
||||
import { Repository, RepositoryInfo } from '@/models/projectManagement/file';
|
||||
|
|
|
@ -24,8 +24,12 @@
|
|||
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
<a-dropdown trigger="click" @select="handleAddSelect">
|
||||
<MsButton v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']" type="icon" class="!mr-0 p-[2px]">
|
||||
<a-dropdown
|
||||
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"
|
||||
trigger="click"
|
||||
@select="handleAddSelect"
|
||||
>
|
||||
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||
<MsIcon
|
||||
type="icon-icon_create_planarity"
|
||||
size="18"
|
||||
|
@ -38,7 +42,7 @@
|
|||
</template>
|
||||
</a-dropdown>
|
||||
<popConfirm
|
||||
v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']"
|
||||
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"
|
||||
mode="add"
|
||||
:all-names="rootModulesName"
|
||||
parent-id="none"
|
||||
|
@ -104,6 +108,7 @@
|
|||
|
||||
import { getModulesCount } from '@/api/modules/project-management/fileManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
|
||||
import { FileListQueryParams } from '@/models/projectManagement/file';
|
||||
|
||||
|
@ -246,7 +251,7 @@
|
|||
@apply bg-white;
|
||||
|
||||
min-width: 1000px;
|
||||
height: calc(100vh - 88px);
|
||||
height: calc(100vh - 76px);
|
||||
border-radius: var(--border-radius-large);
|
||||
.folder {
|
||||
@apply flex cursor-pointer items-center justify-between;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MsCard
|
||||
ref="fullRef"
|
||||
:special-height="132"
|
||||
:special-height="127"
|
||||
show-full-screen
|
||||
hide-back
|
||||
hide-footer
|
||||
|
@ -300,9 +300,10 @@
|
|||
);
|
||||
|
||||
function handleToggleFullScreen(val: boolean) {
|
||||
propsRes.value.heightUsed = val ? 224 : 428;
|
||||
propsRes.value.heightUsed = val ? 214 : 428;
|
||||
}
|
||||
|
||||
// TODO:合并单元格 arco 组件暂时有 bug 未解决,已提 issue
|
||||
function spanMethod(data: {
|
||||
record: TableData;
|
||||
column: TableColumnData | TableOperationColumn;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<MsCard :min-width="1060" :special-height="132" simple>
|
||||
<MsCard :min-width="1060" :special-height="127" simple>
|
||||
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" closable @close="addVisited">
|
||||
{{ t('project.messageManagement.botListTips') }}
|
||||
<template #close-element>
|
||||
|
@ -414,7 +414,9 @@
|
|||
innerHTML: `<div>${t(
|
||||
robot.enable ? 'project.messageManagement.disableContent' : 'project.messageManagement.enableContent',
|
||||
{ robot: robot.name }
|
||||
)}</div><div>${robot.platform === 'MAIL' && !robot.enable ? t('project.messageManagement.enableEmailContentTip') : ''}</div>`,
|
||||
)}</div><div>${
|
||||
robot.platform === 'MAIL' && !robot.enable ? t('project.messageManagement.enableEmailContentTip') : ''
|
||||
}</div>`,
|
||||
}),
|
||||
okText: t(robot.enable ? 'project.messageManagement.disableConfirm' : 'project.messageManagement.enableConfirm', {
|
||||
robot: robot.name,
|
||||
|
|
|
@ -16,11 +16,12 @@
|
|||
label: 'name',
|
||||
email: 'email',
|
||||
}"
|
||||
:search-keys="['label', 'email']"
|
||||
:search-keys="['name', 'email']"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:option-tooltip-content="(item) => `${item.name}(${item.email})`"
|
||||
:option-label-render="
|
||||
(item) => `${item.label}<span class='text-[var(--color-text-2)]'>(${item.email})</span>`
|
||||
(item) => `${item.name}<span class='text-[var(--color-text-2)]'>(${item.email})</span>`
|
||||
"
|
||||
allow-search
|
||||
allow-clear
|
||||
|
|
|
@ -194,12 +194,18 @@
|
|||
></MsBatchForm>
|
||||
<!-- TODO:代码编辑器懒加载 -->
|
||||
<div v-show="form.addType === 'multiple'">
|
||||
<MsCodeEditor v-model:model-value="editorContent" width="100%" height="400px" theme="MS-text">
|
||||
<template #rightTitle>
|
||||
<MsCodeEditor
|
||||
v-model:model-value="editorContent"
|
||||
width="100%"
|
||||
height="400px"
|
||||
theme="MS-text"
|
||||
:show-theme-change="false"
|
||||
>
|
||||
<template #leftTitle>
|
||||
<a-form-item
|
||||
:label="t('system.resourcePool.batchAddResource')"
|
||||
asterisk-position="end"
|
||||
class="hide-wrapper mb-0"
|
||||
class="hide-wrapper mb-0 w-auto"
|
||||
required
|
||||
>
|
||||
</a-form-item>
|
||||
|
@ -345,6 +351,7 @@
|
|||
import { computed, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
|
@ -586,8 +593,10 @@
|
|||
for (let i = 0; i < nodesList?.length; i++) {
|
||||
const node = nodesList[i];
|
||||
// 按顺序拼接:ip、port、monitor、concurrentNumber
|
||||
if (Object.values(node).every((e) => e !== '')) {
|
||||
res += `${node.ip},${node.port},${node.monitor},${node.concurrentNumber}\r`;
|
||||
if (!Object.values(node).every((e) => isEmpty(e))) {
|
||||
res += `${node.ip},${node.port === undefined ? '' : node.port},${
|
||||
node.monitor === undefined ? '' : node.monitor
|
||||
},${node.concurrentNumber === undefined ? '' : node.concurrentNumber}\r`;
|
||||
}
|
||||
}
|
||||
editorContent.value = res;
|
||||
|
@ -792,7 +801,7 @@
|
|||
* 校验批量添加的资源信息
|
||||
* @param cb 校验通过后的回调函数
|
||||
*/
|
||||
function validateBtachNodes(cb: () => void) {
|
||||
function validateBatchNodes(cb: () => void) {
|
||||
if (
|
||||
form.value.testResourceDTO.nodesList.some((e) => {
|
||||
return Object.values(e).every((v) => v !== '') && e.concurrentNumber > 0;
|
||||
|
@ -824,7 +833,7 @@
|
|||
}
|
||||
// node 资源批量添加时,先将代码编辑器的值解析到表单对象中,再校验
|
||||
analyzeCode();
|
||||
validateBtachNodes(save);
|
||||
validateBatchNodes(save);
|
||||
return false;
|
||||
}
|
||||
return save();
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
v-permission="['SYSTEM_USER:READ+UPDATE', 'SYSTEM_USER:READ+DELETE']"
|
||||
:list="tableActions"
|
||||
@select="handleSelect($event, record)"
|
||||
></MsTableMoreAction>
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</ms-base-table>
|
||||
|
@ -145,7 +145,7 @@
|
|||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button type="secondary" :disabled="loading" @click="handleBeforeClose">
|
||||
<a-button type="secondary" :disabled="loading" @click="cancelCreate">
|
||||
{{ t('system.user.editUserModalCancelCreate') }}
|
||||
</a-button>
|
||||
<a-button v-if="userFormMode === 'create'" type="secondary" :loading="loading" @click="saveAndContinue">
|
||||
|
|
Loading…
Reference in New Issue