feat(上传附件组件): 新增文件转存功能&部分高优先级bug解决

This commit is contained in:
baiqi 2024-03-03 13:45:26 +08:00 committed by Craftsman
parent e3caf65b33
commit 569ed4c933
52 changed files with 1148 additions and 416 deletions

View File

@ -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,

View File

@ -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 });
}

View File

@ -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 });
}

View File

@ -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'; // 文件转存

View File

@ -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';

View File

@ -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

View File

@ -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;

View File

@ -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',
}; };

View File

@ -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': '文件转存成功',
}; };

View File

@ -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>

View File

@ -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

View File

@ -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': '邮箱',

View File

@ -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); // 高亮关键字替换
} }
} }
} }

View File

@ -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',

View File

@ -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);

View File

@ -112,6 +112,6 @@ export const editorProps = {
// 是否显示主题切换 // 是否显示主题切换
showThemeChange: { showThemeChange: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: true, default: false,
}, },
}; };

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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>;
}

View File

@ -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;
} }
// 执行请求配置 // 执行请求配置

View File

@ -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[];
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 || '',

View File

@ -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']);

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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 '';

View File

@ -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) {
// 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个 // 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个

View File

@ -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"

View File

@ -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';

View File

@ -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,
}; };

View File

@ -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': '动态转换',
}; };

View File

@ -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>

View File

@ -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',

View File

@ -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': '请输入组织名称',

View File

@ -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%]">

View File

@ -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>

View File

@ -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"

View File

@ -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';

View 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;

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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];
// ipportmonitorconcurrentNumber // ipportmonitorconcurrentNumber
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();

View File

@ -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">