fix(all): 修复中级 bug

This commit is contained in:
baiqi 2024-04-09 20:47:17 +08:00 committed by Craftsman
parent 9e04fb2a47
commit cbfb5e2221
47 changed files with 645 additions and 538 deletions

View File

@ -62,9 +62,10 @@ import {
ScenarioDetail, ScenarioDetail,
ScenarioHistoryItem, ScenarioHistoryItem,
ScenarioHistoryPageParams, ScenarioHistoryPageParams,
ScenarioStepResourceInfo,
} from '@/models/apiTest/scenario'; } from '@/models/apiTest/scenario';
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common'; import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules, TransferFileParams } from '@/models/common';
import { ApiScenarioStatus } from '@/enums/apiEnum'; import { ApiScenarioStatus, ScenarioStepType } from '@/enums/apiEnum';
import type { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import type { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import type { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue'; import type { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
@ -282,6 +283,6 @@ export function updateScenarioPro(id: string | number, priority: CaseLevel | und
} }
// 获取跨项目信息 // 获取跨项目信息
export function getStepProjectInfo(id: string | number) { export function getStepProjectInfo(id: string, type: ScenarioStepType) {
return MSR.get({ url: GetStepProjectInfoUrl, params: id }); return MSR.get<ScenarioStepResourceInfo>({ url: GetStepProjectInfoUrl, params: id, data: { resourceType: type } });
} }

View File

@ -19,7 +19,7 @@ export const GetSystemRequestUrl = '/api/scenario/get/system-request'; // 获取
export const FollowScenarioUrl = '/api/scenario/follow'; // 关注/取消关注接口场景 export const FollowScenarioUrl = '/api/scenario/follow'; // 关注/取消关注接口场景
export const ScenarioScheduleConfigUrl = '/api/scenario/schedule-config'; // 场景定时任务 export const ScenarioScheduleConfigUrl = '/api/scenario/schedule-config'; // 场景定时任务
export const ScenarioScheduleConfigDeleteUrl = '/api/scenario/schedule-config-delete'; // 场景定时任务 export const ScenarioScheduleConfigDeleteUrl = '/api/scenario/schedule-config-delete'; // 场景定时任务
export const GetStepProjectInfoUrl = '/api/scenario/step/project-ifo'; // 获取跨项目信息 export const GetStepProjectInfoUrl = '/api/scenario/step/resource-info'; // 获取跨项目信息
export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景 export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景
export const BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景 export const BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景
export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景 export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景

View File

@ -48,7 +48,7 @@
</a-form-item> </a-form-item>
<template v-else> <template v-else>
<div v-if="props.multiple" class="flex w-full items-center"> <div v-if="props.multiple" class="flex w-full items-center">
<dropdownMenu @link-file="associatedFile" @change="handleChange" /> <dropdownMenu :disabled="props.disabled" @link-file="associatedFile" @change="handleChange" />
<saveAsFilePopover <saveAsFilePopover
v-if="props.fileSaveAsSourceId" v-if="props.fileSaveAsSourceId"
v-model:visible="saveFilePopoverVisible" v-model:visible="saveFilePopoverVisible"
@ -85,7 +85,7 @@
:size="props.tagSize" :size="props.tagSize"
class="m-0 border-none p-0" class="m-0 border-none p-0"
:self-style="{ backgroundColor: 'transparent !important' }" :self-style="{ backgroundColor: 'transparent !important' }"
:closable="data.value !== '__arco__more'" :closable="data.value !== '__arco__more' || props.disabled"
@close="handleClose(data)" @close="handleClose(data)"
> >
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }} {{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}

View File

@ -170,7 +170,7 @@
class="mb-[16px] flex items-baseline gap-[16px] overflow-hidden bg-[var(--color-text-n9)] p-[5px_8px]" class="mb-[16px] flex items-baseline gap-[16px] overflow-hidden bg-[var(--color-text-n9)] p-[5px_8px]"
> >
<div class="break-all text-[var(--color-text-3)]">{{ t('ms.paramsInput.preview') }}</div> <div class="break-all text-[var(--color-text-3)]">{{ t('ms.paramsInput.preview') }}</div>
<a-spin :loading="previewLoading" class="flex flex-1 flex-wrap gap-[8px]"> <a-spin :loading="previewLoading" class="flex flex-1 flex-wrap items-baseline gap-[8px]">
<div class="param-preview">{{ paramPreview }}</div> <div class="param-preview">{{ paramPreview }}</div>
<MsButton type="text" @click="getMockValue">{{ t('ms.paramsInput.previewClick') }}</MsButton> <MsButton type="text" @click="getMockValue">{{ t('ms.paramsInput.previewClick') }}</MsButton>
</a-spin> </a-spin>
@ -510,7 +510,7 @@
} }
if (valueArr[1]) { if (valueArr[1]) {
// //
const functionRegex = /([a-zA-Z]+)(?:\(([^)]*)\))?/; const functionRegex = /([a-zA-Z0-9]+)(?:\(([^)]*)\))?/;
const functionMatch = valueArr[1].match(functionRegex); const functionMatch = valueArr[1].match(functionRegex);
if (functionMatch) { if (functionMatch) {
@ -654,6 +654,7 @@
padding: 4px 8px; padding: 4px 8px;
} }
} }
max-width: 400px; max-width: 400px;
} }
.ms-params-input-setting-trigger { .ms-params-input-setting-trigger {

View File

@ -7,7 +7,7 @@
v-model:expanded-keys="expandedKeys" v-model:expanded-keys="expandedKeys"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
:data="data" :data="filterTreeData"
class="ms-tree" class="ms-tree"
:allow-drop="handleAllowDrop" :allow-drop="handleAllowDrop"
@drag-start="onDragStart" @drag-start="onDragStart"
@ -197,13 +197,13 @@
init(true); init(true);
}); });
const originTreeData = ref<MsTreeNodeData[]>([]); // const filterTreeData = ref<MsTreeNodeData[]>([]); //
watch( watch(
() => data.value, () => data.value,
(val) => { (val) => {
if (!props.keyword) { if (!props.keyword) {
originTreeData.value = cloneDeep(val); filterTreeData.value = cloneDeep(val);
} }
}, },
{ {
@ -220,7 +220,7 @@
const search = (_data: MsTreeNodeData[]) => { const search = (_data: MsTreeNodeData[]) => {
const result: MsTreeNodeData[] = []; const result: MsTreeNodeData[] = [];
_data.forEach((item) => { _data.forEach((item) => {
if (item[props.fieldNames.title].toLowerCase().indexOf(keyword.toLowerCase()) > -1) { if (item[props.fieldNames.title].toLowerCase().includes(keyword.toLowerCase())) {
result.push({ ...item, expanded: true }); result.push({ ...item, expanded: true });
} else if (item[props.fieldNames.children]) { } else if (item[props.fieldNames.children]) {
const filterData = search(item[props.fieldNames.children]); const filterData = search(item[props.fieldNames.children]);
@ -237,13 +237,17 @@
return result; return result;
}; };
return search(originTreeData.value); return search(data.value);
} }
// //
const updateDebouncedSearch = debounce(() => { const updateDebouncedSearch = debounce(() => {
if (props.keyword) { if (props.keyword) {
data.value = searchData(props.keyword); filterTreeData.value = searchData(props.keyword);
nextTick(() => {
// (expandedKeys)
treeRef.value?.expandNode(expandedKeys.value);
});
} }
}, props.searchDebounce); }, props.searchDebounce);
@ -251,7 +255,7 @@
() => props.keyword, () => props.keyword,
(val) => { (val) => {
if (!val) { if (!val) {
data.value = cloneDeep(originTreeData.value); filterTreeData.value = cloneDeep(data.value);
} else { } else {
updateDebouncedSearch(); updateDebouncedSearch();
} }

View File

@ -270,6 +270,7 @@
contextmenu: !props.readOnly, // contextmenu: !props.readOnly, //
...props, ...props,
language: props.language.toLowerCase(), language: props.language.toLowerCase(),
theme: currentTheme.value,
}); });
// //

View File

@ -13,7 +13,7 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { XpathNode } from './types'; import { XpathNode } from './types';
import * as XmlBeautify from 'xml-beautify'; import XmlBeautify from 'xml-beautify';
const props = defineProps<{ const props = defineProps<{
xmlString: string; xmlString: string;
@ -98,7 +98,7 @@
isValidXml.value = true; isValidXml.value = true;
parsedXml.value = xmlDoc; parsedXml.value = xmlDoc;
// XML icon // XML icon
flattenedXml.value = new XmlBeautify({ parser: DOMParser }) flattenedXml.value = new XmlBeautify()
.beautify(props.xmlString) .beautify(props.xmlString)
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')

View File

@ -7,7 +7,7 @@
<a-table <a-table
v-bind="{ ...$attrs, ...scrollObj }" v-bind="{ ...$attrs, ...scrollObj }"
:row-class="getRowClass" :row-class="getRowClass"
:column-resizable="false" :column-resizable="true"
:span-method="spanMethod" :span-method="spanMethod"
:columns="currentColumns" :columns="currentColumns"
:expanded-keys="props.expandedKeys" :expanded-keys="props.expandedKeys"
@ -211,7 +211,7 @@
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 && selectedCount > 0" class="title text-[var(--color-text-2)]"> <span v-if="props.actionConfig && selectedCount > 0 && !showBatchAction" class="title text-[var(--color-text-2)]">
{{ t('msTable.batch.selected', { count: selectedCount }) }} {{ 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') }}

View File

@ -271,7 +271,7 @@ export interface ForEachController {
variable: string; // 变量名 variable: string; // 变量名
} }
export interface CountController { export interface CountController {
loops: number; // 循环次数 loops: string; // 循环次数
loopTime: number; // 循环间隔时间 loopTime: number; // 循环间隔时间
} }
export interface WhileScript { export interface WhileScript {
@ -478,3 +478,11 @@ export interface ApiScenarioBatchOptionResult {
success: number; success: number;
error: number; error: number;
} }
// 场景跨项目步骤资源信息
export interface ScenarioStepResourceInfo {
id: string;
num: number;
name: string;
projectId: string;
projectName: string;
}

View File

@ -545,17 +545,24 @@ export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string | number
* @param treeArr * @param treeArr
* @param targetKeys * @param targetKeys
*/ */
export function deleteNodes<T>(treeArr: TreeNode<T>[], targetKeys: (string | number)[], customKey = 'key'): void { export function deleteNodes<T>(
treeArr: TreeNode<T>[],
targetKeys: (string | number)[],
deleteCondition?: (node: TreeNode<T>, parent?: TreeNode<T>) => boolean,
customKey = 'key'
): void {
const targetKeysSet = new Set(targetKeys); const targetKeysSet = new Set(targetKeys);
function deleteNodesInTree(tree: TreeNode<T>[]): void { function deleteNodesInTree(tree: TreeNode<T>[]): void {
for (let i = tree.length - 1; i >= 0; i--) { for (let i = tree.length - 1; i >= 0; i--) {
const node = tree[i]; const node = tree[i];
if (targetKeysSet.has(node[customKey])) { if (targetKeysSet.has(node[customKey])) {
tree.splice(i, 1); // 直接删除当前节点 if (deleteCondition && deleteCondition(node, node.parent)) {
targetKeysSet.delete(node[customKey]); // 删除后从集合中移除 tree.splice(i, 1); // 直接删除当前节点
// 重新调整剩余子节点的 sort 序号 targetKeysSet.delete(node[customKey]); // 删除后从集合中移除
for (let j = i; j < tree.length; j++) { // 重新调整剩余子节点的 sort 序号
tree[j].sort = j + 1; for (let j = i; j < tree.length; j++) {
tree[j].sort = j + 1;
}
} }
} else if (Array.isArray(node.children)) { } else if (Array.isArray(node.children)) {
deleteNodesInTree(node.children); // 递归删除子节点 deleteNodesInTree(node.children); // 递归删除子节点
@ -634,12 +641,13 @@ export const getHashParameters = (): Record<string, string> => {
* @returns * @returns
*/ */
export const getGenerateId = () => { export const getGenerateId = () => {
const timestamp = new Date().getTime().toString(); return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const randomDigits = Math.floor(Math.random() * 10000) // eslint-disable-next-line no-bitwise
.toString() const r = (Math.random() * 16) | 0;
.padStart(4, '0'); // eslint-disable-next-line no-bitwise
const generateId = timestamp + randomDigits; const v = c === 'x' ? r : (r & 0x3) | 0x8;
return generateId.substring(0, 16); return v.toString(16);
});
}; };
/** /**

View File

@ -46,7 +46,12 @@
</div> </div>
<!-- 前后置请求结束 --> <!-- 前后置请求结束 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<a-radio-group v-model="condition.enableCommonScript" class="mb-[8px]" @change="emit('change')"> <a-radio-group
v-model="condition.enableCommonScript"
class="mb-[8px]"
:disabled="props.disabled"
@change="emit('change')"
>
<a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio> <a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio v-if="hasAnyPermission(['PROJECT_CUSTOM_FUNCTION:READ'])" :value="true"> <a-radio v-if="hasAnyPermission(['PROJECT_CUSTOM_FUNCTION:READ'])" :value="true">
{{ t('apiTestDebug.quote') }} {{ t('apiTestDebug.quote') }}
@ -58,6 +63,7 @@
class="mr-2" class="mr-2"
size="small" size="small"
type="line" type="line"
:disabled="props.disabled"
@change="emit('change')" @change="emit('change')"
/> />
{{ t('apiTestDebug.preconditionAssociatedSceneResult') }} {{ t('apiTestDebug.preconditionAssociatedSceneResult') }}
@ -79,6 +85,7 @@
v-model:model-value="condition.name" v-model:model-value="condition.name"
:placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')" :placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')"
:max-length="255" :max-length="255"
:disabled="props.disabled"
size="small" size="small"
@press-enter="isShowEditScriptNameInput = false" @press-enter="isShowEditScriptNameInput = false"
@blur="isShowEditScriptNameInput = false" @blur="isShowEditScriptNameInput = false"
@ -201,36 +208,39 @@
v-permission="['PROJECT_CUSTOM_FUNCTION:READ']" v-permission="['PROJECT_CUSTOM_FUNCTION:READ']"
type="text" type="text"
class="font-medium" class="font-medium"
:disabled="props.disabled"
@click="showQuoteDrawer = true" @click="showQuoteDrawer = true"
> >
{{ t('apiTestDebug.quote') }} {{ t('apiTestDebug.quote') }}
</MsButton> </MsButton>
</div> </div>
<div v-show="showParameters() || showScript()" class="h-[calc(100%-47px)] min-h-[300px]"> <div v-if="showParameters() || showScript()" class="min-h-[300px]">
<a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit"> <div>
<a-radio v-if="showParameters()" value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio> <a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit">
<a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio> <a-radio v-if="showParameters()" value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio>
</a-radio-group> <a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio>
<paramTable </a-radio-group>
v-if="commonScriptShowType === 'parameters'" </div>
v-model:params="scriptParams" <div class="h-[calc(100%-76px)]">
:scroll="{ x: '100%' }" <paramTable
:columns="scriptColumns" v-if="commonScriptShowType === 'parameters'"
:height-used="heightUsed" v-model:params="scriptParams"
:selectable="false" :disabled-param-value="props.disabled"
/> :scroll="{ x: '100%' }"
<div v-show="commonScriptShowType === 'scriptContent'" class="h-[calc(100%-76px)]"> :columns="scriptColumns"
:height-used="heightUsed"
:selectable="false"
/>
<MsCodeEditor <MsCodeEditor
v-if="condition.commonScriptInfo" v-else-if="commonScriptShowType === 'scriptContent' && condition.commonScriptInfo"
v-model:model-value="condition.commonScriptInfo.script" v-model:model-value="condition.commonScriptInfo.script"
theme="vs" theme="vs"
height="100%" :height="props.sqlCodeEditorHeight || '100%'"
:language="condition.commonScriptInfo.scriptLanguage || LanguageEnum.BEANSHELL_JSR233" :language="condition.commonScriptInfo.scriptLanguage || LanguageEnum.BEANSHELL_JSR233"
:show-full-screen="false" :show-full-screen="false"
:show-theme-change="false" :show-theme-change="false"
read-only read-only
> />
</MsCodeEditor>
</div> </div>
</div> </div>
</div> </div>
@ -289,7 +299,9 @@
v-model:model-value="condition.variableNames" v-model:model-value="condition.variableNames"
:max-length="255" :max-length="255"
:disabled="props.disabled" :disabled="props.disabled"
:placeholder="t('apiTestDebug.storageByColPlaceholder', { a: 'id', b: 'email', c: '{id_1}', d: '{email_1}' })" :placeholder="
t('apiTestDebug.storageByColPlaceholder', { a: 'id', b: 'email', c: '${id_1}', d: '${email_1}' })
"
@input="() => emit('change')" @input="() => emit('change')"
/> />
</div> </div>
@ -350,6 +362,7 @@
ref="extractParamsTableRef" ref="extractParamsTableRef"
:params="condition.extractors" :params="condition.extractors"
:disabled-except-param="props.disabled" :disabled-except-param="props.disabled"
:disabled-param-value="props.disabled"
:default-param-item="defaultExtractParamItem" :default-param-item="defaultExtractParamItem"
:columns="extractParamsColumns" :columns="extractParamsColumns"
:selectable="false" :selectable="false"
@ -411,7 +424,7 @@
> >
<template #content> <template #content>
<moreSetting v-model:config="activeRecord" is-popover class="mt-[12px]" /> <moreSetting v-model:config="activeRecord" is-popover class="mt-[12px]" />
<div class="flex items-center justify-end gap-[8px]"> <div v-show="!props.disabled" class="flex items-center justify-end gap-[8px]">
<a-button type="secondary" size="mini" @click="record.moreSettingPopoverVisible = false"> <a-button type="secondary" size="mini" @click="record.moreSettingPopoverVisible = false">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</a-button> </a-button>
@ -426,7 +439,11 @@
</paramTable> </paramTable>
</div> </div>
</div> </div>
<quoteSqlSourceDrawer v-model:visible="quoteSqlSourceDrawerVisible" @apply="handleQuoteSqlSourceApply" /> <quoteSqlSourceDrawer
v-model:visible="quoteSqlSourceDrawerVisible"
:selected-key="condition.dataSourceId"
@apply="handleQuoteSqlSourceApply"
/>
<fastExtraction <fastExtraction
v-model:visible="fastExtractionVisible" v-model:visible="fastExtractionVisible"
:response="props.response" :response="props.response"
@ -512,6 +529,7 @@
requestRadioTextProps?: Record<string, any>; // requestRadioTextProps?: Record<string, any>; //
showPrePostRequest?: boolean; // showPrePostRequest?: boolean; //
totalList?: ExecuteConditionProcessor[]; // totalList?: ExecuteConditionProcessor[]; //
sqlCodeEditorHeight?: string; // sql
}>(), }>(),
{ {
showAssociatedScene: false, showAssociatedScene: false,
@ -530,27 +548,34 @@
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig'); const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const condition = useVModel(props, 'data', emit); const condition = useVModel(props, 'data', emit);
watchEffect(() => { watch(
if (condition.value.processorType === RequestConditionProcessor.SQL && condition.value.dataSourceId) { () => currentEnvConfig?.value,
// SQL () => {
const dataSourceItem = currentEnvConfig?.value.dataSources.find( if (condition.value.processorType === RequestConditionProcessor.SQL && condition.value.dataSourceId) {
(item) => item.dataSource === condition.value.dataSourceName // SQL
); const dataSourceItem = currentEnvConfig?.value.dataSources.find(
if (dataSourceItem) { (item) => item.dataSource === condition.value.dataSourceName
// );
condition.value.dataSourceName = dataSourceItem.dataSource; if (currentEnvConfig?.value.dataSources.length === 0) {
condition.value.dataSourceId = dataSourceItem.id; //
} else if (currentEnvConfig && currentEnvConfig.value.dataSources.length > 0) { condition.value.dataSourceName = '';
// condition.value.dataSourceId = '';
condition.value.dataSourceName = currentEnvConfig.value.dataSources[0].dataSource; } else if (dataSourceItem) {
condition.value.dataSourceId = currentEnvConfig.value.dataSources[0].id; //
} else { condition.value.dataSourceName = dataSourceItem.dataSource;
// condition.value.dataSourceId = dataSourceItem.id;
condition.value.dataSourceName = ''; } else if (currentEnvConfig && currentEnvConfig.value.dataSources.length > 0) {
condition.value.dataSourceId = ''; //
condition.value.dataSourceName = currentEnvConfig.value.dataSources[0].dataSource;
condition.value.dataSourceId = currentEnvConfig.value.dataSources[0].id;
}
} }
},
{
immediate: true,
deep: true,
} }
}); );
// //
const isShowEditScriptNameInput = ref(false); const isShowEditScriptNameInput = ref(false);

View File

@ -38,6 +38,7 @@
:show-associated-scene="props.showAssociatedScene" :show-associated-scene="props.showAssociatedScene"
:show-pre-post-request="props.showPrePostRequest" :show-pre-post-request="props.showPrePostRequest"
:request-radio-text-props="props.requestRadioTextProps" :request-radio-text-props="props.requestRadioTextProps"
:sql-code-editor-height="props.sqlCodeEditorHeight"
@copy="copyListItem" @copy="copyListItem"
@delete="deleteListItem" @delete="deleteListItem"
@change="emit('change')" @change="emit('change')"
@ -55,8 +56,8 @@
import { conditionTypeNameMap } from '@/config/apiTest'; import { conditionTypeNameMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { ConditionType, ExecuteConditionProcessor } from '@/models/apiTest/common'; import { ConditionType, ExecuteConditionProcessor, RegexExtract } from '@/models/apiTest/common';
import { RequestConditionProcessor } from '@/enums/apiEnum'; import { RequestConditionProcessor, RequestExtractExpressionEnum, RequestExtractScope } from '@/enums/apiEnum';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -69,6 +70,7 @@
response?: string; // response?: string; //
showAssociatedScene?: boolean; showAssociatedScene?: boolean;
showPrePostRequest?: boolean; // showPrePostRequest?: boolean; //
sqlCodeEditorHeight?: string;
}>(), }>(),
{ {
showAssociatedScene: false, showAssociatedScene: false,
@ -201,7 +203,6 @@
if (isEXTRACT) { if (isEXTRACT) {
return; return;
} }
data.value.push({ data.value.push({
id, id,
processorType: RequestConditionProcessor.EXTRACT, processorType: RequestConditionProcessor.EXTRACT,
@ -228,6 +229,11 @@
hasNoIdItem = true; hasNoIdItem = true;
return { return {
...item, ...item,
extractors: item.extractors?.map((e, j) => ({
...e,
extractScope: (e as RegexExtract).extractScope || RequestExtractScope.BODY,
id: new Date().getTime() + j,
})),
id: new Date().getTime() + i, id: new Date().getTime() + i,
}; };
} }

View File

@ -89,10 +89,10 @@
(val) => { (val) => {
if (!val) { if (!val) {
currentEnv.value = (envOptions.value[0]?.value as string) || ''; currentEnv.value = (envOptions.value[0]?.value as string) || '';
nextTick(() => {
initEnvironment();
});
} }
nextTick(() => {
initEnvironment();
});
} }
); );

View File

@ -26,7 +26,7 @@
<template #typeTitle="{ columnConfig }"> <template #typeTitle="{ columnConfig }">
<div class="flex items-center text-[var(--color-text-3)]"> <div class="flex items-center text-[var(--color-text-3)]">
{{ t('apiTestDebug.paramType') }} {{ t('apiTestDebug.paramType') }}
<a-tooltip :content="columnConfig.typeTitleTooltip" position="right"> <a-tooltip :content="columnConfig.typeTitleTooltip" :disabled="!columnConfig.typeTitleTooltip" position="right">
<icon-question-circle <icon-question-circle
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]" class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16" size="16"
@ -59,7 +59,7 @@
<template #extractValueTitle> <template #extractValueTitle>
<div class="flex items-center text-[var(--color-text-3)]"> <div class="flex items-center text-[var(--color-text-3)]">
{{ t('apiTestDebug.extractValueByColumn') }} {{ t('apiTestDebug.extractValueByColumn') }}
<a-tooltip :content="t('caseManagement.caseReview.passRateTip')" position="right"> <a-tooltip :content="t('apiTestDebug.extractValueTitleTip')" position="right">
<icon-question-circle <icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16" size="16"
@ -211,7 +211,7 @@
<MsAddAttachment <MsAddAttachment
v-else-if="record.paramType === RequestParamsType.FILE" v-else-if="record.paramType === RequestParamsType.FILE"
v-model:file-list="record.files" v-model:file-list="record.files"
:disabled="props.disabledExceptParam" :disabled="props.disabledParamValue"
mode="input" mode="input"
:multiple="true" :multiple="true"
:fields="{ :fields="{
@ -819,7 +819,6 @@
() => props.params, () => props.params,
(arr) => { (arr) => {
if (arr.length > 0) { if (arr.length > 0) {
let hasNoIdItem = false; // id
paramsData.value = arr.map((item, i) => { paramsData.value = arr.map((item, i) => {
if (!item) { if (!item) {
// undefined // undefined
@ -830,7 +829,6 @@
} }
if (!item.id) { if (!item.id) {
// id // id
hasNoIdItem = true;
return { return {
...item, ...item,
id: new Date().getTime() + i, id: new Date().getTime() + i,
@ -838,7 +836,11 @@
} }
return item; return item;
}); });
if (hasNoIdItem && !filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault && !props.isTreeTable) { if (
!filterKeyValParams(arr, props.defaultParamItem).lastDataIsDefault &&
!props.isTreeTable &&
!filterKeyValParams(arr, arr[arr.length - 2]).lastDataIsDefault //
) {
addTableLine(arr.length - 1, false, true); addTableLine(arr.length - 1, false, true);
} }
} else { } else {
@ -1033,7 +1035,7 @@
*/ */
function handleSearchParams(val: string, item: FormTableColumn) { function handleSearchParams(val: string, item: FormTableColumn) {
item.autoCompleteParams = item.autoCompleteParams?.map((e) => { item.autoCompleteParams = item.autoCompleteParams?.map((e) => {
e.isShow = (e.label || '').includes(val); e.isShow = (e.label || '').toLowerCase().includes(val.toLowerCase());
return e; return e;
}); });
} }

View File

@ -6,6 +6,7 @@
:response="props.response" :response="props.response"
:disabled="props.disabled" :disabled="props.disabled"
:height-used="heightUsed" :height-used="heightUsed"
:sql-code-editor-height="props.sqlCodeEditorHeight"
@change="emit('change')" @change="emit('change')"
> >
<template v-if="props.isDefinition" #titleRight> <template v-if="props.isDefinition" #titleRight>
@ -44,6 +45,7 @@
isDefinition?: boolean; // isDefinition?: boolean; //
isScenario?: boolean; // isScenario?: boolean; //
disabled?: boolean; disabled?: boolean;
sqlCodeEditorHeight?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', params: ExecuteConditionProcessor[]): void; (e: 'update:params', params: ExecuteConditionProcessor[]): void;

View File

@ -3,6 +3,7 @@
v-model:list="innerConfig.processors" v-model:list="innerConfig.processors"
:disabled="props.disabled" :disabled="props.disabled"
:condition-types="conditionTypes" :condition-types="conditionTypes"
:sql-code-editor-height="props.sqlCodeEditorHeight"
add-text="apiTestDebug.precondition" add-text="apiTestDebug.precondition"
@change="emit('change')" @change="emit('change')"
> >
@ -39,6 +40,7 @@
isDefinition?: boolean; // isDefinition?: boolean; //
isScenario?: boolean; // isScenario?: boolean; //
disabled?: boolean; disabled?: boolean;
sqlCodeEditorHeight?: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:config', params: ExecuteConditionConfig): void; (e: 'update:config', params: ExecuteConditionConfig): void;

View File

@ -202,4 +202,6 @@ export default {
'apiTestDebug.testSuccess': 'Test success', 'apiTestDebug.testSuccess': 'Test success',
'apiTestDebug.searchByDataBaseName': 'Search by data source name', 'apiTestDebug.searchByDataBaseName': 'Search by data source name',
'apiTestDebug.regexMatchRules': 'Expression matching rules', 'apiTestDebug.regexMatchRules': 'Expression matching rules',
'apiTestDebug.extractValueTitleTip':
'Enter the column name and corresponding value in column storage. If you want to extract the first value of the name column, enter name_1',
}; };

View File

@ -189,4 +189,5 @@ export default {
'apiTestDebug.testSuccess': '测试成功', 'apiTestDebug.testSuccess': '测试成功',
'apiTestDebug.searchByDataBaseName': '按数据源名称搜索', 'apiTestDebug.searchByDataBaseName': '按数据源名称搜索',
'apiTestDebug.regexMatchRules': '表达式匹配规则', 'apiTestDebug.regexMatchRules': '表达式匹配规则',
'apiTestDebug.extractValueTitleTip': '输入按列存储中的列名和对应的数值如提取name列的第一个值则输入name_1',
}; };

View File

@ -188,7 +188,7 @@
v-model:model-value="importForm.name" v-model:model-value="importForm.name"
:placeholder="t('apiTestManagement.taskNamePlaceholder')" :placeholder="t('apiTestManagement.taskNamePlaceholder')"
:max-length="255" :max-length="255"
class="flex-1" class="w-[550px]"
></a-input> ></a-input>
<MsButton type="text" @click="taskDrawerVisible = true"> <MsButton type="text" @click="taskDrawerVisible = true">
{{ t('apiTestManagement.timeTaskList') }} {{ t('apiTestManagement.timeTaskList') }}
@ -204,7 +204,7 @@
<a-input <a-input
v-model:model-value="importForm.swaggerUrl" v-model:model-value="importForm.swaggerUrl"
:placeholder="t('apiTestManagement.urlImportPlaceholder')" :placeholder="t('apiTestManagement.urlImportPlaceholder')"
class="w-[700px]" class="w-[550px]"
allow-clear allow-clear
></a-input> ></a-input>
</a-form-item> </a-form-item>
@ -246,7 +246,7 @@
<a-tree-select <a-tree-select
v-model:modelValue="importForm.moduleId" v-model:modelValue="importForm.moduleId"
:data="moduleTree" :data="moduleTree"
class="w-[436px]" class="w-[500px]"
:field-names="{ title: 'name', key: 'id', children: 'children' }" :field-names="{ title: 'name', key: 'id', children: 'children' }"
allow-search allow-search
> >
@ -370,6 +370,7 @@
visible: boolean; visible: boolean;
moduleTree: ModuleTreeNode[]; moduleTree: ModuleTreeNode[];
popupContainer?: string; popupContainer?: string;
activeModule: string;
}>(); }>();
const emit = defineEmits(['update:visible', 'done']); const emit = defineEmits(['update:visible', 'done']);
@ -391,8 +392,8 @@
const defaultForm: ImportApiDefinitionRequest = { const defaultForm: ImportApiDefinitionRequest = {
platform: RequestImportFormat.SWAGGER, platform: RequestImportFormat.SWAGGER,
name: '', name: '',
moduleId: '', moduleId: 'root',
coverData: true, coverData: false,
syncCase: true, syncCase: true,
coverModule: false, coverModule: false,
swaggerUrl: '', swaggerUrl: '',
@ -406,6 +407,19 @@
}; };
const importForm = ref({ ...defaultForm }); const importForm = ref({ ...defaultForm });
const importFormRef = ref<FormInstance>(); const importFormRef = ref<FormInstance>();
watch(
() => visible.value,
(val) => {
if (val) {
importForm.value.moduleId = props.activeModule !== 'all' ? props.activeModule : 'root';
}
},
{
immediate: true,
}
);
const moreSettingActive = ref<number[]>([]); const moreSettingActive = ref<number[]>([]);
const disabledConfirm = computed(() => { const disabledConfirm = computed(() => {
if (importForm.value.type === RequestImportType.API) { if (importForm.value.type === RequestImportType.API) {

View File

@ -258,14 +258,18 @@
style="padding-top: 10px" style="padding-top: 10px"
> >
<a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" /> <a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" />
<a-tooltip :content="t('caseManagement.featureCase.enableTags')"> <span class="flex items-center">
<span class="flex items-center"> <span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span> <span class="mt-[2px]">
<span class="mt-[2px]"> <a-tooltip>
<IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" /> <IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" />
</span> <template #content>
<div>{{ t('caseManagement.featureCase.enableTags') }}</div>
<div>{{ t('caseManagement.featureCase.closeTags') }}</div>
</template>
</a-tooltip>
</span> </span>
</a-tooltip> </span>
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch"> <a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch">

View File

@ -321,14 +321,18 @@
style="padding-top: 10px" style="padding-top: 10px"
> >
<a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" /> <a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" />
<a-tooltip :content="t('caseManagement.featureCase.enableTags')"> <span class="flex items-center">
<span class="flex items-center"> <span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span> <span class="mt-[2px]">
<span class="mt-[2px]"> <a-tooltip>
<IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" /> <IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" />
</span> <template #content>
<div>{{ t('caseManagement.featureCase.enableTags') }}</div>
<div>{{ t('caseManagement.featureCase.closeTags') }}</div>
</template>
</a-tooltip>
</span> </span>
</a-tooltip> </span>
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<a-button type="secondary" :disabled="batchEditLoading" @click="cancelBatchEdit"> <a-button type="secondary" :disabled="batchEditLoading" @click="cancelBatchEdit">

View File

@ -28,7 +28,7 @@
</a-tooltip> </a-tooltip>
</template> </template>
</MsEditableTab> </MsEditableTab>
<environmentSelect ref="environmentSelectRef" /> <environmentSelect ref="environmentSelectRef" v-model:current-env="activeApiTab.environmentId" />
</div> </div>
<api <api
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.type === 'api'" v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.type === 'api'"

View File

@ -43,6 +43,7 @@
<importApi <importApi
v-model:visible="importDrawerVisible" v-model:visible="importDrawerVisible"
:module-tree="folderTree" :module-tree="folderTree"
:active-module="activeModule"
popup-container="#managementContainer" popup-container="#managementContainer"
@done="handleImportDone" @done="handleImportDone"
/> />

View File

@ -43,9 +43,11 @@
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST" v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]" class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]"
> >
<div class="text-[14px] font-normal text-[var(--color-text-4)]"> <a-tooltip :content="currentEnvConfig?.name" :disabled="!currentEnvConfig?.name">
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }} <div class="one-line-text max-w-[250px] text-[14px] font-normal text-[var(--color-text-4)]">
</div> {{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
</div>
</a-tooltip>
<a-select <a-select
v-model:model-value="requestVModel.customizeRequestEnvEnable" v-model:model-value="requestVModel.customizeRequestEnvEnable"
class="w-[150px]" class="w-[150px]"
@ -119,27 +121,39 @@
</a-input-group> </a-input-group>
</div> </div>
<div v-permission="[props.permissionMap?.execute]"> <div v-permission="[props.permissionMap?.execute]">
<a-dropdown-button <template v-if="hasLocalExec">
v-if="hasLocalExec" <a-dropdown-button
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)" v-if="!requestVModel.executeLoading"
class="exec-btn" :disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')" class="exec-btn"
@select="execute" @click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">
{{ t('common.stop') }}
</a-button>
</template>
<a-button
v-else-if="!requestVModel.executeLoading"
class="mr-[12px]"
type="primary"
@click="() => execute('serverExec')"
> >
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else-if="!requestVModel.executeLoading" type="primary" @click="() => execute('serverExec')">
{{ t('apiTestDebug.serverExec') }} {{ t('apiTestDebug.serverExec') }}
</a-button> </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 <a-input
@ -201,7 +215,7 @@
<httpHeader <httpHeader
v-if="requestVModel.activeTab === RequestComposition.HEADER" v-if="requestVModel.activeTab === RequestComposition.HEADER"
v-model:params="requestVModel.headers" v-model:params="requestVModel.headers"
:disabled-param-value="!isEditableApi" :disabled-param-value="!isEditableApi && !isEditableParamValue"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:layout="activeLayout" :layout="activeLayout"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@ -211,7 +225,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.BODY" v-else-if="requestVModel.activeTab === RequestComposition.BODY"
v-model:params="requestVModel.body" v-model:params="requestVModel.body"
:layout="activeLayout" :layout="activeLayout"
:disabled-param-value="!isEditableApi" :disabled-param-value="!isEditableApi && !isEditableParamValue"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
:upload-temp-file-api="uploadTempFile" :upload-temp-file-api="uploadTempFile"
@ -224,7 +238,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.QUERY" v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
v-model:params="requestVModel.query" v-model:params="requestVModel.query"
:layout="activeLayout" :layout="activeLayout"
:disabled-param-value="!isEditableApi" :disabled-param-value="!isEditableApi && !isEditableParamValue"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
@ -233,7 +247,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.REST" v-else-if="requestVModel.activeTab === RequestComposition.REST"
v-model:params="requestVModel.rest" v-model:params="requestVModel.rest"
:layout="activeLayout" :layout="activeLayout"
:disabled-param-value="!isEditableApi" :disabled-param-value="!isEditableApi && !isEditableParamValue"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
@ -554,11 +568,15 @@
// api props.request // api props.request
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined); const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined);
//
const isEditableApi = computed( const isEditableApi = computed(
() => () =>
!props.step?.isQuoteScenarioStep && !props.step?.isQuoteScenarioStep &&
(_stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step) (_stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step)
); );
// api
const isEditableParamValue = computed(() => !props.step?.isQuoteScenarioStep && _stepType.value.isQuoteApi);
// HTTP
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP'); const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const isInitPluginForm = ref(false); const isInitPluginForm = ref(false);
@ -999,7 +1017,7 @@
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol, method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
name: requestVModel.value.name, name: requestVModel.value.name,
unSaved: requestVModel.value.unSaved, unSaved: requestVModel.value.unSaved,
customizeRequest: props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.request, customizeRequest: requestVModel.value.customizeRequest,
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable, customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
children: [ children: [
{ {

View File

@ -67,27 +67,39 @@
allow-clear allow-clear
/> />
<div v-permission="[props.permissionMap?.execute]"> <div v-permission="[props.permissionMap?.execute]">
<a-dropdown-button <template v-if="hasLocalExec">
v-if="hasLocalExec" <a-dropdown-button
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)" v-if="!requestVModel.executeLoading"
class="exec-btn" :disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')" class="exec-btn"
@select="execute" @click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">
{{ t('common.stop') }}
</a-button>
</template>
<a-button
v-else-if="!requestVModel.executeLoading"
class="mr-[12px]"
type="primary"
@click="() => execute('serverExec')"
> >
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else-if="!requestVModel.executeLoading" type="primary" @click="() => execute('serverExec')">
{{ t('apiTestDebug.serverExec') }} {{ t('apiTestDebug.serverExec') }}
</a-button> </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>
<div class="px-[16px]"> <div class="px-[16px]">
@ -443,8 +455,8 @@
} }
); );
// api props.request // case
const isEditableApi = computed(() => _stepType.value.isCopyCase); const isEditableApi = computed(() => !activeStep.value?.isQuoteScenarioStep && _stepType.value.isCopyCase);
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP'); const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const isInitPluginForm = ref(false); const isInitPluginForm = ref(false);
const loading = ref(false); const loading = ref(false);

View File

@ -255,6 +255,7 @@
fullScenarioArr.push(...res); fullScenarioArr.push(...res);
}); });
if (refType === ScenarioStepRefType.COPY) { if (refType === ScenarioStepRefType.COPY) {
// uniqueIdcopyFromStepId
fullScenarioArr = mapTree<MsTableDataItem<ApiScenarioTableItem>>(fullScenarioArr, (node) => { fullScenarioArr = mapTree<MsTableDataItem<ApiScenarioTableItem>>(fullScenarioArr, (node) => {
const id = getGenerateId(); const id = getGenerateId();
return { return {
@ -275,6 +276,7 @@
); );
handleCancel(); handleCancel();
} else { } else {
// iduniqueId id
fullScenarioArr = fullScenarioArr.map((e) => { fullScenarioArr = fullScenarioArr.map((e) => {
const id = getGenerateId(); const id = getGenerateId();
return { return {

View File

@ -79,6 +79,9 @@
<template #status="{ record }"> <template #status="{ record }">
<apiStatus :status="record.status" size="small" /> <apiStatus :status="record.status" size="small" />
</template> </template>
<template #priority="{ record }">
<caseLevel :case-level="record.priority" />
</template>
</ms-base-table> </ms-base-table>
</div> </div>
</template> </template>
@ -90,6 +93,7 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableDataItem } from '@/components/pure/ms-table/type'; import { MsTableDataItem } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
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';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -475,7 +479,7 @@
case 'scenario': case 'scenario':
default: default:
routeName = ApiTestRouteEnum.API_TEST_SCENARIO; routeName = ApiTestRouteEnum.API_TEST_SCENARIO;
query.sId = id; query.id = id;
break; break;
} }
openNewPage(routeName, query); openNewPage(routeName, query);

View File

@ -98,7 +98,8 @@
step.stepType === ScenarioStepType.LOOP_CONTROLLER && step.stepType === ScenarioStepType.LOOP_CONTROLLER &&
step.config.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT && step.config.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT &&
step.config.msCountController && step.config.msCountController &&
step.config.msCountController.loops > 0 !Number.isNaN(step.config.msCountController.loops) &&
Number(step.config.msCountController.loops) > 0
) { ) {
// / // /
const firstHasResultChild = step.children?.find((child) => { const firstHasResultChild = step.children?.find((child) => {

View File

@ -19,11 +19,12 @@
v-model="scriptName" v-model="scriptName"
:placeholder="t('apiScenario.scriptOperationNamePlaceholder')" :placeholder="t('apiScenario.scriptOperationNamePlaceholder')"
:max-length="255" :max-length="255"
:disabled="isReadonly"
size="small" size="small"
/> />
</div> </div>
<div class="mt-[10px] flex flex-1 gap-[8px]"> <div class="mt-[10px] flex flex-1 gap-[8px]">
<conditionContent v-if="visible" v-model:data="activeItem" :is-build-in="true" /> <conditionContent v-if="visible" v-model:data="activeItem" :disabled="isReadonly" :is-build-in="true" />
</div> </div>
<div v-if="currentResponse?.console" class="p-[8px]"> <div v-if="currentResponse?.console" class="p-[8px]">
<div class="mb-[8px] font-medium text-[var(--color-text-1)]">{{ t('apiScenario.executionResult') }}</div> <div class="mb-[8px] font-medium text-[var(--color-text-1)]">{{ t('apiScenario.executionResult') }}</div>
@ -88,6 +89,7 @@
const { t } = useI18n(); const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true }); const visible = defineModel<boolean>('visible', { required: true });
const isReadonly = computed(() => props.step?.isQuoteScenarioStep);
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => { const currentResponse = computed(() => {
if (props.step?.uniqueId) { if (props.step?.uniqueId) {

View File

@ -16,7 +16,7 @@ export const defaultLoopController = {
variable: '', // 变量名 variable: '', // 变量名
}, },
msCountController: { msCountController: {
loops: 1, // 循环次数 loops: '1', // 循环次数
loopTime: 0, // 循环间隔时间 loopTime: 0, // 循环间隔时间
}, },
whileController: { whileController: {

View File

@ -10,14 +10,7 @@
@press-enter="loadExecuteHistoryList" @press-enter="loadExecuteHistoryList"
/> />
</div> </div>
<ms-base-table <ms-base-table v-bind="propsRes" no-disable filter-icon-align-left v-on="propsEvent">
v-bind="propsRes"
:first-column-width="44"
:secnario-id="props.scenarioId"
no-disable
filter-icon-align-left
v-on="propsEvent"
>
<template #num="{ record }"> <template #num="{ record }">
<span type="text" class="px-0">{{ record.num }}</span> <span type="text" class="px-0">{{ record.num }}</span>
</template> </template>
@ -27,12 +20,12 @@
trigger="click" trigger="click"
@popup-visible-change="handleFilterHidden" @popup-visible-change="handleFilterHidden"
> >
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="triggerModeFilterVisible = true"> <MsButton type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="triggerModeFilterVisible = true">
<div class="font-medium"> <div class="font-medium">
{{ t(columnConfig.title as string) }} {{ t(columnConfig.title as string) }}
</div> </div>
<icon-down :class="triggerModeFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" /> <icon-down :class="triggerModeFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button> </MsButton>
<template #content> <template #content>
<div class="arco-table-filters-content"> <div class="arco-table-filters-content">
<div class="ml-[6px] flex items-center justify-start px-[6px] py-[2px]"> <div class="ml-[6px] flex items-center justify-start px-[6px] py-[2px]">
@ -146,10 +139,6 @@
slotName: 'triggerMode', slotName: 'triggerMode',
showTooltip: true, showTooltip: true,
titleSlotName: 'triggerModeFilter', titleSlotName: 'triggerModeFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 100, width: 100,
}, },
{ {
@ -157,10 +146,6 @@
dataIndex: 'status', dataIndex: 'status',
slotName: 'status', slotName: 'status',
titleSlotName: 'statusFilter', titleSlotName: 'statusFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 100, width: 100,
}, },
{ {

View File

@ -13,7 +13,6 @@
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
:data="folderTree" :data="folderTree"
:keyword="moduleKeyword" :keyword="moduleKeyword"
:node-more-actions="folderMoreActions"
:default-expand-all="isExpandAll" :default-expand-all="isExpandAll"
:expand-all="isExpandAll" :expand-all="isExpandAll"
:empty-text="t('apiScenario.tree.noMatchModule')" :empty-text="t('apiScenario.tree.noMatchModule')"
@ -48,7 +47,6 @@
import { getModuleCount, getModuleTree } from '@/api/modules/api-test/scenario'; import { getModuleCount, getModuleTree } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils'; import { mapTree } from '@/utils';
@ -65,27 +63,10 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal();
function handleSelect(value: string | number | Record<string, any> | undefined) {
switch (value) {
case 'newScenario':
emit('newScenario');
break;
case 'import':
emit('import');
break;
case 'addModule':
document.querySelector('#addModulePopSpan')?.dispatchEvent(new Event('click'));
break;
default:
break;
}
}
const virtualListProps = computed(() => { const virtualListProps = computed(() => {
return { return {
height: 'calc(100vh - 343px)', height: '40vh',
threshold: 200, threshold: 200,
fixedSize: true, fixedSize: true,
buffer: 15, // 10 padding buffer: 15, // 10 padding
@ -96,30 +77,10 @@
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
const focusNodeKey = ref<string | number>(''); const focusNodeKey = ref<string | number>('');
const selectedKeys = ref<Array<string | number>>([]); const selectedKeys = ref<Array<string | number>>([]);
const allFolderClass = computed(() =>
selectedKeys.value[0] === 'all' ? 'folder-text folder-text--active' : 'folder-text'
);
const loading = ref(false); const loading = ref(false);
const folderMoreActions: ActionsItem[] = [
{
label: 'common.rename',
eventTag: 'rename',
},
{
isDivider: true,
},
{
label: 'common.delete',
eventTag: 'delete',
danger: true,
},
];
const modulesCount = ref<Record<string, number>>({}); const modulesCount = ref<Record<string, number>>({});
const allScenarioCount = computed(() => modulesCount.value.all || 0);
const isExpandAll = ref(props.isExpandAll); const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); //
/** /**
* 初始化模块树 * 初始化模块树
@ -164,10 +125,6 @@
} }
); );
function changeExpand() {
isExpandAll.value = !isExpandAll.value;
}
/** /**
* 处理文件夹树节点选中事件 * 处理文件夹树节点选中事件
*/ */

View File

@ -92,6 +92,7 @@
}, },
], ],
titleSlotName: 'typeTitle', titleSlotName: 'typeTitle',
typeTitleTooltip: t('apiScenario.params.typeTooltip'),
}, },
{ {
title: 'apiScenario.params.paramValue', title: 'apiScenario.params.paramValue',

View File

@ -1,7 +1,13 @@
<template> <template>
<div class="condition"> <div class="condition">
<div> <div>
<precondition v-model:config="preProcessorConfig" :is-definition="false" @change="emit('change')" /> <precondition
v-model:config="preProcessorConfig"
:is-definition="false"
sql-code-editor-height="300px"
is-scenario
@change="emit('change')"
/>
</div> </div>
<a-divider class="my-[8px]" type="dashed" /> <a-divider class="my-[8px]" type="dashed" />
<div> <div>
@ -9,6 +15,8 @@
v-model:config="postProcessorConfig" v-model:config="postProcessorConfig"
:is-definition="false" :is-definition="false"
:layout="activeLayout" :layout="activeLayout"
sql-code-editor-height="300px"
is-scenario
@change="emit('change')" @change="emit('change')"
/> />
</div> </div>
@ -37,10 +45,6 @@
<style lang="less" scoped> <style lang="less" scoped>
.condition { .condition {
.ms-scroll-bar(); .ms-scroll-bar();
@apply h-full overflow-auto;
overflow: auto;
width: 100%;
height: 700px;
flex-shrink: 0;
} }
</style> </style>

View File

@ -242,6 +242,9 @@
</template> </template>
</TableFilter> </TableFilter>
</template> </template>
<template #stepTotal="{ record }">
{{ record.stepTotal }}
</template>
<template #operation="{ record }"> <template #operation="{ record }">
<MsButton <MsButton
v-permission="['PROJECT_API_SCENARIO:READ+UPDATE']" v-permission="['PROJECT_API_SCENARIO:READ+UPDATE']"
@ -505,14 +508,18 @@
style="padding-top: 10px" style="padding-top: 10px"
> >
<a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" /> <a-switch v-model="batchForm.append" class="mr-1" size="small" type="line" />
<a-tooltip :content="t('caseManagement.featureCase.enableTags')"> <span class="flex items-center">
<span class="flex items-center"> <span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span> <span class="mt-[2px]">
<span class="mt-[2px]"> <a-tooltip>
<IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" /> <IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" />
</span> <template #content>
<div>{{ t('caseManagement.featureCase.enableTags') }}</div>
<div>{{ t('caseManagement.featureCase.closeTags') }}</div>
</template>
</a-tooltip>
</span> </span>
</a-tooltip> </span>
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch"> <a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch">
@ -553,7 +560,7 @@
{{ t('api_scenario.table.batchModalSubTitle', { count: tableSelected.length }) }} {{ t('api_scenario.table.batchModalSubTitle', { count: tableSelected.length }) }}
</div> </div>
</div> </div>
<div v-else-if="isBatchMove"> <div v-else-if="isBatchMove" class="flex items-center">
{{ t('common.batchMove') }} {{ t('common.batchMove') }}
<div <div
class="one-line-text ml-[4px] max-w-[100%] text-[var(--color-text-4)]" class="one-line-text ml-[4px] max-w-[100%] text-[var(--color-text-4)]"
@ -795,6 +802,7 @@
{ {
title: 'apiScenario.table.columns.steps', title: 'apiScenario.table.columns.steps',
dataIndex: 'stepTotal', dataIndex: 'stepTotal',
slotName: 'stepTotal',
showInTable: false, showInTable: false,
showDrag: true, showDrag: true,
width: 100, width: 100,

View File

@ -77,7 +77,7 @@
| ScenarioAddStepActionType.SCRIPT_OPERATION, | ScenarioAddStepActionType.SCRIPT_OPERATION,
step?: ScenarioStepItem step?: ScenarioStepItem
); );
(e: 'addDone'); (e: 'addDone', newStep: ScenarioStepItem);
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -117,7 +117,7 @@
} else { } else {
steps.value.push(defaultLoopStep); steps.value.push(defaultLoopStep);
} }
emit('addDone'); emit('addDone', defaultLoopStep);
break; break;
case ScenarioAddStepActionType.CONDITION_CONTROL: case ScenarioAddStepActionType.CONDITION_CONTROL:
const defaultConditionStep = buildInsertStepInfos( const defaultConditionStep = buildInsertStepInfos(
@ -132,7 +132,7 @@
} else { } else {
steps.value.push(defaultConditionStep); steps.value.push(defaultConditionStep);
} }
emit('addDone'); emit('addDone', defaultConditionStep);
break; break;
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL: case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
const defaultOnlyOnceStep = buildInsertStepInfos( const defaultOnlyOnceStep = buildInsertStepInfos(
@ -147,7 +147,7 @@
} else { } else {
steps.value.push(defaultOnlyOnceStep); steps.value.push(defaultOnlyOnceStep);
} }
emit('addDone'); emit('addDone', defaultOnlyOnceStep);
break; break;
case ScenarioAddStepActionType.WAIT_TIME: case ScenarioAddStepActionType.WAIT_TIME:
const defaultWaitTimeStep = buildInsertStepInfos( const defaultWaitTimeStep = buildInsertStepInfos(
@ -162,7 +162,7 @@
} else { } else {
steps.value.push(defaultWaitTimeStep); steps.value.push(defaultWaitTimeStep);
} }
emit('addDone'); emit('addDone', defaultWaitTimeStep);
break; break;
case ScenarioAddStepActionType.IMPORT_SYSTEM_API: case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
case ScenarioAddStepActionType.CUSTOM_API: case ScenarioAddStepActionType.CUSTOM_API:

View File

@ -26,7 +26,7 @@
:popup-translate="[-7, -10]" :popup-translate="[-7, -10]"
@other-create="(type, step) => emit('otherCreate', type, step, activeCreateAction)" @other-create="(type, step) => emit('otherCreate', type, step, activeCreateAction)"
@close="handleActionsClose" @close="handleActionsClose"
@add-done="emit('addDone')" @add-done="emit('addDone', $event)"
> >
<span></span> <span></span>
</createStepActions> </createStepActions>
@ -93,7 +93,7 @@
step?: ScenarioStepItem, step?: ScenarioStepItem,
activeCreateAction?: CreateStepAction activeCreateAction?: CreateStepAction
); );
(e: 'addDone'); (e: 'addDone', newStep: ScenarioStepItem);
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -12,7 +12,7 @@
/> />
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
{{ t('apiScenario.sum') }} {{ t('apiScenario.sum') }}
<div class="text-[rgb(var(--primary-5))]">{{ totalStepCount }}</div> <div class="text-[rgb(var(--primary-5))]">{{ topLevelStepCount }}</div>
{{ t('apiScenario.steps') }} {{ t('apiScenario.steps') }}
</div> </div>
</div> </div>
@ -171,6 +171,7 @@
const stepTreeRef = ref<InstanceType<typeof stepTree>>(); const stepTreeRef = ref<InstanceType<typeof stepTree>>();
const keyword = ref(''); const keyword = ref('');
const topLevelStepCount = computed(() => scenario.value.steps.length);
const totalStepCount = computed(() => countNodes(scenario.value.steps)); const totalStepCount = computed(() => countNodes(scenario.value.steps));
function handleChangeAll(value: boolean | (string | number | boolean)[]) { function handleChangeAll(value: boolean | (string | number | boolean)[]) {
@ -267,7 +268,7 @@
} }
function batchDelete() { function batchDelete() {
deleteNodes(scenario.value.steps, checkedKeys.value, 'uniqueId'); deleteNodes(scenario.value.steps, checkedKeys.value, (node) => !node.isQuoteScenarioStep, 'uniqueId');
Message.success(t('common.deleteSuccess')); Message.success(t('common.deleteSuccess'));
if (scenario.value.steps.length === 0) { if (scenario.value.steps.length === 0) {
checkedAll.value = false; checkedAll.value = false;
@ -357,13 +358,16 @@
if (!node.enable) { if (!node.enable) {
// uniqueId便waitingDebugStepDetails // uniqueId便waitingDebugStepDetails
checkedKeysSet.delete(node.uniqueId); checkedKeysSet.delete(node.uniqueId);
node.executeStatus = undefined; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; //
} else { } else {
node.executeStatus = ScenarioExecuteStatus.EXECUTING; node.executeStatus = ScenarioExecuteStatus.EXECUTING; //
} }
return !!node.enable; return !!node.enable;
} }
node.executeStatus = undefined; // node.executeStatus = undefined; //
if (node.children && node.children.length > 0) {
return true; //
}
return false; return false;
}); });
const waitingDebugStepDetails = {}; const waitingDebugStepDetails = {};

View File

@ -14,14 +14,11 @@
:content="innerData.msCountController.loops?.toString()" :content="innerData.msCountController.loops?.toString()"
:disabled="!innerData.msCountController.loops" :disabled="!innerData.msCountController.loops"
> >
<a-input-number <a-input
v-model:model-value="innerData.msCountController.loops" v-model:model-value="innerData.msCountController.loops"
class="w-[80px] px-[8px]" class="w-[80px] px-[8px]"
size="mini" size="mini"
:step="1"
:min="1"
hide-button hide-button
:precision="0"
model-event="input" model-event="input"
:disabled="props.disabled" :disabled="props.disabled"
@blur="handleInputChange" @blur="handleInputChange"
@ -29,7 +26,7 @@
<template #prefix> <template #prefix>
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.num') }}:</div> <div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.num') }}:</div>
</template> </template>
</a-input-number> </a-input>
</a-tooltip> </a-tooltip>
</a-input-group> </a-input-group>
<template v-if="innerData.loopType === ScenarioStepLoopTypeEnum.FOREACH"> <template v-if="innerData.loopType === ScenarioStepLoopTypeEnum.FOREACH">

View File

@ -17,13 +17,13 @@
<div> <div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div> <div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
<div class="text-[14px] text-[var(--color-text-1)]"> <div class="text-[14px] text-[var(--color-text-1)]">
{{ originProjectName }} {{ originProjectInfo?.projectName }}
</div> </div>
</div> </div>
<div> <div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.detailName') }}</div> <div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.detailName') }}</div>
<div class="cursor-pointer text-[14px] text-[rgb(var(--primary-5))]" @click="goDetail"> <div class="cursor-pointer text-[14px] text-[rgb(var(--primary-5))]" @click="goDetail">
{{ `${props.data.resourceNum}${props.data.resourceName}` }} {{ `${originProjectInfo?.num}${originProjectInfo?.name}` }}
</div> </div>
</div> </div>
</div> </div>
@ -53,7 +53,7 @@
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepItem, ScenarioStepResourceInfo } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum'; import { ApiTestRouteEnum } from '@/enums/routeEnum';
@ -67,12 +67,11 @@
const { t } = useI18n(); const { t } = useI18n();
const { openNewPage } = useOpenNewPage(); const { openNewPage } = useOpenNewPage();
const originProjectName = ref(''); const originProjectInfo = ref<ScenarioStepResourceInfo>();
async function handleVisibleChange(val: boolean) { async function handleVisibleChange(val: boolean) {
if (val && props.data.originProjectId) { if (val && props.data.originProjectId) {
const res = await getStepProjectInfo(props.data.originProjectId); originProjectInfo.value = await getStepProjectInfo(props.data.resourceId || '', props.data.stepType);
originProjectName.value = res.name;
} }
} }
@ -90,7 +89,7 @@
case _stepType.isQuoteScenario: case _stepType.isQuoteScenario:
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
pId: props.data.originProjectId, pId: props.data.originProjectId,
sId: props.data.resourceId, id: props.data.resourceId,
}); });
break; break;
case _stepType.isQuoteCase: case _stepType.isQuoteCase:

View File

@ -14,7 +14,7 @@
:field-names="{ title: 'name', key: 'uniqueId', children: 'children' }" :field-names="{ title: 'name', key: 'uniqueId', children: 'children' }"
:virtual-list-props="{ :virtual-list-props="{
height: '100%', height: '100%',
threshold: 20, threshold: 200,
fixedSize: true, fixedSize: true,
buffer: 15, // 10 padding buffer: 15, // 10 padding
}" }"
@ -50,10 +50,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="icon-icon_split_turn-down_arrow" :size="14" />
:type="step.expanded ? 'icon-icon_split-turn-down-left' : 'icon-icon_split_turn-down_arrow'"
:size="14"
/>
{{ step.children?.length || 0 }} {{ step.children?.length || 0 }}
</div> </div>
</a-tooltip> </a-tooltip>
@ -67,14 +64,14 @@
></a-switch> ></a-switch>
<!-- 步骤执行 --> <!-- 步骤执行 -->
<MsIcon <MsIcon
v-show="!step.isExecuting" v-show="!step.isExecuting && step.enable"
type="icon-icon_play-round_filled" type="icon-icon_play-round_filled"
:size="18" :size="18"
class="cursor-pointer text-[rgb(var(--link-6))]" class="cursor-pointer text-[rgb(var(--link-6))]"
@click.stop="executeStep(step)" @click.stop="executeStep(step)"
/> />
<MsIcon <MsIcon
v-show="step.isExecuting" v-show="step.isExecuting && step.enable"
type="icon-icon_stop" type="icon-icon_stop"
:size="20" :size="20"
class="cursor-pointer text-[rgb(var(--link-6))]" class="cursor-pointer text-[rgb(var(--link-6))]"
@ -171,13 +168,7 @@
</template> </template>
<template #extra="step"> <template #extra="step">
<stepInsertStepTrigger <stepInsertStepTrigger
v-if=" v-if="!step.isQuoteScenarioStep"
!step.isQuoteScenarioStep &&
!(
step.stepType === ScenarioStepType.API_SCENARIO &&
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(step.refType)
)
"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:steps="steps" v-model:steps="steps"
v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']" v-permission="['PROJECT_API_DEBUG:READ+ADD', 'PROJECT_API_DEFINITION:READ+UPDATE']"
@ -331,7 +322,13 @@
<a-radio :value="false">{{ t('apiScenario.sourceScenario') }}</a-radio> <a-radio :value="false">{{ t('apiScenario.sourceScenario') }}</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item :label="t('apiScenario.currentScenarioTip')"> <a-form-item
:label="
scenarioConfigForm.useCurrentScenarioParam
? t('apiScenario.currentScenarioTip')
: t('apiScenario.sourceScenarioTip')
"
>
<a-radio-group v-model:model-value="scenarioConfigForm.useBothScenarioParam"> <a-radio-group v-model:model-value="scenarioConfigForm.useBothScenarioParam">
<a-radio :value="false">{{ t('apiScenario.empty') }}</a-radio> <a-radio :value="false">{{ t('apiScenario.empty') }}</a-radio>
<a-radio :value="true"> <a-radio :value="true">
@ -1076,7 +1073,8 @@
} }
} }
function handleAddStepDone() { function handleAddStepDone(newStep: ScenarioStepItem) {
selectedKeys.value = [newStep.uniqueId]; //
emit('stepAdd'); emit('stepAdd');
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
@ -1098,7 +1096,7 @@
// api api api // api api api
activeStep.value = step; activeStep.value = step;
if ( if (
(stepDetails.value[step.id] === undefined && step.copyFromStepId) || (stepDetails.value[step.id] === undefined && step.copyFromStepId && !step.isNew) ||
(stepDetails.value[step.id] === undefined && !step.isNew) (stepDetails.value[step.id] === undefined && !step.isNew)
) { ) {
// //
@ -1109,7 +1107,7 @@
activeStep.value = step; activeStep.value = step;
if ( if (
_stepType.isCopyCase && _stepType.isCopyCase &&
((stepDetails.value[step.id] === undefined && step.copyFromStepId) || ((stepDetails.value[step.id] === undefined && step.copyFromStepId && !step.isNew) ||
(stepDetails.value[step.id] === undefined && !step.isNew)) (stepDetails.value[step.id] === undefined && !step.isNew))
) { ) {
// case // case
@ -1444,11 +1442,7 @@
*/ */
function addCustomApiStep(request: RequestParam) { function addCustomApiStep(request: RequestParam) {
request.isNew = false; request.isNew = false;
stepDetails.value[request.stepId] = { stepDetails.value[request.stepId] = request;
...request,
customizeRequest: true,
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
};
scenario.value.stepFileParam[request.stepId] = { scenario.value.stepFileParam[request.stepId] = {
linkFileIds: request.linkFileIds, linkFileIds: request.linkFileIds,
uploadFileIds: request.uploadFileIds, uploadFileIds: request.uploadFileIds,
@ -1490,6 +1484,7 @@
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}); });
} }
selectedKeys.value = [request.stepId]; //
emit('stepAdd'); emit('stepAdd');
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
@ -1572,6 +1567,7 @@
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}); });
} }
selectedKeys.value = [id]; //
emit('stepAdd'); emit('stepAdd');
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }

View File

@ -12,70 +12,77 @@ export default function updateStepStatus(
) { ) {
for (let i = 0; i < steps.length; i++) { for (let i = 0; i < steps.length; i++) {
const node = steps[i]; const node = steps[i];
if ( if (node.enable) {
[ // 启用的步骤才计算
ScenarioStepType.LOOP_CONTROLLER, if (
ScenarioStepType.IF_CONTROLLER, [
ScenarioStepType.ONCE_ONLY_CONTROLLER, ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.API_SCENARIO, ScenarioStepType.IF_CONTROLLER,
].includes(node.stepType) ScenarioStepType.ONCE_ONLY_CONTROLLER,
) { ScenarioStepType.API_SCENARIO,
if (!node.executeStatus) { ].includes(node.stepType)
// 没有执行状态,说明未参与执行,直接跳过 ) {
break; if (!node.executeStatus) {
} // 没有执行状态,说明未参与执行,直接跳过
// 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的 // eslint-disable-next-line no-continue
let hasNotExecuted = false; continue;
let hasFailure = false; }
let hasFakeError = false; // 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
if (!node.children || node.children.length === 0) { let hasNotExecuted = false;
// 逻辑控制器内无步骤,则直接是未执行 let hasFailure = false;
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; let hasFakeError = false;
} else { if (!node.children || node.children.length === 0) {
for (let j = 0; j < node.children.length; j++) { // 逻辑控制器内无步骤,则直接是未执行
const childNode = node.children[j]; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
updateStepStatus([childNode], stepResponses); } else {
if ( for (let j = 0; j < node.children.length; j++) {
childNode.executeStatus && const childNode = node.children[j];
[ScenarioExecuteStatus.EXECUTING, ScenarioExecuteStatus.UN_EXECUTE].includes(childNode.executeStatus) updateStepStatus([childNode], stepResponses);
) { if (childNode.executeStatus === ScenarioExecuteStatus.FAILED) {
// 子节点未执行或正在执行,则逻辑控制器也是未执行 // 子节点有一个失败,逻辑控制器就是失败
hasNotExecuted = true; hasFailure = true;
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAILED) { } else if (childNode.executeStatus === ScenarioExecuteStatus.FAKE_ERROR) {
// 子节点有一个失败,逻辑控制器就是失败 // 子节点有一个误报,逻辑控制器就是误报
hasFailure = true; hasFakeError = true;
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAKE_ERROR) { } else if (
// 子节点有一个误报,逻辑控制器就是误报 childNode.executeStatus &&
hasFakeError = true; [ScenarioExecuteStatus.EXECUTING, ScenarioExecuteStatus.UN_EXECUTE].includes(childNode.executeStatus)
) {
// 子节点未执行或正在执行,则逻辑控制器也是未执行
hasNotExecuted = true;
}
}
// 递归完子节点后,判断当前逻辑控制器的状态
if (hasFailure) {
node.executeStatus = ScenarioExecuteStatus.FAILED;
} else if (hasFakeError) {
node.executeStatus = ScenarioExecuteStatus.FAKE_ERROR;
} else if (hasNotExecuted) {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
} else {
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} }
} }
// 递归完子节点后,判断当前逻辑控制器的状态 } else if (node.stepType === ScenarioStepType.CONSTANT_TIMER) {
if (hasNotExecuted) { // 等待时间直接设置为成功
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态
if (stepResponses[node.uniqueId] && stepResponses[node.uniqueId].length > 0) {
// 存在多个请求结果说明是循环控制器下的步骤,需要判断其子步骤的执行结果
if (stepResponses[node.uniqueId].some((report) => !report.isSuccessful)) {
node.executeStatus = ScenarioExecuteStatus.FAILED;
} else if (
stepResponses[node.uniqueId].some((report) => report.status === ScenarioExecuteStatus.FAKE_ERROR)
) {
node.executeStatus = ScenarioExecuteStatus.FAKE_ERROR;
} else {
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
}
} else {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
} else if (hasFailure) {
node.executeStatus = ScenarioExecuteStatus.FAILED;
} else if (hasFakeError) {
node.executeStatus = ScenarioExecuteStatus.FAKE_ERROR;
} else {
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} }
} }
} else if (node.stepType === ScenarioStepType.CONSTANT_TIMER) {
// 等待时间直接设置为成功
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态
if (stepResponses[node.uniqueId] && stepResponses[node.uniqueId].length > 0) {
if (stepResponses[node.uniqueId].some((report) => !report.isSuccessful)) {
node.executeStatus = ScenarioExecuteStatus.FAILED;
} else if (stepResponses[node.uniqueId].some((report) => report.status === ScenarioExecuteStatus.FAKE_ERROR)) {
node.executeStatus = ScenarioExecuteStatus.FAKE_ERROR;
} else {
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
}
} else {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
}
} }
} }
} }

View File

@ -200,8 +200,8 @@
function share() { function share() {
if (isSupported) { if (isSupported) {
const url = window.location.href; const url = window.location.href;
const dIdParam = `&sId=${scenario.value.id}`; const dIdParam = `&id=${scenario.value.id}`;
const copyUrl = url.includes('sId') ? url.split('&sId')[0] : url; const copyUrl = url.includes('id') ? url.split('&id')[0] : url;
copy(`${copyUrl}${dIdParam}`); copy(`${copyUrl}${dIdParam}`);
Message.success(t('common.copySuccess')); Message.success(t('common.copySuccess'));
} else { } else {

View File

@ -192,19 +192,22 @@
if (scenario.reportId === data.reportId) { if (scenario.reportId === data.reportId) {
// tabtab // tabtab
data.taskResult.requestResults.forEach((result) => { data.taskResult.requestResults.forEach((result) => {
if (scenario.stepResponses[result.stepId] === undefined) { if (result.stepId) {
scenario.stepResponses[result.stepId] = []; // id
} if (scenario.stepResponses[result.stepId] === undefined) {
scenario.stepResponses[result.stepId].push({ scenario.stepResponses[result.stepId] = [];
...result, }
console: data.taskResult.console, scenario.stepResponses[result.stepId].push({
}); ...result,
if (result.status === ScenarioExecuteStatus.FAKE_ERROR) { console: data.taskResult.console,
scenario.executeFakeErrorCount += 1; });
} else if (result.isSuccessful) { if (result.status === ScenarioExecuteStatus.FAKE_ERROR) {
scenario.executeSuccessCount += 1; scenario.executeFakeErrorCount += 1;
} else { } else if (result.isSuccessful) {
scenario.executeFailCount += 1; scenario.executeSuccessCount += 1;
} else {
scenario.executeFailCount += 1;
}
} }
}); });
} }
@ -306,6 +309,8 @@
if (node.enable) { if (node.enable) {
node.executeStatus = ScenarioExecuteStatus.EXECUTING; node.executeStatus = ScenarioExecuteStatus.EXECUTING;
waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id]; waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id];
} else {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
} }
return !!node.enable; return !!node.enable;
}); });
@ -573,8 +578,8 @@
onBeforeMount(() => { onBeforeMount(() => {
selectRecycleCount(); selectRecycleCount();
if (route.query.sId) { if (route.query.id) {
openScenarioTab(route.query.sId as string); openScenarioTab(route.query.id as string);
} }
}); });

View File

@ -1,244 +1,262 @@
export default { export default {
'apiScenario.env': 'Environment: {name}', 'apiScenario.env': 'Current Environment: {name}',
'apiScenario.allScenario': 'All scenarios', 'apiScenario.allScenario': 'All Scenarios',
'apiScenario.createScenario': 'Create scenario', 'apiScenario.createScenario': 'Create Scenario',
'apiScenario.importScenario': 'Import scenario', 'apiScenario.importScenario': 'Import Scenario',
'apiScenario.tree.selectorPlaceholder': 'Please enter the module name', 'apiScenario.tree.selectorPlaceholder': 'Please enter module name',
'apiScenario.tree.folder.allScenario': 'All scenarios', 'apiScenario.tree.folder.allScenario': 'All Scenarios',
'apiScenario.tree.recycleBin': 'Recycle bin', 'apiScenario.tree.recycleBin': 'Recycle Bin',
'apiScenario.tree.noMatchModule': 'No matching module/scene yet', 'apiScenario.tree.noMatchModule': 'No matching module/scenario found',
'apiScenario.createSubModule': 'Create sub-module', 'apiScenario.createSubModule': 'Create Submodule',
'apiScenario.module.deleteTipTitle': 'Delete {name} module?', 'apiScenario.module.deleteTipTitle': 'Delete Module {name}?',
'apiScenario.module.deleteTipContent': 'apiScenario.module.deleteTipContent':
'After deletion, all scenarios under the module will be deleted synchronously. Please operate with caution.', 'Deleting will also delete all scenarios under the module. Proceed with caution.',
'apiScenario.deleteConfirm': 'Confirm', 'apiScenario.deleteConfirm': 'Confirm Delete',
'apiScenario.deleteSuccess': 'Success', 'apiScenario.deleteSuccess': 'Delete Successful',
'apiScenario.moveSuccess': 'Success', 'apiScenario.moveSuccess': 'Move Successful',
'apiScenario.baseInfo': 'Base info', 'apiScenario.baseInfo': 'Basic Information',
'apiScenario.step': 'Step', 'apiScenario.step': 'Step',
'apiScenario.params': 'Params', 'apiScenario.params': 'Parameters',
'apiScenario.prePost': 'Pre/Post', 'apiScenario.prePost': 'Pre/Post',
'apiScenario.assertion': 'Assertion', 'apiScenario.assertion': 'Assertion',
'apiScenario.executeHistory': 'Execute history', 'apiScenario.executeHistory': 'Execution History',
'apiScenario.changeHistory': 'Change history', 'apiScenario.changeHistory': 'Change History',
'apiScenario.dependency': 'Dependencies', 'apiScenario.dependency': 'Dependency',
'apiScenario.quote': 'Reference', 'apiScenario.quote': 'Quote',
'apiScenario.params.convention': 'Convention parameter', 'apiScenario.params.convention': 'Conventional Parameters',
'apiScenario.params.searchPlaceholder': 'Search by name or tag', 'apiScenario.params.searchPlaceholder': 'Search by name or tag',
'apiScenario.params.priority': 'apiScenario.params.priority':
'Variable priority: Temporary parameters>Scene parameters>Environment parameters>Global parameters; Note: Avoid using variables with the same name. When using variables with the same name, scene level CSV has the highest priority', 'Variable Priority: Temporary Parameters > Scenario Parameters > Environment Parameters > Global Parameters; Note: Avoid using variables with the same name. In case of same name variables, scenario-level CSV has the highest priority.',
'apiScenario.params.name': 'Variable name', 'apiScenario.params.name': 'Variable Name',
'apiScenario.params.type': 'Type', 'apiScenario.params.type': 'Type',
'apiScenario.params.paramValue': 'Parameter value', 'apiScenario.params.paramValue': 'Parameter Value',
'apiScenario.params.tag': 'Tag', 'apiScenario.params.tag': 'Tag',
'apiScenario.params.desc': 'Description', 'apiScenario.params.desc': 'Description',
'apiScenario.table.columns.name': 'Name', 'apiScenario.params.typeTooltip': 'Json: only supports UI testing',
'apiScenario.table.columns.level': 'Level', 'apiScenario.table.columns.name': 'Scenario Name',
'apiScenario.table.columns.status': 'status', 'apiScenario.table.columns.level': 'Scenario Level',
'apiScenario.table.columns.runResult': 'Run result', 'apiScenario.table.columns.status': 'Status',
'apiScenario.table.columns.runResult': 'Execution Result',
'apiScenario.table.columns.tags': 'Tags', 'apiScenario.table.columns.tags': 'Tags',
'apiScenario.table.columns.scenarioEnv': 'Scenario environment', 'apiScenario.table.columns.scenarioEnv': 'Scenario Environment',
'apiScenario.table.columns.steps': 'Steps', 'apiScenario.table.columns.steps': 'Number of Steps',
'apiScenario.table.columns.passRate': 'Pass rate', 'apiScenario.table.columns.passRate': 'Pass Rate',
'apiScenario.table.columns.module': 'Module', 'apiScenario.table.columns.module': 'Belongs to Module',
'apiScenario.table.columns.createUser': 'Create user', 'apiScenario.table.columns.createUser': 'Creator',
'apiScenario.table.columns.createTime': 'Create time', 'apiScenario.table.columns.createTime': 'Creation Time',
'apiScenario.table.columns.updateUser': 'Update user', 'apiScenario.table.columns.updateUser': 'Updater',
'apiScenario.table.columns.updateTime': 'Update time', 'apiScenario.table.columns.updateTime': 'Update Time',
'apiScenario.table.columns.deleteUser': 'Delete User', 'apiScenario.table.columns.operation': 'Operator',
'apiScenario.table.columns.operation': 'Operation', 'apiScenario.table.columns.deleteUser': 'Deleter',
'apiScenario.table.columns.deleteTime': 'Delete time', 'apiScenario.table.columns.deleteTime': 'Deletion Time',
'api_scenario.table.tableNoDataAndPlease': 'No data yet, please', 'api_scenario.table.searchPlaceholder': 'Search by ID/Name/Tag',
'api_scenario.table.batchModalSubTitle': '(Selected {count} scenarios)',
'api_scenario.table.chooseAttr': 'Choose Attribute',
'api_scenario.table.attrRequired': 'Attribute cannot be empty',
'api_scenario.table.batchUpdate': 'Batch Update to',
'api_scenario.table.valueRequired': 'Attribute value cannot be empty',
'api_scenario.table.deleteScenarioTipTitle': 'Confirm Delete {name}?',
'api_scenario.table.batchDeleteScenarioTip': 'Confirm delete {count} selected scenarios?',
'api_scenario.table.deleteScenarioTip':
'Deleting may cause failure in resources referencing this scenario. Proceed with caution!',
'api_scenario.table.apiStatus': 'Status',
'api_scenario.table.status.underway': 'Underway',
'api_scenario.table.status.completed': 'Completed',
'api_scenario.table.status.deprecate': 'Deprecated',
'api_scenario.table.tableNoDataAndPlease': 'No data, please',
'api_scenario.table.or': 'or', 'api_scenario.table.or': 'or',
'apiScenario.execute': 'Execute', 'apiScenario.execute': 'Execute',
'apiScenario.prev': 'Last time', 'apiScenario.prev': 'Previous',
'apiScenario.next': 'Next time', 'apiScenario.next': 'Next',
'apiScenario.sumLoop': 'A total of {count} loops', 'apiScenario.sumLoop': 'Total {count} loops',
'apiScenario.times': 'bout', 'apiScenario.times': 'times',
'apiScenario.executionResult': 'Execution result', 'apiScenario.executionResult': 'Execution Result',
// 批量操作文案 'apiScenario.refreshRefScenario': 'Refresh Referenced Scenario Data',
'api_scenario.batch_operation.success': 'Success {success} error {error} ', 'apiScenario.updateRefScenarioSuccess': 'Referenced scenario data has been updated',
'api_scenario.table.batchMoveConfirm': 'Ready to {opt} {count} scenarios', 'apiScenario.replaceSuccess': 'Step replacement successful',
'apiScenario.name': 'Scene name', // Batch operation text
'apiScenario.nameRequired': 'Scene name cannot be empty', 'api_scenario.batch_operation.success': 'Successfully {opt} to {name}',
'apiScenario.namePlaceholder': 'Please enter scene name', 'api_scenario.table.batchMoveConfirm': '{opt} {count} scenarios to selected module',
'apiScenario.belongModule': 'Belonging module', 'apiScenario.name': 'Scenario Name',
'apiScenario.level': 'Scene level', 'apiScenario.nameRequired': 'Scenario name cannot be empty',
'apiScenario.status': 'Scene status', 'apiScenario.namePlaceholder': 'Please enter scenario name',
'apiScenario.belongModule': 'Belongs to Module',
'apiScenario.level': 'Scenario Level',
'apiScenario.status': 'Scenario Status',
'apiScenario.descPlaceholder': 'Please describe the scenario', 'apiScenario.descPlaceholder': 'Please describe the scenario',
'apiScenario.addStep': 'Add steps', 'apiScenario.addStep': 'Add Step',
'apiScenario.requestScenario': 'Request/Scenario', 'apiScenario.requestScenario': 'Request/Scenario',
'apiScenario.importSystemApi': 'Import system requests', 'apiScenario.importSystemApi': 'Import System Request',
'apiScenario.customApi': 'Custom request', 'apiScenario.customApi': 'Custom Request',
'apiScenario.logicControl': 'Logic control', 'apiScenario.logicControl': 'Logic Control',
'apiScenario.loopControl': 'Loop controller', 'apiScenario.loopControl': 'Loop Controller',
'apiScenario.tutorial': 'Tutorial', 'apiScenario.tutorial': 'Tutorial',
'apiScenario.conditionControl': 'Conditional controller', 'apiScenario.conditionControl': 'Condition Controller',
'apiScenario.onlyOnceControl': 'Only once controller', 'apiScenario.onlyOnceControl': 'Only Once Controller',
'apiScenario.other': 'Other', 'apiScenario.other': 'Other',
'apiScenario.scriptOperation': 'Script operation', 'apiScenario.scriptOperation': 'Script Operation',
'apiScenario.waitTime': 'Waiting time', 'apiScenario.waitTime': 'Wait Time',
'apiScenario.quoteApi': 'Reference API', 'apiScenario.quoteApi': 'Quote API',
'apiScenario.copyApi': 'Copy API', 'apiScenario.copyApi': 'Copy API',
'apiScenario.quoteCase': 'Reference CASE', 'apiScenario.quoteCase': 'Quote CASE',
'apiScenario.copyCase': 'Copy CASE', 'apiScenario.copyCase': 'Copy CASE',
'apiScenario.quoteScenario': 'Reference scene', 'apiScenario.quoteScenario': 'Quote Scenario',
'apiScenario.copyScenario': 'Copy scene', 'apiScenario.copyScenario': 'Copy Scenario',
'apiScenario.sum': 'Common', 'apiScenario.sum': 'Total',
'apiScenario.steps': 'steps', 'apiScenario.steps': 'steps',
'apiScenario.expandAllStep': 'Expand all substeps', 'apiScenario.expandAllStep': 'Expand All Substeps',
'apiScenario.collapseAllStep': 'Collapse all substeps', 'apiScenario.collapseAllStep': 'Collapse All Substeps',
'apiScenario.executeTime': 'Execution time', 'apiScenario.executeTime': 'Execution Time:',
'apiScenario.executeResult': 'Execution result', 'apiScenario.executeResult': 'Execution Result',
'apiScenario.checkReport': 'View report', 'apiScenario.checkReport': 'View Report',
'apiScenario.searchByName': 'Search by step name/description', 'apiScenario.searchByName': 'Search by step name/description',
'apiScenario.api': 'API', 'apiScenario.api': 'API',
'apiScenario.case': 'CASE', 'apiScenario.case': 'Case',
'apiScenario.scenario': 'Scenario', 'apiScenario.scenario': 'Scenario',
'apiScenario.sumSelected': 'Total selection', 'apiScenario.sumSelected': 'Selected',
'apiScenario.scenarioConfig': 'Scene configuration', 'apiScenario.scenarioConfig': 'Scenario Configuration',
'apiScenario.noMatchStep': 'No matching step data yet', 'apiScenario.noMatchStep': 'No matching step data',
'apiScenario.pleaseInputStepName': 'Please enter a step name', 'apiScenario.pleaseInputStepName': 'Please enter step name',
'apiScenario.belongProject': 'Project', 'apiScenario.belongProject': 'Belongs to Project',
'apiScenario.detailName': 'Name', 'apiScenario.detailName': 'Name',
'apiScenario.crossProject': 'Cross-project', 'apiScenario.crossProject': 'Cross Project',
'apiScenario.expandStepTip': 'Expand {count} substeps', 'apiScenario.expandStepTip': 'Expand {count} substeps',
'apiScenario.collapseStepTip': 'Collapse {count} substeps', 'apiScenario.collapseStepTip': 'Collapse {count} substeps',
'apiScenario.inside': 'Add substep', 'apiScenario.inside': 'Add Substep',
'apiScenario.before': 'Insert above', 'apiScenario.before': 'Insert Above',
'apiScenario.after': 'Insert below', 'apiScenario.after': 'Insert Below',
'apiScenario.num': 'frequency', 'apiScenario.num': 'Number',
'apiScenario.space': 'Interval(ms)', 'apiScenario.space': 'Interval (ms)',
'apiScenario.timeout': 'Timeout(ms)', 'apiScenario.timeout': 'Timeout (ms)',
'apiScenario.waitTimeMs': 'Wait(ms)', 'apiScenario.waitTimeMs': 'Wait (ms)',
'apiScenario.pleaseInputStepDesc': 'Please enter a step description', 'apiScenario.pleaseInputStepDesc': 'Please enter step description',
'apiScenario.variable': 'Variable name {suffix}', 'apiScenario.variable': 'Variable Name {suffix}',
'apiScenario.valuePrefix': 'variable prefix', 'apiScenario.valuePrefix': 'Variable Prefix',
'apiScenario.value': 'variable', 'apiScenario.value': 'Variable Value',
'apiScenario.conditionValue': 'variable', 'apiScenario.conditionValue': 'Variable Value',
'apiScenario.msWhileVariableValue': 'variable', 'apiScenario.msWhileVariableValue': 'Variable Value',
'apiScenario.msWhileVariableScriptValue': 'expression', 'apiScenario.msWhileVariableScriptValue': 'Expression',
'apiScenario.condition': 'condition', 'apiScenario.condition': 'Condition',
'apiScenario.expression': 'expression', 'apiScenario.expression': 'Expression',
'apiScenario.equal': 'equal', 'apiScenario.equal': 'Equal',
'apiScenario.notEqualTo': 'not equal to', 'apiScenario.notEqualTo': 'Not Equal',
'apiScenario.greater': 'more than the', 'apiScenario.greater': 'Greater',
'apiScenario.less': 'less than', 'apiScenario.less': 'Less',
'apiScenario.greaterOrEqual': 'greater or equal to', 'apiScenario.greaterOrEqual': 'Greater or Equal',
'apiScenario.lessOrEqual': 'less than or equal to', 'apiScenario.lessOrEqual': 'Less or Equal',
'apiScenario.include': 'Include', 'apiScenario.include': 'Include',
'apiScenario.notInclude': 'Not included', 'apiScenario.notInclude': 'Not Include',
'apiScenario.null': 'Null', 'apiScenario.null': 'Null',
'apiScenario.notNull': 'Not Null', 'apiScenario.notNull': 'Not Null',
'apiScenario.range': 'Scope', 'apiScenario.range': 'Range',
'apiScenario.topStep': 'First level steps', 'apiScenario.topStep': 'Top-level Step',
'apiScenario.allStep': 'All substeps', 'apiScenario.allStep': 'All Substeps',
'apiScenario.saveAsApi': 'Save as new interface', 'apiScenario.saveAsApi': 'Save as New API',
'apiScenario.scenarioLevel': 'Scene level', 'apiScenario.scenarioLevel': 'Scenario Level',
'apiScenario.running': 'Executing', 'apiScenario.running': 'Running',
'apiScenario.unExecute': 'Not performed', 'apiScenario.unExecute': 'Not Executed',
'apiScenario.response': 'Response content', 'apiScenario.response': 'Response Content',
'apiScenario.quoteMode': 'Reference mode', 'apiScenario.quoteMode': 'Quote Mode',
'apiScenario.fullQuote': 'Full quote', 'apiScenario.fullQuote': 'Full Quote',
'apiScenario.stepQuote': 'Step reference', 'apiScenario.stepQuote': 'Step Quote',
'apiScenario.runRule': 'Parameter value rules', 'apiScenario.runRule': 'Parameter Value Rule',
'apiScenario.currentScenario': 'Prioritize current scene parameters', 'apiScenario.currentScenario': 'Priority: Current Scenario Parameters',
'apiScenario.sourceScenario': 'Priority source scene parameters', 'apiScenario.sourceScenario': 'Priority: Source Scenario Parameters',
'apiScenario.currentScenarioTip': 'When the current scene parameter does not exist, take', 'apiScenario.currentScenarioTip': 'When current scenario parameters do not exist, take',
'apiScenario.sourceScenarioTip': 'When the source scene parameter does not exist, take', 'apiScenario.sourceScenarioTip': 'When source scenario parameters do not exist, take',
'apiScenario.empty': 'Null value', 'apiScenario.empty': 'Empty Value',
'apiScenario.currentScenarioParams': 'Current scene parameters', 'apiScenario.currentScenarioParams': 'Current Scenario Parameters',
'apiScenario.sourceScenarioParams': 'Source scene parameters', 'apiScenario.sourceScenarioParams': 'Source Scenario Parameters',
'apiScenario.sourceScenarioEnv': 'Source scene environment', 'apiScenario.sourceScenarioEnv': 'Source Scenario Environment',
'apiScenario.valuePriority': 'Value priority', 'apiScenario.valuePriority': 'Value Priority:',
'apiScenario.currentScenarioAndNull': 'apiScenario.currentScenarioAndNull':
'Current step parameters > Current scene parameters > Current environment parameters > Null value', 'Current Step Parameters > Current Scenario Parameters > Current Environment Parameters > Empty Value',
'apiScenario.currentScenarioAndNullAndSourceEnv': 'apiScenario.currentScenarioAndNullAndSourceEnv':
'Current step parameters > Current scene parameters > Source environment parameters > Null value', 'Current Step Parameters > Current Scenario Parameters > Source Environment Parameters > Empty Value',
'apiScenario.currentScenarioAndSourceScenario': 'apiScenario.currentScenarioAndSourceScenario':
'Current step parameters > Current scene parameters > Current environment parameters > Source scene parameters', 'Current Step Parameters > Current Scenario Parameters > Current Environment Parameters > Source Scenario Parameters',
'apiScenario.currentScenarioAndSourceScenarioAndSourceEnv': 'apiScenario.currentScenarioAndSourceScenarioAndSourceEnv':
'Current step parameters > Current scene parameters > Source scene parameters > Source environment parameters', 'Current Step Parameters > Current Scenario Parameters > Source Scenario Parameters > Source Environment Parameters',
'apiScenario.sourceScenarioAndNull': 'Source Scene Parameters > Null', 'apiScenario.sourceScenarioAndNull': 'Source Scenario Parameters > Empty Value',
'apiScenario.sourceScenarioAndNullAndSourceEnv': 'Source Scene Parameters > Source Environment Parameters > Null', 'apiScenario.sourceScenarioAndNullAndSourceEnv':
'Source Scenario Parameters > Source Environment Parameters > Empty Value',
'apiScenario.sourceScenarioAndCurrentScenario': 'apiScenario.sourceScenarioAndCurrentScenario':
'Source scene parameters > Current step parameters > Current scene parameters > Current environment parameters', 'Source Scenario Parameters > Current Step Parameters > Current Scenario Parameters > Current Environment Parameters',
'apiScenario.sourceScenarioAndCurrentScenarioAndSourceEnv': 'apiScenario.sourceScenarioAndCurrentScenarioAndSourceEnv':
'Source scene parameters > Source environment parameters > Current step parameters > Current scene parameters', 'Source Scenario Parameters > Source Environment Parameters > Current Step Parameters > Current Scenario Parameters',
'apiScenario.fullQuoteTip': 'apiScenario.fullQuoteTip':
'Full quotation: Follow the source step content and step status changes, the step status cannot be adjusted', 'Full Quote: Follows the original step content and step status changes. Step status cannot be adjusted.',
'apiScenario.stepQuoteTip': 'apiScenario.stepQuoteTip':
'Step reference: only follow the content changes of the source step, and the step status can be adjusted', 'Step Quote: Only follows the original step content changes. Step status can be adjusted.',
'apiScenario.sourceScenarioEnvTip': 'Operating environment, including environmental parameters', 'apiScenario.sourceScenarioEnvTip': 'Runtime environment, including environment parameters',
'apiScenario.setSuccess': 'Setup successful', 'apiScenario.setSuccess': 'Set Successful',
'apiScenario.pleaseInputUrl': 'Please enter url', 'apiScenario.pleaseInputUrl': 'Please enter URL',
// 执行历史 // Execution History
'apiScenario.executeHistory.searchPlaceholder': 'Search by ID or name', 'apiScenario.executeHistory.searchPlaceholder': 'Search by ID or name',
'apiScenario.executeHistory.num': 'Number', 'apiScenario.executeHistory.num': 'No.',
'apiScenario.executeHistory.execution.triggerMode': 'Trigger mode', 'apiScenario.executeHistory.execution.triggerMode': 'Execution Mode',
'apiScenario.executeHistory.execution.status': 'Execution result', 'apiScenario.executeHistory.execution.status': 'Execution Result',
'apiScenario.executeHistory.execution.operator': 'Operator', 'apiScenario.executeHistory.execution.operator': 'Operator',
'apiScenario.executeHistory.execution.operatorTime': 'Operation time', 'apiScenario.executeHistory.execution.operatorTime': 'Operation Time',
'apiScenario.executeHistory.execution.operation': 'Execution result', 'apiScenario.executeHistory.execution.operation': 'Execution Result',
'apiScenario.executeHistory.status.pending': 'Pending', 'apiScenario.executeHistory.status.pending': 'Queued',
'apiScenario.executeHistory.status.running': 'Running', 'apiScenario.executeHistory.status.running': 'Running',
'apiScenario.executeHistory.status.rerunning': 'Rerunning', 'apiScenario.executeHistory.status.rerunning': 'Rerunning',
'apiScenario.executeHistory.status.error': 'Error', 'apiScenario.executeHistory.status.error': 'Failed',
'apiScenario.executeHistory.status.success': 'Success', 'apiScenario.executeHistory.status.success': 'Successful',
'apiScenario.executeHistory.status.fake.error': 'Fake error', 'apiScenario.executeHistory.status.fake.error': 'False Positive',
'apiScenario.executeHistory.status.fake.stopped': 'Stopped', 'apiScenario.executeHistory.status.fake.stopped': 'Stopped',
// 操作历史 // Operation History
'apiScenario.historyListTip': 'apiScenario.historyListTip':
'View and compare historical changes. According to the rules set by the administrator, the change history data will be automatically deleted.', 'View and compare historical modifications. According to administrator settings, change history data will be automatically deleted.',
'apiScenario.changeOrder': 'Change serial number', 'apiScenario.changeOrder': 'Change Order',
'apiScenario.type': 'Type', 'apiScenario.type': 'Type',
'apiScenario.operationUser': 'Operator', 'apiScenario.operationUser': 'Operator',
'apiScenario.updateTime': 'Update time', 'apiScenario.updateTime': 'Update Time',
// 回收站 // Recycle Bin
'api_scenario.recycle.recover': 'Recover', 'api_scenario.recycle.recover': 'Recover',
'api_scenario.recycle.recoveredSuccessfully': 'Recover success', 'api_scenario.recycle.recoveredSuccessfully': 'Recover Successful',
'api_scenario.recycle.list': 'Recycle list', 'api_scenario.recycle.list': 'Recycle Bin List',
'api_scenario.recycle.batchCleanOut': 'Delete', 'api_scenario.recycle.batchCleanOut': 'Permanently Delete',
'api_scenario.recycle.completedDeleteCaseTitle': 'Are you sure you want to completely delete {name}?', 'api_scenario.recycle.completedDeleteCaseTitle': 'Confirm Permanently Delete {name}?',
'api_scenario.recycle.cleanOutDeleteOnRecycleTip': 'api_scenario.recycle.cleanOutDeleteOnRecycleTip':
'After deletion, the scene cannot be restored, please operate with caution!', 'Deleting will permanently remove the scenario. Proceed with caution!',
'api_scenario.table.searchPlaceholder': 'Search by ID/Name/Tag', 'apiScenario.quoteTreeNoData': 'No quotable data available, switch projects to retrieve data',
'apiScenario.quoteTreeNoData': 'There is currently no reference data. You can switch projects to obtain data.',
'apiScenario.quoteTreeSearchTip': 'Enter module name to search', 'apiScenario.quoteTreeSearchTip': 'Enter module name to search',
'apiScenario.quoteTableSearchTip': 'Search by path or name', 'apiScenario.quoteTableSearchTip': 'Search by path or name',
'apiScenario.collapseAll': 'Collapse all submodules', 'apiScenario.collapseAll': 'Collapse All Submodules',
'apiScenario.expandAll': 'Expand all submodules', 'apiScenario.expandAll': 'Expand All Submodules',
'apiScenario.scriptOperationName': 'Script operation name', 'apiScenario.scriptOperationName': 'Script Operation Name',
'apiScenario.scriptOperationNamePlaceholder': 'Please enter the script operation name', 'apiScenario.scriptOperationNamePlaceholder': 'Please enter script operation name',
'apiScenario.setting.cookie.config': 'Cookie configuration', 'apiScenario.setting.cookie.config': 'Cookie Configuration',
'apiScenario.setting.environment.cookie': 'Environment Cookie', 'apiScenario.setting.environment.cookie': 'Environment Cookie',
'apiScenario.setting.share.cookie': 'Shared Cookie', 'apiScenario.setting.share.cookie': 'Shared Cookie',
'apiScenario.setting.run.config': 'Run configuration', 'apiScenario.setting.run.config': 'Run Configuration',
'apiScenario.setting.step.waitTime': 'Step wait time', 'apiScenario.setting.step.waitTime': 'Step Wait Time',
'apiScenario.setting.waitTime': 'Wait time', 'apiScenario.setting.waitTime': 'Wait Time',
'apiScenario.setting.step.rule': 'Step execution failure rule', 'apiScenario.setting.step.rule': 'Step Execution Failure Rule',
'apiScenario.setting.step.rule.ignore': 'Ignore error and continue execution', 'apiScenario.setting.step.rule.ignore': 'Ignore Error and Continue Execution',
'apiScenario.setting.step.rule.stop': 'Stop/end execution', 'apiScenario.setting.step.rule.stop': 'Stop/End Execution',
'apiScenario.setting.cookie.config.tip': 'apiScenario.setting.cookie.config.tip':
'When there are both global and scene variable cookies, shared cookies will overwrite both global and scene variable cookies', 'When both global variable cookie and scenario variable cookie exist, shared cookie will override global cookie and scenario variable cookie',
'apiScenario.setting.share.cookie.tip': 'apiScenario.setting.share.cookie.tip':
'As long as the system extracts the returned cookie information from the result of a certain step, the subsequent steps will use this cookie. If a cookie variable is added to the request, it will also be overwritten', 'If the system extracts cookie information from the result of a certain step, subsequent steps will use this cookie. If the request adds a Cookie variable, it will be overridden',
'apiScenario.setting.waitTime.tip': 'apiScenario.setting.waitTime.tip':
'When running a scenario, each step of the scenario will wait for a certain time after execution before triggering the next step to start execution', 'When running a scenario, each step of the scenario will wait for the specified time before triggering the next step',
// 定时任务 // Scheduled Task
'apiScenario.schedule.create': 'Create schedule', 'apiScenario.schedule.create': 'Create Scheduled Task',
'apiScenario.schedule.update': 'Update schedule', 'apiScenario.schedule.update': 'Update Scheduled Task',
'apiScenario.schedule.delete': 'Delete schedule', 'apiScenario.schedule.delete': 'Delete Scheduled Task',
'apiScenario.schedule.config.resource_pool': 'Resource pool', 'apiScenario.schedule.config.resource_pool': 'Run Resource Pool',
'apiScenario.schedule.task.status': 'Task status', 'apiScenario.schedule.task.status': 'Task Status',
'apiScenario.schedule.task.schedule': 'Trigger time', 'apiScenario.schedule.task.schedule': 'Task Trigger Time',
'apiScenario.schedule.abbreviation': 'SCHEDULE', 'apiScenario.schedule.abbreviation': 'Scheduled',
'apiScenario.schedule.task.status.tooltip.one': 'Open: execute schedule task', 'apiScenario.schedule.task.status.tooltip.one': 'Enabled: Execute the scheduled task',
'apiScenario.schedule.task.status.tooltip.two': 'Close: stop executing schedule task', 'apiScenario.schedule.task.status.tooltip.two': 'Disabled: Stop the scheduled task',
'apiScenario.schedule.table.tooltip.enable.one': 'Schedule is open', 'apiScenario.schedule.table.tooltip.enable.one': 'Scheduled task is enabled',
'apiScenario.schedule.table.tooltip.enable.two': 'Next trigger time{time}', 'apiScenario.schedule.table.tooltip.enable.two': 'Next run time: {time}',
'apiScenario.schedule.table.tooltip.disable': 'Schedule is close', 'apiScenario.schedule.table.tooltip.disable': 'Scheduled task is disabled',
}; };

