feat(接口测试): mock 页面&部分 bug 修复

This commit is contained in:
baiqi 2024-05-07 19:36:29 +08:00 committed by Craftsman
parent f7cdcea873
commit bb6556ae4d
35 changed files with 1328 additions and 203 deletions

View File

@ -134,12 +134,14 @@
</MsTag>
</a-tooltip>
<div v-if="file.local === true" class="flex items-center">
<a-tooltip :content="t('ms.add.attachment.saveAs')">
<MsButton type="text" status="secondary" class="!mr-0" @click="handleOpenSaveAs(file)">
<MsIcon type="icon-icon_unloading" class="hover:text-[rgb(var(--primary-5))]" size="16" />
</MsButton>
</a-tooltip>
<a-divider direction="vertical" :margin="4"></a-divider>
<template v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])">
<a-tooltip :content="t('ms.add.attachment.saveAs')">
<MsButton type="text" status="secondary" class="!mr-0" @click="handleOpenSaveAs(file)">
<MsIcon type="icon-icon_unloading" class="hover:text-[rgb(var(--primary-5))]" size="16" />
</MsButton>
</a-tooltip>
<a-divider direction="vertical" :margin="4"></a-divider>
</template>
<a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" @click="handleClose(file)">
<MsIcon
@ -203,6 +205,7 @@
import { getAssociatedFileListUrl } from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
import { AssociatedList } from '@/models/caseManagement/featureCase';
import { TableQueryParams, TransferFileParams } from '@/models/common';

View File

@ -168,7 +168,6 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import caseLevel from './caseLevel.vue';
@ -184,7 +183,6 @@
import { CaseLinkEnum } from '@/enums/caseEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
import type { CaseLevel } from './types';
import { initGetModuleCountFunc, type RequestModuleEnum } from './utils';
const router = useRouter();

View File

