feat(接口测试): mock 页面部分接口联调&部分页面调整

This commit is contained in:
baiqi 2024-05-09 16:07:47 +08:00 committed by 刘瑞斌
parent e4c1a9d9d7
commit bdc1843176
24 changed files with 775 additions and 256 deletions

View File

@ -3,12 +3,15 @@ import {
AddCaseUrl,
AddDefinitionScheduleUrl,
AddDefinitionUrl,
AddMockUrl,
AddModuleUrl,
BatchCleanOutApiUrl,
BatchDeleteCaseUrl,
BatchDeleteDefinitionUrl,
BatchDeleteMockUrl,
BatchDeleteRecycleCaseUrl,
BatchEditCaseUrl,
BatchEditMockUrl,
BatchExecuteCaseUrl,
BatchMoveDefinitionUrl,
BatchRecoverApiUrl,
@ -16,6 +19,7 @@ import {
BatchUpdateDefinitionUrl,
CasePageUrl,
CheckDefinitionScheduleUrl,
CopyMockUrl,
DebugCaseUrl,
DebugDefinitionUrl,
DefinitionMockPageUrl,
@ -39,6 +43,7 @@ import {
GetEnvListUrl,
GetEnvModuleUrl,
GetExecuteHistoryUrl,
GetMockUrlUrl,
GetModuleCountUrl,
GetModuleOnlyTreeUrl,
GetModuleTreeUrl,
@ -46,6 +51,7 @@ import {
GetTrashModuleCountUrl,
GetTrashModuleTreeUrl,
ImportDefinitionUrl,
MockDetailUrl,
MoveModuleUrl,
OperationHistoryUrl,
PoolOption,
@ -64,15 +70,19 @@ import {
TransferFileModuleOptionCaseUrl,
TransferFileModuleOptionUrl,
TransferFileUrl,
TransferMockFileModuleOptionUrl,
TransferMockFileUrl,
UpdateCasePriorityUrl,
UpdateCaseStatusUrl,
UpdateCaseUrl,
UpdateDefinitionScheduleUrl,
UpdateDefinitionUrl,
UpdateMockStatusUrl,
UpdateMockUrl,
UpdateModuleUrl,
UploadTempFileCaseUrl,
UploadTempFileUrl,
UploadTempMockFileUrl,
} from '@/api/requrls/api-test/management';
import { ApiCaseReportDetail, ExecuteRequestParams } from '@/models/apiTest/common';
@ -113,8 +123,10 @@ import {
RecoverDefinitionParams,
UpdateScheduleParams,
} from '@/models/apiTest/management';
import type { BatchEditMockParams, MockDetail, MockParams, UpdateMockParams } from '@/models/apiTest/mock';
import {
AddModuleParams,
type BatchApiParams,
CommonList,
DragSortParams,
ModuleTreeNode,
@ -302,10 +314,59 @@ export function updateMockStatusPage(id: string) {
}
// 刪除mock接口
export function deleteDefinitionMockMock(data: mockParams) {
export function deleteMock(data: mockParams) {
return MSR.post({ url: DeleteMockUrl, data });
}
// 上传文件
export function uploadMockTempFile(file: File) {
return MSR.uploadFile({ url: UploadTempMockFileUrl }, { fileList: [file] }, 'file');
}
// 文件转存
export function transferMockFile(data: TransferFileParams) {
return MSR.post({ url: TransferMockFileUrl, data });
}
// 文件转存目录
export function getMockTransferOptions(projectId: string) {
return MSR.get<ModuleTreeNode[]>({ url: TransferMockFileModuleOptionUrl, params: projectId });
}
// 更新 mock
export function updateMock(data: UpdateMockParams) {
return MSR.post({ url: UpdateMockUrl, data });
}
// 获取 mock 详情
export function getMockDetail(data: { id: string; projectId: string }) {
return MSR.post<MockDetail>({ url: MockDetailUrl, data });
}
// 复制 mock
export function copyMock(data: { id: string; projectId: string }) {
return MSR.post({ url: CopyMockUrl, data });
}
// 批量编辑 mock
export function batchEditMock(data: BatchEditMockParams) {
return MSR.post({ url: BatchEditMockUrl, data });
}
// 批量删除 mock
export function batchDeleteMock(data: BatchApiParams) {
return MSR.post({ url: BatchDeleteMockUrl, data });
}
// 添加 mock
export function addMock(data: MockParams) {
return MSR.post({ url: AddMockUrl, data });
}
// 获取 mock url
export function getMockUrl(id: string) {
return MSR.get({ url: GetMockUrlUrl, params: id });
}
/**
*
*/

View File

@ -42,6 +42,16 @@ export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取
export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表
export const UpdateMockStatusUrl = '/api/definition/mock/enable'; // 更新mock状态
export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock
export const UploadTempMockFileUrl = '/api/definition/mock/upload/temp/file'; // mock临时上传文件
export const TransferMockFileUrl = '/api/definition/mock/transfer'; // mock临时文件转存
export const TransferMockFileModuleOptionUrl = '/api/definition/mock/transfer/options'; // mock临时文件转存目录下拉框
export const UpdateMockUrl = '/api/definition/mock/update'; // mock更新
export const MockDetailUrl = '/api/definition/mock/detail'; // mock详情
export const CopyMockUrl = '/api/definition/mock/copy'; // 复制mock
export const BatchEditMockUrl = '/api/definition/mock/batch/edit'; // 批量编辑mock
export const BatchDeleteMockUrl = '/api/definition/mock/batch/delete'; // 批量删除mock
export const AddMockUrl = '/api/definition/mock/add'; // 添加mock
export const GetMockUrlUrl = '/api/definition/mock/get-url'; // 获取mock url
/**
* api回收站

View File

@ -274,7 +274,7 @@
width: 100%;
border: 1px solid var(--color-text-input-border);
background-color: var(--color-text-fff);
&:not(:disabled):hover {
&:not(:disabled, .arco-input-tag-disabled, .arco-input-disabled, .arco-select-view-disabled):hover {
border-color: rgb(var(--primary-5)) !important;
background-color: white;
}
@ -282,6 +282,7 @@
color: var(--color-text-brand);
}
}
.arco-input-tag-disabled,
.arco-select-view-disabled,
.arco-input-disabled {
border-color: var(--color-text-n8) !important;

View File

@ -73,7 +73,7 @@
arrow-class="hidden"
:popup-offset="0"
>
<div class="!w-[calc(100%-28px)]">
<div class="h-full flex-1">
<MsTagsInput
v-model:model-value="inputFiles"
:input-class="props.inputClass"

View File

@ -21,15 +21,17 @@
</template>
<template #title="_props">
<div class="flex w-full items-center gap-[4px] overflow-hidden">
<div
v-if="_props.children && _props.children.length > 0"
class="cursor-pointer"
@click.stop="handleExpand(_props)"
>
<icon-caret-down v-if="_props.expanded" class="text-[var(--color-text-4)]" />
<icon-caret-right v-else class="text-[var(--color-text-4)]" />
</div>
<div v-else class="h-full w-[16px]"></div>
<template v-if="!props.hideSwitcher">
<div
v-if="_props.children && _props.children.length > 0"
class="cursor-pointer"
@click.stop="handleExpand(_props)"
>
<icon-caret-down v-if="_props.expanded" class="text-[var(--color-text-4)]" />
<icon-caret-right v-else class="text-[var(--color-text-4)]" />
</div>
<div v-else class="h-full w-[16px]"></div>
</template>
<a-tooltip
v-if="$slots['title']"
:content="_props[props.fieldNames.title]"
@ -121,6 +123,7 @@
disabledTitleTooltip?: boolean; // tooltip
actionOnNodeClick?: 'expand'; //
nodeHighlightClass?: string; //
hideSwitcher?: boolean; //
titleTooltipPosition?:
| 'top'
| 'tl'

View File

@ -147,6 +147,7 @@
drawerStyle?: Record<string, string>; //
showFullScreen?: boolean; //
maskClosable?: boolean; //
unmountOnClose?: boolean; //
handleBeforeCancel?: () => boolean;
}
@ -160,6 +161,7 @@
disabledWidthDrag: false,
showFullScreen: false,
maskClosable: true,
unmountOnClose: false,
okPermission: () => [], //
});
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue', 'close']);

View File

@ -1,13 +1,26 @@
import type { RequestBodyFormat } from '@/enums/apiEnum';
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';
// mock 信息-匹配项
export interface MatchRuleItem {
id?: string; // 用于前端标识
key: string;
value: string;
condition: string;
description: string;
paramType: RequestParamsType;
files: ({
fileId: string;
fileName: string;
local: boolean; // 是否是本地上传的文件
fileAlias: string; // 文件别名
delete: boolean; // 是否删除
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
} & MsFileItem)[];
}
// mock 信息-响应内容
export interface MockResponse {
@ -45,9 +58,28 @@ export interface MockParams {
tags: string[];
mockMatchRule: MockMatchRule;
response: MockResponse;
apiDefinitionId: string;
apiDefinitionId: string | number;
uploadFileIds: string[];
linkFileIds: string[];
// 前端扩展字段
unSaved?: boolean;
isNew: boolean;
}
// mock 信息-更新
export interface UpdateMockParams extends MockParams {
id: string;
deleteFileIds: string[];
unLinkFileIds: string[];
}
// mock 信息-详情
export interface MockDetail extends MockParams {
id: string;
matching: MockMatchRule;
}
// 批量编辑 mock
export interface BatchEditMockParams extends BatchApiParams {
type: 'Status' | 'Tags'; // 编辑类型
tags: string[]; // 标签
append: boolean; // 是否追加
enable: boolean; // 是否启用
}

View File

@ -48,6 +48,8 @@ export interface BatchApiParams {
currentSelectCount?: number; // 当前已选择的数量
projectId?: string; // 项目 ID
moduleIds?: (string | number)[]; // 模块 ID 集合
versionId?: string; // 版本 ID
refId?: string; // 版本来源
}
// 移动模块树

View File

@ -2,7 +2,6 @@
export interface NodesListItem {
ip: string;
port: string;
monitor: string;
concurrentNumber: number;
}

View File

@ -1,3 +1,5 @@
import { cloneDeep } from 'lodash-es';
import { EQUAL } from '@/components/pure/ms-advance-filter';
import {
@ -237,57 +239,39 @@ export const regexDefaultParamItem = {
responseFormat: ResponseBodyXPathAssertionFormat.XML,
moreSettingPopoverVisible: false,
};
// mock 匹配规则默认项
export const defaultMatchRuleItem = {
key: '',
value: '',
condition: 'EQUALS',
description: '',
paramType: RequestParamsType.STRING,
files: [],
};
// mock 默认参数
export const mockDefaultParams: MockParams = {
isNew: true,
projectId: '',
name: '',
statusCode: 200,
tags: [],
mockMatchRule: {
header: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchRules: [],
matchAll: true,
},
query: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchRules: [],
matchAll: true,
},
rest: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchRules: [],
matchAll: true,
},
body: {
paramType: RequestBodyFormat.FORM_DATA,
formDataMatch: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchRules: [],
matchAll: true,
},
binaryBody: {
@ -300,13 +284,7 @@ export const mockDefaultParams: MockParams = {
},
response: {
statusCode: 200,
headers: [
{
key: '',
value: '',
description: '',
},
],
headers: [],
useApiResponse: false,
apiResponseId: '',
body: {
@ -333,3 +311,65 @@ export const mockDefaultParams: MockParams = {
uploadFileIds: [],
linkFileIds: [],
};
export const makeDefaultParams = () => {
const defaultParams = cloneDeep(mockDefaultParams);
defaultParams.id = Date.now().toString();
defaultParams.mockMatchRule.body.formDataMatch.matchRules.push({
...cloneDeep(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;
};
// mock 匹配规则选项
export const matchRuleOptions = [
{
label: 'mockManagement.equals',
value: 'EQUALS',
},
{
label: 'mockManagement.notEquals',
value: 'NOT_EQUALS',
},
{
label: 'mockManagement.lengthEquals',
value: 'LENGTH_EQUALS',
},
{
label: 'mockManagement.lengthNotEquals',
value: 'LENGTH_NOT_EQUALS',
},
{
label: 'mockManagement.lengthLarge',
value: 'LENGTH_LARGE',
},
{
label: 'mockManagement.lengthLess',
value: 'LENGTH_SHOT',
},
{
label: 'mockManagement.contain',
value: 'CONTAINS',
},
{
label: 'mockManagement.notContain',
value: 'NOT_CONTAINS',
},
{
label: 'mockManagement.empty',
value: 'IS_EMPTY',
},
{
label: 'mockManagement.notEmpty',
value: 'IS_NOT_EMPTY',
},
{
label: 'mockManagement.regular',
value: 'REGULAR_MATCH',
},
];
// mock 参数为文件类型的匹配规则选项
export const mockFileMatchRules = ['EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY'];

View File

@ -6,6 +6,7 @@ import {
type ExecuteConditionConfig,
type ResponseDefinition,
} from '@/models/apiTest/common';
import type { MockBody } from '@/models/apiTest/mock';
import { RequestConditionProcessor, RequestParamsType } from '@/enums/apiEnum';
import {
@ -28,22 +29,24 @@ export interface ParseResult {
}
/**
* body
* /Mock body
* @param body body
*/
export function parseRequestBodyFiles(
body: ExecuteBody,
body: ExecuteBody | MockBody,
response?: ResponseDefinition[],
saveUploadFileIds?: string[],
saveLinkFileIds?: string[]
): ParseResult {
const { formDataBody, binaryBody } = body;
const { binaryBody } = body;
const uploadFileIds = new Set<string>(); // 存储本地上传的文件 id 集合
const linkFileIds = new Set<string>(); // 存储关联文件 id 集合
const tempSaveUploadFileIds = new Set<string>(); // 临时存储 body 内已保存的上传文件 id 集合,用于对比 saveUploadFileIds 以判断有哪些文件被删除
const tempSaveLinkFileIds = new Set<string>(); // 临时存储 body 内已保存的关联文件 id 集合,用于对比 saveLinkFileIds 以判断有哪些文件被取消关联
// 获取上传文件和关联文件
const formValues = formDataBody?.formValues.filter((e) => e) || [];
const formValues =
((body as ExecuteBody).formDataBody?.formValues || (body as MockBody).formDataMatch.matchRules).filter((e) => e) ||
[];
for (let i = 0; i < formValues.length; i++) {
const item = formValues[i];
if (item.paramType === RequestParamsType.FILE) {

View File

@ -115,6 +115,7 @@
:active-module="props.activeModule"
:offspring-ids="props.offspringIds"
:definition-detail="activeApiTab"
:protocol="activeApiTab.protocol"
is-api
/>
</a-tab-pane>

View File

@ -1,14 +1,18 @@
<template>
<MsDrawer
v-model:visible="visible"
unmount-on-close
:title="mockDetail.id ? t('mockManagement.mockDetail') : t('mockManagement.createMock')"
:title="title"
:width="960"
:footer="!mockDetail.id || isEdit"
:footer="!isReadOnly || isEdit"
:ok-text="isEdit ? t('common.save') : t('common.create')"
:save-continue-text="t('mockManagement.saveAndContinue')"
:show-continue="!isEdit"
:ok-loading="loading"
no-content-padding
unmount-on-close
@confirm="handleSave"
@cancel="handleCancel"
@close="handleCancel"
>
<template #tbutton>
<div v-if="mockDetail.id" class="right-operation-button-icon flex items-center gap-[4px]">
@ -23,11 +27,12 @@
</MsButton>
<MsButton
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+DELETE']"
class="mr-0"
type="icon"
status="danger"
status="secondary"
@click="handleDelete"
>
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
<MsIcon type="icon-icon_delete-trash_outlined" />
{{ t('common.delete') }}
</MsButton>
</div>
@ -53,7 +58,7 @@
</div>
</template>
</MsDetailCard>
<a-form ref="mockForm" :model="mockDetail">
<a-form ref="mockForm" :model="mockDetail" :disabled="isReadOnly">
<a-form-item
class="hidden-item"
field="name"
@ -63,7 +68,6 @@
v-model:model-value="mockDetail.name"
:placeholder="t('mockManagement.namePlaceholder')"
class="mb-[16px] w-[732px]"
:disabled="isReadOnly"
></a-input>
</a-form-item>
<a-form-item class="hidden-item" :rules="[{ required: true, message: t('mockManagement.nameNotNull') }]">
@ -74,7 +78,6 @@
unique-value
retain-input-value
:max-tag-count="5"
:disabled="isReadOnly"
/>
</a-form-item>
</a-form>
@ -91,9 +94,11 @@
activeTab === RequestComposition.QUERY ||
activeTab === RequestComposition.REST
"
:id="mockDetail.id"
v-model:matchAll="currentMatchAll"
v-model:matchRules="currentMatchRules"
:key-options="currentKeyOptions"
:disabled="isReadOnly"
/>
<template v-else>
<div class="mb-[8px] flex items-center justify-between">
@ -101,6 +106,7 @@
v-model:model-value="mockDetail.mockMatchRule.body.paramType"
type="button"
size="small"
:disabled="isReadOnly"
@change="handleMockBodyTypeChange"
>
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">
@ -118,9 +124,11 @@
v-else-if="
[RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(mockDetail.mockMatchRule.body.paramType)
"
:id="mockDetail.id"
v-model:matchAll="mockDetail.mockMatchRule.body.formDataMatch.matchAll"
v-model:matchRules="mockDetail.mockMatchRule.body.formDataMatch.matchRules"
:key-options="currentBodyKeyOptions"
:disabled="isReadOnly"
/>
<div v-else-if="mockDetail.mockMatchRule.body.paramType === RequestBodyFormat.BINARY">
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
@ -132,6 +140,10 @@
id: 'fileId',
name: 'fileName',
}"
:file-save-as-source-id="mockDetail.id"
:file-save-as-api="transferMockFile"
:file-module-options-api="getMockTransferOptions"
:disabled="isReadOnly"
@change="handleFileChange"
/>
</div>
@ -160,6 +172,7 @@
:show-theme-change="false"
:show-code-format="true"
:language="currentCodeLanguage"
:read-only="isReadOnly"
>
</MsCodeEditor>
</div>
@ -167,13 +180,14 @@
<mockResponse
v-model:mock-response="mockDetail.response"
:definition-responses="props.definitionDetail.responseDefinition || []"
:disabled="isReadOnly"
/>
</a-spin>
</MsDrawer>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
@ -190,26 +204,40 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import {
addMock,
getMockDetail,
getMockTransferOptions,
transferMockFile,
updateMock,
uploadMockTempFile,
} from '@/api/modules/api-test/management';
import { requestBodyTypeMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { MockParams } from '@/models/apiTest/mock';
import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum';
import {
defaultHeaderParamsItem,
defaultMatchRuleItem,
defaultRequestParamsItem,
mockDefaultParams,
makeDefaultParams,
} from '@/views/api-test/components/config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
const props = defineProps<{
definitionDetail: RequestParam;
detailId?: string;
isCopy?: boolean;
}>();
const emit = defineEmits<{
(e: 'delete'): void;
(e: 'addDone'): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
@ -218,8 +246,17 @@
const loading = ref(false);
const isEdit = ref(false);
const mockDetail = ref<MockParams>(cloneDeep(mockDefaultParams));
const isReadOnly = computed(() => !!mockDetail.value.id && !isEdit.value);
const mockDetail = ref<MockParams>(makeDefaultParams());
const isReadOnly = computed(() => !mockDetail.value.isNew && !isEdit.value);
const title = computed(() => {
if (isReadOnly.value) {
return t('mockManagement.mockDetail');
}
if (isEdit.value) {
return t('mockManagement.updateMock');
}
return t('mockManagement.createMock');
});
const activeTab = ref<RequestComposition>(RequestComposition.BODY);
const mockTabList = [
{
@ -329,17 +366,17 @@
const currentKeyOptions = computed(() => {
switch (activeTab.value) {
case RequestComposition.HEADER:
return props.definitionDetail.headers.filter((e) => ({
return filterKeyValParams(props.definitionDetail.headers, defaultMatchRuleItem).validParams.filter((e) => ({
label: e.key,
value: e.value,
}));
case RequestComposition.QUERY:
return props.definitionDetail.query.filter((e) => ({
return filterKeyValParams(props.definitionDetail.query, defaultMatchRuleItem).validParams.filter((e) => ({
label: e.key,
value: e.value,
}));
case RequestComposition.REST:
return props.definitionDetail.rest.filter((e) => ({
return filterKeyValParams(props.definitionDetail.rest, defaultMatchRuleItem).validParams.filter((e) => ({
label: e.key,
value: e.value,
}));
@ -350,12 +387,19 @@
const currentBodyKeyOptions = computed(() => {
switch (mockDetail.value.mockMatchRule.body.paramType) {
case RequestBodyFormat.FORM_DATA:
return props.definitionDetail.body.formDataBody.formValues.filter((e) => ({
return filterKeyValParams(
props.definitionDetail.body.formDataBody.formValues,
defaultMatchRuleItem
).validParams.map((e) => ({
label: e.key,
value: e.value,
paramType: e.paramType,
}));
case RequestBodyFormat.WWW_FORM:
return props.definitionDetail.body.wwwFormBody.formValues.filter((e) => ({
return filterKeyValParams(
props.definitionDetail.body.wwwFormBody.formValues,
defaultMatchRuleItem
).validParams.filter((e) => ({
label: e.key,
value: e.value,
}));
@ -374,6 +418,61 @@
return LanguageEnum.PLAINTEXT;
});
async function initMockDetail() {
try {
loading.value = true;
const res = await getMockDetail({
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;
mockDetail.value = {
...res,
id: props.isCopy ? '' : res.id,
isNew: props.isCopy,
mockMatchRule: {
...res.matching,
body: {
...res.matching.body,
formDataMatch: {
...res.matching.body.formDataMatch,
matchRules: formDataMatch,
},
},
},
...parseFileResult,
};
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watch(
() => visible.value,
(val) => {
if (val && props.detailId) {
initMockDetail();
}
},
{
immediate: true,
}
);
const fileList = ref<MsFileItem[]>([]);
async function handleFileChange(files: MsFileItem[], file?: MsFileItem) {
@ -381,7 +480,7 @@
if (file?.local && file.file) {
//
loading.value = true;
const res = await Promise.resolve({ data: 'fileId' });
const res = await uploadMockTempFile(file.file);
mockDetail.value.mockMatchRule.body.binaryBody.file = {
...file,
fileId: res.data,
@ -426,8 +525,90 @@
}
);
function handleCancel() {
mockDetail.value = makeDefaultParams();
isEdit.value = false;
visible.value = false;
}
function handleDelete() {
emit('delete');
handleCancel();
}
async function handleSave() {
try {
loading.value = true;
const { body } = mockDetail.value.mockMatchRule;
const validBodyMatchRules = filterKeyValParams(body.formDataMatch.matchRules, defaultMatchRuleItem).validParams;
const validHeaderMatchRules = filterKeyValParams(
mockDetail.value.mockMatchRule.header.matchRules,
defaultMatchRuleItem
).validParams;
const validQueryMatchRules = filterKeyValParams(
mockDetail.value.mockMatchRule.query.matchRules,
defaultMatchRuleItem
).validParams;
const validRestMatchRules = filterKeyValParams(
mockDetail.value.mockMatchRule.rest.matchRules,
defaultMatchRuleItem
).validParams;
const validResponseHeaders = filterKeyValParams(
mockDetail.value.response.headers,
defaultHeaderParamsItem
).validParams;
const parseFileResult = parseRequestBodyFiles(mockDetail.value.mockMatchRule.body);
const params = {
...mockDetail.value,
statusCode: mockDetail.value.response.statusCode,
mockMatchRule: {
...mockDetail.value.mockMatchRule,
body: {
...mockDetail.value.mockMatchRule.body,
formDataMatch: {
...mockDetail.value.mockMatchRule.body.formDataMatch,
matchRules: validBodyMatchRules,
},
},
header: {
...mockDetail.value.mockMatchRule.header,
matchRules: validHeaderMatchRules,
},
query: {
...mockDetail.value.mockMatchRule.query,
matchRules: validQueryMatchRules,
},
rest: {
...mockDetail.value.mockMatchRule.rest,
matchRules: validRestMatchRules,
},
},
response: {
...mockDetail.value.response,
headers: validResponseHeaders,
},
...parseFileResult,
apiDefinitionId: props.definitionDetail.id,
projectId: appStore.currentProjectId,
};
if (isEdit.value) {
await updateMock({
id: mockDetail.value.id || '',
...params,
});
Message.success(t('common.updateSuccess'));
} else {
await addMock(params);
Message.success(t('common.createSuccess'));
}
emit('addDone');
handleCancel();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
</script>

View File

@ -1,56 +1,84 @@
<template>
<a-form ref="formRef" :model="formModel" layout="vertical">
<div
:class="`flex ${
matchRules.length > 1 ? 'items-stretch' : 'items-center'
} gap-[16px] overflow-hidden bg-[var(--color-text-n9)] p-[12px]`"
>
<div class="flex h-auto flex-col items-center">
<a-divider v-show="matchRules.length > 1" direction="vertical" class="h-full" />
<a-select v-model:model-value="matchAll" size="small" class="w-[75px]">
<a-option :value="true">AND</a-option>
<a-option :value="false">OR</a-option>
</a-select>
<a-divider v-show="matchRules.length > 1" direction="vertical" class="h-full" />
</div>
<div class="flex max-h-[300px] flex-1 flex-col gap-[8px]">
<div v-for="(item, idx) in matchRules" :key="`filter_item_${idx}`" class="flex items-start gap-[8px]">
<div class="w-[220px]">
<a-form-item
:field="`list[${idx}].key`"
hide-asterisk
class="hidden-item"
:rules="[{ required: true, message: t('mockManagement.paramNameNotNull') }]"
>
<a-select
v-model="item.key"
:placeholder="t('apiTestDebug.paramName')"
:options="props.keyOptions"
allow-search
@change="() => addMatchRule(idx)"
<a-spin :loading="loading" class="block">
<div
:class="`flex ${
matchRules.length > 1 ? 'items-stretch' : 'items-center'
} gap-[16px] bg-[var(--color-text-n9)] p-[12px]`"
>
<div class="flex h-auto flex-col items-center">
<a-divider v-show="matchRules.length > 1" direction="vertical" class="h-full" />
<a-select v-model:model-value="matchAll" size="small" :disabled="props.disabled" class="w-[75px]">
<a-option :value="true">AND</a-option>
<a-option :value="false">OR</a-option>
</a-select>
<a-divider v-show="matchRules.length > 1" direction="vertical" class="h-full" />
</div>
<div class="flex flex-1 flex-col gap-[8px]">
<div v-for="(item, idx) in matchRules" :key="`filter_item_${idx}`" class="flex items-start gap-[8px]">
<div class="w-[220px]">
<a-form-item
:field="`matchRules[${idx}].key`"
hide-asterisk
class="hidden-item"
:rules="[{ required: true, message: t('mockManagement.paramNameNotNull') }]"
:disabled="props.disabled"
>
</a-select>
</a-form-item>
</div>
<div class="w-[100px]">
<a-form-item :field="`list[${idx}].condition`" hide-asterisk class="hidden-item">
<a-select v-model="item.condition" :options="props.keyOptions" @change="() => addMatchRule(idx)">
</a-select>
</a-form-item>
</div>
<div class="flex-1">
<a-form-item :field="`list[${idx}].value`" class="hidden-item">
<MsParamsInput
v-model:value="item.value"
set-default-class
@change="() => addMatchRule(idx)"
@dblclick="quickInputParams(item)"
@apply="() => addMatchRule(idx)"
/>
</a-form-item>
</div>
<!-- <div class="grow-0">
<a-form-item :field="`list[${idx}].description`" class="hidden-item">
<a-select
v-model="item.key"
:placeholder="t('apiTestDebug.paramName')"
:options="props.keyOptions"
allow-search
@change="(val) => selectedKey(item, idx)"
>
</a-select>
</a-form-item>
</div>
<div class="w-[100px]">
<a-form-item
:field="`matchRules[${idx}].condition`"
hide-asterisk
class="hidden-item"
:disabled="props.disabled"
>
<a-select
v-model="item.condition"
:options="getMatchRuleOptions(item.paramType)"
@change="() => addMatchRule(idx)"
>
</a-select>
</a-form-item>
</div>
<div class="flex-1">
<a-form-item :field="`matchRules[${idx}].value`" class="hidden-item" :disabled="props.disabled">
<MsAddAttachment
v-if="item.paramType === RequestParamsType.FILE"
v-model:file-list="item.files"
mode="input"
:fields="{
id: 'fileId',
name: 'fileName',
}"
input-class="h-[32px]"
:file-save-as-source-id="props.id"
:file-save-as-api="transferMockFile"
:file-module-options-api="getMockTransferOptions"
:disabled="props.disabled"
@change="(files, file) => handleFileChange(files, item, idx, file)"
/>
<MsParamsInput
v-else
v-model:value="item.value"
set-default-class
:disabled="props.disabled"
@change="() => addMatchRule(idx)"
@dblclick="quickInputParams(item)"
@apply="() => addMatchRule(idx)"
/>
</a-form-item>
</div>
<!-- <div class="grow-0">
<a-form-item :field="`matchRules[${idx}].description`" class="hidden-item">
<paramDescInput
v-model:desc="item.description"
@input="() => addMatchRule(idx)"
@ -59,16 +87,17 @@
/>
</a-form-item>
</div> -->
<div
v-if="matchRules.length > 1"
class="mt-[8px] flex h-full cursor-pointer items-start justify-center text-[var(--color-text-4)]"
@click="handleDeleteItem(idx)"
>
<icon-minus-circle />
<div
v-if="matchRules.length > 1 && !props.disabled"
class="mt-[8px] flex h-full cursor-pointer items-start justify-center text-[var(--color-text-4)]"
@click="handleDeleteItem(idx)"
>
<icon-minus-circle />
</div>
</div>
</div>
</div>
</div>
</a-spin>
</a-form>
<a-modal
v-model:visible="showQuickInputParam"
@ -121,22 +150,33 @@
<script setup lang="ts">
import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
import MsParamsInput from '@/components/business/ms-params-input/index.vue';
import { getMockTransferOptions, transferMockFile, uploadMockTempFile } from '@/api/modules/api-test/management';
// import paramDescInput from '@/views/api-test/components/paramDescInput.vue';
import { useI18n } from '@/hooks/useI18n';
import { MatchRuleItem } from '@/models/apiTest/mock';
import { RequestParamsType } from '@/enums/apiEnum';
import { defaultMatchRuleItem, matchRuleOptions, mockFileMatchRules } from '@/views/api-test/components/config';
const props = defineProps<{
id?: string;
keyOptions: SelectOptionData[];
disabled: boolean;
}>();
const emit = defineEmits<{
(
e: 'change',
form: {
matchAll: boolean;
matchRules: Record<string, any>[];
matchRules: MatchRuleItem[];
},
isInit?: boolean
): void;
@ -147,10 +187,11 @@
const matchAll = defineModel<boolean>('matchAll', {
required: true,
});
const matchRules = defineModel<Record<string, any>[]>('matchRules', {
const matchRules = defineModel<MatchRuleItem[]>('matchRules', {
required: true,
});
const loading = ref(false);
const formRef = ref<FormInstance>();
const formModel = ref({
matchAll: matchAll.value,
@ -164,17 +205,88 @@
function addMatchRule(rowIndex: number) {
if (rowIndex === matchRules.value.length - 1) {
matchRules.value.push({
key: '',
value: '',
description: '',
id: `${Date.now() + rowIndex}`,
...cloneDeep(defaultMatchRuleItem),
});
}
}
/**
* 选择参数名称
* @param ruleItem 当前规则项
* @param rowIndex 当前行索引
*/
function selectedKey(ruleItem: MatchRuleItem, rowIndex: number) {
const item = formModel.value.matchRules[rowIndex];
if (item) {
const newParamType =
props.keyOptions.find((e) => e.value === ruleItem.key)?.paramType || defaultMatchRuleItem.paramType;
item.paramType = newParamType;
if (newParamType === RequestParamsType.FILE && !mockFileMatchRules.includes(item.condition)) {
//
item.condition = 'EQUALS';
}
addMatchRule(rowIndex);
}
}
/**
* 获取对应参数类型的匹配规则选项
* @param paramType 参数类型
*/
function getMatchRuleOptions(paramType: RequestParamsType) {
if (paramType === RequestParamsType.FILE) {
return matchRuleOptions
.filter((e) => mockFileMatchRules.includes(e.value))
.map((e) => ({ ...e, label: t(e.label) }));
}
return matchRuleOptions.map((e) => ({ ...e, label: t(e.label) }));
}
function emitChange(from: string, isInit?: boolean) {
emit('change', formModel.value, isInit);
}
async function handleFileChange(
files: MsFileItem[],
record: Record<string, any>,
rowIndex: number,
file?: MsFileItem
) {
try {
if (file?.local && file.file) {
//
loading.value = true;
const res = await uploadMockTempFile(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 || '',
fileName: e.name || e.fileName || '',
}));
}
addMatchRule(rowIndex);
emitChange('handleFileChange');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');

View File

@ -2,7 +2,7 @@
<a-spin :loading="loading" class="block">
<div class="mt-[16px] font-medium">{{ t('apiTestManagement.responseContent') }}</div>
<div class="mt-[8px] flex items-center gap-[4px]">
<a-switch v-model:model-value="mockResponse.useApiResponse" size="small"></a-switch>
<a-switch v-model:model-value="mockResponse.useApiResponse" size="small" :disabled="props.disabled"></a-switch>
{{ t('mockManagement.followDefinition') }}
</div>
<template v-if="!mockResponse.useApiResponse">
@ -19,6 +19,7 @@
v-model:model-value="mockResponse.body.bodyType"
type="button"
size="small"
:disabled="props.disabled"
@change="(val) => emit('change')"
>
<a-radio
@ -67,6 +68,7 @@
:show-language-change="false"
:show-charset-change="false"
show-code-format
:read-only="props.disabled"
>
</MsCodeEditor>
</div>
@ -76,6 +78,7 @@
v-model:model-value="mockResponse.body.binaryBody.description"
:placeholder="t('common.desc')"
:max-length="255"
:disabled="props.disabled"
/>
<MsAddAttachment
v-model:file-list="fileList"
@ -85,6 +88,7 @@
id: 'fileId',
name: 'fileName',
}"
:disabled="props.disabled"
@change="handleFileChange"
/>
</div>
@ -94,6 +98,7 @@
class="mr-[8px]"
size="small"
type="line"
:disabled="props.disabled"
></a-switch>
<span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right">
@ -115,6 +120,8 @@
:columns="columns"
:default-param-item="defaultKeyValueParamItem"
:selectable="false"
:disabled-param-value="props.disabled"
:disabled-except-param="props.disabled"
@change="handleResponseTableChange"
/>
<a-select
@ -122,6 +129,7 @@
v-model:model-value="mockResponse.statusCode"
:options="statusCodeOptions"
class="w-[200px]"
:disabled="props.disabled"
@change="() => emit('change')"
/>
</div>
@ -131,6 +139,7 @@
v-model:model-value="mockResponse.apiResponseId"
:options="mockResponseOptions"
class="w-[150px]"
:disabled="props.disabled"
></a-select>
</div>
</a-spin>
@ -156,6 +165,7 @@
const props = defineProps<{
definitionResponses: ResponseItem[];
uploadTempFileApi?: (...args: any) => Promise<any>; //
disabled: boolean;
}>();
const emit = defineEmits<{
(e: 'change'): void;

View File

@ -36,9 +36,9 @@
@selected-change="handleTableSelect"
@batch-action="handleTableBatch"
>
<template #num="{ record }">
<MsButton type="text" @click="openMockDetailDrawer(record)">
{{ record.num }}
<template #expectNum="{ record }">
<MsButton type="text" @click="handleOpenDetail(record)">
{{ record.expectNum }}
</MsButton>
</template>
<template #enable="{ record }">
@ -50,10 +50,14 @@
></a-switch>
</template>
<template #action="{ record }">
<MsButton type="text" @click="debugMock(record)">
<MsButton type="text" class="!mr-0" @click="debugMock(record)">
{{ t('apiTestManagement.debug') }}
</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton type="text" class="!mr-0" @click="handleCopyMock(record)">
{{ t('common.copy') }}
</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="tableMoreActionList" @select="handleTableMoreActionSelect($event, record)" />
</template>
<template v-if="hasAnyPermission(['PROJECT_API_DEFINITION_MOCK:READ+ADD']) && props.isApi" #empty>
@ -66,10 +70,19 @@
</template>
</ms-base-table>
</div>
<mockDetailDrawer v-model:visible="mockDetailDrawerVisible" :definition-detail="props.definitionDetail" />
<mockDetailDrawer
v-if="mockDetailDrawerVisible"
v-model:visible="mockDetailDrawerVisible"
:definition-detail="mockBelongDefinitionDetail"
:detail-id="activeMockRecord?.id"
:is-copy="isCopy"
@add-done="loadMockList"
@delete="() => removeMock(activeMockRecord)"
/>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
@ -82,8 +95,11 @@
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import {
deleteDefinitionMockMock,
batchDeleteMock,
deleteMock,
getDefinitionDetail,
getDefinitionMockPage,
getMockUrl,
updateMockStatusPage,
} from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
@ -94,6 +110,7 @@
import { ApiDefinitionMockDetail } from '@/models/apiTest/management';
import { OrdTemplateManagement } from '@/models/setting/template';
import { RequestComposition } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
const mockDetailDrawer = defineAsyncComponent(() => import('./mockDetailDrawer.vue'));
@ -105,6 +122,7 @@
offspringIds: string[];
definitionDetail: RequestParam;
readOnly?: boolean; //
protocol: string; //
}>();
const emit = defineEmits<{
(e: 'init', params: any): void;
@ -112,6 +130,7 @@
}>();
const appStore = useAppStore();
const tableStore = useTableStore();
const { t } = useI18n();
const { openModal } = useModal();
@ -120,8 +139,8 @@
let columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'id',
slotName: 'id',
dataIndex: 'expectNum',
slotName: 'expectNum',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
@ -235,11 +254,27 @@
const tableQueryParams = ref<any>();
function loadMockList() {
async function getModuleIds() {
let moduleIds: string[] = [];
if (props.activeModule !== 'all') {
moduleIds = [props.activeModule];
const getAllChildren = await tableStore.getSubShow(TableKeyEnum.API_TEST_MANAGEMENT_CASE);
if (getAllChildren) {
moduleIds = [props.activeModule, ...props.offspringIds];
}
}
return moduleIds;
}
async function loadMockList() {
const selectModules = await getModuleIds();
const params = {
keyword: keyword.value,
projectId: appStore.currentProjectId,
protocol: props.protocol,
apiDefinitionId: props.definitionDetail.id !== 'all' ? props.definitionDetail.id : undefined,
filter: {},
moduleIds: selectModules,
};
setLoadListParams(params);
loadList();
@ -253,19 +288,11 @@
});
}
watch(
() => props.activeModule,
() => {
watchEffect(() => {
if (props.activeModule || props.protocol) {
loadMockList();
}
);
watch(
() => props.definitionDetail.protocol,
() => {
loadMockList();
}
);
});
const changeDefault = async (value: any, record: OrdTemplateManagement) => {
try {
@ -278,10 +305,6 @@
}
};
onBeforeMount(() => {
loadMockList();
});
const tableSelected = ref<(string | number)[]>([]);
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
@ -293,7 +316,7 @@
/**
* 删除接口
*/
function deleteMock(record?: ApiDefinitionMockDetail, isBatch?: boolean, params?: BatchActionQueryParams) {
function removeMock(record?: ApiDefinitionMockDetail, isBatch?: boolean, params?: BatchActionQueryParams) {
let title = t('apiTestManagement.deleteApiTipTitle', { name: record?.name });
let selectIds = [record?.id || ''];
if (isBatch) {
@ -315,15 +338,17 @@
onBeforeOk: async () => {
try {
if (isBatch) {
// await batchDeleteMock({
// selectIds,
// selectAll: !!params?.selectAll,
// excludeIds: params?.excludeIds || [],
// condition: { keyword: keyword.value },
// projectId: appStore.currentProjectId,
// });
const selectModules = await getModuleIds();
await batchDeleteMock({
selectIds,
selectAll: !!params?.selectAll,
excludeIds: params?.excludeIds || [],
condition: { keyword: keyword.value },
projectId: appStore.currentProjectId,
moduleIds: selectModules,
});
} else {
await deleteDefinitionMockMock({
await deleteMock({
id: record?.id as string,
projectId: appStore.currentProjectId,
});
@ -340,6 +365,26 @@
});
}
const { copy, isSupported } = useClipboard({ legacy: true });
async function copyMockUrl(record: ApiDefinitionMockDetail) {
try {
appStore.showLoading();
const url = await getMockUrl(record.id);
if (isSupported) {
copy(url);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('common.copyNotSupport'));
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
}
/**
* 处理表格更多按钮事件
* @param item
@ -349,6 +394,9 @@
case 'delete':
deleteMock(record);
break;
case 'copyMock':
copyMockUrl(record);
break;
default:
break;
}
@ -370,7 +418,7 @@
batchParams.value = params;
switch (event.eventTag) {
case 'delete':
deleteMock(undefined, true, params);
removeMock(undefined, true, params);
break;
default:
break;
@ -385,15 +433,50 @@
mockDetailDrawerVisible.value = true;
}
function openMockDetailDrawer(record: ApiDefinitionMockDetail) {
activeMockRecord.value = record;
mockDetailDrawerVisible.value = true;
const mockBelongDefinitionDetail = ref<RequestParam>(props.definitionDetail);
async function openMockDetailDrawer(record: ApiDefinitionMockDetail) {
try {
activeMockRecord.value = record;
if (props.definitionDetail.id === 'all') {
// mock mock
appStore.showLoading();
const res = await getDefinitionDetail(record.apiDefinitionId);
mockBelongDefinitionDetail.value = {
...(res.request as RequestParam),
id: res.id,
type: 'mock',
isNew: false,
protocol: res.protocol,
activeTab: RequestComposition.BODY,
executeLoading: false,
responseDefinition: res.response,
};
} else {
mockBelongDefinitionDetail.value = props.definitionDetail;
}
mockDetailDrawerVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
}
const isCopy = ref(false);
function handleOpenDetail(record: ApiDefinitionMockDetail) {
isCopy.value = false;
openMockDetailDrawer(record);
}
function handleCopyMock(record: ApiDefinitionMockDetail) {
isCopy.value = true;
openMockDetailDrawer(record);
}
const mockDebugDrawerVisible = ref(false);
function debugMock(record: ApiDefinitionMockDetail) {
activeMockRecord.value = record;
mockDebugDrawerVisible.value = true;
}
defineExpose({
@ -401,7 +484,6 @@
});
if (!props.readOnly) {
const tableStore = useTableStore();
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer');
} else {
columns = columns.filter(

View File

@ -210,6 +210,7 @@ export default {
'mockManagement.batchDeleteMockTip': '确认删除已选中的 {count} 个Mock吗',
'mockManagement.allMock': '全部 MOCK',
'mockManagement.createMock': '创建 MOCK',
'mockManagement.updateMock': '更新 MOCK',
'mockManagement.mockDetail': 'MOCK 详情',
'mockManagement.namePlaceholder': '请输入期望名称',
'mockManagement.nameNotNull': '期望名称不能为空',
@ -217,4 +218,15 @@ export default {
'mockManagement.saveAndContinue': '保存并继续创建',
'mockManagement.paramNameNotNull': '参数名称不能为空',
'mockManagement.followDefinition': '跟随 API 定义',
'mockManagement.equals': '等于',
'mockManagement.notEquals': '不等于',
'mockManagement.lengthEquals': '长度等于',
'mockManagement.lengthLarge': '长度大于',
'mockManagement.lengthLess': '长度小于',
'mockManagement.lengthNotEquals': '长度不等于',
'mockManagement.contain': '包含',
'mockManagement.notContain': '不包含',
'mockManagement.empty': '为空',
'mockManagement.notEmpty': '非空',
'mockManagement.regular': '正则匹配',
};

View File

@ -25,6 +25,7 @@
checkable
block-node
draggable
hide-switcher
@select="(selectedKeys, node) => handleStepSelect(selectedKeys, node as ScenarioStepItem)"
@expand="handleStepExpand"
@more-actions-close="() => setFocusNodeKey('')"

View File

@ -455,7 +455,7 @@
{
scroll: { x: '100%' },
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
heightUsed: 372,
heightUsed: 375,
showSetting: true,
selectable: true,
showSelectAll: true,
@ -503,9 +503,6 @@
keyword: keyword.value,
viewFlag: props.onlyMine,
filter: { status: statusFilters.value, caseLevel: caseFilters.value },
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
combine: filter
? {
...filter.combine,
@ -516,6 +513,9 @@
loadList();
emit('init', {
...tableParams.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
total: propsRes.value.msPagination?.total,
moduleIds: [],
});
}

View File

@ -111,11 +111,17 @@
<statusTag :status="record.status" size="small" />
</template>
<template #reviewPassRule="{ record }">
{{
record.reviewPassRule === 'SINGLE'
? t('caseManagement.caseReview.single')
: t('caseManagement.caseReview.multi')
}}
<a-tag
:color="record.reviewPassRule === 'SINGLE' ? 'rgb(var(--success-2))' : 'rgb(var(--link-2))'"
:class="record.reviewPassRule === 'SINGLE' ? '!text-[rgb(var(--success-6))]' : '!text-[rgb(var(--link-6))]'"
size="small"
>
{{
record.reviewPassRule === 'SINGLE'
? t('caseManagement.caseReview.single')
: t('caseManagement.caseReview.multi')
}}
</a-tag>
</template>
<template #reviewers="{ record }">
<a-tooltip :content="record.reviewers.join('、')">
@ -435,6 +441,7 @@
{
title: 'caseManagement.caseReview.caseCount',
dataIndex: 'caseCount',
showDrag: true,
width: 90,
},
{
@ -442,18 +449,21 @@
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
showDrag: true,
width: 150,
},
{
title: 'caseManagement.caseReview.passRate',
slotName: 'passRate',
titleSlotName: 'passRateColumn',
showDrag: true,
width: 200,
},
{
title: 'caseManagement.caseReview.type',
slotName: 'reviewPassRule',
dataIndex: 'reviewPassRule',
showDrag: true,
width: 90,
},
{
@ -461,35 +471,41 @@
slotName: 'reviewers',
dataIndex: 'reviewers',
titleSlotName: 'reviewersFilter',
showDrag: true,
width: 150,
},
{
title: 'caseManagement.caseReview.creator',
dataIndex: 'createUserName',
showTooltip: true,
showDrag: true,
width: 120,
},
{
title: 'caseManagement.caseReview.module',
dataIndex: 'moduleName',
slotName: 'moduleName',
showDrag: true,
width: 120,
},
{
title: 'caseManagement.caseReview.tag',
dataIndex: 'tags',
isTag: true,
showDrag: true,
width: 170,
},
{
title: 'caseManagement.caseReview.desc',
dataIndex: 'description',
width: 150,
showDrag: true,
showTooltip: true,
},
{
title: 'caseManagement.caseReview.cycle',
dataIndex: 'cycle',
showDrag: true,
width: 350,
},
{

View File

@ -92,15 +92,15 @@
<passRateLine :review-detail="reviewDetail" height="8px" radius="var(--border-radius-mini)" />
</div>
</template>
<div class="px-[24px]">
<!-- <div class="px-[24px]">
<a-divider class="my-0" />
<a-tabs v-model:active-key="showTab" class="no-content">
<a-tab-pane v-for="item of tabList" :key="item.key" :title="item.title" />
</a-tabs>
</div>
</div> -->
</MsCard>
<!-- special-height的170: 上面卡片高度154 + mt的16 -->
<MsCard class="mt-[16px]" :special-height="170" simple has-breadcrumb no-content-padding>
<!-- special-height的170: 上面卡片高度105 + mt的16 -->
<MsCard class="mt-[16px]" :special-height="121" simple has-breadcrumb no-content-padding>
<MsSplitBox>
<template #first>
<div class="p-[16px]">
@ -203,13 +203,13 @@
const onlyMine = ref(false);
const showTab = ref(0);
const tabList = ref([
{
key: 0,
title: t('menu.caseManagement.featureCase'),
},
]);
// const showTab = ref(0);
// const tabList = ref([
// {
// key: 0,
// title: t('menu.caseManagement.featureCase'),
// },
// ]);
const modulesCount = ref<Record<string, any>>({});

View File

@ -91,18 +91,6 @@
></a-input>
<MsFormItemSub :text="t('system.config.baseInfo.pageUrlSub', { url: defaultUrl })" @fill="fillDefaultUrl" />
</a-form-item>
<a-form-item v-xpack :label="t('system.config.prometheus')" field="prometheusHost" asterisk-position="end">
<a-input
v-model:model-value="baseInfoForm.prometheusHost"
:max-length="255"
:placeholder="t('system.config.baseInfo.prometheusPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub
:text="t('system.config.baseInfo.prometheusSub', { prometheus: defaultPrometheus })"
@fill="fillDefaultPrometheus"
/>
</a-form-item>
</a-form>
</MsDrawer>
<MsDrawer
@ -239,22 +227,16 @@
const baseFormRef = ref<FormInstance>();
const baseInfo = ref({
url: 'http://127.0.0.1:8081',
prometheusHost: 'http://prometheus:9090',
});
const baseInfoForm = ref({ ...baseInfo.value });
const baseInfoDesc = ref<Description[]>([]);
//
const defaultUrl = 'https://metersphere.com';
const defaultPrometheus = 'http://prometheus:9090';
function fillDefaultUrl() {
baseInfoForm.value.url = defaultUrl;
}
function fillDefaultPrometheus() {
baseInfoForm.value.prometheusHost = defaultPrometheus;
}
/**
* 初始化基础信息
*/
@ -272,10 +254,6 @@
label: t('system.config.pageUrl'),
value: res.url,
},
{
label: t('system.config.prometheus'),
value: res.prometheusHost,
},
];
} else {
baseInfoDesc.value = [
@ -297,11 +275,8 @@
* 拼接基础信息参数
*/
function makeBaseInfoParams() {
const { url, prometheusHost } = baseInfoForm.value;
return [
{ paramKey: 'base.url', paramValue: url, type: 'text' },
{ paramKey: 'base.prometheus.host', paramValue: prometheusHost, type: 'text' },
];
const { url } = baseInfoForm.value;
return [{ paramKey: 'base.url', paramValue: url, type: 'text' }];
}
/**

View File

@ -78,22 +78,6 @@
<a-option v-for="org of orgOptions" :key="org.id" :value="org.id">{{ org.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item
:label="t('system.resourcePool.use')"
field="use"
class="form-item"
:rules="[{ required: true, message: t('system.resourcePool.useRequired') }]"
asterisk-position="end"
>
<a-checkbox-group v-model:model-value="form.use" @change="() => setIsSave(false)">
<a-checkbox v-for="use of useList" :key="use.value" :value="use.value">{{ t(use.label) }}</a-checkbox>
</a-checkbox-group>
<MsFormItemSub
v-if="form.use.length === 3"
:text="t('system.resourcePool.allUseTip')"
:show-fill-icon="false"
/>
</a-form-item>
<!--TODO:暂无性能测试-->
<!-- <template v-if="isCheckedPerformance">
<a-form-item :label="t('system.resourcePool.mirror')" field="testResourceDTO.loadTestImage" class="form-item">
@ -583,13 +567,6 @@
rules: [{ required: true, message: t('system.resourcePool.portRequired') }],
placeholder: 'system.resourcePool.portPlaceholder',
},
{
filed: 'monitor',
type: 'input',
label: 'system.resourcePool.monitor',
rules: [{ required: true, message: t('system.resourcePool.monitorRequired') }],
placeholder: 'system.resourcePool.monitorPlaceholder',
},
{
filed: 'concurrentNumber',
type: 'inputNumber',
@ -628,8 +605,8 @@
// ipportmonitorconcurrentNumber
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`;
node.concurrentNumber === undefined ? '' : node.concurrentNumber
}\r`;
}
}
editorContent.value = res;
@ -655,12 +632,11 @@
if (e.trim() !== '') {
//
const line = e.split(',');
if (line.every((s) => s.trim() !== '') && !Number.isNaN(Number(line[3]))) {
if (line.every((s) => s.trim() !== '') && !Number.isNaN(Number(line[2]))) {
const item = {
ip: line[0],
port: line[1],
monitor: line[2],
concurrentNumber: Number(line[3]),
concurrentNumber: Number(line[2]),
};
if (i === 0) {
// concurrentNumber

View File

@ -318,7 +318,7 @@
? [
{
label: t('system.resourcePool.detailResources'),
value: nodesList?.map((e) => `${e.ip},${e.port},${e.monitor},${e.concurrentNumber}`),
value: nodesList?.map((e) => `${e.ip},${e.port},${e.concurrentNumber}`),
tagTheme: 'light' as Theme,
tagType: 'default' as TagType,
tagMaxWidth: '280px',