View File

@ -31,6 +31,7 @@ export default {
'apiScenario.params.paramValue': '参数值', 'apiScenario.params.paramValue': '参数值',
'apiScenario.params.tag': '标签', 'apiScenario.params.tag': '标签',
'apiScenario.params.desc': '描述', 'apiScenario.params.desc': '描述',
'apiScenario.params.typeTooltip': 'Json仅支持 UI 测试',
'apiScenario.table.columns.name': '场景名称', 'apiScenario.table.columns.name': '场景名称',
'apiScenario.table.columns.level': '场景等级', 'apiScenario.table.columns.level': '场景等级',
'apiScenario.table.columns.status': '状态', 'apiScenario.table.columns.status': '状态',

View File

@ -72,7 +72,7 @@ export default {
'caseManagement.featureCase.fileName': '文件名', 'caseManagement.featureCase.fileName': '文件名',
'caseManagement.featureCase.description': '描述', 'caseManagement.featureCase.description': '描述',
'caseManagement.featureCase.tags': '标签', 'caseManagement.featureCase.tags': '标签',
'caseManagement.featureCase.enableTags': `开启:新增标签,关闭:覆盖原有标签`, 'caseManagement.featureCase.enableTags': '开启:新增标签',
'caseManagement.featureCase.closeTags': '关闭:覆盖原有标签', 'caseManagement.featureCase.closeTags': '关闭:覆盖原有标签',
'caseManagement.featureCase.appendTag': '追加标签', 'caseManagement.featureCase.appendTag': '追加标签',
'caseManagement.featureCase.batchEdit': '批量编辑 (已选 { number } 条用例)', 'caseManagement.featureCase.batchEdit': '批量编辑 (已选 { number } 条用例)',