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