feat(接口测试): csv参数&JSONPath 提取调整&场景步骤树 hook 提取
This commit is contained in:
parent
2a3421f91d
commit
663b96aa62
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<a-dropdown v-model:popup-visible="dropdownVisible" :disabled="props.disabled" position="tl" trigger="click">
|
<a-dropdown v-model:popup-visible="dropdownVisible" :disabled="props.disabled" position="tl" trigger="click">
|
||||||
<a-button :disabled="props.disabled" size="mini" type="outline">
|
<a-button :disabled="props.disabled" size="mini" type="outline">
|
||||||
<template #icon> <icon-upload class="text-[14px] !text-[rgb(var(--primary-5))]" /> </template>
|
<template #icon>
|
||||||
|
<icon-upload class="!hover:text-[rgb(var(--primary-5))] text-[14px] !text-[rgb(var(--primary-5))]" />
|
||||||
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<MsUpload
|
<MsUpload
|
||||||
|
|
|
@ -449,7 +449,12 @@
|
||||||
td {
|
td {
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
}
|
}
|
||||||
* {
|
.arco-btn-icon {
|
||||||
|
border-color: var(--color-text-n8);
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
*:not(.arco-btn-icon) {
|
||||||
|
border-color: var(--color-text-n8) !important;
|
||||||
color: var(--color-text-4) !important;
|
color: var(--color-text-4) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot :name="item.titleSlotName" :column-config="item">
|
<slot :name="item.titleSlotName" :column-config="item">
|
||||||
<div v-if="item.title" class="title-name pl-1 text-[var(--color-text-3)]">
|
<div v-if="item.title" class="title-name text-[var(--color-text-3)]">
|
||||||
{{ t(item.title as string) }}
|
{{ t(item.title as string) }}
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
|
@ -234,14 +234,11 @@ export interface AssertionConfig {
|
||||||
assertions: ExecuteAssertionItem[];
|
assertions: ExecuteAssertionItem[];
|
||||||
}
|
}
|
||||||
export interface CsvVariable {
|
export interface CsvVariable {
|
||||||
id?: string;
|
id: string;
|
||||||
fileId: string;
|
|
||||||
scenarioId: string;
|
scenarioId: string;
|
||||||
name: string;
|
name: string;
|
||||||
fileName: string;
|
|
||||||
scope: string;
|
scope: string;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
association: boolean;
|
|
||||||
encoding: string;
|
encoding: string;
|
||||||
random: boolean;
|
random: boolean;
|
||||||
variableNames: string;
|
variableNames: string;
|
||||||
|
@ -250,6 +247,14 @@ export interface CsvVariable {
|
||||||
allowQuotedData: boolean;
|
allowQuotedData: boolean;
|
||||||
recycleOnEof: boolean;
|
recycleOnEof: boolean;
|
||||||
stopThreadOnEof: boolean;
|
stopThreadOnEof: boolean;
|
||||||
|
file: {
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
local: boolean; // 是否是本地上传的文件
|
||||||
|
fileAlias: string; // 文件别名
|
||||||
|
delete: boolean; // 是否删除
|
||||||
|
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
|
||||||
|
};
|
||||||
// 以下为前端字段
|
// 以下为前端字段
|
||||||
settingVisible: boolean;
|
settingVisible: boolean;
|
||||||
}
|
}
|
||||||
|
@ -353,7 +358,7 @@ export interface ScenarioStepItem {
|
||||||
stepType: ScenarioStepType;
|
stepType: ScenarioStepType;
|
||||||
refType: ScenarioStepRefType;
|
refType: ScenarioStepRefType;
|
||||||
config: ScenarioStepDetail; // 存储步骤列表需要展示的信息
|
config: ScenarioStepDetail; // 存储步骤列表需要展示的信息
|
||||||
csvFileIds?: string[];
|
csvIds?: string[];
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
versionId?: string;
|
versionId?: string;
|
||||||
children?: ScenarioStepItem[];
|
children?: ScenarioStepItem[];
|
||||||
|
|
|
@ -426,13 +426,11 @@ export const defaultNormalParamItem = {
|
||||||
};
|
};
|
||||||
// 场景-csv参数默认值
|
// 场景-csv参数默认值
|
||||||
export const defaultCsvParamItem: CsvVariable = {
|
export const defaultCsvParamItem: CsvVariable = {
|
||||||
fileId: '',
|
id: '',
|
||||||
scenarioId: '',
|
scenarioId: '',
|
||||||
name: '',
|
name: '',
|
||||||
fileName: '',
|
|
||||||
scope: 'SCENARIO',
|
scope: 'SCENARIO',
|
||||||
enable: true,
|
enable: false,
|
||||||
association: false,
|
|
||||||
encoding: 'UTF-8',
|
encoding: 'UTF-8',
|
||||||
random: false,
|
random: false,
|
||||||
variableNames: '',
|
variableNames: '',
|
||||||
|
@ -442,4 +440,11 @@ export const defaultCsvParamItem: CsvVariable = {
|
||||||
recycleOnEof: false,
|
recycleOnEof: false,
|
||||||
stopThreadOnEof: false,
|
stopThreadOnEof: false,
|
||||||
settingVisible: false,
|
settingVisible: false,
|
||||||
|
file: {
|
||||||
|
fileId: '',
|
||||||
|
fileName: '',
|
||||||
|
local: false,
|
||||||
|
fileAlias: '',
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -252,7 +252,13 @@
|
||||||
JSONPath({
|
JSONPath({
|
||||||
json: parseJson.value,
|
json: parseJson.value,
|
||||||
path: expressionForm.value.expression,
|
path: expressionForm.value.expression,
|
||||||
})?.map((e: any) => JSON.stringify(e).replace(/Number\(([^)]+)\)|Number\(([^)]+)\)/g, '$1$2')) || [];
|
})?.map((e: any) =>
|
||||||
|
typeof e === 'string'
|
||||||
|
? e
|
||||||
|
: JSON.stringify(e)
|
||||||
|
.replace(/Number\(([^)]+)\)/g, '$1')
|
||||||
|
.replace(/^"|"$/g, '')
|
||||||
|
) || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
|
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,7 +222,7 @@
|
||||||
:options="columnConfig.typeOptions || []"
|
:options="columnConfig.typeOptions || []"
|
||||||
class="ms-form-table-input w-[180px]"
|
class="ms-form-table-input w-[180px]"
|
||||||
size="mini"
|
size="mini"
|
||||||
@change="() => addTableLine(rowIndex)"
|
@change="(val) => handleScopeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- 参数值 -->
|
<!-- 参数值 -->
|
||||||
|
@ -282,7 +282,7 @@
|
||||||
<!-- 文件 -->
|
<!-- 文件 -->
|
||||||
<template #file="{ record, rowIndex }">
|
<template #file="{ record, rowIndex }">
|
||||||
<MsAddAttachment
|
<MsAddAttachment
|
||||||
:file-list="[record]"
|
:file-list="[record.file]"
|
||||||
:disabled="props.disabledParamValue"
|
:disabled="props.disabledParamValue"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
mode="input"
|
mode="input"
|
||||||
|
@ -500,6 +500,9 @@
|
||||||
size="small"
|
size="small"
|
||||||
type="line"
|
type="line"
|
||||||
class="ml-[8px]"
|
class="ml-[8px]"
|
||||||
|
:before-change="
|
||||||
|
(newValue) => (props.enableChangeIntercept ? props.enableChangeIntercept(record, newValue) : undefined)
|
||||||
|
"
|
||||||
@change="() => addTableLine(rowIndex)"
|
@change="() => addTableLine(rowIndex)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -624,6 +627,7 @@
|
||||||
import { groupCategoryEnvList, groupProjectEnv } from '@/api/modules/project-management/envManagement';
|
import { groupCategoryEnvList, groupProjectEnv } from '@/api/modules/project-management/envManagement';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { getGenerateId } from '@/utils';
|
||||||
|
|
||||||
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
||||||
import { HttpForm, ProjectOptionItem } from '@/models/projectManagement/environmental';
|
import { HttpForm, ProjectOptionItem } from '@/models/projectManagement/environmental';
|
||||||
|
@ -686,6 +690,9 @@
|
||||||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||||
fileModuleOptionsApi?: (projectId: string) => Promise<ModuleTreeNode[]>; // 文件转存目录下拉框接口
|
fileModuleOptionsApi?: (projectId: string) => Promise<ModuleTreeNode[]>; // 文件转存目录下拉框接口
|
||||||
|
deleteIntercept?: (record: any, deleteCall: () => void) => void; // 删除行拦截器
|
||||||
|
typeChangeIntercept?: (record: any, doChange: () => void) => void; // type 列切换拦截
|
||||||
|
enableChangeIntercept?: (record: any, val: string | number | boolean) => boolean | Promise<boolean>; // enable 列切换拦截
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
params: () => [],
|
params: () => [],
|
||||||
|
@ -734,8 +741,15 @@
|
||||||
emit('treeDelete', record);
|
emit('treeDelete', record);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (props.deleteIntercept) {
|
||||||
|
props.deleteIntercept(record, () => {
|
||||||
paramsData.value.splice(rowIndex, 1);
|
paramsData.value.splice(rowIndex, 1);
|
||||||
emitChange('deleteParam');
|
emitChange('deleteParam');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
paramsData.value.splice(rowIndex, 1);
|
||||||
|
emitChange('deleteParam');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 断言-文档-Begin */
|
/** 断言-文档-Begin */
|
||||||
|
@ -866,13 +880,13 @@
|
||||||
if (rowIndex === paramsData.value.length - 1) {
|
if (rowIndex === paramsData.value.length - 1) {
|
||||||
// Don't change this!!!
|
// Don't change this!!!
|
||||||
// 最后一行的更改才会触发添加新一行
|
// 最后一行的更改才会触发添加新一行
|
||||||
const id = new Date().getTime().toString();
|
const id = getGenerateId();
|
||||||
const lastLineData = paramsData.value[rowIndex]; // 上一行数据
|
const lastLineData = paramsData.value[rowIndex]; // 上一行数据
|
||||||
const selectColumnKeys = props.columns.filter((e) => e.typeOptions).map((e) => e.dataIndex); // 找到下拉框选项的列
|
const selectColumnKeys = props.columns.filter((e) => e.typeOptions).map((e) => e.dataIndex); // 找到下拉框选项的列
|
||||||
const nextLine = {
|
const nextLine = {
|
||||||
id,
|
id,
|
||||||
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
|
||||||
enable: true, // 是否勾选
|
enable: true, // 是否勾选
|
||||||
|
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
||||||
} as any;
|
} as any;
|
||||||
selectColumnKeys.forEach((key) => {
|
selectColumnKeys.forEach((key) => {
|
||||||
// 如果是更改了下拉框导致添加新的一列,需要将更改后的下拉框的值应用到下一行(产品为了方便统一输入参数类型)
|
// 如果是更改了下拉框导致添加新的一列,需要将更改后的下拉框的值应用到下一行(产品为了方便统一输入参数类型)
|
||||||
|
@ -908,7 +922,7 @@
|
||||||
hasNoIdItem = true;
|
hasNoIdItem = true;
|
||||||
return {
|
return {
|
||||||
...cloneDeep(props.defaultParamItem),
|
...cloneDeep(props.defaultParamItem),
|
||||||
id: new Date().getTime() + i,
|
id: getGenerateId(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!item.id) {
|
if (!item.id) {
|
||||||
|
@ -916,7 +930,7 @@
|
||||||
hasNoIdItem = true;
|
hasNoIdItem = true;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
id: new Date().getTime() + i,
|
id: getGenerateId(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
|
@ -926,12 +940,12 @@
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (props.disabledExceptParam) return;
|
if (props.disabledExceptParam) return;
|
||||||
const id = new Date().getTime().toString();
|
const id = getGenerateId();
|
||||||
paramsData.value = [
|
paramsData.value = [
|
||||||
{
|
{
|
||||||
id, // 默认给时间戳 id,若 props.defaultParamItem 有 id,则覆盖
|
id, // 默认给时间戳 id,若 props.defaultParamItem 有 id,则覆盖
|
||||||
...cloneDeep(props.defaultParamItem),
|
|
||||||
enable: true, // 是否勾选
|
enable: true, // 是否勾选
|
||||||
|
...cloneDeep(props.defaultParamItem),
|
||||||
},
|
},
|
||||||
] as any[];
|
] as any[];
|
||||||
emitChange('watch props.params', true);
|
emitChange('watch props.params', true);
|
||||||
|
@ -1006,20 +1020,18 @@
|
||||||
if (props.uploadTempFileApi && file?.local) {
|
if (props.uploadTempFileApi && file?.local) {
|
||||||
appStore.showLoading();
|
appStore.showLoading();
|
||||||
const res = await props.uploadTempFileApi(file.file);
|
const res = await props.uploadTempFileApi(file.file);
|
||||||
record = {
|
record.file = {
|
||||||
...record,
|
|
||||||
...file,
|
...file,
|
||||||
fileId: res.data,
|
fileId: res.data,
|
||||||
fileName: file.name || '',
|
fileName: file.name || '',
|
||||||
fileAlias: file.name || '',
|
fileAlias: file.name || '',
|
||||||
};
|
};
|
||||||
} else if (file) {
|
} else if (files[0]) {
|
||||||
record = {
|
record.file = {
|
||||||
...record,
|
...files[0],
|
||||||
...file,
|
fileId: files[0].uid || files[0].fileId || '',
|
||||||
fileId: file.uid || file.fileId || '',
|
fileName: files[0].originalName || '',
|
||||||
fileName: file.originalName || '',
|
fileAlias: files[0].name || '',
|
||||||
fileAlias: file.name || '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
addTableLine(rowIndex);
|
addTableLine(rowIndex);
|
||||||
|
@ -1101,6 +1113,23 @@
|
||||||
emitChange('handleTypeChange');
|
emitChange('handleTypeChange');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleScopeChange(
|
||||||
|
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[],
|
||||||
|
record: Record<string, any>,
|
||||||
|
rowIndex: number,
|
||||||
|
addLineDisabled?: boolean
|
||||||
|
) {
|
||||||
|
if (props.typeChangeIntercept) {
|
||||||
|
props.typeChangeIntercept(record, () => {
|
||||||
|
addTableLine(rowIndex, addLineDisabled);
|
||||||
|
emitChange('handleScopeChange');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addTableLine(rowIndex, addLineDisabled);
|
||||||
|
emitChange('handleScopeChange');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取更多操作按钮列表
|
* 获取更多操作按钮列表
|
||||||
* @param actions 按钮列表
|
* @param actions 按钮列表
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
:file-save-as-api="props.fileSaveAsApi"
|
:file-save-as-api="props.fileSaveAsApi"
|
||||||
:file-module-options-api="props.fileModuleOptionsApi"
|
:file-module-options-api="props.fileModuleOptionsApi"
|
||||||
@change="handleParamTableChange"
|
@change="handleParamTableChange"
|
||||||
@batch-add="batchAddKeyValVisible = true"
|
@batch-add="() => (batchAddKeyValVisible = true)"
|
||||||
/>
|
/>
|
||||||
<paramTable
|
<paramTable
|
||||||
v-else-if="innerParams.bodyType === RequestBodyFormat.WWW_FORM"
|
v-else-if="innerParams.bodyType === RequestBodyFormat.WWW_FORM"
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_URL_ENCODE"
|
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_URL_ENCODE"
|
||||||
:default-param-item="defaultBodyParamsItem"
|
:default-param-item="defaultBodyParamsItem"
|
||||||
@change="handleParamTableChange"
|
@change="handleParamTableChange"
|
||||||
@batch-add="batchAddKeyValVisible = true"
|
@batch-add="() => (batchAddKeyValVisible = true)"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="innerParams.bodyType === RequestBodyFormat.BINARY">
|
<div v-else-if="innerParams.bodyType === RequestBodyFormat.BINARY">
|
||||||
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
|
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
:scroll="props.scroll"
|
:scroll="props.scroll"
|
||||||
>
|
>
|
||||||
<template #assertionItem="{ record }">
|
<template #assertionItem="{ record }">
|
||||||
|
<a-tooltip :content="record.name">
|
||||||
<div class="flex items-center gap-[4px]">
|
<div class="flex items-center gap-[4px]">
|
||||||
【{{
|
【{{
|
||||||
t(
|
t(
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
}}】
|
}}】
|
||||||
{{ record.name }}
|
{{ record.name }}
|
||||||
</div>
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #condition="{ record }">
|
<template #condition="{ record }">
|
||||||
{{
|
{{
|
||||||
|
@ -61,7 +63,6 @@
|
||||||
{
|
{
|
||||||
title: 'apiTestDebug.assertionItem',
|
title: 'apiTestDebug.assertionItem',
|
||||||
dataIndex: 'assertionItem',
|
dataIndex: 'assertionItem',
|
||||||
showTooltip: true,
|
|
||||||
slotName: 'assertionItem',
|
slotName: 'assertionItem',
|
||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,351 @@
|
||||||
|
<template>
|
||||||
|
<paramTable
|
||||||
|
v-model:params="csvVariables"
|
||||||
|
:columns="csvColumns"
|
||||||
|
:default-param-item="defaultCsvParamItem"
|
||||||
|
:draggable="false"
|
||||||
|
:selectable="false"
|
||||||
|
:delete-intercept="deleteIntercept"
|
||||||
|
:type-change-intercept="typeChangeIntercept"
|
||||||
|
:enable-change-intercept="enableChangeIntercept"
|
||||||
|
:upload-temp-file-api="uploadTempFile"
|
||||||
|
:file-save-as-source-id="props.scenarioId"
|
||||||
|
:file-save-as-api="transferFile"
|
||||||
|
:file-module-options-api="getTransferOptions"
|
||||||
|
@change="handleCsvVariablesChange"
|
||||||
|
>
|
||||||
|
<template #operationPre="{ record }">
|
||||||
|
<a-trigger
|
||||||
|
v-model:popup-visible="record.settingVisible"
|
||||||
|
trigger="click"
|
||||||
|
position="br"
|
||||||
|
class="scenario-csv-trigger"
|
||||||
|
>
|
||||||
|
<MsButton type="text" class="!mr-0" @click="handleRecordConfig(record)">
|
||||||
|
{{ t('apiScenario.params.config') }}
|
||||||
|
</MsButton>
|
||||||
|
<template #content>
|
||||||
|
<div class="scenario-csv-trigger-content">
|
||||||
|
<div class="mb-[16px] flex items-center">
|
||||||
|
<div class="font-semibold text-[var(--color-text-1)]">{{ t('apiScenario.params.csvConfig') }}</div>
|
||||||
|
<!-- <div class="text-[var(--color-text-4)]">({{ record.key }})</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="scenario-csv-trigger-content-scroll">
|
||||||
|
<a-form ref="paramFormRef" :model="paramForm" layout="vertical" scroll-to-first-error>
|
||||||
|
<a-form-item
|
||||||
|
field="name"
|
||||||
|
:label="t('apiScenario.params.csvName')"
|
||||||
|
:rules="[{ required: true, message: t('apiScenario.params.csvNameNotNull') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
class="mb-[16px]"
|
||||||
|
>
|
||||||
|
<a-input v-model:model-value="paramForm.name" :max-length="255"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="variableNames" :label="t('apiScenario.params.csvParamName')" class="mb-[16px]">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="paramForm.variableNames"
|
||||||
|
:placeholder="t('apiScenario.params.csvParamNamePlaceholder')"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="encoding" :label="t('apiScenario.params.csvFileCode')" class="mb-[16px]">
|
||||||
|
<a-select
|
||||||
|
v-model:model-value="paramForm.encoding"
|
||||||
|
:options="encodingOptions"
|
||||||
|
class="w-[120px]"
|
||||||
|
></a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="delimiter" :label="t('apiScenario.params.csvSplitChar')" class="mb-[16px]">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="paramForm.delimiter"
|
||||||
|
:placeholder="t('common.pleaseInput')"
|
||||||
|
:max-length="64"
|
||||||
|
class="w-[120px]"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
field="ignoreFirstLine"
|
||||||
|
:label="t('apiScenario.params.csvIgnoreFirstLine')"
|
||||||
|
class="mb-[16px]"
|
||||||
|
>
|
||||||
|
<a-radio-group v-model:model-value="paramForm.ignoreFirstLine">
|
||||||
|
<a-radio :value="false">False</a-radio>
|
||||||
|
<a-radio :value="true">True</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="random" :label="t('apiScenario.params.csvIsRandom')" class="mb-[16px]">
|
||||||
|
<a-radio-group v-model:model-value="paramForm.random">
|
||||||
|
<a-radio :value="false">False</a-radio>
|
||||||
|
<a-radio :value="true">True</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="allowQuotedData" :label="t('apiScenario.params.csvQuoteAllow')" class="mb-[16px]">
|
||||||
|
<a-radio-group v-model:model-value="paramForm.allowQuotedData">
|
||||||
|
<a-radio :value="false">False</a-radio>
|
||||||
|
<a-radio :value="true">True</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="recycleOnEof" :label="t('apiScenario.params.csvRecycle')" class="mb-[16px]">
|
||||||
|
<a-radio-group v-model:model-value="paramForm.recycleOnEof">
|
||||||
|
<a-radio :value="false">False</a-radio>
|
||||||
|
<a-radio :value="true">True</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="stopThreadOnEof" :label="t('apiScenario.params.csvStop')" class="mb-[16px]">
|
||||||
|
<a-radio-group v-model:model-value="paramForm.stopThreadOnEof">
|
||||||
|
<a-radio :value="false">False</a-radio>
|
||||||
|
<a-radio :value="true">True</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-[8px]">
|
||||||
|
<a-button type="secondary" @click="cancelConfig(record)">{{ t('common.cancel') }}</a-button>
|
||||||
|
<a-button type="primary" @click="applyConfig">{{ t('ms.paramsInput.apply') }}</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-trigger>
|
||||||
|
</template>
|
||||||
|
</paramTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||||
|
|
||||||
|
import { getTransferOptions, transferFile, uploadTempFile } from '@/api/modules/api-test/scenario';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
import { getGenerateId } from '@/utils';
|
||||||
|
|
||||||
|
import { CsvVariable } from '@/models/apiTest/scenario';
|
||||||
|
|
||||||
|
import { defaultCsvParamItem } from '@/views/api-test/components/config';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
scenarioId?: string | number;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'change'): void; // 数据发生变化
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
const csvVariables = defineModel<CsvVariable[]>('csvVariables', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const csvColumns: ParamTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: 'apiScenario.params.csvName',
|
||||||
|
dataIndex: 'name',
|
||||||
|
slotName: 'name',
|
||||||
|
needValidRepeat: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiScenario.params.csvScoped',
|
||||||
|
dataIndex: 'scope',
|
||||||
|
slotName: 'scope',
|
||||||
|
typeOptions: [
|
||||||
|
{
|
||||||
|
label: t('apiScenario.scenario'),
|
||||||
|
value: 'SCENARIO',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('apiScenario.step'),
|
||||||
|
value: 'STEP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
width: 80,
|
||||||
|
titleSlotName: 'typeTitle',
|
||||||
|
typeTitleTooltip: [t('apiScenario.params.csvScopedTip1'), t('apiScenario.params.csvScopedTip2')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiScenario.params.file',
|
||||||
|
dataIndex: 'file',
|
||||||
|
slotName: 'file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiScenario.table.columns.status',
|
||||||
|
dataIndex: 'enable',
|
||||||
|
slotName: 'enable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
slotName: 'operation',
|
||||||
|
dataIndex: 'operation',
|
||||||
|
width: 90,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const paramFormRef = ref<FormInstance>();
|
||||||
|
const paramForm = ref<CsvVariable>(cloneDeep(defaultCsvParamItem));
|
||||||
|
const encodingOptions = [
|
||||||
|
{
|
||||||
|
label: 'UTF-8',
|
||||||
|
value: 'UTF-8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UTF-16',
|
||||||
|
value: 'UTF-16',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'GBK',
|
||||||
|
value: 'GBK',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ISO-8859-15',
|
||||||
|
value: 'ISO-8859-15',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'US-ASCII',
|
||||||
|
value: 'US-ASCII',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleCsvVariablesChange(resultArr: any[], isInit?: boolean) {
|
||||||
|
csvVariables.value = [...resultArr];
|
||||||
|
if (!isInit) {
|
||||||
|
emit('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRecordConfig(record: CsvVariable) {
|
||||||
|
paramForm.value = cloneDeep(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelConfig(record: CsvVariable) {
|
||||||
|
paramFormRef.value?.resetFields();
|
||||||
|
record.settingVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyConfig() {
|
||||||
|
paramFormRef.value?.validate((errors) => {
|
||||||
|
if (!errors) {
|
||||||
|
let newArr = csvVariables.value.map((e) => {
|
||||||
|
if (e.id === paramForm.value.id) {
|
||||||
|
return {
|
||||||
|
...paramForm.value,
|
||||||
|
settingVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
if (newArr.findIndex((e) => e.id === paramForm.value.id) === newArr.length - 1) {
|
||||||
|
newArr = newArr.concat({
|
||||||
|
...cloneDeep(defaultCsvParamItem),
|
||||||
|
id: getGenerateId(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
csvVariables.value = newArr;
|
||||||
|
emit('change');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteIntercept(record: CsvVariable, deleteCall: () => void) {
|
||||||
|
if (!!record.name && !!record.file.fileId) {
|
||||||
|
// 删除有效参数才二次确认
|
||||||
|
openModal({
|
||||||
|
type: 'error',
|
||||||
|
title: t('apiScenario.deleteCsvConfirm', { name: record.name }),
|
||||||
|
content: t('apiScenario.deleteCsvConfirmContent'),
|
||||||
|
okText: t('common.confirmDelete'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
deleteCall();
|
||||||
|
Message.success(t('apiScenario.deleteCsvSuccess'));
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
deleteCall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeChangeIntercept(record: CsvVariable, doChange: () => void) {
|
||||||
|
if (!!record.name && !!record.file.fileId) {
|
||||||
|
// 更改有效参数才二次确认
|
||||||
|
record.scope = record.scope === 'SCENARIO' ? 'STEP' : 'SCENARIO'; // 先把值改回修改前的值
|
||||||
|
openModal({
|
||||||
|
type: 'warning',
|
||||||
|
title: t('apiScenario.changeScopeConfirm', {
|
||||||
|
type: record.scope === 'SCENARIO' ? t('apiScenario.step') : t('apiScenario.scenario'),
|
||||||
|
}),
|
||||||
|
content:
|
||||||
|
record.scope === 'SCENARIO'
|
||||||
|
? t('apiScenario.changeScopeToStepConfirmContent')
|
||||||
|
: t('apiScenario.changeScopeToScenarioConfirmContent'),
|
||||||
|
okText: t('apiScenario.confirmChange'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
record.scope = record.scope === 'SCENARIO' ? 'STEP' : 'SCENARIO';
|
||||||
|
doChange();
|
||||||
|
Message.success(t('apiScenario.changeScopeSuccess'));
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
doChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableChangeIntercept(record: CsvVariable, val: string | number | boolean) {
|
||||||
|
if (val) {
|
||||||
|
if (!record.name) {
|
||||||
|
Message.warning(t('apiScenario.csvNameNotNull'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!record.file.fileId) {
|
||||||
|
Message.warning(t('apiScenario.csvFileNotNull'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.scenario-csv-trigger {
|
||||||
|
@apply bg-white;
|
||||||
|
.scenario-csv-trigger-content {
|
||||||
|
padding: 16px;
|
||||||
|
width: 400px;
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 10%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 3px 14px 2px rgb(0 0 0 / 5%);
|
||||||
|
&::before {
|
||||||
|
@apply absolute left-0 top-0;
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
z-index: -1;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
border: 1px solid var(--color-text-input-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transform: scale(0.5, 0.5);
|
||||||
|
}
|
||||||
|
.scenario-csv-trigger-content-scroll {
|
||||||
|
.ms-scroll-bar();
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-right: -6px;
|
||||||
|
max-height: 400px;
|
||||||
|
.scenario-csv-trigger-content-scroll-preview {
|
||||||
|
@apply w-full overflow-y-auto overflow-x-hidden break-all;
|
||||||
|
.ms-scroll-bar();
|
||||||
|
|
||||||
|
max-height: 100px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<MsDrawer
|
||||||
|
v-model:visible="visible"
|
||||||
|
:width="680"
|
||||||
|
:title="t('apiScenario.quoteCsv')"
|
||||||
|
:ok-text="t('common.quote')"
|
||||||
|
:ok-disabled="propsRes.selectedKeys.size === 0 && !selectedKey"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<MsBaseTable v-bind="propsRes" v-model:selected-key="selectedKey" v-on="propsEvent">
|
||||||
|
<template #scope="{ record }">
|
||||||
|
{{ record.scope === 'scenario' ? t('apiScenario.scenario') : t('apiScenario.step') }}
|
||||||
|
</template>
|
||||||
|
<template #file="{ record }">
|
||||||
|
<a-tooltip :content="record.file?.name">
|
||||||
|
<div>{{ record.file?.name || '-' }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</MsBaseTable>
|
||||||
|
</MsDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
|
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
|
import useTable from '@/components/pure/ms-table/useTable';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { CsvVariable } from '@/models/apiTest/scenario';
|
||||||
|
|
||||||
|
import { defaultCsvParamItem } from '@/views/api-test/components/config';
|
||||||
|
import { filterKeyValParams } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
csvVariables: CsvVariable[];
|
||||||
|
excludeKeys?: string[];
|
||||||
|
isSingle?: boolean;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'confirm', selectedKeys: string[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedKey = ref('');
|
||||||
|
|
||||||
|
const columns: MsTableColumn = [
|
||||||
|
{
|
||||||
|
title: 'apiScenario.params.csvName',
|
||||||
|
dataIndex: 'name',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiScenario.params.csvScoped',
|
||||||
|
dataIndex: 'scope',
|
||||||
|
slotName: 'scope',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiScenario.params.file',
|
||||||
|
dataIndex: 'file',
|
||||||
|
slotName: 'file',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { propsRes, propsEvent } = useTable(undefined, {
|
||||||
|
columns,
|
||||||
|
scroll: { x: '100%' },
|
||||||
|
selectable: true,
|
||||||
|
showSelectorAll: false,
|
||||||
|
selectorType: props.isSingle ? 'radio' : 'checkbox',
|
||||||
|
firstColumnWidth: 44,
|
||||||
|
heightUsed: 122,
|
||||||
|
showPagination: false,
|
||||||
|
excludeKeys: new Set(props.excludeKeys || []),
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
propsRes.value.data = filterKeyValParams(props.csvVariables, defaultCsvParamItem).validParams.filter(
|
||||||
|
(e) => e.enable && !props.excludeKeys?.includes(e.id)
|
||||||
|
);
|
||||||
|
selectedKey.value = '';
|
||||||
|
propsRes.value.selectorType = props.isSingle ? 'radio' : 'checkbox';
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
emit('confirm', props.isSingle ? [selectedKey.value] : Array.from(propsRes.value.selectedKeys));
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
selectedKey.value = '';
|
||||||
|
propsRes.value.selectedKeys.clear();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -76,6 +76,7 @@ export const defaultStepItemCommon = {
|
||||||
executeStatus: undefined,
|
executeStatus: undefined,
|
||||||
isRefScenarioStep: false,
|
isRefScenarioStep: false,
|
||||||
isQuoteScenarioStep: false,
|
isQuoteScenarioStep: false,
|
||||||
|
csvIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultScenario: Scenario = {
|
export const defaultScenario: Scenario = {
|
||||||
|
|
|
@ -30,109 +30,12 @@
|
||||||
@change="handleCommonVariablesChange"
|
@change="handleCommonVariablesChange"
|
||||||
@batch-add="() => (batchAddKeyValVisible = true)"
|
@batch-add="() => (batchAddKeyValVisible = true)"
|
||||||
/>
|
/>
|
||||||
<paramTable
|
<csvParamsTable
|
||||||
v-else
|
v-else
|
||||||
v-model:params="csvVariables"
|
v-model:csvVariables="csvVariables"
|
||||||
:columns="csvColumns"
|
:scenario-id="props.scenarioId"
|
||||||
:default-param-item="defaultCsvParamItem"
|
@change="() => emit('change')"
|
||||||
:draggable="false"
|
/>
|
||||||
:selectable="false"
|
|
||||||
@change="handleCsvVariablesChange"
|
|
||||||
@batch-add="() => (batchAddKeyValVisible = true)"
|
|
||||||
>
|
|
||||||
<template #operationPre="{ record }">
|
|
||||||
<a-trigger
|
|
||||||
v-model:popup-visible="record.settingVisible"
|
|
||||||
trigger="click"
|
|
||||||
position="br"
|
|
||||||
class="scenario-csv-trigger"
|
|
||||||
>
|
|
||||||
<MsButton type="text" class="!mr-0" @click="handleRecordConfig(record)">
|
|
||||||
{{ t('apiScenario.params.config') }}
|
|
||||||
</MsButton>
|
|
||||||
<template #content>
|
|
||||||
<div class="scenario-csv-trigger-content">
|
|
||||||
<div class="mb-[16px] flex items-center">
|
|
||||||
<div class="font-semibold text-[var(--color-text-1)]">{{ t('apiScenario.params.csvConfig') }}</div>
|
|
||||||
<!-- <div class="text-[var(--color-text-4)]">({{ record.key }})</div> -->
|
|
||||||
</div>
|
|
||||||
<div class="scenario-csv-trigger-content-scroll">
|
|
||||||
<a-form ref="paramFormRef" :model="paramForm" layout="vertical">
|
|
||||||
<a-form-item
|
|
||||||
field="name"
|
|
||||||
:label="t('apiScenario.params.csvName')"
|
|
||||||
:rules="[{ required: true, message: t('apiScenario.params.csvNameNotNull') }]"
|
|
||||||
asterisk-position="end"
|
|
||||||
class="mb-[16px]"
|
|
||||||
>
|
|
||||||
<a-input v-model:model-value="paramForm.name" :max-length="255"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="variableNames" :label="t('apiScenario.params.csvParamName')" class="mb-[16px]">
|
|
||||||
<a-input
|
|
||||||
v-model:model-value="paramForm.variableNames"
|
|
||||||
:placeholder="t('apiScenario.params.csvParamNamePlaceholder')"
|
|
||||||
></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="encoding" :label="t('apiScenario.params.csvFileCode')" class="mb-[16px]">
|
|
||||||
<a-select
|
|
||||||
v-model:model-value="paramForm.encoding"
|
|
||||||
:options="encodingOptions"
|
|
||||||
class="w-[120px]"
|
|
||||||
></a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="delimiter" :label="t('apiScenario.params.csvSplitChar')" class="mb-[16px]">
|
|
||||||
<a-input
|
|
||||||
v-model:model-value="paramForm.delimiter"
|
|
||||||
:placeholder="t('common.pleaseInput')"
|
|
||||||
:max-length="64"
|
|
||||||
class="w-[120px]"
|
|
||||||
></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item
|
|
||||||
field="ignoreFirstLine"
|
|
||||||
:label="t('apiScenario.params.csvIgnoreFirstLine')"
|
|
||||||
class="mb-[16px]"
|
|
||||||
>
|
|
||||||
<a-radio-group v-model:model-value="paramForm.ignoreFirstLine">
|
|
||||||
<a-radio :value="false">False</a-radio>
|
|
||||||
<a-radio :value="true">True</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="random" :label="t('apiScenario.params.csvIsRandom')" class="mb-[16px]">
|
|
||||||
<a-radio-group v-model:model-value="paramForm.random">
|
|
||||||
<a-radio :value="false">False</a-radio>
|
|
||||||
<a-radio :value="true">True</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="allowQuotedData" :label="t('apiScenario.params.csvQuoteAllow')" class="mb-[16px]">
|
|
||||||
<a-radio-group v-model:model-value="paramForm.allowQuotedData">
|
|
||||||
<a-radio :value="false">False</a-radio>
|
|
||||||
<a-radio :value="true">True</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="recycleOnEof" :label="t('apiScenario.params.csvRecycle')" class="mb-[16px]">
|
|
||||||
<a-radio-group v-model:model-value="paramForm.recycleOnEof">
|
|
||||||
<a-radio :value="false">False</a-radio>
|
|
||||||
<a-radio :value="true">True</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item field="stopThreadOnEof" :label="t('apiScenario.params.csvStop')" class="mb-[16px]">
|
|
||||||
<a-radio-group v-model:model-value="paramForm.stopThreadOnEof">
|
|
||||||
<a-radio :value="false">False</a-radio>
|
|
||||||
<a-radio :value="true">True</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-end gap-[8px]">
|
|
||||||
<a-button type="secondary" @click="cancelConfig">{{ t('common.cancel') }}</a-button>
|
|
||||||
<a-button type="primary" @click="applyConfig">{{ t('ms.paramsInput.apply') }}</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-trigger>
|
|
||||||
</template>
|
|
||||||
</paramTable>
|
|
||||||
<batchAddKeyVal
|
<batchAddKeyVal
|
||||||
v-model:visible="batchAddKeyValVisible"
|
v-model:visible="batchAddKeyValVisible"
|
||||||
:params="commonVariables"
|
:params="commonVariables"
|
||||||
|
@ -144,18 +47,19 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { FormInstance } from '@arco-design/web-vue';
|
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
|
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import csvParamsTable from './common/csvParamsTable.vue';
|
||||||
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
|
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
|
||||||
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||||
|
|
||||||
import { CommonVariable, CsvVariable } from '@/models/apiTest/scenario';
|
import { CommonVariable, CsvVariable } from '@/models/apiTest/scenario';
|
||||||
|
|
||||||
import { defaultCsvParamItem, defaultNormalParamItem } from '@/views/api-test/components/config';
|
import { defaultNormalParamItem } from '@/views/api-test/components/config';
|
||||||
import { filterKeyValParams } from '@/views/api-test/components/utils';
|
import { filterKeyValParams } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
scenarioId?: string | number;
|
||||||
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'change'): void; // 数据发生变化
|
(e: 'change'): void; // 数据发生变化
|
||||||
}>();
|
}>();
|
||||||
|
@ -168,6 +72,7 @@
|
||||||
const csvVariables = defineModel<CsvVariable[]>('csvVariables', {
|
const csvVariables = defineModel<CsvVariable[]>('csvVariables', {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchValue = ref('');
|
const searchValue = ref('');
|
||||||
const firstSearch = ref(true);
|
const firstSearch = ref(true);
|
||||||
const backupParams = ref(commonVariables.value);
|
const backupParams = ref(commonVariables.value);
|
||||||
|
@ -235,14 +140,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCsvVariablesChange(resultArr: any[], isInit?: boolean) {
|
|
||||||
csvVariables.value = [...resultArr];
|
|
||||||
if (!isInit) {
|
|
||||||
emit('change');
|
|
||||||
firstSearch.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
if (firstSearch.value) {
|
if (firstSearch.value) {
|
||||||
|
@ -271,135 +168,6 @@
|
||||||
}
|
}
|
||||||
emit('change');
|
emit('change');
|
||||||
}
|
}
|
||||||
|
|
||||||
const csvColumns: ParamTableColumn[] = [
|
|
||||||
{
|
|
||||||
title: 'apiScenario.params.csvName',
|
|
||||||
dataIndex: 'name',
|
|
||||||
slotName: 'name',
|
|
||||||
needValidRepeat: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'apiScenario.params.csvScoped',
|
|
||||||
dataIndex: 'scope',
|
|
||||||
slotName: 'scope',
|
|
||||||
typeOptions: [
|
|
||||||
{
|
|
||||||
label: t('apiScenario.scenario'),
|
|
||||||
value: 'SCENARIO',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('apiScenario.step'),
|
|
||||||
value: 'STEP',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
width: 80,
|
|
||||||
titleSlotName: 'typeTitle',
|
|
||||||
typeTitleTooltip: [t('apiScenario.params.csvScopedTip1'), t('apiScenario.params.csvScopedTip2')],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'apiScenario.params.file',
|
|
||||||
dataIndex: 'file',
|
|
||||||
slotName: 'file',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'apiScenario.table.columns.status',
|
|
||||||
dataIndex: 'enable',
|
|
||||||
slotName: 'enable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
slotName: 'operation',
|
|
||||||
dataIndex: 'operation',
|
|
||||||
width: 90,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const paramFormRef = ref<FormInstance>();
|
|
||||||
const paramForm = ref<CsvVariable>(cloneDeep(defaultCsvParamItem));
|
|
||||||
const encodingOptions = [
|
|
||||||
{
|
|
||||||
label: 'UTF-8',
|
|
||||||
value: 'UTF-8',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'UTF-16',
|
|
||||||
value: 'UTF-16',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'GBK',
|
|
||||||
value: 'GBK',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'ISO-8859-15',
|
|
||||||
value: 'ISO-8859-15',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'US-ASCII',
|
|
||||||
value: 'US-ASCII',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function handleRecordConfig(record: CsvVariable) {
|
|
||||||
paramForm.value = cloneDeep(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelConfig() {
|
|
||||||
paramFormRef.value?.resetFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyConfig() {
|
|
||||||
paramFormRef.value?.validate((errors) => {
|
|
||||||
if (!errors) {
|
|
||||||
csvVariables.value = csvVariables.value.map((e) => {
|
|
||||||
if (e.id === paramForm.value.id) {
|
|
||||||
return {
|
|
||||||
...paramForm.value,
|
|
||||||
settingVisible: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
emit('change');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less"></style>
|
||||||
.scenario-csv-trigger {
|
|
||||||
@apply bg-white;
|
|
||||||
.scenario-csv-trigger-content {
|
|
||||||
padding: 16px;
|
|
||||||
width: 400px;
|
|
||||||
border-radius: var(--border-radius-medium);
|
|
||||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 10%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 3px 14px 2px rgb(0 0 0 / 5%);
|
|
||||||
&::before {
|
|
||||||
@apply absolute left-0 top-0;
|
|
||||||
|
|
||||||
content: '';
|
|
||||||
z-index: -1;
|
|
||||||
width: 200%;
|
|
||||||
height: 200%;
|
|
||||||
border: 1px solid var(--color-text-input-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
transform-origin: 0 0;
|
|
||||||
transform: scale(0.5, 0.5);
|
|
||||||
}
|
|
||||||
.scenario-csv-trigger-content-scroll {
|
|
||||||
.ms-scroll-bar();
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-right: -6px;
|
|
||||||
max-height: 400px;
|
|
||||||
.scenario-csv-trigger-content-scroll-preview {
|
|
||||||
@apply w-full overflow-y-auto overflow-x-hidden break-all;
|
|
||||||
.ms-scroll-bar();
|
|
||||||
|
|
||||||
max-height: 100px;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -145,6 +145,7 @@ export default function useCreateActions() {
|
||||||
children: item.children || [],
|
children: item.children || [],
|
||||||
stepType,
|
stepType,
|
||||||
refType,
|
refType,
|
||||||
|
csvIds: [],
|
||||||
originProjectId: item.originProjectId,
|
originProjectId: item.originProjectId,
|
||||||
copyFromStepId: item.copyFromStepId,
|
copyFromStepId: item.copyFromStepId,
|
||||||
...resourceField,
|
...resourceField,
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<a-popover
|
||||||
|
v-model:popup-visible="popoverVisible"
|
||||||
|
position="bl"
|
||||||
|
:disabled="!props.step.csvIds || props.step.csvIds.length === 0"
|
||||||
|
content-class="csv-popover"
|
||||||
|
arrow-class="hidden"
|
||||||
|
:popup-offset="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!props.step.isRefScenarioStep &&
|
||||||
|
props.step.stepType === ScenarioStepType.LOOP_CONTROLLER &&
|
||||||
|
props.step.csvIds?.length
|
||||||
|
"
|
||||||
|
class="csv-tag"
|
||||||
|
>
|
||||||
|
{{ `CSV ${props.step.csvIds?.length}` }}
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<div class="mb-[4px] font-medium text-[var(--color-text-4)]">
|
||||||
|
{{ `${t('apiScenario.csvQuote')}(${props.step.csvIds?.length})` }}
|
||||||
|
</div>
|
||||||
|
<div v-for="csv of csvList" :key="csv.id" class="flex items-center justify-between px-[8px] py-[4px]">
|
||||||
|
<a-tooltip :content="csv.name">
|
||||||
|
<div class="one-line-text w-[142px] text-[var(--color-text-1)]">
|
||||||
|
{{ csv.name }}
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<MsButton type="text" size="mini" class="!mr-0" @click="() => emit('replace', csv.id)">
|
||||||
|
{{ t('common.replace') }}
|
||||||
|
</MsButton>
|
||||||
|
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||||
|
<MsButton type="text" size="mini" class="!mr-0" @click="() => emit('remove', csv.id)">
|
||||||
|
{{ t('common.remove') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { CsvVariable, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||||
|
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
step: ScenarioStepItem;
|
||||||
|
csvVariables: CsvVariable[];
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'replace', id?: string): void;
|
||||||
|
(e: 'remove', id?: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const popoverVisible = ref(false);
|
||||||
|
|
||||||
|
const csvList = computed(() => {
|
||||||
|
if (props.step.csvIds) {
|
||||||
|
return props.step.csvIds
|
||||||
|
.map((id) => props.csvVariables.find((csv) => csv.id === id))
|
||||||
|
.filter((e) => e !== undefined) as CsvVariable[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.csv-popover {
|
||||||
|
padding: 6px;
|
||||||
|
.arco-popover-content {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.csv-tag {
|
||||||
|
@apply cursor-pointer bg-transparent;
|
||||||
|
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--color-text-input-border);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
color: var(--color-text-4);
|
||||||
|
line-height: 18px;
|
||||||
|
&:hover {
|
||||||
|
border-color: rgb(var(--primary-5));
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -95,6 +95,12 @@
|
||||||
@change="handleStepContentChange($event, step)"
|
@change="handleStepContentChange($event, step)"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
<csvTag
|
||||||
|
:step="step"
|
||||||
|
:csv-variables="scenario.scenarioConfig.variable.csvVariables"
|
||||||
|
@remove="(id) => removeCsv(step, id)"
|
||||||
|
@replace="(id) => replaceCsv(step, id)"
|
||||||
|
/>
|
||||||
<!-- 自定义请求、API、CASE、场景步骤名称 -->
|
<!-- 自定义请求、API、CASE、场景步骤名称 -->
|
||||||
<template v-if="checkStepIsApi(step)">
|
<template v-if="checkStepIsApi(step)">
|
||||||
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.config.method" />
|
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.config.method" />
|
||||||
|
@ -176,9 +182,9 @@
|
||||||
v-model:steps="steps"
|
v-model:steps="steps"
|
||||||
v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']"
|
v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']"
|
||||||
:step="step"
|
:step="step"
|
||||||
@click="setFocusNodeKey(step.uniqueId)"
|
@click="() => setFocusNodeKey(step.uniqueId)"
|
||||||
@other-create="handleOtherCreate"
|
@other-create="handleOtherCreate"
|
||||||
@close="setFocusNodeKey('')"
|
@close="() => setFocusNodeKey('')"
|
||||||
@add-done="handleAddStepDone"
|
@add-done="handleAddStepDone"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -223,9 +229,9 @@
|
||||||
:permission-map="permissionMap"
|
:permission-map="permissionMap"
|
||||||
:steps="steps"
|
:steps="steps"
|
||||||
@add-step="addCustomApiStep"
|
@add-step="addCustomApiStep"
|
||||||
@delete-step="deleteStep(activeStep)"
|
@delete-step="() => deleteStep(activeStep)"
|
||||||
@apply-step="applyApiStep"
|
@apply-step="applyApiStep"
|
||||||
@stop-debug="handleStopExecute(activeStep)"
|
@stop-debug="() => handleStopExecute(activeStep)"
|
||||||
@execute="handleApiExecute"
|
@execute="handleApiExecute"
|
||||||
@replace="handleReplaceStep"
|
@replace="handleReplaceStep"
|
||||||
/>
|
/>
|
||||||
|
@ -239,8 +245,8 @@
|
||||||
:step-responses="scenario.stepResponses"
|
:step-responses="scenario.stepResponses"
|
||||||
:permission-map="permissionMap"
|
:permission-map="permissionMap"
|
||||||
@apply-step="applyApiStep"
|
@apply-step="applyApiStep"
|
||||||
@delete-step="deleteStep(activeStep)"
|
@delete-step="() => deleteStep(activeStep)"
|
||||||
@stop-debug="handleStopExecute(activeStep)"
|
@stop-debug="() => handleStopExecute(activeStep)"
|
||||||
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
|
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
|
||||||
@replace="handleReplaceStep"
|
@replace="handleReplaceStep"
|
||||||
/>
|
/>
|
||||||
|
@ -439,6 +445,13 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
<quoteCsvDrawer
|
||||||
|
v-model:visible="csvDrawerVisible"
|
||||||
|
:csv-variables="scenario.scenarioConfig.variable.csvVariables"
|
||||||
|
:exclude-keys="activeStep?.csvIds"
|
||||||
|
:is-single="!!replaceCsvId"
|
||||||
|
@confirm="handleQuoteCsvConfirm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -451,12 +464,14 @@
|
||||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||||
import MsTree from '@/components/business/ms-tree/index.vue';
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
import { ImportData } from '../common/importApiDrawer/index.vue';
|
import { ImportData } from '../common/importApiDrawer/index.vue';
|
||||||
|
import quoteCsvDrawer from '../common/quoteCsvDrawer.vue';
|
||||||
import stepType from '../common/stepType/stepType.vue';
|
import stepType from '../common/stepType/stepType.vue';
|
||||||
import createStepActions from './createAction/createStepActions.vue';
|
import createStepActions from './createAction/createStepActions.vue';
|
||||||
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
|
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
|
||||||
import conditionContent from './stepNodeComposition/conditionContent.vue';
|
import conditionContent from './stepNodeComposition/conditionContent.vue';
|
||||||
|
import csvTag from './stepNodeComposition/csvTag.vue';
|
||||||
import loopControlContent from './stepNodeComposition/loopContent.vue';
|
import loopControlContent from './stepNodeComposition/loopContent.vue';
|
||||||
import quoteContent from './stepNodeComposition/quoteContent.vue';
|
import quoteContent from './stepNodeComposition/quoteContent.vue';
|
||||||
import waitTimeContent from './stepNodeComposition/waitTimeContent.vue';
|
import waitTimeContent from './stepNodeComposition/waitTimeContent.vue';
|
||||||
|
@ -464,37 +479,23 @@
|
||||||
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 saveAsApiModal from '@/views/api-test/components/saveAsApiModal.vue';
|
import saveAsApiModal from '@/views/api-test/components/saveAsApiModal.vue';
|
||||||
|
|
||||||
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
|
|
||||||
import { addCase, getDefinitionDetail } from '@/api/modules/api-test/management';
|
import { addCase, getDefinitionDetail } from '@/api/modules/api-test/management';
|
||||||
import { debugScenario, getScenarioDetail, getScenarioStep } from '@/api/modules/api-test/scenario';
|
import { getScenarioDetail, getScenarioStep } from '@/api/modules/api-test/scenario';
|
||||||
import { getSocket } from '@/api/modules/project-management/commonScript';
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import useModal from '@/hooks/useModal';
|
import useModal from '@/hooks/useModal';
|
||||||
import useAppStore from '@/store/modules/app';
|
import useAppStore from '@/store/modules/app';
|
||||||
import {
|
import { deleteNode, findNodeByKey, getGenerateId, insertNodes, mapTree, TreeNode } from '@/utils';
|
||||||
deleteNode,
|
|
||||||
findNodeByKey,
|
|
||||||
getGenerateId,
|
|
||||||
handleTreeDragDrop,
|
|
||||||
insertNodes,
|
|
||||||
mapTree,
|
|
||||||
traverseTree,
|
|
||||||
TreeNode,
|
|
||||||
} from '@/utils';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExecuteApiRequestFullParams,
|
ExecuteApiRequestFullParams,
|
||||||
ExecuteConditionProcessor,
|
ExecuteConditionProcessor,
|
||||||
ExecutePluginRequestParams,
|
ExecutePluginRequestParams,
|
||||||
RequestResult,
|
|
||||||
} from '@/models/apiTest/common';
|
} from '@/models/apiTest/common';
|
||||||
import { AddApiCaseParams } from '@/models/apiTest/management';
|
import { AddApiCaseParams } from '@/models/apiTest/management';
|
||||||
import {
|
import {
|
||||||
ApiScenarioDebugRequest,
|
|
||||||
CreateStepAction,
|
CreateStepAction,
|
||||||
Scenario,
|
Scenario,
|
||||||
ScenarioStepConfig,
|
ScenarioStepConfig,
|
||||||
ScenarioStepDetail,
|
|
||||||
ScenarioStepDetails,
|
ScenarioStepDetails,
|
||||||
ScenarioStepFileParams,
|
ScenarioStepFileParams,
|
||||||
ScenarioStepItem,
|
ScenarioStepItem,
|
||||||
|
@ -508,8 +509,10 @@
|
||||||
} from '@/enums/apiEnum';
|
} from '@/enums/apiEnum';
|
||||||
|
|
||||||
import type { RequestParam } from '../common/customApiDrawer.vue';
|
import type { RequestParam } from '../common/customApiDrawer.vue';
|
||||||
import updateStepStatus from '../utils';
|
|
||||||
import useCreateActions from './createAction/useCreateActions';
|
import useCreateActions from './createAction/useCreateActions';
|
||||||
|
import useStepExecute from './useStepExecute';
|
||||||
|
import useStepNodeEdit from './useStepNodeEdit';
|
||||||
|
import useStepOperation from './useStepOperation';
|
||||||
import { casePriorityOptions, caseStatusOptions } from '@/views/api-test/components/config';
|
import { casePriorityOptions, caseStatusOptions } from '@/views/api-test/components/config';
|
||||||
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
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';
|
||||||
|
@ -564,6 +567,15 @@
|
||||||
const activeStep = ref<ScenarioStepItem>(); // 用于弹窗配置时记录当前操作的步骤节点
|
const activeStep = ref<ScenarioStepItem>(); // 用于弹窗配置时记录当前操作的步骤节点
|
||||||
const activeStepByCreate = ref<ScenarioStepItem | undefined>(); // 用于抽屉操作创建步骤时记录当前操作的步骤节点
|
const activeStepByCreate = ref<ScenarioStepItem | undefined>(); // 用于抽屉操作创建步骤时记录当前操作的步骤节点
|
||||||
|
|
||||||
|
const { executeStep, handleApiExecute, handleStopExecute } = useStepExecute({
|
||||||
|
scenario,
|
||||||
|
steps,
|
||||||
|
stepDetails,
|
||||||
|
activeStep,
|
||||||
|
isPriorityLocalExec,
|
||||||
|
localExecuteUrl,
|
||||||
|
});
|
||||||
|
|
||||||
function setFocusNodeKey(id: string | number) {
|
function setFocusNodeKey(id: string | number) {
|
||||||
focusStepKey.value = id || '';
|
focusStepKey.value = id || '';
|
||||||
}
|
}
|
||||||
|
@ -705,6 +717,14 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if ((node as ScenarioStepItem).stepType === ScenarioStepType.LOOP_CONTROLLER) {
|
||||||
|
const arr = [...stepMoreActions];
|
||||||
|
arr.splice(1, 0, {
|
||||||
|
label: 'apiScenario.quoteCsv',
|
||||||
|
eventTag: 'quoteCsv',
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
return stepMoreActions;
|
return stepMoreActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -938,6 +958,33 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csvDrawerVisible = ref(false);
|
||||||
|
const replaceCsvId = ref('');
|
||||||
|
function removeCsv(step: ScenarioStepItem, id?: string) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (id && realStep) {
|
||||||
|
realStep.csvIds = realStep.csvIds.filter((item: string) => item !== id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceCsv(step: ScenarioStepItem, id?: string) {
|
||||||
|
csvDrawerVisible.value = true;
|
||||||
|
activeStep.value = step;
|
||||||
|
replaceCsvId.value = id || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuoteCsvConfirm(keys: string[]) {
|
||||||
|
if (activeStep.value) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.uniqueId, 'uniqueId');
|
||||||
|
if (replaceCsvId.value && realStep) {
|
||||||
|
const index = realStep.csvIds.findIndex((item: string) => item === replaceCsvId.value);
|
||||||
|
realStep.csvIds?.splice(index, 1, keys[0]);
|
||||||
|
} else if (realStep) {
|
||||||
|
realStep.csvIds?.push(...keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
|
function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
|
||||||
switch (item.eventTag) {
|
switch (item.eventTag) {
|
||||||
case 'copy':
|
case 'copy':
|
||||||
|
@ -1048,6 +1095,11 @@
|
||||||
activeStep.value = node as ScenarioStepItem;
|
activeStep.value = node as ScenarioStepItem;
|
||||||
saveCaseModalVisible.value = true;
|
saveCaseModalVisible.value = true;
|
||||||
break;
|
break;
|
||||||
|
case 'quoteCsv':
|
||||||
|
activeStep.value = node as ScenarioStepItem;
|
||||||
|
replaceCsvId.value = '';
|
||||||
|
csvDrawerVisible.value = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1062,87 +1114,11 @@
|
||||||
*/
|
*/
|
||||||
const showStepNameEditInputStepId = ref<string | number>('');
|
const showStepNameEditInputStepId = ref<string | number>('');
|
||||||
const tempStepName = ref('');
|
const tempStepName = ref('');
|
||||||
function handleStepNameClick(step: ScenarioStepItem) {
|
|
||||||
tempStepName.value = step.name;
|
|
||||||
showStepNameEditInputStepId.value = step.uniqueId;
|
|
||||||
nextTick(() => {
|
|
||||||
// 等待输入框渲染完成后聚焦
|
|
||||||
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
|
||||||
input?.focus();
|
|
||||||
});
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.draggable = false; // 编辑时禁止拖拽
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyStepNameChange(step: ScenarioStepItem) {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.name = tempStepName.value || realStep.name;
|
|
||||||
realStep.draggable = true; // 编辑完恢复拖拽
|
|
||||||
}
|
|
||||||
showStepNameEditInputStepId.value = '';
|
|
||||||
scenario.value.unSaved = !!tempStepName.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理非 api、case、场景步骤名称编辑
|
* 处理非 api、case、场景步骤名称编辑
|
||||||
*/
|
*/
|
||||||
const showStepDescEditInputStepId = ref<string | number>('');
|
const showStepDescEditInputStepId = ref<string | number>('');
|
||||||
const tempStepDesc = ref('');
|
const tempStepDesc = ref('');
|
||||||
function handleStepDescClick(step: ScenarioStepItem) {
|
|
||||||
tempStepDesc.value = step.name;
|
|
||||||
showStepDescEditInputStepId.value = step.uniqueId;
|
|
||||||
nextTick(() => {
|
|
||||||
// 等待输入框渲染完成后聚焦
|
|
||||||
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
|
||||||
input?.focus();
|
|
||||||
});
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.draggable = false; // 编辑时禁止拖拽
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyStepDescChange(step: ScenarioStepItem) {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.name = tempStepDesc.value || realStep.name;
|
|
||||||
realStep.draggable = true; // 编辑完恢复拖拽
|
|
||||||
}
|
|
||||||
showStepDescEditInputStepId.value = '';
|
|
||||||
scenario.value.unSaved = !!tempStepDesc.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStepContentChange($event: Record<string, any>, step: ScenarioStepItem) {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
Object.keys($event).forEach((key) => {
|
|
||||||
realStep.config[key] = $event[key];
|
|
||||||
});
|
|
||||||
scenario.value.unSaved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理步骤展开折叠
|
|
||||||
*/
|
|
||||||
function handleStepExpand(data: MsTreeExpandedData) {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.expanded = !realStep.expanded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStepToggleEnable(data: ScenarioStepItem) {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.enable = !realStep.enable;
|
|
||||||
scenario.value.unSaved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const importApiDrawerVisible = ref(false);
|
const importApiDrawerVisible = ref(false);
|
||||||
const customCaseDrawerVisible = ref(false);
|
const customCaseDrawerVisible = ref(false);
|
||||||
const customApiDrawerVisible = ref(false);
|
const customApiDrawerVisible = ref(false);
|
||||||
|
@ -1167,240 +1143,17 @@
|
||||||
scenario.value.unSaved = true;
|
scenario.value.unSaved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const { handleStepExpand, handleStepSelect, deleteStep, handleDrop } = useStepOperation({
|
||||||
* 处理步骤选中事件
|
scenario,
|
||||||
* @param _selectedKeys 选中的 key集合
|
steps,
|
||||||
* @param step 点击的步骤节点
|
stepDetails,
|
||||||
*/
|
activeStep,
|
||||||
async function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) {
|
selectedKeys,
|
||||||
const _stepType = getStepType(step);
|
customApiDrawerVisible,
|
||||||
const offspringIds: string[] = [];
|
customCaseDrawerVisible,
|
||||||
mapTree(step.children || [], (e) => {
|
scriptOperationDrawerVisible,
|
||||||
offspringIds.push(e.uniqueId);
|
loading,
|
||||||
return e;
|
|
||||||
});
|
});
|
||||||
selectedKeys.value = [step.uniqueId, ...offspringIds];
|
|
||||||
if (_stepType.isCopyApi || _stepType.isQuoteApi || step.stepType === ScenarioStepType.CUSTOM_REQUEST) {
|
|
||||||
// 复制 api、引用 api、自定义 api打开抽屉
|
|
||||||
activeStep.value = step;
|
|
||||||
if (
|
|
||||||
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
|
||||||
(stepDetails.value[step.id] === undefined && !step.isNew)
|
|
||||||
) {
|
|
||||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
|
||||||
await getStepDetail(step);
|
|
||||||
}
|
|
||||||
customApiDrawerVisible.value = true;
|
|
||||||
} else if (step.stepType === ScenarioStepType.API_CASE) {
|
|
||||||
activeStep.value = step;
|
|
||||||
if (
|
|
||||||
_stepType.isCopyCase &&
|
|
||||||
((stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
|
||||||
(stepDetails.value[step.id] === undefined && !step.isNew))
|
|
||||||
) {
|
|
||||||
// 只有复制的 case 需要查看步骤详情,引用的无法更改所以不需要在此初始化详情
|
|
||||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
|
||||||
await getStepDetail(step);
|
|
||||||
}
|
|
||||||
customCaseDrawerVisible.value = true;
|
|
||||||
} else if (step.stepType === ScenarioStepType.SCRIPT) {
|
|
||||||
activeStep.value = step;
|
|
||||||
if (
|
|
||||||
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
|
||||||
(stepDetails.value[step.id] === undefined && !step.isNew)
|
|
||||||
) {
|
|
||||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
|
||||||
await getStepDetail(step);
|
|
||||||
}
|
|
||||||
scriptOperationDrawerVisible.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const websocketMap: Record<string | number, WebSocket> = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开启websocket监听,接收执行结果
|
|
||||||
*/
|
|
||||||
function debugSocket(step: ScenarioStepItem, _scenario: Scenario, reportId: string | number) {
|
|
||||||
websocketMap[reportId] = getSocket(
|
|
||||||
reportId || '',
|
|
||||||
scenario.value.executeType === 'localExec' ? '/ws/debug' : '',
|
|
||||||
scenario.value.executeType === 'localExec' ? localExecuteUrl?.value : ''
|
|
||||||
);
|
|
||||||
websocketMap[reportId].addEventListener('message', (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
if (data.msgType === 'EXEC_RESULT') {
|
|
||||||
if (step.reportId === data.reportId) {
|
|
||||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
|
||||||
data.taskResult.requestResults.forEach((result: RequestResult) => {
|
|
||||||
if (_scenario.stepResponses[result.stepId] === undefined) {
|
|
||||||
_scenario.stepResponses[result.stepId] = [];
|
|
||||||
}
|
|
||||||
_scenario.stepResponses[result.stepId].push({
|
|
||||||
...result,
|
|
||||||
console: data.taskResult.console,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (data.msgType === 'EXEC_END') {
|
|
||||||
// 执行结束,关闭websocket
|
|
||||||
websocketMap[reportId]?.close();
|
|
||||||
if (step.reportId === data.reportId) {
|
|
||||||
step.isExecuting = false;
|
|
||||||
updateStepStatus([step], _scenario.stepResponses, step.uniqueId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function realExecute(
|
|
||||||
executeParams: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId' | 'stepFileParam'>
|
|
||||||
) {
|
|
||||||
const [currentStep] = executeParams.steps;
|
|
||||||
try {
|
|
||||||
currentStep.isExecuting = true;
|
|
||||||
currentStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
|
||||||
debugSocket(currentStep, scenario.value, executeParams.reportId); // 开启websocket
|
|
||||||
const res = await debugScenario({
|
|
||||||
id: scenario.value.id || '',
|
|
||||||
grouped: false,
|
|
||||||
environmentId: appStore.currentEnvConfig?.id || '',
|
|
||||||
projectId: appStore.currentProjectId,
|
|
||||||
scenarioConfig: scenario.value.scenarioConfig,
|
|
||||||
frontendDebug: scenario.value.executeType === 'localExec',
|
|
||||||
...executeParams,
|
|
||||||
steps: mapTree(executeParams.steps, (node) => {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
enable: node.uniqueId === currentStep.uniqueId || node.enable, // 单步骤执行,则临时无视顶层启用禁用状态
|
|
||||||
parent: null, // 原树形结构存在循环引用,这里要去掉以免 axios 序列化失败
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (scenario.value.executeType === 'localExec' && localExecuteUrl?.value) {
|
|
||||||
await localExecuteApiDebug(localExecuteUrl.value, res);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error);
|
|
||||||
websocketMap[executeParams.reportId].close();
|
|
||||||
currentStep.isExecuting = false;
|
|
||||||
updateStepStatus([currentStep], scenario.value.stepResponses, currentStep.uniqueId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 单个步骤执行调试
|
|
||||||
*/
|
|
||||||
function executeStep(node: MsTreeNodeData) {
|
|
||||||
if (node.isExecuting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.uniqueId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
realStep.reportId = getGenerateId();
|
|
||||||
const _stepDetails: Record<string, any> = {};
|
|
||||||
const stepFileParam = scenario.value.stepFileParam[realStep.id];
|
|
||||||
traverseTree(
|
|
||||||
realStep,
|
|
||||||
(step) => {
|
|
||||||
if (step.enable || step.uniqueId === realStep.uniqueId) {
|
|
||||||
// 启用的步骤才执行;如果点击的是禁用步骤也执行,但是禁用的子步骤不执行
|
|
||||||
_stepDetails[step.id] = stepDetails.value[step.id];
|
|
||||||
step.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
|
||||||
} else {
|
|
||||||
step.executeStatus = undefined;
|
|
||||||
}
|
|
||||||
delete scenario.value.stepResponses[step.uniqueId]; // 先移除上一次的执行结果
|
|
||||||
},
|
|
||||||
(step) => {
|
|
||||||
// 当前步骤是启用的情或是在禁用的步骤上点击执行,才需要继续递归子孙步骤;否则无需向下递归
|
|
||||||
return step.enable || step.uniqueId === realStep.uniqueId;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
scenario.value.executeType = isPriorityLocalExec?.value ? 'localExec' : 'serverExec';
|
|
||||||
realExecute({
|
|
||||||
steps: [realStep as ScenarioStepItem],
|
|
||||||
stepDetails: _stepDetails,
|
|
||||||
reportId: realStep.reportId,
|
|
||||||
stepFileParam: {
|
|
||||||
[realStep.id]: stepFileParam,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 api 详情抽屉的执行动作
|
|
||||||
* @param request 抽屉内的请求参数
|
|
||||||
* @param executeType 执行类型
|
|
||||||
*/
|
|
||||||
function handleApiExecute(request: RequestParam, executeType?: 'localExec' | 'serverExec') {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, request.stepId, 'uniqueId');
|
|
||||||
if (realStep) {
|
|
||||||
delete scenario.value.stepResponses[realStep.uniqueId]; // 先移除上一次的执行结果
|
|
||||||
realStep.reportId = getGenerateId();
|
|
||||||
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
|
||||||
request.executeLoading = true;
|
|
||||||
scenario.value.executeType = executeType;
|
|
||||||
realExecute({
|
|
||||||
steps: [realStep as ScenarioStepItem],
|
|
||||||
stepDetails: {
|
|
||||||
[realStep.id]: request,
|
|
||||||
},
|
|
||||||
reportId: realStep.reportId,
|
|
||||||
stepFileParam: {
|
|
||||||
[realStep.uniqueId]: {
|
|
||||||
uploadFileIds: request.uploadFileIds || [],
|
|
||||||
linkFileIds: request.linkFileIds || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 步骤列表找不到该步骤,说明是新建的自定义请求还未保存,则临时创建一个步骤进行调试(不保存步骤信息)
|
|
||||||
delete scenario.value.stepResponses[request.stepId]; // 先移除上一次的执行结果
|
|
||||||
const reportId = getGenerateId();
|
|
||||||
request.executeLoading = true;
|
|
||||||
activeStep.value = {
|
|
||||||
id: request.stepId,
|
|
||||||
name: t('apiScenario.customApi'),
|
|
||||||
stepType: ScenarioStepType.CUSTOM_REQUEST,
|
|
||||||
refType: ScenarioStepRefType.DIRECT,
|
|
||||||
sort: 1,
|
|
||||||
enable: true,
|
|
||||||
isNew: true,
|
|
||||||
config: {},
|
|
||||||
projectId: appStore.currentProjectId,
|
|
||||||
isExecuting: false,
|
|
||||||
reportId,
|
|
||||||
uniqueId: request.stepId,
|
|
||||||
};
|
|
||||||
realExecute({
|
|
||||||
steps: [activeStep.value],
|
|
||||||
stepDetails: {
|
|
||||||
[request.stepId]: request,
|
|
||||||
},
|
|
||||||
reportId,
|
|
||||||
stepFileParam: {
|
|
||||||
[request.stepId]: {
|
|
||||||
uploadFileIds: request.uploadFileIds || [],
|
|
||||||
linkFileIds: request.linkFileIds || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleStopExecute(step?: ScenarioStepItem) {
|
|
||||||
if (step?.reportId) {
|
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
websocketMap[step.reportId].close();
|
|
||||||
if (realStep) {
|
|
||||||
realStep.isExecuting = false;
|
|
||||||
updateStepStatus([realStep as ScenarioStepItem], scenario.value.stepResponses, realStep.uniqueId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleReplaceStep(newStep: ScenarioStepItem) {
|
function handleReplaceStep(newStep: ScenarioStepItem) {
|
||||||
if (activeStep.value) {
|
if (activeStep.value) {
|
||||||
|
@ -1616,34 +1369,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除
|
|
||||||
*/
|
|
||||||
function deleteStep(step?: ScenarioStepItem) {
|
|
||||||
if (step) {
|
|
||||||
openModal({
|
|
||||||
type: 'error',
|
|
||||||
title: t('common.tip'),
|
|
||||||
content: t('apiScenario.deleteStepConfirm', { name: step.name }),
|
|
||||||
okText: t('common.confirmDelete'),
|
|
||||||
cancelText: t('common.cancel'),
|
|
||||||
okButtonProps: {
|
|
||||||
status: 'danger',
|
|
||||||
},
|
|
||||||
maskClosable: false,
|
|
||||||
onBeforeOk: async () => {
|
|
||||||
customCaseDrawerVisible.value = false;
|
|
||||||
customApiDrawerVisible.value = false;
|
|
||||||
deleteNode(steps.value, step.uniqueId, 'uniqueId');
|
|
||||||
activeStep.value = undefined;
|
|
||||||
scenario.value.unSaved = true;
|
|
||||||
Message.success(t('common.deleteSuccess'));
|
|
||||||
},
|
|
||||||
hideCancel: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加脚本操作步骤
|
* 添加脚本操作步骤
|
||||||
*/
|
*/
|
||||||
|
@ -1693,126 +1418,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 释放允许拖拽步骤到释放的节点内
|
|
||||||
* @param dropNode 释放节点
|
|
||||||
*/
|
|
||||||
function isAllowDropInside(dropNode: MsTreeNodeData) {
|
|
||||||
return (
|
|
||||||
// 逻辑控制器内可以拖拽任意类型的步骤
|
|
||||||
[
|
|
||||||
ScenarioStepType.LOOP_CONTROLLER,
|
|
||||||
ScenarioStepType.IF_CONTROLLER,
|
|
||||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
|
||||||
].includes(dropNode.stepType) ||
|
|
||||||
// 复制的场景内可以释放任意类型的步骤
|
|
||||||
(dropNode.stepType === ScenarioStepType.API_SCENARIO && dropNode.refType === ScenarioStepRefType.COPY)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理步骤节点拖拽事件
|
|
||||||
* @param tree 树数据
|
|
||||||
* @param dragNode 拖拽节点
|
|
||||||
* @param dropNode 释放节点
|
|
||||||
* @param dropPosition 释放位置(取值:-1,,0,,1。 -1:dropNodeId节点之前。 0:dropNodeId节点内。 1:dropNodeId节点后)
|
|
||||||
*/
|
|
||||||
function handleDrop(
|
|
||||||
tree: MsTreeNodeData[],
|
|
||||||
dragNode: MsTreeNodeData,
|
|
||||||
dropNode: MsTreeNodeData,
|
|
||||||
dropPosition: number
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (dropPosition === 0 && !isAllowDropInside(dropNode)) {
|
|
||||||
// Message.error(t('apiScenario.notAllowDropInside')); TODO:不允许释放提示
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
const offspringIds: string[] = [];
|
|
||||||
mapTree(cloneDeep(dragNode.children || []), (e) => {
|
|
||||||
offspringIds.push(e.uniqueId);
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
const stepIdAndOffspringIds = [dragNode.uniqueId, ...offspringIds];
|
|
||||||
if (dropPosition === 0) {
|
|
||||||
// 拖拽到节点内
|
|
||||||
if (selectedKeys.value.includes(dropNode.uniqueId)) {
|
|
||||||
// 释放位置的节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
|
||||||
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
|
|
||||||
}
|
|
||||||
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.uniqueId)) {
|
|
||||||
// 释放位置的节点的父节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
|
||||||
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
|
|
||||||
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.uniqueId)) {
|
|
||||||
// 如果被拖动的节点的父节点在选中的节点中,则需要把被拖动的节点及其子孙节点从选中的节点中移除
|
|
||||||
selectedKeys.value = selectedKeys.value.filter((e) => {
|
|
||||||
for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
|
|
||||||
const id = stepIdAndOffspringIds[i];
|
|
||||||
if (e === id) {
|
|
||||||
stepIdAndOffspringIds.splice(i, 1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'uniqueId');
|
|
||||||
if (dragResult) {
|
|
||||||
Message.success(t('common.moveSuccess'));
|
|
||||||
scenario.value.unSaved = true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
nextTick(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showQuickInput = ref(false);
|
const showQuickInput = ref(false);
|
||||||
const quickInputParamValue = ref<any>('');
|
const quickInputParamValue = ref<any>('');
|
||||||
const quickInputDataKey = ref('');
|
const quickInputDataKey = ref('');
|
||||||
|
|
||||||
function setQuickInput(step: ScenarioStepItem, dataKey: keyof ScenarioStepDetail) {
|
const {
|
||||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
setQuickInput,
|
||||||
if (realStep) {
|
clearQuickInput,
|
||||||
activeStep.value = realStep as ScenarioStepItem;
|
applyQuickInput,
|
||||||
}
|
handleStepDescClick,
|
||||||
quickInputDataKey.value = dataKey;
|
applyStepDescChange,
|
||||||
quickInputParamValue.value = step.config?.[dataKey] || '';
|
handleStepContentChange,
|
||||||
if (quickInputDataKey.value === 'msWhileVariableValue' && activeStep.value?.config.whileController) {
|
handleStepToggleEnable,
|
||||||
quickInputParamValue.value = activeStep.value.config.whileController.msWhileVariable.value;
|
handleStepNameClick,
|
||||||
} else if (quickInputDataKey.value === 'msWhileVariableScriptValue' && activeStep.value?.config.whileController) {
|
applyStepNameChange,
|
||||||
quickInputParamValue.value = activeStep.value.config.whileController.msWhileScript.scriptValue;
|
} = useStepNodeEdit({
|
||||||
} else if (quickInputDataKey.value === 'conditionValue' && activeStep.value?.config) {
|
steps,
|
||||||
quickInputParamValue.value = activeStep.value.config.value || '';
|
scenario,
|
||||||
}
|
activeStep,
|
||||||
showQuickInput.value = true;
|
quickInputDataKey,
|
||||||
}
|
quickInputParamValue,
|
||||||
|
showQuickInput,
|
||||||
function clearQuickInput() {
|
treeRef,
|
||||||
activeStep.value = undefined;
|
tempStepDesc,
|
||||||
quickInputParamValue.value = '';
|
showStepDescEditInputStepId,
|
||||||
quickInputDataKey.value = '';
|
tempStepName,
|
||||||
}
|
showStepNameEditInputStepId,
|
||||||
|
});
|
||||||
function applyQuickInput() {
|
|
||||||
if (activeStep.value) {
|
|
||||||
if (quickInputDataKey.value === 'msWhileVariableValue' && activeStep.value.config.whileController) {
|
|
||||||
activeStep.value.config.whileController.msWhileVariable.value = quickInputParamValue.value;
|
|
||||||
} else if (quickInputDataKey.value === 'msWhileVariableScriptValue' && activeStep.value.config.whileController) {
|
|
||||||
activeStep.value.config.whileController.msWhileScript.scriptValue = quickInputParamValue.value;
|
|
||||||
} else if (quickInputDataKey.value === 'conditionValue' && activeStep.value.config) {
|
|
||||||
activeStep.value.config.value = quickInputParamValue.value;
|
|
||||||
}
|
|
||||||
showQuickInput.value = false;
|
|
||||||
clearQuickInput();
|
|
||||||
scenario.value.unSaved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbClick = ref({
|
const dbClick = ref({
|
||||||
e: null as MouseEvent | null,
|
e: null as MouseEvent | null,
|
||||||
|
@ -1937,8 +1569,6 @@
|
||||||
}
|
}
|
||||||
.ms-tree-node-extra {
|
.ms-tree-node-extra {
|
||||||
@apply !visible !w-auto;
|
@apply !visible !w-auto;
|
||||||
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ms-form {
|
.ms-form {
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
|
||||||
|
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
|
||||||
|
import { debugScenario } from '@/api/modules/api-test/scenario';
|
||||||
|
import { getSocket } from '@/api/modules/project-management/commonScript';
|
||||||
|
import { t } from '@/hooks/useI18n';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { findNodeByKey, getGenerateId, mapTree, traverseTree } from '@/utils';
|
||||||
|
|
||||||
|
import type { RequestResult } from '@/models/apiTest/common';
|
||||||
|
import type { ApiScenarioDebugRequest, Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||||
|
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
import type { RequestParam } from '../common/customApiDrawer.vue';
|
||||||
|
import updateStepStatus from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤执行逻辑
|
||||||
|
*/
|
||||||
|
export default function useStepExecute({
|
||||||
|
scenario,
|
||||||
|
steps,
|
||||||
|
stepDetails,
|
||||||
|
activeStep,
|
||||||
|
isPriorityLocalExec,
|
||||||
|
localExecuteUrl,
|
||||||
|
}: {
|
||||||
|
scenario: Ref<Scenario>;
|
||||||
|
steps: Ref<ScenarioStepItem[]>;
|
||||||
|
stepDetails: Ref<Record<string, any>>;
|
||||||
|
activeStep: Ref<ScenarioStepItem | undefined>;
|
||||||
|
isPriorityLocalExec: Ref<boolean> | undefined;
|
||||||
|
localExecuteUrl: Ref<string> | undefined;
|
||||||
|
}) {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const websocketMap: Record<string | number, WebSocket> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启websocket监听,接收执行结果
|
||||||
|
*/
|
||||||
|
function debugSocket(step: ScenarioStepItem, _scenario: Scenario, reportId: string | number) {
|
||||||
|
websocketMap[reportId] = getSocket(
|
||||||
|
reportId || '',
|
||||||
|
scenario.value.executeType === 'localExec' ? '/ws/debug' : '',
|
||||||
|
scenario.value.executeType === 'localExec' ? localExecuteUrl?.value : ''
|
||||||
|
);
|
||||||
|
websocketMap[reportId].addEventListener('message', (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.msgType === 'EXEC_RESULT') {
|
||||||
|
if (step.reportId === data.reportId) {
|
||||||
|
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||||
|
data.taskResult.requestResults.forEach((result: RequestResult) => {
|
||||||
|
if (_scenario.stepResponses[result.stepId] === undefined) {
|
||||||
|
_scenario.stepResponses[result.stepId] = [];
|
||||||
|
}
|
||||||
|
_scenario.stepResponses[result.stepId].push({
|
||||||
|
...result,
|
||||||
|
console: data.taskResult.console,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.msgType === 'EXEC_END') {
|
||||||
|
// 执行结束,关闭websocket
|
||||||
|
websocketMap[reportId]?.close();
|
||||||
|
if (step.reportId === data.reportId) {
|
||||||
|
step.isExecuting = false;
|
||||||
|
updateStepStatus([step], _scenario.stepResponses, step.uniqueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function realExecute(
|
||||||
|
executeParams: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId' | 'stepFileParam'>
|
||||||
|
) {
|
||||||
|
const [currentStep] = executeParams.steps;
|
||||||
|
try {
|
||||||
|
currentStep.isExecuting = true;
|
||||||
|
currentStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||||
|
debugSocket(currentStep, scenario.value, executeParams.reportId); // 开启websocket
|
||||||
|
const res = await debugScenario({
|
||||||
|
id: scenario.value.id || '',
|
||||||
|
grouped: false,
|
||||||
|
environmentId: appStore.currentEnvConfig?.id || '',
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
scenarioConfig: scenario.value.scenarioConfig,
|
||||||
|
frontendDebug: scenario.value.executeType === 'localExec',
|
||||||
|
...executeParams,
|
||||||
|
steps: mapTree(executeParams.steps, (node) => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
enable: node.uniqueId === currentStep.uniqueId || node.enable, // 单步骤执行,则临时无视顶层启用禁用状态
|
||||||
|
parent: null, // 原树形结构存在循环引用,这里要去掉以免 axios 序列化失败
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (scenario.value.executeType === 'localExec' && localExecuteUrl?.value) {
|
||||||
|
await localExecuteApiDebug(localExecuteUrl.value, res);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
websocketMap[executeParams.reportId].close();
|
||||||
|
currentStep.isExecuting = false;
|
||||||
|
updateStepStatus([currentStep], scenario.value.stepResponses, currentStep.uniqueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个步骤执行调试
|
||||||
|
*/
|
||||||
|
function executeStep(node: MsTreeNodeData) {
|
||||||
|
if (node.isExecuting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.reportId = getGenerateId();
|
||||||
|
const _stepDetails: Record<string, any> = {};
|
||||||
|
const stepFileParam = scenario.value.stepFileParam[realStep.id];
|
||||||
|
traverseTree(
|
||||||
|
realStep,
|
||||||
|
(step) => {
|
||||||
|
if (step.enable || step.uniqueId === realStep.uniqueId) {
|
||||||
|
// 启用的步骤才执行;如果点击的是禁用步骤也执行,但是禁用的子步骤不执行
|
||||||
|
_stepDetails[step.id] = stepDetails.value[step.id];
|
||||||
|
step.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||||
|
} else {
|
||||||
|
step.executeStatus = undefined;
|
||||||
|
}
|
||||||
|
delete scenario.value.stepResponses[step.uniqueId]; // 先移除上一次的执行结果
|
||||||
|
},
|
||||||
|
(step) => {
|
||||||
|
// 当前步骤是启用的情或是在禁用的步骤上点击执行,才需要继续递归子孙步骤;否则无需向下递归
|
||||||
|
return step.enable || step.uniqueId === realStep.uniqueId;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
scenario.value.executeType = isPriorityLocalExec?.value ? 'localExec' : 'serverExec';
|
||||||
|
realExecute({
|
||||||
|
steps: [realStep as ScenarioStepItem],
|
||||||
|
stepDetails: _stepDetails,
|
||||||
|
reportId: realStep.reportId,
|
||||||
|
stepFileParam: {
|
||||||
|
[realStep.id]: stepFileParam,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 api 详情抽屉的执行动作
|
||||||
|
* @param request 抽屉内的请求参数
|
||||||
|
* @param executeType 执行类型
|
||||||
|
*/
|
||||||
|
function handleApiExecute(request: RequestParam, executeType?: 'localExec' | 'serverExec') {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, request.stepId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
delete scenario.value.stepResponses[realStep.uniqueId]; // 先移除上一次的执行结果
|
||||||
|
realStep.reportId = getGenerateId();
|
||||||
|
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||||
|
request.executeLoading = true;
|
||||||
|
scenario.value.executeType = executeType;
|
||||||
|
realExecute({
|
||||||
|
steps: [realStep as ScenarioStepItem],
|
||||||
|
stepDetails: {
|
||||||
|
[realStep.id]: request,
|
||||||
|
},
|
||||||
|
reportId: realStep.reportId,
|
||||||
|
stepFileParam: {
|
||||||
|
[realStep.uniqueId]: {
|
||||||
|
uploadFileIds: request.uploadFileIds || [],
|
||||||
|
linkFileIds: request.linkFileIds || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 步骤列表找不到该步骤,说明是新建的自定义请求还未保存,则临时创建一个步骤进行调试(不保存步骤信息)
|
||||||
|
delete scenario.value.stepResponses[request.stepId]; // 先移除上一次的执行结果
|
||||||
|
const reportId = getGenerateId();
|
||||||
|
request.executeLoading = true;
|
||||||
|
activeStep.value = {
|
||||||
|
id: request.stepId,
|
||||||
|
name: t('apiScenario.customApi'),
|
||||||
|
stepType: ScenarioStepType.CUSTOM_REQUEST,
|
||||||
|
refType: ScenarioStepRefType.DIRECT,
|
||||||
|
sort: 1,
|
||||||
|
enable: true,
|
||||||
|
isNew: true,
|
||||||
|
config: {},
|
||||||
|
projectId: appStore.currentProjectId,
|
||||||
|
isExecuting: false,
|
||||||
|
reportId,
|
||||||
|
uniqueId: request.stepId,
|
||||||
|
};
|
||||||
|
realExecute({
|
||||||
|
steps: [activeStep.value],
|
||||||
|
stepDetails: {
|
||||||
|
[request.stepId]: request,
|
||||||
|
},
|
||||||
|
reportId,
|
||||||
|
stepFileParam: {
|
||||||
|
[request.stepId]: {
|
||||||
|
uploadFileIds: request.uploadFileIds || [],
|
||||||
|
linkFileIds: request.linkFileIds || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStopExecute(step?: ScenarioStepItem) {
|
||||||
|
if (step?.reportId) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
websocketMap[step.reportId].close();
|
||||||
|
if (realStep) {
|
||||||
|
realStep.isExecuting = false;
|
||||||
|
updateStepStatus([realStep as ScenarioStepItem], scenario.value.stepResponses, realStep.uniqueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeStep,
|
||||||
|
handleApiExecute,
|
||||||
|
handleStopExecute,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
|
|
||||||
|
import { findNodeByKey } from '@/utils';
|
||||||
|
|
||||||
|
import type { Scenario, ScenarioStepDetail, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤节点信息更改
|
||||||
|
*/
|
||||||
|
export default function useStepNodeEdit({
|
||||||
|
steps,
|
||||||
|
scenario,
|
||||||
|
activeStep,
|
||||||
|
quickInputDataKey,
|
||||||
|
quickInputParamValue,
|
||||||
|
showQuickInput,
|
||||||
|
treeRef,
|
||||||
|
tempStepDesc,
|
||||||
|
showStepDescEditInputStepId,
|
||||||
|
tempStepName,
|
||||||
|
showStepNameEditInputStepId,
|
||||||
|
}: {
|
||||||
|
steps: Ref<ScenarioStepItem[]>;
|
||||||
|
scenario: Ref<Scenario>;
|
||||||
|
activeStep: Ref<ScenarioStepItem | undefined>;
|
||||||
|
quickInputDataKey: Ref<string>;
|
||||||
|
quickInputParamValue: Ref<any>;
|
||||||
|
showQuickInput: Ref<boolean>;
|
||||||
|
treeRef: Ref<InstanceType<typeof MsTree> | undefined>;
|
||||||
|
tempStepDesc: Ref<string>;
|
||||||
|
showStepDescEditInputStepId: Ref<string | number>;
|
||||||
|
tempStepName: Ref<string>;
|
||||||
|
showStepNameEditInputStepId: Ref<string | number>;
|
||||||
|
}) {
|
||||||
|
/**
|
||||||
|
* 打开快速输入
|
||||||
|
* @param dataKey 快速输入的数据 key
|
||||||
|
*/
|
||||||
|
function setQuickInput(step: ScenarioStepItem, dataKey: keyof ScenarioStepDetail) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
activeStep.value = realStep as ScenarioStepItem;
|
||||||
|
}
|
||||||
|
quickInputDataKey.value = dataKey;
|
||||||
|
quickInputParamValue.value = step.config?.[dataKey] || '';
|
||||||
|
if (quickInputDataKey.value === 'msWhileVariableValue' && activeStep.value?.config.whileController) {
|
||||||
|
quickInputParamValue.value = activeStep.value.config.whileController.msWhileVariable.value;
|
||||||
|
} else if (quickInputDataKey.value === 'msWhileVariableScriptValue' && activeStep.value?.config.whileController) {
|
||||||
|
quickInputParamValue.value = activeStep.value.config.whileController.msWhileScript.scriptValue;
|
||||||
|
} else if (quickInputDataKey.value === 'conditionValue' && activeStep.value?.config) {
|
||||||
|
quickInputParamValue.value = activeStep.value.config.value || '';
|
||||||
|
}
|
||||||
|
showQuickInput.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearQuickInput() {
|
||||||
|
activeStep.value = undefined;
|
||||||
|
quickInputParamValue.value = '';
|
||||||
|
quickInputDataKey.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用快速输入
|
||||||
|
*/
|
||||||
|
function applyQuickInput() {
|
||||||
|
if (activeStep.value) {
|
||||||
|
if (quickInputDataKey.value === 'msWhileVariableValue' && activeStep.value.config.whileController) {
|
||||||
|
activeStep.value.config.whileController.msWhileVariable.value = quickInputParamValue.value;
|
||||||
|
} else if (quickInputDataKey.value === 'msWhileVariableScriptValue' && activeStep.value.config.whileController) {
|
||||||
|
activeStep.value.config.whileController.msWhileScript.scriptValue = quickInputParamValue.value;
|
||||||
|
} else if (quickInputDataKey.value === 'conditionValue' && activeStep.value.config) {
|
||||||
|
activeStep.value.config.value = quickInputParamValue.value;
|
||||||
|
}
|
||||||
|
showQuickInput.value = false;
|
||||||
|
clearQuickInput();
|
||||||
|
scenario.value.unSaved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤描述编辑按钮点击
|
||||||
|
*/
|
||||||
|
function handleStepDescClick(step: ScenarioStepItem) {
|
||||||
|
tempStepDesc.value = step.name;
|
||||||
|
showStepDescEditInputStepId.value = step.uniqueId;
|
||||||
|
nextTick(() => {
|
||||||
|
// 等待输入框渲染完成后聚焦
|
||||||
|
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
||||||
|
input?.focus();
|
||||||
|
});
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.draggable = false; // 编辑时禁止拖拽
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用步骤描述更改
|
||||||
|
*/
|
||||||
|
function applyStepDescChange(step: ScenarioStepItem) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.name = tempStepDesc.value || realStep.name;
|
||||||
|
realStep.draggable = true; // 编辑完恢复拖拽
|
||||||
|
}
|
||||||
|
showStepDescEditInputStepId.value = '';
|
||||||
|
scenario.value.unSaved = !!tempStepDesc.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤内容编辑
|
||||||
|
* @param $event 编辑内容对象信息
|
||||||
|
*/
|
||||||
|
function handleStepContentChange($event: Record<string, any>, step: ScenarioStepItem) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
Object.keys($event).forEach((key) => {
|
||||||
|
realStep.config[key] = $event[key];
|
||||||
|
});
|
||||||
|
scenario.value.unSaved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤启用禁用切换
|
||||||
|
*/
|
||||||
|
function handleStepToggleEnable(data: ScenarioStepItem) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.enable = !realStep.enable;
|
||||||
|
scenario.value.unSaved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤名称编辑按钮点击事件
|
||||||
|
*/
|
||||||
|
function handleStepNameClick(step: ScenarioStepItem) {
|
||||||
|
tempStepName.value = step.name;
|
||||||
|
showStepNameEditInputStepId.value = step.uniqueId;
|
||||||
|
nextTick(() => {
|
||||||
|
// 等待输入框渲染完成后聚焦
|
||||||
|
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
|
||||||
|
input?.focus();
|
||||||
|
});
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.draggable = false; // 编辑时禁止拖拽
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用步骤名称更改
|
||||||
|
*/
|
||||||
|
function applyStepNameChange(step: ScenarioStepItem) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.name = tempStepName.value || realStep.name;
|
||||||
|
realStep.draggable = true; // 编辑完恢复拖拽
|
||||||
|
}
|
||||||
|
showStepNameEditInputStepId.value = '';
|
||||||
|
scenario.value.unSaved = !!tempStepName.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setQuickInput,
|
||||||
|
clearQuickInput,
|
||||||
|
applyQuickInput,
|
||||||
|
handleStepDescClick,
|
||||||
|
applyStepDescChange,
|
||||||
|
handleStepContentChange,
|
||||||
|
handleStepToggleEnable,
|
||||||
|
handleStepNameClick,
|
||||||
|
applyStepNameChange,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,244 @@
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
import type { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
|
||||||
|
import { getScenarioStep } from '@/api/modules/api-test/scenario';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useModal from '@/hooks/useModal';
|
||||||
|
import useAppStore from '@/store/modules/app';
|
||||||
|
import { deleteNode, findNodeByKey, handleTreeDragDrop, mapTree } from '@/utils';
|
||||||
|
|
||||||
|
import type { Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||||
|
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
import getStepType from '../common/stepType/utils';
|
||||||
|
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤树交互
|
||||||
|
*/
|
||||||
|
export default function useStepOperation({
|
||||||
|
scenario,
|
||||||
|
steps,
|
||||||
|
stepDetails,
|
||||||
|
activeStep,
|
||||||
|
selectedKeys,
|
||||||
|
customApiDrawerVisible,
|
||||||
|
customCaseDrawerVisible,
|
||||||
|
scriptOperationDrawerVisible,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
scenario: Ref<Scenario>;
|
||||||
|
steps: Ref<ScenarioStepItem[]>;
|
||||||
|
stepDetails: Ref<Record<string, any>>;
|
||||||
|
activeStep: Ref<ScenarioStepItem | undefined>;
|
||||||
|
selectedKeys: Ref<Array<string | number>>;
|
||||||
|
customApiDrawerVisible: Ref<boolean>;
|
||||||
|
customCaseDrawerVisible: Ref<boolean>;
|
||||||
|
scriptOperationDrawerVisible: Ref<boolean>;
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
}) {
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openModal } = useModal();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤展开折叠
|
||||||
|
*/
|
||||||
|
function handleStepExpand(data: MsTreeExpandedData) {
|
||||||
|
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.uniqueId, 'uniqueId');
|
||||||
|
if (realStep) {
|
||||||
|
realStep.expanded = !realStep.expanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStepDetail(step: ScenarioStepItem) {
|
||||||
|
try {
|
||||||
|
appStore.showLoading();
|
||||||
|
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] = {
|
||||||
|
...res,
|
||||||
|
stepId: step.id,
|
||||||
|
protocol: step.config.protocol || '',
|
||||||
|
method: step.config.method || '',
|
||||||
|
...parseRequestBodyResult,
|
||||||
|
};
|
||||||
|
scenario.value.stepFileParam[step.id] = {
|
||||||
|
...parseRequestBodyResult,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
appStore.hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤选中事件
|
||||||
|
* @param _selectedKeys 选中的 key集合
|
||||||
|
* @param step 点击的步骤节点
|
||||||
|
*/
|
||||||
|
async function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) {
|
||||||
|
const _stepType = getStepType(step);
|
||||||
|
const offspringIds: string[] = [];
|
||||||
|
mapTree(step.children || [], (e) => {
|
||||||
|
offspringIds.push(e.uniqueId);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
selectedKeys.value = [step.uniqueId, ...offspringIds];
|
||||||
|
if (_stepType.isCopyApi || _stepType.isQuoteApi || step.stepType === ScenarioStepType.CUSTOM_REQUEST) {
|
||||||
|
// 复制 api、引用 api、自定义 api打开抽屉
|
||||||
|
activeStep.value = step;
|
||||||
|
if (
|
||||||
|
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
||||||
|
(stepDetails.value[step.id] === undefined && !step.isNew)
|
||||||
|
) {
|
||||||
|
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
||||||
|
await getStepDetail(step);
|
||||||
|
}
|
||||||
|
customApiDrawerVisible.value = true;
|
||||||
|
} else if (step.stepType === ScenarioStepType.API_CASE) {
|
||||||
|
activeStep.value = step;
|
||||||
|
if (
|
||||||
|
_stepType.isCopyCase &&
|
||||||
|
((stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
||||||
|
(stepDetails.value[step.id] === undefined && !step.isNew))
|
||||||
|
) {
|
||||||
|
// 只有复制的 case 需要查看步骤详情,引用的无法更改所以不需要在此初始化详情
|
||||||
|
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
||||||
|
await getStepDetail(step);
|
||||||
|
}
|
||||||
|
customCaseDrawerVisible.value = true;
|
||||||
|
} else if (step.stepType === ScenarioStepType.SCRIPT) {
|
||||||
|
activeStep.value = step;
|
||||||
|
if (
|
||||||
|
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
||||||
|
(stepDetails.value[step.id] === undefined && !step.isNew)
|
||||||
|
) {
|
||||||
|
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
||||||
|
await getStepDetail(step);
|
||||||
|
}
|
||||||
|
scriptOperationDrawerVisible.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除
|
||||||
|
*/
|
||||||
|
function deleteStep(step?: ScenarioStepItem) {
|
||||||
|
if (step) {
|
||||||
|
openModal({
|
||||||
|
type: 'error',
|
||||||
|
title: t('common.tip'),
|
||||||
|
content: t('apiScenario.deleteStepConfirm', { name: step.name }),
|
||||||
|
okText: t('common.confirmDelete'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
maskClosable: false,
|
||||||
|
onBeforeOk: async () => {
|
||||||
|
customCaseDrawerVisible.value = false;
|
||||||
|
customApiDrawerVisible.value = false;
|
||||||
|
deleteNode(steps.value, step.uniqueId, 'uniqueId');
|
||||||
|
activeStep.value = undefined;
|
||||||
|
scenario.value.unSaved = true;
|
||||||
|
Message.success(t('common.deleteSuccess'));
|
||||||
|
},
|
||||||
|
hideCancel: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放允许拖拽步骤到释放的节点内
|
||||||
|
* @param dropNode 释放节点
|
||||||
|
*/
|
||||||
|
function isAllowDropInside(dropNode: MsTreeNodeData) {
|
||||||
|
return (
|
||||||
|
// 逻辑控制器内可以拖拽任意类型的步骤
|
||||||
|
[
|
||||||
|
ScenarioStepType.LOOP_CONTROLLER,
|
||||||
|
ScenarioStepType.IF_CONTROLLER,
|
||||||
|
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||||
|
].includes(dropNode.stepType) ||
|
||||||
|
// 复制的场景内可以释放任意类型的步骤
|
||||||
|
(dropNode.stepType === ScenarioStepType.API_SCENARIO && dropNode.refType === ScenarioStepRefType.COPY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理步骤节点拖拽事件
|
||||||
|
* @param tree 树数据
|
||||||
|
* @param dragNode 拖拽节点
|
||||||
|
* @param dropNode 释放节点
|
||||||
|
* @param dropPosition 释放位置(取值:-1,,0,,1。 -1:dropNodeId节点之前。 0:dropNodeId节点内。 1:dropNodeId节点后)
|
||||||
|
*/
|
||||||
|
function handleDrop(
|
||||||
|
tree: MsTreeNodeData[],
|
||||||
|
dragNode: MsTreeNodeData,
|
||||||
|
dropNode: MsTreeNodeData,
|
||||||
|
dropPosition: number
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (dropPosition === 0 && !isAllowDropInside(dropNode)) {
|
||||||
|
// Message.error(t('apiScenario.notAllowDropInside')); TODO:不允许释放提示
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
const offspringIds: string[] = [];
|
||||||
|
mapTree(cloneDeep(dragNode.children || []), (e) => {
|
||||||
|
offspringIds.push(e.uniqueId);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
const stepIdAndOffspringIds = [dragNode.uniqueId, ...offspringIds];
|
||||||
|
if (dropPosition === 0) {
|
||||||
|
// 拖拽到节点内
|
||||||
|
if (selectedKeys.value.includes(dropNode.uniqueId)) {
|
||||||
|
// 释放位置的节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||||
|
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
|
||||||
|
}
|
||||||
|
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.uniqueId)) {
|
||||||
|
// 释放位置的节点的父节点已选中,则需要把拖动的节点及其子孙节点也需要选中(因为父级选中子级也会展示选中状态)
|
||||||
|
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
|
||||||
|
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.uniqueId)) {
|
||||||
|
// 如果被拖动的节点的父节点在选中的节点中,则需要把被拖动的节点及其子孙节点从选中的节点中移除
|
||||||
|
selectedKeys.value = selectedKeys.value.filter((e) => {
|
||||||
|
for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
|
||||||
|
const id = stepIdAndOffspringIds[i];
|
||||||
|
if (e === id) {
|
||||||
|
stepIdAndOffspringIds.splice(i, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'uniqueId');
|
||||||
|
if (dragResult) {
|
||||||
|
Message.success(t('common.moveSuccess'));
|
||||||
|
scenario.value.unSaved = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
nextTick(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleStepExpand,
|
||||||
|
handleStepSelect,
|
||||||
|
deleteStep,
|
||||||
|
handleDrop,
|
||||||
|
};
|
||||||
|
}
|
|
@ -22,6 +22,8 @@
|
||||||
v-if="activeKey === ScenarioCreateComposition.PARAMS"
|
v-if="activeKey === ScenarioCreateComposition.PARAMS"
|
||||||
v-model:commonVariables="scenario.scenarioConfig.variable.commonVariables"
|
v-model:commonVariables="scenario.scenarioConfig.variable.commonVariables"
|
||||||
v-model:csvVariables="scenario.scenarioConfig.variable.csvVariables"
|
v-model:csvVariables="scenario.scenarioConfig.variable.csvVariables"
|
||||||
|
:scenario-id="scenario.id"
|
||||||
|
@change="() => (scenario.unSaved = true)"
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane
|
<a-tab-pane
|
||||||
|
@ -40,7 +42,7 @@
|
||||||
<assertion
|
<assertion
|
||||||
v-if="activeKey === ScenarioCreateComposition.ASSERTION"
|
v-if="activeKey === ScenarioCreateComposition.ASSERTION"
|
||||||
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
|
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
|
||||||
@change="scenario.unSaved = true"
|
@change="() => (scenario.unSaved = true)"
|
||||||
/>
|
/>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|
|
@ -146,9 +146,14 @@
|
||||||
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
|
import { defaultCsvParamItem, defaultNormalParamItem } from '../components/config';
|
||||||
import { defaultScenario } from './components/config';
|
import { defaultScenario } from './components/config';
|
||||||
import updateStepStatus from './components/utils';
|
import updateStepStatus from './components/utils';
|
||||||
import { filterAssertions, filterConditionsSqlValidParams } from '@/views/api-test/components/utils';
|
import {
|
||||||
|
filterAssertions,
|
||||||
|
filterConditionsSqlValidParams,
|
||||||
|
filterKeyValParams,
|
||||||
|
} from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
// 异步导入
|
// 异步导入
|
||||||
const detail = defineAsyncComponent(() => import('./detail/index.vue'));
|
const detail = defineAsyncComponent(() => import('./detail/index.vue'));
|
||||||
|
@ -510,6 +515,16 @@
|
||||||
preProcessorConfig: filterConditionsSqlValidParams(
|
preProcessorConfig: filterConditionsSqlValidParams(
|
||||||
activeScenarioTab.value.scenarioConfig.preProcessorConfig
|
activeScenarioTab.value.scenarioConfig.preProcessorConfig
|
||||||
),
|
),
|
||||||
|
variable: {
|
||||||
|
commonVariables: filterKeyValParams(
|
||||||
|
activeScenarioTab.value.scenarioConfig.variable.commonVariables,
|
||||||
|
defaultNormalParamItem
|
||||||
|
).validParams,
|
||||||
|
csvVariables: filterKeyValParams(
|
||||||
|
activeScenarioTab.value.scenarioConfig.variable.csvVariables,
|
||||||
|
defaultCsvParamItem
|
||||||
|
).validParams,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
projectId: appStore.currentProjectId,
|
projectId: appStore.currentProjectId,
|
||||||
environmentId: appStore.getCurrentEnvId || '',
|
environmentId: appStore.getCurrentEnvId || '',
|
||||||
|
|
|
@ -277,4 +277,19 @@ export default {
|
||||||
'apiScenario.execute.no.step.tips': 'No open step',
|
'apiScenario.execute.no.step.tips': 'No open step',
|
||||||
'apiScenario.preConditionTip': 'Execute once before the scene step',
|
'apiScenario.preConditionTip': 'Execute once before the scene step',
|
||||||
'apiScenario.postConditionTip': 'Execute once after the scene step',
|
'apiScenario.postConditionTip': 'Execute once after the scene step',
|
||||||
|
'apiScenario.deleteCsvConfirm': 'Are you sure you want to delete {name}?',
|
||||||
|
'apiScenario.deleteCsvConfirmContent':
|
||||||
|
'After deletion, all scenes using the CSV file will be updated, please operate with caution!',
|
||||||
|
'apiScenario.deleteCsvSuccess': 'Deleted',
|
||||||
|
'apiScenario.changeScopeConfirm': 'Are you sure you want to change the scope to {type}?',
|
||||||
|
'apiScenario.changeScopeToScenarioConfirmContent':
|
||||||
|
'After being changed, the parameters in the CSV file will take effect for the entire scene, so please operate with caution!',
|
||||||
|
'apiScenario.changeScopeToStepConfirmContent':
|
||||||
|
'After modification, the parameters in the CSV file will only take effect for the steps, please operate with caution!',
|
||||||
|
'apiScenario.confirmChange': 'Confirm changes',
|
||||||
|
'apiScenario.changeScopeSuccess': 'Change successful',
|
||||||
|
'apiScenario.quoteCsv': 'Quote CSV',
|
||||||
|
'apiScenario.csvQuote': 'CSV quote',
|
||||||
|
'apiScenario.csvNameNotNull': 'CSV name cannot be empty',
|
||||||
|
'apiScenario.csvFileNotNull': 'CSV file cannot be empty',
|
||||||
};
|
};
|
||||||
|
|
|
@ -274,4 +274,16 @@ export default {
|
||||||
'apiScenario.execute.no.step.tips': '没有开启的步骤',
|
'apiScenario.execute.no.step.tips': '没有开启的步骤',
|
||||||
'apiScenario.preConditionTip': '在场景步骤前分别执行一次',
|
'apiScenario.preConditionTip': '在场景步骤前分别执行一次',
|
||||||
'apiScenario.postConditionTip': '在场景步骤后分别执行一次',
|
'apiScenario.postConditionTip': '在场景步骤后分别执行一次',
|
||||||
|
'apiScenario.deleteCsvConfirm': '确认删除 {name} 吗?',
|
||||||
|
'apiScenario.deleteCsvConfirmContent': '删除后,使用该 CSV 文件的场景则全部更新,请谨慎操作!',
|
||||||
|
'apiScenario.deleteCsvSuccess': '已删除',
|
||||||
|
'apiScenario.changeScopeConfirm': '确认更改作用域为 {type} 吗?',
|
||||||
|
'apiScenario.changeScopeToScenarioConfirmContent': '更改后,该 CSV 文件内的参数对整个场景生效,请谨慎操作!',
|
||||||
|
'apiScenario.changeScopeToStepConfirmContent': '更改后,该 CSV 文件内的参数仅对步骤生效,请谨慎操作!',
|
||||||
|
'apiScenario.confirmChange': '确认更改',
|
||||||
|
'apiScenario.changeScopeSuccess': '已更改',
|
||||||
|
'apiScenario.quoteCsv': '引用 CSV',
|
||||||
|
'apiScenario.csvQuote': 'CSV 引用',
|
||||||
|
'apiScenario.csvNameNotNull': 'CSV 名称不能为空',
|
||||||
|
'apiScenario.csvFileNotNull': 'CSV 文件不能为空',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue