feat(接口场景): 场景配置&场景拖拽&场景启用禁用
This commit is contained in:
parent
008f930a98
commit
ce9811329d
|
@ -468,7 +468,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arco-radio-checked:not(.arco-radio-disabled) {
|
||||
.arco-radio-icon {
|
||||
@apply !bg-white;
|
||||
|
@ -596,7 +595,6 @@
|
|||
.ms-container {
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.ms-form {
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px;
|
||||
|
@ -616,18 +614,19 @@
|
|||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
margin-right: 24px;
|
||||
}
|
||||
.arco-radio-group-button {
|
||||
padding: 1px;
|
||||
background-color: var(--color-text-n8);
|
||||
.arco-radio-button {
|
||||
@apply bg-transparent;
|
||||
@apply bg-transparent;
|
||||
|
||||
margin: 1px;
|
||||
}
|
||||
.arco-radio-checked {
|
||||
@apply bg-white;
|
||||
@apply bg-white;
|
||||
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
|
@ -643,7 +642,8 @@
|
|||
}
|
||||
.arco-radio-checked:not(.arco-radio-disabled) {
|
||||
.arco-radio-icon {
|
||||
@apply !bg-white;
|
||||
@apply !bg-white;
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
|
@ -658,11 +658,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 气泡弹窗 **/
|
||||
.arco-popover-title {
|
||||
font-size: 14px;
|
||||
|
|
|
@ -312,6 +312,12 @@ export default function useTableProps<T>(
|
|||
}
|
||||
};
|
||||
|
||||
// 重置筛选
|
||||
const clearSelector = () => {
|
||||
propsRes.value.selectorStatus = SelectAllEnum.NONE; // 重置选择器状态
|
||||
resetSelector();
|
||||
};
|
||||
|
||||
// 获取当前表格的选中项数量
|
||||
const getSelectedCount = () => {
|
||||
const { selectorStatus, msPagination, excludeKeys, selectedKeys } = propsRes.value;
|
||||
|
@ -472,6 +478,7 @@ export default function useTableProps<T>(
|
|||
setAdvanceFilter,
|
||||
resetPagination,
|
||||
getSelectedCount,
|
||||
clearSelector,
|
||||
resetSelector,
|
||||
getTableQueryParams,
|
||||
setTableSelected,
|
||||
|
|
|
@ -255,7 +255,7 @@ export enum ScenarioStepType {
|
|||
LOOP_CONTROLLER = 'LOOP_CONTROLLER', // 循环控制器
|
||||
API = 'API', // 接口定义
|
||||
CUSTOM_REQUEST = 'CUSTOM_REQUEST', // 自定义请求
|
||||
API_SCENARIO = ' API_SCENARIO', // 场景
|
||||
API_SCENARIO = 'API_SCENARIO', // 场景
|
||||
IF_CONTROLLER = 'IF_CONTROLLER', // 条件控制器
|
||||
ONCE_ONLY_CONTROLLER = 'ONCE_ONLY_CONTROLLER', // 一次控制器
|
||||
CONSTANT_TIMER = 'CONSTANT_TIMER', // 等待控制器
|
||||
|
|
|
@ -298,8 +298,20 @@ export interface LoopStepDetail extends StepDetailsCommon {
|
|||
msCountController: CountController;
|
||||
whileController: WhileController;
|
||||
}
|
||||
export interface ScenarioStepConfig {
|
||||
useCurrentScenarioParam: boolean; // 是否优先使用当前场景参数
|
||||
useBothScenarioParam: boolean; // 是否当前场景参数和源场景参数都应用(勾选非空值时为 true)
|
||||
enableScenarioEnv: boolean; // 是否应用源场景环境
|
||||
}
|
||||
export type ScenarioStepDetail = Partial<
|
||||
CustomApiStepDetail & ConditionStepDetail & LoopStepDetail & { protocol: string; method: RequestMethods }
|
||||
CustomApiStepDetail &
|
||||
ConditionStepDetail &
|
||||
LoopStepDetail &
|
||||
ScenarioStepConfig & {
|
||||
protocol: string;
|
||||
method: RequestMethods;
|
||||
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
|
||||
}
|
||||
>;
|
||||
export interface ScenarioStepItem {
|
||||
id: string | number;
|
||||
|
|
|
@ -260,7 +260,6 @@
|
|||
:is-expanded="isVerticalExpanded"
|
||||
:request-result="props.stepResponses?.[requestVModel.stepId]"
|
||||
:console="props.stepResponses?.[requestVModel.stepId]?.console"
|
||||
:show-empty="false"
|
||||
:is-edit="false"
|
||||
is-definition
|
||||
:loading="requestVModel.executeLoading || loading"
|
||||
|
@ -362,6 +361,7 @@
|
|||
|
||||
export type RequestParam = ExecuteApiRequestFullParams & {
|
||||
response?: RequestTaskResult;
|
||||
customizeRequest?: boolean;
|
||||
customizeRequestEnvEnable?: boolean;
|
||||
} & RequestCustomAttr;
|
||||
|
||||
|
@ -395,11 +395,12 @@
|
|||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
const loading = defineModel<boolean>('detailLoading', { default: false });
|
||||
|
||||
const defaultDebugParams: RequestParam = {
|
||||
const defaultApiParams: RequestParam = {
|
||||
name: '',
|
||||
type: 'api',
|
||||
stepId: '',
|
||||
resourceId: '',
|
||||
customizeRequest: true,
|
||||
customizeRequestEnvEnable: false,
|
||||
protocol: 'HTTP',
|
||||
url: '',
|
||||
|
@ -455,7 +456,7 @@
|
|||
executeLoading: false,
|
||||
};
|
||||
|
||||
const requestVModel = ref<RequestParam>(defaultDebugParams);
|
||||
const requestVModel = ref<RequestParam>(defaultApiParams);
|
||||
const _stepType = computed(() => {
|
||||
if (props.step) {
|
||||
return getStepType(props.step);
|
||||
|
@ -832,7 +833,9 @@
|
|||
if (val) {
|
||||
verticalSplitBoxRef.value?.expand(0.6);
|
||||
} else {
|
||||
verticalSplitBoxRef.value?.collapse(1);
|
||||
verticalSplitBoxRef.value?.collapse(
|
||||
splitContainerRef.value ? `${splitContainerRef.value.clientHeight - 42}px` : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -843,7 +846,8 @@
|
|||
if (val) {
|
||||
changeVerticalExpand(true);
|
||||
} else {
|
||||
changeVerticalExpand(false);
|
||||
isVerticalExpanded.value = false;
|
||||
verticalSplitBoxRef.value?.collapse(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -931,6 +935,7 @@
|
|||
protocol: requestVModel.value.protocol,
|
||||
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
|
||||
name: requestVModel.value.name,
|
||||
customizeRequest: props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.request,
|
||||
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
|
||||
children: [
|
||||
{
|
||||
|
@ -1066,7 +1071,7 @@
|
|||
if (props.request) {
|
||||
// 查看自定义请求、引用 api、复制 api
|
||||
requestVModel.value = cloneDeep({
|
||||
...defaultDebugParams,
|
||||
...defaultApiParams,
|
||||
...props.request,
|
||||
isNew: false,
|
||||
});
|
||||
|
@ -1082,7 +1087,7 @@
|
|||
} else {
|
||||
// 新建自定义请求
|
||||
requestVModel.value = cloneDeep({
|
||||
...defaultDebugParams,
|
||||
...defaultApiParams,
|
||||
stepId: getGenerateId(),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -40,73 +40,303 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center p-[16px]">
|
||||
<a-input
|
||||
v-model:model-value="requestVModel.name"
|
||||
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
|
||||
allow-clear
|
||||
:max-length="255"
|
||||
:show-word-limit="!isQuote"
|
||||
:disabled="isQuote"
|
||||
/>
|
||||
<executeButton
|
||||
ref="executeRef"
|
||||
class="ml-[16px]"
|
||||
:execute-loading="requestVModel.executeLoading"
|
||||
@execute="handleExecute"
|
||||
@stop-debug="stopDebug"
|
||||
/>
|
||||
<a-empty
|
||||
v-if="pluginError && !isHttpProtocol"
|
||||
:description="t('apiTestDebug.noPlugin')"
|
||||
class="h-[200px] items-center justify-center"
|
||||
>
|
||||
<template #image>
|
||||
<MsIcon type="icon-icon_plugin_outlined" size="48" />
|
||||
</template>
|
||||
</a-empty>
|
||||
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
|
||||
<div class="px-[18px] pt-[8px]">
|
||||
<div class="flex flex-wrap items-center justify-between gap-[12px]">
|
||||
<div class="flex flex-1 items-center gap-[16px]">
|
||||
<a-select
|
||||
v-if="requestVModel.isNew"
|
||||
v-model:model-value="requestVModel.protocol"
|
||||
:options="protocolOptions"
|
||||
:loading="protocolLoading"
|
||||
:disabled="_stepType.isQuoteCase"
|
||||
class="w-[90px]"
|
||||
@change="(val) => handleActiveDebugProtocolChange(val as string)"
|
||||
/>
|
||||
<div v-else class="flex items-center gap-[4px]">
|
||||
<apiMethodName
|
||||
:method="(requestVModel.protocol as RequestMethods)"
|
||||
tag-background-color="rgb(var(--link-7))"
|
||||
tag-text-color="white"
|
||||
is-tag
|
||||
class="flex items-center"
|
||||
/>
|
||||
<a-tooltip v-if="!isHttpProtocol" :content="requestVModel.name" :mouse-enter-delay="500">
|
||||
<div class="one-line-text max-w-[350px]"> {{ requestVModel.name }}</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-input-group v-if="isHttpProtocol" class="flex-1">
|
||||
<apiMethodSelect
|
||||
v-model:model-value="requestVModel.method"
|
||||
class="w-[140px]"
|
||||
:disabled="_stepType.isQuoteCase"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<a-input
|
||||
v-model:model-value="requestVModel.url"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiTestDebug.urlPlaceholder')"
|
||||
allow-clear
|
||||
class="hover:z-10"
|
||||
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
|
||||
:disabled="_stepType.isQuoteCase"
|
||||
@input="() => (isUrlError = false)"
|
||||
@change="handleUrlChange"
|
||||
/>
|
||||
</a-input-group>
|
||||
</div>
|
||||
<div>
|
||||
<a-dropdown-button
|
||||
v-if="hasLocalExec"
|
||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
class="exec-btn"
|
||||
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||
@select="execute"
|
||||
>
|
||||
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
|
||||
<template #icon>
|
||||
<icon-down />
|
||||
</template>
|
||||
<template #content>
|
||||
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
|
||||
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
<a-button v-else-if="!requestVModel.executeLoading" type="primary" @click="() => execute('serverExec')">
|
||||
{{ t('apiTestDebug.serverExec') }}
|
||||
</a-button>
|
||||
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-input
|
||||
v-if="activeStep?.stepType && (_stepType.isCopyCase || _stepType.isQuoteCase) && isHttpProtocol"
|
||||
v-model:model-value="requestVModel.name"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
|
||||
:disabled="!isEditableApi"
|
||||
allow-clear
|
||||
class="mt-[8px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-[16px]">
|
||||
<MsTab
|
||||
v-model:active-key="requestVModel.activeTab"
|
||||
:content-tab-list="contentTabList"
|
||||
:get-text-func="getTabBadge"
|
||||
class="no-content relative mt-[8px] border-b"
|
||||
/>
|
||||
</div>
|
||||
<div ref="splitContainerRef" class="h-[calc(100%-87px)]">
|
||||
<MsSplitBox
|
||||
ref="verticalSplitBoxRef"
|
||||
v-model:size="splitBoxSize"
|
||||
:max="!showResponse ? 1 : 0.98"
|
||||
min="10px"
|
||||
:direction="activeLayout"
|
||||
second-container-class="!overflow-y-hidden"
|
||||
:class="!showResponse ? 'hidden-second' : 'show-second'"
|
||||
@expand-change="handleVerticalExpandChange"
|
||||
>
|
||||
<template #first>
|
||||
<a-spin class="block h-full w-full" :loading="requestVModel.executeLoading || loading">
|
||||
<div
|
||||
:class="`flex h-full min-w-[800px] flex-col p-[16px] ${
|
||||
activeLayout === 'horizontal' ? ' pr-[16px]' : ''
|
||||
}`"
|
||||
>
|
||||
<div class="tab-pane-container">
|
||||
<a-spin
|
||||
v-show="requestVModel.activeTab === RequestComposition.PLUGIN"
|
||||
:loading="pluginLoading"
|
||||
class="min-h-[100px] w-full"
|
||||
>
|
||||
<MsFormCreate
|
||||
v-model:api="fApi"
|
||||
:rule="currentPluginScript"
|
||||
:option="currentPluginOptions"
|
||||
@change="
|
||||
() => {
|
||||
if (isInitPluginForm) {
|
||||
handlePluginFormChange();
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
</a-spin>
|
||||
<httpHeader
|
||||
v-if="requestVModel.activeTab === RequestComposition.HEADER"
|
||||
v-model:params="requestVModel.headers"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:layout="activeLayout"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<httpBody
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.BODY"
|
||||
v-model:params="requestVModel.body"
|
||||
:layout="activeLayout"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
:upload-temp-file-api="uploadTempFileCase"
|
||||
:file-save-as-source-id="scenarioId"
|
||||
:file-save-as-api="transferFileCase"
|
||||
:file-module-options-api="getTransferOptionsCase"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<httpQuery
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
|
||||
v-model:params="requestVModel.query"
|
||||
:layout="activeLayout"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<httpRest
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.REST"
|
||||
v-model:params="requestVModel.rest"
|
||||
:layout="activeLayout"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<precondition
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION"
|
||||
v-model:config="requestVModel.children[0].preProcessorConfig"
|
||||
is-definition
|
||||
:disabled="!isEditableApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<postcondition
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION"
|
||||
v-model:config="requestVModel.children[0].postProcessorConfig"
|
||||
:response="props.stepResponses?.[requestVModel.stepId].responseResult.body"
|
||||
:layout="activeLayout"
|
||||
:disabled="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
is-definition
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<assertion
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION"
|
||||
v-model:params="requestVModel.children[0].assertionConfig.assertions"
|
||||
:response="props.stepResponses?.[requestVModel.stepId].responseResult.body"
|
||||
is-definition
|
||||
:disabled="!isEditableApi"
|
||||
:assertion-config="requestVModel.children[0].assertionConfig"
|
||||
/>
|
||||
<auth
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
|
||||
v-model:params="requestVModel.authConfig"
|
||||
:disabled="!isEditableApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<setting
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.SETTING"
|
||||
v-model:params="requestVModel.otherConfig"
|
||||
:disabled="!isEditableApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
<template #second>
|
||||
<response
|
||||
v-if="visible"
|
||||
v-show="showResponse"
|
||||
v-model:active-layout="activeLayout"
|
||||
v-model:active-tab="requestVModel.responseActiveTab"
|
||||
:is-http-protocol="isHttpProtocol"
|
||||
:is-priority-local-exec="isPriorityLocalExec"
|
||||
:request-url="requestVModel.url"
|
||||
:is-expanded="isVerticalExpanded"
|
||||
:request-result="props.stepResponses?.[requestVModel.stepId]"
|
||||
:console="props.stepResponses?.[requestVModel.stepId]?.console"
|
||||
:is-edit="false"
|
||||
is-definition
|
||||
:loading="requestVModel.executeLoading || loading"
|
||||
@change-expand="changeVerticalExpand"
|
||||
@change-layout="handleActiveLayoutChange"
|
||||
@execute="execute"
|
||||
/>
|
||||
</template>
|
||||
</MsSplitBox>
|
||||
</div>
|
||||
</div>
|
||||
<requestAndResponse
|
||||
ref="requestAndResponseRef"
|
||||
:detail-loading="loading"
|
||||
:disabled-except-param="isQuote"
|
||||
:disabled-param-value="isQuote"
|
||||
:request="requestVModel"
|
||||
:is-priority-local-exec="isPriorityLocalExec"
|
||||
:file-save-as-source-id="requestVModel.resourceId"
|
||||
:file-module-options-api="getTransferOptionsCase"
|
||||
:file-save-as-api="transferFileCase"
|
||||
:upload-temp-file="uploadTempFileCase"
|
||||
is-show-common-content-tab-key
|
||||
@execute="handleExecute"
|
||||
/>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { InputInstance } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { InputInstance, Message, SelectOptionData } from '@arco-design/web-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||
import assertion from '@/components/business/ms-assertion/index.vue';
|
||||
import stepType from './stepType/stepType.vue';
|
||||
import executeButton from '@/views/api-test/components/executeButton.vue';
|
||||
import requestAndResponse from '@/views/api-test/components/requestAndResponse.vue';
|
||||
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
|
||||
import auth from '@/views/api-test/components/requestComposition/auth.vue';
|
||||
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
|
||||
import precondition from '@/views/api-test/components/requestComposition/precondition.vue';
|
||||
import response from '@/views/api-test/components/requestComposition/response/index.vue';
|
||||
import setting from '@/views/api-test/components/requestComposition/setting.vue';
|
||||
import { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
|
||||
|
||||
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
|
||||
import {
|
||||
getCaseDetail,
|
||||
getTransferOptionsCase,
|
||||
transferFileCase,
|
||||
uploadTempFileCase,
|
||||
} from '@/api/modules/api-test/management';
|
||||
import { characterLimit } from '@/utils';
|
||||
import { useAppStore } from '@/store';
|
||||
import { characterLimit, parseQueryParams } from '@/utils';
|
||||
import { scrollIntoView } from '@/utils/dom';
|
||||
|
||||
import { RequestResult } from '@/models/apiTest/common';
|
||||
import { ExecuteConditionConfig, PluginConfig, RequestResult } from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import {
|
||||
RequestAuthType,
|
||||
RequestBodyFormat,
|
||||
RequestComposition,
|
||||
RequestConditionProcessor,
|
||||
RequestMethods,
|
||||
ResponseComposition,
|
||||
ScenarioStepRefType,
|
||||
ScenarioStepType,
|
||||
} from '@/enums/apiEnum';
|
||||
|
||||
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
|
||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
import getStepType from './stepType/utils';
|
||||
import {
|
||||
defaultBodyParams,
|
||||
defaultBodyParamsItem,
|
||||
defaultHeaderParamsItem,
|
||||
defaultKeyValueParamItem,
|
||||
defaultRequestParamsItem,
|
||||
defaultResponse,
|
||||
} from '@/views/api-test/components/config';
|
||||
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
import { Api } from '@form-create/arco-design';
|
||||
// 懒加载Http协议组件
|
||||
const httpHeader = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/header.vue'));
|
||||
const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue'));
|
||||
const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue'));
|
||||
const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue'));
|
||||
|
||||
const props = defineProps<{
|
||||
request?: RequestParam; // 请求参数集合
|
||||
|
@ -119,34 +349,34 @@
|
|||
(e: 'stopDebug'): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
const activeStep = defineModel<ScenarioStepItem>('activeStep', {
|
||||
required: false,
|
||||
});
|
||||
// 注入祖先组件提供的属性
|
||||
const scenarioId = inject<string | number>('scenarioId');
|
||||
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
|
||||
|
||||
const defaultCaseParams: RequestParam = {
|
||||
id: `case-${Date.now()}`,
|
||||
const defaultApiParams: RequestParam = {
|
||||
name: '',
|
||||
type: 'api',
|
||||
stepId: '',
|
||||
resourceId: '',
|
||||
type: 'case',
|
||||
moduleId: 'root',
|
||||
customizeRequestEnvEnable: false,
|
||||
protocol: 'HTTP',
|
||||
tags: [],
|
||||
description: '',
|
||||
priority: 'P0',
|
||||
url: '',
|
||||
activeTab: RequestComposition.HEADER,
|
||||
closable: true,
|
||||
method: RequestMethods.GET,
|
||||
unSaved: false,
|
||||
headers: [],
|
||||
body: cloneDeep(defaultBodyParams),
|
||||
query: [],
|
||||
rest: [],
|
||||
polymorphicName: '',
|
||||
name: '',
|
||||
path: '',
|
||||
projectId: '',
|
||||
uploadFileIds: [],
|
||||
linkFileIds: [],
|
||||
authConfig: {
|
||||
|
@ -184,17 +414,21 @@
|
|||
followRedirects: true,
|
||||
autoRedirects: false,
|
||||
},
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
response: cloneDeep(defaultResponse),
|
||||
responseDefinition: [cloneDeep(defaultResponseItem)],
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
isNew: true,
|
||||
unSaved: false,
|
||||
executeLoading: false,
|
||||
preDependency: [], // 前置依赖
|
||||
postDependency: [], // 后置依赖
|
||||
};
|
||||
|
||||
const requestVModel = ref<RequestParam>(props.request || cloneDeep(defaultCaseParams));
|
||||
const requestVModel = ref<RequestParam>(defaultApiParams);
|
||||
const _stepType = computed(() => {
|
||||
if (activeStep.value) {
|
||||
return getStepType(activeStep.value);
|
||||
}
|
||||
return {
|
||||
isCopyCase: false,
|
||||
isQuoteCase: false,
|
||||
};
|
||||
});
|
||||
const isCopyCase = computed(
|
||||
() =>
|
||||
activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.COPY
|
||||
|
@ -204,12 +438,7 @@
|
|||
() =>
|
||||
activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.REF
|
||||
);
|
||||
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
||||
|
||||
const stepName = ref(activeStep.value?.name);
|
||||
watchEffect(() => {
|
||||
stepName.value = activeStep.value?.name;
|
||||
});
|
||||
watch(
|
||||
() => props.stepResponses,
|
||||
(val) => {
|
||||
|
@ -222,9 +451,18 @@
|
|||
}
|
||||
);
|
||||
|
||||
const executeRef = ref<InstanceType<typeof executeButton>>();
|
||||
const requestAndResponseRef = ref<InstanceType<typeof requestAndResponse>>();
|
||||
const isPriorityLocalExec = computed(() => executeRef.value?.isPriorityLocalExec ?? false);
|
||||
// 复制 api 只要加载过一次后就会保存,所以 props.request 是不为空的
|
||||
const isEditableApi = computed(() => _stepType.value.isCopyCase);
|
||||
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
||||
const isInitPluginForm = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
function handleActiveDebugChange() {
|
||||
if (!loading.value || (!isHttpProtocol.value && isInitPluginForm.value)) {
|
||||
// 如果是因为加载详情触发的change则不需要标记为未保存;或者是插件协议的话需要等待表单初始化完毕
|
||||
requestVModel.value.unSaved = true;
|
||||
}
|
||||
}
|
||||
|
||||
const isShowEditStepNameInput = ref(false);
|
||||
const stepNameInputRef = ref<InputInstance>();
|
||||
|
@ -238,26 +476,468 @@
|
|||
isShowEditStepNameInput.value = false;
|
||||
}
|
||||
|
||||
// 请求内容公共tabKey
|
||||
const commonContentTabKey = [
|
||||
RequestComposition.PRECONDITION,
|
||||
RequestComposition.POST_CONDITION,
|
||||
RequestComposition.ASSERTION,
|
||||
];
|
||||
// 请求内容插件tab
|
||||
const pluginContentTab = [
|
||||
{
|
||||
value: RequestComposition.PLUGIN,
|
||||
label: t('apiTestDebug.pluginData'),
|
||||
},
|
||||
];
|
||||
// Http 请求的tab
|
||||
const httpContentTabList = [
|
||||
{
|
||||
value: RequestComposition.HEADER,
|
||||
label: t('apiTestDebug.header'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.BODY,
|
||||
label: t('apiTestDebug.body'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.QUERY,
|
||||
label: RequestComposition.QUERY,
|
||||
},
|
||||
{
|
||||
value: RequestComposition.REST,
|
||||
label: RequestComposition.REST,
|
||||
},
|
||||
{
|
||||
value: RequestComposition.PRECONDITION,
|
||||
label: t('apiTestDebug.prefix'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.POST_CONDITION,
|
||||
label: t('apiTestDebug.post'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.ASSERTION,
|
||||
label: t('apiTestDebug.assertion'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.AUTH,
|
||||
label: t('apiTestDebug.auth'),
|
||||
},
|
||||
{
|
||||
value: RequestComposition.SETTING,
|
||||
label: t('apiTestDebug.setting'),
|
||||
},
|
||||
];
|
||||
const headerNum = computed(
|
||||
() => filterKeyValParams(requestVModel.value?.headers ?? [], defaultHeaderParamsItem).validParams?.length
|
||||
);
|
||||
const restNum = computed(
|
||||
() => filterKeyValParams(requestVModel.value?.rest ?? [], defaultRequestParamsItem).validParams?.length
|
||||
);
|
||||
const queryNum = computed(
|
||||
() => filterKeyValParams(requestVModel.value?.query ?? [], defaultRequestParamsItem).validParams?.length
|
||||
);
|
||||
const preProcessorNum = computed(() => requestVModel.value.children[0].preProcessorConfig.processors.length);
|
||||
const postProcessorNum = computed(() => requestVModel.value.children[0].postProcessorConfig.processors.length);
|
||||
const assertionsNum = computed(() => requestVModel.value.children[0].assertionConfig.assertions.length);
|
||||
// 根据协议类型获取请求内容tab
|
||||
const contentTabList = computed(() => {
|
||||
// HTTP 协议 tabs
|
||||
if (isHttpProtocol.value) {
|
||||
// 引用API:请求头、query、rest、前后置、断言。如果没有数据直接隐藏tab
|
||||
if (!isEditableApi.value) {
|
||||
return httpContentTabList.filter(
|
||||
(item) =>
|
||||
!(!restNum.value && item.value === RequestComposition.REST) &&
|
||||
!(!queryNum.value && item.value === RequestComposition.QUERY) &&
|
||||
!(!headerNum.value && item.value === RequestComposition.HEADER) &&
|
||||
!(!preProcessorNum.value && item.value === RequestComposition.PRECONDITION) &&
|
||||
!(!postProcessorNum.value && item.value === RequestComposition.POST_CONDITION) &&
|
||||
!(!assertionsNum.value && item.value === RequestComposition.ASSERTION)
|
||||
);
|
||||
}
|
||||
return httpContentTabList;
|
||||
}
|
||||
if (!isEditableApi.value) {
|
||||
return [
|
||||
...pluginContentTab,
|
||||
...httpContentTabList
|
||||
.filter((e) => commonContentTabKey.includes(e.value))
|
||||
.filter(
|
||||
(item) =>
|
||||
!(!preProcessorNum.value && item.value === RequestComposition.PRECONDITION) &&
|
||||
!(!postProcessorNum.value && item.value === RequestComposition.POST_CONDITION) &&
|
||||
!(!assertionsNum.value && item.value === RequestComposition.ASSERTION)
|
||||
),
|
||||
];
|
||||
}
|
||||
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取 tab 的参数数量徽标
|
||||
*/
|
||||
function getTabBadge(tabKey: RequestComposition) {
|
||||
switch (tabKey) {
|
||||
case RequestComposition.HEADER:
|
||||
return `${headerNum.value > 0 ? headerNum.value : ''}`;
|
||||
case RequestComposition.BODY:
|
||||
return requestVModel.value.body?.bodyType !== RequestBodyFormat.NONE ? '1' : '';
|
||||
case RequestComposition.QUERY:
|
||||
return `${queryNum.value > 0 ? queryNum.value : ''}`;
|
||||
case RequestComposition.REST:
|
||||
return `${restNum.value > 0 ? restNum.value : ''}`;
|
||||
case RequestComposition.PRECONDITION:
|
||||
return `${preProcessorNum.value > 99 ? '99+' : preProcessorNum.value || ''}`;
|
||||
case RequestComposition.POST_CONDITION:
|
||||
return `${postProcessorNum.value > 99 ? '99+' : postProcessorNum.value || ''}`;
|
||||
case RequestComposition.ASSERTION:
|
||||
return `${assertionsNum.value > 99 ? '99+' : assertionsNum.value || ''}`;
|
||||
case RequestComposition.AUTH:
|
||||
return requestVModel.value.authConfig.authType !== RequestAuthType.NONE ? '1' : '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const protocolLoading = ref(false);
|
||||
const protocolOptions = ref<SelectOptionData[]>([]);
|
||||
|
||||
async function initProtocolList() {
|
||||
try {
|
||||
protocolLoading.value = true;
|
||||
const res = await getProtocolList(appStore.currentOrgId);
|
||||
protocolOptions.value = res.map((e) => ({
|
||||
label: e.protocol,
|
||||
value: e.protocol,
|
||||
polymorphicName: e.polymorphicName,
|
||||
pluginId: e.pluginId,
|
||||
}));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
protocolLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const hasLocalExec = ref(false); // 是否配置了api本地执行
|
||||
|
||||
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); // 存储初始化过后的插件配置
|
||||
const temporaryPluginFormMap: Record<string, any> = {}; // 缓存插件表单,避免切换传入的 API 数据导致动态表单数据丢失
|
||||
const pluginLoading = ref(false);
|
||||
const fApi = ref<Api>();
|
||||
const currentPluginOptions = computed<Record<string, any>>(
|
||||
() => pluginScriptMap.value[requestVModel.value.protocol]?.options || {}
|
||||
);
|
||||
const currentPluginScript = computed<Record<string, any>[]>(
|
||||
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
|
||||
);
|
||||
|
||||
// 处理插件表单输入框变化
|
||||
const handlePluginFormChange = debounce(() => {
|
||||
if (isEditableApi.value) {
|
||||
// 复制或者新建的时候需要缓存表单数据,引用的不能更改
|
||||
temporaryPluginFormMap[requestVModel.value.stepId] = fApi.value?.formData();
|
||||
}
|
||||
handleActiveDebugChange();
|
||||
}, 300);
|
||||
|
||||
/**
|
||||
* 控制插件表单字段显示
|
||||
*/
|
||||
function controlPluginFormFields() {
|
||||
const currentFormFields = fApi.value?.fields();
|
||||
let fields: string[] = [];
|
||||
if (requestVModel.value.customizeRequestEnvEnable) {
|
||||
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields || [];
|
||||
} else {
|
||||
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
|
||||
}
|
||||
// 确保fields展示完整
|
||||
fApi.value?.hidden(false, fields);
|
||||
if (currentFormFields && currentFormFields.length < fields.length) {
|
||||
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
|
||||
} else {
|
||||
// 隐藏多余的字段
|
||||
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插件表单数据
|
||||
*/
|
||||
function setPluginFormData() {
|
||||
const tempForm = temporaryPluginFormMap[requestVModel.value.stepId];
|
||||
if (tempForm || !requestVModel.value.isNew) {
|
||||
// 如果缓存的表单数据存在或者是编辑状态,则需要将之前的输入数据填充
|
||||
const formData = isEditableApi.value ? tempForm || requestVModel.value : requestVModel.value;
|
||||
nextTick(() => {
|
||||
if (fApi.value) {
|
||||
fApi.value.nextRefresh(() => {
|
||||
const form = {};
|
||||
controlPluginFormFields().forEach((key) => {
|
||||
form[key] = formData[key];
|
||||
});
|
||||
fApi.value?.setValue(cloneDeep(form));
|
||||
setTimeout(() => {
|
||||
// 初始化时赋值会触发表单数据变更,300ms 是为了与 handlePluginFormChange的防抖时间保持一致
|
||||
isInitPluginForm.value = true;
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
nextTick(() => {
|
||||
controlPluginFormFields();
|
||||
fApi.value?.clearValidateState();
|
||||
fApi.value?.resetFields();
|
||||
isInitPluginForm.value = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const pluginError = ref(false);
|
||||
|
||||
async function initPluginScript() {
|
||||
const pluginId = protocolOptions.value.find((e) => e.value === requestVModel.value.protocol)?.pluginId;
|
||||
if (!pluginId) {
|
||||
Message.warning(t('apiTestDebug.noPluginTip'));
|
||||
pluginError.value = true;
|
||||
return;
|
||||
}
|
||||
pluginError.value = false;
|
||||
isInitPluginForm.value = false;
|
||||
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
|
||||
setPluginFormData();
|
||||
// 已经初始化过
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pluginLoading.value = true;
|
||||
const res = await getPluginScript(pluginId);
|
||||
pluginScriptMap.value[requestVModel.value.protocol] = res;
|
||||
setPluginFormData();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
pluginLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理协议切换,非 HTTP 协议切换到插件协议时需要初始化插件表单
|
||||
*/
|
||||
function handleActiveDebugProtocolChange(val: string) {
|
||||
if (val !== 'HTTP') {
|
||||
requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
||||
initPluginScript();
|
||||
} else {
|
||||
requestVModel.value.activeTab = RequestComposition.HEADER;
|
||||
if (!Object.values(RequestMethods).includes(requestVModel.value.method)) {
|
||||
// 第三方插件协议切换到 HTTP 时,请求方法默认设置 GET
|
||||
requestVModel.value.method = RequestMethods.GET;
|
||||
}
|
||||
}
|
||||
handleActiveDebugChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理url输入框变化,解析成参数表格
|
||||
*/
|
||||
function handleUrlChange(val: string) {
|
||||
const params = parseQueryParams(val.trim());
|
||||
if (params.length > 0) {
|
||||
requestVModel.value.query = [
|
||||
...params.map((e, i) => ({
|
||||
id: (new Date().getTime() + i).toString(),
|
||||
...defaultRequestParamsItem,
|
||||
...e,
|
||||
})),
|
||||
cloneDeep(defaultRequestParamsItem),
|
||||
];
|
||||
requestVModel.value.activeTab = RequestComposition.QUERY;
|
||||
[requestVModel.value.url] = val.split('?');
|
||||
}
|
||||
handleActiveDebugChange();
|
||||
}
|
||||
|
||||
const showResponse = computed(
|
||||
() => isHttpProtocol.value || requestVModel.value.response?.requestResults[0]?.responseResult.responseCode
|
||||
);
|
||||
const splitBoxSize = ref<string | number>(!showResponse.value ? 1 : 0.6);
|
||||
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
|
||||
const splitContainerRef = ref<HTMLElement>();
|
||||
const secondBoxHeight = ref(0);
|
||||
|
||||
watch(
|
||||
() => splitBoxSize.value,
|
||||
debounce((val) => {
|
||||
// 动画 300ms
|
||||
if (splitContainerRef.value) {
|
||||
if (typeof val === 'string' && val.includes('px')) {
|
||||
val = Number(val.split('px')[0]);
|
||||
secondBoxHeight.value = splitContainerRef.value.clientHeight - val;
|
||||
} else {
|
||||
secondBoxHeight.value = splitContainerRef.value.clientHeight * (1 - val);
|
||||
}
|
||||
}
|
||||
}, 300),
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const verticalSplitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
|
||||
const isVerticalExpanded = ref(true);
|
||||
|
||||
function handleVerticalExpandChange(val: boolean) {
|
||||
isVerticalExpanded.value = val;
|
||||
}
|
||||
|
||||
function changeVerticalExpand(val: boolean) {
|
||||
isVerticalExpanded.value = val;
|
||||
if (val) {
|
||||
verticalSplitBoxRef.value?.expand(0.6);
|
||||
} else {
|
||||
verticalSplitBoxRef.value?.collapse(
|
||||
splitContainerRef.value ? `${splitContainerRef.value.clientHeight - 42}px` : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => showResponse.value,
|
||||
(val) => {
|
||||
nextTick(() => {
|
||||
if (val) {
|
||||
changeVerticalExpand(true);
|
||||
} else {
|
||||
isVerticalExpanded.value = false;
|
||||
verticalSplitBoxRef.value?.collapse(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
function handleActiveLayoutChange() {
|
||||
isVerticalExpanded.value = true;
|
||||
splitBoxSize.value = 0.6;
|
||||
verticalSplitBoxRef.value?.expand(0.6);
|
||||
}
|
||||
|
||||
function filterConditionsSqlValidParams(condition: ExecuteConditionConfig) {
|
||||
const conditionCopy = cloneDeep(condition);
|
||||
conditionCopy.processors = conditionCopy.processors.map((processor) => {
|
||||
if (processor.processorType === RequestConditionProcessor.SQL) {
|
||||
processor.extractParams = filterKeyValParams(
|
||||
processor.extractParams || [],
|
||||
defaultKeyValueParamItem
|
||||
).validParams;
|
||||
}
|
||||
return processor;
|
||||
});
|
||||
return conditionCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求参数
|
||||
* @param executeType 执行类型,执行时传入
|
||||
*/
|
||||
function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
|
||||
const isExecute = executeType === 'localExec' || executeType === 'serverExec';
|
||||
const polymorphicName = protocolOptions.value.find(
|
||||
(e) => e.value === requestVModel.value.protocol
|
||||
)?.polymorphicName; // 协议多态名称
|
||||
let parseRequestBodyResult;
|
||||
let requestParams;
|
||||
if (isHttpProtocol.value) {
|
||||
const { formDataBody, wwwFormBody } = requestVModel.value.body;
|
||||
const realFormDataBodyValues = filterKeyValParams(
|
||||
formDataBody.formValues,
|
||||
defaultBodyParamsItem,
|
||||
isExecute
|
||||
).validParams;
|
||||
const realWwwFormBodyValues = filterKeyValParams(
|
||||
wwwFormBody.formValues,
|
||||
defaultBodyParamsItem,
|
||||
isExecute
|
||||
).validParams;
|
||||
parseRequestBodyResult = parseRequestBodyFiles(
|
||||
requestVModel.value.body,
|
||||
requestVModel.value.uploadFileIds, // 外面解析详情的时候传入
|
||||
requestVModel.value.linkFileIds // 外面解析详情的时候传入
|
||||
);
|
||||
requestParams = {
|
||||
authConfig: requestVModel.value.authConfig,
|
||||
body: {
|
||||
...requestVModel.value.body,
|
||||
formDataBody: {
|
||||
formValues: realFormDataBodyValues,
|
||||
},
|
||||
wwwFormBody: {
|
||||
formValues: realWwwFormBodyValues,
|
||||
},
|
||||
},
|
||||
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
|
||||
otherConfig: requestVModel.value.otherConfig,
|
||||
path: requestVModel.value.url || requestVModel.value.path,
|
||||
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
|
||||
rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem, isExecute).validParams,
|
||||
url: requestVModel.value.url,
|
||||
polymorphicName,
|
||||
};
|
||||
} else {
|
||||
requestParams = {
|
||||
...fApi.value?.formData(),
|
||||
polymorphicName,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...requestParams,
|
||||
resourceId: requestVModel.value.resourceId,
|
||||
stepId: requestVModel.value.stepId,
|
||||
activeTab: requestVModel.value.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
|
||||
responseActiveTab: ResponseComposition.BODY,
|
||||
protocol: requestVModel.value.protocol,
|
||||
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
|
||||
name: requestVModel.value.name,
|
||||
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
|
||||
children: [
|
||||
{
|
||||
polymorphicName: 'MsCommonElement', // 协议多态名称,写死MsCommonElement
|
||||
assertionConfig: requestVModel.value.children[0].assertionConfig,
|
||||
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
|
||||
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
|
||||
},
|
||||
],
|
||||
executeLoading: isExecute,
|
||||
...parseRequestBodyResult,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行调试
|
||||
* @param val 执行类型
|
||||
*/
|
||||
async function handleExecute(executeType?: 'localExec' | 'serverExec') {
|
||||
async function execute(executeType?: 'localExec' | 'serverExec') {
|
||||
requestVModel.value.executeLoading = true;
|
||||
if (isHttpProtocol.value) {
|
||||
emit('execute', requestAndResponseRef.value?.makeRequestParams(executeType), executeType);
|
||||
emit('execute', makeRequestParams(executeType), executeType);
|
||||
} else {
|
||||
// 插件需要校验动态表单
|
||||
// fApi.value?.validate(async (valid) => {
|
||||
// if (valid === true) {
|
||||
// emit('execute', requestAndResponseRef.value?.makeRequestParams(executeType), executeType);
|
||||
// } else {
|
||||
// requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
||||
// nextTick(() => {
|
||||
// scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
fApi.value?.validate(async (valid) => {
|
||||
if (valid === true) {
|
||||
emit('execute', makeRequestParams(executeType), executeType);
|
||||
} else {
|
||||
requestVModel.value.activeTab = RequestComposition.PLUGIN;
|
||||
nextTick(() => {
|
||||
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,16 +946,20 @@
|
|||
}
|
||||
|
||||
function handleClose() {
|
||||
if (!isQuote.value) {
|
||||
emit('applyStep', { ...requestVModel.value, ...requestAndResponseRef.value?.makeRequestParams() });
|
||||
// 关闭时若不是创建行为则是编辑行为,需要触发 applyStep
|
||||
if (!requestVModel.value.isNew) {
|
||||
emit('applyStep', cloneDeep(makeRequestParams()));
|
||||
}
|
||||
}
|
||||
|
||||
const isUrlError = ref(false);
|
||||
// const showAddDependencyDrawer = ref(false);
|
||||
// const addDependencyMode = ref<'pre' | 'post'>('pre');
|
||||
|
||||
function handleDelete() {
|
||||
emit('deleteStep');
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
async function initQuoteCaseDetail() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
@ -297,10 +981,10 @@
|
|||
url: res.path,
|
||||
name: res.name, // request里面还有个name但是是null
|
||||
resourceId: res.id,
|
||||
stepId: activeStep.value?.id || '',
|
||||
...parseRequestBodyResult,
|
||||
};
|
||||
nextTick(() => {
|
||||
requestAndResponseRef.value?.setActiveTabByFirst();
|
||||
// 等待内容渲染出来再隐藏loading
|
||||
loading.value = false;
|
||||
});
|
||||
|
@ -315,14 +999,15 @@
|
|||
() => visible.value,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
requestVModel.value = {
|
||||
...cloneDeep(defaultCaseParams),
|
||||
if (protocolOptions.value.length === 0) {
|
||||
await initProtocolList();
|
||||
}
|
||||
// 查看自定义请求、引用 api、复制 api
|
||||
requestVModel.value = cloneDeep({
|
||||
...defaultApiParams,
|
||||
...props.request,
|
||||
response: {
|
||||
requestResults: [props.stepResponses?.[props.request?.stepId] || defaultResponse.requestResults[0]],
|
||||
console: props.stepResponses?.[props.request?.stepId]?.console || '',
|
||||
},
|
||||
};
|
||||
isNew: false,
|
||||
});
|
||||
if (isQuote.value || isCopyNeedInit.value) {
|
||||
// 引用时,需要初始化引用的详情;复制只在第一次初始化的时候需要加载后台数据(request.request是复制请求时列表参数字段request会为 null,以此判断释放第一次初始化)
|
||||
initQuoteCaseDetail();
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
import { getSystemRequest } from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getGenerateId, mapTree } from '@/utils';
|
||||
|
||||
import type { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||
import type { ApiScenarioTableItem } from '@/models/apiTest/scenario';
|
||||
|
@ -193,8 +194,13 @@
|
|||
visible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取复制或引用的步骤数据
|
||||
* @param refType 复制或引用
|
||||
*/
|
||||
async function getScenarioSteps(refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF) {
|
||||
const scenarioMap: Record<string, MsTableDataItem<ApiScenarioTableItem>[]> = {};
|
||||
// 可以跨项目选择,但是接口的项目 id 是单个,所以需要按项目分组
|
||||
selectedScenarios.value.forEach((e) => {
|
||||
if (!scenarioMap[e.projectId]) {
|
||||
scenarioMap[e.projectId] = [];
|
||||
|
@ -203,6 +209,7 @@
|
|||
});
|
||||
const scenarioRequestArr: any[] = [];
|
||||
Object.keys(scenarioMap).forEach((projectId) => {
|
||||
// 组装请求
|
||||
scenarioRequestArr.push(
|
||||
getSystemRequest({
|
||||
scenarioRequest: {
|
||||
|
@ -225,8 +232,15 @@
|
|||
fullScenarioArr = fullScenarioArr.map((e) => {
|
||||
return {
|
||||
...e,
|
||||
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
|
||||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
}),
|
||||
name: `copy-${e.name}`,
|
||||
copyFromStepId: e.id,
|
||||
copyFromStepId: e.resourceId,
|
||||
};
|
||||
});
|
||||
emit(
|
||||
|
@ -239,6 +253,21 @@
|
|||
);
|
||||
handleCancel();
|
||||
} else {
|
||||
fullScenarioArr = fullScenarioArr.map((e) => {
|
||||
return {
|
||||
...e,
|
||||
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
|
||||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
config: {
|
||||
isRefScenarioStep: true, // 默认是完全引用的
|
||||
},
|
||||
id: getGenerateId(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
emit(
|
||||
'quote',
|
||||
cloneDeep({
|
||||
|
|
|
@ -287,13 +287,18 @@
|
|||
if (tableSelectedKeys.value.includes(key)) {
|
||||
// 取消选中
|
||||
tableSelectedData.value = tableSelectedData.value.filter((e) => e.id !== key);
|
||||
}
|
||||
if (selectedData) {
|
||||
} else if (selectedData) {
|
||||
tableSelectedData.value.push(selectedData);
|
||||
}
|
||||
emit('select', tableSelectedData.value);
|
||||
}
|
||||
|
||||
function clearSelector() {
|
||||
tableSelectedData.value = [];
|
||||
currentTable.value.clearSelector();
|
||||
emit('select', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格全选事件处理
|
||||
*/
|
||||
|
@ -310,10 +315,13 @@
|
|||
// 绑定表格事件
|
||||
useApiTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
|
||||
useApiTable.propsEvent.value.selectAllChange = handleSelectAllChange;
|
||||
useApiTable.propsEvent.value.clearSelector = clearSelector;
|
||||
useCaseTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
|
||||
useCaseTable.propsEvent.value.selectAllChange = handleSelectAllChange;
|
||||
useCaseTable.propsEvent.value.clearSelector = clearSelector;
|
||||
useScenarioTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
|
||||
useScenarioTable.propsEvent.value.selectAllChange = handleSelectAllChange;
|
||||
useScenarioTable.propsEvent.value.clearSelector = clearSelector;
|
||||
|
||||
function loadPage(ids?: (string | number)[]) {
|
||||
nextTick(() => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Scenario } from '@/models/apiTest/scenario';
|
||||
import { Scenario, ScenarioStepConfig } from '@/models/apiTest/scenario';
|
||||
import {
|
||||
ApiScenarioStatus,
|
||||
RequestAssertionCondition,
|
||||
|
@ -51,6 +51,13 @@ export const defaultTimeController = {
|
|||
delay: 0, // 等待时间
|
||||
};
|
||||
|
||||
// 场景配置
|
||||
export const defaultScenarioStepConfig: ScenarioStepConfig = {
|
||||
enableScenarioEnv: false,
|
||||
useBothScenarioParam: false,
|
||||
useCurrentScenarioParam: true,
|
||||
};
|
||||
|
||||
export const defaultStepItemCommon = {
|
||||
checked: false,
|
||||
expanded: false,
|
||||
|
@ -62,6 +69,7 @@ export const defaultStepItemCommon = {
|
|||
id: '',
|
||||
name: '',
|
||||
enable: true,
|
||||
isRefScenarioStep: false,
|
||||
},
|
||||
createActionsVisible: false,
|
||||
responsePopoverVisible: false,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
|||
import {
|
||||
defaultConditionController,
|
||||
defaultLoopController,
|
||||
defaultScenarioStepConfig,
|
||||
defaultStepItemCommon,
|
||||
defaultTimeController,
|
||||
} from '../../config';
|
||||
|
@ -109,11 +110,13 @@ export default function useCreateActions() {
|
|||
config = cloneDeep(defaultConditionController);
|
||||
} else if (stepType === ScenarioStepType.CONSTANT_TIMER) {
|
||||
config = cloneDeep(defaultTimeController);
|
||||
} else if (stepType === ScenarioStepType.API_SCENARIO) {
|
||||
config = cloneDeep(defaultScenarioStepConfig);
|
||||
}
|
||||
if (item.id) {
|
||||
if (item.id || item.resourceId) {
|
||||
// 引用复制接口、用例、场景时的源资源信息
|
||||
resourceField = {
|
||||
resourceId: item.id,
|
||||
resourceId: item.id || item.resourceId,
|
||||
resourceNum: item.num,
|
||||
resourceName: item.name,
|
||||
};
|
||||
|
@ -132,6 +135,7 @@ export default function useCreateActions() {
|
|||
config: {
|
||||
...defaultStepItemCommon.config,
|
||||
...config,
|
||||
isRefScenarioStep: item.config?.isRefScenarioStep || false,
|
||||
},
|
||||
children: item.children || [],
|
||||
stepType,
|
||||
|
|
|
@ -217,7 +217,8 @@
|
|||
});
|
||||
} else {
|
||||
scenario.value.steps = mapTree(scenario.value.steps, (node) => {
|
||||
if (ids.has(node.id)) {
|
||||
if (ids.has(node.id) && node.config.isRefScenarioStep !== true) {
|
||||
// 如果是完全引用的场景下的子孙步骤,则不允许操作启用禁用
|
||||
node.enable = isBatchEnable.value;
|
||||
}
|
||||
return node;
|
||||
|
|
|
@ -58,8 +58,9 @@
|
|||
</div>
|
||||
</a-tooltip>
|
||||
<div class="mr-[8px] flex items-center gap-[8px]">
|
||||
<!-- 步骤启用/禁用 -->
|
||||
<!-- 步骤启用/禁用,完全引用的场景下的子孙步骤不可禁用 -->
|
||||
<a-switch
|
||||
v-show="step.config.isRefScenarioStep !== true"
|
||||
v-model:model-value="step.enable"
|
||||
size="small"
|
||||
@click.stop="handleStepToggleEnable(step)"
|
||||
|
@ -241,7 +242,7 @@
|
|||
:request="currentStepDetail"
|
||||
:step-responses="scenario.stepResponses"
|
||||
@apply-step="applyApiStep"
|
||||
@delete-step="deleteCaseStep"
|
||||
@delete-step="deleteCaseStep(activeStep)"
|
||||
@stop-debug="handleStopExecute(activeStep)"
|
||||
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
|
||||
/>
|
||||
|
@ -286,6 +287,85 @@
|
|||
</template>
|
||||
</MsCodeEditor>
|
||||
</a-modal>
|
||||
<a-modal
|
||||
v-model:visible="showScenarioConfig"
|
||||
:title="t('apiScenario.scenarioConfig')"
|
||||
:ok-text="t('common.confirm')"
|
||||
class="ms-modal-form"
|
||||
body-class="!overflow-hidden !p-0"
|
||||
:width="680"
|
||||
title-align="start"
|
||||
@ok="applyQuickInput"
|
||||
>
|
||||
<a-form :model="scenarioConfigForm" layout="vertical" class="ms-form">
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
{{ t('apiScenario.quoteMode') }}
|
||||
<a-tooltip position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
<template #content>
|
||||
<div>{{ t('apiScenario.fullQuoteTip') }}</div>
|
||||
<div>{{ t('apiScenario.stepQuoteTip') }}</div>
|
||||
</template>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<a-radio-group v-model:model-value="scenarioConfigForm.refType">
|
||||
<a-radio :value="ScenarioStepRefType.REF">{{ t('apiScenario.fullQuote') }}</a-radio>
|
||||
<a-radio :value="ScenarioStepRefType.PARTIAL_REF">{{ t('apiScenario.stepQuote') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('apiScenario.runRule')">
|
||||
<a-radio-group v-model:model-value="scenarioConfigForm.useCurrentScenarioParam" type="button">
|
||||
<a-radio :value="true">{{ t('apiScenario.currentScenario') }}</a-radio>
|
||||
<a-radio :value="false">{{ t('apiScenario.sourceScenario') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="" class="hidden-item">
|
||||
<a-radio-group v-model:model-value="scenarioConfigForm.useBothScenarioParam">
|
||||
<a-radio :value="false">{{ t('apiScenario.empty') }}</a-radio>
|
||||
<a-radio :value="true">{{ t('apiScenario.sourceScenarioEnv') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="" class="hidden-item !mb-0">
|
||||
<div class="flex items-center gap-[8px]">
|
||||
<a-checkbox v-model:model-value="scenarioConfigForm.enableScenarioEnv" class="ml-[6px]"></a-checkbox>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
{{ t('apiScenario.sourceScenarioEnv') }}
|
||||
<a-tooltip :content="t('apiScenario.sourceScenarioEnvTip')" position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="text-[var(--color-text-4)]">
|
||||
{{ t('apiScenario.valuePriority') }}
|
||||
</div>
|
||||
<div v-if="scenarioConfigForm.useCurrentScenarioParam" class="text-[var(--color-text-1)]">
|
||||
{{ t('apiScenario.currentScenarioAndNull') }}
|
||||
</div>
|
||||
<div v-else class="text-[var(--color-text-1)]">
|
||||
{{ t('apiScenario.sourceScenarioAndNull') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-[12px]">
|
||||
<a-button type="secondary" @click="cancelScenarioConfig">{{ t('common.cancel') }}</a-button>
|
||||
<a-button type="primary" @click="saveScenarioConfig">{{ t('common.confirm') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -327,7 +407,13 @@
|
|||
} from '@/utils';
|
||||
|
||||
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
|
||||
import { ApiScenarioDebugRequest, CreateStepAction, Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import {
|
||||
ApiScenarioDebugRequest,
|
||||
CreateStepAction,
|
||||
Scenario,
|
||||
ScenarioStepConfig,
|
||||
ScenarioStepItem,
|
||||
} from '@/models/apiTest/scenario';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import {
|
||||
ResponseComposition,
|
||||
|
@ -380,6 +466,7 @@
|
|||
const loading = ref(false);
|
||||
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||
const focusStepKey = ref<string | number>(''); // 聚焦的key
|
||||
const activeStep = ref<ScenarioStepItem>(); // 用于抽屉操作创建步骤、弹窗配置时记录当前操作的步骤节点
|
||||
|
||||
function setFocusNodeKey(id: string | number) {
|
||||
focusStepKey.value = id || '';
|
||||
|
@ -516,6 +603,53 @@
|
|||
return stepMoreActions;
|
||||
}
|
||||
|
||||
const scenarioConfigForm = ref<
|
||||
ScenarioStepConfig & {
|
||||
refType: ScenarioStepRefType;
|
||||
}
|
||||
>({
|
||||
refType: ScenarioStepRefType.REF,
|
||||
enableScenarioEnv: false,
|
||||
useBothScenarioParam: false,
|
||||
useCurrentScenarioParam: true,
|
||||
});
|
||||
const showScenarioConfig = ref(false);
|
||||
|
||||
function cancelScenarioConfig() {
|
||||
showScenarioConfig.value = false;
|
||||
scenarioConfigForm.value = {
|
||||
refType: ScenarioStepRefType.REF,
|
||||
enableScenarioEnv: false,
|
||||
useBothScenarioParam: false,
|
||||
useCurrentScenarioParam: true,
|
||||
};
|
||||
}
|
||||
|
||||
function saveScenarioConfig() {
|
||||
if (activeStep.value) {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.id, 'id');
|
||||
if (realStep) {
|
||||
realStep.refType = scenarioConfigForm.value.refType;
|
||||
realStep.config = {
|
||||
...realStep.config,
|
||||
...scenarioConfigForm.value,
|
||||
};
|
||||
realStep.children = mapTree<ScenarioStepItem>(realStep.children || [], (child) => {
|
||||
// 更新子孙步骤是否完全引用
|
||||
if (scenarioConfigForm.value.refType === ScenarioStepRefType.REF) {
|
||||
child.config.isRefScenarioStep = true;
|
||||
child.enable = true;
|
||||
} else {
|
||||
child.config.isRefScenarioStep = false;
|
||||
}
|
||||
return child;
|
||||
});
|
||||
Message.success(t('apiScenario.setSuccess'));
|
||||
cancelScenarioConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
|
||||
switch (item.eventTag) {
|
||||
case 'copy':
|
||||
|
@ -569,7 +703,12 @@
|
|||
);
|
||||
break;
|
||||
case 'config':
|
||||
console.log('config', node);
|
||||
activeStep.value = node as ScenarioStepItem;
|
||||
scenarioConfigForm.value = {
|
||||
refType: node.refType,
|
||||
...node.config,
|
||||
};
|
||||
showScenarioConfig.value = true;
|
||||
break;
|
||||
case 'delete':
|
||||
deleteNode(steps.value, node.id, 'id');
|
||||
|
@ -659,7 +798,6 @@
|
|||
const customCaseDrawerVisible = ref(false);
|
||||
const customApiDrawerVisible = ref(false);
|
||||
const scriptOperationDrawerVisible = ref(false);
|
||||
const activeStep = ref<ScenarioStepItem>(); // 用于抽屉操作创建步骤时记录当前操作的步骤节点
|
||||
const activeCreateAction = ref<CreateStepAction>(); // 用于抽屉操作创建步骤时记录当前插入类型
|
||||
const currentStepDetail = computed<any>(() => {
|
||||
// TODO: 步骤详情类型
|
||||
|
@ -1008,7 +1146,11 @@
|
|||
*/
|
||||
function addCustomApiStep(request: RequestParam) {
|
||||
request.isNew = false;
|
||||
stepDetails.value[request.stepId] = request;
|
||||
stepDetails.value[request.stepId] = {
|
||||
...request,
|
||||
customizeRequest: true,
|
||||
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
|
||||
};
|
||||
emit('updateResource', request.uploadFileIds, request.linkFileIds);
|
||||
if (activeStep.value && activeCreateAction.value) {
|
||||
handleCreateStep(
|
||||
|
@ -1028,8 +1170,6 @@
|
|||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
config: {
|
||||
customizeRequest: true,
|
||||
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
|
||||
protocol: request.protocol,
|
||||
method: request.method,
|
||||
},
|
||||
|
@ -1058,11 +1198,10 @@
|
|||
/**
|
||||
* 删除
|
||||
*/
|
||||
function deleteCaseStep() {
|
||||
if (activeStep.value) {
|
||||
function deleteCaseStep(step?: ScenarioStepItem) {
|
||||
if (step) {
|
||||
customCaseDrawerVisible.value = false;
|
||||
steps.value = steps.value.filter((item) => item.id !== activeStep.value?.id);
|
||||
delete stepDetails.value[activeStep.value?.id];
|
||||
deleteNode(steps.value, step.id, 'id');
|
||||
activeStep.value = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -1103,11 +1242,16 @@
|
|||
* @param dropNode 释放节点
|
||||
*/
|
||||
function isAllowDropInside(dropNode: MsTreeNodeData) {
|
||||
return [
|
||||
ScenarioStepType.LOOP_CONTROLLER,
|
||||
ScenarioStepType.IF_CONTROLLER,
|
||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||
].includes(dropNode.stepType);
|
||||
return (
|
||||
// 逻辑控制器内可以拖拽任意类型的步骤
|
||||
[
|
||||
ScenarioStepType.LOOP_CONTROLLER,
|
||||
ScenarioStepType.IF_CONTROLLER,
|
||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||
].includes(dropNode.stepType) ||
|
||||
// 复制的场景内可以释放任意类型的步骤
|
||||
(dropNode.stepType === ScenarioStepType.API_SCENARIO && dropNode.refType === ScenarioStepRefType.COPY)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1359,4 +1503,10 @@
|
|||
@apply !visible !w-auto;
|
||||
}
|
||||
}
|
||||
.ms-form {
|
||||
:deep(.arco-form-item-wrapper-col),
|
||||
:deep(.arco-form-item-content) {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -119,10 +119,11 @@
|
|||
ApiScenarioGetModuleParams,
|
||||
ApiScenarioTableItem,
|
||||
Scenario,
|
||||
ScenarioStepItem,
|
||||
} from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import { defaultScenario } from './components/config';
|
||||
|
|
|
@ -139,6 +139,21 @@ export default {
|
|||
'apiScenario.running': '执行中',
|
||||
'apiScenario.unExecute': '未执行',
|
||||
'apiScenario.response': '响应内容',
|
||||
'apiScenario.quoteMode': '引用模式',
|
||||
'apiScenario.fullQuote': '完全引用',
|
||||
'apiScenario.stepQuote': '步骤引用',
|
||||
'apiScenario.runRule': '运行取值规则设置',
|
||||
'apiScenario.currentScenario': '当前场景参数',
|
||||
'apiScenario.sourceScenario': '源场景参数',
|
||||
'apiScenario.empty': '空值',
|
||||
'apiScenario.sourceScenarioEnv': '源场景环境',
|
||||
'apiScenario.valuePriority': '取值优先级:',
|
||||
'apiScenario.currentScenarioAndNull': '步骤参数>场景参数>环境参数>空值',
|
||||
'apiScenario.sourceScenarioAndNull': '源场景参数>源场景环境>步骤参数>当前场景参数>当前环境参数',
|
||||
'apiScenario.fullQuoteTip': '完全引用:跟随源步骤内容及步骤状态变化,步骤状态不可调整',
|
||||
'apiScenario.stepQuoteTip': '步骤引用:仅跟随源步骤内容变化,步骤状态可调整',
|
||||
'apiScenario.sourceScenarioEnvTip': '运行环境,含环境参数',
|
||||
'apiScenario.setSuccess': '设置成功',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||
'apiScenario.executeHistory.num': '序号',
|
||||
|
|
|
@ -167,10 +167,7 @@
|
|||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="t('system.config.email.from')"
|
||||
field="from" asterisk-position="end"
|
||||
:rules="[emailRule]">
|
||||
<a-form-item :label="t('system.config.email.from')" field="from" asterisk-position="end" :rules="[emailRule]">
|
||||
<a-input
|
||||
v-model:model-value="emailConfigForm.from"
|
||||
:max-length="255"
|
||||
|
|
Loading…
Reference in New Issue