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": { "dependencies": {
"@7polo/kity": "2.0.8", "@7polo/kity": "2.0.8",
"@7polo/kityminder-core": "1.4.53", "@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", "@arco-themes/vue-ms-theme-default": "^0.0.30",
"@form-create/arco-design": "^3.1.23", "@form-create/arco-design": "^3.1.23",
"@halo-dev/richtext-editor": "0.0.0-alpha.33", "@halo-dev/richtext-editor": "0.0.0-alpha.33",

View File

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

View File

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

View File

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

View File

@ -16,7 +16,8 @@ export default function useOpenNewPage() {
window.open( window.open(
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${ `${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${appStore.currentOrgId}&projectId=${
appStore.currentProjectId 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 { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
import { ApiDefinitionCustomField } from '@/models/apiTest/management'; import { ApiDefinitionCustomField } from '@/models/apiTest/management';
@ -184,13 +185,14 @@ export type ScenarioStepLoopType = 'num' | 'while' | 'forEach';
// 场景步骤-循环控制器-循环类型 // 场景步骤-循环控制器-循环类型
export type ScenarioStepLoopWhileType = 'condition' | 'expression'; export type ScenarioStepLoopWhileType = 'condition' | 'expression';
// 场景步骤-步骤插入类型 // 场景步骤-步骤插入类型
export type CreateStepAction = 'addChildStep' | 'insertBefore' | 'insertAfter' | undefined; export type CreateStepAction = 'inside' | 'before' | 'after';
// 场景步骤 // 场景步骤
export interface Scenario { export interface Scenario {
id: string; id: string;
name: string; name: string;
moduleId: string | number; moduleId: string | number;
stepInfo: ScenarioStepInfo; stepInfo: ScenarioStepInfo;
priority: CaseLevel;
status: RequestDefinitionStatus; status: RequestDefinitionStatus;
tags: string[]; tags: string[];
params: Record<string, any>[]; params: Record<string, any>[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,6 @@
no-disable no-disable
filter-icon-align-left filter-icon-align-left
v-on="currentTable.propsEvent.value" v-on="currentTable.propsEvent.value"
@selected-change="handleTableSelect"
> >
<template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }"> <template v-if="props.protocol === 'HTTP'" #methodFilter="{ columnConfig }">
<a-trigger <a-trigger
@ -88,7 +87,7 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.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 useTable from '@/components/pure/ms-table/useTable';
import { MsTreeNodeData } from '@/components/business/ms-tree/types'; import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
@ -99,8 +98,11 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; 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 { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum'; import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { SelectAllEnum } from '@/enums/tableEnum';
const props = defineProps<{ const props = defineProps<{
type: 'api' | 'case' | 'scenario'; type: 'api' | 'case' | 'scenario';
@ -108,9 +110,12 @@
protocol: string; protocol: string;
projectId: string | number; projectId: string | number;
moduleIds: (string | number)[]; // id id moduleIds: (string | number)[]; // id id
selectedApis: MsTableDataItem<ApiDefinitionDetail>[]; //
selectedCases: MsTableDataItem<ApiCaseDetail>[]; //
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'select', ids: (string | number)[]): void; (e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -161,11 +166,11 @@
showTooltip: true, showTooltip: true,
width: 200, width: 200,
}, },
{ // {
title: 'apiTestManagement.version', // title: 'apiTestManagement.version',
dataIndex: 'versionName', // dataIndex: 'versionName',
width: 100, // width: 100,
}, // },
{ {
title: 'common.tag', title: 'common.tag',
dataIndex: 'tags', dataIndex: 'tags',
@ -192,7 +197,10 @@
const methodFilters = ref(Object.keys(RequestMethods)); const methodFilters = ref(Object.keys(RequestMethods));
const statusFilterVisible = ref(false); const statusFilterVisible = ref(false);
const statusFilters = ref(Object.keys(RequestDefinitionStatus)); 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(() => { const currentTable = computed(() => {
switch (props.type) { 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)[]) { function loadPage(ids?: (string | number)[]) {
nextTick(() => { nextTick(() => {
// currentTable // 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) { function handleFilterHidden(val: boolean) {
if (!val) { if (!val) {
loadPage(); loadPage();
@ -240,14 +307,6 @@
loadPage(); loadPage();
} }
/**
* 处理表格选中
*/
function handleTableSelect(arr: (string | number)[]) {
tableSelected.value = arr;
emit('select', arr);
}
function openApiDetail(id: string | number) { function openApiDetail(id: string | number) {
let routeName: RouteRecordName; let routeName: RouteRecordName;
const query: Record<string, any> = {}; const query: Record<string, any> = {};

View File

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

View File

@ -28,32 +28,32 @@
v-if="showAddChildStep" v-if="showAddChildStep"
:class="[ :class="[
'arco-trigger-menu-item !mx-0 !w-full', '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" /> <icon-plus size="12" />
{{ t('apiScenario.addChildStep') }} {{ t('apiScenario.inside') }}
</div> </div>
<div <div
:class="[ :class="[
'arco-trigger-menu-item !mx-0 !w-full', '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" /> <icon-left size="12" />
{{ t('apiScenario.insertBefore') }} {{ t('apiScenario.before') }}
</div> </div>
<div <div
:class="[ :class="[
'arco-trigger-menu-item !mx-0 !w-full', '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" /> <icon-left size="12" />
{{ t('apiScenario.insertAfter') }} {{ t('apiScenario.after') }}
</div> </div>
</div> </div>
</template> </template>

View File

@ -2,100 +2,171 @@ import { cloneDeep } from 'lodash-es';
import { ScenarioStepItem } from '../stepTree.vue'; 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 { CreateStepAction } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum';
import { defaultStepItemCommon } from '../../config'; import { defaultStepItemCommon } from '../../config';
import steps from '@arco-design/web-vue/es/steps';
export default function useCreateActions() { export default function useCreateActions() {
const { t } = useI18n();
/** /**
* *
* @param selectedKeys id
* @param step
* @param parent
*/ */
function isParentSelected( function checkedIfNeed(
selectedKeys: (string | number)[], selectedKeys: (string | number)[],
step: ScenarioStepItem, step: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
parent?: TreeNode<ScenarioStepItem> parent?: TreeNode<ScenarioStepItem>
) { ) {
if (parent && selectedKeys.includes(parent.id)) { 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( function handleCreateStep(
defaultStepInfo: ScenarioStepItem, defaultStepInfo: ScenarioStepItem,
step: ScenarioStepItem, step: ScenarioStepItem,
steps: ScenarioStepItem[],
createStepAction: CreateStepAction, createStepAction: CreateStepAction,
selectedKeys: (string | number)[] selectedKeys: (string | number)[]
) { ) {
const newStep = {
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id: getGenerateId(),
};
switch (createStepAction) { switch (createStepAction) {
case 'addChildStep': case 'inside':
const id = getGenerateId(); newStep.order = step.children ? step.children.length : 0;
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);
}
break; break;
case 'insertBefore': case 'before':
insertNode<ScenarioStepItem>( newStep.order = step.order;
step.children || steps.value,
step.id,
{
...cloneDeep(defaultStepItemCommon),
...defaultStepInfo,
id: getGenerateId(),
order: step.order,
},
'before',
(parent) => isParentSelected(selectedKeys, step, parent),
'id'
);
break; break;
case 'insertAfter': case 'after':
insertNode<ScenarioStepItem>( default:
step.children || steps.value, newStep.order = step.order + 1;
step.id, break;
{ }
...cloneDeep(defaultStepItemCommon), insertNodes<ScenarioStepItem>(
...defaultStepInfo, step.parent?.children || steps,
id: getGenerateId(), step.id,
order: step.order + 1, newStep,
}, createStepAction,
'after', (newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
(parent) => isParentSelected(selectedKeys, step, 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; break;
default: default:
break; 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 { return {
handleCreateStep, handleCreateStep,
isParentSelected, buildInsertStepInfos,
handleCreateSteps,
checkedIfNeed,
}; };
} }

View File

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

View File

@ -51,7 +51,7 @@
> >
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]"> <div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
<MsIcon <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" :size="14"
/> />
{{ step.children?.length || 0 }} {{ step.children?.length || 0 }}
@ -167,7 +167,7 @@
/> />
</template> </template>
<template #extraEnd="step"> <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>
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty> <template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
<div <div
@ -193,7 +193,12 @@
:request="activeStep?.request" :request="activeStep?.request"
@add-step="addCustomApiStep" @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 <scriptOperationDrawer
v-if="scriptOperationDrawerVisible" v-if="scriptOperationDrawerVisible"
v-model:visible="scriptOperationDrawerVisible" v-model:visible="scriptOperationDrawerVisible"
@ -242,6 +247,7 @@
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types'; import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue'; import executeStatus from '../common/executeStatus.vue';
import { ImportData } from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType.vue'; import stepType from '../common/stepType.vue';
import createStepActions from './createAction/createStepActions.vue'; import createStepActions from './createAction/createStepActions.vue';
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue'; import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
@ -254,7 +260,15 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; 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 { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario'; import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
@ -278,7 +292,7 @@
name: string; name: string;
description: string; description: string;
method?: RequestMethods; method?: RequestMethods;
status?: ScenarioExecuteStatus; executeStatus?: ScenarioExecuteStatus;
num?: number; // num?: number; //
// //
belongProjectId?: string; belongProjectId?: string;
@ -293,7 +307,7 @@
checked: boolean; // checked: boolean; //
expanded: boolean; // expanded: boolean; //
createActionsVisible?: boolean; // createActionsVisible?: boolean; //
parent?: ScenarioStepItem | ScenarioStepItem[]; // undefined parent?: ScenarioStepItem; // undefined
loopNum: number; loopNum: number;
loopType: 'num' | 'while' | 'forEach'; loopType: 'num' | 'while' | 'forEach';
loopSpace: number; 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)) { if (parent && selectedKeys.value.includes(parent.id)) {
// //
selectedKeys.value.push(step.id); selectedKeys.value.push(step.id);
@ -455,7 +469,7 @@
switch (item.eventTag) { switch (item.eventTag) {
case 'copy': case 'copy':
const id = getGenerateId(); const id = getGenerateId();
insertNode<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
steps.value, steps.value,
node.id, node.id,
{ {
@ -472,7 +486,7 @@
id, id,
}, },
'after', 'after',
isParentSelected, checkedIfNeed,
'id' 'id'
); );
break; break;
@ -574,7 +588,6 @@
customApiDrawerVisible.value = true; customApiDrawerVisible.value = true;
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) { } else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
activeStep.value = step; activeStep.value = step;
console.log('activeStep', activeStep.value);
scriptOperationDrawerVisible.value = true; 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 步骤 * 添加自定义 API 步骤
*/ */
function addCustomApiStep(request: RequestParam) { function addCustomApiStep(request: RequestParam) {
if (activeStep.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.CUSTOM_API, type: ScenarioStepType.CUSTOM_API,
@ -624,6 +683,7 @@
request: cloneDeep(request), request: cloneDeep(request),
} as ScenarioStepItem, } as ScenarioStepItem,
activeStep.value, activeStep.value,
steps.value,
activeCreateAction.value, activeCreateAction.value,
selectedKeys.value selectedKeys.value
); );
@ -643,7 +703,7 @@
* 添加脚本操作步骤 * 添加脚本操作步骤
*/ */
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) { function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
if (activeStep.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.SCRIPT_OPERATION, type: ScenarioStepType.SCRIPT_OPERATION,
@ -651,10 +711,10 @@
script: cloneDeep(scriptProcessor), script: cloneDeep(scriptProcessor),
} as ScenarioStepItem, } as ScenarioStepItem,
activeStep.value, activeStep.value,
steps.value,
activeCreateAction.value, activeCreateAction.value,
selectedKeys.value selectedKeys.value
); );
console.log('activeStep', activeStep.value);
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...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 tree 树数据
* @param dragNode 拖拽节点 * @param dragNode 拖拽节点
* @param dropNode 释放节点 * @param dropNode 释放节点
@ -681,19 +753,38 @@
dropPosition: number dropPosition: number
) { ) {
try { try {
if (dropPosition === 0 && !isAllowDropInside(dropNode)) {
// Message.error(t('apiScenario.notAllowDropInside')); TODO:
return;
}
loading.value = true; 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 (dropPosition === 0) {
// //
if (selectedKeys.value.includes(dropNode.id)) { 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)) { } 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)) { } 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'); const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
if (dragResult) { if (dragResult) {

View File

@ -51,6 +51,16 @@
allow-search allow-search
/> />
</a-form-item> </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-form-item :label="t('apiScenario.status')" class="mb-[16px]">
<a-select <a-select
v-model:model-value="scenario.status" v-model:model-value="scenario.status"
@ -120,6 +130,8 @@
<script setup lang="ts"> <script setup lang="ts">
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/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 apiStatus from '@/views/api-test/components/apiStatus.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -128,6 +140,8 @@
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum'; import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
// //
const step = defineAsyncComponent(() => import('../components/step/index.vue')); const step = defineAsyncComponent(() => import('../components/step/index.vue'));
const params = defineAsyncComponent(() => import('../components/params.vue')); const params = defineAsyncComponent(() => import('../components/params.vue'));

View File

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

View File

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