feat(接口测试): mock接口联调

This commit is contained in:
baiqi 2024-05-10 18:28:54 +08:00 committed by 刘瑞斌
parent c245a17d72
commit d785728c2e
24 changed files with 515 additions and 181 deletions

View File

@ -92,7 +92,7 @@
:size="props.tagSize" :size="props.tagSize"
class="m-0 border-none p-0" class="m-0 border-none p-0"
:self-style="{ backgroundColor: 'transparent !important' }" :self-style="{ backgroundColor: 'transparent !important' }"
:closable="data.value !== '__arco__more' || props.disabled" :closable="data.value !== '__arco__more' && !props.disabled"
@close="handleClose(data)" @close="handleClose(data)"
> >
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }} {{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}
@ -106,7 +106,9 @@
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" /> <icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
<div class="text-[var(--color-text-4)]">{{ t('ms.add.attachment.alreadyDelete') }}</div> <div class="text-[var(--color-text-4)]">{{ t('ms.add.attachment.alreadyDelete') }}</div>
<MsButton type="text" @click="clearDeletedFiles">{{ t('ms.add.attachment.quickClear') }}</MsButton> <MsButton type="text" :disabled="props.disabled" @click="clearDeletedFiles">
{{ t('ms.add.attachment.quickClear') }}
</MsButton>
</div> </div>
<div class="file-list"> <div class="file-list">
<div v-for="file of alreadyDeleteFiles" :key="file.value" class="file-list-item"> <div v-for="file of alreadyDeleteFiles" :key="file.value" class="file-list-item">
@ -116,8 +118,12 @@
</MsTag> </MsTag>
</a-tooltip> </a-tooltip>
<a-tooltip :content="t('ms.add.attachment.remove')"> <a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" @click="handleClose(file)"> <MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon type="icon-icon_unlink" class="hover:text-[rgb(var(--primary-5))]" size="16" /> <MsIcon
type="icon-icon_unlink"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
</div> </div>
@ -137,25 +143,39 @@
<div v-if="file.local === true" class="flex items-center"> <div v-if="file.local === true" class="flex items-center">
<template v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])"> <template v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])">
<a-tooltip :content="t('ms.add.attachment.saveAs')"> <a-tooltip :content="t('ms.add.attachment.saveAs')">
<MsButton type="text" status="secondary" class="!mr-0" @click="handleOpenSaveAs(file)"> <MsButton
<MsIcon type="icon-icon_unloading" class="hover:text-[rgb(var(--primary-5))]" size="16" /> type="text"
status="secondary"
class="!mr-0"
:disabled="props.disabled"
@click="handleOpenSaveAs(file)"
>
<MsIcon
type="icon-icon_unloading"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
<a-divider direction="vertical" :margin="4"></a-divider> <a-divider direction="vertical" :margin="4"></a-divider>
</template> </template>
<a-tooltip :content="t('ms.add.attachment.remove')"> <a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" @click="handleClose(file)"> <MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon <MsIcon
type="icon-icon_delete-trash_outlined" type="icon-icon_delete-trash_outlined"
class="hover:text-[rgb(var(--primary-5))]" :class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16" size="16"
/> />
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
</div> </div>
<a-tooltip v-else :content="t('ms.add.attachment.cancelAssociate')"> <a-tooltip v-else :content="t('ms.add.attachment.cancelAssociate')">
<MsButton type="text" status="secondary" @click="handleClose(file)"> <MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon type="icon-icon_unlink" class="hover:text-[rgb(var(--primary-5))]" size="16" /> <MsIcon
type="icon-icon_unlink"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
</div> </div>

View File

@ -4,21 +4,25 @@
<span class="text-[var(--color-text-1)]">{{ t('ms.assertion.responseTime') }}</span> <span class="text-[var(--color-text-1)]">{{ t('ms.assertion.responseTime') }}</span>
<span class="text-[var(--color-text-4)]">(ms)</span> <span class="text-[var(--color-text-4)]">(ms)</span>
</div> </div>
<a-input-number <div class="flex items-center gap-[4px]">
v-model="condition.expectedValue" <div class="whitespace-nowrap">{{ t('advanceFilter.operator.le') }}</div>
:disabled="props.disabled" <a-input-number
:step="100" v-model="condition.expectedValue"
:min="0" :disabled="props.disabled"
:precision="0" :step="100"
mode="button" :min="0"
model-event="input" :precision="0"
@change=" mode="button"
() => model-event="input"
emit('change', { class="w-[250px]"
...condition, @change="
}) () =>
" emit('change', {
/> ...condition,
})
"
/>
</div>
</div> </div>
</template> </template>

View File

@ -84,13 +84,7 @@
getCurrentItemState.assertionType !== ResponseAssertionType.SCRIPT, getCurrentItemState.assertionType !== ResponseAssertionType.SCRIPT,
}" }"
> >
<a-scrollbar <div class="w-full">
:style="{
overflow: 'auto',
height: '100%',
width: '100%',
}"
>
<!-- 响应头 --> <!-- 响应头 -->
<ResponseHeaderTab <ResponseHeaderTab
v-if="getCurrentItemState.assertionType === ResponseAssertionType.RESPONSE_HEADER" v-if="getCurrentItemState.assertionType === ResponseAssertionType.RESPONSE_HEADER"
@ -130,7 +124,7 @@
@change="handleChange" @change="handleChange"
/> />
<!-- 脚本 --> <!-- 脚本 -->
</a-scrollbar> </div>
<ScriptTab <ScriptTab
v-if="getCurrentItemState.assertionType === ResponseAssertionType.SCRIPT" v-if="getCurrentItemState.assertionType === ResponseAssertionType.SCRIPT"
v-model:data="getCurrentItemState" v-model:data="getCurrentItemState"

View File