@ -213,7 +213,9 @@
:disabled="props.disabled"
:data="autoCompleteParams"
:placeholder="t('ms.paramsInput.placeholder', { at: '@' })"
:class="`ms-params-input ${paramSettingVisible ? 'ms-params-input--focus' : ''}`"
:class="`${props.setDefaultClass ? 'ms-params-input--default' : 'ms-params-input'} ${
paramSettingVisible ? 'ms-params-input--focus' : ''
}`"
:trigger-props="{ contentClass: 'ms-params-input-trigger' }"
:filter-option="false"
:size="props.size"
@ -271,6 +273,7 @@
value: string;
disabled?: boolean;
size?: 'small' | 'large' | 'medium' | 'mini';
setDefaultClass?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:value', val: string): void;
@ -707,7 +710,8 @@
border-color: transparent;
}
}
.ms-params-input {
.ms-params-input,
.ms-params-input--default {
.ms-params-input-suffix-icon,
.ms-params-input-suffix-icon--disabled {
@apply invisible;

View File

@ -13,7 +13,7 @@
<slot name="titleRight"></slot>
</div>
</div>
<div class="ms-detail-card-desc">
<div v-if="showingDescription.length > 0" class="ms-detail-card-desc">
<div
v-for="item of showingDescription"
:key="item.key"

View File

@ -58,26 +58,22 @@
>
<icon-drag-dot-vertical class="absolute left-[-3px] top-[50%] w-[14px]" size="14" />
</div>
<a-scrollbar class="ms-drawer-body-scrollbar">
<div class="ms-drawer-body">
<slot>
<MsDescription
v-if="props.descriptions && props.descriptions.length > 0"
:descriptions="props.descriptions"
:show-skeleton="props.showSkeleton"
:skeleton-line="10"
>
<template #value="{ item }">
<slot name="descValue" :item="item">
{{
item.value === undefined || item.value === null || item.value?.toString() === '' ? '-' : item.value
}}
</slot>
</template>
</MsDescription>
</slot>
</div>
</a-scrollbar>
<div class="ms-drawer-body">
<slot>
<MsDescription
v-if="props.descriptions && props.descriptions.length > 0"
:descriptions="props.descriptions"
:show-skeleton="props.showSkeleton"
:skeleton-line="10"
>
<template #value="{ item }">
<slot name="descValue" :item="item">
{{ item.value === undefined || item.value === null || item.value?.toString() === '' ? '-' : item.value }}
</slot>
</template>
</MsDescription>
</slot>
</div>
<template #footer>
<slot name="footer">
<div class="flex items-center justify-between">
@ -310,15 +306,13 @@
.arco-drawer-body {
@apply overflow-hidden;
}
.ms-drawer-body-scrollbar {
.ms-drawer-body {
@apply h-full w-full overflow-auto;
.ms-scroll-bar();
min-width: 650px;
min-height: 500px;
}
.ms-drawer-body {
@apply h-full;
}
.arco-scrollbar-track-direction-vertical {
right: -12px;
}

View File

@ -1,7 +1,7 @@
<template>
<a-tabs v-model:active-key="innerActiveKey" :class="[props.class, props.noContent ? 'no-content' : '']">
<a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="item.label">
<template #title>
<template v-if="props.showBadge" #title>
<a-badge
v-if="props.getTextFunc(item.value) !== ''"
:class="item.value === innerActiveKey ? 'active-badge' : ''"
@ -28,8 +28,10 @@
class?: string;
getTextFunc?: (value: any) => string;
noContent?: boolean;
showBadge?: boolean;
}>(),
{
showBadge: true,
getTextFunc: (value: any) => value,
class: '',
}

View File

@ -268,7 +268,7 @@
</style>
<style>
.column-drawer .ms-drawer-body-scrollbar {
.column-drawer .ms-drawer-body {
min-width: auto !important;
}
</style>

View File

@ -199,7 +199,7 @@
watch(
() => appStore.getCurrentTopMenu?.name,
(val) => {
() => {
checkMessageRead();
},
{

View File

@ -22,14 +22,13 @@
<div class="divider h-full">
<Suspense>
<TaskCenter group="project" mode="modal"></TaskCenter>
<TaskCenter v-if="visible" group="project" mode="modal"></TaskCenter>
</Suspense>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, Suspense } from 'vue';
import { useVModel } from '@vueuse/core';
import TaskCenter from '@/views/project-management/taskCenter/component/taskCom.vue';

View File

@ -1,5 +1,5 @@
import { ConditionType } from '@/models/apiTest/common';
import { RequestBodyFormat, RequestConditionProcessor, ScenarioStepType } from '@/enums/apiEnum';
import { RequestBodyFormat, RequestConditionProcessor } from '@/enums/apiEnum';
// 条件操作类型
export type ConditionTypeNameMap = Record<ConditionType, string>;

View File

@ -1,4 +1,5 @@
import { RouteEnum } from '@/enums/routeEnum';
import { TaskCenterEnum } from '@/enums/taskCenter';
export const MENU_LEVEL = ['SYSTEM', 'ORGANIZATION', 'PROJECT'] as const; // 菜单级别
@ -321,6 +322,73 @@ export const pathMap: PathMapItem[] = [
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
children: [
{
key: 'SETTING_SYSTEM_TASK_CENTER_REAL_TIME', // 系统设置-系统-任务中心-实时任务
locale: 'project.taskCenter.real',
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
children: [
{
key: 'SETTING_SYSTEM_TASK_CENTER_REAL_TIME_API_CASE', // 系统设置-系统-任务中心-实时任务-接口用例
locale: 'project.taskCenter.interfaceCase',
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
routeQuery: {
tab: 'real',
type: TaskCenterEnum.API_CASE,
},
},
{
key: 'SETTING_SYSTEM_TASK_CENTER_REAL_TIME_API_SCENARIO', // 系统设置-系统-任务中心-实时任务-接口场景
locale: 'project.taskCenter.apiScenario',
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
routeQuery: {
tab: 'real',
type: TaskCenterEnum.API_SCENARIO,
},
},
],
},
{
key: 'SETTING_SYSTEM_TASK_CENTER_TIME', // 系统设置-系统-任务中心-定时任务
locale: 'apiTestManagement.timeTask',
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
routeQuery: {
tab: 'timeTask',
},
children: [
{
key: 'SETTING_SYSTEM_TASK_CENTER_TIME_API_SCENARIO', // 系统设置-系统-任务中心-定时任务-接口场景
locale: 'project.taskCenter.apiScenario',
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
routeQuery: {
tab: 'timeTask',
type: TaskCenterEnum.API_SCENARIO,
},
},
{
key: 'SETTING_SYSTEM_TASK_CENTER_TIME_API_IMPORT', // 系统设置-系统-任务中心-定时任务-接口导入
locale: 'project.taskCenter.apiImport',
route: RouteEnum.SETTING_SYSTEM_TASK_CENTER,
permission: [],
level: MENU_LEVEL[0],
routeQuery: {
tab: 'timeTask',
type: TaskCenterEnum.API_IMPORT,
},
},
],
},
],
},
{
key: 'SETTING_SYSTEM_PLUGIN_MANAGEMENT', // 系统设置-系统-插件管理
@ -372,6 +440,73 @@ export const pathMap: PathMapItem[] = [
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
children: [
{
key: 'SETTING_ORGANIZATION_TASK_CENTER_REAL_TIME', // 系统设置-组织-任务中心-实时任务
locale: 'project.taskCenter.real',
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
children: [
{
key: 'SETTING_ORGANIZATION_TASK_CENTER_REAL_TIME_API_CASE', // 系统设置-组织-任务中心-实时任务-接口用例
locale: 'project.taskCenter.interfaceCase',
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
routeQuery: {
tab: 'real',
type: TaskCenterEnum.API_CASE,
},
},
{
key: 'SETTING_ORGANIZATION_TASK_CENTER_REAL_TIME_API_SCENARIO', // 系统设置-组织-任务中心-实时任务-接口场景
locale: 'project.taskCenter.apiScenario',
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
routeQuery: {
tab: 'real',
type: TaskCenterEnum.API_SCENARIO,
},
},
],
},
{
key: 'SETTING_ORGANIZATION_TASK_CENTER_TIME', // 系统设置-组织-任务中心-定时任务
locale: 'apiTestManagement.timeTask',
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
routeQuery: {
tab: 'timeTask',
},
children: [
{
key: 'SETTING_ORGANIZATION_TASK_CENTER_TIME_API_SCENARIO', // 系统设置-组织-任务中心-定时任务-接口场景
locale: 'project.taskCenter.apiScenario',
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
routeQuery: {
tab: 'timeTask',
type: TaskCenterEnum.API_SCENARIO,
},
},
{
key: 'SETTING_ORGANIZATION_TASK_CENTER_TIME_API_IMPORT', // 系统设置-组织-任务中心-定时任务-接口导入
locale: 'project.taskCenter.apiImport',
route: RouteEnum.SETTING_ORGANIZATION_TASK_CENTER,
permission: [],
level: MENU_LEVEL[1],
routeQuery: {
tab: 'timeTask',
type: TaskCenterEnum.API_IMPORT,
},
},
],
},
],
},
{
key: 'SETTING_ORGANIZATION_TEMPLATE', // 系统设置-组织-模板管理
@ -735,11 +870,76 @@ export const pathMap: PathMapItem[] = [
key: 'PROJECT_MANAGEMENT_TASK_CENTER', // 项目管理-任务中心
locale: 'menu.projectManagement.taskCenter',
route: '',
routeQuery: {
task: 'projectTask',
},
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'PROJECT_MANAGEMENT_TASK_CENTER_REAL_TIME', // 项目管理-任务中心-实时任务
locale: 'project.taskCenter.realTimeTask',
route: '',
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'PROJECT_MANAGEMENT_TASK_CENTER_REAL_TIME_API_CASE', // 项目管理-任务中心-实时任务-接口用例
locale: 'project.taskCenter.interfaceCase',
route: '',
permission: [],
level: MENU_LEVEL[2],
routeQuery: {
task: true,
tab: 'real',
type: TaskCenterEnum.API_CASE,
},
},
{
key: 'PROJECT_MANAGEMENT_TASK_CENTER_REAL_TIME_API_SCENARIO', // 项目管理-任务中心-实时任务-接口场景
locale: 'project.taskCenter.apiScenario',
route: '',
permission: [],
level: MENU_LEVEL[2],
routeQuery: {
task: true,
tab: 'real',
type: TaskCenterEnum.API_SCENARIO,
},
},
],
},
{
key: 'PROJECT_MANAGEMENT_TASK_CENTER_TIME', // 项目管理-任务中心-定时任务
locale: 'project.taskCenter.scheduledTask',
route: '',
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'PROJECT_MANAGEMENT_TASK_CENTER_TIME_API_SCENARIO', // 项目管理-任务中心-定时任务-接口场景
locale: 'project.taskCenter.apiScenario',
route: '',
permission: [],
level: MENU_LEVEL[2],
routeQuery: {
task: true,
tab: 'timing',
type: TaskCenterEnum.API_SCENARIO,
},
},
{
key: 'PROJECT_MANAGEMENT_TASK_CENTER_TIME_API_IMPORT', // 项目管理-任务中心-定时任务-接口导入
locale: 'project.taskCenter.apiImport',
route: '',
permission: [],
level: MENU_LEVEL[2],
routeQuery: {
task: true,
tab: 'timing',
type: TaskCenterEnum.API_IMPORT,
},
},
],
},
],
},
],
},

View File

@ -38,20 +38,20 @@ export default function usePathMap() {
*/
const jumpRouteByMapKey = (key: PathMapRoute, routeQuery?: Record<string, any>, openNewPage = false) => {
const pathNode = findNodeByKey<PathMapItem>(pathMap, key as unknown as string);
if (pathNode) {
if (pathNode && (pathNode.route || key.includes('PROJECT_MANAGEMENT_TASK_CENTER'))) {
if (openNewPage) {
window.open(
`${window.location.origin}#${router.resolve({ name: pathNode?.route }).fullPath}?${new URLSearchParams({
`${window.location.origin}#${router.resolve({ name: pathNode.route }).fullPath}?${new URLSearchParams({
...routeQuery,
...pathNode?.routeQuery,
...pathNode.routeQuery,
}).toString()}`
);
} else {
router.push({
name: pathNode?.route,
name: pathNode.route,
query: {
...routeQuery,
...pathNode?.routeQuery,
...pathNode.routeQuery,
},
});
}

