feat(接口场景): 脚本操作&api、case 文件处理&交互优化

This commit is contained in:
baiqi 2024-03-29 18:00:45 +08:00 committed by Craftsman
parent 7048ec91e2
commit 56ad892491
30 changed files with 390 additions and 244 deletions

View File

@ -49,7 +49,7 @@
"@tiptap/vue-3": "^2.1.13", "@tiptap/vue-3": "^2.1.13",
"@types/color": "^3.0.4", "@types/color": "^3.0.4",
"@types/node": "^20.11.16", "@types/node": "^20.11.16",
"@vueuse/core": "^10.7.2", "@vueuse/core": "^10.9.0",
"@xmldom/xmldom": "^0.8.10", "@xmldom/xmldom": "^0.8.10",
"ace-builds": "^1.24.2", "ace-builds": "^1.24.2",
"ahooks-vue": "^0.15.1", "ahooks-vue": "^0.15.1",

View File

@ -53,7 +53,7 @@
onBeforeMount(async () => { onBeforeMount(async () => {
try { try {
await appStore.initSystemVersion(); // appStore.initSystemVersion(); //
// license // license
if (appStore.packageType === 'enterprise') { if (appStore.packageType === 'enterprise') {
licenseStore.getValidateLicense(); licenseStore.getValidateLicense();
@ -115,7 +115,7 @@
setLocalStorage('salt', publicKey); setLocalStorage('salt', publicKey);
}; };
onMounted(async () => { onBeforeMount(async () => {
await getPublicKey(); await getPublicKey();
if (WHITE_LIST.find((el) => el.path === window.location.hash.split('#')[1]) === undefined) { if (WHITE_LIST.find((el) => el.path === window.location.hash.split('#')[1]) === undefined) {
await checkIsLogin(); await checkIsLogin();

View File

@ -35,6 +35,7 @@ import {
UpdateScenarioUrl, UpdateScenarioUrl,
} from '@/api/requrls/api-test/scenario'; } from '@/api/requrls/api-test/scenario';
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { import {
ApiScenarioBatchDeleteParams, ApiScenarioBatchDeleteParams,
ApiScenarioBatchEditParams, ApiScenarioBatchEditParams,
@ -55,6 +56,9 @@ import {
} from '@/models/apiTest/scenario'; } from '@/models/apiTest/scenario';
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common'; import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common';
import type { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import type { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
// 更新模块 // 更新模块
export function updateModule(data: ApiScenarioModuleUpdateParams) { export function updateModule(data: ApiScenarioModuleUpdateParams) {
return MSR.post({ url: UpdateModuleUrl, data }); return MSR.post({ url: UpdateModuleUrl, data });
@ -207,7 +211,10 @@ export function getScenarioDetail(id: string) {
// 获取场景步骤详情 // 获取场景步骤详情
export function getScenarioStep(stepId: string | number) { export function getScenarioStep(stepId: string | number) {
return MSR.get({ url: GetScenarioStepUrl, params: stepId }); return MSR.get<Partial<RequestParam & CaseRequestParam & ExecuteConditionProcessor>>({
url: GetScenarioStepUrl,
params: stepId,
});
} }
// 文件转存 // 文件转存

View File

@ -68,31 +68,31 @@
arrow-class="hidden" arrow-class="hidden"
:popup-offset="0" :popup-offset="0"
> >
<MsTagsInput <div class="!w-[calc(100%-28px)]">
v-model:model-value="inputFiles" <MsTagsInput
:disabled="props.disabled" v-model:model-value="inputFiles"
:input-class="props.inputClass" :input-class="props.inputClass"
placeholder=" " placeholder=" "
:max-tag-count="1" :max-tag-count="1"
:size="props.inputSize" :size="props.inputSize"
readonly readonly
class="!w-[calc(100%-28px)]" >
> <template v-if="alreadyDeleteFiles.length > 0" #prefix>
<template v-if="alreadyDeleteFiles.length > 0" #prefix> <icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" /> </template>
</template> <template #tag="{ data }">
<template #tag="{ data }"> <MsTag
<MsTag :size="props.tagSize"
:size="props.tagSize" class="m-0 border-none p-0"
class="m-0 border-none p-0" :self-style="{ backgroundColor: 'transparent !important' }"
:self-style="{ backgroundColor: 'transparent !important' }" :closable="data.value !== '__arco__more'"
:closable="data.value !== '__arco__more'" @close="handleClose(data)"
@close="handleClose(data)" >
> {{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }} </MsTag>
</MsTag> </template>
</template> </MsTagsInput>
</MsTagsInput> </div>
<template #content> <template #content>
<div class="flex w-[200px] flex-col gap-[8px]"> <div class="flex w-[200px] flex-col gap-[8px]">
<template v-if="alreadyDeleteFiles.length > 0"> <template v-if="alreadyDeleteFiles.length > 0">
@ -379,6 +379,14 @@
function handleSaveFileFinish(fileId: string) { function handleSaveFileFinish(fileId: string) {
if (savingFile.value) { if (savingFile.value) {
inputFiles.value = inputFiles.value.map((e) => {
if (e.value === savingFile.value?.fileId || e.value === savingFile.value?.uid) {
// uidfileId uidfileId
e.value = fileId;
e.local = false;
}
return e;
});
savingFile.value.fileId = fileId; savingFile.value.fileId = fileId;
savingFile.value.local = false; savingFile.value.local = false;
} }

View File

@ -23,12 +23,12 @@
<div class="relative w-full"> <div class="relative w-full">
<MsCodeEditor <MsCodeEditor
ref="codeEditorRef" ref="codeEditorRef"
v-model:model-value="innerCodeValue" v-model:model-value="code"
title="" title=""
:width="expandMenu ? '100%' : '68%'" :width="expandMenu ? '100%' : '68%'"
height="460px" height="460px"
theme="vs" theme="vs"
:language="innerLanguagesType" :language="language"
:read-only="props.disabled" :read-only="props.disabled"
:show-full-screen="false" :show-full-screen="false"
:show-theme-change="false" :show-theme-change="false"
@ -36,7 +36,7 @@
<template #rightBox> <template #rightBox>
<MsScriptMenu <MsScriptMenu
v-model:expand="expandMenu" v-model:expand="expandMenu"
v-model:languagesType="innerLanguagesType" v-model:languagesType="language"
:disabled="props.disabled" :disabled="props.disabled"
@insert="insertHandler" @insert="insertHandler"
@form-api-import="formApiImport" @form-api-import="formApiImport"
@ -48,7 +48,7 @@
</div> </div>
<MsCodeEditor <MsCodeEditor
v-else v-else
v-model:model-value="executionResultValue" v-model:model-value="executionResult"
title="" title=""
width="100%" width="100%"
height="calc(100vh - 155px)" height="calc(100vh - 155px)"
@ -60,7 +60,7 @@
/> />
<InsertCommonScript <InsertCommonScript
v-model:visible="showInsertDrawer" v-model:visible="showInsertDrawer"
:script-language="innerLanguagesType" :script-language="language"
:enable-radio-selected="props.enableRadioSelected" :enable-radio-selected="props.enableRadioSelected"
@save="saveHandler" @save="saveHandler"
/> />
@ -73,7 +73,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue'; import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { Language, LanguageEnum } from '@/components/pure/ms-code-editor/types'; import { Language, LanguageEnum } from '@/components/pure/ms-code-editor/types';
@ -93,43 +92,27 @@
defineProps<{ defineProps<{
showType: 'commonScript' | 'executionResult'; // showType: 'commonScript' | 'executionResult'; //
disabled?: boolean; disabled?: boolean;
language: Language;
code: string;
enableRadioSelected?: boolean; enableRadioSelected?: boolean;
executionResult?: string; //
showHeader?: boolean; showHeader?: boolean;
}>(), }>(),
{ {
showHeader: true, showHeader: true,
} }
); );
const emit = defineEmits<{
(e: 'update:language', value: Language): void;
(e: 'update:code', value: string): void;
}>();
const { t } = useI18n(); const { t } = useI18n();
const projectId = ref<string>(appStore.currentProjectId); const projectId = ref<string>(appStore.currentProjectId);
const innerLanguagesType = useVModel(props, 'language', emit); const language = defineModel<Language>('language', {
const executionResultValue = useVModel(props, 'executionResult', emit); required: true,
});
watch( const executionResult = defineModel<string>('executionResult', {
() => innerLanguagesType.value, default: '',
(val) => { });
emit('update:language', val); const code = defineModel<string>('code', {
} required: true,
); });
const innerCodeValue = useVModel(props, 'code', emit);
watch(
() => props.code,
(val) => {
innerCodeValue.value = val;
}
);
const expandMenu = ref<boolean>(false); const expandMenu = ref<boolean>(false);
@ -153,8 +136,8 @@
const confirmLoading = ref<boolean>(false); const confirmLoading = ref<boolean>(false);
function insertHandler(code: string) { function insertHandler(_code: string) {
codeEditorRef.value?.insertContent(code); codeEditorRef.value?.insertContent(_code);
} }
function saveHandler(data: CommonScriptItem[]) { function saveHandler(data: CommonScriptItem[]) {
@ -185,7 +168,7 @@ ${item.script}
} }
function clearCode() { function clearCode() {
innerCodeValue.value = ''; code.value = '';
} }
defineExpose({ defineExpose({
@ -193,7 +176,7 @@ ${item.script}
insertHandler, insertHandler,
undoHandler, undoHandler,
clearCode, clearCode,
executionResultValue, executionResult,
}); });
</script> </script>

View File

@ -90,6 +90,7 @@
v-permission="props.okPermission || []" v-permission="props.okPermission || []"
type="secondary" type="secondary"
:loading="props.okLoading" :loading="props.okLoading"
:disabled="okDisabled"
@click="handleContinue" @click="handleContinue"
> >
{{ t(props.saveContinueText || 'ms.drawer.saveContinue') }} {{ t(props.saveContinueText || 'ms.drawer.saveContinue') }}

View File

@ -16,6 +16,7 @@
@press-enter="tagInputEnter" @press-enter="tagInputEnter"
@blur="tagInputBlur" @blur="tagInputBlur"
@clear="emit('clear')" @clear="emit('clear')"
@click="emit('click')"
> >
<template v-if="$slots.prefix" #prefix> <template v-if="$slots.prefix" #prefix>
<slot name="prefix"></slot> <slot name="prefix"></slot>
@ -66,7 +67,7 @@
size: 'medium', size: 'medium',
} }
); );
const emit = defineEmits(['update:modelValue', 'update:inputValue', 'change', 'clear', 'blur']); const emit = defineEmits(['update:modelValue', 'update:inputValue', 'change', 'clear', 'blur', 'click']);
const { t } = useI18n(); const { t } = useI18n();

View File

@ -11,12 +11,17 @@ export default function useOpenNewPage() {
const appStore = useAppStore(); const appStore = useAppStore();
const router = useRouter(); const router = useRouter();
function openNewPage(name: RouteRecordName | undefined, query = {}) { function openNewPage(name: RouteRecordName | undefined, query: Record<string, any> = {}) {
const pId = query.pId || appStore.currentProjectId;
if (pId) {
// 如果传入参数指定了项目 id则使用传入的项目 id
delete query.pId;
}
const queryParams = new URLSearchParams(query).toString(); const queryParams = new URLSearchParams(query).toString();
window.open( window.open(
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${ `${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${
appStore.currentProjectId appStore.currentOrgId
}&${queryParams}`, }&pId=${pId}&${queryParams}`,
'_blank' '_blank'
); );
} }

View File

@ -21,9 +21,12 @@ import {
ExecuteApiRequestFullParams, ExecuteApiRequestFullParams,
ExecuteAssertionItem, ExecuteAssertionItem,
ExecuteConditionConfig, ExecuteConditionConfig,
ExecuteConditionProcessor,
RequestResult, RequestResult,
ResponseDefinition, ResponseDefinition,
} from './common'; } from './common';
import type { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import type { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
// 场景-更新模块参数 // 场景-更新模块参数
export interface ApiScenarioModuleUpdateParams { export interface ApiScenarioModuleUpdateParams {
@ -190,7 +193,7 @@ export interface ScenarioHistoryItem {
createUserName: string; createUserName: string;
versionName: string; versionName: string;
} }
// 场景步骤-自定义请求
export type CustomApiStep = ExecuteApiRequestFullParams & { export type CustomApiStep = ExecuteApiRequestFullParams & {
protocol: string; protocol: string;
activeTab: RequestComposition; activeTab: RequestComposition;
@ -241,6 +244,7 @@ export interface Variable {
commonVariables: CommonVariable[]; commonVariables: CommonVariable[];
csvVariables: CsvVariable[]; csvVariables: CsvVariable[];
} }
// 场景配置
export interface ScenarioConfig { export interface ScenarioConfig {
variable: Variable; variable: Variable;
preProcessorConfig: ExecuteConditionConfig; preProcessorConfig: ExecuteConditionConfig;
@ -298,11 +302,13 @@ export interface LoopStepDetail extends StepDetailsCommon {
msCountController: CountController; msCountController: CountController;
whileController: WhileController; whileController: WhileController;
} }
// 场景引用配置
export interface ScenarioStepConfig { export interface ScenarioStepConfig {
useCurrentScenarioParam: boolean; // 是否优先使用当前场景参数 useCurrentScenarioParam: boolean; // 是否优先使用当前场景参数
useBothScenarioParam: boolean; // 是否当前场景参数和源场景参数都应用(勾选非空值时为 true useBothScenarioParam: boolean; // 是否当前场景参数和源场景参数都应用(勾选非空值时为 true
enableScenarioEnv: boolean; // 是否应用源场景环境 enableScenarioEnv: boolean; // 是否应用源场景环境
} }
// 场景步骤详情
export type ScenarioStepDetail = Partial< export type ScenarioStepDetail = Partial<
CustomApiStepDetail & CustomApiStepDetail &
ConditionStepDetail & ConditionStepDetail &
@ -312,14 +318,16 @@ export type ScenarioStepDetail = Partial<
method: RequestMethods; method: RequestMethods;
} }
>; >;
// 场景步骤项
export interface ScenarioStepItem { export interface ScenarioStepItem {
id: string | number; id: string | number;
sort: number; sort: number;
name: string; name: string;
enable: boolean; // 是否启用 enable: boolean; // 是否启用
copyFromStepId?: string; // 如果步骤是复制的这个字段是复制的步骤id如果复制的步骤也是复制的并且没有加载过详情则这个 id 是最原始的 被复制的步骤 id copyFromStepId?: string; // 如果步骤是复制的这个字段是复制的步骤id如果复制的步骤也是复制的并且没有加载过详情则这个 id 是最原始的 被复制的步骤 id
resourceId?: string; // 详情或者引用的类型才有 resourceId?: string; // 详情或者引用、复制的类型才有
resourceNum?: string; // 详情或者引用的类型才有 resourceNum?: string; // 详情或者引用、复制的类型才有
originProjectId?: string; // 如果步骤是复制的这个字段是复制的资源的所在的项目id
stepType: ScenarioStepType; stepType: ScenarioStepType;
refType: ScenarioStepRefType; refType: ScenarioStepRefType;
config: ScenarioStepDetail; // 存储步骤列表需要展示的信息 config: ScenarioStepDetail; // 存储步骤列表需要展示的信息
@ -342,6 +350,15 @@ export interface ScenarioStepItem {
isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值 isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用 isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
} }
// 场景步骤文件参数
export interface ScenarioStepFileParams {
uploadFileIds: string[];
linkFileIds: string[];
deleteFileIds?: string[];
unLinkFileIds?: string[];
}
// 场景步骤详情
export type ScenarioStepDetails = RequestParam | CaseRequestParam | ExecuteConditionProcessor;
// 场景 // 场景
export interface Scenario { export interface Scenario {
id?: string | number; id?: string | number;
@ -357,7 +374,8 @@ export interface Scenario {
environmentId?: string; environmentId?: string;
scenarioConfig: ScenarioConfig; scenarioConfig: ScenarioConfig;
steps: ScenarioStepItem[]; steps: ScenarioStepItem[];
stepDetails: Record<string, ScenarioStepDetail>; stepDetails: Record<string, ScenarioStepDetails>; // case、api、脚本操作抽屉的详情结构
stepFileParam: Record<string, ScenarioStepFileParams>;
follow?: boolean; follow?: boolean;
uploadFileIds: string[]; uploadFileIds: string[];
linkFileIds: string[]; linkFileIds: string[];
@ -375,6 +393,7 @@ export interface Scenario {
isExecute?: boolean; // 是否从列表执行进去场景详情 isExecute?: boolean; // 是否从列表执行进去场景详情
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤 isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤
} }
// 场景详情
export interface ScenarioDetail extends Scenario { export interface ScenarioDetail extends Scenario {
stepTotal: number; stepTotal: number;
requestPassRate: string; requestPassRate: string;
@ -390,7 +409,7 @@ export interface ScenarioDetail extends Scenario {
updateTime: number; updateTime: number;
updateUser: string; updateUser: string;
} }
// 场景-执行请求参数
export interface ApiScenarioDebugRequest { export interface ApiScenarioDebugRequest {
id: string | number; // 场景 id id: string | number; // 场景 id
grouped: boolean; grouped: boolean;
@ -420,7 +439,7 @@ export interface ApiScenarioUpdateDTO extends Partial<Scenario> {
deleteFileIds?: string[]; deleteFileIds?: string[];
unLinkFileIds?: string[]; unLinkFileIds?: string[];
} }
// 导入系统请求时获取步骤列表的分组入参
export interface GetSystemRequestTypeParams { export interface GetSystemRequestTypeParams {
moduleIds?: (string | number)[]; moduleIds?: (string | number)[];
selectedIds: (string | number)[]; selectedIds: (string | number)[];
@ -429,7 +448,7 @@ export interface GetSystemRequestTypeParams {
protocol?: string; protocol?: string;
versionId?: string; versionId?: string;
} }
// 导入系统请求时获取步骤列表的入参
export interface GetSystemRequestParams { export interface GetSystemRequestParams {
apiRequest?: GetSystemRequestTypeParams; apiRequest?: GetSystemRequestTypeParams;
caseRequest?: GetSystemRequestTypeParams; caseRequest?: GetSystemRequestTypeParams;

View File

@ -1,4 +1,3 @@
import { useAppStore } from '@/store';
import { clearToken, hasToken, isLoginExpires } from '@/utils/auth'; import { clearToken, hasToken, isLoginExpires } from '@/utils/auth';
import NProgress from 'nprogress'; // progress bar import NProgress from 'nprogress'; // progress bar

View File

@ -2,8 +2,6 @@ import { defineStore } from 'pinia';
import { getLicenseInfo } from '@/api/modules/setting/authorizedManagement'; import { getLicenseInfo } from '@/api/modules/setting/authorizedManagement';
import useAppStore from '../app';
const useLicenseStore = defineStore('license', { const useLicenseStore = defineStore('license', {
persist: true, persist: true,
state: (): { status: string | null } => ({ state: (): { status: string | null } => ({
@ -21,7 +19,6 @@ const useLicenseStore = defineStore('license', {
}, },
// license校验 // license校验
async getValidateLicense() { async getValidateLicense() {
const appStore = useAppStore();
try { try {
const result = await getLicenseInfo(); const result = await getLicenseInfo();
if (!result || !result.status || !result.license || !result.license.count) { if (!result || !result.status || !result.license || !result.license.count) {

View File

@ -59,8 +59,8 @@ const useUserStore = defineStore('user', {
} { } {
const appStore = useAppStore(); const appStore = useAppStore();
state.userRoleRelations?.forEach(ug => { state.userRoleRelations?.forEach((ug) => {
state.userRolePermissions?.forEach(gp => { state.userRolePermissions?.forEach((gp) => {
if (gp.userRole.id === ug.roleId) { if (gp.userRole.id === ug.roleId) {
ug.userRolePermissions = gp.userRolePermissions; ug.userRolePermissions = gp.userRolePermissions;
ug.userRole = gp.userRole; ug.userRole = gp.userRole;
@ -151,13 +151,8 @@ const useUserStore = defineStore('user', {
setToken(res.sessionId, res.csrfToken); setToken(res.sessionId, res.csrfToken);
this.setInfo(res); this.setInfo(res);
const { orgId, pId } = getHashParameters(); const { orgId, pId } = getHashParameters();
// 如果访问页面的时候携带了组织 ID和项目 ID则不设置 appStore.setCurrentOrgId(forceSet ? res.lastOrganizationId || '' : orgId || '');
if (!orgId || forceSet) { appStore.setCurrentProjectId(forceSet ? res.lastProjectId || '' : pId || '');
appStore.setCurrentOrgId(res.lastOrganizationId || '');
}
if (!pId || forceSet) {
appStore.setCurrentProjectId(res.lastProjectId || '');
}
return true; return true;
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -86,7 +86,7 @@
</div> </div>
<div class="flex items-center justify-between px-[12px] pt-[12px]"> <div class="flex items-center justify-between px-[12px] pt-[12px]">
<div class="flex items-center"> <div class="flex items-center">
<a-tooltip :content="condition.name"> <a-tooltip v-if="condition.name" :content="condition.name">
<div class="script-name-container"> <div class="script-name-container">
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]"> <div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ condition.name }} {{ condition.name }}

View File

@ -72,6 +72,7 @@
isPriorityLocalExec, isPriorityLocalExec,
execute, execute,
localExecuteUrl, localExecuteUrl,
hasLocalExec,
}); });
</script> </script>

View File

@ -279,6 +279,9 @@
gap: 12px; gap: 12px;
} }
.code-container { .code-container {
@apply overflow-y-auto;
.ms-scroll-bar();
padding: 12px; padding: 12px;
max-height: 400px; max-height: 400px;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);

View File

@ -778,21 +778,24 @@
emitChange('addTableLine addLineDisabled', isInit); emitChange('addTableLine addLineDisabled', isInit);
return; return;
} }
if ( if (rowIndex === paramsData.value.length - 1) {
rowIndex === paramsData.value.length - 1 && // Don't change this!!!
(paramsData.value[rowIndex].key ||
paramsData.value[rowIndex].projectId ||
paramsData.value[rowIndex].header ||
paramsData.value[rowIndex].variableName ||
paramsData.value[rowIndex].expression)
) {
// //
const id = new Date().getTime().toString(); const id = new Date().getTime().toString();
paramsData.value.push({ const lastLineData = paramsData.value[rowIndex]; //
const selectColumnKeys = props.columns.filter((e) => e.typeOptions).map((e) => e.dataIndex); //
const nextLine = {
id, id,
...cloneDeep(props.defaultParamItem), // ...cloneDeep(props.defaultParamItem), //
enable: true, // enable: true, //
} as any); } as any;
selectColumnKeys.forEach((key) => {
// 便
if (key) {
nextLine[key] = lastLineData[key];
}
});
paramsData.value.push(nextLine);
} }
emitChange('addTableLine', isInit); emitChange('addTableLine', isInit);
handleMustContainColChange(true); handleMustContainColChange(true);

View File

@ -108,7 +108,6 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import caseTable from '../case/caseTable.vue'; import caseTable from '../case/caseTable.vue';
// import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue'; // import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import apiTable from './apiTable.vue'; import apiTable from './apiTable.vue';
@ -230,15 +229,15 @@
{ {
polymorphicName: 'MsCommonElement', // MsCommonElement polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: { assertionConfig: {
enableGlobal: false, enableGlobal: true,
assertions: [], assertions: [],
}, },
postProcessorConfig: { postProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
preProcessorConfig: { preProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
}, },

View File

@ -169,15 +169,15 @@
{ {
polymorphicName: 'MsCommonElement', // MsCommonElement polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: { assertionConfig: {
enableGlobal: false, enableGlobal: true,
assertions: [], assertions: [],
}, },
postProcessorConfig: { postProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
preProcessorConfig: { preProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
}, },

View File

@ -63,15 +63,14 @@ export default {
'apiTestManagement.importMode': 'Import mode', 'apiTestManagement.importMode': 'Import mode',
'apiTestManagement.importModeTip1': 'Cover:', 'apiTestManagement.importModeTip1': 'Cover:',
'apiTestManagement.importModeTip2': 'apiTestManagement.importModeTip2':
'1.If the request type + path is the same for the same interface, if the request parameter content is inconsistent, it will be overwritten.', '1.The same interface already exists in the system (the request type + path are consistent). If the request parameter content is inconsistent, the original interface of the system will be overwritten.',
'apiTestManagement.importModeTip3': 'apiTestManagement.importModeTip3':
'2.The same interface request type + path are the same, and the request parameter content is the same and does not change.', '2.The same interface already exists in the system (request type + path are consistent), if the content of the request parameters is consistent, no changes will be made.',
'apiTestManagement.importModeTip4': 'apiTestManagement.importModeTip4': '3.Interfaces that do not exist in the system are added.',
'3.If the interface is not the same and the request type + path is the same, add it',
'apiTestManagement.importModeTip5': 'Not covered:', 'apiTestManagement.importModeTip5': 'Not covered:',
'apiTestManagement.importModeTip6': 'apiTestManagement.importModeTip6':
'1.If the same interface request type + path are the same, no changes will be made.', '1.If the same interface already exists in the system (the request type + path is the same), no changes will be made.',
'apiTestManagement.importModeTip7': '2.If the request type + path are not the same for the same interface, add a new', 'apiTestManagement.importModeTip7': '2.Interfaces that do not exist in the system are added.',
'apiTestManagement.cover': 'Cover', 'apiTestManagement.cover': 'Cover',
'apiTestManagement.uncover': 'Not covered', 'apiTestManagement.uncover': 'Not covered',
'apiTestManagement.moreSetting': 'More settings', 'apiTestManagement.moreSetting': 'More settings',

View File

@ -60,12 +60,12 @@ export default {
'apiTestManagement.belongModule': '所属模块', 'apiTestManagement.belongModule': '所属模块',
'apiTestManagement.importMode': '导入模式', 'apiTestManagement.importMode': '导入模式',
'apiTestManagement.importModeTip1': '覆盖:', 'apiTestManagement.importModeTip1': '覆盖:',
'apiTestManagement.importModeTip2': '1.同一接口请求类型+路径一致,请求参数内容不一致则覆盖', 'apiTestManagement.importModeTip2': '1.系统已存在的同一接口请求类型+路径一致,请求参数内容不一致则覆盖系统原接口',
'apiTestManagement.importModeTip3': '2.同一接口请求类型+路径一致,请求参数内容一致不做变更', 'apiTestManagement.importModeTip3': '2.系统已存在的同一接口请求类型+路径一致,请求参数内容一致不做变更',
'apiTestManagement.importModeTip4': '3.非同一接口,请求类型+路径一致,则新增', 'apiTestManagement.importModeTip4': '3.系统不存在的接口,则新增',
'apiTestManagement.importModeTip5': '不覆盖:', 'apiTestManagement.importModeTip5': '不覆盖:',
'apiTestManagement.importModeTip6': '1.同一接口请求类型+路径一致,则不做变更', 'apiTestManagement.importModeTip6': '1.系统已存在的同一接口请求类型+路径一致,则不做变更',
'apiTestManagement.importModeTip7': '2.非同一接口请求类型+路径一致,则新增', 'apiTestManagement.importModeTip7': '2.系统不存在的接口,则新增',
'apiTestManagement.cover': '覆盖', 'apiTestManagement.cover': '覆盖',
'apiTestManagement.uncover': '不覆盖', 'apiTestManagement.uncover': '不覆盖',
'apiTestManagement.moreSetting': '更多设置', 'apiTestManagement.moreSetting': '更多设置',

View File

@ -5,6 +5,7 @@
no-content-padding no-content-padding
:show-continue="true" :show-continue="true"
:footer="requestVModel.isNew === true" :footer="requestVModel.isNew === true"
:ok-disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
@confirm="handleSave" @confirm="handleSave"
@continue="handleContinue" @continue="handleContinue"
@close="handleClose" @close="handleClose"
@ -20,7 +21,7 @@
</div> </div>
<div <div
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST" v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
class="ml-auto flex items-center gap-[16px]" class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]"
> >
<div class="text-[14px] font-normal text-[var(--color-text-4)]"> <div class="text-[14px] font-normal text-[var(--color-text-4)]">
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }} {{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
@ -28,6 +29,7 @@
<a-select <a-select
v-model:model-value="requestVModel.customizeRequestEnvEnable" v-model:model-value="requestVModel.customizeRequestEnvEnable"
class="w-[150px]" class="w-[150px]"
popup-container=".customApiDrawer-title-right"
@change="handleUseEnvChange" @change="handleUseEnvChange"
> >
<template #prefix> <template #prefix>
@ -318,7 +320,7 @@
RequestResult, RequestResult,
RequestTaskResult, RequestTaskResult,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepFileParams, ScenarioStepItem } from '@/models/apiTest/scenario';
import { EnvConfig } from '@/models/projectManagement/environmental'; import { EnvConfig } from '@/models/projectManagement/environmental';
import { import {
RequestAuthType, RequestAuthType,
@ -366,6 +368,8 @@
unSaved: boolean; unSaved: boolean;
uploadFileIds: string[]; uploadFileIds: string[];
linkFileIds: string[]; linkFileIds: string[];
deleteFileIds?: string[];
unLinkFileIds?: string[];
} }
export type RequestParam = ExecuteApiRequestFullParams & { export type RequestParam = ExecuteApiRequestFullParams & {
@ -384,6 +388,7 @@
update: string; update: string;
}; };
stepResponses?: Record<string | number, RequestResult>; stepResponses?: Record<string | number, RequestResult>;
fileParams?: ScenarioStepFileParams;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -399,6 +404,7 @@
// //
const scenarioId = inject<string | number>('scenarioId'); const scenarioId = inject<string | number>('scenarioId');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig'); const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const hasLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec'); const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const visible = defineModel<boolean>('visible', { required: true }); const visible = defineModel<boolean>('visible', { required: true });
@ -439,15 +445,15 @@
{ {
polymorphicName: 'MsCommonElement', // MsCommonElement polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: { assertionConfig: {
enableGlobal: false, enableGlobal: true,
assertions: [], assertions: [],
}, },
postProcessorConfig: { postProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
preProcessorConfig: { preProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
}, },
@ -656,8 +662,6 @@
} }
} }
const hasLocalExec = ref(false); // api
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); // const pluginScriptMap = ref<Record<string, PluginConfig>>({}); //
const temporaryPluginFormMap: Record<string, any> = {}; // API const temporaryPluginFormMap: Record<string, any> = {}; // API
const pluginLoading = ref(false); const pluginLoading = ref(false);
@ -909,8 +913,8 @@
).validParams; ).validParams;
parseRequestBodyResult = parseRequestBodyFiles( parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.body, requestVModel.value.body,
requestVModel.value.uploadFileIds, // props.fileParams?.uploadFileIds || requestVModel.value.uploadFileIds, // api requestVModel
requestVModel.value.linkFileIds // props.fileParams?.linkFileIds || requestVModel.value.linkFileIds // api requestVModel
); );
requestParams = { requestParams = {
authConfig: requestVModel.value.authConfig, authConfig: requestVModel.value.authConfig,

View File

@ -266,7 +266,7 @@
import { scrollIntoView } from '@/utils/dom'; import { scrollIntoView } from '@/utils/dom';
import { ExecuteConditionConfig, PluginConfig, RequestResult } from '@/models/apiTest/common'; import { ExecuteConditionConfig, PluginConfig, RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepFileParams, ScenarioStepItem } from '@/models/apiTest/scenario';
import { import {
RequestAuthType, RequestAuthType,
RequestBodyFormat, RequestBodyFormat,
@ -298,6 +298,7 @@
const props = defineProps<{ const props = defineProps<{
request?: RequestParam; // request?: RequestParam; //
stepResponses?: Record<string | number, RequestResult>; stepResponses?: Record<string | number, RequestResult>;
fileParams?: ScenarioStepFileParams;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'applyStep', request: RequestParam): void; (e: 'applyStep', request: RequestParam): void;
@ -351,15 +352,15 @@
{ {
polymorphicName: 'MsCommonElement', // MsCommonElement polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: { assertionConfig: {
enableGlobal: false, enableGlobal: true,
assertions: [], assertions: [],
}, },
postProcessorConfig: { postProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
preProcessorConfig: { preProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
}, },
@ -797,11 +798,14 @@
defaultBodyParamsItem, defaultBodyParamsItem,
isExecute isExecute
).validParams; ).validParams;
parseRequestBodyResult = parseRequestBodyFiles( if (activeStep.value?.refType === ScenarioStepRefType.COPY) {
requestVModel.value.body, // case
requestVModel.value.uploadFileIds, // parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.linkFileIds // requestVModel.value.body,
); props.fileParams?.uploadFileIds || requestVModel.value.uploadFileIds, // case requestVModel
props.fileParams?.linkFileIds || requestVModel.value.linkFileIds // case requestVModel
);
}
requestParams = { requestParams = {
authConfig: requestVModel.value.authConfig, authConfig: requestVModel.value.authConfig,
body: { body: {
@ -878,8 +882,8 @@
} }
function handleClose() { function handleClose() {
// applyStep // applyStep case
if (!requestVModel.value.isNew) { if (!requestVModel.value.isNew && activeStep.value?.refType === ScenarioStepRefType.COPY) {
emit('applyStep', cloneDeep(makeRequestParams())); emit('applyStep', cloneDeep(makeRequestParams()));
} }
} }

View File

@ -198,7 +198,13 @@
* 获取复制或引用的步骤数据 * 获取复制或引用的步骤数据
* @param refType 复制或引用 * @param refType 复制或引用
*/ */
async function getScenarioSteps(refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF) { async function getScenarioSteps(
refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF,
other: {
api: MsTableDataItem<ApiDefinitionDetail>[];
case: MsTableDataItem<ApiCaseDetail>[];
}
) {
const scenarioMap: Record<string, MsTableDataItem<ApiScenarioTableItem>[]> = {}; const scenarioMap: Record<string, MsTableDataItem<ApiScenarioTableItem>[]> = {};
// id // id
selectedScenarios.value.forEach((e) => { selectedScenarios.value.forEach((e) => {
@ -236,18 +242,19 @@
return { return {
...node, ...node,
copyFromStepId: node.id, copyFromStepId: node.id,
originProjectId: node.projectId,
id: getGenerateId(), id: getGenerateId(),
}; };
}), }),
name: `copy-${e.name}`,
copyFromStepId: e.resourceId, copyFromStepId: e.resourceId,
originProjectId: e.projectId,
}; };
}); });
emit( emit(
'copy', 'copy',
cloneDeep({ cloneDeep({
api: selectedApis.value, api: other.api,
case: selectedCases.value, case: other.case,
scenario: fullScenarioArr, scenario: fullScenarioArr,
}) })
); );
@ -260,18 +267,20 @@
return { return {
...node, ...node,
copyFromStepId: node.id, copyFromStepId: node.id,
originProjectId: node.projectId,
id: getGenerateId(), id: getGenerateId(),
isQuoteScenarioStep: true, isQuoteScenarioStep: true,
isRefScenarioStep: true, // isRefScenarioStep: true, //
}; };
}), }),
originProjectId: e.projectId,
}; };
}); });
emit( emit(
'quote', 'quote',
cloneDeep({ cloneDeep({
api: selectedApis.value, api: other.api,
case: selectedCases.value, case: other.case,
scenario: fullScenarioArr, scenario: fullScenarioArr,
}) })
); );
@ -286,14 +295,25 @@
} }
async function handleCopy() { async function handleCopy() {
const copyApis = selectedApis.value.map((e) => ({
...e,
originProjectId: e.projectId,
}));
const copyCases = selectedCases.value.map((e) => ({
...e,
originProjectId: e.projectId,
}));
if (selectedScenarios.value.length > 0) { if (selectedScenarios.value.length > 0) {
await getScenarioSteps(ScenarioStepRefType.COPY); await getScenarioSteps(ScenarioStepRefType.COPY, {
api: copyApis,
case: copyCases,
});
} else { } else {
emit( emit(
'copy', 'copy',
cloneDeep({ cloneDeep({
api: selectedApis.value, api: copyApis,
case: selectedCases.value, case: copyCases,
scenario: selectedScenarios.value, scenario: selectedScenarios.value,
}) })
); );
@ -302,14 +322,25 @@
} }
async function handleQuote() { async function handleQuote() {
const quoteApis = selectedApis.value.map((e) => ({
...e,
originProjectId: e.projectId,
}));
const quoteCases = selectedCases.value.map((e) => ({
...e,
originProjectId: e.projectId,
}));
if (selectedScenarios.value.length > 0) { if (selectedScenarios.value.length > 0) {
await getScenarioSteps(ScenarioStepRefType.REF); await getScenarioSteps(ScenarioStepRefType.REF, {
api: quoteApis,
case: quoteCases,
});
} else { } else {
emit( emit(
'quote', 'quote',
cloneDeep({ cloneDeep({
api: selectedApis.value, api: quoteApis,
case: selectedCases.value, case: quoteCases,
scenario: selectedScenarios.value, scenario: selectedScenarios.value,
}) })
); );

View File

@ -5,9 +5,12 @@
:width="960" :width="960"
no-content-padding no-content-padding
disabled-width-drag disabled-width-drag
:footer="!props.detail"
@close="handleClose"
@cancel="handleDrawerCancel" @cancel="handleDrawerCancel"
> >
<div class="ml-[16px] mt-[10px]"> <div class="ml-[16px] mt-[10px]">
<!-- <stepTypeVue v-if="props.step" :step="props.step" /> -->
{{ t('apiScenario.scriptOperationName') }} {{ t('apiScenario.scriptOperationName') }}
</div> </div>
<div class="ml-[16px] mt-[3px] max-w-[70%]"> <div class="ml-[16px] mt-[3px] max-w-[70%]">
@ -19,9 +22,9 @@
/> />
</div> </div>
<div class="mt-[10px] flex h-[calc(100%-40px)] gap-[8px]"> <div class="mt-[10px] flex h-[calc(100%-40px)] gap-[8px]">
<conditionContent v-model:data="activeItem" :is-build-in="true" :is-format="true" /> <conditionContent v-if="visible" v-model:data="activeItem" :is-build-in="true" :is-format="true" />
</div> </div>
<template #footer> <template v-if="!props.detail" #footer>
<a-button type="secondary" @click="handleDrawerCancel"> <a-button type="secondary" @click="handleDrawerCancel">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</a-button> </a-button>
@ -36,19 +39,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types'; import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import conditionContent from '@/views/api-test/components/condition/content.vue';
// import stepTypeVue from './stepType/stepType.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ExecuteConditionProcessor } from '@/models/apiTest/common'; import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { RequestConditionProcessor } from '@/enums/apiEnum'; import { RequestConditionProcessor } from '@/enums/apiEnum';
const conditionContent = defineAsyncComponent(() => import('@/views/api-test/components/condition/content.vue'));
const props = defineProps<{ const props = defineProps<{
script?: ExecuteConditionProcessor; detail?: ExecuteConditionProcessor;
step?: ScenarioStepItem;
name?: string; name?: string;
}>(); }>();
const emit = defineEmits<{
(e: 'add', name: string, scriptProcessor: ExecuteConditionProcessor): void;
(e: 'save', name: string, scriptProcessor: ExecuteConditionProcessor): void;
}>();
const defaultScript = { const defaultScript = {
processorType: RequestConditionProcessor.SCRIPT, processorType: RequestConditionProcessor.SCRIPT,
@ -56,44 +69,45 @@
script: '', script: '',
scriptLanguage: LanguageEnum.BEANSHELL_JSR233, scriptLanguage: LanguageEnum.BEANSHELL_JSR233,
commonScriptInfo: {}, commonScriptInfo: {},
} as ExecuteConditionProcessor; polymorphicName: 'MsScriptElement',
const scriptName = ref(props.name || ''); } as unknown as ExecuteConditionProcessor;
const activeItem = ref(props.script || defaultScript); const scriptName = ref('');
const activeItem = ref<ExecuteConditionProcessor>(cloneDeep(defaultScript));
const { t } = useI18n(); const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true }); const visible = defineModel<boolean>('visible', { required: true });
const emit = defineEmits<{ watch(
(e: 'save', name: string, scriptProcessor: ExecuteConditionProcessor): void; () => visible.value,
}>(); (val) => {
if (val) {
function resetField() { scriptName.value = props.name || '';
scriptName.value = ''; activeItem.value = cloneDeep(props.detail || defaultScript);
activeItem.value = { }
processorType: RequestConditionProcessor.SCRIPT, }
enableCommonScript: false, );
script: '',
scriptLanguage: LanguageEnum.BEANSHELL_JSR233,
commonScriptInfo: {},
} as ExecuteConditionProcessor;
}
function handleDrawerCancel() { function handleDrawerCancel() {
resetField();
visible.value = false; visible.value = false;
} }
function saveAndContinue() { function saveAndContinue() {
emit('save', scriptName.value, activeItem.value); emit('add', scriptName.value, activeItem.value);
resetField();
} }
function save() { function save() {
emit('save', scriptName.value, activeItem.value); emit('add', scriptName.value, activeItem.value);
resetField();
visible.value = false; visible.value = false;
} }
function handleClose() {
if (props.detail) {
emit('save', scriptName.value, activeItem.value);
}
scriptName.value = '';
activeItem.value = defaultScript as unknown as ExecuteConditionProcessor;
}
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -94,11 +94,11 @@ export const defaultScenario: Scenario = {
csvVariables: [], csvVariables: [],
}, },
preProcessorConfig: { preProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
postProcessorConfig: { postProcessorConfig: {
enableGlobal: false, enableGlobal: true,
processors: [], processors: [],
}, },
assertionConfig: { assertionConfig: {
@ -114,6 +114,7 @@ export const defaultScenario: Scenario = {
}, },
steps: [], steps: [],
stepDetails: {}, stepDetails: {},
stepFileParam: {},
executeTime: 0, executeTime: 0,
executeSuccessCount: 0, executeSuccessCount: 0,
executeFailCount: 0, executeFailCount: 0,

View File

@ -116,9 +116,9 @@ export default function useCreateActions() {
if (item.id || item.resourceId) { if (item.id || item.resourceId) {
// 引用复制接口、用例、场景时的源资源信息 // 引用复制接口、用例、场景时的源资源信息
resourceField = { resourceField = {
resourceId: item.id || item.resourceId, resourceId: item.id || item.resourceId, // 场景会调接口获取信息所以有resourceId接口、用例没有下同
resourceNum: item.num, resourceNum: item.num || item.resourceNum,
resourceName: item.name, resourceName: item.name || item.resourceName,
}; };
} }
if (item.protocol) { if (item.protocol) {
@ -141,6 +141,7 @@ export default function useCreateActions() {
children: item.children || [], children: item.children || [],
stepType, stepType,
refType, refType,
originProjectId: item.originProjectId,
copyFromStepId: item.copyFromStepId, copyFromStepId: item.copyFromStepId,
...resourceField, ...resourceField,
name: name || item.name, name: name || item.name,

View File

@ -1,13 +1,20 @@
<template> <template>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<!-- <a-popover position="bl" content-class="detail-popover" arrow-class="hidden"> <a-popover
v-if="
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.API_SCENARIO].includes(props.data.stepType)
"
position="bl"
content-class="detail-popover"
arrow-class="hidden"
>
<MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" /> <MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
<template #content> <template #content>
<div class="flex flex-col gap-[16px]"> <div class="flex flex-col gap-[16px]">
<div> <div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div> <div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
<div class="text-[14px] text-[var(--color-text-1)]"> <div class="text-[14px] text-[var(--color-text-1)]">
{{ props.data.belongProjectName }} <!-- {{ props.data.belongProjectName }} -->
</div> </div>
</div> </div>
<div> <div>
@ -18,9 +25,9 @@
</div> </div>
</div> </div>
</template> </template>
</a-popover> --> </a-popover>
<!-- <MsTag <MsTag
v-if="props.data.projectId !== appStore.currentProjectId" v-if="props.data.originProjectId !== appStore.currentProjectId"
theme="outline" theme="outline"
size="small" size="small"
:self-style="{ :self-style="{
@ -30,22 +37,23 @@
}" }"
> >
{{ t('apiScenario.crossProject') }} {{ t('apiScenario.crossProject') }}
</MsTag> --> </MsTag>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue'; import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
// import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepItem } from '@/models/apiTest/scenario';
// import { ApiTestRouteEnum } from '@/enums/routeEnum'; import { ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
// import getStepType from '@/views/api-test/scenario/components/common/stepType/utils'; import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
const props = defineProps<{ const props = defineProps<{
data: ScenarioStepItem; data: ScenarioStepItem;
@ -53,27 +61,36 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
// const { openNewPage } = useOpenNewPage(); const { openNewPage } = useOpenNewPage();
// function goDetail() { function goDetail() {
// const _stepType = getStepType(props.data); const _stepType = getStepType(props.data);
// switch (true) { switch (true) {
// case _stepType.isCopyApi: case _stepType.isCopyApi:
// case _stepType.isQuoteApi: case _stepType.isQuoteApi:
// openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id, pId: props.data.projectId }); openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
// break; pId: props.data.originProjectId,
// case _stepType.isCopyScenario: dId: props.data.resourceId,
// case _stepType.isQuoteScenario: });
// openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id, pId: props.data.projectId }); break;
// break; case _stepType.isCopyScenario:
// case _stepType.isQuoteCase: case _stepType.isQuoteScenario:
// case _stepType.isCopyCase: openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
// openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id, pId: props.data.projectId }); pId: props.data.originProjectId,
// break; sId: props.data.resourceId,
// default: });
// break; break;
// } case _stepType.isQuoteCase:
// } case _stepType.isCopyCase:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
pId: props.data.originProjectId,
cId: props.data.resourceId,
});
break;
default:
break;
}
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -88,7 +88,7 @@
<!-- 步骤差异内容按步骤类型展示不同组件 --> <!-- 步骤差异内容按步骤类型展示不同组件 -->
<component <component
:is="getStepContent(step)" :is="getStepContent(step)"
:data="step.config" :data="checkStepIsApi(step) || step.stepType === ScenarioStepType.API_SCENARIO ? step : step.config"
:step-id="step.id" :step-id="step.id"
@quick-input="setQuickInput(step, $event)" @quick-input="setQuickInput(step, $event)"
@change="handleStepContentChange($event, step)" @change="handleStepContentChange($event, step)"
@ -240,7 +240,8 @@
</createStepActions> </createStepActions>
<customApiDrawer <customApiDrawer
v-model:visible="customApiDrawerVisible" v-model:visible="customApiDrawerVisible"
:request="currentStepDetail" :request="currentStepDetail as unknown as RequestParam"
:file-params="currentStepFileParams"
:step="activeStep" :step="activeStep"
:step-responses="scenario.stepResponses" :step-responses="scenario.stepResponses"
@add-step="addCustomApiStep" @add-step="addCustomApiStep"
@ -251,7 +252,8 @@
<customCaseDrawer <customCaseDrawer
v-model:visible="customCaseDrawerVisible" v-model:visible="customCaseDrawerVisible"
:active-step="activeStep" :active-step="activeStep"
:request="currentStepDetail" :request="currentStepDetail as unknown as RequestParam"
:file-params="currentStepFileParams"
:step-responses="scenario.stepResponses" :step-responses="scenario.stepResponses"
@apply-step="applyApiStep" @apply-step="applyApiStep"
@delete-step="deleteCaseStep(activeStep)" @delete-step="deleteCaseStep(activeStep)"
@ -265,11 +267,12 @@
@quote="handleImportApiApply('quote', $event)" @quote="handleImportApiApply('quote', $event)"
/> />
<scriptOperationDrawer <scriptOperationDrawer
v-if="scriptOperationDrawerVisible"
v-model:visible="scriptOperationDrawerVisible" v-model:visible="scriptOperationDrawerVisible"
:script="currentStepDetail" :detail="currentStepDetail as unknown as ExecuteConditionProcessor"
:step="activeStep"
:name="activeStep?.name" :name="activeStep?.name"
@save="addScriptStep" @add="addScriptStep"
@save="saveScriptStep"
/> />
<a-modal <a-modal
v-model:visible="showQuickInput" v-model:visible="showQuickInput"
@ -406,7 +409,6 @@
import waitTimeContent from './stepNodeComposition/waitTimeContent.vue'; import waitTimeContent from './stepNodeComposition/waitTimeContent.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import responseResult from '@/views/api-test/components/requestComposition/response/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common'; import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { debugScenario, getScenarioStep } from '@/api/modules/api-test/scenario'; import { debugScenario, getScenarioStep } from '@/api/modules/api-test/scenario';
@ -429,6 +431,8 @@
CreateStepAction, CreateStepAction,
Scenario, Scenario,
ScenarioStepConfig, ScenarioStepConfig,
ScenarioStepDetails,
ScenarioStepFileParams,
ScenarioStepItem, ScenarioStepItem,
} from '@/models/apiTest/scenario'; } from '@/models/apiTest/scenario';
import { EnvConfig } from '@/models/projectManagement/environmental'; import { EnvConfig } from '@/models/projectManagement/environmental';
@ -442,6 +446,7 @@
import type { RequestParam } from '../common/customApiDrawer.vue'; import type { RequestParam } from '../common/customApiDrawer.vue';
import useCreateActions from './createAction/useCreateActions'; import useCreateActions from './createAction/useCreateActions';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils'; import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config'; import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
@ -451,6 +456,9 @@
const customCaseDrawer = defineAsyncComponent(() => import('../common/customCaseDrawer.vue')); const customCaseDrawer = defineAsyncComponent(() => import('../common/customCaseDrawer.vue'));
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue')); const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue')); const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
const responseResult = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue')
);
const props = defineProps<{ const props = defineProps<{
stepKeyword: string; stepKeyword: string;
@ -470,13 +478,14 @@
required: true, required: true,
}); });
// //
const stepDetails = defineModel<Record<string, any>>('stepDetails', { const stepDetails = defineModel<Record<string, ScenarioStepDetails>>('stepDetails', {
required: true, required: true,
}); });
const scenario = defineModel<Scenario>('scenario', { const scenario = defineModel<Scenario>('scenario', {
required: true, required: true,
}); });
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec'); const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const localExecuteUrl = inject<Ref<string>>('localExecuteUrl');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig'); const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const selectedKeys = ref<(string | number)[]>([]); // const selectedKeys = ref<(string | number)[]>([]); //
@ -510,11 +519,10 @@
* 根据步骤类型获取步骤内容组件 * 根据步骤类型获取步骤内容组件
*/ */
function getStepContent(step: ScenarioStepItem) { function getStepContent(step: ScenarioStepItem) {
const _stepType = getStepType(step);
if (_stepType.isQuoteApi || _stepType.isQuoteCase || _stepType.isQuoteScenario) {
return quoteContent;
}
switch (step.stepType) { switch (step.stepType) {
case ScenarioStepType.API:
case ScenarioStepType.API_CASE:
case ScenarioStepType.API_SCENARIO:
case ScenarioStepType.CUSTOM_REQUEST: case ScenarioStepType.CUSTOM_REQUEST:
return quoteContent; return quoteContent;
case ScenarioStepType.LOOP_CONTROLLER: case ScenarioStepType.LOOP_CONTROLLER:
@ -741,11 +749,16 @@
case 'copy': case 'copy':
const id = getGenerateId(); const id = getGenerateId();
const stepDetail = stepDetails.value[node.id]; const stepDetail = stepDetails.value[node.id];
const stepFileParam = scenario.value.stepFileParam[node.id];
const { isQuoteScenario } = getStepType(node as ScenarioStepItem); const { isQuoteScenario } = getStepType(node as ScenarioStepItem);
if (stepDetail) { if (stepDetail) {
// //
stepDetails.value[id] = cloneDeep(stepDetail); stepDetails.value[id] = cloneDeep(stepDetail);
} }
if (stepFileParam) {
//
scenario.value.stepFileParam[id] = cloneDeep(stepFileParam);
}
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
steps.value, steps.value,
node.id, node.id,
@ -754,11 +767,16 @@
mapTree<ScenarioStepItem>(node, (childNode) => { mapTree<ScenarioStepItem>(node, (childNode) => {
const childId = getGenerateId(); const childId = getGenerateId();
const childStepDetail = stepDetails.value[node.id]; const childStepDetail = stepDetails.value[node.id];
const childStepFileParam = scenario.value.stepFileParam[node.id];
let childCopyFromStepId = childNode.id; let childCopyFromStepId = childNode.id;
if (childStepDetail) { if (childStepDetail) {
// //
stepDetails.value[childId] = cloneDeep(childStepDetail); stepDetails.value[childId] = cloneDeep(childStepDetail);
} }
if (childStepFileParam) {
//
scenario.value.stepFileParam[id] = cloneDeep(childStepFileParam);
}
if (!isQuoteScenario) { if (!isQuoteScenario) {
// id // id
if (childStepDetail || (childNode.isNew && childNode.stepRefType === ScenarioStepRefType.REF)) { if (childStepDetail || (childNode.isNew && childNode.stepRefType === ScenarioStepRefType.REF)) {
@ -892,24 +910,33 @@
const customApiDrawerVisible = ref(false); const customApiDrawerVisible = ref(false);
const scriptOperationDrawerVisible = ref(false); const scriptOperationDrawerVisible = ref(false);
const activeCreateAction = ref<CreateStepAction>(); // const activeCreateAction = ref<CreateStepAction>(); //
const currentStepDetail = computed<any>(() => { const currentStepDetail = computed<ScenarioStepDetails | undefined>(() => {
// TODO:
if (activeStep.value) { if (activeStep.value) {
return stepDetails.value[activeStep.value.id]; return stepDetails.value[activeStep.value.id];
} }
return undefined; });
const currentStepFileParams = computed<ScenarioStepFileParams | undefined>(() => {
if (activeStep.value) {
return scenario.value.stepFileParam[activeStep.value.id];
}
}); });
async function getStepDetail(step: ScenarioStepItem) { async function getStepDetail(step: ScenarioStepItem) {
try { try {
appStore.showLoading(); appStore.showLoading();
const res = await getScenarioStep(step.copyFromStepId || step.id); const res = await getScenarioStep(step.copyFromStepId || step.id);
let parseRequestBodyResult;
if (step.config.protocol === 'HTTP' && res.body) {
parseRequestBodyResult = parseRequestBodyFiles(res.body); // id
}
stepDetails.value[step.id] = { stepDetails.value[step.id] = {
...res, ...res,
stepId: step.id, stepId: step.id,
protocol: step.config.protocol, protocol: step.config.protocol,
method: step.config.method, method: step.config.method,
...parseRequestBodyResult,
}; };
scenario.value.stepFileParam[step.id] = parseRequestBodyResult;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -947,6 +974,13 @@
customCaseDrawerVisible.value = true; customCaseDrawerVisible.value = true;
} else if (step.stepType === ScenarioStepType.SCRIPT) { } else if (step.stepType === ScenarioStepType.SCRIPT) {
activeStep.value = step; activeStep.value = step;
if (
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
(stepDetails.value[step.id] === undefined && !step.isNew)
) {
//
await getStepDetail(step);
}
scriptOperationDrawerVisible.value = true; scriptOperationDrawerVisible.value = true;
} }
} }
@ -971,16 +1005,11 @@
/** /**
* 开启websocket监听接收执行结果 * 开启websocket监听接收执行结果
*/ */
function debugSocket( function debugSocket(step: ScenarioStepItem, reportId: string | number, executeType?: 'localExec' | 'serverExec') {
step: ScenarioStepItem,
reportId: string | number,
executeType?: 'localExec' | 'serverExec',
localExecuteUrl?: string
) {
websocketMap[reportId] = getSocket( websocketMap[reportId] = getSocket(
reportId || '', reportId || '',
executeType === 'localExec' ? '/ws/debug' : '', executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? localExecuteUrl : '' executeType === 'localExec' ? localExecuteUrl?.value : ''
); );
websocketMap[reportId].addEventListener('message', (event) => { websocketMap[reportId].addEventListener('message', (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
@ -1022,13 +1051,12 @@
ApiScenarioDebugRequest, ApiScenarioDebugRequest,
'steps' | 'stepDetails' | 'reportId' | 'uploadFileIds' | 'linkFileIds' 'steps' | 'stepDetails' | 'reportId' | 'uploadFileIds' | 'linkFileIds'
>, >,
executeType?: 'localExec' | 'serverExec', executeType?: 'localExec' | 'serverExec'
localExecuteUrl?: string
) { ) {
const [currentStep] = executeParams.steps; const [currentStep] = executeParams.steps;
try { try {
currentStep.isExecuting = true; currentStep.isExecuting = true;
debugSocket(currentStep, executeParams.reportId, executeType, localExecuteUrl); // websocket debugSocket(currentStep, executeParams.reportId, executeType); // websocket
const res = await debugScenario({ const res = await debugScenario({
id: scenario.value.id || '', id: scenario.value.id || '',
grouped: false, grouped: false,
@ -1044,8 +1072,8 @@
}; };
}), }),
}); });
if (executeType === 'localExec' && localExecuteUrl) { if (executeType === 'localExec' && localExecuteUrl?.value) {
await localExecuteApiDebug(localExecuteUrl, res); await localExecuteApiDebug(localExecuteUrl.value, res);
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -1073,15 +1101,16 @@
} }
const stepDetail = stepDetails.value[realStep.id]; const stepDetail = stepDetails.value[realStep.id];
delete scenario.value.stepResponses[realStep.id]; // delete scenario.value.stepResponses[realStep.id]; //
const stepFileParam = scenario.value.stepFileParam[realStep.id];
realExecute( realExecute(
{ {
steps: [realStep as ScenarioStepItem], steps: [realStep as ScenarioStepItem],
stepDetails: { stepDetails: {
[realStep.id]: stepDetails.value[realStep.id], [realStep.id]: stepDetail,
}, },
reportId: realStep.reportId, reportId: realStep.reportId,
uploadFileIds: stepDetail?.uploadFileIds || [], uploadFileIds: stepFileParam?.uploadFileIds || [],
linkFileIds: stepDetail?.linkFileIds || [], linkFileIds: stepFileParam?.linkFileIds || [],
}, },
isPriorityLocalExec?.value ? 'localExec' : 'serverExec' isPriorityLocalExec?.value ? 'localExec' : 'serverExec'
); );
@ -1245,6 +1274,12 @@
customizeRequest: true, customizeRequest: true,
customizeRequestEnvEnable: request.customizeRequestEnvEnable, customizeRequestEnvEnable: request.customizeRequestEnvEnable,
}; };
scenario.value.stepFileParam[request.stepId] = {
linkFileIds: request.linkFileIds,
uploadFileIds: request.uploadFileIds,
deleteFileIds: request.deleteFileIds,
unLinkFileIds: request.unLinkFileIds,
};
emit('updateResource', request.uploadFileIds, request.linkFileIds); emit('updateResource', request.uploadFileIds, request.linkFileIds);
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
@ -1287,7 +1322,13 @@
} }
if (activeStep.value) { if (activeStep.value) {
request.isNew = false; request.isNew = false;
stepDetails.value[activeStep.value?.id] = request; stepDetails.value[activeStep.value.id] = request;
scenario.value.stepFileParam[activeStep.value?.id] = {
linkFileIds: request.linkFileIds,
uploadFileIds: request.uploadFileIds,
deleteFileIds: request.deleteFileIds,
unLinkFileIds: request.unLinkFileIds,
};
emit('updateResource', request.uploadFileIds, request.linkFileIds); emit('updateResource', request.uploadFileIds, request.linkFileIds);
activeStep.value = undefined; activeStep.value = undefined;
} }
@ -1337,6 +1378,14 @@
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
function saveScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
if (activeStep.value) {
stepDetails.value[activeStep.value.id] = cloneDeep(scriptProcessor);
activeStep.value.name = name;
activeStep.value = undefined;
}
}
/** /**
* 释放允许拖拽步骤到释放的节点内 * 释放允许拖拽步骤到释放的节点内
* @param dropNode 释放节点 * @param dropNode 释放节点

View File

@ -529,6 +529,7 @@
try { try {
appStore.showLoading(); appStore.showLoading();
const res = await getScenarioDetail(typeof record === 'string' ? record : record.id); const res = await getScenarioDetail(typeof record === 'string' ? record : record.id);
res.stepFileParam = {};
res.stepDetails = {}; res.stepDetails = {};
if (!res.steps) { if (!res.steps) {
res.steps = []; res.steps = [];
@ -551,11 +552,15 @@
} }
}); });
const hasLocalExec = computed(() => executeButtonRef.value?.hasLocalExec);
const localExecuteUrl = computed(() => executeButtonRef.value?.localExecuteUrl);
const isPriorityLocalExec = computed(() => executeButtonRef.value?.isPriorityLocalExec); const isPriorityLocalExec = computed(() => executeButtonRef.value?.isPriorityLocalExec);
const scenarioId = computed(() => activeScenarioTab.value.id); const scenarioId = computed(() => activeScenarioTab.value.id);
const scenarioExecuteLoading = computed(() => activeScenarioTab.value.executeLoading); const scenarioExecuteLoading = computed(() => activeScenarioTab.value.executeLoading);
// //
provide('isPriorityLocalExec', readonly(isPriorityLocalExec)); provide('isPriorityLocalExec', readonly(isPriorityLocalExec));
provide('hasLocalExec', readonly(hasLocalExec));
provide('localExecuteUrl', readonly(localExecuteUrl));
provide('currentEnvConfig', readonly(currentEnvConfig)); provide('currentEnvConfig', readonly(currentEnvConfig));
provide('scenarioId', scenarioId); provide('scenarioId', scenarioId);
provide('scenarioExecuteLoading', scenarioExecuteLoading); provide('scenarioExecuteLoading', scenarioExecuteLoading);

View File

@ -808,11 +808,11 @@
:deep(.apiFieldIdClass) { :deep(.apiFieldIdClass) {
margin-bottom: 0; margin-bottom: 0;
} }
:deep(.systemFieldWrapper) { :deep(.systemFieldWrapper) {
.arco-form-item { .arco-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
padding: 8px; padding: 8px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
@ -821,11 +821,11 @@
background: var(--color-text-n9); background: var(--color-text-n9);
} }
} }
.tagWrapper { .tagWrapper {
.arco-form-item { .arco-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
padding: 8px; padding: 8px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;