diff --git a/frontend/src/components/business/ms-add-attachment/index.vue b/frontend/src/components/business/ms-add-attachment/index.vue index 1e2dab425a..5448738f09 100644 --- a/frontend/src/components/business/ms-add-attachment/index.vue +++ b/frontend/src/components/business/ms-add-attachment/index.vue @@ -92,7 +92,7 @@ :size="props.tagSize" class="m-0 border-none p-0" :self-style="{ backgroundColor: 'transparent !important' }" - :closable="data.value !== '__arco__more' || props.disabled" + :closable="data.value !== '__arco__more' && !props.disabled" @close="handleClose(data)" > {{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }} @@ -106,7 +106,9 @@
{{ t('ms.add.attachment.alreadyDelete') }}
- {{ t('ms.add.attachment.quickClear') }} + + {{ t('ms.add.attachment.quickClear') }} +
@@ -116,8 +118,12 @@ - - + +
@@ -137,25 +143,39 @@
- +
- - + +
diff --git a/frontend/src/components/business/ms-assertion/comp/ResponseTimeTab.vue b/frontend/src/components/business/ms-assertion/comp/ResponseTimeTab.vue index 5ea7b384cd..e6cbcde23c 100644 --- a/frontend/src/components/business/ms-assertion/comp/ResponseTimeTab.vue +++ b/frontend/src/components/business/ms-assertion/comp/ResponseTimeTab.vue @@ -4,21 +4,25 @@ {{ t('ms.assertion.responseTime') }} (ms) - +
+
{{ t('advanceFilter.operator.le') }}
+ +
diff --git a/frontend/src/components/business/ms-assertion/index.vue b/frontend/src/components/business/ms-assertion/index.vue index dcdbb6f6dd..b5be272006 100644 --- a/frontend/src/components/business/ms-assertion/index.vue +++ b/frontend/src/components/business/ms-assertion/index.vue @@ -84,13 +84,7 @@ getCurrentItemState.assertionType !== ResponseAssertionType.SCRIPT, }" > - +
- +
h(t(element?.meta?.locale || '')), + title: () => h('div', t(element?.meta?.locale || '')), }} class={BOTTOM_MENU_LIST.includes(element?.name as string) ? 'arco-menu-inline--bottom' : ''} > diff --git a/frontend/src/components/pure/ms-code-editor/index.vue b/frontend/src/components/pure/ms-code-editor/index.vue index 23424f335e..3cf53ebaf3 100644 --- a/frontend/src/components/pure/ms-code-editor/index.vue +++ b/frontend/src/components/pure/ms-code-editor/index.vue @@ -282,7 +282,6 @@ if (height > 1000) { codeheight.value = `1000px`; } - editor.layout(); } const init = () => { diff --git a/frontend/src/models/apiTest/mock.ts b/frontend/src/models/apiTest/mock.ts index 7c41cbc1a0..dd2535701a 100644 --- a/frontend/src/models/apiTest/mock.ts +++ b/frontend/src/models/apiTest/mock.ts @@ -3,16 +3,22 @@ import type { MsFileItem } from '@/components/pure/ms-upload/types'; import type { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum'; import type { BatchApiParams } from '../common'; -import type { ExecuteBinaryBody, KeyValueParam, ResponseDefinitionBody } from './common'; +import type { + ExecuteBinaryBody, + ExecuteJsonBody, + ExecuteValueBody, + KeyValueParam, + ResponseDefinitionBody, +} from './common'; // mock 信息-匹配项 export interface MatchRuleItem { id?: string; // 用于前端标识 + paramType: RequestParamsType; // 用于前端标识 key: string; value: string; condition: string; description: string; - paramType: RequestParamsType; files: ({ fileId: string; fileName: string; @@ -37,10 +43,13 @@ export interface MockMatchRuleCommon { } // mock 信息-请求体匹配规则 export interface MockBody { - paramType: RequestBodyFormat; - formDataMatch: MockMatchRuleCommon; + bodyType: RequestBodyFormat; + formDataBody: MockMatchRuleCommon; + wwwFormBody: MockMatchRuleCommon; + jsonBody: ExecuteJsonBody; + xmlBody: ExecuteValueBody; + rawBody: ExecuteValueBody; binaryBody: ExecuteBinaryBody; - raw: string; } // mock 信息-匹配规则集合 export interface MockMatchRule { @@ -74,7 +83,6 @@ export interface UpdateMockParams extends MockParams { // mock 信息-详情 export interface MockDetail extends MockParams { id: string; - matching: MockMatchRule; } // 批量编辑 mock export interface BatchEditMockParams extends BatchApiParams { diff --git a/frontend/src/views/api-test/components/batchAddKeyVal.vue b/frontend/src/views/api-test/components/batchAddKeyVal.vue index 210099b0e6..7012f589c9 100644 --- a/frontend/src/views/api-test/components/batchAddKeyVal.vue +++ b/frontend/src/views/api-test/components/batchAddKeyVal.vue @@ -22,11 +22,10 @@ -
+
-
+
{ const defaultParams = cloneDeep(mockDefaultParams); defaultParams.id = Date.now().toString(); - defaultParams.mockMatchRule.body.formDataMatch.matchRules.push({ - ...cloneDeep(defaultMatchRuleItem), + defaultParams.mockMatchRule.body.formDataBody.matchRules.push({ + ...defaultMatchRuleItem, id: Date.now().toString(), }); - defaultParams.mockMatchRule.header.matchRules.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); - defaultParams.mockMatchRule.query.matchRules.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); - defaultParams.mockMatchRule.rest.matchRules.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); - defaultParams.response.headers.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); - return defaultParams; + defaultParams.mockMatchRule.body.wwwFormBody.matchRules.push({ + ...defaultMatchRuleItem, + id: Date.now().toString(), + }); + defaultParams.mockMatchRule.header.matchRules.push({ ...defaultMatchRuleItem, id: Date.now().toString() }); + defaultParams.mockMatchRule.query.matchRules.push({ ...defaultMatchRuleItem, id: Date.now().toString() }); + defaultParams.mockMatchRule.rest.matchRules.push({ ...defaultMatchRuleItem, id: Date.now().toString() }); + defaultParams.response.headers.push({ ...defaultMatchRuleItem, id: Date.now().toString() }); + return cloneDeep(defaultParams); }; // mock 匹配规则选项 export const matchRuleOptions = [ diff --git a/frontend/src/views/api-test/components/requestComposition/body.vue b/frontend/src/views/api-test/components/requestComposition/body.vue index 4c7d5e3d74..0cb7712822 100644 --- a/frontend/src/views/api-test/components/requestComposition/body.vue +++ b/frontend/src/views/api-test/components/requestComposition/body.vue @@ -84,11 +84,10 @@
-->
-
+
@@ -88,6 +89,7 @@ import { hasAnyPermission } from '@/utils/permission'; import { ProtocolItem } from '@/models/apiTest/common'; + import { MockDetail } from '@/models/apiTest/mock'; import { ModuleTreeNode } from '@/models/common'; import { RequestAuthType, @@ -323,6 +325,10 @@ } } + function handleMockDebug(mock: MockDetail) { + apiRef.value?.openApiTabAndDebugMock(mock); + } + onBeforeMount(() => { initMemberOptions(); initProtocolList(); diff --git a/frontend/src/views/api-test/management/components/management/mock/mockDetailDrawer.vue b/frontend/src/views/api-test/management/components/management/mock/mockDetailDrawer.vue index 7cd9924884..d7987a53f4 100644 --- a/frontend/src/views/api-test/management/components/management/mock/mockDetailDrawer.vue +++ b/frontend/src/views/api-test/management/components/management/mock/mockDetailDrawer.vue @@ -20,7 +20,7 @@ v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']" type="icon" status="secondary" - @click="isEdit = true" + @click="handleChangeEdit" > {{ t('common.edit') }} @@ -37,7 +37,7 @@
- +
{{ t('apiTestDebug.noneBody') }}
-
+ +
-->
-
+
import { Message } from '@arco-design/web-vue'; + import { cloneDeep } from 'lodash-es'; import MsButton from '@/components/pure/ms-button/index.vue'; import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; @@ -216,6 +222,7 @@ import { useI18n } from '@/hooks/useI18n'; import useAppStore from '@/store/modules/app'; + import { ResponseDefinition } from '@/models/apiTest/common'; import { MockParams } from '@/models/apiTest/mock'; import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum'; @@ -231,6 +238,7 @@ definitionDetail: RequestParam; detailId?: string; isCopy?: boolean; + isEditMode?: boolean; }>(); const emit = defineEmits<{ (e: 'delete'): void; @@ -245,7 +253,7 @@ }); const loading = ref(false); - const isEdit = ref(false); + const isEdit = ref(props.isEditMode); const mockDetail = ref(makeDefaultParams()); const isReadOnly = computed(() => !mockDetail.value.isNew && !isEdit.value); const title = computed(() => { @@ -287,7 +295,7 @@ .validParams.length; return `${headerNum > 0 ? headerNum : ''}`; case RequestComposition.BODY: - return mockDetail.value.mockMatchRule.body.paramType !== RequestBodyFormat.NONE ? '1' : ''; + return mockDetail.value.mockMatchRule.body.bodyType !== RequestBodyFormat.NONE ? '1' : ''; case RequestComposition.QUERY: const queryNum = filterKeyValParams(mockDetail.value.mockMatchRule.query.matchRules, defaultRequestParamsItem) .validParams.length; @@ -384,15 +392,16 @@ return []; } }); + // 当前请求 body 的参数名选项集合 const currentBodyKeyOptions = computed(() => { - switch (mockDetail.value.mockMatchRule.body.paramType) { + switch (mockDetail.value.mockMatchRule.body.bodyType) { case RequestBodyFormat.FORM_DATA: return filterKeyValParams( props.definitionDetail.body.formDataBody.formValues, defaultMatchRuleItem ).validParams.map((e) => ({ label: e.key, - value: e.value, + value: e.key, paramType: e.paramType, })); case RequestBodyFormat.WWW_FORM: @@ -407,17 +416,71 @@ return []; } }); - // 当前代码编辑器的语言 + + // 当前请求 body 显示的代码 + const currentBodyCode = computed({ + get() { + if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.JSON) { + return mockDetail.value.mockMatchRule.body.jsonBody.jsonValue; + } + if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.XML) { + return mockDetail.value.mockMatchRule.body.xmlBody.value; + } + return mockDetail.value.mockMatchRule.body.rawBody.value; + }, + set(val) { + if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.JSON) { + mockDetail.value.mockMatchRule.body.jsonBody.jsonValue = val; + } else if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.XML) { + mockDetail.value.mockMatchRule.body.xmlBody.value = val; + } else { + mockDetail.value.mockMatchRule.body.rawBody.value = val; + } + }, + }); + // 当前请求 body 代码编辑器的语言 const currentCodeLanguage = computed(() => { - if (mockDetail.value.mockMatchRule.body.paramType === RequestBodyFormat.JSON) { + if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.JSON) { return LanguageEnum.JSON; } - if (mockDetail.value.mockMatchRule.body.paramType === RequestBodyFormat.XML) { + if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.XML) { return LanguageEnum.XML; } return LanguageEnum.PLAINTEXT; }); + /** + * 添加默认的匹配规则项 + */ + function appendDefaultMatchRuleItem() { + const { body } = mockDetail.value.mockMatchRule; + mockDetail.value.mockMatchRule.body = { + ...body, + formDataBody: { + matchAll: body.formDataBody.matchAll, + matchRules: [...body.formDataBody.matchRules, cloneDeep(defaultMatchRuleItem)], + }, + wwwFormBody: { + matchAll: body.wwwFormBody.matchAll, + matchRules: [...body.wwwFormBody.matchRules, cloneDeep(defaultMatchRuleItem)], + }, + }; + mockDetail.value.mockMatchRule.header.matchRules = [ + ...mockDetail.value.mockMatchRule.header.matchRules, + cloneDeep(defaultMatchRuleItem), + ]; + mockDetail.value.mockMatchRule.query.matchRules = [ + ...mockDetail.value.mockMatchRule.query.matchRules, + cloneDeep(defaultMatchRuleItem), + ]; + mockDetail.value.mockMatchRule.rest.matchRules = [ + ...mockDetail.value.mockMatchRule.rest.matchRules, + cloneDeep(defaultMatchRuleItem), + ]; + } + + const fileList = ref([]); + async function initMockDetail() { try { loading.value = true; @@ -425,34 +488,45 @@ id: props.detailId || '', projectId: appStore.currentProjectId, }); - const parseFileResult = parseRequestBodyFiles(res.matching.body); - const formDataMatch = - res.matching.body.paramType === RequestBodyFormat.FORM_DATA - ? res.matching.body.formDataMatch.matchRules.map((item) => { - const newParamType = - currentBodyKeyOptions.value.find((e) => e.value === item.key)?.paramType || - defaultMatchRuleItem.paramType; - item.paramType = newParamType; - item.files = item.files || []; - return item; - }) - : res.matching.body.formDataMatch.matchRules; + // form-data 的匹配规则含有文件类型,特殊处理 + const formDataMatch = res.mockMatchRule.body.formDataBody.matchRules.map((item) => { + const newParamType = + currentBodyKeyOptions.value.find((e) => e.value === item.key)?.paramType || defaultMatchRuleItem.paramType; + item.paramType = newParamType; + item.files = item.files || []; + return item; + }); mockDetail.value = { ...res, id: props.isCopy ? '' : res.id, isNew: props.isCopy, + name: props.isCopy ? `${res.name}_copy` : res.name, mockMatchRule: { - ...res.matching, + ...res.mockMatchRule, body: { - ...res.matching.body, - formDataMatch: { - ...res.matching.body.formDataMatch, + ...res.mockMatchRule.body, + formDataBody: { + ...res.mockMatchRule.body.formDataBody, matchRules: formDataMatch, }, }, }, + }; + // 等formDataMatch解析完毕赋值后才能正确解析 body 内的文件类型值 + const parseFileResult = parseRequestBodyFiles(mockDetail.value.mockMatchRule.body, [ + res.response as unknown as ResponseDefinition, + ]); + mockDetail.value = { + ...mockDetail.value, ...parseFileResult, }; + fileList.value = mockDetail.value.mockMatchRule.body.binaryBody.file + ? [mockDetail.value.mockMatchRule.body.binaryBody.file as unknown as MsFileItem] + : []; + if (props.isCopy) { + appendDefaultMatchRuleItem(); + } + isEdit.value = !!props.isEditMode; } catch (error) { // eslint-disable-next-line no-console console.log(error); @@ -464,8 +538,12 @@ watch( () => visible.value, (val) => { - if (val && props.detailId) { - initMockDetail(); + if (val) { + if (props.detailId) { + initMockDetail(); + } else { + fileList.value = []; + } } }, { @@ -473,8 +551,6 @@ } ); - const fileList = ref([]); - async function handleFileChange(files: MsFileItem[], file?: MsFileItem) { try { if (file?.local && file.file) { @@ -512,25 +588,17 @@ } } - watch( - () => mockDetail.value.mockMatchRule.body.paramType, - (val) => { - if (val === RequestBodyFormat.JSON) { - mockDetail.value.mockMatchRule.body.raw = props.definitionDetail.body.jsonBody.jsonValue; - } else if (val === RequestBodyFormat.XML) { - mockDetail.value.mockMatchRule.body.raw = props.definitionDetail.body.xmlBody.value || ''; - } else if (val === RequestBodyFormat.RAW) { - mockDetail.value.mockMatchRule.body.raw = props.definitionDetail.body.rawBody.value || ''; - } - } - ); - function handleCancel() { mockDetail.value = makeDefaultParams(); isEdit.value = false; visible.value = false; } + function handleChangeEdit() { + appendDefaultMatchRuleItem(); + isEdit.value = true; + } + function handleDelete() { emit('delete'); handleCancel(); @@ -540,7 +608,14 @@ try { loading.value = true; const { body } = mockDetail.value.mockMatchRule; - const validBodyMatchRules = filterKeyValParams(body.formDataMatch.matchRules, defaultMatchRuleItem).validParams; + const validFormDataBodyMatchRules = filterKeyValParams( + body.formDataBody.matchRules, + defaultMatchRuleItem + ).validParams; + const validWwwFormBodyMatchRules = filterKeyValParams( + body.wwwFormBody.matchRules, + defaultMatchRuleItem + ).validParams; const validHeaderMatchRules = filterKeyValParams( mockDetail.value.mockMatchRule.header.matchRules, defaultMatchRuleItem @@ -557,17 +632,26 @@ mockDetail.value.response.headers, defaultHeaderParamsItem ).validParams; - const parseFileResult = parseRequestBodyFiles(mockDetail.value.mockMatchRule.body); - const params = { + const parseFileResult = parseRequestBodyFiles( + mockDetail.value.mockMatchRule.body, + [mockDetail.value.response as unknown as ResponseDefinition], + mockDetail.value.uploadFileIds, + mockDetail.value.linkFileIds + ); + const params: MockParams = { ...mockDetail.value, statusCode: mockDetail.value.response.statusCode, mockMatchRule: { ...mockDetail.value.mockMatchRule, body: { ...mockDetail.value.mockMatchRule.body, - formDataMatch: { - ...mockDetail.value.mockMatchRule.body.formDataMatch, - matchRules: validBodyMatchRules, + formDataBody: { + ...mockDetail.value.mockMatchRule.body.formDataBody, + matchRules: validFormDataBodyMatchRules, + }, + wwwFormBody: { + ...mockDetail.value.mockMatchRule.body.wwwFormBody, + matchRules: validWwwFormBodyMatchRules, }, }, header: { @@ -587,14 +671,16 @@ ...mockDetail.value.response, headers: validResponseHeaders, }, - ...parseFileResult, apiDefinitionId: props.definitionDetail.id, projectId: appStore.currentProjectId, + uploadFileIds: parseFileResult.uploadFileIds, + linkFileIds: parseFileResult.linkFileIds, }; if (isEdit.value) { await updateMock({ id: mockDetail.value.id || '', ...params, + ...parseFileResult, }); Message.success(t('common.updateSuccess')); } else { diff --git a/frontend/src/views/api-test/management/components/management/mock/mockResponse.vue b/frontend/src/views/api-test/management/components/management/mock/mockResponse.vue index 2f3a10275f..ab6bfb6450 100644 --- a/frontend/src/views/api-test/management/components/management/mock/mockResponse.vue +++ b/frontend/src/views/api-test/management/components/management/mock/mockResponse.vue @@ -154,6 +154,7 @@ import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue'; import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.vue'; + import { uploadMockTempFile } from '@/api/modules/api-test/management'; import { responseHeaderOption } from '@/config/apiTest'; import { useI18n } from '@/hooks/useI18n'; @@ -164,7 +165,6 @@ const props = defineProps<{ definitionResponses: ResponseItem[]; - uploadTempFileApi?: (...args: any) => Promise; // 上传临时文件接口 disabled: boolean; }>(); const emit = defineEmits<{ @@ -267,16 +267,17 @@ const fileList = ref([]); const loading = ref(false); - async function handleFileChange() { + + async function handleFileChange(files: MsFileItem[], file?: MsFileItem) { try { - if (fileList.value[0] && fileList.value[0].local && fileList.value[0].file && props.uploadTempFileApi) { + if (file?.local && file.file) { loading.value = true; - const res = await props.uploadTempFileApi(fileList.value[0].file); + const res = await uploadMockTempFile(file.file); mockResponse.value.body.binaryBody.file = { - ...fileList.value[0], + ...file, fileId: res.data, - fileName: fileList.value[0]?.name || '', - fileAlias: fileList.value[0]?.name || '', + fileName: file?.name || '', + fileAlias: file?.name || '', local: true, }; loading.value = false; @@ -298,6 +299,19 @@ loading.value = false; } } + + watch( + () => mockResponse.value.body.binaryBody.file?.fileId, + () => { + fileList.value = mockResponse.value.body.binaryBody.file + ? [mockResponse.value.body.binaryBody.file as unknown as MsFileItem] + : []; + }, + { + deep: true, + immediate: true, + } + );