View File

@ -0,0 +1,53 @@
import type { RequestBodyFormat } from '@/enums/apiEnum';
import type { ExecuteBinaryBody, KeyValueParam, ResponseDefinitionBody } from './common';
// mock 信息-匹配项
export interface MatchRuleItem {
key: string;
value: string;
condition: string;
description: string;
}
// mock 信息-响应内容
export interface MockResponse {
statusCode: number;
headers: KeyValueParam[];
useApiResponse: boolean;
apiResponseId?: string; // useApiResponse 为 true 时必填
body: ResponseDefinitionBody;
}
// mock 信息-请求通用匹配规则
export interface MockMatchRuleCommon {
matchRules: MatchRuleItem[];
matchAll: boolean;
}
// mock 信息-请求体匹配规则
export interface MockBody {
paramType: RequestBodyFormat;
formDataMatch: MockMatchRuleCommon;
binaryBody: ExecuteBinaryBody;
raw: string;
}
// mock 信息-匹配规则集合
export interface MockMatchRule {
header: MockMatchRuleCommon;
query: MockMatchRuleCommon;
rest: MockMatchRuleCommon;
body: MockBody;
}
// mock 信息
export interface MockParams {
id?: string;
projectId: string;
name: string;
statusCode: number;
tags: string[];
mockMatchRule: MockMatchRule;
response: MockResponse;
apiDefinitionId: string;
uploadFileIds: string[];
linkFileIds: string[];
// 前端扩展字段
unSaved?: boolean;
}

View File

@ -4,6 +4,7 @@ import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
import { EnvConfig, EnvironmentItem } from '@/models/projectManagement/environmental';
import type { LoginConfig, PageConfig, PlatformConfig, ThemeConfig } from '@/models/setting/config';
import { ProjectListItem } from '@/models/setting/project';
import type { TaskCenterEnum } from '@/enums/taskCenter';
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';

View File