@ -461,7 +461,7 @@
key={element?.name} key={element?.name}
v-slots={{ v-slots={{
icon, icon,
title: () => 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' : ''} class={BOTTOM_MENU_LIST.includes(element?.name as string) ? 'arco-menu-inline--bottom' : ''}
> >

View File

@ -282,7 +282,6 @@
if (height > 1000) { if (height > 1000) {
codeheight.value = `1000px`; codeheight.value = `1000px`;
} }
editor.layout();
} }
const init = () => { const init = () => {

View File

@ -3,16 +3,22 @@ import type { MsFileItem } from '@/components/pure/ms-upload/types';
import type { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum'; import type { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
import type { BatchApiParams } from '../common'; import type { BatchApiParams } from '../common';
import type { ExecuteBinaryBody, KeyValueParam, ResponseDefinitionBody } from './common'; import type {
ExecuteBinaryBody,
ExecuteJsonBody,
ExecuteValueBody,
KeyValueParam,
ResponseDefinitionBody,
} from './common';
// mock 信息-匹配项 // mock 信息-匹配项
export interface MatchRuleItem { export interface MatchRuleItem {
id?: string; // 用于前端标识 id?: string; // 用于前端标识
paramType: RequestParamsType; // 用于前端标识
key: string; key: string;
value: string; value: string;
condition: string; condition: string;
description: string; description: string;
paramType: RequestParamsType;
files: ({ files: ({
fileId: string; fileId: string;
fileName: string; fileName: string;
@ -37,10 +43,13 @@ export interface MockMatchRuleCommon {
} }
// mock 信息-请求体匹配规则 // mock 信息-请求体匹配规则
export interface MockBody { export interface MockBody {
paramType: RequestBodyFormat; bodyType: RequestBodyFormat;
formDataMatch: MockMatchRuleCommon; formDataBody: MockMatchRuleCommon;
wwwFormBody: MockMatchRuleCommon;
jsonBody: ExecuteJsonBody;
xmlBody: ExecuteValueBody;
rawBody: ExecuteValueBody;
binaryBody: ExecuteBinaryBody; binaryBody: ExecuteBinaryBody;
raw: string;
} }
// mock 信息-匹配规则集合 // mock 信息-匹配规则集合
export interface MockMatchRule { export interface MockMatchRule {
@ -74,7 +83,6 @@ export interface UpdateMockParams extends MockParams {
// mock 信息-详情 // mock 信息-详情
export interface MockDetail extends MockParams { export interface MockDetail extends MockParams {
id: string; id: string;
matching: MockMatchRule;
} }
// 批量编辑 mock // 批量编辑 mock
export interface BatchEditMockParams extends BatchApiParams { export interface BatchEditMockParams extends BatchApiParams {

View File

@ -22,11 +22,10 @@
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<div class="flex h-full"> <div class="h-full">
<MsCodeEditor <MsCodeEditor
v-if="visible" v-if="visible"
v-model:model-value="batchParamsCode" v-model:model-value="batchParamsCode"
class="flex-1"
theme="vs" theme="vs"
height="100%" height="100%"
:show-full-screen="false" :show-full-screen="false"

View File

@ -123,10 +123,9 @@
{{ t('common.copy') }} {{ t('common.copy') }}
</a-button> </a-button>
</div> </div>
<div class="flex h-[412px]"> <div class="h-[412px]">
<MsCodeEditor <MsCodeEditor
v-model:model-value="scriptEx" v-model:model-value="scriptEx"
class="flex-1"
theme="vs" theme="vs"
:language="LanguageEnum.BEANSHELL_JSR233" :language="LanguageEnum.BEANSHELL_JSR233"
width="500px" width="500px"

View File

@ -114,12 +114,12 @@ export const defaultBodyParams: ExecuteBody = {
jsonValue: '', jsonValue: '',
}, },
xmlBody: { value: '' }, xmlBody: { value: '' },
rawBody: { value: '' },
binaryBody: { binaryBody: {
description: '', description: '',
file: undefined, file: undefined,
sendAsBody: false, sendAsBody: false,
}, },
rawBody: { value: '' },
}; };
// 默认的响应内容结构 // 默认的响应内容结构
@ -299,17 +299,25 @@ export const mockDefaultParams: MockParams = {
matchAll: true, matchAll: true,
}, },
body: { body: {
paramType: RequestBodyFormat.FORM_DATA, bodyType: RequestBodyFormat.FORM_DATA,
formDataMatch: { formDataBody: {
matchRules: [], matchRules: [],
matchAll: true, matchAll: true,
}, },
wwwFormBody: {
matchRules: [],
matchAll: true,
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
rawBody: { value: '' },
binaryBody: { binaryBody: {
description: '', description: '',
file: undefined, file: undefined,
sendAsBody: false, sendAsBody: false,
}, },
raw: '',
}, },
}, },
response: { response: {
@ -344,15 +352,19 @@ export const mockDefaultParams: MockParams = {
export const makeDefaultParams = () => { export const makeDefaultParams = () => {
const defaultParams = cloneDeep(mockDefaultParams); const defaultParams = cloneDeep(mockDefaultParams);
defaultParams.id = Date.now().toString(); defaultParams.id = Date.now().toString();
defaultParams.mockMatchRule.body.formDataMatch.matchRules.push({ defaultParams.mockMatchRule.body.formDataBody.matchRules.push({
...cloneDeep(defaultMatchRuleItem), ...defaultMatchRuleItem,
id: Date.now().toString(), id: Date.now().toString(),
}); });
defaultParams.mockMatchRule.header.matchRules.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); defaultParams.mockMatchRule.body.wwwFormBody.matchRules.push({
defaultParams.mockMatchRule.query.matchRules.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); ...defaultMatchRuleItem,
defaultParams.mockMatchRule.rest.matchRules.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); id: Date.now().toString(),
defaultParams.response.headers.push({ ...cloneDeep(defaultMatchRuleItem), id: Date.now().toString() }); });
return defaultParams; 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 匹配规则选项 // mock 匹配规则选项
export const matchRuleOptions = [ export const matchRuleOptions = [

View File

@ -84,11 +84,10 @@
</a-tooltip> </a-tooltip>
</div> --> </div> -->
</div> </div>
<div v-else class="flex h-[calc(100%-34px)]"> <div v-else class="h-[calc(100%-34px)]">
<MsCodeEditor <MsCodeEditor
v-model:model-value="currentBodyCode" v-model:model-value="currentBodyCode"
:read-only="props.disabledExceptParam" :read-only="props.disabledExceptParam"
class="flex-1"
theme="vs" theme="vs"
height="100%" height="100%"
:show-full-screen="false" :show-full-screen="false"

View File

@ -13,7 +13,11 @@
</div> </div>
</template> </template>
<template #condition="{ record }"> <template #condition="{ record }">
{{ t(statusCodeOptions.find((item) => item.value === record.condition)?.label || '') }} {{
record.assertionType === FullResponseAssertionType.RESPONSE_TIME
? t('advanceFilter.operator.le')
: t(statusCodeOptions.find((item) => item.value === record.condition)?.label || '')
}}
</template> </template>
<template #status="{ record }"> <template #status="{ record }">
<MsTag :type="record.pass === true ? 'success' : 'danger'" theme="light"> <MsTag :type="record.pass === true ? 'success' : 'danger'" theme="light">
@ -33,6 +37,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { RequestResult, ResponseAssertionTableItem } from '@/models/apiTest/common'; import { RequestResult, ResponseAssertionTableItem } from '@/models/apiTest/common';
import { FullResponseAssertionType } from '@/enums/apiEnum';
import { responseAssertionTypeMap } from '@/views/api-test/components/config'; import { responseAssertionTypeMap } from '@/views/api-test/components/config';

View File

@ -45,8 +45,9 @@ export function parseRequestBodyFiles(
const tempSaveLinkFileIds = new Set<string>(); // 临时存储 body 内已保存的关联文件 id 集合,用于对比 saveLinkFileIds 以判断有哪些文件被取消关联 const tempSaveLinkFileIds = new Set<string>(); // 临时存储 body 内已保存的关联文件 id 集合,用于对比 saveLinkFileIds 以判断有哪些文件被取消关联
// 获取上传文件和关联文件 // 获取上传文件和关联文件
const formValues = const formValues =
((body as ExecuteBody).formDataBody?.formValues || (body as MockBody).formDataMatch.matchRules).filter((e) => e) || ((body as ExecuteBody).formDataBody?.formValues || (body as MockBody).formDataBody?.matchRules || []).filter(
[]; (e) => e
) || [];
for (let i = 0; i < formValues.length; i++) { for (let i = 0; i < formValues.length; i++) {
const item = formValues[i]; const item = formValues[i];
if (item.paramType === RequestParamsType.FILE) { if (item.paramType === RequestParamsType.FILE) {

View File

@ -151,6 +151,7 @@
import { ProtocolItem } from '@/models/apiTest/common'; import { ProtocolItem } from '@/models/apiTest/common';
import { ApiDefinitionDetail } from '@/models/apiTest/management'; import { ApiDefinitionDetail } from '@/models/apiTest/management';
import { MockDetail } from '@/models/apiTest/mock';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { import {
RequestAuthType, RequestAuthType,
@ -380,6 +381,10 @@
} }
} }
async function openApiTabAndDebugMock(mock: MockDetail) {
await openApiTab(mock.apiDefinitionId as string);
}
// //
watch( watch(
() => activeApiTab.value.isNew, () => activeApiTab.value.isNew,
@ -452,6 +457,7 @@
defineExpose({ defineExpose({
openApiTab, openApiTab,
addApiTab, addApiTab,
openApiTabAndDebugMock,
refreshTable, refreshTable,
}); });
</script> </script>

View File

@ -31,7 +31,6 @@
<MsCodeEditor <MsCodeEditor
v-show="headerShowType === 'raw'" v-show="headerShowType === 'raw'"
:model-value="headerRawCode" :model-value="headerRawCode"
class="flex-1"
theme="MS-text" theme="MS-text"
height="200px" height="200px"
:show-full-screen="false" :show-full-screen="false"
@ -70,7 +69,6 @@
<MsCodeEditor <MsCodeEditor
v-show="queryShowType === 'raw'" v-show="queryShowType === 'raw'"
:model-value="queryRawCode" :model-value="queryRawCode"
class="flex-1"
theme="MS-text" theme="MS-text"
height="200px" height="200px"
:show-full-screen="false" :show-full-screen="false"
@ -109,7 +107,6 @@
<MsCodeEditor <MsCodeEditor
v-show="restShowType === 'raw'" v-show="restShowType === 'raw'"
:model-value="restRawCode" :model-value="restRawCode"
class="flex-1"
theme="MS-text" theme="MS-text"
height="200px" height="200px"
:show-full-screen="false" :show-full-screen="false"
@ -169,7 +166,6 @@
) )
" "
:model-value="bodyCode" :model-value="bodyCode"
class="flex-1"
theme="vs" theme="vs"
height="200px" height="200px"
:language="bodyCodeLanguage" :language="bodyCodeLanguage"
@ -210,7 +206,6 @@
<MsCodeEditor <MsCodeEditor
v-show="pluginShowType === 'raw'" v-show="pluginShowType === 'raw'"
:model-value="pluginRawCode" :model-value="pluginRawCode"
class="flex-1"
theme="MS-text" theme="MS-text"
height="400px" height="400px"
:show-full-screen="false" :show-full-screen="false"
@ -287,7 +282,6 @@
<MsCodeEditor <MsCodeEditor
v-else v-else
:model-value="responseCode" :model-value="responseCode"
class="flex-1"
theme="vs" theme="vs"
height="200px" height="200px"
:language="responseCodeLanguage" :language="responseCodeLanguage"

View File

@ -67,6 +67,7 @@
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:protocol="props.protocol" :protocol="props.protocol"
:definition-detail="activeApiTab" :definition-detail="activeApiTab"
@debug="handleMockDebug"
/> />
</template> </template>
@ -88,6 +89,7 @@
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
import { ProtocolItem } from '@/models/apiTest/common'; import { ProtocolItem } from '@/models/apiTest/common';
import { MockDetail } from '@/models/apiTest/mock';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { import {
RequestAuthType, RequestAuthType,
@ -323,6 +325,10 @@
} }
} }
function handleMockDebug(mock: MockDetail) {
apiRef.value?.openApiTabAndDebugMock(mock);
}
onBeforeMount(() => { onBeforeMount(() => {
initMemberOptions(); initMemberOptions();
initProtocolList(); initProtocolList();

View File

@ -20,7 +20,7 @@
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']" v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']"
type="icon" type="icon"
status="secondary" status="secondary"
@click="isEdit = true" @click="handleChangeEdit"
> >
<MsIcon type="icon-icon_edit_outlined" /> <MsIcon type="icon-icon_edit_outlined" />
{{ t('common.edit') }} {{ t('common.edit') }}
@ -37,7 +37,7 @@
</MsButton> </MsButton>
</div> </div>
</template> </template>
<a-spin :loading="loading" class="block p-[16px]"> <a-spin v-if="visible" :loading="loading" class="block p-[16px]">
<MsDetailCard <MsDetailCard
:title="`【${props.definitionDetail.num}】${props.definitionDetail.name}`" :title="`【${props.definitionDetail.num}】${props.definitionDetail.name}`"
:description="[]" :description="[]"
@ -103,7 +103,7 @@
<template v-else> <template v-else>
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<a-radio-group <a-radio-group
v-model:model-value="mockDetail.mockMatchRule.body.paramType" v-model:model-value="mockDetail.mockMatchRule.body.bodyType"
type="button" type="button"
size="small" size="small"
:disabled="isReadOnly" :disabled="isReadOnly"
@ -115,22 +115,28 @@
</a-radio-group> </a-radio-group>
</div> </div>
<div <div
v-if="mockDetail.mockMatchRule.body.paramType === RequestBodyFormat.NONE" v-if="mockDetail.mockMatchRule.body.bodyType === RequestBodyFormat.NONE"
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]" class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
> >
{{ t('apiTestDebug.noneBody') }} {{ t('apiTestDebug.noneBody') }}
</div> </div>
<mockMatchRuleForm <mockMatchRuleForm
v-else-if=" v-else-if="mockDetail.mockMatchRule.body.bodyType === RequestBodyFormat.FORM_DATA"
[RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(mockDetail.mockMatchRule.body.paramType)
"
:id="mockDetail.id" :id="mockDetail.id"
v-model:matchAll="mockDetail.mockMatchRule.body.formDataMatch.matchAll" v-model:matchAll="mockDetail.mockMatchRule.body.formDataBody.matchAll"
v-model:matchRules="mockDetail.mockMatchRule.body.formDataMatch.matchRules" v-model:matchRules="mockDetail.mockMatchRule.body.formDataBody.matchRules"
:key-options="currentBodyKeyOptions" :key-options="currentBodyKeyOptions"
:disabled="isReadOnly" :disabled="isReadOnly"
/> />
<div v-else-if="mockDetail.mockMatchRule.body.paramType === RequestBodyFormat.BINARY"> <mockMatchRuleForm
v-else-if="mockDetail.mockMatchRule.body.bodyType === RequestBodyFormat.WWW_FORM"
:id="mockDetail.id"
v-model:matchAll="mockDetail.mockMatchRule.body.wwwFormBody.matchAll"
v-model:matchRules="mockDetail.mockMatchRule.body.wwwFormBody.matchRules"
:key-options="currentBodyKeyOptions"
:disabled="isReadOnly"
/>
<div v-else-if="mockDetail.mockMatchRule.body.bodyType === RequestBodyFormat.BINARY">
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]"> <div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<MsAddAttachment <MsAddAttachment
v-model:file-list="fileList" v-model:file-list="fileList"
@ -162,12 +168,11 @@
</a-tooltip> </a-tooltip>
</div> --> </div> -->
</div> </div>
<div v-else class="flex h-[300px]"> <div v-else>
<MsCodeEditor <MsCodeEditor
v-model:model-value="mockDetail.mockMatchRule.body.raw" v-model:model-value="currentBodyCode"
class="flex-1"
theme="vs" theme="vs"
height="100%" is-adaptive
:show-full-screen="false" :show-full-screen="false"
:show-theme-change="false" :show-theme-change="false"
:show-code-format="true" :show-code-format="true"
@ -188,6 +193,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
@ -216,6 +222,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ResponseDefinition } from '@/models/apiTest/common';
import { MockParams } from '@/models/apiTest/mock'; import { MockParams } from '@/models/apiTest/mock';
import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum';
@ -231,6 +238,7 @@
definitionDetail: RequestParam; definitionDetail: RequestParam;
detailId?: string; detailId?: string;
isCopy?: boolean; isCopy?: boolean;
isEditMode?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'delete'): void; (e: 'delete'): void;
@ -245,7 +253,7 @@
}); });
const loading = ref(false); const loading = ref(false);
const isEdit = ref(false); const isEdit = ref(props.isEditMode);
const mockDetail = ref<MockParams>(makeDefaultParams()); const mockDetail = ref<MockParams>(makeDefaultParams());
const isReadOnly = computed(() => !mockDetail.value.isNew && !isEdit.value); const isReadOnly = computed(() => !mockDetail.value.isNew && !isEdit.value);
const title = computed(() => { const title = computed(() => {
@ -287,7 +295,7 @@
.validParams.length; .validParams.length;
return `${headerNum > 0 ? headerNum : ''}`; return `${headerNum > 0 ? headerNum : ''}`;
case RequestComposition.BODY: case RequestComposition.BODY:
return mockDetail.value.mockMatchRule.body.paramType !== RequestBodyFormat.NONE ? '1' : ''; return mockDetail.value.mockMatchRule.body.bodyType !== RequestBodyFormat.NONE ? '1' : '';
case RequestComposition.QUERY: case RequestComposition.QUERY:
const queryNum = filterKeyValParams(mockDetail.value.mockMatchRule.query.matchRules, defaultRequestParamsItem) const queryNum = filterKeyValParams(mockDetail.value.mockMatchRule.query.matchRules, defaultRequestParamsItem)
.validParams.length; .validParams.length;
@ -384,15 +392,16 @@
return []; return [];
} }
}); });
// body
const currentBodyKeyOptions = computed(() => { const currentBodyKeyOptions = computed(() => {
switch (mockDetail.value.mockMatchRule.body.paramType) { switch (mockDetail.value.mockMatchRule.body.bodyType) {
case RequestBodyFormat.FORM_DATA: case RequestBodyFormat.FORM_DATA:
return filterKeyValParams( return filterKeyValParams(
props.definitionDetail.body.formDataBody.formValues, props.definitionDetail.body.formDataBody.formValues,
defaultMatchRuleItem defaultMatchRuleItem
).validParams.map((e) => ({ ).validParams.map((e) => ({
label: e.key, label: e.key,
value: e.value, value: e.key,
paramType: e.paramType, paramType: e.paramType,
})); }));
case RequestBodyFormat.WWW_FORM: case RequestBodyFormat.WWW_FORM:
@ -407,17 +416,71 @@
return []; 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(() => { const currentCodeLanguage = computed(() => {
if (mockDetail.value.mockMatchRule.body.paramType === RequestBodyFormat.JSON) { if (mockDetail.value.mockMatchRule.body.bodyType === RequestBodyFormat.JSON) {
return LanguageEnum.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.XML;
} }
return LanguageEnum.PLAINTEXT; 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<MsFileItem[]>([]);
async function initMockDetail() { async function initMockDetail() {
try { try {
loading.value = true; loading.value = true;
@ -425,34 +488,45 @@
id: props.detailId || '', id: props.detailId || '',
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}); });
const parseFileResult = parseRequestBodyFiles(res.matching.body); // form-data
const formDataMatch = const formDataMatch = res.mockMatchRule.body.formDataBody.matchRules.map((item) => {
res.matching.body.paramType === RequestBodyFormat.FORM_DATA const newParamType =
? res.matching.body.formDataMatch.matchRules.map((item) => { currentBodyKeyOptions.value.find((e) => e.value === item.key)?.paramType || defaultMatchRuleItem.paramType;
const newParamType = item.paramType = newParamType;
currentBodyKeyOptions.value.find((e) => e.value === item.key)?.paramType || item.files = item.files || [];
defaultMatchRuleItem.paramType; return item;
item.paramType = newParamType; });
item.files = item.files || [];
return item;
})
: res.matching.body.formDataMatch.matchRules;
mockDetail.value = { mockDetail.value = {
...res, ...res,
id: props.isCopy ? '' : res.id, id: props.isCopy ? '' : res.id,
isNew: props.isCopy, isNew: props.isCopy,
name: props.isCopy ? `${res.name}_copy` : res.name,
mockMatchRule: { mockMatchRule: {
...res.matching, ...res.mockMatchRule,
body: { body: {
...res.matching.body, ...res.mockMatchRule.body,
formDataMatch: { formDataBody: {
...res.matching.body.formDataMatch, ...res.mockMatchRule.body.formDataBody,
matchRules: formDataMatch, matchRules: formDataMatch,
}, },
}, },
}, },
};
// formDataMatch body
const parseFileResult = parseRequestBodyFiles(mockDetail.value.mockMatchRule.body, [
res.response as unknown as ResponseDefinition,
]);
mockDetail.value = {
...mockDetail.value,
...parseFileResult, ...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) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -464,8 +538,12 @@
watch( watch(
() => visible.value, () => visible.value,
(val) => { (val) => {
if (val && props.detailId) { if (val) {
initMockDetail(); if (props.detailId) {
initMockDetail();
} else {
fileList.value = [];
}
} }
}, },
{ {
@ -473,8 +551,6 @@
} }
); );
const fileList = ref<MsFileItem[]>([]);
async function handleFileChange(files: MsFileItem[], file?: MsFileItem) { async function handleFileChange(files: MsFileItem[], file?: MsFileItem) {
try { try {
if (file?.local && file.file) { 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() { function handleCancel() {
mockDetail.value = makeDefaultParams(); mockDetail.value = makeDefaultParams();
isEdit.value = false; isEdit.value = false;
visible.value = false; visible.value = false;
} }
function handleChangeEdit() {
appendDefaultMatchRuleItem();
isEdit.value = true;
}
function handleDelete() { function handleDelete() {
emit('delete'); emit('delete');
handleCancel(); handleCancel();
@ -540,7 +608,14 @@
try { try {
loading.value = true; loading.value = true;
const { body } = mockDetail.value.mockMatchRule; 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( const validHeaderMatchRules = filterKeyValParams(
mockDetail.value.mockMatchRule.header.matchRules, mockDetail.value.mockMatchRule.header.matchRules,
defaultMatchRuleItem defaultMatchRuleItem
@ -557,17 +632,26 @@
mockDetail.value.response.headers, mockDetail.value.response.headers,
defaultHeaderParamsItem defaultHeaderParamsItem
).validParams; ).validParams;
const parseFileResult = parseRequestBodyFiles(mockDetail.value.mockMatchRule.body); const parseFileResult = parseRequestBodyFiles(
const params = { mockDetail.value.mockMatchRule.body,
[mockDetail.value.response as unknown as ResponseDefinition],
mockDetail.value.uploadFileIds,
mockDetail.value.linkFileIds
);
const params: MockParams = {
...mockDetail.value, ...mockDetail.value,
statusCode: mockDetail.value.response.statusCode, statusCode: mockDetail.value.response.statusCode,
mockMatchRule: { mockMatchRule: {
...mockDetail.value.mockMatchRule, ...mockDetail.value.mockMatchRule,
body: { body: {
...mockDetail.value.mockMatchRule.body, ...mockDetail.value.mockMatchRule.body,
formDataMatch: { formDataBody: {
...mockDetail.value.mockMatchRule.body.formDataMatch, ...mockDetail.value.mockMatchRule.body.formDataBody,
matchRules: validBodyMatchRules, matchRules: validFormDataBodyMatchRules,
},
wwwFormBody: {
...mockDetail.value.mockMatchRule.body.wwwFormBody,
matchRules: validWwwFormBodyMatchRules,
}, },
}, },
header: { header: {
@ -587,14 +671,16 @@
...mockDetail.value.response, ...mockDetail.value.response,
headers: validResponseHeaders, headers: validResponseHeaders,
}, },
...parseFileResult,
apiDefinitionId: props.definitionDetail.id, apiDefinitionId: props.definitionDetail.id,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
uploadFileIds: parseFileResult.uploadFileIds,
linkFileIds: parseFileResult.linkFileIds,
}; };
if (isEdit.value) { if (isEdit.value) {
await updateMock({ await updateMock({
id: mockDetail.value.id || '', id: mockDetail.value.id || '',
...params, ...params,
...parseFileResult,
}); });
Message.success(t('common.updateSuccess')); Message.success(t('common.updateSuccess'));
} else { } else {

View File

@ -154,6 +154,7 @@
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue'; import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.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 { responseHeaderOption } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -164,7 +165,6 @@
const props = defineProps<{ const props = defineProps<{
definitionResponses: ResponseItem[]; definitionResponses: ResponseItem[];
uploadTempFileApi?: (...args: any) => Promise<any>; //
disabled: boolean; disabled: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -267,16 +267,17 @@
const fileList = ref<MsFileItem[]>([]); const fileList = ref<MsFileItem[]>([]);
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
async function handleFileChange() {
async function handleFileChange(files: MsFileItem[], file?: MsFileItem) {
try { try {
if (fileList.value[0] && fileList.value[0].local && fileList.value[0].file && props.uploadTempFileApi) { if (file?.local && file.file) {
loading.value = true; loading.value = true;
const res = await props.uploadTempFileApi(fileList.value[0].file); const res = await uploadMockTempFile(file.file);
mockResponse.value.body.binaryBody.file = { mockResponse.value.body.binaryBody.file = {
...fileList.value[0], ...file,
fileId: res.data, fileId: res.data,
fileName: fileList.value[0]?.name || '', fileName: file?.name || '',
fileAlias: fileList.value[0]?.name || '', fileAlias: file?.name || '',
local: true, local: true,
}; };
loading.value = false; loading.value = false;
@ -298,6 +299,19 @@
loading.value = false; 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,
}
);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -46,10 +46,14 @@
v-model="record.enable" v-model="record.enable"
size="small" size="small"
type="line" type="line"
@change="(value) => changeDefault(value, record)" :before-change="() => handleBeforeEnableChange(record)"
></a-switch> ></a-switch>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<MsButton type="text" class="!mr-0" @click="editMock(record)">
{{ t('common.edit') }}
</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton type="text" class="!mr-0" @click="debugMock(record)"> <MsButton type="text" class="!mr-0" @click="debugMock(record)">
{{ t('apiTestManagement.debug') }} {{ t('apiTestManagement.debug') }}
</MsButton> </MsButton>
@ -70,12 +74,104 @@
</template> </template>
</ms-base-table> </ms-base-table>
</div> </div>
<a-modal v-model:visible="showBatchModal" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<template #title>
{{ t('common.edit') }}
<div class="text-[var(--color-text-4)]">
{{
t('apiTestManagement.batchModalSubTitle', {
count: batchParams.currentSelectCount || tableSelected.length,
})
}}
</div>
</template>
<a-form ref="batchFormRef" class="rounded-[4px]" :model="batchForm" layout="vertical">
<a-form-item
field="attr"
:label="t('apiTestManagement.chooseAttr')"
:rules="[{ required: true, message: t('apiTestManagement.attrRequired') }]"
asterisk-position="end"
>
<a-select v-model="batchForm.attr" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of attrOptions" :key="item.value" :value="item.value">
{{ t(item.name) }}
</a-option>
</a-select>
</a-form-item>
<a-form-item
v-if="batchForm.attr === 'Tags'"
field="values"
:label="t('apiTestManagement.batchUpdate')"
:validate-trigger="['blur', 'input']"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end"
class="mb-0"
required
>
<MsTagsInput
v-model:model-value="batchForm.values"
placeholder="common.tagsInputPlaceholder"
allow-clear
unique-value
retain-input-value
/>
</a-form-item>
<a-form-item
v-else
field="value"
:label="t('apiTestManagement.batchUpdate')"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end"
class="mb-0"
>
<a-radio-group v-model:model-value="batchForm.value">
<a-radio :value="true">
{{ t('common.enable') }}
</a-radio>
<a-radio :value="false">
{{ t('common.disable') }}
</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
<template #footer>
<div class="flex" :class="[batchForm.attr === 'Tags' ? 'justify-between' : 'justify-end']">
<div
v-if="batchForm.attr === 'Tags'"
class="flex flex-row items-center justify-center"
style="padding-top: 10px"
>
<a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" />
<span class="flex items-center">
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
<span class="mt-[2px]">
<a-tooltip>
<IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" />
<template #content>
<div>{{ t('caseManagement.featureCase.enableTags') }}</div>
<div>{{ t('caseManagement.featureCase.closeTags') }}</div>
</template>
</a-tooltip>
</span>
</span>
</div>
<div class="flex justify-end">
<a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch">
{{ t('common.cancel') }}
</a-button>
<a-button class="ml-3" type="primary" :loading="batchUpdateLoading" @click="batchUpdate">
{{ t('common.update') }}
</a-button>
</div>
</div>
</template>
</a-modal>
<mockDetailDrawer <mockDetailDrawer
v-if="mockDetailDrawerVisible"
v-model:visible="mockDetailDrawerVisible" v-model:visible="mockDetailDrawerVisible"
:definition-detail="mockBelongDefinitionDetail" :definition-detail="mockBelongDefinitionDetail"
:detail-id="activeMockRecord?.id" :detail-id="activeMockRecord?.id"
:is-copy="isCopy" :is-copy="isCopy"
:is-edit-mode="isEdit"
@add-done="loadMockList" @add-done="loadMockList"
@delete="() => removeMock(activeMockRecord)" @delete="() => removeMock(activeMockRecord)"
/> />
@ -83,7 +179,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard } from '@vueuse/core'; import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
@ -92,13 +188,17 @@
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import mockDetailDrawer from './mockDetailDrawer.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { import {
batchDeleteMock, batchDeleteMock,
batchEditMock,
deleteMock, deleteMock,
getDefinitionDetail, getDefinitionDetail,
getDefinitionMockPage, getDefinitionMockPage,
getMockDetail,
getMockUrl, getMockUrl,
updateMockStatusPage, updateMockStatusPage,
} from '@/api/modules/api-test/management'; } from '@/api/modules/api-test/management';
@ -109,12 +209,10 @@
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
import { ApiDefinitionMockDetail } from '@/models/apiTest/management'; import { ApiDefinitionMockDetail } from '@/models/apiTest/management';
import { OrdTemplateManagement } from '@/models/setting/template'; import { MockDetail } from '@/models/apiTest/mock';
import { RequestComposition } from '@/enums/apiEnum'; import { RequestComposition } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const mockDetailDrawer = defineAsyncComponent(() => import('./mockDetailDrawer.vue'));
const props = defineProps<{ const props = defineProps<{
isApi?: boolean; // case tab isApi?: boolean; // case tab
class?: string; class?: string;
@ -127,6 +225,7 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'init', params: any): void; (e: 'init', params: any): void;
(e: 'change'): void; (e: 'change'): void;
(e: 'debug', mock: MockDetail): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -147,7 +246,7 @@
sorter: true, sorter: true,
}, },
fixed: 'left', fixed: 'left',
width: 100, width: 120,
}, },
{ {
title: 'mockManagement.name', title: 'mockManagement.name',
@ -200,7 +299,7 @@
slotName: 'action', slotName: 'action',
dataIndex: 'operation', dataIndex: 'operation',
fixed: 'right', fixed: 'right',
width: 150, width: 200,
}, },
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
@ -223,17 +322,11 @@
const batchActions = { const batchActions = {
baseAction: [ baseAction: [
{ {
label: 'mockManagement.batchEnable', label: 'mockManagement.batchEdit',
eventTag: 'batchEnable', eventTag: 'edit',
}, },
{ {
label: 'mockManagement.batchDisEnable', label: 'mockManagement.batchDelete',
eventTag: 'batchDisEnable',
},
],
moreAction: [
{
label: 'common.delete',
eventTag: 'delete', eventTag: 'delete',
danger: true, danger: true,
}, },
@ -294,16 +387,17 @@
} }
}); });
const changeDefault = async (value: any, record: OrdTemplateManagement) => { async function handleBeforeEnableChange(record: ApiDefinitionMockDetail) {
try { try {
await updateMockStatusPage(record.id); await updateMockStatusPage(record.id);
Message.success(t('system.orgTemplate.setSuccessfully')); Message.success(record.enable ? t('common.disableSuccess') : t('common.enableSuccess'));
loadMockList(); return true;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
return false;
} }
}; }
const tableSelected = ref<(string | number)[]>([]); const tableSelected = ref<(string | number)[]>([]);
const batchParams = ref<BatchActionQueryParams>({ const batchParams = ref<BatchActionQueryParams>({
@ -317,10 +411,10 @@
* 删除接口 * 删除接口
*/ */
function removeMock(record?: ApiDefinitionMockDetail, isBatch?: boolean, params?: BatchActionQueryParams) { function removeMock(record?: ApiDefinitionMockDetail, isBatch?: boolean, params?: BatchActionQueryParams) {
let title = t('apiTestManagement.deleteApiTipTitle', { name: record?.name }); let title = t('apiTestManagement.deleteMockTip', { name: record?.name });
let selectIds = [record?.id || '']; let selectIds = [record?.id || ''];
if (isBatch) { if (isBatch) {
title = t('apiTestManagement.batchDeleteMockTip', { title = t('mockManagement.batchDeleteMockTip', {
count: params?.currentSelectCount || tableSelected.value.length, count: params?.currentSelectCount || tableSelected.value.length,
}); });
selectIds = tableSelected.value as string[]; selectIds = tableSelected.value as string[];
@ -392,7 +486,7 @@
function handleTableMoreActionSelect(item: ActionsItem, record: ApiDefinitionMockDetail) { function handleTableMoreActionSelect(item: ActionsItem, record: ApiDefinitionMockDetail) {
switch (item.eventTag) { switch (item.eventTag) {
case 'delete': case 'delete':
deleteMock(record); removeMock(record);
break; break;
case 'copyMock': case 'copyMock':
copyMockUrl(record); copyMockUrl(record);
@ -409,6 +503,76 @@
tableSelected.value = arr; tableSelected.value = arr;
} }
const showBatchModal = ref(false);
const batchUpdateLoading = ref(false);
const batchFormRef = ref<FormInstance>();
const batchForm = ref({
attr: 'Status' as 'Status' | 'Tags',
value: true,
values: [],
append: false,
});
const fullAttrs = [
{
name: 'common.status',
value: 'Status',
},
{
name: 'common.tag',
value: 'Tags',
},
];
const attrOptions = computed(() => {
if (props.protocol === 'HTTP') {
return fullAttrs;
}
return fullAttrs.filter((e) => e.value !== 'method');
});
function cancelBatch() {
showBatchModal.value = false;
batchFormRef.value?.resetFields();
batchForm.value = {
attr: 'Status',
value: true,
values: [],
append: false,
};
}
function batchUpdate() {
batchFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
batchUpdateLoading.value = true;
await batchEditMock({
selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [],
condition: {
keyword: keyword.value,
},
projectId: appStore.currentProjectId,
moduleIds: await getModuleIds(),
type: batchForm.value.attr,
append: batchForm.value.append,
tags: batchForm.value.attr === 'Tags' ? batchForm.value.values : [],
enable: batchForm.value.attr === 'Status' ? batchForm.value.value : false,
});
Message.success(t('common.updateSuccess'));
cancelBatch();
resetSelector();
loadMockList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
batchUpdateLoading.value = false;
}
}
});
}
/** /**
* 处理表格选中后批量操作 * 处理表格选中后批量操作
* @param event 批量操作事件对象 * @param event 批量操作事件对象
@ -420,6 +584,9 @@
case 'delete': case 'delete':
removeMock(undefined, true, params); removeMock(undefined, true, params);
break; break;
case 'edit':
showBatchModal.value = true;
break;
default: default:
break; break;
} }
@ -427,9 +594,13 @@
const mockDetailDrawerVisible = ref(false); const mockDetailDrawerVisible = ref(false);
const activeMockRecord = ref<ApiDefinitionMockDetail>(); const activeMockRecord = ref<ApiDefinitionMockDetail>();
const isCopy = ref(false);
const isEdit = ref(false);
function createMock() { function createMock() {
activeMockRecord.value = undefined; activeMockRecord.value = undefined;
isCopy.value = false;
isEdit.value = false;
mockDetailDrawerVisible.value = true; mockDetailDrawerVisible.value = true;
} }
@ -463,20 +634,37 @@
} }
} }
const isCopy = ref(false);
function handleOpenDetail(record: ApiDefinitionMockDetail) { function handleOpenDetail(record: ApiDefinitionMockDetail) {
isEdit.value = false;
isCopy.value = false; isCopy.value = false;
openMockDetailDrawer(record); openMockDetailDrawer(record);
} }
function handleCopyMock(record: ApiDefinitionMockDetail) { function handleCopyMock(record: ApiDefinitionMockDetail) {
isCopy.value = true; isCopy.value = true;
isEdit.value = false;
openMockDetailDrawer(record); openMockDetailDrawer(record);
} }
function debugMock(record: ApiDefinitionMockDetail) { function editMock(record: ApiDefinitionMockDetail) {
activeMockRecord.value = record; isEdit.value = true;
openMockDetailDrawer(record);
}
async function debugMock(record: ApiDefinitionMockDetail) {
try {
appStore.showLoading();
const res = await getMockDetail({
id: record.id,
projectId: appStore.currentProjectId,
});
emit('debug', res);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
} }
defineExpose({ defineExpose({

View File

@ -123,7 +123,8 @@ export default {
'apiTestManagement.collapseApi': 'Hide all requests', 'apiTestManagement.collapseApi': 'Hide all requests',
'apiTestManagement.paramName': 'Parameter name', 'apiTestManagement.paramName': 'Parameter name',
'apiTestManagement.paramVal': 'Parameter value', 'apiTestManagement.paramVal': 'Parameter value',
'apiTestManagement.deleteMockTip': 'After deletion, it cannot be restored. Are you sure you want to delete it?', 'apiTestManagement.deleteMockTip':
'Deleting a mock expectation will cause the test task using the expectation to fail, so please operate with caution!',
'apiTestManagement.preview': 'Preview', 'apiTestManagement.preview': 'Preview',
'apiTestManagement.shareUrlCopied': 'Sharing link copied to clipboard', 'apiTestManagement.shareUrlCopied': 'Sharing link copied to clipboard',
'apiTestManagement.detail': 'Detail', 'apiTestManagement.detail': 'Detail',

View File

@ -118,7 +118,7 @@ export default {
'apiTestManagement.collapseApi': '隐藏全部请求', 'apiTestManagement.collapseApi': '隐藏全部请求',
'apiTestManagement.paramName': '参数名', 'apiTestManagement.paramName': '参数名',
'apiTestManagement.paramVal': '参数值', 'apiTestManagement.paramVal': '参数值',
'apiTestManagement.deleteMockTip': '删除后不可恢复,确认删除吗?', 'apiTestManagement.deleteMockTip': '删除 Mock 期望会导致使用了该期望的测试任务执行失败,请谨慎操作!',
'apiTestManagement.preview': '预览', 'apiTestManagement.preview': '预览',
'apiTestManagement.shareUrlCopied': '分享链接已复制到剪贴板', 'apiTestManagement.shareUrlCopied': '分享链接已复制到剪贴板',
'apiTestManagement.detail': '详情', 'apiTestManagement.detail': '详情',
@ -213,7 +213,7 @@ export default {
'mockManagement.copyMock': '复制Mock地址', 'mockManagement.copyMock': '复制Mock地址',
'mockManagement.batchEnable': '批量启用', 'mockManagement.batchEnable': '批量启用',
'mockManagement.batchDisEnable': '批量禁用', 'mockManagement.batchDisEnable': '批量禁用',
'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个Mock吗', 'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个 Mock 吗?',
'mockManagement.allMock': '全部 MOCK', 'mockManagement.allMock': '全部 MOCK',
'mockManagement.createMock': '创建 MOCK', 'mockManagement.createMock': '创建 MOCK',
'mockManagement.updateMock': '更新 MOCK', 'mockManagement.updateMock': '更新 MOCK',
@ -235,4 +235,6 @@ export default {
'mockManagement.empty': '为空', 'mockManagement.empty': '为空',
'mockManagement.notEmpty': '非空', 'mockManagement.notEmpty': '非空',
'mockManagement.regular': '正则匹配', 'mockManagement.regular': '正则匹配',
'mockManagement.batchEdit': '批量编辑',
'mockManagement.batchDelete': '批量删除',
}; };

View File

@ -253,7 +253,7 @@
class="overflow-hidden" class="overflow-hidden"
is-preview is-preview
> >
<div class="absolute w-full bg-white" style="height: calc(100% - 28px)"></div> <div class="w-full bg-white" style="height: calc(100% - 28px)"></div>
</defaultLayout> </defaultLayout>
</div> </div>
</div> </div>

View File

@ -9,18 +9,15 @@
@close="handleClose" @close="handleClose"
> >
<div class="w-full"> <div class="w-full">
<div class="w-full"> <MsCodeEditor
<MsCodeEditor v-model:model-value="pluginScript"
v-model:model-value="pluginScript" title="JSON"
title="JSON" height="calc(100vh - 155px)"
width="100%" theme="MS-text"
height="calc(100vh - 155px)" :read-only="props.readOnly"
theme="MS-text" :show-theme-change="false"
:read-only="props.readOnly" :show-title-line="true"
:show-theme-change="false" />
:show-title-line="true"
/>
</div>
</div> </div>
</MsDrawer> </MsDrawer>
</template> </template>

View File

@ -71,7 +71,7 @@ export default {
'system.resourcePool.batchAddTipConfirm': 'Got it', 'system.resourcePool.batchAddTipConfirm': 'Got it',
'system.resourcePool.batchAddResource': 'Batch add resources', 'system.resourcePool.batchAddResource': 'Batch add resources',
'system.resourcePool.changeAddTypeTip': 'system.resourcePool.changeAddTypeTip':
'After switching, the content of the added resources will continue to appear in yaml; the added resources can be modified in batches', 'After switching, the content of the added resources will continue to appear in csv; the added resources can be modified in batches',
'system.resourcePool.changeAddTypePopTitle': 'Toggle add resource type?', 'system.resourcePool.changeAddTypePopTitle': 'Toggle add resource type?',
'system.resourcePool.ip': 'IP', 'system.resourcePool.ip': 'IP',
'system.resourcePool.ipRequired': 'Please enter an IP address', 'system.resourcePool.ipRequired': 'Please enter an IP address',

View File

@ -68,7 +68,7 @@ export default {
'system.resourcePool.batchAdd': '批量添加', 'system.resourcePool.batchAdd': '批量添加',
'system.resourcePool.batchAddTipConfirm': '知道了', 'system.resourcePool.batchAddTipConfirm': '知道了',
'system.resourcePool.batchAddResource': '批量添加资源', 'system.resourcePool.batchAddResource': '批量添加资源',
'system.resourcePool.changeAddTypeTip': '切换后,已添加资源内容将继续显示在 yaml 内;可批量修改已添加资源', 'system.resourcePool.changeAddTypeTip': '切换后,已添加资源内容将继续显示在 csv 内;可批量修改已添加资源',
'system.resourcePool.changeAddTypePopTitle': '切换添加资源类型?', 'system.resourcePool.changeAddTypePopTitle': '切换添加资源类型?',
'system.resourcePool.allUseTip': '如果配置多个测试类型,会存在抢占资源的情况,建议一种测试类型配置一个资源池', 'system.resourcePool.allUseTip': '如果配置多个测试类型,会存在抢占资源的情况,建议一种测试类型配置一个资源池',
'system.resourcePool.ip': 'IP', 'system.resourcePool.ip': 'IP',