feat(接口场景): 场景步骤 90%&导入系统请求API&CASE完成

This commit is contained in:
baiqi 2024-03-22 18:17:00 +08:00 committed by Craftsman
parent 09454001bd
commit cef0f5895f
23 changed files with 378 additions and 262 deletions

View File

@ -179,7 +179,7 @@ export function updateDefinition(data: ApiDefinitionUpdateParams) {
} }
// 获取接口定义详情 // 获取接口定义详情
export function getDefinitionDetail(id: string) { export function getDefinitionDetail(id: string | number) {
return MSR.get<ApiDefinitionDetail>({ url: GetDefinitionDetailUrl, params: id }); return MSR.get<ApiDefinitionDetail>({ url: GetDefinitionDetailUrl, params: id });
} }

View File

@ -416,16 +416,17 @@
background-color: rgb(var(--primary-7)); background-color: rgb(var(--primary-7));
} }
} }
.arco-checkbox-disabled, .arco-checkbox-disabled.arco-checkbox-checked,
.arco-checkbox-disabled:hover { .arco-checkbox-disabled.arco-checkbox-checked:hover {
.arco-checkbox-icon { .arco-checkbox-icon {
@apply bg-white; @apply bg-white text-white;
border: 1px solid var(--color-text-input-border); border: none;
background: rgb(var(--primary-2)) !important;
}
.arco-checkbox-icon-check {
background-color: rgb(var(--primary-2)) !important;
} }
}
.arco-checkbox-disabled.arco-checkbox-checked .arco-checkbox-icon {
background: rgb(var(--primary-2)) !important ;
} }
/** radio **/ /** radio **/

View File

@ -20,7 +20,7 @@
<slot name="title"> <slot name="title">
<div class="flex flex-1 items-center justify-between"> <div class="flex flex-1 items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<a-tooltip :content="props.title"> <a-tooltip :disabled="!props.title" :content="props.title">
<span> {{ characterLimit(props.title) }}</span> <span> {{ characterLimit(props.title) }}</span>
</a-tooltip> </a-tooltip>
@ -161,7 +161,7 @@
showFullScreen: false, showFullScreen: false,
okPermission: () => [], // okPermission: () => [], //
}); });
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue']); const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue', 'close']);
const { t } = useI18n(); const { t } = useI18n();
@ -191,6 +191,7 @@
const handleClose = () => { const handleClose = () => {
visible.value = false; visible.value = false;
emit('update:visible', false); emit('update:visible', false);
emit('close');
}; };
const resizing = ref(false); // const resizing = ref(false); //

View File