@ -23,7 +23,7 @@
const props = withDefaults(
defineProps<{
method?: RequestMethods;
method: RequestMethods | string;
isTag?: boolean;
tagSize?: Size;
tagBackgroundColor?: string;
@ -64,7 +64,7 @@
const methodColor = computed(() => {
if (props.method) {
const colorMap = colorMaps.find((item) => item.includes.includes(props.method!));
const colorMap = colorMaps.find((item) => item.includes.includes(props.method as RequestMethods));
return colorMap?.color || 'rgb(var(--link-7))'; // key
}
return 'rgb(var(--link-7))';

View File

@ -471,7 +471,7 @@
</template>
<script setup lang="ts">
import { useClipboard, useVModel } from '@vueuse/core';
import { useClipboard } from '@vueuse/core';
import { InputInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
@ -506,7 +506,7 @@
XPathExtract,
} from '@/models/apiTest/common';
import { ParamsRequestType } from '@/models/projectManagement/commonScript';
import { DataSourceItem, EnvConfig } from '@/models/projectManagement/environmental';
import { DataSourceItem } from '@/models/projectManagement/environmental';
import {
RequestConditionProcessor,
RequestExtractEnvType,
@ -624,10 +624,6 @@ if (!result){
const scriptDefinedRef = ref<InstanceType<typeof MsScriptDefined>>();
function undoScript() {
scriptDefinedRef.value?.undoHandler();
}
function clearScript() {
condition.value.script = '';
}

View File

@ -34,7 +34,7 @@
</div>
<div v-else class="flex min-w-[42px] items-center justify-between">
<div class="one-line-text">
{{ item.name || t(conditionTypeNameMap[item.processorType as keyof typeof conditionTypeNameMap]) }}</div
{{ t(conditionTypeNameMap[item.processorType as keyof typeof conditionTypeNameMap]) }}</div
>
<a-badge
v-if="item.processorType === RequestConditionProcessor.REQUEST_SCRIPT"

View File

@ -10,6 +10,7 @@ import {
ResponseAssertionItem,
ResponseDefinition,
} from '@/models/apiTest/common';
import type { MockParams } from '@/models/apiTest/mock';
import {
RequestAssertionCondition,
RequestBodyFormat,
@ -236,3 +237,99 @@ export const regexDefaultParamItem = {
responseFormat: ResponseBodyXPathAssertionFormat.XML,
moreSettingPopoverVisible: false,
};
// mock 默认参数
export const mockDefaultParams: MockParams = {
projectId: '',
name: '',
statusCode: 200,
tags: [],
mockMatchRule: {
header: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchAll: true,
},
query: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchAll: true,
},
rest: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchAll: true,
},
body: {
paramType: RequestBodyFormat.FORM_DATA,
formDataMatch: {
matchRules: [
{
key: '',
value: '',
condition: 'EQUALS',
description: '',
},
],
matchAll: true,
},
binaryBody: {
description: '',
file: undefined,
sendAsBody: false,
},
raw: '',
},
},
response: {
statusCode: 200,
headers: [
{
key: '',
value: '',
description: '',
},
],
useApiResponse: false,
apiResponseId: '',
body: {
bodyType: ResponseBodyFormat.JSON,
jsonBody: {
jsonValue: '',
enableJsonSchema: false,
enableTransition: false,
},
xmlBody: {
value: '',
},
rawBody: {
value: '',
},
binaryBody: {
description: '',
file: undefined,
sendAsBody: false,
},
},
},
apiDefinitionId: '',
uploadFileIds: [],
linkFileIds: [],
};

View File

@ -828,9 +828,11 @@
() => props.params,
(arr) => {
if (arr.length > 0) {
let hasNoIdItem = false;
paramsData.value = arr.map((item, i) => {
if (!item) {
// undefined
hasNoIdItem = true;
return {
...cloneDeep(props.defaultParamItem),
id: new Date().getTime() + i,
@ -838,6 +840,7 @@
}
if (!item.id) {
// id
hasNoIdItem = true;
return {
...item,
id: new Date().getTime() + i,
@ -845,7 +848,7 @@
}
return item;
});
if (!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) {
addTableLine(arr.length - 1, false, true);
}
} else {

View File

@ -325,9 +325,11 @@
emit('change');
}
function handleParamTableChange(resultArr: any[]) {
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
currentTableParams.value = [...resultArr];
emit('change');
if (!isInit) {
emit('change');
}
}
function changeBodyFormat(val: RequestBodyFormat) {

View File

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

View File

@ -64,6 +64,7 @@
:active-module="props.activeModule"
:offspring-ids="props.offspringIds"
:protocol="props.protocol"
:definition-detail="activeApiTab"
/>
</template>

View File

@ -1,25 +0,0 @@
<template>
<MsDrawer
v-model:visible="visible"
unmount-on-close
:title="t('caseManagement.featureCase.caseDetail')"
:width="960"
:footer="false"
no-content-padding
>
</MsDrawer>
</template>
<script setup lang="ts">
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
required: true,
});
</script>
<style lang="less" scoped></style>

View File

@ -2,13 +2,16 @@
<MsDrawer
v-model:visible="visible"
unmount-on-close
:title="t('mockManagement.mockDetail')"
:title="mockDetail.id ? t('mockManagement.mockDetail') : t('mockManagement.createMock')"
:width="960"
:footer="false"
:footer="!mockDetail.id || isEdit"
:ok-text="isEdit ? t('common.save') : t('common.create')"
:save-continue-text="t('mockManagement.saveAndContinue')"
:show-continue="!isEdit"
no-content-padding
>
<template #tbutton>
<div class="right-operation-button-icon flex items-center gap-[4px]">
<div v-if="mockDetail.id" class="right-operation-button-icon flex items-center gap-[4px]">
<MsButton
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']"
type="icon"
@ -29,94 +32,180 @@
</MsButton>
</div>
</template>
<MsDetailCard :title="`【${mockDetail.num}】${mockDetail.name}`" :description="[]" class="mb-[16px]">
<template #titleRight>
<div class="flex items-center gap-[16px]">
<div class="flex items-center gap-[8px]">
<div class="whitespace-nowrap text-[var(--color-text-4)]">{{ t('apiTestManagement.apiType') }}</div>
<apiMethodName :method="mockDetail.method" tag-size="small" is-tag />
<a-spin :loading="loading" class="block p-[16px]">
<MsDetailCard
:title="`【${props.definitionDetail.num}】${props.definitionDetail.name}`"
:description="[]"
class="mb-[16px]"
>
<template #titleRight>
<div class="flex items-center gap-[16px]">
<div class="flex items-center gap-[8px]">
<div class="whitespace-nowrap text-[var(--color-text-4)]">{{ t('apiTestManagement.apiType') }}</div>
<apiMethodName :method="props.definitionDetail.method" tag-size="small" is-tag />
</div>
<div class="flex items-center gap-[8px]">
<div class="whitespace-nowrap text-[var(--color-text-4)]">{{ t('apiTestManagement.path') }}</div>
<a-tooltip :content="props.definitionDetail.url">
<div class="one-line-text">{{ props.definitionDetail.url }}</div>
</a-tooltip>
</div>
</div>
<div class="flex items-center gap-[8px]">
<div class="whitespace-nowrap text-[var(--color-text-4)]">{{ t('apiTestManagement.path') }}</div>
<a-tooltip :content="mockDetail.apiPath">
<div class="one-line-text">{{ mockDetail.apiPath }}</div>
</a-tooltip>
</template>
</MsDetailCard>
<a-form ref="mockForm" :model="mockDetail">
<a-form-item
class="hidden-item"
field="name"
:rules="[{ required: true, message: t('mockManagement.nameNotNull') }]"
>
<a-input
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') }]">
<MsTagsInput
v-model:model-value="mockDetail.tags"
class="mb-[16px] w-[732px]"
allow-clear
unique-value
retain-input-value
:max-tag-count="5"
:disabled="isReadOnly"
/>
</a-form-item>
</a-form>
<div class="font-medium">{{ t('mockManagement.matchRule') }}</div>
<MsTab
v-model:active-key="activeTab"
:content-tab-list="mockTabList"
:get-text-func="getTabBadge"
class="no-content relative my-[8px] border-b"
/>
<mockMatchRuleForm
v-if="
activeTab === RequestComposition.HEADER ||
activeTab === RequestComposition.QUERY ||
activeTab === RequestComposition.REST
"
v-model:matchAll="currentMatchAll"
v-model:matchRules="currentMatchRules"
:key-options="currentKeyOptions"
/>
<template v-else>
<div class="mb-[8px] flex items-center justify-between">
<a-radio-group
v-model:model-value="mockDetail.mockMatchRule.body.paramType"
type="button"
size="small"
@change="handleMockBodyTypeChange"
>
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">
{{ requestBodyTypeMap[item] }}
</a-radio>
</a-radio-group>
</div>
<div
v-if="mockDetail.mockMatchRule.body.paramType === 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)]"
>
{{ t('apiTestDebug.noneBody') }}
</div>
<mockMatchRuleForm
v-else-if="
[RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(mockDetail.mockMatchRule.body.paramType)
"
v-model:matchAll="mockDetail.mockMatchRule.body.formDataMatch.matchAll"
v-model:matchRules="mockDetail.mockMatchRule.body.formDataMatch.matchRules"
:key-options="currentBodyKeyOptions"
/>
<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]">
<MsAddAttachment
v-model:file-list="fileList"
mode="input"
:multiple="false"
:fields="{
id: 'fileId',
name: 'fileName',
}"
@change="handleFileChange"
/>
</div>
<!-- <div class="flex items-center">
<a-switch v-model:model-value="innerParams.binarySend" class="mr-[8px]" size="small" type="line"></a-switch>
<span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right">
<template #content>
<div>{{ t('apiTestDebug.sendAsMainTextTip1') }}</div>
<div>{{ t('apiTestDebug.sendAsMainTextTip2') }}</div>
</template>
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div> -->
</div>
<div v-else class="flex h-[300px]">
<MsCodeEditor
v-model:model-value="mockDetail.mockMatchRule.body.raw"
class="flex-1"
theme="vs"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-code-format="true"
:language="currentCodeLanguage"
>
</MsCodeEditor>
</div>
</template>
</MsDetailCard>
<a-form ref="mockForm" :model="mockDetail">
<a-form-item
class="hidden-item"
field="name"
:rules="[{ required: true, message: t('mockManagement.nameNotNull') }]"
>
<a-input
v-model:model-value="mockDetail.name"
:placeholder="t('mockManagement.namePlaceholder')"
class="w-[732px]"
:disabled="isReadOnly"
></a-input>
</a-form-item>
<a-form-item class="hidden-item" :rules="[{ required: true, message: t('mockManagement.nameNotNull') }]">
<MsTagsInput
v-model:model-value="mockDetail.tags"
class="w-[732px]"
:placeholder="t('mockManagement.namePlaceholder')"
allow-clear
unique-value
retain-input-value
:max-tag-count="5"
:disabled="isReadOnly"
/>
</a-form-item>
</a-form>
<div class="mb-[8px] font-medium">{{ t('mockManagement.matchRule') }}</div>
<div class="mb-[8px] flex items-center justify-between">
<a-radio-group v-model:model-value="mockDetail.bodyType" type="button" size="small" :disabled="isReadOnly">
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">
{{ requestBodyTypeMap[item] }}
</a-radio>
</a-radio-group>
</div>
<div
v-if="mockDetail.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)]"
>
{{ t('apiTestDebug.noneBody') }}
</div>
<div v-else class="flex h-[calc(100%-34px)]">
<MsCodeEditor
v-model:model-value="currentBodyCode"
:read-only="isReadOnly"
class="flex-1"
theme="vs"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-code-format="true"
:language="currentCodeLanguage"
>
</MsCodeEditor>
</div>
<mockResponse
v-model:mock-response="mockDetail.response"
:definition-responses="props.definitionDetail.responseDefinition || []"
/>
</a-spin>
</MsDrawer>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
import mockMatchRuleForm from './mockMatchRuleForm.vue';
import mockResponse from './mockResponse.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { requestBodyTypeMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import { RequestBodyFormat } from '@/enums/apiEnum';
import { MockParams } from '@/models/apiTest/mock';
import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum';
import {
defaultHeaderParamsItem,
defaultRequestParamsItem,
mockDefaultParams,
} from '@/views/api-test/components/config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{
definitionDetail: RequestParam;
}>();
const emit = defineEmits<{
(e: 'delete'): void;
}>();
@ -127,42 +216,216 @@
required: true,
});
const loading = ref(false);
const isEdit = ref(false);
const mockDetail = ref<any>();
const isReadOnly = computed(() => !isEdit.value && !mockDetail.value.id);
const mockDetail = ref<MockParams>(cloneDeep(mockDefaultParams));
const isReadOnly = computed(() => !!mockDetail.value.id && !isEdit.value);
const activeTab = ref<RequestComposition>(RequestComposition.BODY);
const mockTabList = [
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.QUERY,
label: 'Query',
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
];
//
const currentBodyCode = computed({
/**
* 获取 tab 的参数数量徽标
*/
function getTabBadge(tabKey: RequestComposition) {
switch (tabKey) {
case RequestComposition.HEADER:
const headerNum = filterKeyValParams(mockDetail.value.mockMatchRule.header.matchRules, defaultHeaderParamsItem)
.validParams.length;
return `${headerNum > 0 ? headerNum : ''}`;
case RequestComposition.BODY:
return mockDetail.value.mockMatchRule.body.paramType !== RequestBodyFormat.NONE ? '1' : '';
case RequestComposition.QUERY:
const queryNum = filterKeyValParams(mockDetail.value.mockMatchRule.query.matchRules, defaultRequestParamsItem)
.validParams.length;
return `${queryNum > 0 ? queryNum : ''}`;
case RequestComposition.REST:
const restNum = filterKeyValParams(mockDetail.value.mockMatchRule.rest.matchRules, defaultRequestParamsItem)
.validParams.length;
return `${restNum > 0 ? restNum : ''}`;
default:
return '';
}
}
function handleMockBodyTypeChange() {
mockDetail.value.unSaved = true;
}
const currentMatchAll = computed({
get() {
if (mockDetail.value.bodyType === RequestBodyFormat.JSON) {
return mockDetail.value.jsonBody.jsonValue;
switch (activeTab.value) {
case RequestComposition.HEADER:
return mockDetail.value.mockMatchRule.header.matchAll;
case RequestComposition.QUERY:
return mockDetail.value.mockMatchRule.query.matchAll;
case RequestComposition.REST:
return mockDetail.value.mockMatchRule.rest.matchAll;
default:
return false;
}
if (mockDetail.value.bodyType === RequestBodyFormat.XML) {
return mockDetail.value.xmlBody.value;
}
return mockDetail.value.rawBody.value;
},
set(val) {
if (mockDetail.value.bodyType === RequestBodyFormat.JSON) {
mockDetail.value.jsonBody.jsonValue = val;
} else if (mockDetail.value.bodyType === RequestBodyFormat.XML) {
mockDetail.value.xmlBody.value = val;
} else {
mockDetail.value.rawBody.value = val;
switch (activeTab.value) {
case RequestComposition.HEADER:
mockDetail.value.mockMatchRule.header.matchAll = val;
break;
case RequestComposition.QUERY:
mockDetail.value.mockMatchRule.query.matchAll = val;
break;
case RequestComposition.REST:
mockDetail.value.mockMatchRule.rest.matchAll = val;
break;
default:
break;
}
},
});
const currentMatchRules = computed({
get() {
switch (activeTab.value) {
case RequestComposition.HEADER:
return mockDetail.value.mockMatchRule.header.matchRules;
case RequestComposition.QUERY:
return mockDetail.value.mockMatchRule.query.matchRules;
case RequestComposition.REST:
return mockDetail.value.mockMatchRule.rest.matchRules;
default:
return [];
}
},
set(val) {
switch (activeTab.value) {
case RequestComposition.HEADER:
mockDetail.value.mockMatchRule.header.matchRules = val;
break;
case RequestComposition.QUERY:
mockDetail.value.mockMatchRule.query.matchRules = val;
break;
case RequestComposition.REST:
mockDetail.value.mockMatchRule.rest.matchRules = val;
break;
default:
break;
}
},
});
const currentKeyOptions = computed(() => {
switch (activeTab.value) {
case RequestComposition.HEADER:
return props.definitionDetail.headers.filter((e) => ({
label: e.key,
value: e.value,
}));
case RequestComposition.QUERY:
return props.definitionDetail.query.filter((e) => ({
label: e.key,
value: e.value,
}));
case RequestComposition.REST:
return props.definitionDetail.rest.filter((e) => ({
label: e.key,
value: e.value,
}));
default:
return [];
}
});
const currentBodyKeyOptions = computed(() => {
switch (mockDetail.value.mockMatchRule.body.paramType) {
case RequestBodyFormat.FORM_DATA:
return props.definitionDetail.body.formDataBody.formValues.filter((e) => ({
label: e.key,
value: e.value,
}));
case RequestBodyFormat.WWW_FORM:
return props.definitionDetail.body.wwwFormBody.formValues.filter((e) => ({
label: e.key,
value: e.value,
}));
default:
return [];
}
});
//
const currentCodeLanguage = computed(() => {
if (mockDetail.value.bodyType === RequestBodyFormat.JSON) {
if (mockDetail.value.mockMatchRule.body.paramType === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (mockDetail.value.bodyType === RequestBodyFormat.XML) {
if (mockDetail.value.mockMatchRule.body.paramType === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
const fileList = ref<MsFileItem[]>([]);
async function handleFileChange(files: MsFileItem[], file?: MsFileItem) {
try {
if (file?.local && file.file) {
//
loading.value = true;
const res = await Promise.resolve({ data: 'fileId' });
mockDetail.value.mockMatchRule.body.binaryBody.file = {
...file,
fileId: res.data,
fileName: file?.name || '',
fileAlias: file?.name || '',
local: true,
};
loading.value = false;
} else {
//
mockDetail.value.mockMatchRule.body.binaryBody.file = {
...fileList.value[0],
fileId: fileList.value[0]?.uid,
fileName: fileList.value[0]?.originalName || '',
fileAlias: fileList.value[0]?.name || '',
local: false,
};
}
if (
mockDetail.value.mockMatchRule.body.binaryBody.file &&
!mockDetail.value.mockMatchRule.body.binaryBody.file.fileId
) {
mockDetail.value.mockMatchRule.body.binaryBody.file = undefined;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
}
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 handleDelete() {
emit('delete');
}

View File

@ -0,0 +1,228 @@
<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-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">
<paramDescInput
v-model:desc="item.description"
@input="() => addMatchRule(idx)"
@dblclick="quickInputDesc(item)"
@change="handleDescChange"
/>
</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>
</div>
</div>
</div>
</a-form>
<a-modal
v-model:visible="showQuickInputParam"
:title="t('ms.paramsInput.value')"
:ok-text="t('apiTestDebug.apply')"
:ok-button-props="{ disabled: !quickInputParamValue || quickInputParamValue.trim() === '' }"
class="ms-modal-form"
body-class="!p-0"
:width="680"
title-align="start"
@ok="applyQuickInputParam"
@close="clearQuickInputParam"
>
<MsCodeEditor
v-if="showQuickInputParam"
v-model:model-value="quickInputParamValue"
theme="vs"
height="300px"
:show-full-screen="false"
>
<template #rightTitle>
<div class="flex justify-between">
<div class="text-[var(--color-text-4)]">
{{ t('apiTestDebug.quickInputParamsTip') }}
</div>
</div>
</template>
</MsCodeEditor>
</a-modal>
<!-- <a-modal
v-model:visible="showQuickInputDesc"
:title="t('apiTestDebug.desc')"
:ok-text="t('common.save')"
:ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }"
class="ms-modal-form"
body-class="!p-0"
:width="480"
title-align="start"
:auto-size="{ minRows: 2 }"
@ok="applyQuickInputDesc"
@close="clearQuickInputDesc"
>
<a-textarea
v-model:model-value="quickInputDescValue"
:placeholder="t('apiTestDebug.descPlaceholder')"
:max-length="1000"
></a-textarea>
</a-modal> -->
</template>
<script setup lang="ts">
import { FormInstance, SelectOptionData } from '@arco-design/web-vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsParamsInput from '@/components/business/ms-params-input/index.vue';
// import paramDescInput from '@/views/api-test/components/paramDescInput.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
keyOptions: SelectOptionData[];
}>();
const emit = defineEmits<{
(
e: 'change',
form: {
matchAll: boolean;
matchRules: Record<string, any>[];
},
isInit?: boolean
): void;
}>();
const { t } = useI18n();
const matchAll = defineModel<boolean>('matchAll', {
required: true,
});
const matchRules = defineModel<Record<string, any>[]>('matchRules', {
required: true,
});
const formRef = ref<FormInstance>();
const formModel = ref({
matchAll: matchAll.value,
matchRules: matchRules.value,
});
function handleDeleteItem(index: number) {
matchRules.value.splice(index, 1);
}
function addMatchRule(rowIndex: number) {
if (rowIndex === matchRules.value.length - 1) {
matchRules.value.push({
key: '',
value: '',
description: '',
});
}
}
function emitChange(from: string, isInit?: boolean) {
emit('change', formModel.value, isInit);
}
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');
function quickInputParams(record: any) {
activeQuickInputRecord.value = record;
showQuickInputParam.value = true;
quickInputParamValue.value = record.value;
}
function clearQuickInputParam() {
activeQuickInputRecord.value = {};
quickInputParamValue.value = '';
}
function applyQuickInputParam() {
activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false;
addMatchRule(matchRules.value.findIndex((e) => e.id === activeQuickInputRecord.value.id));
clearQuickInputParam();
emitChange('applyQuickInputParam');
}
// const showQuickInputDesc = ref(false);
// const quickInputDescValue = ref('');
// function quickInputDesc(record: any) {
// activeQuickInputRecord.value = record;
// showQuickInputDesc.value = true;
// quickInputDescValue.value = record.description;
// }
// function clearQuickInputDesc() {
// activeQuickInputRecord.value = {};
// quickInputDescValue.value = '';
// }
// function applyQuickInputDesc() {
// activeQuickInputRecord.value.description = quickInputDescValue.value;
// showQuickInputDesc.value = false;
// addMatchRule(matchRules.value.findIndex((e) => e.id === activeQuickInputRecord.value.id));
// clearQuickInputDesc();
// emitChange('applyQuickInputDesc');
// }
// function handleDescChange() {
// emitChange('handleDescChange');
// }
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,304 @@
<template>
<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>
{{ t('mockManagement.followDefinition') }}
</div>
<template v-if="!mockResponse.useApiResponse">
<MsTab
v-model:active-key="activeTab"
:content-tab-list="responseCompositionTabList"
class="no-content relative my-[8px] border-b"
:show-badge="false"
/>
<div class="mt-[8px]">
<template v-if="activeTab === ResponseComposition.BODY">
<div class="mb-[8px] flex items-center justify-between">
<a-radio-group
v-model:model-value="mockResponse.body.bodyType"
type="button"
size="small"
@change="(val) => emit('change')"
>
<a-radio
v-for="item of ResponseBodyFormat"
v-show="item !== ResponseBodyFormat.NONE"
:key="item"
:value="item"
>
{{ ResponseBodyFormat[item].toLowerCase() }}
</a-radio>
</a-radio-group>
<!-- <div v-if="mockResponse.body.bodyType === ResponseBodyFormat.JSON" class="ml-auto flex items-center">
<a-radio-group
v-model:model-value="mockResponse.body.jsonBody.enableJsonSchema"
size="mini"
@change="emit('change')"
>
<a-radio :value="false">Json</a-radio>
<a-radio class="mr-0" :value="true"> Json Schema </a-radio>
</a-radio-group>
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="mockResponse.body.jsonBody.enableTransition" size="small" type="line" />
{{ t('apiTestManagement.dynamicConversion') }}
</div>
</div> -->
</div>
<div
v-if="
[ResponseBodyFormat.JSON, ResponseBodyFormat.XML, ResponseBodyFormat.RAW].includes(
mockResponse.body.bodyType
)
"
>
<!-- <MsJsonSchema
v-if="mockResponse.body.jsonBody.enableJsonSchema"
:data="mockResponse.body.jsonBody.jsonSchema"
:columns="jsonSchemaColumns"
/> -->
<MsCodeEditor
ref="responseEditorRef"
v-model:model-value="currentBodyCode"
:language="currentCodeLanguage"
theme="vs"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
show-code-format
>
</MsCodeEditor>
</div>
<div v-else>
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input
v-model:model-value="mockResponse.body.binaryBody.description"
:placeholder="t('common.desc')"
:max-length="255"
/>
<MsAddAttachment
v-model:file-list="fileList"
mode="input"
:multiple="false"
:fields="{
id: 'fileId',
name: 'fileName',
}"
@change="handleFileChange"
/>
</div>
<div class="flex items-center">
<a-switch
v-model:model-value="mockResponse.body.binaryBody.sendAsBody"
class="mr-[8px]"
size="small"
type="line"
></a-switch>
<span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right">
<template #content>
<div>{{ t('apiTestDebug.sendAsMainTextTip1') }}</div>
<div>{{ t('apiTestDebug.sendAsMainTextTip2') }}</div>
</template>
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</div>
</template>
<paramTable
v-else-if="activeTab === ResponseComposition.HEADER"
:params="mockResponse.headers"
:columns="columns"
:default-param-item="defaultKeyValueParamItem"
:selectable="false"
@change="handleResponseTableChange"
/>
<a-select
v-else
v-model:model-value="mockResponse.statusCode"
:options="statusCodeOptions"
class="w-[200px]"
@change="() => emit('change')"
/>
</div>
</template>
<div v-else class="mt-[8px]">
<a-select
v-model:model-value="mockResponse.apiResponseId"
:options="mockResponseOptions"
class="w-[150px]"
></a-select>
</div>
</a-spin>
</template>
<script setup lang="ts">
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsTab from '@/components/pure/ms-tab/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsAddAttachment from '@/components/business/ms-add-attachment/index.vue';
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.vue';
import { responseHeaderOption } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import { MockResponse } from '@/models/apiTest/mock';
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
import { defaultKeyValueParamItem, statusCodes } from '@/views/api-test/components/config';
const props = defineProps<{
definitionResponses: ResponseItem[];
uploadTempFileApi?: (...args: any) => Promise<any>; //
}>();
const emit = defineEmits<{
(e: 'change'): void;
}>();
const { t } = useI18n();
const activeTab = ref<ResponseComposition>(ResponseComposition.BODY);
const mockResponse = defineModel<MockResponse>('mockResponse', {
required: true,
});
const mockResponseOptions = computed(() =>
props.definitionResponses.map((item) => ({
label: `${t(item.label || item.name)}(${item.statusCode})`,
value: item.id,
}))
);
const responseCompositionTabList = [
{
label: t('apiTestDebug.responseBody'),
value: ResponseComposition.BODY,
},
{
label: t('apiTestDebug.responseHeader'),
value: ResponseComposition.HEADER,
},
{
label: t('apiTestManagement.responseCode'),
value: ResponseComposition.CODE,
},
];
const statusCodeOptions = statusCodes.map((e) => ({
label: e.toString(),
value: e,
}));
const columns: ParamTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
slotName: 'key',
inputType: 'autoComplete',
autoCompleteParams: responseHeaderOption,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
slotName: 'value',
isNormal: true,
inputType: 'input',
},
{
title: '',
dataIndex: 'operation',
slotName: 'operation',
width: 35,
},
];
function handleResponseTableChange(arr: any[]) {
mockResponse.value.headers = [...arr];
emit('change');
}
//
const currentBodyCode = computed({
get() {
if (mockResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
return mockResponse.value.body.jsonBody.jsonValue;
}
if (mockResponse.value.body.bodyType === ResponseBodyFormat.XML) {
return mockResponse.value.body.xmlBody.value;
}
return mockResponse.value.body.rawBody.value;
},
set(val) {
if (mockResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
mockResponse.value.body.jsonBody.jsonValue = val;
} else if (mockResponse.value.body.bodyType === ResponseBodyFormat.XML) {
mockResponse.value.body.xmlBody.value = val;
} else {
mockResponse.value.body.rawBody.value = val;
}
},
});
//
const currentCodeLanguage = computed(() => {
if (mockResponse.value.body.bodyType === ResponseBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (mockResponse.value.body.bodyType === ResponseBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
const fileList = ref<MsFileItem[]>([]);
const loading = ref<boolean>(false);
async function handleFileChange() {
try {
if (fileList.value[0] && fileList.value[0].local && fileList.value[0].file && props.uploadTempFileApi) {
loading.value = true;
const res = await props.uploadTempFileApi(fileList.value[0].file);
mockResponse.value.body.binaryBody.file = {
...fileList.value[0],
fileId: res.data,
fileName: fileList.value[0]?.name || '',
fileAlias: fileList.value[0]?.name || '',
local: true,
};
loading.value = false;
} else if (fileList.value[0]) {
mockResponse.value.body.binaryBody.file = {
...fileList.value[0],
fileId: fileList.value[0].uid,
fileName: fileList.value[0]?.originalName || '',
fileAlias: fileList.value[0]?.name || '',
local: false,
};
} else {
mockResponse.value.body.binaryBody.file = undefined;
}
emit('change');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
}
</script>
<style lang="less" scoped>
.response-head {
@apply flex flex-wrap items-center justify-between border-b;
padding: 8px 16px;
border-color: var(--color-text-n8);
gap: 8px;
}
.arco-tabs-content {
@apply hidden;
}
</style>

View File

@ -66,8 +66,7 @@
</template>
</ms-base-table>
</div>
<mockDetailDrawer v-model:visible="mockDetailDrawerVisible" />
<mockDebugDrawer v-model:visible="mockDebugDrawerVisible" />
<mockDetailDrawer v-model:visible="mockDetailDrawerVisible" :definition-detail="props.definitionDetail" />
</template>
<script setup lang="ts">
@ -80,6 +79,7 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import {
deleteDefinitionMockMock,
@ -97,14 +97,13 @@
import { TableKeyEnum } from '@/enums/tableEnum';
const mockDetailDrawer = defineAsyncComponent(() => import('./mockDetailDrawer.vue'));
const mockDebugDrawer = defineAsyncComponent(() => import('./mockDebugDrawer.vue'));
const props = defineProps<{
isApi?: boolean; // case tab
class?: string;
activeModule: string;
offspringIds: string[];
protocol: string; //
definitionDetail: RequestParam;
readOnly?: boolean; //
}>();
const emit = defineEmits<{
@ -262,7 +261,7 @@
);
watch(
() => props.protocol,
() => props.definitionDetail.protocol,
() => {
loadMockList();
}

View File

@ -214,4 +214,7 @@ export default {
'mockManagement.namePlaceholder': '请输入期望名称',
'mockManagement.nameNotNull': '期望名称不能为空',
'mockManagement.matchRule': '匹配规则',
'mockManagement.saveAndContinue': '保存并继续创建',
'mockManagement.paramNameNotNull': '参数名称不能为空',
'mockManagement.followDefinition': '跟随 API 定义',
};

View File

@ -99,19 +99,19 @@
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.config.method" />
<div
v-if="step.uniqueId === showStepNameEditInputStepId"
class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
class="name-warp absolute left-0 top-[-1px] z-10 w-[450px]"
@click.stop
>
<a-input
v-model:model-value="tempStepName"
:placeholder="t('apiScenario.pleaseInputStepName')"
:max-length="255"
size="small"
size="mini"
@press-enter="applyStepNameChange(step)"
@blur="applyStepNameChange(step)"
/>
</div>
<a-tooltip :content="step.name">
<a-tooltip v-else :content="step.name">
<div class="step-name-container">
<div class="one-line-text mr-[4px] max-w-[350px] font-medium text-[var(--color-text-1)]">
{{ step.name }}
@ -129,20 +129,20 @@
<template v-else>
<div
v-if="step.uniqueId === showStepDescEditInputStepId"
class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
class="desc-warp absolute left-0 top-[-1px] z-10 w-[450px]"
>
<a-input
v-model:model-value="tempStepDesc"
:default-value="step.name || t('apiScenario.pleaseInputStepDesc')"
:placeholder="t('apiScenario.pleaseInputStepDesc')"
:max-length="255"
size="small"
size="mini"
@press-enter="applyStepDescChange(step)"
@blur="applyStepDescChange(step)"
@click.stop
>
<template #prefix>
{{ t('common.desc') }}
<div class="text-[12px] leading-[20px]">{{ t('common.desc') }}</div>
</template>
</a-input>
</div>
@ -1875,10 +1875,9 @@
background-color: var(--color-text-n9) !important;
}
.step-node-content {
@apply flex w-full flex-1 items-center;
@apply flex w-full flex-1 flex-nowrap items-center;
gap: 8px;
margin-right: 6px;
}
.step-name-container {
@apply flex items-center;

View File

@ -22,7 +22,7 @@
ref="executeButtonRef"
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
:execute-loading="activeScenarioTab.executeLoading"
@execute="handleExecute"
@execute="(type) => handleExecute(type)"
@stop-debug="handleStopExecute"
/>
<a-button

View File

@ -55,7 +55,7 @@
v-model:model-value="record.value"
:placeholder="t('common.pleaseSelect')"
class="param-input w-full min-w-[250px]"
:disabled="!record.enable || !hasAnyPermission(permissionsMap[props.group][props.moduleType].edit)"
:disabled="!record.enable || !hasAnyPermission(permissionsMap[props.group][props.moduleType]?.edit)"
@change="() => changeRunRules(record)"
>
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value">
@ -112,12 +112,12 @@
size="small"
type="line"
:before-change="() => handleBeforeEnableChange(record)"
:disabled="!hasAnyPermission(permissionsMap[props.group][props.moduleType].edit)"
:disabled="!hasAnyPermission(permissionsMap[props.group][props.moduleType]?.edit)"
/>
<a-divider direction="vertical" />
<MsButton
class="!mr-0"
:disabled="!hasAnyPermission(permissionsMap[props.group][props.moduleType].edit)"
:disabled="!hasAnyPermission(permissionsMap[props.group][props.moduleType]?.edit)"
@click="delSchedule(record)"
>{{ t('common.delete') }}
</MsButton>
@ -259,7 +259,7 @@
},
};
const hasOperationPermission = computed(() =>
hasAnyPermission([...permissionsMap[props.group][props.moduleType].edit])
hasAnyPermission([...(permissionsMap[props.group][props.moduleType]?.edit || '')])
);
const columns: MsTableColumn = [
@ -411,12 +411,12 @@
{
label: 'project.taskCenter.batchEnable',
eventTag: 'batchEnable',
anyPermission: permissionsMap[props.group][props.moduleType].edit,
anyPermission: permissionsMap[props.group][props.moduleType]?.edit,
},
{
label: 'project.taskCenter.batchDisable',
eventTag: 'batchDisable',
anyPermission: permissionsMap[props.group][props.moduleType].edit,
anyPermission: permissionsMap[props.group][props.moduleType]?.edit,
},
],
};

View File

@ -21,7 +21,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import ApiCase from './apiCase.vue';
import ScheduledTask from './scheduledTask.vue';
@ -33,13 +33,14 @@
import type { ResourceTypeMapKey } from './utils';
const { t } = useI18n();
const activeTab = ref<ResourceTypeMapKey>(TaskCenterEnum.API_CASE);
const props = defineProps<{
group: 'system' | 'organization' | 'project';
mode?: 'modal' | 'normal';
}>();
const route = useRoute();
const realTabList = ref([
{
value: TaskCenterEnum.API_CASE,
@ -75,7 +76,8 @@
},
]);
const activeTask = ref('real');
const activeTask = ref(route.query.tab || 'real');
const activeTab = ref<ResourceTypeMapKey>((route.query.type as ResourceTypeMapKey) || TaskCenterEnum.API_CASE);
const rightTabList = computed(() => {
return activeTask.value === 'real' ? realTabList.value : timingTabList.value;

View File

@ -135,9 +135,9 @@
{{ t(typeOptions.find((e) => e.value === record.type)?.label || '') }}
</template>
<template #content="{ record }">
<div v-if="record.module === 'SYSTEM' || record.type === 'DELETE'" class="one-line-text">{{
record.content
}}</div>
<div v-if="record.module === 'SYSTEM' || record.type === 'DELETE'" class="one-line-text">
{{ record.content }}
</div>
<MsButton v-else @click="handleNameClick(record)">
<div class="one-line-text">
{{ record.content }}
@ -186,6 +186,7 @@
const props = defineProps<{
mode: (typeof MENU_LEVEL)[number]; // //
}>();
const { t } = useI18n();
const appStore = useAppStore();

View File

@ -5,8 +5,6 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import TaskCenter from '@/views/project-management/taskCenter/component/taskCom.vue';
</script>