feat(接口测试): csv参数&JSONPath 提取调整&场景步骤树 hook 提取
This commit is contained in:
parent
2a3421f91d
commit
663b96aa62
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<a-dropdown v-model:popup-visible="dropdownVisible" :disabled="props.disabled" position="tl" trigger="click">
|
||||
<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>
|
||||
<template #content>
|
||||
<MsUpload
|
||||
|
|
|
@ -449,7 +449,12 @@
|
|||
td {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
}"
|
||||
>
|
||||
<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) }}
|
||||
</div>
|
||||
</slot>
|
||||
|
|
|
@ -234,14 +234,11 @@ export interface AssertionConfig {
|
|||
assertions: ExecuteAssertionItem[];
|
||||
}
|
||||
export interface CsvVariable {
|
||||
id?: string;
|
||||
fileId: string;
|
||||
id: string;
|
||||
scenarioId: string;
|
||||
name: string;
|
||||
fileName: string;
|
||||
scope: string;
|
||||
enable: boolean;
|
||||
association: boolean;
|
||||
encoding: string;
|
||||
random: boolean;
|
||||
variableNames: string;
|
||||
|
@ -250,6 +247,14 @@ export interface CsvVariable {
|
|||
allowQuotedData: boolean;
|
||||
recycleOnEof: boolean;
|
||||
stopThreadOnEof: boolean;
|
||||
file: {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
local: boolean; // 是否是本地上传的文件
|
||||
fileAlias: string; // 文件别名
|
||||
delete: boolean; // 是否删除
|
||||
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
|
||||
};
|
||||
// 以下为前端字段
|
||||
settingVisible: boolean;
|
||||
}
|
||||
|
@ -353,7 +358,7 @@ export interface ScenarioStepItem {
|
|||
stepType: ScenarioStepType;
|
||||
refType: ScenarioStepRefType;
|
||||
config: ScenarioStepDetail; // 存储步骤列表需要展示的信息
|
||||
csvFileIds?: string[];
|
||||
csvIds?: string[];
|
||||
projectId?: string;
|
||||
versionId?: string;
|
||||
children?: ScenarioStepItem[];
|
||||
|
|
|
@ -426,13 +426,11 @@ export const defaultNormalParamItem = {
|
|||
};
|
||||
// 场景-csv参数默认值
|
||||
export const defaultCsvParamItem: CsvVariable = {
|
||||
fileId: '',
|
||||
id: '',
|
||||
scenarioId: '',
|
||||
name: '',
|
||||
fileName: '',
|
||||
scope: 'SCENARIO',
|
||||
enable: true,
|
||||
association: false,
|
||||
enable: false,
|
||||
encoding: 'UTF-8',
|
||||
random: false,
|
||||
variableNames: '',
|
||||
|
@ -442,4 +440,11 @@ export const defaultCsvParamItem: CsvVariable = {
|
|||
recycleOnEof: false,
|
||||
stopThreadOnEof: false,
|
||||
settingVisible: false,
|
||||
file: {
|
||||
fileId: '',
|
||||
fileName: '',
|
||||
local: false,
|
||||
fileAlias: '',
|
||||
delete: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -252,7 +252,13 @@
|
|||
JSONPath({
|
||||
json: parseJson.value,
|
||||
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) {
|
||||
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
|
||||
}
|
||||
|
|
|
@ -222,7 +222,7 @@
|
|||
:options="columnConfig.typeOptions || []"
|
||||
class="ms-form-table-input w-[180px]"
|
||||
size="mini"
|
||||
@change="() => addTableLine(rowIndex)"
|
||||
@change="(val) => handleScopeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
|
||||
/>
|
||||
</template>
|
||||
<!-- 参数值 -->
|
||||
|
@ -282,7 +282,7 @@
|
|||
<!-- 文件 -->
|
||||
<template #file="{ record, rowIndex }">
|
||||
<MsAddAttachment
|
||||
:file-list="[record]"
|
||||
:file-list="[record.file]"
|
||||
:disabled="props.disabledParamValue"
|
||||
:multiple="false"
|
||||
mode="input"
|
||||
|
@ -500,6 +500,9 @@
|
|||
size="small"
|
||||
type="line"
|
||||
class="ml-[8px]"
|
||||
:before-change="
|
||||
(newValue) => (props.enableChangeIntercept ? props.enableChangeIntercept(record, newValue) : undefined)
|
||||
"
|
||||
@change="() => addTableLine(rowIndex)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -624,6 +627,7 @@
|
|||
import { groupCategoryEnvList, groupProjectEnv } from '@/api/modules/project-management/envManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getGenerateId } from '@/utils';
|
||||
|
||||
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
|
||||
import { HttpForm, ProjectOptionItem } from '@/models/projectManagement/environmental';
|
||||
|
@ -686,6 +690,9 @@
|
|||
fileSaveAsSourceId?: string | number; // 文件转存关联的资源id
|
||||
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; // 文件转存接口
|
||||
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: () => [],
|
||||
|
@ -734,8 +741,15 @@
|
|||
emit('treeDelete', record);
|
||||
return;
|
||||
}
|
||||
paramsData.value.splice(rowIndex, 1);
|
||||
emitChange('deleteParam');
|
||||
if (props.deleteIntercept) {
|
||||
props.deleteIntercept(record, () => {
|
||||
paramsData.value.splice(rowIndex, 1);
|
||||
emitChange('deleteParam');
|
||||
});
|
||||
} else {
|
||||
paramsData.value.splice(rowIndex, 1);
|
||||
emitChange('deleteParam');
|
||||
}
|
||||
}
|
||||
|
||||
/** 断言-文档-Begin */
|
||||
|
@ -866,13 +880,13 @@
|
|||
if (rowIndex === paramsData.value.length - 1) {
|
||||
// Don't change this!!!
|
||||
// 最后一行的更改才会触发添加新一行
|
||||
const id = new Date().getTime().toString();
|
||||
const id = getGenerateId();
|
||||
const lastLineData = paramsData.value[rowIndex]; // 上一行数据
|
||||
const selectColumnKeys = props.columns.filter((e) => e.typeOptions).map((e) => e.dataIndex); // 找到下拉框选项的列
|
||||
const nextLine = {
|
||||
id,
|
||||
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
||||
enable: true, // 是否勾选
|
||||
...cloneDeep(props.defaultParamItem), // 深拷贝,避免有嵌套引用类型,数据隔离
|
||||
} as any;
|
||||
selectColumnKeys.forEach((key) => {
|
||||
// 如果是更改了下拉框导致添加新的一列,需要将更改后的下拉框的值应用到下一行(产品为了方便统一输入参数类型)
|
||||
|
@ -908,7 +922,7 @@
|
|||
hasNoIdItem = true;
|
||||
return {
|
||||
...cloneDeep(props.defaultParamItem),
|
||||
id: new Date().getTime() + i,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
}
|
||||
if (!item.id) {
|
||||
|
@ -916,7 +930,7 @@
|
|||
hasNoIdItem = true;
|
||||
return {
|
||||
...item,
|
||||
id: new Date().getTime() + i,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
|
@ -926,12 +940,12 @@
|
|||
}
|
||||
} else {
|
||||
if (props.disabledExceptParam) return;
|
||||
const id = new Date().getTime().toString();
|
||||
const id = getGenerateId();
|
||||
paramsData.value = [
|
||||
{
|
||||
id, // 默认给时间戳 id,若 props.defaultParamItem 有 id,则覆盖
|
||||
...cloneDeep(props.defaultParamItem),
|
||||
enable: true, // 是否勾选
|
||||
...cloneDeep(props.defaultParamItem),
|
||||
},
|
||||
] as any[];
|
||||
emitChange('watch props.params', true);
|
||||
|
@ -1006,20 +1020,18 @@
|
|||
if (props.uploadTempFileApi && file?.local) {
|
||||
appStore.showLoading();
|
||||
const res = await props.uploadTempFileApi(file.file);
|
||||
record = {
|
||||
...record,
|
||||
record.file = {
|
||||
...file,
|
||||
fileId: res.data,
|
||||
fileName: file.name || '',
|
||||
fileAlias: file.name || '',
|
||||
};
|
||||
} else if (file) {
|
||||
record = {
|
||||
...record,
|
||||
...file,
|
||||
fileId: file.uid || file.fileId || '',
|
||||
fileName: file.originalName || '',
|
||||
fileAlias: file.name || '',
|
||||
} else if (files[0]) {
|
||||
record.file = {
|
||||
...files[0],
|
||||
fileId: files[0].uid || files[0].fileId || '',
|
||||
fileName: files[0].originalName || '',
|
||||
fileAlias: files[0].name || '',
|
||||
};
|
||||
}
|
||||
addTableLine(rowIndex);
|
||||
|
@ -1101,6 +1113,23 @@
|
|||
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 按钮列表
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
:file-save-as-api="props.fileSaveAsApi"
|
||||
:file-module-options-api="props.fileModuleOptionsApi"
|
||||
@change="handleParamTableChange"
|
||||
@batch-add="batchAddKeyValVisible = true"
|
||||
@batch-add="() => (batchAddKeyValVisible = true)"
|
||||
/>
|
||||
<paramTable
|
||||
v-else-if="innerParams.bodyType === RequestBodyFormat.WWW_FORM"
|
||||
|
@ -47,7 +47,7 @@
|
|||
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_URL_ENCODE"
|
||||
:default-param-item="defaultBodyParamsItem"
|
||||
@change="handleParamTableChange"
|
||||
@batch-add="batchAddKeyValVisible = true"
|
||||
@batch-add="() => (batchAddKeyValVisible = true)"
|
||||
/>
|
||||
<div v-else-if="innerParams.bodyType === RequestBodyFormat.BINARY">
|
||||
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
|
||||
|
|
|
@ -7,15 +7,17 @@
|
|||
:scroll="props.scroll"
|
||||
>
|
||||
<template #assertionItem="{ record }">
|
||||
<div class="flex items-center gap-[4px]">
|
||||
【{{
|
||||
t(
|
||||
responseAssertionTypeMap[(record as ResponseAssertionTableItem).assertionType] ||
|
||||
'apiTestDebug.responseBody'
|
||||
)
|
||||
}}】
|
||||
{{ record.name }}
|
||||
</div>
|
||||
<a-tooltip :content="record.name">
|
||||
<div class="flex items-center gap-[4px]">
|
||||
【{{
|
||||
t(
|
||||
responseAssertionTypeMap[(record as ResponseAssertionTableItem).assertionType] ||
|
||||
'apiTestDebug.responseBody'
|
||||
)
|
||||
}}】
|
||||
{{ record.name }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #condition="{ record }">
|
||||
{{
|
||||
|
@ -61,7 +63,6 @@
|
|||
{
|
||||
title: 'apiTestDebug.assertionItem',
|
||||
dataIndex: 'assertionItem',
|
||||
showTooltip: true,
|
||||
slotName: 'assertionItem',
|
||||
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,
|
||||
isRefScenarioStep: false,
|
||||
isQuoteScenarioStep: false,
|
||||
csvIds: [],
|
||||
};
|
||||
|
||||
export const defaultScenario: Scenario = {
|
||||
|
|
|
@ -30,109 +30,12 @@
|
|||
@change="handleCommonVariablesChange"
|
||||
@batch-add="() => (batchAddKeyValVisible = true)"
|
||||
/>
|
||||
<paramTable
|
||||
<csvParamsTable
|
||||
v-else
|
||||
v-model:params="csvVariables"
|
||||
:columns="csvColumns"
|
||||
:default-param-item="defaultCsvParamItem"
|
||||
: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>
|
||||
v-model:csvVariables="csvVariables"
|
||||
:scenario-id="props.scenarioId"
|
||||
@change="() => emit('change')"
|
||||
/>
|
||||
<batchAddKeyVal
|
||||
v-model:visible="batchAddKeyValVisible"
|
||||
:params="commonVariables"
|
||||
|
@ -144,18 +47,19 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||
|
||||
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';
|
||||
|
||||
const props = defineProps<{
|
||||
scenarioId?: string | number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change'): void; // 数据发生变化
|
||||
}>();
|
||||
|
@ -168,6 +72,7 @@
|
|||
const csvVariables = defineModel<CsvVariable[]>('csvVariables', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const searchValue = ref('');
|
||||
const firstSearch = ref(true);
|
||||
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() {
|
||||
if (firstSearch.value) {
|
||||
|
@ -271,135 +168,6 @@
|
|||
}
|
||||
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>
|
||||
|
||||
<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>
|
||||
<style lang="less"></style>
|
||||
|
|
|
@ -145,6 +145,7 @@ export default function useCreateActions() {
|
|||
children: item.children || [],
|
||||
stepType,
|
||||
refType,
|
||||
csvIds: [],
|
||||
originProjectId: item.originProjectId,
|
||||
copyFromStepId: item.copyFromStepId,
|
||||
...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)"
|
||||
@click.stop
|
||||
/>
|
||||
<csvTag
|
||||
:step="step"
|
||||
:csv-variables="scenario.scenarioConfig.variable.csvVariables"
|
||||
@remove="(id) => removeCsv(step, id)"
|
||||
@replace="(id) => replaceCsv(step, id)"
|
||||
/>
|
||||
<!-- 自定义请求、API、CASE、场景步骤名称 -->
|
||||
<template v-if="checkStepIsApi(step)">
|
||||
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.config.method" />
|
||||
|
@ -176,9 +182,9 @@
|
|||
v-model:steps="steps"
|
||||
v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']"
|
||||
:step="step"
|
||||
@click="setFocusNodeKey(step.uniqueId)"
|
||||
@click="() => setFocusNodeKey(step.uniqueId)"
|
||||
@other-create="handleOtherCreate"
|
||||
@close="setFocusNodeKey('')"
|
||||
@close="() => setFocusNodeKey('')"
|
||||
@add-done="handleAddStepDone"
|
||||
/>
|
||||
</template>
|
||||
|
@ -223,9 +229,9 @@
|
|||
:permission-map="permissionMap"
|
||||
:steps="steps"
|
||||
@add-step="addCustomApiStep"
|
||||
@delete-step="deleteStep(activeStep)"
|
||||
@delete-step="() => deleteStep(activeStep)"
|
||||
@apply-step="applyApiStep"
|
||||
@stop-debug="handleStopExecute(activeStep)"
|
||||
@stop-debug="() => handleStopExecute(activeStep)"
|
||||
@execute="handleApiExecute"
|
||||
@replace="handleReplaceStep"
|
||||
/>
|
||||
|
@ -239,8 +245,8 @@
|
|||
:step-responses="scenario.stepResponses"
|
||||
:permission-map="permissionMap"
|
||||
@apply-step="applyApiStep"
|
||||
@delete-step="deleteStep(activeStep)"
|
||||
@stop-debug="handleStopExecute(activeStep)"
|
||||
@delete-step="() => deleteStep(activeStep)"
|
||||
@stop-debug="() => handleStopExecute(activeStep)"
|
||||
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
|
||||
@replace="handleReplaceStep"
|
||||
/>
|
||||
|
@ -439,6 +445,13 @@
|
|||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<quoteCsvDrawer
|
||||
v-model:visible="csvDrawerVisible"
|
||||
:csv-variables="scenario.scenarioConfig.variable.csvVariables"
|
||||
:exclude-keys="activeStep?.csvIds"
|
||||
:is-single="!!replaceCsvId"
|
||||
@confirm="handleQuoteCsvConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -451,12 +464,14 @@
|
|||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/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 quoteCsvDrawer from '../common/quoteCsvDrawer.vue';
|
||||
import stepType from '../common/stepType/stepType.vue';
|
||||
import createStepActions from './createAction/createStepActions.vue';
|
||||
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
|
||||
import conditionContent from './stepNodeComposition/conditionContent.vue';
|
||||
import csvTag from './stepNodeComposition/csvTag.vue';
|
||||
import loopControlContent from './stepNodeComposition/loopContent.vue';
|
||||
import quoteContent from './stepNodeComposition/quoteContent.vue';
|
||||
import waitTimeContent from './stepNodeComposition/waitTimeContent.vue';
|
||||
|
@ -464,37 +479,23 @@
|
|||
import { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.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 { debugScenario, getScenarioDetail, getScenarioStep } from '@/api/modules/api-test/scenario';
|
||||
import { getSocket } from '@/api/modules/project-management/commonScript';
|
||||
import { getScenarioDetail, 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,
|
||||
getGenerateId,
|
||||
handleTreeDragDrop,
|
||||
insertNodes,
|
||||
mapTree,
|
||||
traverseTree,
|
||||
TreeNode,
|
||||
} from '@/utils';
|
||||
import { deleteNode, findNodeByKey, getGenerateId, insertNodes, mapTree, TreeNode } from '@/utils';
|
||||
|
||||
import {
|
||||
ExecuteApiRequestFullParams,
|
||||
ExecuteConditionProcessor,
|
||||
ExecutePluginRequestParams,
|
||||
RequestResult,
|
||||
} from '@/models/apiTest/common';
|
||||
import { AddApiCaseParams } from '@/models/apiTest/management';
|
||||
import {
|
||||
ApiScenarioDebugRequest,
|
||||
CreateStepAction,
|
||||
Scenario,
|
||||
ScenarioStepConfig,
|
||||
ScenarioStepDetail,
|
||||
ScenarioStepDetails,
|
||||
ScenarioStepFileParams,
|
||||
ScenarioStepItem,
|
||||
|
@ -508,8 +509,10 @@
|
|||
} from '@/enums/apiEnum';
|
||||
|
||||
import type { RequestParam } from '../common/customApiDrawer.vue';
|
||||
import updateStepStatus from '../utils';
|
||||
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 { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
|
||||
|
@ -564,6 +567,15 @@
|
|||
const activeStep = ref<ScenarioStepItem>(); // 用于弹窗配置时记录当前操作的步骤节点
|
||||
const activeStepByCreate = ref<ScenarioStepItem | undefined>(); // 用于抽屉操作创建步骤时记录当前操作的步骤节点
|
||||
|
||||
const { executeStep, handleApiExecute, handleStopExecute } = useStepExecute({
|
||||
scenario,
|
||||
steps,
|
||||
stepDetails,
|
||||
activeStep,
|
||||
isPriorityLocalExec,
|
||||
localExecuteUrl,
|
||||
});
|
||||
|
||||
function setFocusNodeKey(id: string | number) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
switch (item.eventTag) {
|
||||
case 'copy':
|
||||
|
@ -1048,6 +1095,11 @@
|
|||
activeStep.value = node as ScenarioStepItem;
|
||||
saveCaseModalVisible.value = true;
|
||||
break;
|
||||
case 'quoteCsv':
|
||||
activeStep.value = node as ScenarioStepItem;
|
||||
replaceCsvId.value = '';
|
||||
csvDrawerVisible.value = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -1062,87 +1114,11 @@
|
|||
*/
|
||||
const showStepNameEditInputStepId = ref<string | number>('');
|
||||
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、场景步骤名称编辑
|
||||
*/
|
||||
const showStepDescEditInputStepId = ref<string | number>('');
|
||||
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 customCaseDrawerVisible = ref(false);
|
||||
const customApiDrawerVisible = ref(false);
|
||||
|
@ -1167,240 +1143,17 @@
|
|||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理步骤选中事件
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
const { handleStepExpand, handleStepSelect, deleteStep, handleDrop } = useStepOperation({
|
||||
scenario,
|
||||
steps,
|
||||
stepDetails,
|
||||
activeStep,
|
||||
selectedKeys,
|
||||
customApiDrawerVisible,
|
||||
customCaseDrawerVisible,
|
||||
scriptOperationDrawerVisible,
|
||||
loading,
|
||||
});
|
||||
|
||||
function handleReplaceStep(newStep: ScenarioStepItem) {
|
||||
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 quickInputParamValue = ref<any>('');
|
||||
const quickInputDataKey = ref('');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
const {
|
||||
setQuickInput,
|
||||
clearQuickInput,
|
||||
applyQuickInput,
|
||||
handleStepDescClick,
|
||||
applyStepDescChange,
|
||||
handleStepContentChange,
|
||||
handleStepToggleEnable,
|
||||
handleStepNameClick,
|
||||
applyStepNameChange,
|
||||
} = useStepNodeEdit({
|
||||
steps,
|
||||
scenario,
|
||||
activeStep,
|
||||
quickInputDataKey,
|
||||
quickInputParamValue,
|
||||
showQuickInput,
|
||||
treeRef,
|
||||
tempStepDesc,
|
||||
showStepDescEditInputStepId,
|
||||
tempStepName,
|
||||
showStepNameEditInputStepId,
|
||||
});
|
||||
|
||||
const dbClick = ref({
|
||||
e: null as MouseEvent | null,
|
||||
|
@ -1937,8 +1569,6 @@
|
|||
}
|
||||
.ms-tree-node-extra {
|
||||
@apply !visible !w-auto;
|
||||
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
.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-model:commonVariables="scenario.scenarioConfig.variable.commonVariables"
|
||||
v-model:csvVariables="scenario.scenarioConfig.variable.csvVariables"
|
||||
:scenario-id="scenario.id"
|
||||
@change="() => (scenario.unSaved = true)"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
|
@ -40,7 +42,7 @@
|
|||
<assertion
|
||||
v-if="activeKey === ScenarioCreateComposition.ASSERTION"
|
||||
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
|
||||
@change="scenario.unSaved = true"
|
||||
@change="() => (scenario.unSaved = true)"
|
||||
/>
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
|
|
|
@ -146,9 +146,14 @@
|
|||
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import { defaultCsvParamItem, defaultNormalParamItem } from '../components/config';
|
||||
import { defaultScenario } from './components/config';
|
||||
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'));
|
||||
|
@ -510,6 +515,16 @@
|
|||
preProcessorConfig: filterConditionsSqlValidParams(
|
||||
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,
|
||||
environmentId: appStore.getCurrentEnvId || '',
|
||||
|
|
|
@ -277,4 +277,19 @@ export default {
|
|||
'apiScenario.execute.no.step.tips': 'No open step',
|
||||
'apiScenario.preConditionTip': 'Execute once before 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.preConditionTip': '在场景步骤前分别执行一次',
|
||||
'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