@ -29,8 +29,9 @@
<template #title> <template #title>
<SelectALL <SelectALL
v-if="attrs.selectorType === 'checkbox'" v-if="attrs.selectorType === 'checkbox'"
:total="selectTotal" :total="attrs.showPagination ? (attrs.msPagination as MsPaginationI).total : (attrs.data as MsTableDataItem<TableData>[]).length"
:current="selectCurrent" :selected-keys="props.selectedKeys"
:current-data="attrs.data as Record<string,any>[]"
:show-select-all="!!attrs.showPagination && props.showSelectorAll" :show-select-all="!!attrs.showPagination && props.showSelectorAll"
:disabled="(attrs.data as []).length === 0" :disabled="(attrs.data as []).length === 0"
@change="handleSelectAllChange" @change="handleSelectAllChange"
@ -208,17 +209,17 @@
class="mt-[16px] flex h-[32px] flex-row flex-nowrap items-center" class="mt-[16px] flex h-[32px] flex-row flex-nowrap items-center"
:class="{ 'justify-between': showBatchAction }" :class="{ 'justify-between': showBatchAction }"
> >
<span v-if="!props.actionConfig && selectCurrent > 0" class="title text-[var(--color-text-2)]" <span v-if="!props.actionConfig && selectedCount > 0" class="title text-[var(--color-text-2)]">
>{{ t('msTable.batch.selected', { count: selectCurrent }) }} {{ t('msTable.batch.selected', { count: selectedCount }) }}
<a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">{{ <a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">
t('msTable.batch.clear') {{ t('msTable.batch.clear') }}
}}</a-button></span </a-button>
> </span>
<div class="flex flex-grow"> <div class="flex flex-grow">
<batch-action <batch-action
v-if="showBatchAction" v-if="showBatchAction"
class="flex-1" class="flex-1"
:select-row-count="selectCurrent" :select-row-count="selectedCount"
:action-config="props.actionConfig" :action-config="props.actionConfig"
@batch-action="handleBatchAction" @batch-action="handleBatchAction"
@clear="emit('clearSelector')" @clear="emit('clearSelector')"
@ -227,7 +228,6 @@
<div class="min-w-[500px]"> <div class="min-w-[500px]">
<ms-pagination <ms-pagination
v-if="!!attrs.showPagination" v-if="!!attrs.showPagination"
v-show="props.selectorStatus !== SelectAllEnum.CURRENT"
size="small" size="small"
v-bind="(attrs.msPagination as MsPaginationI)" v-bind="(attrs.msPagination as MsPaginationI)"
hide-on-single-page hide-on-single-page
@ -329,31 +329,6 @@
(e: 'moduleChange'): void; (e: 'moduleChange'): void;
}>(); }>();
const attrs = useAttrs(); const attrs = useAttrs();
// -
const selectTotal = computed(() => {
const { selectorStatus } = props;
if (!attrs.showPagination) {
// total
return (attrs.data as MsTableDataItem<TableData>[]).length;
}
if (selectorStatus === SelectAllEnum.CURRENT) {
const { pageSize, total } = attrs.msPagination as MsPaginationI;
if (pageSize > total) {
return total;
}
return pageSize;
}
return (attrs.msPagination as MsPaginationI)?.total || appStore.pageSize;
});
// -
const selectCurrent = computed(() => {
const { selectorStatus, excludeKeys, selectedKeys } = props;
if (selectorStatus === SelectAllEnum.ALL) {
return selectTotal.value - excludeKeys.size;
}
return selectedKeys.size;
});
// Active // Active
const editActiveKey = ref<string>(''); const editActiveKey = ref<string>('');
@ -483,8 +458,15 @@
emit('pageSizeChange', v); emit('pageSizeChange', v);
}; };
const selectedCount = computed(() => {
if (props.selectorStatus === SelectAllEnum.ALL && attrs.msPagination) {
return (attrs.msPagination as MsPaginationI).total - props.excludeKeys.size;
}
return props.selectedKeys.size;
});
const showBatchAction = computed(() => { const showBatchAction = computed(() => {
return selectCurrent.value > 0 && attrs.selectable; return selectedCount.value > 0 && attrs.selectable;
}); });
const handleBatchAction = (value: BatchActionParams) => { const handleBatchAction = (value: BatchActionParams) => {
@ -493,7 +475,7 @@
selectedIds: Array.from(selectedKeys), selectedIds: Array.from(selectedKeys),
excludeIds: Array.from(excludeKeys), excludeIds: Array.from(excludeKeys),
selectAll: selectorStatus === SelectAllEnum.ALL, selectAll: selectorStatus === SelectAllEnum.ALL,
currentSelectCount: selectCurrent.value, currentSelectCount: selectedCount.value,
params: { params: {
...(attrs.msPagination as MsPaginationI), ...(attrs.msPagination as MsPaginationI),
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="ms-table-select-all"> <div class="ms-table-select-all">
<a-checkbox <a-checkbox
v-model="checked" v-model:model-value="checked"
:disabled="props.disabled" :disabled="props.disabled"
class="text-base" class="text-base"
:indeterminate="indeterminate" :indeterminate="indeterminate"
@ -20,14 +20,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watchEffect } from 'vue';
import MsIcon from '../ms-icon-font/index.vue'; import MsIcon from '../ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { SelectAllEnum } from '@/enums/tableEnum'; import { SelectAllEnum } from '@/enums/tableEnum';
import { MsTableDataItem } from './type';
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits<{ const emit = defineEmits<{
@ -35,7 +35,13 @@
}>(); }>();
const props = withDefaults( const props = withDefaults(
defineProps<{ current: number; total: number; showSelectAll: boolean; disabled: boolean }>(), defineProps<{
selectedKeys: Set<string>;
total: number;
currentData: MsTableDataItem<Record<string, any>>[];
showSelectAll: boolean;
disabled: boolean;
}>(),
{ {
current: 0, current: 0,
total: 0, total: 0,
@ -44,20 +50,17 @@
} }
); );
const checked = ref(false); const checked = computed({
const indeterminate = ref(false); get: () => {
return props.selectedKeys.size === props.total;
watchEffect(() => { },
if (props.current === 0) { set: (value) => {
checked.value = false; return value;
indeterminate.value = false; },
} else if (props.current < props.total) { });
checked.value = false; const indeterminate = computed(() => {
indeterminate.value = true; // 0
} else if (props.current === props.total) { return props.selectedKeys.size > 0 && props.selectedKeys.size < props.total;
checked.value = true;
indeterminate.value = false;
}
}); });
const handleSelect = (v: string | number | Record<string, any> | undefined) => { const handleSelect = (v: string | number | Record<string, any> | undefined) => {
@ -65,9 +68,11 @@
}; };
const handleCheckChange = () => { const handleCheckChange = () => {
if (checked.value) { if (props.currentData.some((item) => !props.selectedKeys.has(item.id))) {
//
handleSelect(SelectAllEnum.CURRENT); handleSelect(SelectAllEnum.CURRENT);
} else { } else {
//
handleSelect(SelectAllEnum.NONE); handleSelect(SelectAllEnum.NONE);
} }
}; };

View File

@ -59,6 +59,7 @@ export type MsTableErrorStatus = boolean | 'error' | 'empty';
export type MsTableDataItem<T> = T & { export type MsTableDataItem<T> = T & {
updateTime?: string | number | null; updateTime?: string | number | null;
createTime?: string | number | null; createTime?: string | number | null;
children?: MsTableDataItem<T>[];
} & TableData; } & TableData;
// 表格属性 // 表格属性
export interface MsTableProps<T> { export interface MsTableProps<T> {

View File

@ -296,8 +296,17 @@ export default function useTableProps<T>(
// 重置选择器 // 重置选择器
const resetSelector = (isNone = true) => { const resetSelector = (isNone = true) => {
propsRes.value.selectedKeys.clear(); if (propsRes.value.selectorStatus === SelectAllEnum.ALL) {
propsRes.value.excludeKeys.clear(); // 当前是跨页全部选中状态,则取消当前页的选中项
propsRes.value.data.forEach((item) => {
propsRes.value.selectedKeys.delete(item.id);
propsRes.value.excludeKeys.add(item.id);
});
} else {
// 当前是当前页选中状态,则清空选中项
propsRes.value.selectedKeys.clear();
propsRes.value.excludeKeys.clear();
}
if (isNone) { if (isNone) {
propsRes.value.selectorStatus = SelectAllEnum.NONE; propsRes.value.selectorStatus = SelectAllEnum.NONE;
} }
@ -311,26 +320,18 @@ export default function useTableProps<T>(
// 如果是全选状态,返回总数减去排除的数量 // 如果是全选状态,返回总数减去排除的数量
return msPagination.total - excludeKeys.size; return msPagination.total - excludeKeys.size;
} }
// if (selectorStatus === SelectAllEnum.NONE) {
// 如果是全不选状态,返回选中的数量
return selectedKeys.size; return selectedKeys.size;
// }
// if (selectorStatus === SelectAllEnum.CURRENT) {
// // 如果是当前页状态,返回当前页减去排除的数量
// return msPagination.pageSize - excludeKeys.size;
// }
} }
}; };
const collectIds = (data, rowKey: string, selectedKeys: Set<string>) => { const collectIds = (data: MsTableDataItem<T>[], rowKey: string) => {
data.forEach((item: any) => { data.forEach((item: MsTableDataItem<T>) => {
if (item[rowKey] && !selectedKeys.has(item[rowKey])) { if (item[rowKey] && !propsRes.value.selectedKeys.has(item[rowKey])) {
selectedKeys.add(item[rowKey]); propsRes.value.selectedKeys.add(item[rowKey]);
} }
if (item.children) { if (item.children) {
collectIds(item.children, rowKey, selectedKeys); collectIds(item.children, rowKey);
} }
}); });
return selectedKeys;
}; };
// 获取表格请求参数 // 获取表格请求参数
@ -398,19 +399,19 @@ export default function useTableProps<T>(
}, },
// 重置筛选 // 重置筛选
clearSelector: () => { clearSelector: () => {
propsRes.value.selectorStatus = SelectAllEnum.NONE; // 重置选择器状态
resetSelector(); resetSelector();
}, },
// 表格SelectAll change // 表格SelectAll change
selectAllChange: (v: SelectAllEnum) => { selectAllChange: (v: SelectAllEnum) => {
propsRes.value.selectorStatus = v; propsRes.value.selectorStatus = v;
const { data, rowKey, selectedKeys } = propsRes.value; const { data, rowKey } = propsRes.value;
if (v === SelectAllEnum.NONE) { if (v === SelectAllEnum.NONE) {
// 清空选中项 // 清空选中项
resetSelector(); resetSelector();
} else { } else {
resetSelector(false); collectIds(data as MsTableDataItem<T>[], rowKey);
propsRes.value.selectedKeys = collectIds(data, rowKey, selectedKeys);
} }
}, },

View File

@ -417,9 +417,9 @@ export function insertNodes<T>(
} }
if (typeof customFunc === 'function') { if (typeof customFunc === 'function') {
if (Array.isArray(newNodes)) { if (Array.isArray(newNodes)) {
newNodes.forEach((newNode) => customFunc(newNode, parent || node.parent)); newNodes.forEach((newNode) => customFunc(newNode, position === 'inside' ? node : parent));
} else { } else {
customFunc(newNodes, parent || node.parent); customFunc(newNodes, position === 'inside' ? node : parent);
} }
} }
// 插入后返回 true // 插入后返回 true

View File

@ -1,5 +1,5 @@
interface Tree { interface Tree {
id: string | number; id?: string | number;
groupId?: number; groupId?: number;
children?: Tree[]; children?: Tree[];
[key: string]: any; [key: string]: any;

View File

@ -2,6 +2,7 @@
<a-select <a-select
v-model:model-value="method" v-model:model-value="method"
:placeholder="t('common.pleaseSelect')" :placeholder="t('common.pleaseSelect')"
:disabled="props.disabled"
@change="(val) => emit('change', val as string)" @change="(val) => emit('change', val as string)"
> >
<template #label="{ data }"> <template #label="{ data }">
@ -24,6 +25,7 @@
const props = defineProps<{ const props = defineProps<{
modelValue: string; modelValue: string;
disabled?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string): void; (e: 'update:modelValue', value: string): void;

View File

@ -834,6 +834,7 @@
const formData = tempForm || requestVModel.value; const formData = tempForm || requestVModel.value;
if (fApi.value) { if (fApi.value) {
fApi.value.nextTick(() => { fApi.value.nextTick(() => {
// 使nextTick使v-if
const form = {}; const form = {};
controlPluginFormFields().forEach((key) => { controlPluginFormFields().forEach((key) => {
form[key] = formData[key]; form[key] = formData[key];
@ -934,6 +935,17 @@
const splitContainerRef = ref<HTMLElement>(); const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0); const secondBoxHeight = ref(0);
watch(
() => showResponse.value,
(val) => {
if (val) {
splitBoxSize.value = 0.6;
} else {
splitBoxSize.value = 1;
}
}
);
watch( watch(
() => splitBoxSize.value, () => splitBoxSize.value,
debounce((val) => { debounce((val) => {

View File

@ -1,56 +1,42 @@
<template> <template>
<MsDrawer <MsDrawer
v-model:visible="visible" v-model:visible="visible"
:title="t('apiScenario.customApi')"
:width="960" :width="960"
no-content-padding no-content-padding
disabled-width-drag
:show-continue="true" :show-continue="true"
:footer="!!requestVModel.isNew"
@confirm="handleSave" @confirm="handleSave"
@continue="handleContinue" @continue="handleContinue"
@close="handleClose"
> >
<a-empty
v-if="pluginError && !isHttpProtocol"
:description="t('apiTestDebug.noPlugin')"
class="h-[200px] items-center justify-center"
>
<template #image>
<MsIcon type="icon-icon_plugin_outlined" size="48" />
</template>
</a-empty>
<template #title> <template #title>
<div style="width: 100%"> <div class="flex items-center gap-[8px]">
<div style="float: left"> {{ t('apiScenario.customApi') }}</div> <stepType
<div style="float: right"> v-if="props.requestType"
<span v-show="props.requestType !== ScenarioStepType.CUSTOM_API"
v-show="requestVModel.useEnv === 'false'" :type="props.requestType"
style=" />
float: left; {{ title }}
margin-top: 8px; </div>
margin-right: 16px; <div v-if="requestVModel.isNew" class="ml-auto flex items-center gap-[16px]">
font-size: 14px; <div v-show="requestVModel.useEnv === 'false'" class="text-[14px] font-normal text-[var(--color-text-4)]">
font-weight: 400; {{ t('apiScenario.env', { name: props.envDetailItem?.name }) }}
line-height: 22px;
"
>{{ t('apiScenario.env', { name: props.envDetailItem?.name }) }}
</span>
<MsSelect
v-model:model-value="requestVModel.useEnv"
:style="{ width: '150px', float: 'right' }"
:allow-search="false"
:options="[
{ label: t('common.quote'), value: 'true' },
{ label: t('common.notQuote'), value: 'false' },
]"
:multiple="false"
value-key="value"
label-key="label"
:prefix="t('project.environmental.env')"
@change="handleUseEnvChange"
>
</MsSelect>
</div> </div>
<MsSelect
v-model:model-value="requestVModel.useEnv"
:allow-search="false"
:options="[
{ label: t('common.quote'), value: 'true' },
{ label: t('common.notQuote'), value: 'false' },
]"
:multiple="false"
value-key="value"
label-key="label"
:prefix="t('project.environmental.env')"
class="w-[150px]"
@change="handleUseEnvChange"
>
</MsSelect>
</div> </div>
</template> </template>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col"> <div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
@ -62,6 +48,7 @@
v-model:model-value="requestVModel.protocol" v-model:model-value="requestVModel.protocol"
:options="protocolOptions" :options="protocolOptions"
:loading="protocolLoading" :loading="protocolLoading"
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
class="w-[90px]" class="w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)" @change="(val) => handleActiveDebugProtocolChange(val as string)"
/> />
@ -73,10 +60,7 @@
is-tag is-tag
class="flex items-center" class="flex items-center"
/> />
<a-tooltip v-if="!isHttpProtocol" content="requestVModel.label" :mouse-enter-delay="500"> <a-tooltip v-if="!isHttpProtocol" :content="requestVModel.label" :mouse-enter-delay="500">
<div class="one-line-text max-w-[350px]"> requestVModel.label</div>
</a-tooltip>
<a-tooltip :content="requestVModel.label" :mouse-enter-delay="500">
<div class="one-line-text max-w-[350px]"> {{ requestVModel.label }}</div> <div class="one-line-text max-w-[350px]"> {{ requestVModel.label }}</div>
</a-tooltip> </a-tooltip>
</div> </div>
@ -84,6 +68,7 @@
<apiMethodSelect <apiMethodSelect
v-model:model-value="requestVModel.method" v-model:model-value="requestVModel.method"
class="w-[140px]" class="w-[140px]"
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<a-input <a-input
@ -93,6 +78,7 @@
allow-clear allow-clear
class="hover:z-10" class="hover:z-10"
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''" :style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
@input="() => (isUrlError = false)" @input="() => (isUrlError = false)"
@change="handleUrlChange" @change="handleUrlChange"
/> />
@ -100,25 +86,41 @@
</div> </div>
<div> <div>
<a-dropdown-button <a-dropdown-button
v-if="!requestVModel.executeLoading" v-if="hasLocalExec"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)" :disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
class="exec-btn" class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')" @click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute" @select="execute"
> >
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }} {{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template v-if="hasLocalExec" #icon> <template #icon>
<icon-down /> <icon-down />
</template> </template>
<template v-if="hasLocalExec" #content> <template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'"> <a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }} {{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption> </a-doption>
</template> </template>
</a-dropdown-button> </a-dropdown-button>
<a-button v-else-if="!requestVModel.executeLoading" type="primary" @click="() => execute('serverExec')">
{{ t('apiTestDebug.serverExec') }}
</a-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button> <a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
</div> </div>
</div> </div>
<a-input
v-if="
props.requestType &&
[ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(props.requestType) &&
isHttpProtocol
"
v-model:model-value="requestVModel.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
disabled
allow-clear
class="mt-[8px]"
/>
</div> </div>
<div class="px-[16px]"> <div class="px-[16px]">
<MsTab <MsTab
@ -234,6 +236,7 @@
</template> </template>
<template #second> <template #second>
<response <response
v-if="visible"
v-show="showResponse" v-show="showResponse"
v-model:active-layout="activeLayout" v-model:active-layout="activeLayout"
v-model:active-tab="requestVModel.responseActiveTab" v-model:active-tab="requestVModel.responseActiveTab"
@ -255,7 +258,16 @@
</MsSplitBox> </MsSplitBox>
</div> </div>
</div> </div>
<addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" /> <a-empty
v-if="pluginError && !isHttpProtocol"
:description="t('apiTestDebug.noPlugin')"
class="h-[200px] items-center justify-center"
>
<template #image>
<MsIcon type="icon-icon_plugin_outlined" size="48" />
</template>
</a-empty>
<!-- <addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" /> -->
</MsDrawer> </MsDrawer>
</template> </template>
@ -271,6 +283,7 @@
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue'; import assertion from '@/components/business/ms-assertion/index.vue';
import MsSelect from '@/components/business/ms-select'; import MsSelect from '@/components/business/ms-select';
import stepType from './stepType.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue'; import auth from '@/views/api-test/components/requestComposition/auth.vue';
@ -280,8 +293,8 @@
import setting from '@/views/api-test/components/requestComposition/setting.vue'; import setting from '@/views/api-test/components/requestComposition/setting.vue';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common'; import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
import { getDefinitionDetail } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript'; import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import { getGenerateId, parseQueryParams } from '@/utils'; import { getGenerateId, parseQueryParams } from '@/utils';
@ -294,7 +307,6 @@
PluginConfig, PluginConfig,
RequestTaskResult, RequestTaskResult,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { CustomApiStep } from '@/models/apiTest/scenario';
import { ModuleTreeNode, TransferFileParams } from '@/models/common'; import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { import {
RequestAuthType, RequestAuthType,
@ -303,6 +315,7 @@
RequestConditionProcessor, RequestConditionProcessor,
RequestMethods, RequestMethods,
ResponseComposition, ResponseComposition,
ScenarioStepType,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
import { import {
@ -316,23 +329,20 @@
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils'; import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
import type { Api } from '@form-create/arco-design'; import type { Api } from '@form-create/arco-design';
const visible = defineModel<boolean>('visible', { required: true });
// Http // Http
const httpHeader = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/header.vue')); const httpHeader = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/header.vue'));
const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue')); const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue'));
const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue')); const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue'));
const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue')); const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue'));
const addDependencyDrawer = defineAsyncComponent( // const addDependencyDrawer = defineAsyncComponent(
() => import('@/views/api-test/management/components/addDependencyDrawer.vue') // () => import('@/views/api-test/management/components/addDependencyDrawer.vue')
); // );
export interface RequestCustomAttr { export interface RequestCustomAttr {
type: 'api'; type: 'api';
isNew: boolean; isNew: boolean;
protocol: string; protocol: string;
activeTab: RequestComposition; activeTab: RequestComposition;
mode?: 'debug';
executeLoading: boolean; // loading executeLoading: boolean; // loading
isCopy?: boolean; // isCopy?: boolean; //
isExecute?: boolean; // isExecute?: boolean; //
@ -341,11 +351,14 @@
export type RequestParam = ExecuteApiRequestFullParams & { export type RequestParam = ExecuteApiRequestFullParams & {
response?: RequestTaskResult; response?: RequestTaskResult;
useEnv: string; useEnv: string;
request?: ExecuteApiRequestFullParams; //
} & RequestCustomAttr & } & RequestCustomAttr &
TabItem; TabItem;
const props = defineProps<{ const props = defineProps<{
request?: RequestParam; // request?: RequestParam; //
requestType?: ScenarioStepType;
stepName: string;
detailLoading?: boolean; // detailLoading?: boolean; //
envDetailItem?: { envDetailItem?: {
id?: string; id?: string;
@ -367,11 +380,13 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'addStep', request: RequestParam): void; (e: 'addStep', request: RequestParam): void;
(e: 'applyStep', request: RequestParam): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
const loading = defineModel<boolean>('detailLoading', { default: false }); const loading = defineModel<boolean>('detailLoading', { default: false });
const defaultDebugParams: RequestParam = { const defaultDebugParams: RequestParam = {
@ -438,7 +453,12 @@
}; };
const requestVModel = ref<RequestParam>(props.request || defaultDebugParams); const requestVModel = ref<RequestParam>(props.request || defaultDebugParams);
const title = computed(() => {
if (props.requestType && [ScenarioStepType.COPY_API, ScenarioStepType.QUOTE_API].includes(props.requestType)) {
return props.stepName;
}
return t('apiScenario.customApi');
});
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP'); const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const temporaryResponseMap = {}; // websockettab const temporaryResponseMap = {}; // websockettab
const isInitPluginForm = ref(false); const isInitPluginForm = ref(false);
@ -506,9 +526,7 @@
const contentTabList = computed(() => { const contentTabList = computed(() => {
// HTTP tabs // HTTP tabs
if (isHttpProtocol.value) { if (isHttpProtocol.value) {
return requestVModel.value.mode === 'debug' return httpContentTabList;
? httpContentTabList
: httpContentTabList.filter((e) => !commonContentTabKey.includes(e.value));
} }
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))]; return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
}); });
@ -568,7 +586,7 @@
const localExecuteUrl = ref(''); const localExecuteUrl = ref('');
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); // const pluginScriptMap = ref<Record<string, PluginConfig>>({}); //
const temporaryPluginFormMap: Record<string, any> = {}; // tab const temporaryPluginFormMap: Record<string, any> = {}; // API
const pluginLoading = ref(false); const pluginLoading = ref(false);
const fApi = ref<Api>(); const fApi = ref<Api>();
const currentPluginOptions = computed<Record<string, any>>( const currentPluginOptions = computed<Record<string, any>>(
@ -577,10 +595,19 @@
const currentPluginScript = computed<Record<string, any>[]>( const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || [] () => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
); );
const isCopyApiNeedInit = computed(
() => props.requestType === ScenarioStepType.COPY_API && props.request?.request === null
);
const isEditableApi = computed(
() => props.requestType === ScenarioStepType.COPY_API || props.requestType === ScenarioStepType.CUSTOM_API
);
// //
const handlePluginFormChange = debounce(() => { const handlePluginFormChange = debounce(() => {
temporaryPluginFormMap[requestVModel.value.id] = fApi.value?.formData(); if (isEditableApi.value) {
//
temporaryPluginFormMap[requestVModel.value.id] = fApi.value?.formData();
}
handleActiveDebugChange(); handleActiveDebugChange();
}, 300); }, 300);
@ -604,11 +631,11 @@
*/ */
function setPluginFormData() { function setPluginFormData() {
const tempForm = temporaryPluginFormMap[requestVModel.value.id]; const tempForm = temporaryPluginFormMap[requestVModel.value.id];
if (tempForm || !requestVModel.value.isNew || requestVModel.value.isCopy) { if (tempForm || !requestVModel.value.isNew) {
// //
const formData = tempForm || requestVModel.value; const formData = isEditableApi.value ? tempForm || requestVModel.value : requestVModel.value;
if (fApi.value) { if (fApi.value) {
fApi.value.nextTick(() => { fApi.value.nextRefresh(() => {
const form = {}; const form = {};
controlPluginFormFields().forEach((key) => { controlPluginFormFields().forEach((key) => {
form[key] = formData[key]; form[key] = formData[key];
@ -621,12 +648,8 @@
}); });
} }
} else { } else {
fApi.value?.nextTick(() => {
controlPluginFormFields();
});
nextTick(() => { nextTick(() => {
// form-create tab controlPluginFormFields();
fApi.value?.resetFields();
}); });
} }
} }
@ -720,6 +743,17 @@
const splitContainerRef = ref<HTMLElement>(); const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0); const secondBoxHeight = ref(0);
watch(
() => showResponse.value,
(val) => {
if (val) {
splitBoxSize.value = 0.6;
} else {
splitBoxSize.value = 1;
}
}
);
watch( watch(
() => splitBoxSize.value, () => splitBoxSize.value,
debounce((val) => { debounce((val) => {
@ -927,6 +961,7 @@
await props.localExecuteApi(localExecuteUrl.value, res); await props.localExecuteApi(localExecuteUrl.value, res);
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally { } finally {
requestVModel.value.executeLoading = false; requestVModel.value.executeLoading = false;
@ -964,8 +999,8 @@
} }
function handleContinue() { function handleContinue() {
requestVModel.value.isNew = false; //
emit('addStep', requestVModel.value); emit('addStep', requestVModel.value);
requestVModel.value = { ...defaultDebugParams };
} }
function handleSave() { function handleSave() {
@ -973,36 +1008,73 @@
visible.value = false; visible.value = false;
} }
const isUrlError = ref(false); function handleClose() {
const showAddDependencyDrawer = ref(false); // applyStep
const addDependencyMode = ref<'pre' | 'post'>('pre'); if (!requestVModel.value.isNew) {
emit('applyStep', requestVModel.value);
// watch(
// () => visible.value,
// async (val) => {
// if (val) {
// await initProtocolList();
// if (props.request) {
// requestVModel.value = { ...defaultDebugParams, ...props.request };
// handleActiveDebugProtocolChange(requestVModel.value.protocol);
// } else {
// requestVModel.value = { ...defaultDebugParams };
// }
// } else {
// requestVModel.value = { ...defaultDebugParams };
// }
// }
// );
onBeforeMount(() => {
initProtocolList();
if (props.request) {
requestVModel.value = { ...defaultDebugParams, ...props.request };
handleActiveDebugProtocolChange(requestVModel.value.protocol);
} else {
requestVModel.value = { ...defaultDebugParams };
} }
}); }
const isUrlError = ref(false);
// const showAddDependencyDrawer = ref(false);
// const addDependencyMode = ref<'pre' | 'post'>('pre');
async function initQuoteApiDetail() {
try {
loading.value = true;
const res = await getDefinitionDetail(requestVModel.value.id);
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
requestVModel.value = {
responseActiveTab: ResponseComposition.BODY,
executeLoading: false,
activeTab: res.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
unSaved: false,
isNew: false,
label: res.name,
...res.request,
...res,
response: cloneDeep(defaultResponse),
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
url: res.path,
name: res.name, // requestnamenull
id: res.id,
...parseRequestBodyResult,
};
nextTick(() => {
// loading
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
}
watch(
() => visible.value,
async (val) => {
if (val) {
if (props.request) {
requestVModel.value = { ...defaultDebugParams, ...props.request };
if (
props.requestType === ScenarioStepType.QUOTE_API ||
isCopyApiNeedInit.value
// (request.requestrequest null)
) {
initQuoteApiDetail();
}
}
await initProtocolList();
if (props.request) {
handleActiveDebugProtocolChange(requestVModel.value.protocol);
}
}
}
);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -233,9 +233,10 @@
*/ */
function handleSelectAllChange(v: SelectAllEnum) { function handleSelectAllChange(v: SelectAllEnum) {
if (v === SelectAllEnum.CURRENT) { if (v === SelectAllEnum.CURRENT) {
tableSelectedData.value = currentTable.value.propsRes.value.data; tableSelectedData.value.push(...currentTable.value.propsRes.value.data);
} else { } else {
tableSelectedData.value = []; const dataSet = new Set(currentTable.value.propsRes.value.data.map((e) => e.id));
tableSelectedData.value = tableSelectedData.value.filter((e) => !dataSet.has(e.id));
} }
emit('select', tableSelectedData.value); emit('select', tableSelectedData.value);
} }

View File

@ -112,7 +112,7 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(), stepId: getGenerateId(),
order: steps.value.length + 1, order: steps.value.length + 1,
type: ScenarioStepType.LOOP_CONTROL, type: ScenarioStepType.LOOP_CONTROL,
name: t('apiScenario.loopControl'), name: t('apiScenario.loopControl'),
@ -134,7 +134,7 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(), stepId: getGenerateId(),
order: steps.value.length + 1, order: steps.value.length + 1,
type: ScenarioStepType.CONDITION_CONTROL, type: ScenarioStepType.CONDITION_CONTROL,
name: t('apiScenario.conditionControl'), name: t('apiScenario.conditionControl'),
@ -156,7 +156,7 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(), stepId: getGenerateId(),
order: steps.value.length + 1, order: steps.value.length + 1,
type: ScenarioStepType.ONLY_ONCE_CONTROL, type: ScenarioStepType.ONLY_ONCE_CONTROL,
name: t('apiScenario.onlyOnceControl'), name: t('apiScenario.onlyOnceControl'),
@ -178,7 +178,7 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(), stepId: getGenerateId(),
order: steps.value.length + 1, order: steps.value.length + 1,
type: ScenarioStepType.WAIT_TIME, type: ScenarioStepType.WAIT_TIME,
name: t('apiScenario.waitTime'), name: t('apiScenario.waitTime'),
@ -189,7 +189,7 @@
case ScenarioAddStepActionType.CUSTOM_API: case ScenarioAddStepActionType.CUSTOM_API:
case ScenarioAddStepActionType.SCRIPT_OPERATION: case ScenarioAddStepActionType.SCRIPT_OPERATION:
if (step.value) { if (step.value) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.stepId, 'stepId');
if (realStep) { if (realStep) {
emit('otherCreate', val, realStep as ScenarioStepItem); emit('otherCreate', val, realStep as ScenarioStepItem);
} }

View File

@ -6,7 +6,7 @@
position="br" position="br"
@popup-visible-change="handleActionTriggerChange" @popup-visible-change="handleActionTriggerChange"
> >
<MsButton :id="step.id" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')"> <MsButton :id="step.stepId" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" /> <MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton> </MsButton>
<template #content> <template #content>
@ -131,7 +131,7 @@
function handleActionsClose() { function handleActionsClose() {
activeCreateAction.value = undefined; activeCreateAction.value = undefined;
innerStep.value.createActionsVisible = false; innerStep.value.createActionsVisible = false;
document.getElementById(innerStep.value.id.toString())?.click(); document.getElementById(innerStep.value.stepId.toString())?.click();
} }
</script> </script>

View File

@ -15,18 +15,18 @@ export default function useCreateActions() {
/** /**
* *
* @param selectedKeys id * @param selectedKeys stepId
* @param step * @param steps
* @param parent * @param parent
*/ */
function checkedIfNeed( function checkedIfNeed(
selectedKeys: (string | number)[], selectedKeys: (string | number)[],
step: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[], steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
parent?: TreeNode<ScenarioStepItem> parent?: TreeNode<ScenarioStepItem>
) { ) {
if (parent && selectedKeys.includes(parent.id)) { if (parent && selectedKeys.includes(parent.stepId)) {
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态) // 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
selectedKeys = selectedKeys.concat(step.map((item) => item.id)); selectedKeys.push(...steps.map((item) => item.stepId));
} }
} }
@ -36,7 +36,7 @@ export default function useCreateActions() {
* @param step * @param step
* @param steps * @param steps
* @param createStepAction * @param createStepAction
* @param selectedKeys id * @param selectedKeys stepId
*/ */
function handleCreateStep( function handleCreateStep(
defaultStepInfo: ScenarioStepItem, defaultStepInfo: ScenarioStepItem,
@ -48,7 +48,7 @@ export default function useCreateActions() {
const newStep = { const newStep = {
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
...defaultStepInfo, ...defaultStepInfo,
id: getGenerateId(), stepId: getGenerateId(),
}; };
switch (createStepAction) { switch (createStepAction) {
case 'inside': case 'inside':
@ -64,11 +64,11 @@ export default function useCreateActions() {
} }
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
step.parent?.children || steps, step.parent?.children || steps,
step.id, step.stepId,
newStep, newStep,
createStepAction, createStepAction,
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent), (newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
'id' 'stepId'
); );
} }
@ -81,7 +81,8 @@ export default function useCreateActions() {
function buildInsertStepInfos( function buildInsertStepInfos(
newSteps: Record<string, any>[], newSteps: Record<string, any>[],
type: ScenarioStepType, type: ScenarioStepType,
startOrder: number startOrder: number,
stepsDetailMap: Record<string, any>
): ScenarioStepItem[] { ): ScenarioStepItem[] {
let name: string; let name: string;
switch (type) { switch (type) {
@ -125,10 +126,12 @@ export default function useCreateActions() {
break; break;
} }
return newSteps.map((item, index) => { return newSteps.map((item, index) => {
const stepId = getGenerateId();
stepsDetailMap[stepId] = item; // 导入系统请求的引用接口和 case 的时候需要先存储一下引用的接口/用例信息
return { return {
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
...item, ...item,
id: getGenerateId(), stepId,
type, type,
name, name,
order: startOrder + index, order: startOrder + index,
@ -143,7 +146,7 @@ export default function useCreateActions() {
* @param steps * @param steps
* @param createStepAction * @param createStepAction
* @param type * @param type
* @param selectedKeys id * @param selectedKeys stepId
*/ */
function handleCreateSteps( function handleCreateSteps(
step: ScenarioStepItem, step: ScenarioStepItem,
@ -154,11 +157,11 @@ export default function useCreateActions() {
) { ) {
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
step.parent?.children || steps, step.parent?.children || steps,
step.id, step.stepId,
readyInsertSteps, readyInsertSteps,
createStepAction, createStepAction,
undefined, undefined,
'id' 'stepId'
); );
checkedIfNeed(selectedKeys, readyInsertSteps, step); checkedIfNeed(selectedKeys, readyInsertSteps, step);
} }

View File

@ -86,6 +86,7 @@
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
v-model:stepKeyword="keyword" v-model:stepKeyword="keyword"
:expand-all="isExpandAll" :expand-all="isExpandAll"
:steps-detail-map="stepInfo.stepsDetailMap"
/> />
</div> </div>
</div> </div>
@ -127,6 +128,7 @@
executeTime?: string; // executeTime?: string; //
executeSuccessCount?: number; // executeSuccessCount?: number; //
executeFailCount?: number; // executeFailCount?: number; //
stepsDetailMap: Record<string, any>; //
} }
const props = defineProps<{ const props = defineProps<{
@ -200,7 +202,7 @@
try { try {
let ids = checkedKeys.value; let ids = checkedKeys.value;
if (batchToggleRange.value === 'top') { if (batchToggleRange.value === 'top') {
ids = stepInfo.value.steps.map((item) => item.id); ids = stepInfo.value.steps.map((item) => item.stepId);
} }
console.log('ids', ids); console.log('ids', ids);
await new Promise((resolve) => { await new Promise((resolve) => {

View File

@ -23,7 +23,7 @@
</a-select> </a-select>
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal"> <a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
<a-input <a-input
:id="innerData.id" :id="innerData.stepId"
v-model:model-value="innerData.variableVal" v-model:model-value="innerData.variableVal"
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
@ -41,7 +41,7 @@
import { conditionOptions } from '@/views/api-test/scenario/components/config'; import { conditionOptions } from '@/views/api-test/scenario/components/config';
export interface ConditionContentProps { export interface ConditionContentProps {
id: string; stepId: string;
variableName: string; variableName: string;
condition: string; condition: string;
variableVal: string; variableVal: string;
@ -75,7 +75,7 @@
() => dbClick?.value.timeStamp, () => dbClick?.value.timeStamp,
() => { () => {
// @ts-ignore // @ts-ignore
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) { if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) {
emit('quickInput', 'variableVal'); emit('quickInput', 'variableVal');
} }
} }

View File

@ -87,7 +87,7 @@
</a-select> </a-select>
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal"> <a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
<a-input <a-input
:id="innerData.id" :id="innerData.stepId"
v-model:model-value="innerData.variableVal" v-model:model-value="innerData.variableVal"
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
@ -99,7 +99,7 @@
</template> </template>
<a-tooltip v-else :content="innerData.expression" :disabled="!innerData.expression"> <a-tooltip v-else :content="innerData.expression" :disabled="!innerData.expression">
<a-input <a-input
:id="innerData.id" :id="innerData.stepId"
v-model:model-value="innerData.expression" v-model:model-value="innerData.expression"
size="mini" size="mini"
class="w-[200px] px-[8px]" class="w-[200px] px-[8px]"
@ -159,7 +159,7 @@
import { conditionOptions } from '@/views/api-test/scenario/components/config'; import { conditionOptions } from '@/views/api-test/scenario/components/config';
export interface LoopContentProps { export interface LoopContentProps {
id: string | number; stepId: string | number;
num: number; num: number;
name: string; name: string;
type: ScenarioStepType; type: ScenarioStepType;
@ -227,7 +227,7 @@
() => dbClick?.value.timeStamp, () => dbClick?.value.timeStamp,
() => { () => {
// @ts-ignore // @ts-ignore
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) { if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) {
emit('quickInput', innerData.value.loopWhileType === 'condition' ? 'variableVal' : 'expression'); emit('quickInput', innerData.value.loopWhileType === 'condition' ? 'variableVal' : 'expression');
} }
} }

View File

@ -47,6 +47,7 @@
const props = defineProps<{ const props = defineProps<{
data: { data: {
id: string | number; id: string | number;
stepId: string | number;
belongProjectId: string; belongProjectId: string;
belongProjectName: string; belongProjectName: string;
num: number; num: number;

View File

@ -24,7 +24,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
export interface WaitTimeContentProps { export interface WaitTimeContentProps {
id: string | number; stepId: string | number;
waitTime: number; waitTime: number;
} }

View File

@ -11,7 +11,7 @@
:expand-all="props.expandAll" :expand-all="props.expandAll"
:node-more-actions="stepMoreActions" :node-more-actions="stepMoreActions"
:filter-more-action-func="setStepMoreAction" :filter-more-action-func="setStepMoreAction"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :field-names="{ title: 'name', key: 'stepId', children: 'children' }"
:virtual-list-props="{ :virtual-list-props="{
height: '100%', height: '100%',
threshold: 20, threshold: 20,
@ -88,7 +88,7 @@
<template v-if="checkStepIsApi(step)"> <template v-if="checkStepIsApi(step)">
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" /> <apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
<div <div
v-if="step.id === showStepNameEditInputStepId" v-if="step.stepId === showStepNameEditInputStepId"
class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]" class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
@click.stop @click.stop
> >
@ -117,7 +117,7 @@
<!-- 其他步骤描述 --> <!-- 其他步骤描述 -->
<template v-else> <template v-else>
<div <div
v-if="step.id === showStepDescEditInputStepId" v-if="step.stepId === showStepDescEditInputStepId"
class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]" class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
> >
<a-input <a-input
@ -161,7 +161,7 @@
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:steps="steps" v-model:steps="steps"
:step="step" :step="step"
@click="setFocusNodeKey(step.id)" @click="setFocusNodeKey(step.stepId)"
@other-create="handleOtherCreate" @other-create="handleOtherCreate"
@close="setFocusNodeKey('')" @close="setFocusNodeKey('')"
/> />
@ -187,11 +187,13 @@
</a-button> </a-button>
</createStepActions> </createStepActions>
<customApiDrawer <customApiDrawer
v-if="customApiDrawerVisible"
v-model:visible="customApiDrawerVisible" v-model:visible="customApiDrawerVisible"
:env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }" :env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }"
:request="activeStep?.request" :request="currentStepDetail"
:request-type="activeStep?.type"
:step-name="activeStep?.name || ''"
@add-step="addCustomApiStep" @add-step="addCustomApiStep"
@apply-step="applyApiStep"
/> />
<importApiDrawer <importApiDrawer
v-if="importApiDrawerVisible" v-if="importApiDrawerVisible"
@ -202,7 +204,7 @@
<scriptOperationDrawer <scriptOperationDrawer
v-if="scriptOperationDrawerVisible" v-if="scriptOperationDrawerVisible"
v-model:visible="scriptOperationDrawerVisible" v-model:visible="scriptOperationDrawerVisible"
:script="activeStep?.script" :script="currentStepDetail"
:name="activeStep?.name" :name="activeStep?.name"
@save="addScriptStep" @save="addScriptStep"
/> />
@ -285,7 +287,7 @@
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue')); const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
export interface ScenarioStepItem { export interface ScenarioStepItem {
id: string | number; stepId: string | number;
order: number; order: number;
enabled: boolean; // enabled: boolean; //
type: ScenarioStepType; type: ScenarioStepType;
@ -333,6 +335,10 @@
const checkedKeys = defineModel<(string | number)[]>('checkedKeys', { const checkedKeys = defineModel<(string | number)[]>('checkedKeys', {
required: true, required: true,
}); });
//
const stepsDetailMap = defineModel<Record<string, any>>('stepsDetailMap', {
required: true,
});
const selectedKeys = ref<(string | number)[]>([]); // const selectedKeys = ref<(string | number)[]>([]); //
const loading = ref(false); const loading = ref(false);
@ -391,9 +397,9 @@
* 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中 * 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中
*/ */
function checkedIfNeed(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.stepId)) {
// //
selectedKeys.value.push(step.id); selectedKeys.value.push(step.stepId);
} }
} }
@ -452,7 +458,7 @@
eventTag: 'copy', eventTag: 'copy',
}, },
{ {
label: 'apiScenario.saveAsCase', label: 'apiTestManagement.saveAsCase',
eventTag: 'saveAsCase', eventTag: 'saveAsCase',
}, },
{ {
@ -471,30 +477,30 @@
const id = getGenerateId(); const id = getGenerateId();
insertNodes<ScenarioStepItem>( insertNodes<ScenarioStepItem>(
steps.value, steps.value,
node.id, node.stepId,
{ {
...cloneDeep( ...cloneDeep(
mapTree<ScenarioStepItem>(node, (childNode) => { mapTree<ScenarioStepItem>(node, (childNode) => {
return { return {
...childNode, ...childNode,
id: getGenerateId(), // TODO: ID stepId: getGenerateId(), // TODO: ID
}; };
})[0] })[0]
), ),
name: `copy-${node.name}`, name: `copy-${node.name}`,
order: node.order + 1, order: node.order + 1,
id, stepId: id,
}, },
'after', 'after',
checkedIfNeed, checkedIfNeed,
'id' 'stepId'
); );
break; break;
case 'config': case 'config':
console.log('config', node); console.log('config', node);
break; break;
case 'delete': case 'delete':
deleteNode(steps.value, node.id, 'id'); deleteNode(steps.value, node.stepId, 'stepId');
break; break;
default: default:
break; break;
@ -512,7 +518,7 @@
const tempStepName = ref(''); const tempStepName = ref('');
function handleStepNameClick(step: ScenarioStepItem) { function handleStepNameClick(step: ScenarioStepItem) {
tempStepName.value = step.name; tempStepName.value = step.name;
showStepNameEditInputStepId.value = step.id; showStepNameEditInputStepId.value = step.stepId;
nextTick(() => { nextTick(() => {
// //
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement; const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
@ -521,7 +527,7 @@
} }
function applyStepNameChange(step: ScenarioStepItem) { function applyStepNameChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
if (realStep) { if (realStep) {
realStep.name = tempStepName.value; realStep.name = tempStepName.value;
} }
@ -535,7 +541,7 @@
const tempStepDesc = ref(''); const tempStepDesc = ref('');
function handleStepDescClick(step: ScenarioStepItem) { function handleStepDescClick(step: ScenarioStepItem) {
tempStepDesc.value = step.description; tempStepDesc.value = step.description;
showStepDescEditInputStepId.value = step.id; showStepDescEditInputStepId.value = step.stepId;
nextTick(() => { nextTick(() => {
// //
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement; const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
@ -544,7 +550,7 @@
} }
function applyStepDescChange(step: ScenarioStepItem) { function applyStepDescChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
if (realStep) { if (realStep) {
realStep.description = tempStepDesc.value; realStep.description = tempStepDesc.value;
} }
@ -552,7 +558,7 @@
} }
function handleStepContentChange($event, step: ScenarioStepItem) { function handleStepContentChange($event, step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
if (realStep) { if (realStep) {
Object.keys($event).forEach((key) => { Object.keys($event).forEach((key) => {
realStep[key] = $event[key]; realStep[key] = $event[key];
@ -564,7 +570,7 @@
* 处理步骤展开折叠 * 处理步骤展开折叠
*/ */
function handleStepExpand(data: MsTreeExpandedData) { function handleStepExpand(data: MsTreeExpandedData) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.stepId, 'stepId');
if (realStep) { if (realStep) {
realStep.expanded = !realStep.expanded; realStep.expanded = !realStep.expanded;
} }
@ -575,15 +581,27 @@
const scriptOperationDrawerVisible = ref(false); const scriptOperationDrawerVisible = ref(false);
const activeStep = ref<ScenarioStepItem>(); // const activeStep = ref<ScenarioStepItem>(); //
const activeCreateAction = ref<CreateStepAction>(); // const activeCreateAction = ref<CreateStepAction>(); //
const currentStepDetail = computed<any>(() => {
// TODO:
if (activeStep.value) {
return stepsDetailMap.value[activeStep.value.stepId];
}
return undefined;
});
/**
* 处理步骤选中事件
* @param _selectedKeys 选中的 key集合
* @param step 点击的步骤节点
*/
function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) { function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) {
const offspringIds: string[] = []; const offspringIds: string[] = [];
mapTree(step.children || [], (e) => { mapTree(step.children || [], (e) => {
offspringIds.push(e.id); offspringIds.push(e.stepId);
return e; return e;
}); });
selectedKeys.value = [step.id, ...offspringIds]; selectedKeys.value = [step.stepId, ...offspringIds];
if (step.type === ScenarioStepType.CUSTOM_API) { if ([ScenarioStepType.CUSTOM_API, ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(step.type)) {
activeStep.value = step; activeStep.value = step;
customApiDrawerVisible.value = true; customApiDrawerVisible.value = true;
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) { } else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
@ -651,17 +669,20 @@
const insertApiSteps = buildInsertStepInfos( const insertApiSteps = buildInsertStepInfos(
data.api, data.api,
type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API, type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API,
order order,
stepsDetailMap.value
); );
const insertCaseSteps = buildInsertStepInfos( const insertCaseSteps = buildInsertStepInfos(
data.case, data.case,
type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE, type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE,
order + insertApiSteps.length order + insertApiSteps.length,
stepsDetailMap.value
); );
const insertScenarioSteps = buildInsertStepInfos( const insertScenarioSteps = buildInsertStepInfos(
data.scenario, data.scenario,
type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO, type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO,
order + insertApiSteps.length + insertCaseSteps.length order + insertApiSteps.length + insertCaseSteps.length,
stepsDetailMap.value
); );
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps); const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
@ -675,12 +696,13 @@
* 添加自定义 API 步骤 * 添加自定义 API 步骤
*/ */
function addCustomApiStep(request: RequestParam) { function addCustomApiStep(request: RequestParam) {
const id = getGenerateId();
stepsDetailMap.value[id] = request;
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.CUSTOM_API, type: ScenarioStepType.CUSTOM_API,
name: t('apiScenario.customApi'), name: t('apiScenario.customApi'),
request: cloneDeep(request),
} as ScenarioStepItem, } as ScenarioStepItem,
activeStep.value, activeStep.value,
steps.value, steps.value,
@ -690,25 +712,35 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(), stepId: id,
order: steps.value.length + 1, order: steps.value.length + 1,
type: ScenarioStepType.CUSTOM_API, type: ScenarioStepType.CUSTOM_API,
name: t('apiScenario.customApi'), name: t('apiScenario.customApi'),
request: cloneDeep(request),
} as ScenarioStepItem); } as ScenarioStepItem);
} }
} }
/**
* API 详情抽屉关闭时应用更改
*/
function applyApiStep(request: RequestParam) {
if (activeStep.value) {
stepsDetailMap.value[activeStep.value?.stepId] = request;
activeStep.value = undefined;
}
}
/** /**
* 添加脚本操作步骤 * 添加脚本操作步骤
*/ */
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) { function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
const id = getGenerateId();
stepsDetailMap.value[id] = cloneDeep(scriptProcessor);
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
type: ScenarioStepType.SCRIPT_OPERATION, type: ScenarioStepType.SCRIPT_OPERATION,
name, name,
script: cloneDeep(scriptProcessor),
} as ScenarioStepItem, } as ScenarioStepItem,
activeStep.value, activeStep.value,
steps.value, steps.value,
@ -718,11 +750,10 @@
} else { } else {
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id: getGenerateId(), stepId: id,
order: steps.value.length + 1, order: steps.value.length + 1,
type: ScenarioStepType.SCRIPT_OPERATION, type: ScenarioStepType.SCRIPT_OPERATION,
name, name,
script: cloneDeep(scriptProcessor),
} as ScenarioStepItem); } as ScenarioStepItem);
} }
} }
@ -760,20 +791,20 @@
loading.value = true; loading.value = true;
const offspringIds: string[] = []; const offspringIds: string[] = [];
mapTree(dragNode.children || [], (e) => { mapTree(dragNode.children || [], (e) => {
offspringIds.push(e.id); offspringIds.push(e.stepId);
return e; return e;
}); });
const stepIdAndOffspringIds = [dragNode.id, ...offspringIds]; const stepIdAndOffspringIds = [dragNode.stepId, ...offspringIds];
if (dropPosition === 0) { if (dropPosition === 0) {
// //
if (selectedKeys.value.includes(dropNode.id)) { if (selectedKeys.value.includes(dropNode.stepId)) {
// //
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds); 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.stepId)) {
// //
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds); 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.stepId)) {
// //
selectedKeys.value = selectedKeys.value.filter((e) => { selectedKeys.value = selectedKeys.value.filter((e) => {
for (let i = 0; i < stepIdAndOffspringIds.length; i++) { for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
@ -786,7 +817,7 @@
return true; return true;
}); });
} }
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id'); const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'stepId');
if (dragResult) { if (dragResult) {
Message.success(t('common.moveSuccess')); Message.success(t('common.moveSuccess'));
} }
@ -805,7 +836,7 @@
const quickInputDataKey = ref(''); const quickInputDataKey = ref('');
function setQuickInput(step: ScenarioStepItem, dataKey: string) { function setQuickInput(step: ScenarioStepItem, dataKey: string) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
if (realStep) { if (realStep) {
activeStep.value = realStep as ScenarioStepItem; activeStep.value = realStep as ScenarioStepItem;
} }

View File

@ -125,6 +125,7 @@
executeTime: '', executeTime: '',
executeSuccessCount: 0, executeSuccessCount: 0,
executeFailCount: 0, executeFailCount: 0,
stepsDetailMap: {},
} as ScenarioStepInfo, } as ScenarioStepInfo,
status: RequestDefinitionStatus.PROCESSING, status: RequestDefinitionStatus.PROCESSING,
tags: [], tags: [],