fix(all): 修复中级 bug
This commit is contained in:
parent
9e04fb2a47
commit
cbfb5e2221
|
@ -62,9 +62,10 @@ import {
|
|||
ScenarioDetail,
|
||||
ScenarioHistoryItem,
|
||||
ScenarioHistoryPageParams,
|
||||
ScenarioStepResourceInfo,
|
||||
} from '@/models/apiTest/scenario';
|
||||
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 } 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) {
|
||||
return MSR.get({ url: GetStepProjectInfoUrl, params: id });
|
||||
export function getStepProjectInfo(id: string, type: ScenarioStepType) {
|
||||
return MSR.get<ScenarioStepResourceInfo>({ url: GetStepProjectInfoUrl, params: id, data: { resourceType: type } });
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export const GetSystemRequestUrl = '/api/scenario/get/system-request'; // 获取
|
|||
export const FollowScenarioUrl = '/api/scenario/follow'; // 关注/取消关注接口场景
|
||||
export const ScenarioScheduleConfigUrl = '/api/scenario/schedule-config'; // 场景定时任务
|
||||
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 BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景
|
||||
export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</a-form-item>
|
||||
<template v-else>
|
||||
<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
|
||||
v-if="props.fileSaveAsSourceId"
|
||||
v-model:visible="saveFilePopoverVisible"
|
||||
|
@ -85,7 +85,7 @@
|
|||
:size="props.tagSize"
|
||||
class="m-0 border-none p-0"
|
||||
:self-style="{ backgroundColor: 'transparent !important' }"
|
||||
:closable="data.value !== '__arco__more'"
|
||||
:closable="data.value !== '__arco__more' || props.disabled"
|
||||
@close="handleClose(data)"
|
||||
>
|
||||
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
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>
|
||||
<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>
|
||||
<MsButton type="text" @click="getMockValue">{{ t('ms.paramsInput.previewClick') }}</MsButton>
|
||||
</a-spin>
|
||||
|
@ -510,7 +510,7 @@
|
|||
}
|
||||
if (valueArr[1]) {
|
||||
// 匹配函数名和参数
|
||||
const functionRegex = /([a-zA-Z]+)(?:\(([^)]*)\))?/;
|
||||
const functionRegex = /([a-zA-Z0-9]+)(?:\(([^)]*)\))?/;
|
||||
const functionMatch = valueArr[1].match(functionRegex);
|
||||
|
||||
if (functionMatch) {
|
||||
|
@ -654,6 +654,7 @@
|
|||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
max-width: 400px;
|
||||
}
|
||||
.ms-params-input-setting-trigger {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
:data="data"
|
||||
:data="filterTreeData"
|
||||
class="ms-tree"
|
||||
:allow-drop="handleAllowDrop"
|
||||
@drag-start="onDragStart"
|
||||
|
@ -197,13 +197,13 @@
|
|||
init(true);
|
||||
});
|
||||
|
||||
const originTreeData = ref<MsTreeNodeData[]>([]); // 初始化时全量的树数据或在非搜索情况下更新后的全量树数据
|
||||
const filterTreeData = ref<MsTreeNodeData[]>([]); // 初始化时全量的树数据或在非搜索情况下更新后的全量树数据
|
||||
|
||||
watch(
|
||||
() => data.value,
|
||||
(val) => {
|
||||
if (!props.keyword) {
|
||||
originTreeData.value = cloneDeep(val);
|
||||
filterTreeData.value = cloneDeep(val);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -220,7 +220,7 @@
|
|||
const search = (_data: MsTreeNodeData[]) => {
|
||||
const result: MsTreeNodeData[] = [];
|
||||
_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 });
|
||||
} else if (item[props.fieldNames.children]) {
|
||||
const filterData = search(item[props.fieldNames.children]);
|
||||
|
@ -237,13 +237,17 @@
|
|||
return result;
|
||||
};
|
||||
|
||||
return search(originTreeData.value);
|
||||
return search(data.value);
|
||||
}
|
||||
|
||||
// 防抖搜索
|
||||
const updateDebouncedSearch = debounce(() => {
|
||||
if (props.keyword) {
|
||||
data.value = searchData(props.keyword);
|
||||
filterTreeData.value = searchData(props.keyword);
|
||||
nextTick(() => {
|
||||
// 展开所有搜索到的节点(expandedKeys控制了节点展开,但是节点展开折叠图标未变化,需要手动触发展开事件)
|
||||
treeRef.value?.expandNode(expandedKeys.value);
|
||||
});
|
||||
}
|
||||
}, props.searchDebounce);
|
||||
|
||||
|
@ -251,7 +255,7 @@
|
|||
() => props.keyword,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
data.value = cloneDeep(originTreeData.value);
|
||||
filterTreeData.value = cloneDeep(data.value);
|
||||
} else {
|
||||
updateDebouncedSearch();
|
||||
}
|
||||
|
|
|
@ -270,6 +270,7 @@
|
|||
contextmenu: !props.readOnly, // 只读模式下禁用右键菜单
|
||||
...props,
|
||||
language: props.language.toLowerCase(),
|
||||
theme: currentTheme.value,
|
||||
});
|
||||
|
||||
// 监听值的变化
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { XpathNode } from './types';
|
||||
import * as XmlBeautify from 'xml-beautify';
|
||||
import XmlBeautify from 'xml-beautify';
|
||||
|
||||
const props = defineProps<{
|
||||
xmlString: string;
|
||||
|
@ -98,7 +98,7 @@
|
|||
isValidXml.value = true;
|
||||
parsedXml.value = xmlDoc;
|
||||
// 先将 XML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
||||
flattenedXml.value = new XmlBeautify({ parser: DOMParser })
|
||||
flattenedXml.value = new XmlBeautify()
|
||||
.beautify(props.xmlString)
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<a-table
|
||||
v-bind="{ ...$attrs, ...scrollObj }"
|
||||
:row-class="getRowClass"
|
||||
:column-resizable="false"
|
||||
:column-resizable="true"
|
||||
:span-method="spanMethod"
|
||||
:columns="currentColumns"
|
||||
:expanded-keys="props.expandedKeys"
|
||||
|
@ -211,7 +211,7 @@
|
|||
class="mt-[16px] flex h-[32px] flex-row flex-nowrap items-center"
|
||||
: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 }) }}
|
||||
<a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">
|
||||
{{ t('msTable.batch.clear') }}
|
||||
|
|
|
@ -271,7 +271,7 @@ export interface ForEachController {
|
|||
variable: string; // 变量名
|
||||
}
|
||||
export interface CountController {
|
||||
loops: number; // 循环次数
|
||||
loops: string; // 循环次数
|
||||
loopTime: number; // 循环间隔时间
|
||||
}
|
||||
export interface WhileScript {
|
||||
|
@ -478,3 +478,11 @@ export interface ApiScenarioBatchOptionResult {
|
|||
success: number;
|
||||
error: number;
|
||||
}
|
||||
// 场景跨项目步骤资源信息
|
||||
export interface ScenarioStepResourceInfo {
|
||||
id: string;
|
||||
num: number;
|
||||
name: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
|
|
@ -545,17 +545,24 @@ export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string | number
|
|||
* @param treeArr 目标树
|
||||
* @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);
|
||||
function deleteNodesInTree(tree: TreeNode<T>[]): void {
|
||||
for (let i = tree.length - 1; i >= 0; i--) {
|
||||
const node = tree[i];
|
||||
if (targetKeysSet.has(node[customKey])) {
|
||||
tree.splice(i, 1); // 直接删除当前节点
|
||||
targetKeysSet.delete(node[customKey]); // 删除后从集合中移除
|
||||
// 重新调整剩余子节点的 sort 序号
|
||||
for (let j = i; j < tree.length; j++) {
|
||||
tree[j].sort = j + 1;
|
||||
if (deleteCondition && deleteCondition(node, node.parent)) {
|
||||
tree.splice(i, 1); // 直接删除当前节点
|
||||
targetKeysSet.delete(node[customKey]); // 删除后从集合中移除
|
||||
// 重新调整剩余子节点的 sort 序号
|
||||
for (let j = i; j < tree.length; j++) {
|
||||
tree[j].sort = j + 1;
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(node.children)) {
|
||||
deleteNodesInTree(node.children); // 递归删除子节点
|
||||
|
@ -634,12 +641,13 @@ export const getHashParameters = (): Record<string, string> => {
|
|||
* @returns
|
||||
*/
|
||||
export const getGenerateId = () => {
|
||||
const timestamp = new Date().getTime().toString();
|
||||
const randomDigits = Math.floor(Math.random() * 10000)
|
||||
.toString()
|
||||
.padStart(4, '0');
|
||||
const generateId = timestamp + randomDigits;
|
||||
return generateId.substring(0, 16);
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const r = (Math.random() * 16) | 0;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,7 +46,12 @@
|
|||
</div>
|
||||
<!-- 前后置请求结束 -->
|
||||
<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 v-if="hasAnyPermission(['PROJECT_CUSTOM_FUNCTION:READ'])" :value="true">
|
||||
{{ t('apiTestDebug.quote') }}
|
||||
|
@ -58,6 +63,7 @@
|
|||
class="mr-2"
|
||||
size="small"
|
||||
type="line"
|
||||
:disabled="props.disabled"
|
||||
@change="emit('change')"
|
||||
/>
|
||||
{{ t('apiTestDebug.preconditionAssociatedSceneResult') }}
|
||||
|
@ -79,6 +85,7 @@
|
|||
v-model:model-value="condition.name"
|
||||
:placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')"
|
||||
:max-length="255"
|
||||
:disabled="props.disabled"
|
||||
size="small"
|
||||
@press-enter="isShowEditScriptNameInput = false"
|
||||
@blur="isShowEditScriptNameInput = false"
|
||||
|
@ -201,36 +208,39 @@
|
|||
v-permission="['PROJECT_CUSTOM_FUNCTION:READ']"
|
||||
type="text"
|
||||
class="font-medium"
|
||||
:disabled="props.disabled"
|
||||
@click="showQuoteDrawer = true"
|
||||
>
|
||||
{{ t('apiTestDebug.quote') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
<div v-show="showParameters() || showScript()" class="h-[calc(100%-47px)] min-h-[300px]">
|
||||
<a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit">
|
||||
<a-radio v-if="showParameters()" value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio>
|
||||
<a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio>
|
||||
</a-radio-group>
|
||||
<paramTable
|
||||
v-if="commonScriptShowType === 'parameters'"
|
||||
v-model:params="scriptParams"
|
||||
:scroll="{ x: '100%' }"
|
||||
:columns="scriptColumns"
|
||||
:height-used="heightUsed"
|
||||
:selectable="false"
|
||||
/>
|
||||
<div v-show="commonScriptShowType === 'scriptContent'" class="h-[calc(100%-76px)]">
|
||||
<div v-if="showParameters() || showScript()" class="min-h-[300px]">
|
||||
<div>
|
||||
<a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit">
|
||||
<a-radio v-if="showParameters()" value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio>
|
||||
<a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="h-[calc(100%-76px)]">
|
||||
<paramTable
|
||||
v-if="commonScriptShowType === 'parameters'"
|
||||
v-model:params="scriptParams"
|
||||
:disabled-param-value="props.disabled"
|
||||
:scroll="{ x: '100%' }"
|
||||
:columns="scriptColumns"
|
||||
:height-used="heightUsed"
|
||||
:selectable="false"
|
||||
/>
|
||||
<MsCodeEditor
|
||||
v-if="condition.commonScriptInfo"
|
||||
v-else-if="commonScriptShowType === 'scriptContent' && condition.commonScriptInfo"
|
||||
v-model:model-value="condition.commonScriptInfo.script"
|
||||
theme="vs"
|
||||
height="100%"
|
||||
:height="props.sqlCodeEditorHeight || '100%'"
|
||||
:language="condition.commonScriptInfo.scriptLanguage || LanguageEnum.BEANSHELL_JSR233"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
read-only
|
||||
>
|
||||
</MsCodeEditor>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -289,7 +299,9 @@
|
|||
v-model:model-value="condition.variableNames"
|
||||
:max-length="255"
|
||||
: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')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -350,6 +362,7 @@
|
|||
ref="extractParamsTableRef"
|
||||
:params="condition.extractors"
|
||||
:disabled-except-param="props.disabled"
|
||||
:disabled-param-value="props.disabled"
|
||||
:default-param-item="defaultExtractParamItem"
|
||||
:columns="extractParamsColumns"
|
||||
:selectable="false"
|
||||
|
@ -411,7 +424,7 @@
|
|||
>
|
||||
<template #content>
|
||||
<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">
|
||||
{{ t('common.cancel') }}
|
||||
</a-button>
|
||||
|
@ -426,7 +439,11 @@
|
|||
</paramTable>
|
||||
</div>
|
||||
</div>
|
||||
<quoteSqlSourceDrawer v-model:visible="quoteSqlSourceDrawerVisible" @apply="handleQuoteSqlSourceApply" />
|
||||
<quoteSqlSourceDrawer
|
||||
v-model:visible="quoteSqlSourceDrawerVisible"
|
||||
:selected-key="condition.dataSourceId"
|
||||
@apply="handleQuoteSqlSourceApply"
|
||||
/>
|
||||
<fastExtraction
|
||||
v-model:visible="fastExtractionVisible"
|
||||
:response="props.response"
|
||||
|
@ -512,6 +529,7 @@
|
|||
requestRadioTextProps?: Record<string, any>; // 前后置请求前后置按钮文本
|
||||
showPrePostRequest?: boolean; // 是否展示前后置请求忽略
|
||||
totalList?: ExecuteConditionProcessor[]; // 总列表
|
||||
sqlCodeEditorHeight?: string; // sql脚本编辑器高度
|
||||
}>(),
|
||||
{
|
||||
showAssociatedScene: false,
|
||||
|
@ -530,27 +548,34 @@
|
|||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
const condition = useVModel(props, 'data', emit);
|
||||
|
||||
watchEffect(() => {
|
||||
if (condition.value.processorType === RequestConditionProcessor.SQL && condition.value.dataSourceId) {
|
||||
// 如果是SQL类型的条件且已选数据源,需要根据环境切换数据源
|
||||
const dataSourceItem = currentEnvConfig?.value.dataSources.find(
|
||||
(item) => item.dataSource === condition.value.dataSourceName
|
||||
);
|
||||
if (dataSourceItem) {
|
||||
// 每次初始化都去查找一下最新的数据源,因为切换环境的时候数据源也需要切换
|
||||
condition.value.dataSourceName = dataSourceItem.dataSource;
|
||||
condition.value.dataSourceId = dataSourceItem.id;
|
||||
} else if (currentEnvConfig && currentEnvConfig.value.dataSources.length > 0) {
|
||||
// 如果没有找到,就默认取第一个数据源
|
||||
condition.value.dataSourceName = currentEnvConfig.value.dataSources[0].dataSource;
|
||||
condition.value.dataSourceId = currentEnvConfig.value.dataSources[0].id;
|
||||
} else {
|
||||
// 如果没有数据源,就清除已选的数据源
|
||||
condition.value.dataSourceName = '';
|
||||
condition.value.dataSourceId = '';
|
||||
watch(
|
||||
() => currentEnvConfig?.value,
|
||||
() => {
|
||||
if (condition.value.processorType === RequestConditionProcessor.SQL && condition.value.dataSourceId) {
|
||||
// 如果是SQL类型的条件且已选数据源,需要根据环境切换数据源
|
||||
const dataSourceItem = currentEnvConfig?.value.dataSources.find(
|
||||
(item) => item.dataSource === condition.value.dataSourceName
|
||||
);
|
||||
if (currentEnvConfig?.value.dataSources.length === 0) {
|
||||
// 如果没有数据源,就清除已选的数据源
|
||||
condition.value.dataSourceName = '';
|
||||
condition.value.dataSourceId = '';
|
||||
} else if (dataSourceItem) {
|
||||
// 每次初始化都去查找一下最新的数据源,因为切换环境的时候数据源也需要切换
|
||||
condition.value.dataSourceName = dataSourceItem.dataSource;
|
||||
condition.value.dataSourceId = dataSourceItem.id;
|
||||
} else if (currentEnvConfig && currentEnvConfig.value.dataSources.length > 0) {
|
||||
// 如果没有找到,就默认取第一个数据源
|
||||
condition.value.dataSourceName = currentEnvConfig.value.dataSources[0].dataSource;
|
||||
condition.value.dataSourceId = currentEnvConfig.value.dataSources[0].id;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// 是否显示脚本名称编辑框
|
||||
const isShowEditScriptNameInput = ref(false);
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
:show-associated-scene="props.showAssociatedScene"
|
||||
:show-pre-post-request="props.showPrePostRequest"
|
||||
:request-radio-text-props="props.requestRadioTextProps"
|
||||
:sql-code-editor-height="props.sqlCodeEditorHeight"
|
||||
@copy="copyListItem"
|
||||
@delete="deleteListItem"
|
||||
@change="emit('change')"
|
||||
|
@ -55,8 +56,8 @@
|
|||
import { conditionTypeNameMap } from '@/config/apiTest';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { ConditionType, ExecuteConditionProcessor } from '@/models/apiTest/common';
|
||||
import { RequestConditionProcessor } from '@/enums/apiEnum';
|
||||
import { ConditionType, ExecuteConditionProcessor, RegexExtract } from '@/models/apiTest/common';
|
||||
import { RequestConditionProcessor, RequestExtractExpressionEnum, RequestExtractScope } from '@/enums/apiEnum';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -69,6 +70,7 @@
|
|||
response?: string; // 响应内容
|
||||
showAssociatedScene?: boolean;
|
||||
showPrePostRequest?: boolean; // 是否展示前后置请求忽略选项
|
||||
sqlCodeEditorHeight?: string;
|
||||
}>(),
|
||||
{
|
||||
showAssociatedScene: false,
|
||||
|
@ -201,7 +203,6 @@
|
|||
if (isEXTRACT) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.value.push({
|
||||
id,
|
||||
processorType: RequestConditionProcessor.EXTRACT,
|
||||
|
@ -228,6 +229,11 @@
|
|||
hasNoIdItem = true;
|
||||
return {
|
||||
...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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -89,10 +89,10 @@
|
|||
(val) => {
|
||||
if (!val) {
|
||||
currentEnv.value = (envOptions.value[0]?.value as string) || '';
|
||||
nextTick(() => {
|
||||
initEnvironment();
|
||||
});
|
||||
}
|
||||
nextTick(() => {
|
||||
initEnvironment();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<template #typeTitle="{ columnConfig }">
|
||||
<div class="flex items-center text-[var(--color-text-3)]">
|
||||
{{ t('apiTestDebug.paramType') }}
|
||||
<a-tooltip :content="columnConfig.typeTitleTooltip" position="right">
|
||||
<a-tooltip :content="columnConfig.typeTitleTooltip" :disabled="!columnConfig.typeTitleTooltip" position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
|
@ -59,7 +59,7 @@
|
|||
<template #extractValueTitle>
|
||||
<div class="flex items-center text-[var(--color-text-3)]">
|
||||
{{ t('apiTestDebug.extractValueByColumn') }}
|
||||
<a-tooltip :content="t('caseManagement.caseReview.passRateTip')" position="right">
|
||||
<a-tooltip :content="t('apiTestDebug.extractValueTitleTip')" position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
|
@ -211,7 +211,7 @@
|
|||
<MsAddAttachment
|
||||
v-else-if="record.paramType === RequestParamsType.FILE"
|
||||
v-model:file-list="record.files"
|
||||
:disabled="props.disabledExceptParam"
|
||||
:disabled="props.disabledParamValue"
|
||||
mode="input"
|
||||
:multiple="true"
|
||||
:fields="{
|
||||
|
@ -819,7 +819,6 @@
|
|||
() => props.params,
|
||||
(arr) => {
|
||||
if (arr.length > 0) {
|
||||
let hasNoIdItem = false; // 是否有没有id的项,用以判断是否是后台数据初始化表格
|
||||
paramsData.value = arr.map((item, i) => {
|
||||
if (!item) {
|
||||
// 批量添加过来的数据最后一行会是 undefined
|
||||
|
@ -830,7 +829,6 @@
|
|||
}
|
||||
if (!item.id) {
|
||||
// 后台存储无id,渲染时需要手动添加一次
|
||||
hasNoIdItem = true;
|
||||
return {
|
||||
...item,
|
||||
id: new Date().getTime() + i,
|
||||
|
@ -838,7 +836,11 @@
|
|||
}
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
|
@ -1033,7 +1035,7 @@
|
|||
*/
|
||||
function handleSearchParams(val: string, item: FormTableColumn) {
|
||||
item.autoCompleteParams = item.autoCompleteParams?.map((e) => {
|
||||
e.isShow = (e.label || '').includes(val);
|
||||
e.isShow = (e.label || '').toLowerCase().includes(val.toLowerCase());
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:response="props.response"
|
||||
:disabled="props.disabled"
|
||||
:height-used="heightUsed"
|
||||
:sql-code-editor-height="props.sqlCodeEditorHeight"
|
||||
@change="emit('change')"
|
||||
>
|
||||
<template v-if="props.isDefinition" #titleRight>
|
||||
|
@ -44,6 +45,7 @@
|
|||
isDefinition?: boolean; // 是否是定义页面
|
||||
isScenario?: boolean; // 是否是场景页面
|
||||
disabled?: boolean;
|
||||
sqlCodeEditorHeight?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:params', params: ExecuteConditionProcessor[]): void;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
v-model:list="innerConfig.processors"
|
||||
:disabled="props.disabled"
|
||||
:condition-types="conditionTypes"
|
||||
:sql-code-editor-height="props.sqlCodeEditorHeight"
|
||||
add-text="apiTestDebug.precondition"
|
||||
@change="emit('change')"
|
||||
>
|
||||
|
@ -39,6 +40,7 @@
|
|||
isDefinition?: boolean; // 是否是定义页面
|
||||
isScenario?: boolean; // 是否是场景页面
|
||||
disabled?: boolean;
|
||||
sqlCodeEditorHeight?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:config', params: ExecuteConditionConfig): void;
|
||||
|
|
|
@ -202,4 +202,6 @@ export default {
|
|||
'apiTestDebug.testSuccess': 'Test success',
|
||||
'apiTestDebug.searchByDataBaseName': 'Search by data source name',
|
||||
'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',
|
||||
};
|
||||
|
|
|
@ -189,4 +189,5 @@ export default {
|
|||
'apiTestDebug.testSuccess': '测试成功',
|
||||
'apiTestDebug.searchByDataBaseName': '按数据源名称搜索',
|
||||
'apiTestDebug.regexMatchRules': '表达式匹配规则',
|
||||
'apiTestDebug.extractValueTitleTip': '输入按列存储中的列名和对应的数值,如提取name列的第一个值则输入name_1',
|
||||
};
|
||||
|
|
|
@ -188,7 +188,7 @@
|
|||
v-model:model-value="importForm.name"
|
||||
:placeholder="t('apiTestManagement.taskNamePlaceholder')"
|
||||
:max-length="255"
|
||||
class="flex-1"
|
||||
class="w-[550px]"
|
||||
></a-input>
|
||||
<MsButton type="text" @click="taskDrawerVisible = true">
|
||||
{{ t('apiTestManagement.timeTaskList') }}
|
||||
|
@ -204,7 +204,7 @@
|
|||
<a-input
|
||||
v-model:model-value="importForm.swaggerUrl"
|
||||
:placeholder="t('apiTestManagement.urlImportPlaceholder')"
|
||||
class="w-[700px]"
|
||||
class="w-[550px]"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
@ -246,7 +246,7 @@
|
|||
<a-tree-select
|
||||
v-model:modelValue="importForm.moduleId"
|
||||
:data="moduleTree"
|
||||
class="w-[436px]"
|
||||
class="w-[500px]"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
allow-search
|
||||
>
|
||||
|
@ -370,6 +370,7 @@
|
|||
visible: boolean;
|
||||
moduleTree: ModuleTreeNode[];
|
||||
popupContainer?: string;
|
||||
activeModule: string;
|
||||
}>();
|
||||
const emit = defineEmits(['update:visible', 'done']);
|
||||
|
||||
|
@ -391,8 +392,8 @@
|
|||
const defaultForm: ImportApiDefinitionRequest = {
|
||||
platform: RequestImportFormat.SWAGGER,
|
||||
name: '',
|
||||
moduleId: '',
|
||||
coverData: true,
|
||||
moduleId: 'root',
|
||||
coverData: false,
|
||||
syncCase: true,
|
||||
coverModule: false,
|
||||
swaggerUrl: '',
|
||||
|
@ -406,6 +407,19 @@
|
|||
};
|
||||
const importForm = ref({ ...defaultForm });
|
||||
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 disabledConfirm = computed(() => {
|
||||
if (importForm.value.type === RequestImportType.API) {
|
||||
|
|
|
@ -258,14 +258,18 @@
|
|||
style="padding-top: 10px"
|
||||
>
|
||||
<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="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<a-tooltip>
|
||||
<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>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch">
|
||||
|
|
|
@ -321,14 +321,18 @@
|
|||
style="padding-top: 10px"
|
||||
>
|
||||
<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="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<a-tooltip>
|
||||
<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>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a-button type="secondary" :disabled="batchEditLoading" @click="cancelBatchEdit">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</a-tooltip>
|
||||
</template>
|
||||
</MsEditableTab>
|
||||
<environmentSelect ref="environmentSelectRef" />
|
||||
<environmentSelect ref="environmentSelectRef" v-model:current-env="activeApiTab.environmentId" />
|
||||
</div>
|
||||
<api
|
||||
v-show="(activeApiTab.id === 'all' && currentTab === 'api') || activeApiTab.type === 'api'"
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
<importApi
|
||||
v-model:visible="importDrawerVisible"
|
||||
:module-tree="folderTree"
|
||||
:active-module="activeModule"
|
||||
popup-container="#managementContainer"
|
||||
@done="handleImportDone"
|
||||
/>
|
||||
|
|
|
@ -43,9 +43,11 @@
|
|||
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
|
||||
class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]"
|
||||
>
|
||||
<div class="text-[14px] font-normal text-[var(--color-text-4)]">
|
||||
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
|
||||
</div>
|
||||
<a-tooltip :content="currentEnvConfig?.name" :disabled="!currentEnvConfig?.name">
|
||||
<div class="one-line-text max-w-[250px] text-[14px] font-normal text-[var(--color-text-4)]">
|
||||
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-select
|
||||
v-model:model-value="requestVModel.customizeRequestEnvEnable"
|
||||
class="w-[150px]"
|
||||
|
@ -119,27 +121,39 @@
|
|||
</a-input-group>
|
||||
</div>
|
||||
<div v-permission="[props.permissionMap?.execute]">
|
||||
<a-dropdown-button
|
||||
v-if="hasLocalExec"
|
||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
class="exec-btn"
|
||||
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||
@select="execute"
|
||||
<template v-if="hasLocalExec">
|
||||
<a-dropdown-button
|
||||
v-if="!requestVModel.executeLoading"
|
||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
class="exec-btn"
|
||||
@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') }}
|
||||
</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>
|
||||
<a-input
|
||||
|
@ -201,7 +215,7 @@
|
|||
<httpHeader
|
||||
v-if="requestVModel.activeTab === RequestComposition.HEADER"
|
||||
v-model:params="requestVModel.headers"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-param-value="!isEditableApi && !isEditableParamValue"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:layout="activeLayout"
|
||||
:second-box-height="secondBoxHeight"
|
||||
|
@ -211,7 +225,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.BODY"
|
||||
v-model:params="requestVModel.body"
|
||||
:layout="activeLayout"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-param-value="!isEditableApi && !isEditableParamValue"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
:upload-temp-file-api="uploadTempFile"
|
||||
|
@ -224,7 +238,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
|
||||
v-model:params="requestVModel.query"
|
||||
:layout="activeLayout"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-param-value="!isEditableApi && !isEditableParamValue"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
|
@ -233,7 +247,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.REST"
|
||||
v-model:params="requestVModel.rest"
|
||||
:layout="activeLayout"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-param-value="!isEditableApi && !isEditableParamValue"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
|
@ -554,11 +568,15 @@
|
|||
|
||||
// 复制 api 只要加载过一次后就会保存,所以 props.request 是不为空的
|
||||
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined);
|
||||
// 全可编辑接口
|
||||
const isEditableApi = computed(
|
||||
() =>
|
||||
!props.step?.isQuoteScenarioStep &&
|
||||
(_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 isInitPluginForm = ref(false);
|
||||
|
@ -999,7 +1017,7 @@
|
|||
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
|
||||
name: requestVModel.value.name,
|
||||
unSaved: requestVModel.value.unSaved,
|
||||
customizeRequest: props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.request,
|
||||
customizeRequest: requestVModel.value.customizeRequest,
|
||||
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -67,27 +67,39 @@
|
|||
allow-clear
|
||||
/>
|
||||
<div v-permission="[props.permissionMap?.execute]">
|
||||
<a-dropdown-button
|
||||
v-if="hasLocalExec"
|
||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
class="exec-btn"
|
||||
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||
@select="execute"
|
||||
<template v-if="hasLocalExec">
|
||||
<a-dropdown-button
|
||||
v-if="!requestVModel.executeLoading"
|
||||
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
|
||||
class="exec-btn"
|
||||
@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') }}
|
||||
</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 class="px-[16px]">
|
||||
|
@ -443,8 +455,8 @@
|
|||
}
|
||||
);
|
||||
|
||||
// 复制 api 只要加载过一次后就会保存,所以 props.request 是不为空的
|
||||
const isEditableApi = computed(() => _stepType.value.isCopyCase);
|
||||
// 非引用场景下的复制 case 可更改
|
||||
const isEditableApi = computed(() => !activeStep.value?.isQuoteScenarioStep && _stepType.value.isCopyCase);
|
||||
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
||||
const isInitPluginForm = ref(false);
|
||||
const loading = ref(false);
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
fullScenarioArr.push(...res);
|
||||
});
|
||||
if (refType === ScenarioStepRefType.COPY) {
|
||||
// 复制需要递归给每个节点生成新的uniqueId,并记录copyFromStepId
|
||||
fullScenarioArr = mapTree<MsTableDataItem<ApiScenarioTableItem>>(fullScenarioArr, (node) => {
|
||||
const id = getGenerateId();
|
||||
return {
|
||||
|
@ -275,6 +276,7 @@
|
|||
);
|
||||
handleCancel();
|
||||
} else {
|
||||
// 引用只需要给场景节点生成新的步骤 id,内部步骤只需要生成uniqueId作为前端渲染 id
|
||||
fullScenarioArr = fullScenarioArr.map((e) => {
|
||||
const id = getGenerateId();
|
||||
return {
|
||||
|
|
|
@ -79,6 +79,9 @@
|
|||
<template #status="{ record }">
|
||||
<apiStatus :status="record.status" size="small" />
|
||||
</template>
|
||||
<template #priority="{ record }">
|
||||
<caseLevel :case-level="record.priority" />
|
||||
</template>
|
||||
</ms-base-table>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -90,6 +93,7 @@
|
|||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import { MsTableDataItem } from '@/components/pure/ms-table/type';
|
||||
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 apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
|
@ -475,7 +479,7 @@
|
|||
case 'scenario':
|
||||
default:
|
||||
routeName = ApiTestRouteEnum.API_TEST_SCENARIO;
|
||||
query.sId = id;
|
||||
query.id = id;
|
||||
break;
|
||||
}
|
||||
openNewPage(routeName, query);
|
||||
|
|
|
@ -98,7 +98,8 @@
|
|||
step.stepType === ScenarioStepType.LOOP_CONTROLLER &&
|
||||
step.config.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT &&
|
||||
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) => {
|
||||
|
|
|
@ -19,11 +19,12 @@
|
|||
v-model="scriptName"
|
||||
:placeholder="t('apiScenario.scriptOperationNamePlaceholder')"
|
||||
:max-length="255"
|
||||
:disabled="isReadonly"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<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 v-if="currentResponse?.console" class="p-[8px]">
|
||||
<div class="mb-[8px] font-medium text-[var(--color-text-1)]">{{ t('apiScenario.executionResult') }}</div>
|
||||
|
@ -88,6 +89,7 @@
|
|||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
const isReadonly = computed(() => props.step?.isQuoteScenarioStep);
|
||||
const currentLoop = ref(1);
|
||||
const currentResponse = computed(() => {
|
||||
if (props.step?.uniqueId) {
|
||||
|
|
|
@ -16,7 +16,7 @@ export const defaultLoopController = {
|
|||
variable: '', // 变量名
|
||||
},
|
||||
msCountController: {
|
||||
loops: 1, // 循环次数
|
||||
loops: '1', // 循环次数
|
||||
loopTime: 0, // 循环间隔时间
|
||||
},
|
||||
whileController: {
|
||||
|
|
|
@ -10,14 +10,7 @@
|
|||
@press-enter="loadExecuteHistoryList"
|
||||
/>
|
||||
</div>
|
||||
<ms-base-table
|
||||
v-bind="propsRes"
|
||||
:first-column-width="44"
|
||||
:secnario-id="props.scenarioId"
|
||||
no-disable
|
||||
filter-icon-align-left
|
||||
v-on="propsEvent"
|
||||
>
|
||||
<ms-base-table v-bind="propsRes" no-disable filter-icon-align-left v-on="propsEvent">
|
||||
<template #num="{ record }">
|
||||
<span type="text" class="px-0">{{ record.num }}</span>
|
||||
</template>
|
||||
|
@ -27,12 +20,12 @@
|
|||
trigger="click"
|
||||
@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">
|
||||
{{ t(columnConfig.title as string) }}
|
||||
</div>
|
||||
<icon-down :class="triggerModeFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
|
||||
</a-button>
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<div class="arco-table-filters-content">
|
||||
<div class="ml-[6px] flex items-center justify-start px-[6px] py-[2px]">
|
||||
|
@ -146,10 +139,6 @@
|
|||
slotName: 'triggerMode',
|
||||
showTooltip: true,
|
||||
titleSlotName: 'triggerModeFilter',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
sorter: true,
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
|
@ -157,10 +146,6 @@
|
|||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
titleSlotName: 'statusFilter',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
sorter: true,
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
v-model:selected-keys="selectedKeys"
|
||||
:data="folderTree"
|
||||
:keyword="moduleKeyword"
|
||||
:node-more-actions="folderMoreActions"
|
||||
:default-expand-all="isExpandAll"
|
||||
:expand-all="isExpandAll"
|
||||
:empty-text="t('apiScenario.tree.noMatchModule')"
|
||||
|
@ -48,7 +47,6 @@
|
|||
|
||||
import { getModuleCount, getModuleTree } from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { mapTree } from '@/utils';
|
||||
|
||||
|
@ -65,27 +63,10 @@
|
|||
|
||||
const appStore = useAppStore();
|
||||
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(() => {
|
||||
return {
|
||||
height: 'calc(100vh - 343px)',
|
||||
height: '40vh',
|
||||
threshold: 200,
|
||||
fixedSize: true,
|
||||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
|
@ -96,30 +77,10 @@
|
|||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
const focusNodeKey = ref<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 folderMoreActions: ActionsItem[] = [
|
||||
{
|
||||
label: 'common.rename',
|
||||
eventTag: 'rename',
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
{
|
||||
label: 'common.delete',
|
||||
eventTag: 'delete',
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
const modulesCount = ref<Record<string, number>>({});
|
||||
const allScenarioCount = computed(() => modulesCount.value.all || 0);
|
||||
const isExpandAll = ref(props.isExpandAll);
|
||||
const rootModulesName = ref<string[]>([]); // 根模块名称列表
|
||||
|
||||
/**
|
||||
* 初始化模块树
|
||||
|
@ -164,10 +125,6 @@
|
|||
}
|
||||
);
|
||||
|
||||
function changeExpand() {
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件夹树节点选中事件
|
||||
*/
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
},
|
||||
],
|
||||
titleSlotName: 'typeTitle',
|
||||
typeTitleTooltip: t('apiScenario.params.typeTooltip'),
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.params.paramValue',
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
<template>
|
||||
<div class="condition">
|
||||
<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>
|
||||
<a-divider class="my-[8px]" type="dashed" />
|
||||
<div>
|
||||
|
@ -9,6 +15,8 @@
|
|||
v-model:config="postProcessorConfig"
|
||||
:is-definition="false"
|
||||
:layout="activeLayout"
|
||||
sql-code-editor-height="300px"
|
||||
is-scenario
|
||||
@change="emit('change')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -37,10 +45,6 @@
|
|||
<style lang="less" scoped>
|
||||
.condition {
|
||||
.ms-scroll-bar();
|
||||
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
flex-shrink: 0;
|
||||
@apply h-full overflow-auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -242,6 +242,9 @@
|
|||
</template>
|
||||
</TableFilter>
|
||||
</template>
|
||||
<template #stepTotal="{ record }">
|
||||
{{ record.stepTotal }}
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_SCENARIO:READ+UPDATE']"
|
||||
|
@ -505,14 +508,18 @@
|
|||
style="padding-top: 10px"
|
||||
>
|
||||
<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="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<span class="flex items-center">
|
||||
<span class="mr-1">{{ t('caseManagement.featureCase.appendTag') }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<a-tooltip>
|
||||
<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>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a-button type="secondary" :disabled="batchUpdateLoading" @click="cancelBatch">
|
||||
|
@ -553,7 +560,7 @@
|
|||
{{ t('api_scenario.table.batchModalSubTitle', { count: tableSelected.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isBatchMove">
|
||||
<div v-else-if="isBatchMove" class="flex items-center">
|
||||
{{ t('common.batchMove') }}
|
||||
<div
|
||||
class="one-line-text ml-[4px] max-w-[100%] text-[var(--color-text-4)]"
|
||||
|
@ -795,6 +802,7 @@
|
|||
{
|
||||
title: 'apiScenario.table.columns.steps',
|
||||
dataIndex: 'stepTotal',
|
||||
slotName: 'stepTotal',
|
||||
showInTable: false,
|
||||
showDrag: true,
|
||||
width: 100,
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
| ScenarioAddStepActionType.SCRIPT_OPERATION,
|
||||
step?: ScenarioStepItem
|
||||
);
|
||||
(e: 'addDone');
|
||||
(e: 'addDone', newStep: ScenarioStepItem);
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
@ -117,7 +117,7 @@
|
|||
} else {
|
||||
steps.value.push(defaultLoopStep);
|
||||
}
|
||||
emit('addDone');
|
||||
emit('addDone', defaultLoopStep);
|
||||
break;
|
||||
case ScenarioAddStepActionType.CONDITION_CONTROL:
|
||||
const defaultConditionStep = buildInsertStepInfos(
|
||||
|
@ -132,7 +132,7 @@
|
|||
} else {
|
||||
steps.value.push(defaultConditionStep);
|
||||
}
|
||||
emit('addDone');
|
||||
emit('addDone', defaultConditionStep);
|
||||
break;
|
||||
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
|
||||
const defaultOnlyOnceStep = buildInsertStepInfos(
|
||||
|
@ -147,7 +147,7 @@
|
|||
} else {
|
||||
steps.value.push(defaultOnlyOnceStep);
|
||||
}
|
||||
emit('addDone');
|
||||
emit('addDone', defaultOnlyOnceStep);
|
||||
break;
|
||||
case ScenarioAddStepActionType.WAIT_TIME:
|
||||
const defaultWaitTimeStep = buildInsertStepInfos(
|
||||
|
@ -162,7 +162,7 @@
|
|||
} else {
|
||||
steps.value.push(defaultWaitTimeStep);
|
||||
}
|
||||
emit('addDone');
|
||||
emit('addDone', defaultWaitTimeStep);
|
||||
break;
|
||||
case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
|
||||
case ScenarioAddStepActionType.CUSTOM_API:
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
:popup-translate="[-7, -10]"
|
||||
@other-create="(type, step) => emit('otherCreate', type, step, activeCreateAction)"
|
||||
@close="handleActionsClose"
|
||||
@add-done="emit('addDone')"
|
||||
@add-done="emit('addDone', $event)"
|
||||
>
|
||||
<span></span>
|
||||
</createStepActions>
|
||||
|
@ -93,7 +93,7 @@
|
|||
step?: ScenarioStepItem,
|
||||
activeCreateAction?: CreateStepAction
|
||||
);
|
||||
(e: 'addDone');
|
||||
(e: 'addDone', newStep: ScenarioStepItem);
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
/>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
{{ t('apiScenario.sum') }}
|
||||
<div class="text-[rgb(var(--primary-5))]">{{ totalStepCount }}</div>
|
||||
<div class="text-[rgb(var(--primary-5))]">{{ topLevelStepCount }}</div>
|
||||
{{ t('apiScenario.steps') }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -171,6 +171,7 @@
|
|||
const stepTreeRef = ref<InstanceType<typeof stepTree>>();
|
||||
const keyword = ref('');
|
||||
|
||||
const topLevelStepCount = computed(() => scenario.value.steps.length);
|
||||
const totalStepCount = computed(() => countNodes(scenario.value.steps));
|
||||
|
||||
function handleChangeAll(value: boolean | (string | number | boolean)[]) {
|
||||
|
@ -267,7 +268,7 @@
|
|||
}
|
||||
|
||||
function batchDelete() {
|
||||
deleteNodes(scenario.value.steps, checkedKeys.value, 'uniqueId');
|
||||
deleteNodes(scenario.value.steps, checkedKeys.value, (node) => !node.isQuoteScenarioStep, 'uniqueId');
|
||||
Message.success(t('common.deleteSuccess'));
|
||||
if (scenario.value.steps.length === 0) {
|
||||
checkedAll.value = false;
|
||||
|
@ -357,13 +358,16 @@
|
|||
if (!node.enable) {
|
||||
// 如果步骤未开启,则删除已选 uniqueId,方便下面waitingDebugStepDetails详情判断是否携带
|
||||
checkedKeysSet.delete(node.uniqueId);
|
||||
node.executeStatus = undefined;
|
||||
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; // 已选中但未开启的步骤显示未执行状态
|
||||
} else {
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING; // 已选中且已开启的步骤显示执行中状态
|
||||
}
|
||||
return !!node.enable;
|
||||
}
|
||||
node.executeStatus = undefined; // 未选中的步骤不显示执行状态
|
||||
if (node.children && node.children.length > 0) {
|
||||
return true; // 因为存在选中了子步骤但是没有选中父步骤,所以需要递归完整树
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const waitingDebugStepDetails = {};
|
||||
|
|
|
@ -14,14 +14,11 @@
|
|||
:content="innerData.msCountController.loops?.toString()"
|
||||
:disabled="!innerData.msCountController.loops"
|
||||
>
|
||||
<a-input-number
|
||||
<a-input
|
||||
v-model:model-value="innerData.msCountController.loops"
|
||||
class="w-[80px] px-[8px]"
|
||||
size="mini"
|
||||
:step="1"
|
||||
:min="1"
|
||||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
:disabled="props.disabled"
|
||||
@blur="handleInputChange"
|
||||
|
@ -29,7 +26,7 @@
|
|||
<template #prefix>
|
||||
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.num') }}:</div>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-input>
|
||||
</a-tooltip>
|
||||
</a-input-group>
|
||||
<template v-if="innerData.loopType === ScenarioStepLoopTypeEnum.FOREACH">
|
||||
|
|
|
@ -17,13 +17,13 @@
|
|||
<div>
|
||||
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
|
||||
<div class="text-[14px] text-[var(--color-text-1)]">
|
||||
{{ originProjectName }}
|
||||
{{ originProjectInfo?.projectName }}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{{ `【${props.data.resourceNum}】${props.data.resourceName}` }}
|
||||
{{ `【${originProjectInfo?.num}】${originProjectInfo?.name}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@
|
|||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
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 { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
|
@ -67,12 +67,11 @@
|
|||
const { t } = useI18n();
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
const originProjectName = ref('');
|
||||
const originProjectInfo = ref<ScenarioStepResourceInfo>();
|
||||
|
||||
async function handleVisibleChange(val: boolean) {
|
||||
if (val && props.data.originProjectId) {
|
||||
const res = await getStepProjectInfo(props.data.originProjectId);
|
||||
originProjectName.value = res.name;
|
||||
originProjectInfo.value = await getStepProjectInfo(props.data.resourceId || '', props.data.stepType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,7 +89,7 @@
|
|||
case _stepType.isQuoteScenario:
|
||||
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
|
||||
pId: props.data.originProjectId,
|
||||
sId: props.data.resourceId,
|
||||
id: props.data.resourceId,
|
||||
});
|
||||
break;
|
||||
case _stepType.isQuoteCase:
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
:field-names="{ title: 'name', key: 'uniqueId', children: 'children' }"
|
||||
:virtual-list-props="{
|
||||
height: '100%',
|
||||
threshold: 20,
|
||||
threshold: 200,
|
||||
fixedSize: true,
|
||||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
}"
|
||||
|
@ -50,10 +50,7 @@
|
|||
"
|
||||
>
|
||||
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-1)]">
|
||||
<MsIcon
|
||||
:type="step.expanded ? 'icon-icon_split-turn-down-left' : 'icon-icon_split_turn-down_arrow'"
|
||||
:size="14"
|
||||
/>
|
||||
<MsIcon type="icon-icon_split_turn-down_arrow" :size="14" />
|
||||
{{ step.children?.length || 0 }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
@ -67,14 +64,14 @@
|
|||
></a-switch>
|
||||
<!-- 步骤执行 -->
|
||||
<MsIcon
|
||||
v-show="!step.isExecuting"
|
||||
v-show="!step.isExecuting && step.enable"
|
||||
type="icon-icon_play-round_filled"
|
||||
:size="18"
|
||||
class="cursor-pointer text-[rgb(var(--link-6))]"
|
||||
@click.stop="executeStep(step)"
|
||||
/>
|
||||
<MsIcon
|
||||
v-show="step.isExecuting"
|
||||
v-show="step.isExecuting && step.enable"
|
||||
type="icon-icon_stop"
|
||||
:size="20"
|
||||
class="cursor-pointer text-[rgb(var(--link-6))]"
|
||||
|
@ -171,13 +168,7 @@
|
|||
</template>
|
||||
<template #extra="step">
|
||||
<stepInsertStepTrigger
|
||||
v-if="
|
||||
!step.isQuoteScenarioStep &&
|
||||
!(
|
||||
step.stepType === ScenarioStepType.API_SCENARIO &&
|
||||
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(step.refType)
|
||||
)
|
||||
"
|
||||
v-if="!step.isQuoteScenarioStep"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:steps="steps"
|
||||
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-group>
|
||||
</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 :value="false">{{ t('apiScenario.empty') }}</a-radio>
|
||||
<a-radio :value="true">
|
||||
|
@ -1076,7 +1073,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleAddStepDone() {
|
||||
function handleAddStepDone(newStep: ScenarioStepItem) {
|
||||
selectedKeys.value = [newStep.uniqueId]; // 选中新添加的步骤
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
@ -1098,7 +1096,7 @@
|
|||
// 复制 api、引用 api、自定义 api打开抽屉
|
||||
activeStep.value = step;
|
||||
if (
|
||||
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
||||
(stepDetails.value[step.id] === undefined && step.copyFromStepId && !step.isNew) ||
|
||||
(stepDetails.value[step.id] === undefined && !step.isNew)
|
||||
) {
|
||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
||||
|
@ -1109,7 +1107,7 @@
|
|||
activeStep.value = step;
|
||||
if (
|
||||
_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))
|
||||
) {
|
||||
// 只有复制的 case 需要查看步骤详情,引用的无法更改所以不需要在此初始化详情
|
||||
|
@ -1444,11 +1442,7 @@
|
|||
*/
|
||||
function addCustomApiStep(request: RequestParam) {
|
||||
request.isNew = false;
|
||||
stepDetails.value[request.stepId] = {
|
||||
...request,
|
||||
customizeRequest: true,
|
||||
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
|
||||
};
|
||||
stepDetails.value[request.stepId] = request;
|
||||
scenario.value.stepFileParam[request.stepId] = {
|
||||
linkFileIds: request.linkFileIds,
|
||||
uploadFileIds: request.uploadFileIds,
|
||||
|
@ -1490,6 +1484,7 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
});
|
||||
}
|
||||
selectedKeys.value = [request.stepId]; // 选中新添加的步骤
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
@ -1572,6 +1567,7 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
});
|
||||
}
|
||||
selectedKeys.value = [id]; // 选中新添加的步骤
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
|
|
@ -12,70 +12,77 @@ export default function updateStepStatus(
|
|||
) {
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const node = steps[i];
|
||||
if (
|
||||
[
|
||||
ScenarioStepType.LOOP_CONTROLLER,
|
||||
ScenarioStepType.IF_CONTROLLER,
|
||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||
ScenarioStepType.API_SCENARIO,
|
||||
].includes(node.stepType)
|
||||
) {
|
||||
if (!node.executeStatus) {
|
||||
// 没有执行状态,说明未参与执行,直接跳过
|
||||
break;
|
||||
}
|
||||
// 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
|
||||
let hasNotExecuted = false;
|
||||
let hasFailure = false;
|
||||
let hasFakeError = false;
|
||||
if (!node.children || node.children.length === 0) {
|
||||
// 逻辑控制器内无步骤,则直接是未执行
|
||||
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
|
||||
} else {
|
||||
for (let j = 0; j < node.children.length; j++) {
|
||||
const childNode = node.children[j];
|
||||
updateStepStatus([childNode], stepResponses);
|
||||
if (
|
||||
childNode.executeStatus &&
|
||||
[ScenarioExecuteStatus.EXECUTING, ScenarioExecuteStatus.UN_EXECUTE].includes(childNode.executeStatus)
|
||||
) {
|
||||
// 子节点未执行或正在执行,则逻辑控制器也是未执行
|
||||
hasNotExecuted = true;
|
||||
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAILED) {
|
||||
// 子节点有一个失败,逻辑控制器就是失败
|
||||
hasFailure = true;
|
||||
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAKE_ERROR) {
|
||||
// 子节点有一个误报,逻辑控制器就是误报
|
||||
hasFakeError = true;
|
||||
if (node.enable) {
|
||||
// 启用的步骤才计算
|
||||
if (
|
||||
[
|
||||
ScenarioStepType.LOOP_CONTROLLER,
|
||||
ScenarioStepType.IF_CONTROLLER,
|
||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||
ScenarioStepType.API_SCENARIO,
|
||||
].includes(node.stepType)
|
||||
) {
|
||||
if (!node.executeStatus) {
|
||||
// 没有执行状态,说明未参与执行,直接跳过
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
// 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
|
||||
let hasNotExecuted = false;
|
||||
let hasFailure = false;
|
||||
let hasFakeError = false;
|
||||
if (!node.children || node.children.length === 0) {
|
||||
// 逻辑控制器内无步骤,则直接是未执行
|
||||
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
|
||||
} else {
|
||||
for (let j = 0; j < node.children.length; j++) {
|
||||
const childNode = node.children[j];
|
||||
updateStepStatus([childNode], stepResponses);
|
||||
if (childNode.executeStatus === ScenarioExecuteStatus.FAILED) {
|
||||
// 子节点有一个失败,逻辑控制器就是失败
|
||||
hasFailure = true;
|
||||
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAKE_ERROR) {
|
||||
// 子节点有一个误报,逻辑控制器就是误报
|
||||
hasFakeError = true;
|
||||
} else if (
|
||||
childNode.executeStatus &&
|
||||
[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;
|
||||
}
|
||||
}
|
||||
// 递归完子节点后,判断当前逻辑控制器的状态
|
||||
if (hasNotExecuted) {
|
||||
} 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;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,8 +200,8 @@
|
|||
function share() {
|
||||
if (isSupported) {
|
||||
const url = window.location.href;
|
||||
const dIdParam = `&sId=${scenario.value.id}`;
|
||||
const copyUrl = url.includes('sId') ? url.split('&sId')[0] : url;
|
||||
const dIdParam = `&id=${scenario.value.id}`;
|
||||
const copyUrl = url.includes('id') ? url.split('&id')[0] : url;
|
||||
copy(`${copyUrl}${dIdParam}`);
|
||||
Message.success(t('common.copySuccess'));
|
||||
} else {
|
||||
|
|
|
@ -192,19 +192,22 @@
|
|||
if (scenario.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
if (scenario.stepResponses[result.stepId] === undefined) {
|
||||
scenario.stepResponses[result.stepId] = [];
|
||||
}
|
||||
scenario.stepResponses[result.stepId].push({
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
});
|
||||
if (result.status === ScenarioExecuteStatus.FAKE_ERROR) {
|
||||
scenario.executeFakeErrorCount += 1;
|
||||
} else if (result.isSuccessful) {
|
||||
scenario.executeSuccessCount += 1;
|
||||
} else {
|
||||
scenario.executeFailCount += 1;
|
||||
if (result.stepId) {
|
||||
// 过滤掉前后置配置的执行结果,没有步骤 id
|
||||
if (scenario.stepResponses[result.stepId] === undefined) {
|
||||
scenario.stepResponses[result.stepId] = [];
|
||||
}
|
||||
scenario.stepResponses[result.stepId].push({
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
});
|
||||
if (result.status === ScenarioExecuteStatus.FAKE_ERROR) {
|
||||
scenario.executeFakeErrorCount += 1;
|
||||
} else if (result.isSuccessful) {
|
||||
scenario.executeSuccessCount += 1;
|
||||
} else {
|
||||
scenario.executeFailCount += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -306,6 +309,8 @@
|
|||
if (node.enable) {
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id];
|
||||
} else {
|
||||
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
|
||||
}
|
||||
return !!node.enable;
|
||||
});
|
||||
|
@ -573,8 +578,8 @@
|
|||
|
||||
onBeforeMount(() => {
|
||||
selectRecycleCount();
|
||||
if (route.query.sId) {
|
||||
openScenarioTab(route.query.sId as string);
|
||||
if (route.query.id) {
|
||||
openScenarioTab(route.query.id as string);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,244 +1,262 @@
|
|||
export default {
|
||||
'apiScenario.env': 'Environment: {name}',
|
||||
'apiScenario.allScenario': 'All scenarios',
|
||||
'apiScenario.createScenario': 'Create scenario',
|
||||
'apiScenario.importScenario': 'Import scenario',
|
||||
'apiScenario.tree.selectorPlaceholder': 'Please enter the module name',
|
||||
'apiScenario.tree.folder.allScenario': 'All scenarios',
|
||||
'apiScenario.tree.recycleBin': 'Recycle bin',
|
||||
'apiScenario.tree.noMatchModule': 'No matching module/scene yet',
|
||||
'apiScenario.createSubModule': 'Create sub-module',
|
||||
'apiScenario.module.deleteTipTitle': 'Delete {name} module?',
|
||||
'apiScenario.env': 'Current Environment: {name}',
|
||||
'apiScenario.allScenario': 'All Scenarios',
|
||||
'apiScenario.createScenario': 'Create Scenario',
|
||||
'apiScenario.importScenario': 'Import Scenario',
|
||||
'apiScenario.tree.selectorPlaceholder': 'Please enter module name',
|
||||
'apiScenario.tree.folder.allScenario': 'All Scenarios',
|
||||
'apiScenario.tree.recycleBin': 'Recycle Bin',
|
||||
'apiScenario.tree.noMatchModule': 'No matching module/scenario found',
|
||||
'apiScenario.createSubModule': 'Create Submodule',
|
||||
'apiScenario.module.deleteTipTitle': 'Delete Module {name}?',
|
||||
'apiScenario.module.deleteTipContent':
|
||||
'After deletion, all scenarios under the module will be deleted synchronously. Please operate with caution.',
|
||||
'apiScenario.deleteConfirm': 'Confirm',
|
||||
'apiScenario.deleteSuccess': 'Success',
|
||||
'apiScenario.moveSuccess': 'Success',
|
||||
'apiScenario.baseInfo': 'Base info',
|
||||
'Deleting will also delete all scenarios under the module. Proceed with caution.',
|
||||
'apiScenario.deleteConfirm': 'Confirm Delete',
|
||||
'apiScenario.deleteSuccess': 'Delete Successful',
|
||||
'apiScenario.moveSuccess': 'Move Successful',
|
||||
'apiScenario.baseInfo': 'Basic Information',
|
||||
'apiScenario.step': 'Step',
|
||||
'apiScenario.params': 'Params',
|
||||
'apiScenario.params': 'Parameters',
|
||||
'apiScenario.prePost': 'Pre/Post',
|
||||
'apiScenario.assertion': 'Assertion',
|
||||
'apiScenario.executeHistory': 'Execute history',
|
||||
'apiScenario.changeHistory': 'Change history',
|
||||
'apiScenario.dependency': 'Dependencies',
|
||||
'apiScenario.quote': 'Reference',
|
||||
'apiScenario.params.convention': 'Convention parameter',
|
||||
'apiScenario.executeHistory': 'Execution History',
|
||||
'apiScenario.changeHistory': 'Change History',
|
||||
'apiScenario.dependency': 'Dependency',
|
||||
'apiScenario.quote': 'Quote',
|
||||
'apiScenario.params.convention': 'Conventional Parameters',
|
||||
'apiScenario.params.searchPlaceholder': 'Search by name or tag',
|
||||
'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',
|
||||
'apiScenario.params.name': 'Variable name',
|
||||
'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.type': 'Type',
|
||||
'apiScenario.params.paramValue': 'Parameter value',
|
||||
'apiScenario.params.paramValue': 'Parameter Value',
|
||||
'apiScenario.params.tag': 'Tag',
|
||||
'apiScenario.params.desc': 'Description',
|
||||
'apiScenario.table.columns.name': 'Name',
|
||||
'apiScenario.table.columns.level': 'Level',
|
||||
'apiScenario.table.columns.status': 'status',
|
||||
'apiScenario.table.columns.runResult': 'Run result',
|
||||
'apiScenario.params.typeTooltip': 'Json: only supports UI testing',
|
||||
'apiScenario.table.columns.name': 'Scenario Name',
|
||||
'apiScenario.table.columns.level': 'Scenario Level',
|
||||
'apiScenario.table.columns.status': 'Status',
|
||||
'apiScenario.table.columns.runResult': 'Execution Result',
|
||||
'apiScenario.table.columns.tags': 'Tags',
|
||||
'apiScenario.table.columns.scenarioEnv': 'Scenario environment',
|
||||
'apiScenario.table.columns.steps': 'Steps',
|
||||
'apiScenario.table.columns.passRate': 'Pass rate',
|
||||
'apiScenario.table.columns.module': 'Module',
|
||||
'apiScenario.table.columns.createUser': 'Create user',
|
||||
'apiScenario.table.columns.createTime': 'Create time',
|
||||
'apiScenario.table.columns.updateUser': 'Update user',
|
||||
'apiScenario.table.columns.updateTime': 'Update time',
|
||||
'apiScenario.table.columns.deleteUser': 'Delete User',
|
||||
'apiScenario.table.columns.operation': 'Operation',
|
||||
'apiScenario.table.columns.deleteTime': 'Delete time',
|
||||
'api_scenario.table.tableNoDataAndPlease': 'No data yet, please',
|
||||
'apiScenario.table.columns.scenarioEnv': 'Scenario Environment',
|
||||
'apiScenario.table.columns.steps': 'Number of Steps',
|
||||
'apiScenario.table.columns.passRate': 'Pass Rate',
|
||||
'apiScenario.table.columns.module': 'Belongs to Module',
|
||||
'apiScenario.table.columns.createUser': 'Creator',
|
||||
'apiScenario.table.columns.createTime': 'Creation Time',
|
||||
'apiScenario.table.columns.updateUser': 'Updater',
|
||||
'apiScenario.table.columns.updateTime': 'Update Time',
|
||||
'apiScenario.table.columns.operation': 'Operator',
|
||||
'apiScenario.table.columns.deleteUser': 'Deleter',
|
||||
'apiScenario.table.columns.deleteTime': 'Deletion Time',
|
||||
'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',
|
||||
'apiScenario.execute': 'Execute',
|
||||
'apiScenario.prev': 'Last time',
|
||||
'apiScenario.next': 'Next time',
|
||||
'apiScenario.sumLoop': 'A total of {count} loops',
|
||||
'apiScenario.times': 'bout',
|
||||
'apiScenario.executionResult': 'Execution result',
|
||||
// 批量操作文案
|
||||
'api_scenario.batch_operation.success': 'Success {success} ,error {error} ',
|
||||
'api_scenario.table.batchMoveConfirm': 'Ready to {opt} {count} scenarios',
|
||||
'apiScenario.name': 'Scene name',
|
||||
'apiScenario.nameRequired': 'Scene name cannot be empty',
|
||||
'apiScenario.namePlaceholder': 'Please enter scene name',
|
||||
'apiScenario.belongModule': 'Belonging module',
|
||||
'apiScenario.level': 'Scene level',
|
||||
'apiScenario.status': 'Scene status',
|
||||
'apiScenario.prev': 'Previous',
|
||||
'apiScenario.next': 'Next',
|
||||
'apiScenario.sumLoop': 'Total {count} loops',
|
||||
'apiScenario.times': 'times',
|
||||
'apiScenario.executionResult': 'Execution Result',
|
||||
'apiScenario.refreshRefScenario': 'Refresh Referenced Scenario Data',
|
||||
'apiScenario.updateRefScenarioSuccess': 'Referenced scenario data has been updated',
|
||||
'apiScenario.replaceSuccess': 'Step replacement successful',
|
||||
// Batch operation text
|
||||
'api_scenario.batch_operation.success': 'Successfully {opt} to {name}',
|
||||
'api_scenario.table.batchMoveConfirm': '{opt} {count} scenarios to selected module',
|
||||
'apiScenario.name': 'Scenario Name',
|
||||
'apiScenario.nameRequired': 'Scenario name cannot be empty',
|
||||
'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.addStep': 'Add steps',
|
||||
'apiScenario.addStep': 'Add Step',
|
||||
'apiScenario.requestScenario': 'Request/Scenario',
|
||||
'apiScenario.importSystemApi': 'Import system requests',
|
||||
'apiScenario.customApi': 'Custom request',
|
||||
'apiScenario.logicControl': 'Logic control',
|
||||
'apiScenario.loopControl': 'Loop controller',
|
||||
'apiScenario.importSystemApi': 'Import System Request',
|
||||
'apiScenario.customApi': 'Custom Request',
|
||||
'apiScenario.logicControl': 'Logic Control',
|
||||
'apiScenario.loopControl': 'Loop Controller',
|
||||
'apiScenario.tutorial': 'Tutorial',
|
||||
'apiScenario.conditionControl': 'Conditional controller',
|
||||
'apiScenario.onlyOnceControl': 'Only once controller',
|
||||
'apiScenario.conditionControl': 'Condition Controller',
|
||||
'apiScenario.onlyOnceControl': 'Only Once Controller',
|
||||
'apiScenario.other': 'Other',
|
||||
'apiScenario.scriptOperation': 'Script operation',
|
||||
'apiScenario.waitTime': 'Waiting time',
|
||||
'apiScenario.quoteApi': 'Reference API',
|
||||
'apiScenario.scriptOperation': 'Script Operation',
|
||||
'apiScenario.waitTime': 'Wait Time',
|
||||
'apiScenario.quoteApi': 'Quote API',
|
||||
'apiScenario.copyApi': 'Copy API',
|
||||
'apiScenario.quoteCase': 'Reference CASE',
|
||||
'apiScenario.quoteCase': 'Quote CASE',
|
||||
'apiScenario.copyCase': 'Copy CASE',
|
||||
'apiScenario.quoteScenario': 'Reference scene',
|
||||
'apiScenario.copyScenario': 'Copy scene',
|
||||
'apiScenario.sum': 'Common',
|
||||
'apiScenario.quoteScenario': 'Quote Scenario',
|
||||
'apiScenario.copyScenario': 'Copy Scenario',
|
||||
'apiScenario.sum': 'Total',
|
||||
'apiScenario.steps': 'steps',
|
||||
'apiScenario.expandAllStep': 'Expand all substeps',
|
||||
'apiScenario.collapseAllStep': 'Collapse all substeps',
|
||||
'apiScenario.executeTime': 'Execution time',
|
||||
'apiScenario.executeResult': 'Execution result',
|
||||
'apiScenario.checkReport': 'View report',
|
||||
'apiScenario.expandAllStep': 'Expand All Substeps',
|
||||
'apiScenario.collapseAllStep': 'Collapse All Substeps',
|
||||
'apiScenario.executeTime': 'Execution Time:',
|
||||
'apiScenario.executeResult': 'Execution Result',
|
||||
'apiScenario.checkReport': 'View Report',
|
||||
'apiScenario.searchByName': 'Search by step name/description',
|
||||
'apiScenario.api': 'API',
|
||||
'apiScenario.case': 'CASE',
|
||||
'apiScenario.case': 'Case',
|
||||
'apiScenario.scenario': 'Scenario',
|
||||
'apiScenario.sumSelected': 'Total selection',
|
||||
'apiScenario.scenarioConfig': 'Scene configuration',
|
||||
'apiScenario.noMatchStep': 'No matching step data yet',
|
||||
'apiScenario.pleaseInputStepName': 'Please enter a step name',
|
||||
'apiScenario.belongProject': 'Project',
|
||||
'apiScenario.sumSelected': 'Selected',
|
||||
'apiScenario.scenarioConfig': 'Scenario Configuration',
|
||||
'apiScenario.noMatchStep': 'No matching step data',
|
||||
'apiScenario.pleaseInputStepName': 'Please enter step name',
|
||||
'apiScenario.belongProject': 'Belongs to Project',
|
||||
'apiScenario.detailName': 'Name',
|
||||
'apiScenario.crossProject': 'Cross-project',
|
||||
'apiScenario.crossProject': 'Cross Project',
|
||||
'apiScenario.expandStepTip': 'Expand {count} substeps',
|
||||
'apiScenario.collapseStepTip': 'Collapse {count} substeps',
|
||||
'apiScenario.inside': 'Add substep',
|
||||
'apiScenario.before': 'Insert above',
|
||||
'apiScenario.after': 'Insert below',
|
||||
'apiScenario.num': 'frequency',
|
||||
'apiScenario.space': 'Interval(ms)',
|
||||
'apiScenario.timeout': 'Timeout(ms)',
|
||||
'apiScenario.waitTimeMs': 'Wait(ms)',
|
||||
'apiScenario.pleaseInputStepDesc': 'Please enter a step description',
|
||||
'apiScenario.variable': 'Variable name {suffix}',
|
||||
'apiScenario.valuePrefix': 'variable prefix',
|
||||
'apiScenario.value': 'variable',
|
||||
'apiScenario.conditionValue': 'variable',
|
||||
'apiScenario.msWhileVariableValue': 'variable',
|
||||
'apiScenario.msWhileVariableScriptValue': 'expression',
|
||||
'apiScenario.condition': 'condition',
|
||||
'apiScenario.expression': 'expression',
|
||||
'apiScenario.equal': 'equal',
|
||||
'apiScenario.notEqualTo': 'not equal to',
|
||||
'apiScenario.greater': 'more than the',
|
||||
'apiScenario.less': 'less than',
|
||||
'apiScenario.greaterOrEqual': 'greater or equal to',
|
||||
'apiScenario.lessOrEqual': 'less than or equal to',
|
||||
'apiScenario.inside': 'Add Substep',
|
||||
'apiScenario.before': 'Insert Above',
|
||||
'apiScenario.after': 'Insert Below',
|
||||
'apiScenario.num': 'Number',
|
||||
'apiScenario.space': 'Interval (ms)',
|
||||
'apiScenario.timeout': 'Timeout (ms)',
|
||||
'apiScenario.waitTimeMs': 'Wait (ms)',
|
||||
'apiScenario.pleaseInputStepDesc': 'Please enter step description',
|
||||
'apiScenario.variable': 'Variable Name {suffix}',
|
||||
'apiScenario.valuePrefix': 'Variable Prefix',
|
||||
'apiScenario.value': 'Variable Value',
|
||||
'apiScenario.conditionValue': 'Variable Value',
|
||||
'apiScenario.msWhileVariableValue': 'Variable Value',
|
||||
'apiScenario.msWhileVariableScriptValue': 'Expression',
|
||||
'apiScenario.condition': 'Condition',
|
||||
'apiScenario.expression': 'Expression',
|
||||
'apiScenario.equal': 'Equal',
|
||||
'apiScenario.notEqualTo': 'Not Equal',
|
||||
'apiScenario.greater': 'Greater',
|
||||
'apiScenario.less': 'Less',
|
||||
'apiScenario.greaterOrEqual': 'Greater or Equal',
|
||||
'apiScenario.lessOrEqual': 'Less or Equal',
|
||||
'apiScenario.include': 'Include',
|
||||
'apiScenario.notInclude': 'Not included',
|
||||
'apiScenario.notInclude': 'Not Include',
|
||||
'apiScenario.null': 'Null',
|
||||
'apiScenario.notNull': 'Not Null',
|
||||
'apiScenario.range': 'Scope',
|
||||
'apiScenario.topStep': 'First level steps',
|
||||
'apiScenario.allStep': 'All substeps',
|
||||
'apiScenario.saveAsApi': 'Save as new interface',
|
||||
'apiScenario.scenarioLevel': 'Scene level',
|
||||
'apiScenario.running': 'Executing',
|
||||
'apiScenario.unExecute': 'Not performed',
|
||||
'apiScenario.response': 'Response content',
|
||||
'apiScenario.quoteMode': 'Reference mode',
|
||||
'apiScenario.fullQuote': 'Full quote',
|
||||
'apiScenario.stepQuote': 'Step reference',
|
||||
'apiScenario.runRule': 'Parameter value rules',
|
||||
'apiScenario.currentScenario': 'Prioritize current scene parameters',
|
||||
'apiScenario.sourceScenario': 'Priority source scene parameters',
|
||||
'apiScenario.currentScenarioTip': 'When the current scene parameter does not exist, take',
|
||||
'apiScenario.sourceScenarioTip': 'When the source scene parameter does not exist, take',
|
||||
'apiScenario.empty': 'Null value',
|
||||
'apiScenario.currentScenarioParams': 'Current scene parameters',
|
||||
'apiScenario.sourceScenarioParams': 'Source scene parameters',
|
||||
'apiScenario.sourceScenarioEnv': 'Source scene environment',
|
||||
'apiScenario.valuePriority': 'Value priority:',
|
||||
'apiScenario.range': 'Range',
|
||||
'apiScenario.topStep': 'Top-level Step',
|
||||
'apiScenario.allStep': 'All Substeps',
|
||||
'apiScenario.saveAsApi': 'Save as New API',
|
||||
'apiScenario.scenarioLevel': 'Scenario Level',
|
||||
'apiScenario.running': 'Running',
|
||||
'apiScenario.unExecute': 'Not Executed',
|
||||
'apiScenario.response': 'Response Content',
|
||||
'apiScenario.quoteMode': 'Quote Mode',
|
||||
'apiScenario.fullQuote': 'Full Quote',
|
||||
'apiScenario.stepQuote': 'Step Quote',
|
||||
'apiScenario.runRule': 'Parameter Value Rule',
|
||||
'apiScenario.currentScenario': 'Priority: Current Scenario Parameters',
|
||||
'apiScenario.sourceScenario': 'Priority: Source Scenario Parameters',
|
||||
'apiScenario.currentScenarioTip': 'When current scenario parameters do not exist, take',
|
||||
'apiScenario.sourceScenarioTip': 'When source scenario parameters do not exist, take',
|
||||
'apiScenario.empty': 'Empty Value',
|
||||
'apiScenario.currentScenarioParams': 'Current Scenario Parameters',
|
||||
'apiScenario.sourceScenarioParams': 'Source Scenario Parameters',
|
||||
'apiScenario.sourceScenarioEnv': 'Source Scenario Environment',
|
||||
'apiScenario.valuePriority': 'Value Priority:',
|
||||
'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':
|
||||
'Current step parameters > Current scene parameters > Source environment parameters > Null value',
|
||||
'Current Step Parameters > Current Scenario Parameters > Source Environment Parameters > Empty Value',
|
||||
'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':
|
||||
'Current step parameters > Current scene parameters > Source scene parameters > Source environment parameters',
|
||||
'apiScenario.sourceScenarioAndNull': 'Source Scene Parameters > Null',
|
||||
'apiScenario.sourceScenarioAndNullAndSourceEnv': 'Source Scene Parameters > Source Environment Parameters > Null',
|
||||
'Current Step Parameters > Current Scenario Parameters > Source Scenario Parameters > Source Environment Parameters',
|
||||
'apiScenario.sourceScenarioAndNull': 'Source Scenario Parameters > Empty Value',
|
||||
'apiScenario.sourceScenarioAndNullAndSourceEnv':
|
||||
'Source Scenario Parameters > Source Environment Parameters > Empty Value',
|
||||
'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':
|
||||
'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':
|
||||
'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':
|
||||
'Step reference: only follow the content changes of the source step, and the step status can be adjusted',
|
||||
'apiScenario.sourceScenarioEnvTip': 'Operating environment, including environmental parameters',
|
||||
'apiScenario.setSuccess': 'Setup successful',
|
||||
'apiScenario.pleaseInputUrl': 'Please enter url',
|
||||
// 执行历史
|
||||
'Step Quote: Only follows the original step content changes. Step status can be adjusted.',
|
||||
'apiScenario.sourceScenarioEnvTip': 'Runtime environment, including environment parameters',
|
||||
'apiScenario.setSuccess': 'Set Successful',
|
||||
'apiScenario.pleaseInputUrl': 'Please enter URL',
|
||||
// Execution History
|
||||
'apiScenario.executeHistory.searchPlaceholder': 'Search by ID or name',
|
||||
'apiScenario.executeHistory.num': 'Number',
|
||||
'apiScenario.executeHistory.execution.triggerMode': 'Trigger mode',
|
||||
'apiScenario.executeHistory.execution.status': 'Execution result',
|
||||
'apiScenario.executeHistory.num': 'No.',
|
||||
'apiScenario.executeHistory.execution.triggerMode': 'Execution Mode',
|
||||
'apiScenario.executeHistory.execution.status': 'Execution Result',
|
||||
'apiScenario.executeHistory.execution.operator': 'Operator',
|
||||
'apiScenario.executeHistory.execution.operatorTime': 'Operation time',
|
||||
'apiScenario.executeHistory.execution.operation': 'Execution result',
|
||||
'apiScenario.executeHistory.status.pending': 'Pending',
|
||||
'apiScenario.executeHistory.execution.operatorTime': 'Operation Time',
|
||||
'apiScenario.executeHistory.execution.operation': 'Execution Result',
|
||||
'apiScenario.executeHistory.status.pending': 'Queued',
|
||||
'apiScenario.executeHistory.status.running': 'Running',
|
||||
'apiScenario.executeHistory.status.rerunning': 'Rerunning',
|
||||
'apiScenario.executeHistory.status.error': 'Error',
|
||||
'apiScenario.executeHistory.status.success': 'Success',
|
||||
'apiScenario.executeHistory.status.fake.error': 'Fake error',
|
||||
'apiScenario.executeHistory.status.error': 'Failed',
|
||||
'apiScenario.executeHistory.status.success': 'Successful',
|
||||
'apiScenario.executeHistory.status.fake.error': 'False Positive',
|
||||
'apiScenario.executeHistory.status.fake.stopped': 'Stopped',
|
||||
// 操作历史
|
||||
// Operation History
|
||||
'apiScenario.historyListTip':
|
||||
'View and compare historical changes. According to the rules set by the administrator, the change history data will be automatically deleted.',
|
||||
'apiScenario.changeOrder': 'Change serial number',
|
||||
'View and compare historical modifications. According to administrator settings, change history data will be automatically deleted.',
|
||||
'apiScenario.changeOrder': 'Change Order',
|
||||
'apiScenario.type': 'Type',
|
||||
'apiScenario.operationUser': 'Operator',
|
||||
'apiScenario.updateTime': 'Update time',
|
||||
'apiScenario.updateTime': 'Update Time',
|
||||
|
||||
// 回收站
|
||||
// Recycle Bin
|
||||
'api_scenario.recycle.recover': 'Recover',
|
||||
'api_scenario.recycle.recoveredSuccessfully': 'Recover success',
|
||||
'api_scenario.recycle.list': 'Recycle list',
|
||||
'api_scenario.recycle.batchCleanOut': 'Delete',
|
||||
'api_scenario.recycle.completedDeleteCaseTitle': 'Are you sure you want to completely delete {name}?',
|
||||
'api_scenario.recycle.recoveredSuccessfully': 'Recover Successful',
|
||||
'api_scenario.recycle.list': 'Recycle Bin List',
|
||||
'api_scenario.recycle.batchCleanOut': 'Permanently Delete',
|
||||
'api_scenario.recycle.completedDeleteCaseTitle': 'Confirm Permanently Delete {name}?',
|
||||
'api_scenario.recycle.cleanOutDeleteOnRecycleTip':
|
||||
'After deletion, the scene cannot be restored, please operate with caution!',
|
||||
'api_scenario.table.searchPlaceholder': 'Search by ID/Name/Tag',
|
||||
'apiScenario.quoteTreeNoData': 'There is currently no reference data. You can switch projects to obtain data.',
|
||||
'Deleting will permanently remove the scenario. Proceed with caution!',
|
||||
'apiScenario.quoteTreeNoData': 'No quotable data available, switch projects to retrieve data',
|
||||
'apiScenario.quoteTreeSearchTip': 'Enter module name to search',
|
||||
'apiScenario.quoteTableSearchTip': 'Search by path or name',
|
||||
'apiScenario.collapseAll': 'Collapse all submodules',
|
||||
'apiScenario.expandAll': 'Expand all submodules',
|
||||
'apiScenario.collapseAll': 'Collapse All Submodules',
|
||||
'apiScenario.expandAll': 'Expand All Submodules',
|
||||
|
||||
'apiScenario.scriptOperationName': 'Script operation name',
|
||||
'apiScenario.scriptOperationNamePlaceholder': 'Please enter the script operation name',
|
||||
'apiScenario.scriptOperationName': '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.share.cookie': 'Shared Cookie',
|
||||
'apiScenario.setting.run.config': 'Run configuration',
|
||||
'apiScenario.setting.step.waitTime': 'Step wait time',
|
||||
'apiScenario.setting.waitTime': 'Wait time',
|
||||
'apiScenario.setting.step.rule': 'Step execution failure rule',
|
||||
'apiScenario.setting.step.rule.ignore': 'Ignore error and continue execution',
|
||||
'apiScenario.setting.step.rule.stop': 'Stop/end execution',
|
||||
'apiScenario.setting.run.config': 'Run Configuration',
|
||||
'apiScenario.setting.step.waitTime': 'Step Wait Time',
|
||||
'apiScenario.setting.waitTime': 'Wait Time',
|
||||
'apiScenario.setting.step.rule': 'Step Execution Failure Rule',
|
||||
'apiScenario.setting.step.rule.ignore': 'Ignore Error and Continue Execution',
|
||||
'apiScenario.setting.step.rule.stop': 'Stop/End Execution',
|
||||
'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':
|
||||
'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':
|
||||
'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',
|
||||
// 定时任务
|
||||
'apiScenario.schedule.create': 'Create schedule',
|
||||
'apiScenario.schedule.update': 'Update schedule',
|
||||
'apiScenario.schedule.delete': 'Delete schedule',
|
||||
'apiScenario.schedule.config.resource_pool': 'Resource pool',
|
||||
'apiScenario.schedule.task.status': 'Task status',
|
||||
'apiScenario.schedule.task.schedule': 'Trigger time',
|
||||
'apiScenario.schedule.abbreviation': 'SCHEDULE',
|
||||
'apiScenario.schedule.task.status.tooltip.one': 'Open: execute schedule task',
|
||||
'apiScenario.schedule.task.status.tooltip.two': 'Close: stop executing schedule task',
|
||||
'apiScenario.schedule.table.tooltip.enable.one': 'Schedule is open',
|
||||
'apiScenario.schedule.table.tooltip.enable.two': 'Next trigger time:{time}',
|
||||
'apiScenario.schedule.table.tooltip.disable': 'Schedule is close',
|
||||
'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 Scheduled Task',
|
||||
'apiScenario.schedule.update': 'Update Scheduled Task',
|
||||
'apiScenario.schedule.delete': 'Delete Scheduled Task',
|
||||
'apiScenario.schedule.config.resource_pool': 'Run Resource Pool',
|
||||
'apiScenario.schedule.task.status': 'Task Status',
|
||||
'apiScenario.schedule.task.schedule': 'Task Trigger Time',
|
||||
'apiScenario.schedule.abbreviation': 'Scheduled',
|
||||
'apiScenario.schedule.task.status.tooltip.one': 'Enabled: Execute the scheduled task',
|
||||
'apiScenario.schedule.task.status.tooltip.two': 'Disabled: Stop the scheduled task',
|
||||
'apiScenario.schedule.table.tooltip.enable.one': 'Scheduled task is enabled',
|
||||
'apiScenario.schedule.table.tooltip.enable.two': 'Next run time: {time}',
|
||||
'apiScenario.schedule.table.tooltip.disable': 'Scheduled task is disabled',
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ export default {
|
|||
'apiScenario.params.paramValue': '参数值',
|
||||
'apiScenario.params.tag': '标签',
|
||||
'apiScenario.params.desc': '描述',
|
||||
'apiScenario.params.typeTooltip': 'Json:仅支持 UI 测试',
|
||||
'apiScenario.table.columns.name': '场景名称',
|
||||
'apiScenario.table.columns.level': '场景等级',
|
||||
'apiScenario.table.columns.status': '状态',
|
||||
|
|
|
@ -72,7 +72,7 @@ export default {
|
|||
'caseManagement.featureCase.fileName': '文件名',
|
||||
'caseManagement.featureCase.description': '描述',
|
||||
'caseManagement.featureCase.tags': '标签',
|
||||
'caseManagement.featureCase.enableTags': `开启:新增标签,关闭:覆盖原有标签`,
|
||||
'caseManagement.featureCase.enableTags': '开启:新增标签',
|
||||
'caseManagement.featureCase.closeTags': '关闭:覆盖原有标签',
|
||||
'caseManagement.featureCase.appendTag': '追加标签',
|
||||
'caseManagement.featureCase.batchEdit': '批量编辑 (已选 { number } 条用例)',
|
||||
|
|
Loading…
Reference in New Issue