feat(接口场景): 场景步骤 80%

This commit is contained in:
baiqi 2024-03-21 20:26:58 +08:00 committed by 刘瑞斌
parent ca99eeca14
commit e2bef32de2
20 changed files with 498 additions and 219 deletions

View File

@ -38,7 +38,7 @@
"dependencies": {
"@7polo/kity": "2.0.8",
"@7polo/kityminder-core": "1.4.53",
"@arco-design/web-vue": "^2.54.4",
"@arco-design/web-vue": "^2.55.0",
"@arco-themes/vue-ms-theme-default": "^0.0.30",
"@form-create/arco-design": "^3.1.23",
"@halo-dev/richtext-editor": "0.0.0-alpha.33",

View File

@ -137,23 +137,18 @@
width: 960px;
}
}
.ms-modal-response {
.arco-modal {
width: 800px;
height: 523px
height: 523px;
}
}
.ms-modal-response-body{
.arco-modal-body{
padding: 0;
.ms-modal-response-body {
.arco-modal-body {
overflow: inherit;
padding: 0;
}
}
.ms-modal-small {
.arco-modal {
width: 480px;
@ -394,9 +389,8 @@
}
.arco-checkbox-icon-check {
@apply text-white;
.arco-checkbox-icon {
background-color: rgb(var(--primary-5));
}
background-color: rgb(var(--primary-5));
}
}
.arco-checkbox {

View File

@ -10,7 +10,7 @@
</a-button>
<template #content>
<MsUpload
v-model:file-list="innerFileList"
v-model:file-list="fileList"
accept="none"
:auto-upload="false"
:show-file-list="false"
@ -229,7 +229,7 @@
const { t } = useI18n();
const innerFileList = defineModel<MsFileItem[]>('fileList', {
const fileList = defineModel<MsFileItem[]>('fileList', {
// TODO:MsFileItem
required: true,
});
@ -245,7 +245,7 @@
onBeforeMount(() => {
//
const defaultFiles = innerFileList.value.filter((item) => item) || [];
const defaultFiles = fileList.value.filter((item) => item) || [];
if (defaultFiles.length > 0) {
if (props.multiple) {
inputFiles.value = defaultFiles.map((item) => ({
@ -266,18 +266,18 @@
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
if (props.multiple) {
innerFileList.value.push(fileItem);
fileList.value.push(fileItem);
inputFiles.value.push({
...fileItem,
value: fileItem[props.fields.id] || fileItem.uid || '',
label: fileItem[props.fields.name] || fileItem.name || '',
});
} else {
innerFileList.value = [fileItem];
fileList.value = [fileItem];
inputFileName.value = fileItem.name || '';
}
fileItem.local = true;
emit('change', innerFileList.value, fileItem);
emit('change', fileList.value, fileItem);
nextTick(() => {
// emit
buttonDropDownVisible.value = false;
@ -295,7 +295,7 @@
//
watch(
() => innerFileList.value,
() => fileList.value,
(arr) => {
getListFunParams.value.combine.hiddenIds = arr
.filter((item) => !item.local)
@ -308,9 +308,9 @@
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
if (props.mode === 'button') {
innerFileList.value.push(...fileResultList);
fileList.value.push(...fileResultList);
} else if (props.multiple) {
innerFileList.value.push(...fileResultList);
fileList.value.push(...fileResultList);
inputFiles.value.push(
...fileResultList.map((item) => ({
...item,
@ -321,10 +321,10 @@
} else {
//
const file = fileResultList[0];
innerFileList.value = [{ ...file, fileId: file.uid || '', fileName: file.name || '' }];
fileList.value = [{ ...file, fileId: file.uid || '', fileName: file.name || '' }];
inputFileName.value = file.name || '';
}
emit('change', innerFileList.value);
emit('change', fileList.value);
}
const inputFilesPopoverVisible = ref(false);
@ -341,9 +341,7 @@
function handleClose(data: TagData) {
inputFiles.value = inputFiles.value.filter((item) => item.value !== data.value);
innerFileList.value = innerFileList.value.filter(
(item) => (item[props.fields.id] || item.uid) !== (data[props.fields.id] || data.value)
);
fileList.value = fileList.value.filter((item) => (item.uid || item[props.fields.id]) !== data.value);
if (inputFiles.value.length === 0) {
inputFilesPopoverVisible.value = false;
}
@ -353,7 +351,7 @@
function handleFileClear() {
inputFileName.value = '';
inputFiles.value = [];
innerFileList.value = [];
fileList.value = [];
emit('change', []);
}
@ -367,7 +365,7 @@
function handleOpenSaveAs(item: TagData) {
inputFilesPopoverVisible.value = false;
// uid
savingFile.value = innerFileList.value.find((file) => (file.uid || file[props.fields.id]) === item.value);
savingFile.value = fileList.value.find((file) => (file.uid || file[props.fields.id]) === item.value);
saveFilePopoverVisible.value = true;
}

View File

@ -300,7 +300,7 @@
.ms-drawer-body-scrollbar {
@apply h-full w-full overflow-auto;
min-width: 680px;
min-width: 650px;
min-height: 500px;
}
.ms-drawer-body {

View File

@ -16,7 +16,8 @@ export default function useOpenNewPage() {
window.open(
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${
appStore.currentProjectId
}&${queryParams}`
}&${queryParams}`,
'_blank'
);
}

View File

@ -1,3 +1,4 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
import { ApiDefinitionCustomField } from '@/models/apiTest/management';
@ -184,13 +185,14 @@ export type ScenarioStepLoopType = 'num' | 'while' | 'forEach';
// 场景步骤-循环控制器-循环类型
export type ScenarioStepLoopWhileType = 'condition' | 'expression';
// 场景步骤-步骤插入类型
export type CreateStepAction = 'addChildStep' | 'insertBefore' | 'insertAfter' | undefined;
export type CreateStepAction = 'inside' | 'before' | 'after';
// 场景步骤
export interface Scenario {
id: string;
name: string;
moduleId: string | number;
stepInfo: ScenarioStepInfo;
priority: CaseLevel;
status: RequestDefinitionStatus;
tags: string[];
params: Record<string, any>[];

View File

@ -357,58 +357,70 @@ export function findNodePathByKey<T>(
return null;
}
/**
* /
* /
* @param treeArr
* @param targetKey
* @param newNode
* @param newNodes /
* @param position
* @param customKey key
*/
export function insertNode<T>(
export function insertNodes<T>(
treeArr: TreeNode<T>[],
targetKey: string | number,
newNode: TreeNode<T>,
newNodes: TreeNode<T> | TreeNode<T>[],
position: 'before' | 'after' | 'inside',
customFunc?: (node: TreeNode<T>, parent?: TreeNode<T>) => void,
customKey = 'key'
): void {
function insertNewNodes(
array: TreeNode<T>[],
startIndex: number,
parent: TreeNode<T> | undefined,
startOrder: number
) {
if (Array.isArray(newNodes)) {
// 插入节点数组
newNodes.forEach((newNode, index) => {
newNode.parent = parent;
newNode.order = startOrder + index;
});
array.splice(startIndex, 0, ...newNodes);
} else {
// 插入单个节点
newNodes.parent = parent;
newNodes.order = startOrder;
array.splice(startIndex, 0, newNodes);
}
// 更新插入节点之后的节点的 order
const newLength = Array.isArray(newNodes) ? newNodes.length : 1;
for (let j = startIndex + newLength; j < array.length; j++) {
array[j].order += newLength;
}
}
function insertNodeInTree(tree: TreeNode<T>[], parent?: TreeNode<T>): boolean {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node[customKey] === targetKey) {
// 如果当前节点的 customKey 与目标 customKey 匹配,则在当前节点前/后/内部插入新节点
const childrenArray = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
const index = childrenArray.findIndex((item) => item[customKey] === node[customKey]);
const parentChildren = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
const index = parentChildren.findIndex((item) => item[customKey] === node[customKey]);
if (position === 'before') {
newNode.parent = parent || node.parent;
newNode.order = node.order;
childrenArray.splice(index, 0, newNode);
for (let j = index + 1; j < childrenArray.length; j++) {
// 更新插入节点之后的节点的 order
if (childrenArray[j].order !== undefined) {
childrenArray[j].order += 1;
}
}
insertNewNodes(parentChildren, index, parent || node.parent, node.order);
} else if (position === 'after') {
newNode.parent = parent || node.parent;
newNode.order = node.order + 1;
childrenArray.splice(index + 1, 0, newNode);
// 更新插入节点之后的节点的 order
for (let j = index + 2; j < childrenArray.length; j++) {
if (childrenArray[j].order !== undefined) {
childrenArray[j].order += 1;
}
}
insertNewNodes(parentChildren, index + 1, parent || node.parent, node.order + 1);
} else if (position === 'inside') {
if (!node.children) {
node.children = [];
}
newNode.parent = node;
newNode.order = node.children.length + 1;
node.children.push(newNode);
insertNewNodes(node.children, node.children.length, node, node.children.length + 1);
}
if (typeof customFunc === 'function') {
customFunc(newNode, parent);
if (Array.isArray(newNodes)) {
newNodes.forEach((newNode) => customFunc(newNode, parent || node.parent));
} else {
customFunc(newNodes, parent || node.parent);
}
}
// 插入后返回 true
return true;
@ -456,10 +468,10 @@ export function handleTreeDragDrop<T>(
// 拖动节点插入到目标节点的 children 数组中
if (dropPosition === 0) {
insertNode(dropNode.parent?.children || treeArr, dropNode[customKey], dragNode, 'inside', undefined, customKey);
insertNodes(dropNode.parent?.children || treeArr, dropNode[customKey], dragNode, 'inside', undefined, customKey);
} else {
// 拖动节点插入到目标节点的前/后
insertNode(
insertNodes(
dropNode.parent?.children || treeArr,
dropNode[customKey],
dragNode,

View File

@ -144,26 +144,28 @@
}
});
async function handleFileChange(files: MsFileItem[]) {
async function handleFileChange(files: MsFileItem[], file?: MsFileItem) {
if (!props.uploadTempFileApi) return;
if (files.length === 0) {
if (files.length === 0 && file === undefined) {
innerParams.value.binaryBody.file = undefined;
emit('change');
return;
}
try {
if (fileList.value[0]?.local && fileList.value[0].file) {
if (file?.local && file.file) {
//
appStore.showLoading();
const res = await props.uploadTempFileApi(fileList.value[0].file);
const res = await props.uploadTempFileApi(file.file);
innerParams.value.binaryBody.file = {
...fileList.value[0],
...file,
fileId: res.data,
fileName: fileList.value[0]?.name || '',
fileAlias: fileList.value[0]?.name || '',
fileName: file?.name || '',
fileAlias: file?.name || '',
local: true,
};
appStore.hideLoading();
} else {
//
innerParams.value.binaryBody.file = {
...fileList.value[0],
fileId: fileList.value[0]?.uid,

View File

@ -178,7 +178,7 @@
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.requestResult?.headers.trim();
return props.requestResult?.responseResult?.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.requestResult?.body
? `${t('apiTestDebug.requestUrl')}:\n${props.requestResult.url}\n${t('apiTestDebug.header')}:\n${

View File

@ -7,22 +7,22 @@
disabled-width-drag
>
<div class="h-full w-full overflow-hidden">
<a-tabs v-model:active-key="activeKey" @change="resetModuleAndTable">
<a-tabs v-model:active-key="activeKey" @change="resetModule">
<a-tab-pane key="api" :title="t('apiScenario.api')" />
<a-tab-pane key="case" :title="t('apiScenario.case')" />
<a-tab-pane key="scenario" :title="t('apiScenario.scenario')" />
</a-tabs>
<a-divider :margin="0"></a-divider>
<div class="flex">
<div class="flex h-[calc(100%-49px)]">
<div class="w-[300px] border-r p-[16px]">
<div class="flex flex-col">
<div class="mb-[12px] flex items-center gap-[8px]">
<MsProjectSelect v-model:project="currentProject" @change="resetModuleAndTable" />
<MsProjectSelect v-model:project="currentProject" @change="resetModule" />
<a-select
v-model:model-value="protocol"
:options="protocolOptions"
class="w-[90px]"
@change="resetModuleAndTable"
@change="resetModule"
/>
</div>
<moduleTree
@ -42,6 +42,9 @@
:protocol="protocol"
:project-id="currentProject"
:module-ids="moduleIds"
:selected-apis="selectedApis"
:selected-cases="selectedCases"
:selected-scenarios="selectedScenarios"
@select="handleTableSelect"
/>
</div>
@ -68,8 +71,12 @@
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="handleCopy">{{ t('common.copy') }}</a-button>
<a-button type="primary" @click="handleQuote">{{ t('common.quote') }}</a-button>
<a-button type="primary" :disabled="totalSelected === 0" @click="handleCopy">
{{ t('common.copy') }}
</a-button>
<a-button type="primary" :disabled="totalSelected === 0" @click="handleQuote">
{{ t('common.quote') }}
</a-button>
</div>
</div>
</template>
@ -78,9 +85,11 @@
<script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { MsTableDataItem } from '@/components/pure/ms-table/type';
import MsProjectSelect from '@/components/business/ms-project-select/index.vue';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import moduleTree from './moduleTree.vue';
@ -90,9 +99,18 @@
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
export interface ImportData {
api: MsTableDataItem<ApiDefinitionDetail>[];
case: MsTableDataItem<ApiCaseDetail>[];
scenario: MsTableDataItem<ApiScenarioTableItem>[];
}
const emit = defineEmits<{
(e: 'copy', data: any[]): void;
(e: 'quote', data: any[]): void;
(e: 'copy', data: ImportData): void;
(e: 'quote', data: ImportData): void;
}>();
const appStore = useAppStore();
@ -103,20 +121,20 @@
});
const activeKey = ref<'api' | 'case' | 'scenario'>('api');
const selectedApis = ref<any[]>([]);
const selectedCases = ref<any[]>([]);
const selectedScenarios = ref<any[]>([]);
const selectedApis = ref<MsTableDataItem<ApiDefinitionDetail>[]>([]);
const selectedCases = ref<MsTableDataItem<ApiCaseDetail>[]>([]);
const selectedScenarios = ref<MsTableDataItem<ApiScenarioTableItem>[]>([]);
const totalSelected = computed(() => {
return selectedApis.value.length + selectedCases.value.length + selectedScenarios.value.length;
});
function handleTableSelect(ids: (string | number)[]) {
function handleTableSelect(data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]) {
if (activeKey.value === 'api') {
selectedApis.value = ids;
selectedApis.value = data as MsTableDataItem<ApiDefinitionDetail>[];
} else if (activeKey.value === 'case') {
selectedCases.value = ids;
selectedCases.value = data as MsTableDataItem<ApiCaseDetail>[];
} else if (activeKey.value === 'scenario') {
selectedScenarios.value = ids;
selectedScenarios.value = data as MsTableDataItem<ApiScenarioTableItem>[];
}
}
@ -148,9 +166,8 @@
const apiTableRef = ref<InstanceType<typeof apiTable>>();
const moduleIds = ref<(string | number)[]>([]);
function resetModuleAndTable() {
function resetModule() {
moduleTreeRef.value?.init(activeKey.value);
apiTableRef.value?.loadPage(['root']); // id
}
function handleModuleSelect(ids: (string | number)[], node: MsTreeNodeData) {
@ -171,33 +188,40 @@
}
function handleCopy() {
emit('copy', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
emit(
'copy',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: selectedScenarios.value,
})
);
handleCancel();
}
function handleQuote() {
emit('quote', [...selectedApis.value, ...selectedCases.value, ...selectedScenarios.value]);
emit(
'quote',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: selectedScenarios.value,
})
);
handleCancel();
}
watch(
() => visible.value,
(val) => {
if (val) {
// 使 v-if tick
nextTick(() => {
resetModuleAndTable();
});
}
},
{
immediate: true,
}
);
onBeforeMount(() => {
initProtocolList();
});
// 使 v-if
onMounted(() => {
nextTick(() => {
// 使 v-if nextTick ref
moduleTreeRef.value?.init(activeKey.value);
});
});
</script>
<style lang="less" scoped>

View File

@ -105,6 +105,7 @@
folderTree.value = await getScenarioModuleTree(params);
}
selectedKeys.value = [folderTree.value[0]?.id];
emit('select', [folderTree.value[0]?.id], folderTree.value[0]);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -22,7 +22,6 @@
no-disable
filter-icon-align-left
v-on="currentTable.propsEvent.value"
@selected-change="handleTableSelect"
>
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
<a-trigger
@ -88,7 +87,7 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import { MsTableColumn, MsTableDataItem } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
@ -99,8 +98,11 @@
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { SelectAllEnum } from '@/enums/tableEnum';
const props = defineProps<{
type: 'api' | 'case' | 'scenario';
@ -108,9 +110,12 @@
protocol: string;
projectId: string | number;
moduleIds: (string | number)[]; // id id
selectedApis: MsTableDataItem<ApiDefinitionDetail>[]; //
selectedCases: MsTableDataItem<ApiCaseDetail>[]; //
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; //
}>();
const emit = defineEmits<{
(e: 'select', ids: (string | number)[]): void;
(e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
}>();
const { t } = useI18n();
@ -161,11 +166,11 @@
showTooltip: true,
width: 200,
},
{
title: 'apiTestManagement.version',
dataIndex: 'versionName',
width: 100,
},
// {
// title: 'apiTestManagement.version',
// dataIndex: 'versionName',
// width: 100,
// },
{
title: 'common.tag',
dataIndex: 'tags',
@ -192,7 +197,10 @@
const methodFilters = ref(Object.keys(RequestMethods));
const statusFilterVisible = ref(false);
const statusFilters = ref(Object.keys(RequestDefinitionStatus));
const tableSelected = ref<(string | number)[]>([]);
const tableSelectedData = ref<MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]>([]);
const tableSelectedKeys = computed(() => {
return tableSelectedData.value.map((e) => e.id);
});
//
const currentTable = computed(() => {
switch (props.type) {
@ -206,6 +214,40 @@
}
});
/**
* 表格单行选中事件处理
*/
function handleRowSelectChange(key: string) {
const selectedData = currentTable.value.propsRes.value.data.find((e) => e.id === key);
if (tableSelectedKeys.value.includes(key)) {
//
tableSelectedData.value = tableSelectedData.value.filter((e) => e.id !== key);
} else if (selectedData) {
tableSelectedData.value.push(selectedData);
}
emit('select', tableSelectedData.value);
}
/**
* 表格全选事件处理
*/
function handleSelectAllChange(v: SelectAllEnum) {
if (v === SelectAllEnum.CURRENT) {
tableSelectedData.value = currentTable.value.propsRes.value.data;
} else {
tableSelectedData.value = [];
}
emit('select', tableSelectedData.value);
}
//
useApiTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
useApiTable.propsEvent.value.selectAllChange = handleSelectAllChange;
useCaseTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
useCaseTable.propsEvent.value.selectAllChange = handleSelectAllChange;
useScenarioTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
useScenarioTable.propsEvent.value.selectAllChange = handleSelectAllChange;
function loadPage(ids?: (string | number)[]) {
nextTick(() => {
// currentTable
@ -226,6 +268,31 @@
});
}
watch(
() => props.type,
(val) => {
switch (val) {
case 'api':
tableSelectedData.value = props.selectedApis;
break;
case 'case':
tableSelectedData.value = props.selectedCases;
break;
case 'scenario':
default:
tableSelectedData.value = props.selectedScenarios;
break;
}
}
);
watch(
() => tableSelectedKeys.value,
(arr) => {
currentTable.value.propsRes.value.selectedKeys = new Set(arr);
}
);
function handleFilterHidden(val: boolean) {
if (!val) {
loadPage();
@ -240,14 +307,6 @@
loadPage();
}
/**
* 处理表格选中
*/
function handleTableSelect(arr: (string | number)[]) {
tableSelected.value = arr;
emit('select', arr);
}
function openApiDetail(id: string | number) {
let routeName: RouteRecordName;
const query: Record<string, any> = {};

View File

@ -98,13 +98,14 @@
function handleCreateActionSelect(val: ScenarioAddStepActionType) {
switch (val) {
case ScenarioAddStepActionType.LOOP_CONTROL:
if (step.value) {
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.LOOP_CONTROL,
name: t('apiScenario.loopControl'),
} as ScenarioStepItem,
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
@ -119,13 +120,14 @@
}
break;
case ScenarioAddStepActionType.CONDITION_CONTROL:
if (step.value) {
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.CONDITION_CONTROL,
name: t('apiScenario.conditionControl'),
} as ScenarioStepItem,
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
@ -140,13 +142,14 @@
}
break;
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
if (step.value) {
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.ONLY_ONCE_CONTROL,
name: t('apiScenario.onlyOnceControl'),
} as ScenarioStepItem,
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
@ -161,13 +164,14 @@
}
break;
case ScenarioAddStepActionType.WAIT_TIME:
if (step.value) {
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.WAIT_TIME,
name: t('apiScenario.waitTime'),
} as ScenarioStepItem,
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);

View File

@ -28,32 +28,32 @@
v-if="showAddChildStep"
:class="[
'arco-trigger-menu-item !mx-0 !w-full',
activeCreateAction === 'addChildStep' ? 'step-tree-active-action' : '',
activeCreateAction === 'inside' ? 'step-tree-active-action' : '',
]"
@click="handleTriggerActionClick('addChildStep')"
@click="handleTriggerActionClick('inside')"
>
<icon-plus size="12" />
{{ t('apiScenario.addChildStep') }}
{{ t('apiScenario.inside') }}
</div>
<div
:class="[
'arco-trigger-menu-item !mx-0 !w-full',
activeCreateAction === 'insertBefore' ? 'step-tree-active-action' : '',
activeCreateAction === 'before' ? 'step-tree-active-action' : '',
]"
@click="handleTriggerActionClick('insertBefore')"
@click="handleTriggerActionClick('before')"
>
<icon-left size="12" />
{{ t('apiScenario.insertBefore') }}
{{ t('apiScenario.before') }}
</div>
<div
:class="[
'arco-trigger-menu-item !mx-0 !w-full',
activeCreateAction === 'insertAfter' ? 'step-tree-active-action' : '',
activeCreateAction === 'after' ? 'step-tree-active-action' : '',
]"
@click="handleTriggerActionClick('insertAfter')"
@click="handleTriggerActionClick('after')"
>
<icon-left size="12" />
{{ t('apiScenario.insertAfter') }}
{{ t('apiScenario.after') }}
</div>
</div>
</template>

View File

@ -2,100 +2,171 @@ import { cloneDeep } from 'lodash-es';
import { ScenarioStepItem } from '../stepTree.vue';
import { getGenerateId, insertNode, TreeNode } from '@/utils';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId, insertNodes, TreeNode } from '@/utils';
import { CreateStepAction } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum';
import { defaultStepItemCommon } from '../../config';
import steps from '@arco-design/web-vue/es/steps';
export default function useCreateActions() {
const { t } = useI18n();
/**
*
*
* @param selectedKeys id
* @param step
* @param parent
*/
function isParentSelected(
function checkedIfNeed(
selectedKeys: (string | number)[],
step: ScenarioStepItem,
step: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
parent?: TreeNode<ScenarioStepItem>
) {
if (parent && selectedKeys.includes(parent.id)) {
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
selectedKeys.push(step.id);
selectedKeys = selectedKeys.concat(step.map((item) => item.id));
}
}
/**
* /
* /-
* @param defaultStepInfo
* @param step
* @param steps
* @param createStepAction
* @param selectedKeys id
*/
function handleCreateStep(
defaultStepInfo: ScenarioStepItem,
step: ScenarioStepItem,
steps: ScenarioStepItem[],
createStepAction: CreateStepAction,
selectedKeys: (string | number)[]
) {
const newStep = {
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id: getGenerateId(),
};
switch (createStepAction) {
case 'addChildStep':
const id = getGenerateId();
if (step.children) {
step.children.push({
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id,
order: step.children.length + 1,
});
} else {
step.children = [
{
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id,
order: 1,
},
];
}
if (selectedKeys.includes(step.id)) {
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
selectedKeys.push(id);
}
case 'inside':
newStep.order = step.children ? step.children.length : 0;
break;
case 'insertBefore':
insertNode<ScenarioStepItem>(
step.children || steps.value,
step.id,
{
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id: getGenerateId(),
order: step.order,
},
'before',
(parent) => isParentSelected(selectedKeys, step, parent),
'id'
);
case 'before':
newStep.order = step.order;
break;
case 'insertAfter':
insertNode<ScenarioStepItem>(
step.children || steps.value,
step.id,
{
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id: getGenerateId(),
order: step.order + 1,
},
'after',
(parent) => isParentSelected(selectedKeys, step, parent),
case 'after':
default:
newStep.order = step.order + 1;
break;
}
insertNodes<ScenarioStepItem>(
step.parent?.children || steps,
step.id,
newStep,
createStepAction,
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
'id'
);
}
'id'
);
/**
*
* @param newSteps
* @param type
* @param startOrder
*/
function buildInsertStepInfos(
newSteps: Record<string, any>[],
type: ScenarioStepType,
startOrder: number
): ScenarioStepItem[] {
let name: string;
switch (type) {
case ScenarioStepType.LOOP_CONTROL:
name = t('apiScenario.loopControl');
break;
case ScenarioStepType.CONDITION_CONTROL:
name = t('apiScenario.conditionControl');
break;
case ScenarioStepType.ONLY_ONCE_CONTROL:
name = t('apiScenario.onlyOnceControl');
break;
case ScenarioStepType.WAIT_TIME:
name = t('apiScenario.waitTime');
break;
case ScenarioStepType.QUOTE_API:
name = t('apiScenario.quoteApi');
break;
case ScenarioStepType.COPY_API:
name = t('apiScenario.copyApi');
break;
case ScenarioStepType.QUOTE_CASE:
name = t('apiScenario.quoteCase');
break;
case ScenarioStepType.COPY_CASE:
name = t('apiScenario.copyCase');
break;
case ScenarioStepType.QUOTE_SCENARIO:
name = t('apiScenario.quoteScenario');
break;
case ScenarioStepType.COPY_SCENARIO:
name = t('apiScenario.copyScenario');
break;
case ScenarioStepType.CUSTOM_API:
name = t('apiScenario.customApi');
break;
case ScenarioStepType.SCRIPT_OPERATION:
name = t('apiScenario.scriptOperation');
break;
default:
break;
}
return newSteps.map((item, index) => {
return {
...cloneDeep(defaultStepItemCommon),
...item,
id: getGenerateId(),
type,
name,
order: startOrder + index,
};
});
}
/**
* /-
* @param step
* @param readyInsertSteps buildInsertStepInfos得到构建后的步骤信息
* @param steps
* @param createStepAction
* @param type
* @param selectedKeys id
*/
function handleCreateSteps(
step: ScenarioStepItem,
readyInsertSteps: ScenarioStepItem[],
steps: ScenarioStepItem[],
createStepAction: CreateStepAction,
selectedKeys: (string | number)[]
) {
insertNodes<ScenarioStepItem>(
step.parent?.children || steps,
step.id,
readyInsertSteps,
createStepAction,
undefined,
'id'
);
checkedIfNeed(selectedKeys, readyInsertSteps, step);
}
return {
handleCreateStep,
isParentSelected,
buildInsertStepInfos,
handleCreateSteps,
checkedIfNeed,
};
}

View File

@ -163,7 +163,7 @@
if (val.length === 0) {
checkedAll.value = false;
indeterminate.value = false;
} else if (val.length === stepInfo.value.steps.length) {
} else if (val.length === totalStepCount.value) {
checkedAll.value = true;
indeterminate.value = false;
} else {

View File

@ -51,7 +51,7 @@
>
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
<MsIcon
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
:type="step.expanded ? 'icon-icon_split-turn-down-left' : 'icon-icon_split_turn-down_arrow'"
:size="14"
/>
{{ step.children?.length || 0 }}
@ -167,7 +167,7 @@
/>
</template>
<template #extraEnd="step">
<executeStatus v-if="step.status" :status="step.status" size="small" />
<executeStatus v-if="step.executeStatus" :status="step.executeStatus" size="small" />
</template>
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
<div
@ -193,7 +193,12 @@
:request="activeStep?.request"
@add-step="addCustomApiStep"
/>
<importApiDrawer v-if="importApiDrawerVisible" v-model:visible="importApiDrawerVisible" />
<importApiDrawer
v-if="importApiDrawerVisible"
v-model:visible="importApiDrawerVisible"
@copy="handleImportApiApply('copy', $event)"
@quote="handleImportApiApply('quote', $event)"
/>
<scriptOperationDrawer
v-if="scriptOperationDrawerVisible"
v-model:visible="scriptOperationDrawerVisible"
@ -242,6 +247,7 @@
import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue';
import { ImportData } from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType.vue';
import createStepActions from './createAction/createStepActions.vue';
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
@ -254,7 +260,15 @@
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { deleteNode, findNodeByKey, getGenerateId, handleTreeDragDrop, insertNode, mapTree, TreeNode } from '@/utils';
import {
deleteNode,
findNodeByKey,
getGenerateId,
handleTreeDragDrop,
insertNodes,
mapTree,
TreeNode,
} from '@/utils';
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
@ -278,7 +292,7 @@
name: string;
description: string;
method?: RequestMethods;
status?: ScenarioExecuteStatus;
executeStatus?: ScenarioExecuteStatus;
num?: number; //
//
belongProjectId?: string;
@ -293,7 +307,7 @@
checked: boolean; //
expanded: boolean; //
createActionsVisible?: boolean; //
parent?: ScenarioStepItem | ScenarioStepItem[]; // undefined
parent?: ScenarioStepItem; // undefined
loopNum: number;
loopType: 'num' | 'while' | 'forEach';
loopSpace: number;
@ -376,7 +390,7 @@
/**
* 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中
*/
function isParentSelected(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
if (parent && selectedKeys.value.includes(parent.id)) {
//
selectedKeys.value.push(step.id);
@ -455,7 +469,7 @@
switch (item.eventTag) {
case 'copy':
const id = getGenerateId();
insertNode<ScenarioStepItem>(
insertNodes<ScenarioStepItem>(
steps.value,
node.id,
{
@ -472,7 +486,7 @@
id,
},
'after',
isParentSelected,
checkedIfNeed,
'id'
);
break;
@ -574,7 +588,6 @@
customApiDrawerVisible.value = true;
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
activeStep.value = step;
console.log('activeStep', activeStep.value);
scriptOperationDrawerVisible.value = true;
}
}
@ -611,12 +624,58 @@
}
}
const { handleCreateStep } = useCreateActions();
const { handleCreateStep, handleCreateSteps, buildInsertStepInfos } = useCreateActions();
/**
* 处理导入系统请求
* @param type 导入类型
* @param data 导入数据
*/
function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) {
let order = steps.value.length + 1;
if (activeStep.value && activeCreateAction.value) {
switch (activeCreateAction.value) {
case 'inside':
order = activeStep.value.children ? activeStep.value.children.length : 0;
break;
case 'before':
order = activeStep.value.order;
break;
case 'after':
order = activeStep.value.order + 1;
break;
default:
break;
}
}
const insertApiSteps = buildInsertStepInfos(
data.api,
type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API,
order
);
const insertCaseSteps = buildInsertStepInfos(
data.case,
type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE,
order + insertApiSteps.length
);
const insertScenarioSteps = buildInsertStepInfos(
data.scenario,
type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO,
order + insertApiSteps.length + insertCaseSteps.length
);
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
if (activeStep.value && activeCreateAction.value) {
handleCreateSteps(activeStep.value, insertSteps, steps.value, activeCreateAction.value, selectedKeys.value);
} else {
steps.value = steps.value.concat(insertSteps);
}
}
/**
* 添加自定义 API 步骤
*/
function addCustomApiStep(request: RequestParam) {
if (activeStep.value) {
if (activeStep.value && activeCreateAction.value) {
handleCreateStep(
{
type: ScenarioStepType.CUSTOM_API,
@ -624,6 +683,7 @@
request: cloneDeep(request),
} as ScenarioStepItem,
activeStep.value,
steps.value,
activeCreateAction.value,
selectedKeys.value
);
@ -643,7 +703,7 @@
* 添加脚本操作步骤
*/
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
if (activeStep.value) {
if (activeStep.value && activeCreateAction.value) {
handleCreateStep(
{
type: ScenarioStepType.SCRIPT_OPERATION,
@ -651,10 +711,10 @@
script: cloneDeep(scriptProcessor),
} as ScenarioStepItem,
activeStep.value,
steps.value,
activeCreateAction.value,
selectedKeys.value
);
console.log('activeStep', activeStep.value);
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
@ -668,7 +728,19 @@
}
/**
* 处理文件夹树节点拖拽事件
* 释放允许拖拽步骤到释放的节点内
* @param dropNode 释放节点
*/
function isAllowDropInside(dropNode: MsTreeNodeData) {
return [
ScenarioStepType.LOOP_CONTROL,
ScenarioStepType.CONDITION_CONTROL,
ScenarioStepType.ONLY_ONCE_CONTROL,
].includes(dropNode.type);
}
/**
* 处理步骤节点拖拽事件
* @param tree 树数据
* @param dragNode 拖拽节点
* @param dropNode 释放节点
@ -681,19 +753,38 @@
dropPosition: number
) {
try {
if (dropPosition === 0 && !isAllowDropInside(dropNode)) {
// Message.error(t('apiScenario.notAllowDropInside')); TODO:
return;
}
loading.value = true;
const offspringIds: string[] = [];
mapTree(dragNode.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
const stepIdAndOffspringIds = [dragNode.id, ...offspringIds];
if (dropPosition === 0) {
//
if (selectedKeys.value.includes(dropNode.id)) {
//
selectedKeys.value.push(dragNode.id);
//
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
}
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.id)) {
//
selectedKeys.value.push(dragNode.id);
//
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.id)) {
//
selectedKeys.value = selectedKeys.value.filter((e) => e !== dragNode.id);
//
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, 'id');
if (dragResult) {

View File

@ -51,6 +51,16 @@
allow-search
/>
</a-form-item>
<a-form-item :label="t('apiScenario.scenarioLevel')">
<a-select v-model:model-value="scenario.priority" :placeholder="t('common.pleaseSelect')">
<template #label>
<span class="text-[var(--color-text-2)]"> <caseLevel :case-level="scenario.priority" /></span>
</template>
<a-option v-for="item of casePriorityOptions" :key="item.value" :value="item.value">
<caseLevel :case-level="item.label as CaseLevel" />
</a-option>
</a-select>
</a-form-item>
<a-form-item :label="t('apiScenario.status')" class="mb-[16px]">
<a-select
v-model:model-value="scenario.status"
@ -120,6 +130,8 @@
<script setup lang="ts">
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { useI18n } from '@/hooks/useI18n';
@ -128,6 +140,8 @@
import { ModuleTreeNode } from '@/models/common';
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
//
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
const params = defineAsyncComponent(() => import('../components/params.vue'));

View File

@ -15,10 +15,12 @@
</a-tooltip>
</template>
</MsEditableTab>
<div class="flex items-center gap-[8px]">
<div v-if="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]">
<environmentSelect />
<a-button type="primary" :loading="saveLoading" @click="saveScenario">
{{ t('common.save') }}
</a-button>
<!-- <executeButton /> -->
</div>
</div>
<a-divider class="!my-0" />
@ -79,6 +81,8 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import scenarioModuleTree from './components/scenarioModuleTree.vue';
import { ScenarioStepInfo } from './components/step/index.vue';
import environmentSelect from '@/views/api-test/components/environmentSelect.vue';
// import executeButton from '@/views/api-test/components/executeButton.vue';
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
import { getTrashModuleCount } from '@/api/modules/api-test/scenario';
@ -114,6 +118,7 @@
isNew: true,
name: '',
moduleId: 'root',
priority: 'P0',
stepInfo: {
id: new Date().getTime(),
steps: [],

View File

@ -105,9 +105,9 @@ export default {
'apiScenario.crossProject': '跨项目',
'apiScenario.expandStepTip': '展开 {count} 个子步骤',
'apiScenario.collapseStepTip': '折叠 {count} 个子步骤',
'apiScenario.addChildStep': '添加子步骤',
'apiScenario.insertBefore': '在之前插入步骤',
'apiScenario.insertAfter': '在之后插入步骤',
'apiScenario.inside': '添加子步骤',
'apiScenario.before': '在之前插入步骤',
'apiScenario.after': '在之后插入步骤',
'apiScenario.num': '次数',
'apiScenario.space': '间隔(ms)',
'apiScenario.overTime': '超时(ms)',
@ -132,6 +132,7 @@ export default {
'apiScenario.topStep': '一级步骤',
'apiScenario.allStep': '所有子步骤',
'apiScenario.saveAsApi': '保存为新接口',
'apiScenario.scenarioLevel': '场景等级',
// 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号',