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',
},
],
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-debugger': 2,
'no-param-reassign': 0,
'prefer-regex-literals': 0,
'import/no-extraneous-dependencies': 0,

View File

@ -12,6 +12,8 @@ import {
LocalExecuteApiDebugUrl,
MoveDebugModuleUrl,
TestMockUrl,
TransferFileUrl,
TransferOptionsUrl,
UpdateApiDebugUrl,
UpdateDebugModuleUrl,
UploadTempFileUrl,
@ -25,7 +27,7 @@ import {
UpdateDebugModule,
UpdateDebugParams,
} from '@/models/apiTest/debug';
import { DragSortParams, ModuleTreeNode, MoveModules } from '@/models/common';
import { DragSortParams, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common';
// 获取模块树
export function getDebugModules() {
@ -101,3 +103,13 @@ export function testMock(key: string) {
export function uploadTempFile(file: File) {
return MSR.uploadFile({ url: UploadTempFileUrl }, { fileList: [file] }, 'file');
}
// 文件转存
export function transferFile(data: TransferFileParams) {
return MSR.post({ url: TransferFileUrl, data });
}
// 文件转存目录
export function getTransferOptions(projectId: string) {
return MSR.get({ url: TransferOptionsUrl, params: projectId });
}

View File

@ -16,6 +16,7 @@ import {
RegisterByInviteUrl,
ResetPasswordUrl,
UpdateUserUrl,
ValidInviteUrl,
} from '@/api/requrls/setting/user';
import type { CommonList, TableQueryParams } from '@/models/common';
@ -118,3 +119,8 @@ export function inviteUser(data: InviteUserParams) {
export function registerByInvite(data: RegisterByInviteParams) {
return MSR.post({ url: RegisterByInviteUrl, data });
}
// 检查邀请链接是否过期
export function validInvite(id: string) {
return MSR.get({ url: ValidInviteUrl, params: id });
}

View File

@ -13,3 +13,5 @@ export const GetDebugModulesUrl = '/api/debug/module/tree'; // 查询模块树
export const DeleteDebugModuleUrl = '/api/debug/module/delete'; // 删除模块
export const DragDebugUrl = '/api/debug/edit/pos'; // 拖拽调试节点
export const UploadTempFileUrl = '/api/debug/upload/temp/file'; // 上传文件
export const TransferOptionsUrl = '/api/debug/transfer/options'; // 文件转存目录
export const TransferFileUrl = '/api/debug/transfer'; // 文件转存

View File

@ -30,3 +30,5 @@ export const GetProjectsUrl = '/system/user/get/project';
export const RegisterByInviteUrl = '/system/user/register-by-invite';
// 邀请用户
export const InviteUserUrl = '/system/user/invite';
// 检查邀请链接是否过期
export const ValidInviteUrl = '/system/user/check-invite';

View File

@ -49,12 +49,13 @@
const { t } = useI18n();
const innerFileList = defineModel<MsFileItem[]>('fileList', {
required: true,
default: () => [],
});
const dropdownVisible = ref(false);
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
fileItem.local = true;
emit('change', _fileList, fileItem);
nextTick(() => {
// emit

View File

@ -9,20 +9,21 @@
{{ t('system.orgTemplate.addAttachment') }}
</a-button>
<template #content>
<a-upload
ref="uploadRef"
<MsUpload
v-model:file-list="innerFileList"
:limit="50"
accept="none"
:auto-upload="false"
:show-file-list="false"
:limit="50"
size-unit="MB"
:multiple="props.multiple"
class="w-full"
@change="handleChange"
>
<template #upload-button>
<a-button type="text" class="arco-dropdown-option !text-[var(--color-text-1)]">
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}
</a-button>
</template>
</a-upload>
<a-button type="text" class="arco-dropdown-option !text-[var(--color-text-1)]">
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}
</a-button>
</MsUpload>
<a-button type="text" class="arco-dropdown-option !text-[var(--color-text-1)]" @click="associatedFile">
<MsIcon type="icon-icon_link-copy_outlined" size="16" />
{{ t('caseManagement.featureCase.associatedFile') }}
@ -36,13 +37,26 @@
</div>
</a-form-item>
<template v-else>
<div v-if="props.multiple" class="flex w-full items-center gap-[4px]">
<dropdownMenu v-model:file-list="innerFileList" @link-file="associatedFile" @change="handleChange" />
<div v-if="props.multiple" class="flex w-full items-center">
<dropdownMenu @link-file="associatedFile" @change="handleChange" />
<saveAsFilePopover
v-if="props.fileSaveAsSourceId"
v-model:visible="saveFilePopoverVisible"
:saving-file="savingFile"
:file-id-key="props.fields.id"
:file-save-as-api="props.fileSaveAsApi"
:file-save-as-source-id="props.fileSaveAsSourceId"
:file-module-options-api="props.fileModuleOptionsApi"
@finish="handleSaveFileFinish"
/>
<a-popover
v-model:popup-visible="inputFilesPopoverVisible"
trigger="click"
position="bottom"
position="bl"
:disabled="inputFiles.length === 0"
content-class="ms-add-attachment-files-popover"
arrow-class="hidden"
:popup-offset="0"
>
<MsTagsInput
v-model:model-value="inputFiles"
@ -104,7 +118,7 @@
</a-tooltip>
<div v-if="file.local === true" class="flex items-center">
<a-tooltip :content="t('ms.add.attachment.saveAs')">
<MsButton type="text" status="secondary" class="!mr-0" @click="handleClose(file)">
<MsButton type="text" status="secondary" class="!mr-0" @click="handleOpenSaveAs(file)">
<MsIcon type="icon-icon_unloading" class="hover:text-[rgb(var(--primary-5))]" size="16" />
</MsButton>
</a-tooltip>
@ -132,7 +146,7 @@
</a-popover>
</div>
<div v-else class="flex w-full items-center gap-[4px]">
<dropdownMenu v-model:file-list="innerFileList" @link-file="associatedFile" @change="handleChange" />
<dropdownMenu @link-file="associatedFile" @change="handleChange" />
<a-input
v-model:model-value="inputFileName"
:class="props.inputClass"
@ -163,22 +177,24 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
import dropdownMenu from './dropdownMenu.vue';
import saveAsFilePopover from './saveAsFilePopover.vue';
import { getAssociatedFileListUrl } from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import { AssociatedList } from '@/models/caseManagement/featureCase';
import { TableQueryParams } from '@/models/common';
import { TableQueryParams, TransferFileParams } from '@/models/common';
import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils';
const props = withDefaults(
defineProps<{
mode: 'button' | 'input';
mode?: 'button' | 'input';
fileList: MsFileItem[]; // TODO:MsFileItem
multiple?: boolean;
inputClass?: string;
@ -188,6 +204,9 @@
id: string; // id
name: string;
};
fileSaveAsSourceId?: string | number; // id
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; //
fileModuleOptionsApi?: (...args) => Promise<any>; //
}>(),
{
mode: 'button',
@ -243,7 +262,6 @@
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
//
const isRepeat = _fileList.filter((item) => item.name === fileItem.name).length > 1;
debugger;
if (isRepeat) {
Message.error(t('ms.add.attachment.repeatFileTip'));
innerFileList.value = _fileList.reduce((prev: MsFileItem[], current: MsFileItem) => {
@ -253,24 +271,21 @@
}
return prev;
}, []);
} else {
innerFileList.value = _fileList.map((item) => ({ ...item, local: true }));
if (props.multiple) {
inputFiles.value = _fileList.map((item) => ({
...item,
value: item?.uid || '',
label: item?.name || '',
local: true,
}));
} else {
inputFileName.value = fileItem.name || '';
}
emit('change', _fileList, { ...fileItem, local: true });
nextTick(() => {
// emit
buttonDropDownVisible.value = false;
} else if (props.multiple) {
innerFileList.value.push(fileItem);
inputFiles.value.push({
...fileItem,
value: fileItem[props.fields.id] || fileItem.uid || '',
label: fileItem[props.fields.name] || fileItem.name || '',
});
} else {
inputFileName.value = fileItem.name || '';
}
emit('change', _fileList, { ...fileItem, local: true });
nextTick(() => {
// emit
buttonDropDownVisible.value = false;
});
}
function associatedFile() {
@ -309,8 +324,9 @@
);
} else {
//
innerFileList.value = fileResultList;
inputFileName.value = fileResultList[0].name || '';
const file = fileResultList[0];
innerFileList.value = [{ ...file, fileId: file.uid || '', fileName: file.name || '' }];
inputFileName.value = file.name || '';
}
emit('change', innerFileList.value);
}
@ -344,8 +360,38 @@
innerFileList.value = [];
emit('change', []);
}
const saveFilePopoverVisible = ref(false);
const savingFile = ref<MsFileItem>();
/**
* 打开文件转存弹窗
* @param item 点击转存的文件标签项
*/
function handleOpenSaveAs(item: TagData) {
inputFilesPopoverVisible.value = false;
// uid
savingFile.value = innerFileList.value.find((file) => (file.uid || file[props.fields.id]) === item.value);
saveFilePopoverVisible.value = true;
}
function handleSaveFileFinish(fileId: string) {
if (savingFile.value) {
savingFile.value.fileId = fileId;
savingFile.value.local = false;
}
}
</script>
<style lang="less">
.ms-add-attachment-files-popover {
padding: 16px;
.arco-popover-content {
margin-top: 0;
}
}
</style>
<style lang="less" scoped>
.file-list {
@apply flex flex-col overflow-y-auto overflow-x-hidden;

View File

@ -8,4 +8,8 @@ export default {
'ms.add.attachment.cancelAssociate': 'Disassociate',
'ms.add.attachment.saveAs': 'Save',
'ms.add.attachment.repeatFileTip': 'File already exists.',
'ms.add.attachment.saveAsTitle': 'Please select the transfer directory',
'ms.add.attachment.saveAsNamePlaceholder': 'Please enter file name',
'ms.add.attachment.saveAsModulePlaceholder': 'Please select the transfer directory',
'ms.add.attachment.saveAsSuccess': 'File transfer successful',
};

View File

@ -8,4 +8,8 @@ export default {
'ms.add.attachment.cancelAssociate': '取消关联',
'ms.add.attachment.saveAs': '转存',
'ms.add.attachment.repeatFileTip': '文件重复',
'ms.add.attachment.saveAsTitle': '请选择转存目录',
'ms.add.attachment.saveAsNamePlaceholder': '请输入文件名称',
'ms.add.attachment.saveAsModulePlaceholder': '请选择转存目录',
'ms.add.attachment.saveAsSuccess': '文件转存成功',
};

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>
</div>
</div>
<div v-if="props.showType === 'commonScript'" class="flex bg-[var(--color-bg-3)]">
<div class="relative w-full">
<MsCodeEditor
ref="codeEditorRef"
v-model:model-value="innerCodeValue"
title=""
:width="expandMenu ? '100%' : '68%'"
height="460px"
theme="vs"
:language="innerLanguagesType"
:read-only="false"
:show-full-screen="false"
:show-theme-change="false"
>
<template #rightBox>
<MsScriptMenu
v-model:expand="expandMenu"
v-model:languagesType="innerLanguagesType"
@insert="insertHandler"
@form-api-import="formApiImport"
@insert-common-script="insertCommonScript"
/>
</template>
</MsCodeEditor>
</div>
<div v-if="props.showType === 'commonScript'" class="relative flex w-full">
<MsCodeEditor
ref="codeEditorRef"
v-model:model-value="innerCodeValue"
title=""
:width="expandMenu ? '100%' : '68%'"
height="460px"
theme="vs"
:language="innerLanguagesType"
:read-only="false"
:show-full-screen="false"
:show-theme-change="false"
>
<template #rightBox>
<MsScriptMenu
v-model:expand="expandMenu"
v-model:languagesType="innerLanguagesType"
@insert="insertHandler"
@form-api-import="formApiImport"
@insert-common-script="insertCommonScript"
/>
</template>
</MsCodeEditor>
</div>
<MsCodeEditor
v-else

View File

@ -7,7 +7,7 @@ export default {
'ms.personal.apiKey': 'APIKEY',
'ms.personal.tripartite': '三方平台账号',
'ms.personal.changeAvatar': '更换头像',
'ms.personal.name': '用户名',
'ms.personal.name': '用户',
'ms.personal.namePlaceholder': '请输入用户名称',
'ms.personal.nameRequired': '用户名称不能为空',
'ms.personal.email': '邮箱',

View File

@ -173,7 +173,7 @@ export default defineComponent(
if (e[key]?.toLowerCase().includes(val.toLowerCase())) {
// 是否匹配
hasMatch = true;
item[props.labelKey || 'label'] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
}
}
}

View File

@ -28,7 +28,7 @@
>
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
<MsIcon v-else type="icon-icon_magnify_outlined" />
{{ t('msCodeEditor.fullScreen') }}
{{ t(isFullScreen ? 'common.offFullScreen' : 'common.fullScreen') }}
</div>
</div>
<div v-if="$slots.subHeader" class="basis-full">
@ -88,7 +88,7 @@
hideFooter: boolean; //
loading: boolean; // loading
isEdit: boolean; //
specialHeight: number; //
specialHeight: number; // autoHeight
hideBack: boolean; //
autoHeight: boolean; //
otherWidth: number; //
@ -145,32 +145,17 @@
const cardOverHeight = computed(() => {
if (isFullScreen.value) {
if (props.hideFooter) {
//
return 62;
}
return 142;
return 106;
}
if (props.simple) {
//
return props.noContentPadding ? 76 + _specialHeight : 124 + _specialHeight;
}
if (props.hideFooter) {
//
return props.noContentPadding ? 140 + _specialHeight : 180 + _specialHeight;
}
return 264 + _specialHeight;
return 190 + _specialHeight;
});
const getComputedContentStyle = computed(() => {
if (props.isFullscreen || isFullScreen.value) {
return {
overflow: 'auto',
width: 'auto',
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight.value}px)`,
};
}
if (props.noContentPadding) {
if (props.isFullscreen || isFullScreen.value || props.noContentPadding) {
return {
overflow: 'auto',
width: 'auto',

View File

@ -1,7 +1,7 @@
<template>
<div
ref="fullRef"
class="h-full overflow-hidden rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[12px]"
class="flex h-full flex-col rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[12px]"
>
<div v-if="showTitleLine" class="mb-[8px] flex items-center justify-between">
<div class="flex flex-wrap gap-[4px]">
@ -33,11 +33,11 @@
<span class="flex items-center gap-[4px] font-medium">{{ title }}</span>
</slot>
</div>
<div>
<div class="ml-auto flex items-center gap-[8px]">
<slot name="rightTitle"> </slot>
<div
v-if="showFullScreen"
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
class="cursor-pointer text-right !text-[var(--color-text-4)]"
@click="toggleFullScreen"
>
<MsIcon v-if="isFullScreen" type="icon-icon_minify_outlined" />
@ -46,12 +46,8 @@
</div>
</div>
</div>
<!-- 这里的 36px 是顶部标题的 36px -->
<div
:class="`flex ${
showTitleLine ? 'h-[calc(100%-32px)]' : 'h-full'
} w-full flex-row overflow-hidden rounded-[var(--border-radius-small)]`"
>
<!-- 这里的 32px 是顶部标题的 32px -->
<div class="flex w-full flex-1 flex-row rounded-[var(--border-radius-small)]">
<div
ref="codeContainerRef"
:class="['ms-code-editor', isFullScreen ? 'ms-code-editor-full-screen' : '', currentTheme]"
@ -327,7 +323,7 @@
<style lang="less" scoped>
.ms-code-editor {
@apply z-10 overflow-hidden;
@apply z-10;
width: v-bind(width);
height: v-bind(height);
@ -336,6 +332,9 @@
color: rgb(var(--primary-5));
}
}
:deep(.overflowingContentWidgets) {
z-index: 9999;
}
}
.ms-code-editor-full-screen {
height: calc(100vh - 66px);

View File

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

View File

@ -29,7 +29,7 @@
<script setup lang="ts">
import { hasAllPermission } from '@/utils/permission';
interface MenuItem {
export interface MenuItem {
title: string;
level: number;
name: string;

View File

@ -8,7 +8,7 @@
>
<slot>
<div :class="['ms-more-action-trigger-content', visible ? 'ms-more-action-trigger-content--focus' : '']">
<MsButton type="text" size="mini" class="more-icon-btn">
<MsButton type="text" size="mini" class="more-icon-btn" @click="visible = !visible">
<MsIcon type="icon-icon_more_outlined" size="16" class="text-[var(--color-text-4)]" />
</MsButton>
</div>

View File

@ -2,7 +2,7 @@
<a-upload
v-if="showDropArea"
v-bind="{ ...props }"
v-model:file-list="fileList"
v-model:file-list="innerFileList"
:accept="
[UploadAcceptEnum.none, UploadAcceptEnum.unknown].includes(UploadAcceptEnum[props.accept])
? '*'
@ -30,7 +30,7 @@
<div v-else class="ms-upload-icon ms-upload-icon--default"></div>
</div>
<!-- 支持多文件上传时不需要展示选择文件后的信息已选的文件使用文件列表搭配展示 -->
<template v-if="fileList.length === 0 || props.multiple">
<template v-if="innerFileList.length === 0 || props.multiple">
<div class="ms-upload-main-text">
{{ t(props.mainText || 'ms.upload.importModalDragText') }}
</div>
@ -47,11 +47,11 @@
</template>
<template v-else>
<div class="ms-upload-main-text w-full">
<a-tooltip :content="fileList[0]?.name">
<span class="one-line-text w-[80%] text-center"> {{ fileList[0]?.name }}</span>
<a-tooltip :content="innerFileList[0]?.name">
<span class="one-line-text w-[80%] text-center"> {{ innerFileList[0]?.name }}</span>
</a-tooltip>
</div>
<div class="ms-upload-sub-text">{{ formatFileSize(fileList[0]?.file?.size || 0) }}</div>
<div class="ms-upload-sub-text">{{ formatFileSize(innerFileList[0]?.file?.size || 0) }}</div>
</template>
</div>
</slot>
@ -77,6 +77,7 @@
// props
type UploadProps = Partial<{
fileList: MsFileItem[];
mainText: string; //
subText: string; //
showSubText: boolean; //
@ -96,7 +97,6 @@
limit: number; //
}> & {
accept: UploadType;
fileList: MsFileItem[];
};
const props = withDefaults(defineProps<UploadProps>(), {
@ -110,35 +110,23 @@
const defaultMaxSize = 50;
const fileList = ref<MsFileItem[]>(props.fileList);
watch(
() => props.fileList,
(val) => {
fileList.value = val;
}
);
watch(
() => fileList.value,
(val) => {
emit('update:fileList', val);
}
);
const innerFileList = defineModel<MsFileItem[]>('fileList', {
default: () => [],
});
const fileIconType = computed(() => {
// (绿)
if (fileList.value.length > 0 && !props.multiple) {
return getFileIcon(fileList.value[0], UploadStatus.done);
if (innerFileList.value.length > 0 && !props.multiple) {
return getFileIcon(innerFileList.value[0], UploadStatus.done);
}
//
return FileIconMap[props.accept][UploadStatus.init];
});
async function beforeUpload(file: File) {
if (!props.multiple && fileList.value.length > 0) {
if (!props.multiple && innerFileList.value.length > 0) {
//
fileList.value = [];
innerFileList.value = [];
}
const maxSize = props.maxSize || defaultMaxSize;
const _maxSize = props.sizeUnit === 'MB' ? maxSize * 1024 * 1024 : maxSize * 1024;

View File

@ -32,7 +32,7 @@ export enum RequestBodyFormat {
RAW = 'RAW',
BINARY = 'BINARY',
}
// 接口响应格式
// 接口响应格式
export enum RequestContentTypeEnum {
JSON = 'application/json',
TEXT = 'application/text',
@ -52,6 +52,14 @@ export enum ResponseComposition {
CONSOLE = 'CONSOLE',
EXTRACT = 'EXTRACT',
ASSERTION = 'ASSERTION',
CODE = 'CODE',
}
// 接口响应体格式
export enum ResponseBodyFormat {
JSON = 'JSON',
XML = 'XML',
RAW = 'RAW',
BINARY = 'BINARY',
}
// 接口定义状态
export enum RequestDefinitionStatus {

View File

@ -16,7 +16,7 @@ export default function useFullScreen(
originalStyle.value = dom.getAttribute('style') || '';
mergeStyles(
dom,
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 100; width: 100%; height: 100%;'
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; width: 100%; height: 100%;'
);
isFullScreen.value = true;
}

View File

@ -1,3 +1,7 @@
import { ResponseBodyFormat } from '@/enums/apiEnum';
import { EnableKeyValueParam, ExecuteBinaryBody, ExecuteJsonBody, ExecuteValueBody } from './debug';
// 获取插件表单选项参数
export interface GetPluginOptionsParams {
orgId: string;
@ -23,7 +27,8 @@ export interface PluginConfig {
options: Record<string, any>;
script: Record<string, any>[];
scriptType: string;
fields?: string[]; // 插件脚本内配置的全部字段集合
apiDebugFields?: string[]; // 接口调试脚本内配置的全部字段集合
apiDefinitionFields?: string[]; // 接口定义脚本内配置的全部字段集合
}
// 响应结果
export interface ResponseResult {
@ -48,3 +53,49 @@ export interface ResponseResult {
}[]; // 请求结果
console: string;
}
// 响应定义-body
export interface ResponseDefinitionBody {
bodyType: ResponseBodyFormat;
jsonBody: ExecuteJsonBody;
xmlBody: ExecuteValueBody;
rawBody: ExecuteValueBody;
binaryBody: ExecuteBinaryBody;
}
export interface ResponseDefinitionHeader extends EnableKeyValueParam {
notBlankValue: boolean;
valid: boolean;
}
// 响应定义
export interface ResponseDefinition {
id: string | number;
statusCode: string | number;
defaultFlag: boolean; // 默认响应标志
name: string; // 响应名称
headers: ResponseDefinitionHeader[];
body: ResponseDefinitionBody;
}
// 接口定义-JsonSchema
export interface JsonSchema {
example: Record<string, any>;
id: string;
title: string;
type: string;
description: string;
items: string;
mock: Record<string, any>;
properties: Record<string, any>;
additionalProperties: string;
required: string[];
pattern: string;
maxLength: number;
minLength: number;
minimum: number;
maximum: number;
schema: string;
format: string;
enumString: string[];
enumInteger: number[];
enumNumber: number[];
extensions: Record<string, any>;
}

View File

@ -20,6 +20,8 @@ import {
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
import { JsonSchema } from './common';
// 条件操作类型
export type ConditionType = RequestConditionProcessor;
// 断言-匹配条件规则
@ -78,15 +80,18 @@ export interface ExecuteBinaryBody {
file?: {
fileId: string;
fileName: string;
fileAlias: string; // 文件别名
local: boolean; // 是否是本地上传的文件
delete?: boolean; // 关联文件是否被删除
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
};
sendAsBody?: boolean; // 是否作为正文发送,只有 mock 有此字段
}
// 接口请求json-body参数集合信息
export interface ExecuteJsonBody {
enableJsonSchema?: boolean;
enableTransition?: boolean;
jsonSchema?: string;
jsonSchema?: JsonSchema;
jsonValue: string;
}
// 执行请求配置

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 { StatusType } from '@/enums/caseEnum';

View File

@ -75,3 +75,12 @@ export interface DragSortParams {
moveId: string;
moduleId?: string;
}
// 文件转存入参
export interface TransferFileParams {
projectId: string;
sourceId: string | number;
name?: string;
fileId: string;
local: true;
moduleId: string;
}

View File

@ -2,15 +2,15 @@
<div class="condition-content">
<!-- 脚本操作 -->
<template v-if="condition.processorType === RequestConditionProcessor.SCRIPT">
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[16px]">
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[8px]">
<a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio :value="true">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<div
v-if="!condition.enableCommonScript"
class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
class="relative flex-1 rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)]"
>
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] top-[12px] z-10 w-[calc(100%-24px)]">
<a-input
ref="scriptNameInputRef"
v-model:model-value="condition.scriptName"
@ -21,12 +21,12 @@
@blur="isShowEditScriptNameInput = false"
/>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center justify-between px-[12px] pt-[12px]">
<div class="flex items-center">
<a-tooltip :content="condition.commonScriptInfo?.name">
<a-tooltip :content="condition.scriptName">
<div class="script-name-container">
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ condition.commonScriptInfo?.name }}
{{ condition.scriptName }}
</div>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
</div>
@ -741,7 +741,7 @@ if (!result){
@apply flex flex-1 flex-col overflow-y-auto;
.ms-scroll-bar();
padding: 16px;
padding: 8px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
.script-name-container {

View File

@ -174,6 +174,9 @@
id: 'fileId',
name: 'fileName',
}"
:file-save-as-source-id="props.fileSaveAsSourceId"
:file-save-as-api="props.fileSaveAsApi"
:file-module-options-api="props.fileModuleOptionsApi"
input-class="param-input h-[24px]"
input-size="small"
tag-size="small"
@ -469,6 +472,7 @@
import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app';
import { TransferFileParams } from '@/models/common';
import { ProjectOptionItem } from '@/models/projectManagement/environmental';
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
@ -518,6 +522,9 @@
columnIndex: number;
}) => { rowspan?: number; colspan?: number } | void;
uploadTempFileApi?: (...args) => Promise<any>; //
fileSaveAsSourceId?: string | number; // id
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; //
fileModuleOptionsApi?: (...args) => Promise<any>; //
}>(),
{
params: () => [],
@ -771,9 +778,11 @@
() => props.params,
(arr) => {
if (arr.length > 0) {
// id
let hasNoIdItem = false; // id
propsRes.value.data = arr.map((item, i) => {
if (!item.id) {
// id
hasNoIdItem = true;
return {
...item,
id: new Date().getTime() + i,
@ -781,7 +790,7 @@
}
return item;
});
if (!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
addTableLine(arr.length - 1);
}
} else {
@ -816,18 +825,21 @@
try {
if (props.uploadTempFileApi && file?.local) {
//
const fileItem = files[0];
appStore.showLoading();
const res = await props.uploadTempFileApi(fileItem.file);
record.files = [
{
...fileItem,
fileId: res.data,
fileName: fileItem.name || '',
local: true,
},
];
const res = await props.uploadTempFileApi(file.file);
for (let i = 0; i < record.files.length; i++) {
const item = record.files[i];
if ([item.fileId, item.uid].includes(file.uid)) {
record.files[i] = {
...file,
fileId: res.data,
fileName: file.name || '',
};
break;
}
}
} else {
//
record.files = files.map((e) => ({
...e,
fileId: e.uid || e.fileId || '',

View File

@ -7,6 +7,7 @@
:cancel-button-props="{ disabled: loading }"
:on-before-ok="beforeConfirm"
:popup-container="props.popupContainer || 'body'"
:popup-offset="props.popupOffset"
@popup-visible-change="reset"
>
<template #content>
@ -72,6 +73,7 @@
fieldConfig?: FieldConfig;
parentId?: string; // id
nodeId?: string; // id
popupOffset?: number;
}>();
const emit = defineEmits(['update:visible', 'close', 'addFinish', 'renameFinish', 'updateDescFinish']);

View File

@ -33,6 +33,9 @@
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_DATA"
:default-param-item="defaultBodyParamsItem"
:upload-temp-file-api="props.uploadTempFileApi"
:file-save-as-source-id="props.fileSaveAsSourceId"
:file-save-as-api="props.fileSaveAsApi"
:file-module-options-api="props.fileModuleOptionsApi"
@change="handleParamTableChange"
/>
<paramTable
@ -79,22 +82,16 @@
</a-tooltip>
</div> -->
</div>
<div v-else class="flex h-[calc(100%-100px)]">
<div v-else class="flex h-[calc(100%-34px)]">
<MsCodeEditor
v-model:model-value="currentBodyCode"
class="flex-1"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:language="currentCodeLanguage"
>
<template #rightTitle>
<div class="flex flex-col">
<div class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t('apiTestDebug.batchAddParamsTip1') }}
</div>
</div>
</template>
</MsCodeEditor>
</div>
</template>
@ -114,6 +111,7 @@
import useAppStore from '@/store/modules/app';
import { ExecuteBody } from '@/models/apiTest/debug';
import { TransferFileParams } from '@/models/common';
import { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
@ -124,6 +122,9 @@
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
uploadTempFileApi?: (...args) => Promise<any>; //
fileSaveAsSourceId?: string | number; // id
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; //
fileModuleOptionsApi?: (...args) => Promise<any>; //
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;

View File

@ -160,6 +160,9 @@
:layout="activeLayout"
:second-box-height="secondBoxHeight"
:upload-temp-file-api="props.uploadTempFileApi"
:file-save-as-source-id="props.fileSaveAsSourceId"
:file-save-as-api="props.fileSaveAsApi"
:file-module-options-api="props.fileModuleOptionsApi"
@change="handleActiveDebugChange"
/>
<debugQuery
@ -208,11 +211,11 @@
v-model:active-layout="activeLayout"
v-model:active-tab="requestVModel.responseActiveTab"
:is-expanded="isExpanded"
:response="requestVModel.response"
:response-definition="requestVModel.responseDefinition"
:hide-layout-switch="props.hideResponseLayoutSwitch"
:request="requestVModel"
:loading="requestVModel.executeLoading"
:is-edit="props.isDefinition"
:upload-temp-file-api="props.uploadTempFileApi"
@change-expand="changeExpand"
@change-layout="handleActiveLayoutChange"
@change="handleActiveDebugChange"
@ -294,9 +297,9 @@
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { hasAnyPermission } from '@/utils/permission';
import { PluginConfig } from '@/models/apiTest/common';
import { PluginConfig, ResponseDefinition } from '@/models/apiTest/common';
import { ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common';
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import {
RequestAuthType,
RequestBodyFormat,
@ -324,7 +327,10 @@
protocol: string;
activeTab: RequestComposition;
}
export type RequestParam = ExecuteHTTPRequestFullParams & RequestCustomAttr & TabItem;
export type RequestParam = ExecuteHTTPRequestFullParams & {
responseDefinition?: ResponseDefinition;
} & RequestCustomAttr &
TabItem;
const props = defineProps<{
request: RequestParam; //
@ -337,6 +343,9 @@
createApi: (...args) => Promise<any>; //
updateApi: (...args) => Promise<any>; //
uploadTempFileApi?: (...args) => Promise<any>; //
fileSaveAsSourceId?: string | number; // id
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; //
fileModuleOptionsApi?: (...args) => Promise<any>; //
permissionMap: {
execute: string;
create: string;
@ -528,7 +537,7 @@
const formData = tempForm || requestVModel.value;
if (fApi.value) {
const form = {};
pluginScriptMap.value[requestVModel.value.protocol].fields?.forEach((key) => {
pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields?.forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(form);

View File

@ -1,7 +1,454 @@
<template>
<div> </div>
<div class="mt-[8px] w-full">
<MsEditableTab
v-model:active-tab="activeResponse"
v-model:tabs="responseTabs"
at-least-one
hide-more-action
@add="addResponseTab"
>
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.isDefault" class="response-tab-default-icon"></div>
{{ tab.label }}({{ tab.code }})
<MsMoreAction
:list="
tab.isDefault
? tabMoreActionList.filter((e) => e.eventTag !== 'setDefault' && e.eventTag !== 'delete')
: tabMoreActionList
"
class="response-more-action"
@select="(e) => handleMoreActionSelect(e, tab as ResponseItem)"
/>
<popConfirm
v-model:visible="tab.showRenamePopConfirm"
mode="tabRename"
:field-config="{ field: tab.label }"
:all-names="responseTabs.map((e) => e.label)"
:popup-offset="20"
@rename-finish="(val) => (tab.label = val)"
>
<span :id="`renameSpan${tab.id}`" class="relative"></span>
</popConfirm>
<a-popconfirm
v-model:popup-visible="tab.showPopConfirm"
position="bottom"
content-class="w-[300px]"
:ok-text="t('common.confirm')"
:popup-offset="20"
@ok="() => handleDeleteResponseTab(tab.id)"
>
<template #icon>
<icon-exclamation-circle-fill class="!text-[rgb(var(--danger-6))]" />
</template>
<template #content>
<div class="font-semibold text-[var(--color-text-1)]">
{{ t('apiTestManagement.confirmDelete', { name: tab.label }) }}
</div>
</template>
<div class="relative"></div>
</a-popconfirm>
</div>
</template>
</MsEditableTab>
</div>
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]">
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
</a-tabs>
<div class="response-container">
<div class="mb-[8px] flex items-center justify-between">
<a-radio-group
v-model:model-value="innerResponse.body.bodyType"
type="button"
size="small"
@change="(val) => changeBodyFormat(val as ResponseBodyFormat)"
>
<a-radio v-for="item of ResponseBodyFormat" :key="item" :value="item">
{{ ResponseBodyFormat[item].toLowerCase() }}
</a-radio>
</a-radio-group>
<div class="flex items-center gap-[24px]">
<a-radio-group size="mini" @change="(val) => changeJsonBodyFormat(val as ResponseBodyFormat)">
<a-radio value="Json" :default-checked="!innerResponse.body.jsonBody.enableJsonSchema">Json</a-radio>
<a-radio class="mr-0" value="JsonSchema" :default-checked="innerResponse.body.jsonBody.enableJsonSchema">
Json Schema
</a-radio>
</a-radio-group>
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="innerResponse.body.jsonBody.enableTransition" size="small" type="line" />
{{ t('apiTestManagement.dynamicConversion') }}
</div>
</div>
</div>
<template v-if="activeTab === ResponseComposition.BODY">
<div
v-if="
[ResponseBodyFormat.JSON, ResponseBodyFormat.XML, ResponseBodyFormat.RAW].includes(
innerResponse.body.bodyType
)
"
class="h-[calc(100%-35px)]"
>
<MsCodeEditor
ref="responseEditorRef"
v-model:model-value="currentBodyCode"
:language="currentCodeLanguage"
theme="vs"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</div>
<div v-else>
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input
v-model:model-value="innerResponse.body.binaryBody.description"
:placeholder="t('common.desc')"
:max-length="255"
/>
<MsAddAttachment
v-model:file-list="fileList"
mode="input"
:multiple="false"
:fields="{
id: 'fileId',
name: 'fileName',
}"
@change="handleFileChange"
/>
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
<style lang="less" scoped></style>
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ResponseDefinition } from '@/models/apiTest/common';
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
const props = defineProps<{
response: ResponseDefinition;
uploadTempFileApi?: (...args) => Promise<any>; //
}>();
const emit = defineEmits<{
(e: 'change'): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
export interface ResponseItem extends TabItem {
isDefault?: boolean; // tab
code: number; //
showPopConfirm?: boolean; //
showRenamePopConfirm?: boolean; //
}
const activeTab = defineModel<ResponseComposition>('activeTab', {
required: true,
default: ResponseComposition.BODY,
});
const innerResponse = defineModel<ResponseDefinition>('response', {
required: true,
});
const responseTabs = ref<ResponseItem[]>([
{
id: new Date().getTime(),
label: t('apiTestManagement.response'),
closable: false,
code: 200,
isDefault: true,
showPopConfirm: false,
showRenamePopConfirm: false,
},
]);
const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
responseTabs.value.push({
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
code: 200,
...defaultProps,
id: new Date().getTime(),
isDefault: false,
showPopConfirm: false,
showRenamePopConfirm: false,
});
activeResponse.value = responseTabs.value[responseTabs.value.length - 1];
emit('change');
}
const tabMoreActionList: ActionsItem[] = [
{
label: t('apiTestManagement.setDefault'),
eventTag: 'setDefault',
},
{
label: t('common.rename'),
eventTag: 'rename',
},
{
label: t('common.copy'),
eventTag: 'copy',
},
{
isDivider: true,
},
{
label: t('common.delete'),
eventTag: 'delete',
danger: true,
},
];
const renameValue = ref('');
function handleMoreActionSelect(e: ActionsItem, _tab: ResponseItem) {
switch (e.eventTag) {
case 'setDefault':
responseTabs.value = responseTabs.value.map((tab) => {
tab.isDefault = _tab.id === tab.id;
return tab;
});
break;
case 'rename':
renameValue.value = _tab.label || '';
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
break;
case 'copy':
addResponseTab({ ..._tab, label: `${_tab.label}-Copy` });
break;
case 'delete':
_tab.showPopConfirm = true;
break;
default:
break;
}
}
function handleDeleteResponseTab(id: number | string) {
responseTabs.value = responseTabs.value.filter((tab) => tab.id !== id);
if (id === activeResponse.value.id) {
[activeResponse.value] = responseTabs.value;
}
}
const responseCompositionTabList = [
{
label: t('apiTestDebug.responseBody'),
value: ResponseComposition.BODY,
},
{
label: t('apiTestDebug.responseHeader'),
value: ResponseComposition.HEADER,
},
{
label: t('apiTestManagement.responseCode'),
value: ResponseComposition.CODE,
},
];
function changeBodyFormat(val: ResponseBodyFormat) {
innerResponse.value.body.bodyType = val;
emit('change');
}
function changeJsonBodyFormat(val: string) {
innerResponse.value.body.jsonBody.enableJsonSchema = val === 'JsonSchema';
emit('change');
}
//
const currentBodyCode = computed({
get() {
if (innerResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
return innerResponse.value.body.jsonBody.jsonValue;
}
if (innerResponse.value.body.bodyType === ResponseBodyFormat.XML) {
return innerResponse.value.body.xmlBody.value;
}
return innerResponse.value.body.rawBody.value;
},
set(val) {
if (innerResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
innerResponse.value.body.jsonBody.jsonValue = val;
} else if (innerResponse.value.body.bodyType === ResponseBodyFormat.XML) {
innerResponse.value.body.xmlBody.value = val;
} else {
innerResponse.value.body.rawBody.value = val;
}
},
});
//
const currentCodeLanguage = computed(() => {
if (innerResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (innerResponse.value.body.bodyType === ResponseBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
const { copy, isSupported } = useClipboard();
function copyScript() {
if (isSupported) {
copy(currentBodyCode.value);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
const fileList = ref<any[]>(
innerResponse.value.body.binaryBody && innerResponse.value.body.binaryBody.file
? [innerResponse.value.body.binaryBody.file]
: []
);
async function handleFileChange(files: MsFileItem[]) {
if (files.length === 0) {
innerResponse.value.body.binaryBody.file = undefined;
return;
}
if (!props.uploadTempFileApi) return;
try {
if (fileList.value[0]?.local) {
appStore.showLoading();
const res = await props.uploadTempFileApi(fileList.value[0].file);
innerResponse.value.body.binaryBody.file = {
...fileList.value[0],
fileId: res.data,
fileName: fileList.value[0]?.name || '',
fileAlias: fileList.value[0]?.name || '',
local: true,
};
appStore.hideLoading();
} else {
innerResponse.value.body.binaryBody.file = {
...fileList.value[0],
fileId: fileList.value[0].uid,
fileName: fileList.value[0]?.originalName || '',
fileAlias: fileList.value[0]?.name || '',
local: false,
};
}
emit('change');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const columns: MsTableColumn = [
{
title: 'apiTestDebug.content',
dataIndex: 'content',
showTooltip: true,
},
{
title: 'apiTestDebug.status',
dataIndex: 'status',
slotName: 'status',
width: 80,
},
{
title: '',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: { x: '100%' },
columns,
});
propsRes.value.data = [
{
id: new Date().getTime(),
content: 'Response Code equals: 200',
status: 1,
desc: '',
},
{
id: new Date().getTime(),
content: '$.users[1].age REGEX: 31',
status: 0,
desc: `Value expected to match regexp '31', but it did not match: '30' match: '30'`,
},
] as any;
</script>
<style lang="less" scoped>
.response-container {
margin-top: 8px;
height: calc(100% - 88px);
.response-header-pre {
@apply h-full overflow-auto bg-white;
.ms-scroll-bar();
padding: 8px 12px;
border-radius: var(--border-radius-small);
}
}
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-tabs-tab) {
@apply leading-none;
}
.response-tab {
@apply flex items-center;
.response-tab-default-icon {
@apply rounded-full;
margin-right: 4px;
width: 16px;
height: 16px;
background: url('@/assets/svg/icons/default.svg') no-repeat;
background-size: contain;
box-shadow: 0 0 7px 0 rgb(15 0 78 / 9%);
}
:deep(.response-more-action) {
margin-left: 4px;
.more-icon-btn {
@apply invisible;
}
}
&:hover {
:deep(.response-more-action) {
.more-icon-btn {
@apply visible;
}
}
}
}
</style>

View File

@ -24,7 +24,7 @@
</MsButton>
</template>
<div
v-if="props.isEdit && props.response.requestResults[0]?.responseResult?.responseCode"
v-if="props.isEdit && props.request.response.requestResults[0]?.responseResult?.responseCode"
class="ml-[4px] flex items-center"
>
<MsButton
@ -56,31 +56,31 @@
</a-radio-group>
</div>
<div
v-if="props.response.requestResults[0]?.responseResult?.responseCode"
v-if="props.request.response.requestResults[0]?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px]"
>
<a-popover position="left" content-class="response-popover-content">
<div :style="{ color: statusCodeColor }">
{{ props.response.requestResults[0].responseResult.responseCode }}
{{ props.request.response.requestResults[0].responseResult.responseCode }}
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
<div :style="{ color: statusCodeColor }">
{{ props.response.requestResults[0].responseResult.responseCode }}
{{ props.request.response.requestResults[0].responseResult.responseCode }}
</div>
</div>
</template>
</a-popover>
<a-popover position="left" content-class="w-[400px]">
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseTime }} ms
{{ props.request.response.requestResults[0].responseResult.responseTime }} ms
</div>
<template #content>
<div class="mb-[8px] flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div>
<div class="text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseTime }} ms
{{ props.request.response.requestResults[0].responseResult.responseTime }} ms
</div>
</div>
<responseTimeLine :response-timing="timingInfo" />
@ -88,94 +88,48 @@
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
{{ props.request.response.requestResults[0].responseResult.responseSize }} bytes
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
{{ props.request.response.requestResults[0].responseResult.responseSize }} bytes
</div>
</div>
</template>
</a-popover>
<!-- <a-popover position="left" content-class="response-popover-content">
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
<div class="text-[var(--color-text-1)]">{{ props.request.response.env }}</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.runningEnv') }}</div>
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
<div class="text-[var(--color-text-1)]">{{ props.request.response.env }}</div>
</div>
</template>
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
<div class="text-[var(--color-text-1)]">{{ props.request.response.resource }}</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.resourcePool') }}</div>
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
<div class="text-[var(--color-text-1)]">{{ props.request.response.resource }}</div>
</div>
</template>
</a-popover> -->
</div>
</div>
<a-spin :loading="props.loading" class="h-[calc(100%-42px)] w-full px-[18px] pb-[18px]">
<div v-if="props.isEdit" class="my-[8px] w-full">
<MsEditableTab
v-model:active-tab="activeResponse"
v-model:tabs="responseTabs"
at-least-one
hide-more-action
@add="addResponseTab"
>
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.isDefault" class="response-tab-default-icon"></div>
{{ tab.label }}({{ tab.code }})
<MsMoreAction
:list="
tab.isDefault
? tabMoreActionList.filter((e) => e.eventTag !== 'setDefault' && e.eventTag !== 'delete')
: tabMoreActionList
"
class="response-more-action"
@select="(e) => handleMoreActionSelect(e, tab as ResponseItem)"
/>
<popConfirm
v-model:visible="tab.showRenamePopConfirm"
mode="tabRename"
:field-config="{ field: tab.label }"
:all-names="responseTabs.map((e) => e.label)"
@rename-finish="(val) => (tab.label = val)"
>
<span :id="`renameSpan${tab.id}`" class="relative"></span>
</popConfirm>
<a-popconfirm
v-model:popup-visible="tab.showPopConfirm"
position="bottom"
content-class="w-[300px]"
:ok-text="t('common.confirm')"
:popup-offset="20"
@ok="() => handleDeleteResponseTab(tab.id)"
>
<template #icon>
<icon-exclamation-circle-fill class="!text-[rgb(var(--danger-6))]" />
</template>
<template #content>
<div class="font-semibold text-[var(--color-text-1)]">
{{ t('apiTestManagement.confirmDelete', { name: tab.label }) }}
</div>
</template>
<div class="relative"></div>
</a-popconfirm>
</div>
</template>
</MsEditableTab>
</div>
<result
v-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
<a-spin :loading="props.loading" class="h-[calc(100%-35px)] w-full px-[18px] pb-[18px]">
<edit
v-if="props.isEdit && activeResponseType === 'content' && props.responseDefinition"
v-model:activeTab="activeTab"
:response="props.responseDefinition"
:upload-temp-file-api="props.uploadTempFileApi"
@change="handleResponseChange"
/>
<result
v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
v-model:activeTab="activeTab"
:response="props.response"
:request="props.request"
/>
</a-spin>
@ -186,32 +140,29 @@
import { useVModel } from '@vueuse/core';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import type { Direction } from '@/components/pure/ms-split-box/index.vue';
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import edit from './edit.vue';
import result from './result.vue';
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
import { useI18n } from '@/hooks/useI18n';
import { ResponseResult } from '@/models/apiTest/common';
import { ResponseDefinition } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum';
import type { RequestParam } from '../index.vue';
const props = withDefaults(
defineProps<{
activeTab: keyof typeof ResponseComposition;
activeTab: ResponseComposition;
activeLayout?: Direction;
isExpanded: boolean;
response: ResponseResult;
request?: RequestParam;
responseDefinition?: ResponseDefinition;
request: RequestParam;
hideLayoutSwitch?: boolean; //
loading?: boolean;
isEdit?: boolean; //
uploadTempFileApi?: (...args) => Promise<any>; //
}>(),
{
activeLayout: 'vertical',
@ -220,7 +171,7 @@
);
const emit = defineEmits<{
(e: 'update:activeLayout', value: Direction): void;
(e: 'update:activeTab', value: keyof typeof ResponseComposition): void;
(e: 'update:activeTab', value: ResponseComposition): void;
(e: 'changeExpand', value: boolean): void;
(e: 'changeLayout', value: Direction): void;
(e: 'change'): void;
@ -241,7 +192,7 @@
sslHandshakeTime,
tcpHandshakeTime,
transferStartTime,
} = props.response.requestResults[0].responseResult;
} = props.request.response.requestResults[0].responseResult;
return {
dnsLookupTime,
tcpHandshakeTime,
@ -255,7 +206,7 @@
});
//
const statusCodeColor = computed(() => {
const code = props.response.requestResults[0].responseResult.responseCode;
const code = props.request.response.requestResults[0].responseResult.responseCode;
if (code >= 200 && code < 300) {
return 'rgb(var(--success-7)';
}
@ -265,13 +216,8 @@
return 'rgb(var(--danger-7)';
});
/** 响应内容编辑状态逻辑 */
export interface ResponseItem extends TabItem {
isDefault?: boolean; // tab
code: number; //
showPopConfirm?: boolean; //
showRenamePopConfirm?: boolean; //
function handleResponseChange() {
emit('change');
}
const activeResponseType = ref<'content' | 'result'>('content');
@ -279,87 +225,6 @@
function setActiveResponse(val: 'content' | 'result') {
activeResponseType.value = val;
}
const responseTabs = ref<ResponseItem[]>([
{
id: new Date().getTime(),
label: t('apiTestManagement.response'),
closable: false,
code: 200,
isDefault: true,
showPopConfirm: false,
showRenamePopConfirm: false,
},
]);
const activeResponse = ref<ResponseItem>(responseTabs.value[0]);
function addResponseTab(defaultProps?: Partial<ResponseItem>) {
responseTabs.value.push({
label: t('apiTestManagement.response', { count: responseTabs.value.length + 1 }),
code: 200,
...defaultProps,
id: new Date().getTime(),
isDefault: false,
showPopConfirm: false,
showRenamePopConfirm: false,
});
activeResponse.value = responseTabs.value[responseTabs.value.length - 1];
emit('change');
}
const tabMoreActionList: ActionsItem[] = [
{
label: t('apiTestManagement.setDefault'),
eventTag: 'setDefault',
},
{
label: t('common.rename'),
eventTag: 'rename',
},
{
label: t('common.copy'),
eventTag: 'copy',
},
{
isDivider: true,
},
{
label: t('common.delete'),
eventTag: 'delete',
danger: true,
},
];
const renameValue = ref('');
function handleMoreActionSelect(e: ActionsItem, _tab: ResponseItem) {
switch (e.eventTag) {
case 'setDefault':
responseTabs.value = responseTabs.value.map((tab) => {
tab.isDefault = _tab.id === tab.id;
return tab;
});
break;
case 'rename':
renameValue.value = _tab.label || '';
document.querySelector(`#renameSpan${_tab.id}`)?.dispatchEvent(new Event('click'));
break;
case 'copy':
addResponseTab({ ..._tab, label: `${_tab.label}-Copy` });
break;
case 'delete':
_tab.showPopConfirm = true;
break;
default:
break;
}
}
function handleDeleteResponseTab(id: number | string) {
responseTabs.value = responseTabs.value.filter((tab) => tab.id !== id);
if (id === activeResponse.value.id) {
[activeResponse.value] = responseTabs.value;
}
}
</script>
<style lang="less">
@ -382,30 +247,4 @@
border-color: var(--color-text-n8);
gap: 8px;
}
.response-tab {
@apply flex items-center;
.response-tab-default-icon {
@apply rounded-full;
margin-right: 4px;
width: 16px;
height: 16px;
background: url('@/assets/svg/icons/default.svg') no-repeat;
background-size: contain;
box-shadow: 0 0 7px 0 rgb(15 0 78 / 9%);
}
:deep(.response-more-action) {
margin-left: 4px;
.more-icon-btn {
@apply invisible;
}
}
&:hover {
:deep(.response-more-action) {
.more-icon-btn {
@apply visible;
}
}
}
}
</style>

View File

@ -6,7 +6,7 @@
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
ref="responseEditorRef"
:model-value="props.response.requestResults[0].responseResult?.body"
:model-value="props.request.response.requestResults[0].responseResult?.body"
:language="responseLanguage"
theme="vs"
height="100%"
@ -27,7 +27,7 @@
</MsCodeEditor>
<MsCodeEditor
v-else-if="activeTab === ResponseComposition.CONSOLE"
:model-value="props.response.console.trim()"
:model-value="props.request.response.console.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
@ -72,14 +72,12 @@
import { useI18n } from '@/hooks/useI18n';
import { ResponseResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const props = defineProps<{
response: ResponseResult;
request?: RequestParam;
request: RequestParam;
}>();
const { t } = useI18n();
@ -109,13 +107,14 @@
// value: ResponseComposition.ASSERTION,
// }, // TODO:
];
const activeTab = defineModel<keyof typeof ResponseComposition>('activeTab', {
const activeTab = defineModel<ResponseComposition>('activeTab', {
required: true,
default: ResponseComposition.BODY,
});
//
const responseLanguage = computed(() => {
const { contentType } = props.response.requestResults[0].responseResult;
const { contentType } = props.request.response.requestResults[0].responseResult;
if (contentType.includes('json')) {
return LanguageEnum.JSON;
}
@ -132,7 +131,7 @@
function copyScript() {
if (isSupported) {
copy(props.response.requestResults[0].responseResult.body);
copy(props.request.response.requestResults[0].responseResult.body);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
@ -142,16 +141,16 @@
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.response.requestResults[0].responseResult?.headers.trim();
return props.request.response.requestResults[0].responseResult?.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.response.requestResults[0].body
return props.request.response.requestResults[0].body
? `${t('apiTestDebug.requestUrl')}:\n${props.request?.url}\n${t('apiTestDebug.header')}:\n${
props.response.requestResults[0].headers
}\nBody:\n${props.response.requestResults[0].body.trim()}`
props.request.response.requestResults[0].headers
}\nBody:\n${props.request.response.requestResults[0].body.trim()}`
: '';
// case ResponseComposition.EXTRACT:
// return Object.keys(props.response.extract)
// .map((e) => `${e}: ${props.response.extract[e]}`)
// return Object.keys(props.request.response.extract)
// .map((e) => `${e}: ${props.request.response.extract[e]}`)
// .join('\n'); // TODO:
default:
return '';

View File

@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash-es';
import { cloneDeep, isEqual } from 'lodash-es';
import { ExecuteBody } from '@/models/apiTest/debug';
import { RequestParamsType } from '@/enums/apiEnum';
@ -122,7 +122,7 @@ export function filterKeyValParams(params: Record<string, any>[], defaultParamIt
delete lastData.enable;
delete defaultParam.id;
delete defaultParam.enable;
const lastDataIsDefault = JSON.stringify(lastData) === JSON.stringify(defaultParam);
const lastDataIsDefault = isEqual(lastData, defaultParam);
let validParams: Record<string, any>[] = [];
if (lastDataIsDefault) {
// 如果最后一条数据是默认数据,非用户添加更改的,说明是无效参数,删除最后一个

View File

@ -28,7 +28,13 @@
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<popConfirm mode="add" :all-names="rootModulesName" parent-id="NONE" @add-finish="initModules">
<popConfirm
v-if="hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD'])"
mode="add"
:all-names="rootModulesName"
parent-id="NONE"
@add-finish="initModules"
>
<MsButton v-permission="['PROJECT_API_DEBUG:READ+ADD']" type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
@ -87,8 +93,7 @@
</template>
<template #extra="nodeData">
<popConfirm
v-if="nodeData.id !== 'root'"
v-permission="['PROJECT_API_DEBUG:READ+UPDATE']"
v-if="nodeData.id !== 'root' && hasAnyPermission(['PROJECT_API_DEBUG:READ+UPDATE'])"
mode="rename"
:parent-id="nodeData.id"
:node-id="nodeData.id"
@ -102,8 +107,7 @@
</popConfirm>
<!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块API不可添加子模块 -->
<popConfirm
v-if="nodeData.id !== 'root' && nodeData.type !== 'API'"
v-permission="['PROJECT_API_DEBUG:READ+ADD']"
v-if="nodeData.id !== 'root' && nodeData.type !== 'API' && hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD'])"
mode="add"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:parent-id="nodeData.id"

View File

@ -41,6 +41,9 @@
:execute-api="executeDebug"
:local-execute-api="localExecuteApiDebug"
:upload-temp-file-api="uploadTempFile"
:file-save-as-source-id="activeDebug.id"
:file-save-as-api="transferFile"
:file-module-options-api="getTransferOptions"
:permission-map="{
execute: 'PROJECT_API_DEBUG:READ+EXECUTE',
update: 'PROJECT_API_DEBUG:READ+UPDATE',
@ -104,7 +107,9 @@
addDebug,
executeDebug,
getDebugDetail,
getTransferOptions,
localExecuteApiDebug,
transferFile,
updateDebug,
uploadTempFile,
} from '@/api/modules/api-test/debug';

View File

@ -107,6 +107,7 @@
RequestBodyFormat,
RequestComposition,
RequestMethods,
ResponseBodyFormat,
ResponseComposition,
} from '@/enums/apiEnum';
@ -250,6 +251,25 @@
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: {
id: 'default',
defaultFlag: true,
name: t('common.success'),
headers: [],
body: {
bodyType: ResponseBodyFormat.JSON,
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
},
statusCode: 200,
},
isNew: true,
};

View File

@ -86,4 +86,6 @@ export default {
'apiTestManagement.setDefault': '设为默认',
'apiTestManagement.confirmDelete': '确认删除 {name} 吗?',
'apiTestManagement.response': '响应{count}',
'apiTestManagement.responseCode': '响应码',
'apiTestManagement.dynamicConversion': '动态转换',
};

View File

@ -1,6 +1,11 @@
<template>
<div class="invite-page">
<div class="form-box w-1/3 rounded-[12px] bg-white">
<a-empty v-if="isInviteOverTime" :description="t('invite.overTime')" class="h-[400px] w-full">
<template #image>
<icon-close-circle-fill :size="68" />
</template>
</a-empty>
<div v-else class="form-box w-1/3 rounded-[12px] bg-white">
<div class="form-box-title">{{ t('invite.title') }}</div>
<a-form
ref="registerFormRef"
@ -38,13 +43,15 @@
import MsPasswordInput from '@/components/pure/ms-password-input/index.vue';
import { registerByInvite } from '@/api/modules/setting/user';
import { registerByInvite, validInvite } from '@/api/modules/setting/user';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { encrypted, sleep } from '@/utils';
import { validatePasswordLength, validateWordPassword } from '@/utils/validate';
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const form = ref({
@ -86,10 +93,19 @@
],
};
function validatePsw(value: string) {
pswValidateRes.value = validateWordPassword(value);
pswLengthValidateRes.value = validatePasswordLength(value);
}
const isInviteOverTime = ref(false); //
onBeforeMount(async () => {
try {
appStore.showLoading();
await validInvite(route.query.inviteId as string);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
isInviteOverTime.value = true;
} finally {
appStore.hideLoading();
}
});
function confirmInvite() {
registerFormRef.value?.validate(async (errors) => {
@ -160,4 +176,7 @@
color: rgb(var(--danger-6));
}
}
:deep(.arco-empty-description) {
font-size: 18px;
}
</style>

View File

@ -15,6 +15,7 @@ export default {
'invite.passwordLengthRule': 'The length is 8-32 digits',
'invite.passwordWordRule': 'Must contain numbers and letters, Chinese or spaces are not allowed',
'invite.success': 'Registered successfully',
'invite.overTime': 'The invitation link has expired',
'personal.info': 'My Info',
'personal.switchOrg': 'Switch Org',
'personal.searchOrgPlaceholder': 'Please enter organization name',

View File

@ -15,6 +15,7 @@ export default {
'invite.passwordLengthRule': '长度为8-32位',
'invite.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
'invite.success': '注册成功',
'invite.overTime': '邀请链接已过期',
'personal.info': '个人信息',
'personal.switchOrg': '切换组织',
'personal.searchOrgPlaceholder': '请输入组织名称',

View File

@ -76,7 +76,7 @@
:upload-image="handleUploadImage"
/>
</a-form-item>
<AddAttachment v-model:file-list="fileList" @change="handleChange" @link-file="associatedFile" />
<AddAttachment v-model:file-list="fileList" multiple @change="handleChange" @link-file="associatedFile" />
</a-form>
<!-- 文件列表开始 -->
<div class="w-[90%]">

View File

@ -105,6 +105,7 @@
</a-tooltip>
<template v-if="item.key === 'name'">
<popConfirm
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
mode="fileRename"
:field-config="{
field: detail.name,
@ -114,9 +115,9 @@
:all-names="[]"
@rename-finish="detailDrawerRef?.initDetail"
>
<MsButton v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']" class="!mr-0 ml-[8px]">{{
t('common.rename')
}}</MsButton>
<MsButton class="!mr-0 ml-[8px]">
{{ t('common.rename') }}
</MsButton>
</popConfirm>
<template v-if="UploadAcceptEnum.image.includes(fileType)">
<a-divider
@ -130,6 +131,7 @@
</template>
<template v-if="item.key === 'desc'">
<popConfirm
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
mode="fileUpdateDesc"
:title="t('project.fileManagement.desc')"
:field-config="{
@ -142,9 +144,7 @@
:all-names="[]"
@update-desc-finish="detailDrawerRef?.initDetail"
>
<MsButton v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']" class="ml-[8px]"
><MsIcon type="icon-icon_edit_outlined"></MsIcon
></MsButton>
<MsButton class="ml-[8px]"> <MsIcon type="icon-icon_edit_outlined" /></MsButton>
</popConfirm>
</template>
</div>

View File

@ -39,8 +39,7 @@
<template v-if="!props.isModal" #extra="nodeData">
<!-- 默认模块的 id 是root默认模块不可编辑不可添加子模块 -->
<popConfirm
v-if="nodeData.id !== 'root'"
v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']"
v-if="nodeData.id !== 'root' && hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"
mode="add"
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
:parent-id="nodeData.id"
@ -52,8 +51,7 @@
</MsButton>
</popConfirm>
<popConfirm
v-if="nodeData.id !== 'root'"
v-permission="['PROJECT_FILE_MANAGEMENT:READ+UPDATE']"
v-if="nodeData.id !== 'root' && hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
mode="rename"
:parent-id="nodeData.id"
:node-id="nodeData.id"

View File

@ -35,6 +35,7 @@
</template>
<template #itemAction="{ item }">
<popConfirm
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+UPDATE'])"
mode="repositoryRename"
:node-id="item.id"
:field-config="{ field: renameStorageTitle }"
@ -161,6 +162,7 @@
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import { validateGitUrl } from '@/utils/validate';
import { Repository, RepositoryInfo } from '@/models/projectManagement/file';

View File

@ -24,8 +24,12 @@
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<a-dropdown trigger="click" @select="handleAddSelect">
<MsButton v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']" type="icon" class="!mr-0 p-[2px]">
<a-dropdown
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"
trigger="click"
@select="handleAddSelect"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
@ -38,7 +42,7 @@
</template>
</a-dropdown>
<popConfirm
v-permission="['PROJECT_FILE_MANAGEMENT:READ+ADD']"
v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"
mode="add"
:all-names="rootModulesName"
parent-id="none"
@ -104,6 +108,7 @@
import { getModulesCount } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
import { FileListQueryParams } from '@/models/projectManagement/file';
@ -246,7 +251,7 @@
@apply bg-white;
min-width: 1000px;
height: calc(100vh - 88px);
height: calc(100vh - 76px);
border-radius: var(--border-radius-large);
.folder {
@apply flex cursor-pointer items-center justify-between;

View File

@ -1,7 +1,7 @@
<template>
<MsCard
ref="fullRef"
:special-height="132"
:special-height="127"
show-full-screen
hide-back
hide-footer
@ -300,9 +300,10 @@
);
function handleToggleFullScreen(val: boolean) {
propsRes.value.heightUsed = val ? 224 : 428;
propsRes.value.heightUsed = val ? 214 : 428;
}
// TODO: arco bug issue
function spanMethod(data: {
record: TableData;
column: TableColumnData | TableOperationColumn;

View File

@ -1,6 +1,6 @@
<template>
<div>
<MsCard :min-width="1060" :special-height="132" simple>
<MsCard :min-width="1060" :special-height="127" simple>
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" closable @close="addVisited">
{{ t('project.messageManagement.botListTips') }}
<template #close-element>
@ -414,7 +414,9 @@
innerHTML: `<div>${t(
robot.enable ? 'project.messageManagement.disableContent' : 'project.messageManagement.enableContent',
{ robot: robot.name }
)}</div><div>${robot.platform === 'MAIL' && !robot.enable ? t('project.messageManagement.enableEmailContentTip') : ''}</div>`,
)}</div><div>${
robot.platform === 'MAIL' && !robot.enable ? t('project.messageManagement.enableEmailContentTip') : ''
}</div>`,
}),
okText: t(robot.enable ? 'project.messageManagement.disableConfirm' : 'project.messageManagement.enableConfirm', {
robot: robot.name,

View File

@ -16,11 +16,12 @@
label: 'name',
email: 'email',
}"
:search-keys="['label', 'email']"
:search-keys="['name', 'email']"
value-key="id"
label-key="name"
:option-tooltip-content="(item) => `${item.name}(${item.email})`"
:option-label-render="
(item) => `${item.label}<span class='text-[var(--color-text-2)]'>${item.email}</span>`
(item) => `${item.name}<span class='text-[var(--color-text-2)]'>${item.email}</span>`
"
allow-search
allow-clear

View File

@ -194,12 +194,18 @@
></MsBatchForm>
<!-- TODO:代码编辑器懒加载 -->
<div v-show="form.addType === 'multiple'">
<MsCodeEditor v-model:model-value="editorContent" width="100%" height="400px" theme="MS-text">
<template #rightTitle>
<MsCodeEditor
v-model:model-value="editorContent"
width="100%"
height="400px"
theme="MS-text"
:show-theme-change="false"
>
<template #leftTitle>
<a-form-item
:label="t('system.resourcePool.batchAddResource')"
asterisk-position="end"
class="hide-wrapper mb-0"
class="hide-wrapper mb-0 w-auto"
required
>
</a-form-item>
@ -345,6 +351,7 @@
import { computed, onBeforeMount, Ref, ref, watch, watchEffect } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { isEmpty } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
@ -586,8 +593,10 @@
for (let i = 0; i < nodesList?.length; i++) {
const node = nodesList[i];
// ipportmonitorconcurrentNumber
if (Object.values(node).every((e) => e !== '')) {
res += `${node.ip},${node.port},${node.monitor},${node.concurrentNumber}\r`;
if (!Object.values(node).every((e) => isEmpty(e))) {
res += `${node.ip},${node.port === undefined ? '' : node.port},${
node.monitor === undefined ? '' : node.monitor
},${node.concurrentNumber === undefined ? '' : node.concurrentNumber}\r`;
}
}
editorContent.value = res;
@ -792,7 +801,7 @@
* 校验批量添加的资源信息
* @param cb 校验通过后的回调函数
*/
function validateBtachNodes(cb: () => void) {
function validateBatchNodes(cb: () => void) {
if (
form.value.testResourceDTO.nodesList.some((e) => {
return Object.values(e).every((v) => v !== '') && e.concurrentNumber > 0;
@ -824,7 +833,7 @@
}
// node
analyzeCode();
validateBtachNodes(save);
validateBatchNodes(save);
return false;
}
return save();

View File

@ -86,7 +86,7 @@
v-permission="['SYSTEM_USER:READ+UPDATE', 'SYSTEM_USER:READ+DELETE']"
:list="tableActions"
@select="handleSelect($event, record)"
></MsTableMoreAction>
/>
</template>
</template>
</ms-base-table>
@ -145,7 +145,7 @@
</a-form-item>
</a-form>
<template #footer>
<a-button type="secondary" :disabled="loading" @click="handleBeforeClose">
<a-button type="secondary" :disabled="loading" @click="cancelCreate">
{{ t('system.user.editUserModalCancelCreate') }}
</a-button>
<a-button v-if="userFormMode === 'create'" type="secondary" :loading="loading" @click="saveAndContinue">