feat(接口测试): csv参数表格&mock 调整

This commit is contained in:
baiqi 2024-05-16 14:14:17 +08:00 committed by Craftsman
parent 5784ac55a0
commit dd97da9e43
22 changed files with 457 additions and 210 deletions

View File

@ -93,7 +93,7 @@
class="m-0 border-none p-0" class="m-0 border-none p-0"
:self-style="{ backgroundColor: 'transparent !important' }" :self-style="{ backgroundColor: 'transparent !important' }"
:closable="data.value !== '__arco__more' && !props.disabled" :closable="data.value !== '__arco__more' && !props.disabled"
@close="handleClose(data)" @close="() => handleClose(data)"
> >
{{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }} {{ data.value === '__arco__more' ? data.label.replace('...', '') : data.label }}
</MsTag> </MsTag>

View File

@ -121,7 +121,7 @@
{ {
title: 'project.fileManagement.type', title: 'project.fileManagement.type',
dataIndex: 'fileType', dataIndex: 'fileType',
width: 90, width: 100,
}, },
{ {
title: 'project.fileManagement.size', title: 'project.fileManagement.size',

View File

@ -148,8 +148,11 @@
personalMenusVisible.value = false; personalMenusVisible.value = false;
orgKeyword.value = ''; orgKeyword.value = '';
await userStore.isLogin(true); await userStore.isLogin(true);
if (!appStore.currentProjectId || appStore.currentProjectId === 'no_such_project') { if (
// (, ) (!appStore.currentProjectId || appStore.currentProjectId === 'no_such_project') &&
!(route.name as string).startsWith(SettingRouteEnum.SETTING)
) {
// (, )访
router.push({ router.push({
name: NO_PROJECT_ROUTE_NAME, name: NO_PROJECT_ROUTE_NAME,
}); });

View File

@ -1,17 +1,18 @@
<template> <template>
<MsMinderEditor <MsMinderEditor
v-model:activeExtraKey="activeExtraKey"
:tags="tags" :tags="tags"
:import-json="props.importJson" :import-json="props.importJson"
:replaceable-tags="replaceableTags" :replaceable-tags="replaceableTags"
:insert-node="insertNode" :insert-node="insertNode"
:priority-disable-check="priorityDisableCheck" :priority-disable-check="priorityDisableCheck"
:after-tag-edit="afterTagEdit" :after-tag-edit="afterTagEdit"
:extract-content-tab-list="extractContentTabList"
single-tag single-tag
tag-enable tag-enable
sequence-enable sequence-enable
@click="handleNodeClick" @click="handleNodeClick"
> />
</MsMinderEditor>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -34,6 +35,25 @@
const tags = [...topTags, t('ms.minders.precondition'), ...descTags, t('ms.minders.stepExpect'), t('common.remark')]; const tags = [...topTags, t('ms.minders.precondition'), ...descTags, t('ms.minders.stepExpect'), t('common.remark')];
const visible = ref<boolean>(false); const visible = ref<boolean>(false);
const nodeData = ref<any>({}); const nodeData = ref<any>({});
const extractContentTabList = [
{
label: t('common.baseInfo'),
value: 'baseInfo',
},
{
label: t('caseManagement.featureCase.attachment'),
value: 'attachment',
},
{
value: 'comments',
label: t('caseManagement.featureCase.comments'),
},
{
value: 'bug',
label: t('caseManagement.featureCase.bug'),
},
];
const activeExtraKey = ref('baseInfo');
function handleNodeClick(data: any) { function handleNodeClick(data: any) {
if (data.resource && data.resource.includes(caseTag)) { if (data.resource && data.resource.includes(caseTag)) {

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="ms-minder-editor-container"> <div class="ms-minder-editor-container">
<div class="flex-1">
<minderHeader <minderHeader
:sequence-enable="props.sequenceEnable" :sequence-enable="props.sequenceEnable"
:tag-enable="props.tagEnable" :tag-enable="props.tagEnable"
@ -43,16 +44,21 @@
:priority-prefix="props.priorityPrefix" :priority-prefix="props.priorityPrefix"
:priority-start-with-zero="props.priorityStartWithZero" :priority-start-with-zero="props.priorityStartWithZero"
:insert-node="props.insertNode" :insert-node="props.insertNode"
@after-mount="emit('afterMount')" @after-mount="() => emit('afterMount')"
@save="save" @save="save"
@enter-node="handleEnterNode" @enter-node="handleEnterNode"
/> />
</div> </div>
<div v-if="props.extractContentTabList?.length" class="ms-minder-editor-extra-content">
<MsTab v-model:activeKey="activeExtraKey" :content-tab-list="props.extractContentTabList" mode="button" />
</div>
</div>
</template> </template>
<script lang="ts" name="minderEditor" setup> <script lang="ts" name="minderEditor" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import minderHeader from './main/header.vue'; import minderHeader from './main/header.vue';
import mainEditor from './main/mainEditor.vue'; import mainEditor from './main/mainEditor.vue';
@ -104,6 +110,10 @@
emit('enterNode', data); emit('enterNode', data);
} }
const activeExtraKey = defineModel<string>('activeExtraKey', {
default: '',
});
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
if (window.minder.on) { if (window.minder.on) {
@ -123,6 +133,14 @@
<style lang="less" scoped> <style lang="less" scoped>
.ms-minder-editor-container { .ms-minder-editor-container {
@apply relative h-full; @apply relative flex h-full;
.ms-minder-editor-extra-content {
@apply border-l;
padding: 16px;
width: 35%;
min-width: 360px;
border-color: var(--color-text-n8);
}
} }
</style> </style>

View File

@ -44,6 +44,7 @@ export const mainEditorProps = {
default: 500, default: 500,
}, },
disabled: Boolean, disabled: Boolean,
extractContentTabList: [] as PropType<{ label: string; value: string }[]>,
}; };
export const headerProps = { export const headerProps = {

View File

@ -1,5 +1,9 @@
<template> <template>
<a-tabs v-model:active-key="innerActiveKey" :class="[props.class, props.noContent ? 'no-content' : '']"> <a-tabs
v-if="props.mode === 'origin'"
v-model:active-key="innerActiveKey"
:class="[props.class, props.noContent ? 'no-content' : '']"
>
<a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="item.label"> <a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="item.label">
<template v-if="props.showBadge" #title> <template v-if="props.showBadge" #title>
<a-badge <a-badge
@ -18,11 +22,23 @@
</template> </template>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<div v-else class="ms-tab--button">
<div
v-for="item of props.contentTabList"
:key="item.value"
class="ms-tab--button-item"
:class="item.value === innerActiveKey ? 'ms-tab--button-item--active' : ''"
@click="innerActiveKey = item.value"
>
{{ item.label }}
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
mode?: 'origin' | 'button';
activeKey: string; activeKey: string;
contentTabList: { label: string; value: string }[]; contentTabList: { label: string; value: string }[];
class?: string; class?: string;
@ -31,6 +47,7 @@
showBadge?: boolean; showBadge?: boolean;
}>(), }>(),
{ {
mode: 'origin',
showBadge: true, showBadge: true,
getTextFunc: (value: any) => value, getTextFunc: (value: any) => value,
class: '', class: '',
@ -63,4 +80,35 @@
display: none; display: none;
} }
} }
.ms-tab--button {
@apply flex;
border-radius: var(--border-radius-small);
.ms-tab--button-item {
@apply cursor-pointer;
padding: 4px 12px;
border: 1px solid var(--color-text-n8);
color: var(--color-text-2);
&:first-child {
border-top-left-radius: var(--border-radius-small);
border-bottom-left-radius: var(--border-radius-small);
}
&:last-child {
border-top-right-radius: var(--border-radius-small);
border-bottom-right-radius: var(--border-radius-small);
}
&:not(:last-child) {
margin-right: -1px;
}
&:hover {
color: rgb(var(--primary-5));
}
}
.ms-tab--button-item--active {
z-index: 2;
border: 1px solid rgb(var(--primary-5)) !important;
color: rgb(var(--primary-5));
}
}
</style> </style>

View File

@ -247,7 +247,7 @@
wrapper-id="ms-table-footer-wrapper" wrapper-id="ms-table-footer-wrapper"
:size="props.paginationSize" :size="props.paginationSize"
@batch-action="handleBatchAction" @batch-action="handleBatchAction"
@clear="emit('clearSelector')" @clear="() => emit('clearSelector')"
/> />
</div> </div>
<ms-pagination <ms-pagination
@ -269,7 +269,7 @@
:show-subdirectory="!!attrs.showSubdirectory" :show-subdirectory="!!attrs.showSubdirectory"
@init-data="handleInitColumn" @init-data="handleInitColumn"
@page-size-change="pageSizeChange" @page-size-change="pageSizeChange"
@module-change="emit('moduleChange')" @module-change="() => emit('moduleChange')"
></ColumnSelector> ></ColumnSelector>
</div> </div>
</template> </template>

View File

@ -234,7 +234,7 @@ export interface AssertionConfig {
assertions: ExecuteAssertionItem[]; assertions: ExecuteAssertionItem[];
} }
export interface CsvVariable { export interface CsvVariable {
id: string; id?: string;
fileId: string; fileId: string;
scenarioId: string; scenarioId: string;
name: string; name: string;
@ -250,6 +250,8 @@ export interface CsvVariable {
allowQuotedData: boolean; allowQuotedData: boolean;
recycleOnEof: boolean; recycleOnEof: boolean;
stopThreadOnEof: boolean; stopThreadOnEof: boolean;
// 以下为前端字段
settingVisible: boolean;
} }
export interface CommonVariable { export interface CommonVariable {
id: string | number; id: string | number;

View File

@ -297,7 +297,7 @@ export const mockDefaultParams: MockParams = {
matchAll: true, matchAll: true,
}, },
body: { body: {
bodyType: RequestBodyFormat.FORM_DATA, bodyType: RequestBodyFormat.NONE,
formDataBody: { formDataBody: {
matchRules: [], matchRules: [],
matchAll: true, matchAll: true,
@ -348,7 +348,7 @@ export const mockDefaultParams: MockParams = {
uploadFileIds: [], uploadFileIds: [],
linkFileIds: [], linkFileIds: [],
}; };
export const makeDefaultParams = () => { export const makeMockDefaultParams = () => {
const defaultParams = cloneDeep(mockDefaultParams); const defaultParams = cloneDeep(mockDefaultParams);
defaultParams.id = Date.now().toString(); defaultParams.id = Date.now().toString();
defaultParams.mockMatchRule.body.formDataBody.matchRules.push({ defaultParams.mockMatchRule.body.formDataBody.matchRules.push({
@ -426,7 +426,6 @@ export const defaultNormalParamItem = {
}; };
// 场景-csv参数默认值 // 场景-csv参数默认值
export const defaultCsvParamItem: CsvVariable = { export const defaultCsvParamItem: CsvVariable = {
id: '',
fileId: '', fileId: '',
scenarioId: '', scenarioId: '',
name: '', name: '',
@ -442,4 +441,5 @@ export const defaultCsvParamItem: CsvVariable = {
allowQuotedData: false, allowQuotedData: false,
recycleOnEof: false, recycleOnEof: false,
stopThreadOnEof: false, stopThreadOnEof: false,
settingVisible: false,
}; };

View File

@ -125,6 +125,30 @@
/> />
</a-popover> </a-popover>
</template> </template>
<template #name="{ record, columnConfig, rowIndex }">
<a-popover
position="tl"
:disabled="!record[columnConfig.dataIndex as string] || record[columnConfig.dataIndex as string].trim() === ''"
class="ms-params-input-popover"
>
<template #content>
<div class="param-popover-title">
{{ t('apiTestDebug.paramName') }}
</div>
<div class="param-popover-value">
{{ record[columnConfig.dataIndex as string] }}
</div>
</template>
<a-input
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabledExceptParam || columnConfig.disabledColumn"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="ms-form-table-input"
size="mini"
@input="() => addTableLine(rowIndex, columnConfig.addLineDisabled)"
/>
</a-popover>
</template>
<!-- 参数类型 --> <!-- 参数类型 -->
<template #paramType="{ record, columnConfig, rowIndex }"> <template #paramType="{ record, columnConfig, rowIndex }">
<a-tooltip <a-tooltip
@ -242,7 +266,7 @@
input-class="ms-form-table-input h-[24px]" input-class="ms-form-table-input h-[24px]"
input-size="small" input-size="small"
tag-size="small" tag-size="small"
@change="(files, file) => handleFileChange(files, record, rowIndex, file)" @change="(files, file) => handleFilesChange(files, record, rowIndex, file)"
@delete-file="() => emitChange('deleteFile')" @delete-file="() => emitChange('deleteFile')"
/> />
<MsParamsInput <MsParamsInput
@ -251,14 +275,14 @@
:disabled="props.disabledParamValue" :disabled="props.disabledParamValue"
size="mini" size="mini"
@change="() => addTableLine(rowIndex, columnConfig.addLineDisabled)" @change="() => addTableLine(rowIndex, columnConfig.addLineDisabled)"
@dblclick="quickInputParams(record)" @dblclick="() => quickInputParams(record)"
@apply="() => addTableLine(rowIndex, columnConfig.addLineDisabled)" @apply="() => addTableLine(rowIndex, columnConfig.addLineDisabled)"
/> />
</template> </template>
<!-- 文件 --> <!-- 文件 -->
<template #file="{ record, rowIndex }"> <template #file="{ record, rowIndex }">
<MsAddAttachment <MsAddAttachment
v-model:file-list="record.files" :file-list="[record]"
:disabled="props.disabledParamValue" :disabled="props.disabledParamValue"
:multiple="false" :multiple="false"
mode="input" mode="input"
@ -321,7 +345,7 @@
:disabled="props.disabledExceptParam || columnConfig.disabledColumn" :disabled="props.disabledExceptParam || columnConfig.disabledColumn"
size="mini" size="mini"
@input="() => addTableLine(rowIndex)" @input="() => addTableLine(rowIndex)"
@dblclick="quickInputDesc(record)" @dblclick="() => quickInputDesc(record)"
@change="handleDescChange" @change="handleDescChange"
/> />
</template> </template>
@ -464,7 +488,7 @@
v-if="Array.isArray(record.domain)" v-if="Array.isArray(record.domain)"
:tag-list="getDomain(record.domain)" :tag-list="getDomain(record.domain)"
size="small" size="small"
@click="showHostModal(record)" @click="() => showHostModal(record)"
/> />
<div v-else class="text-[var(--color-text-1)]">{{ '-' }}</div> <div v-else class="text-[var(--color-text-1)]">{{ '-' }}</div>
</template> </template>
@ -924,7 +948,10 @@
emitChange('toggleRequired'); emitChange('toggleRequired');
} }
async function handleFileChange( /**
* 处理表格内多个文件上传/关联
*/
async function handleFilesChange(
files: MsFileItem[], files: MsFileItem[],
record: Record<string, any>, record: Record<string, any>,
rowIndex: number, rowIndex: number,
@ -957,6 +984,45 @@
})); }));
} }
addTableLine(rowIndex); addTableLine(rowIndex);
emitChange('handleFilesChange');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
}
/**
* 处理表格内单个文件上传/关联
*/
async function handleFileChange(
files: MsFileItem[],
record: Record<string, any>,
rowIndex: number,
file?: MsFileItem
) {
try {
if (props.uploadTempFileApi && file?.local) {
appStore.showLoading();
const res = await props.uploadTempFileApi(file.file);
record = {
...record,
...file,
fileId: res.data,
fileName: file.name || '',
fileAlias: file.name || '',
};
} else if (file) {
record = {
...record,
...file,
fileId: file.uid || file.fileId || '',
fileName: file.originalName || '',
fileAlias: file.name || '',
};
}
addTableLine(rowIndex);
emitChange('handleFileChange'); emitChange('handleFileChange');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -11,7 +11,7 @@
:default-param-item="defaultHeaderParamsItem" :default-param-item="defaultHeaderParamsItem"
:draggable="!props.disabledExceptParam" :draggable="!props.disabledExceptParam"
@change="handleParamTableChange" @change="handleParamTableChange"
@batch-add="batchAddKeyValVisible = true" @batch-add="() => (batchAddKeyValVisible = true)"
/> />
<batchAddKeyVal <batchAddKeyVal
v-model:visible="batchAddKeyValVisible" v-model:visible="batchAddKeyValVisible"

View File

@ -281,7 +281,7 @@
async function initModuleCount() { async function initModuleCount() {
try { try {
const res = await getDebugModuleCount({ const res = await getDebugModuleCount({
keyword: moduleKeyword.value, keyword: '',
}); });
modulesCount.value = res; modulesCount.value = res;
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => { folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {

View File

@ -10,7 +10,7 @@
@open-api-tab="(record, isExecute) => openApiTab({ apiInfo: record, isCopy: false, isExecute })" @open-api-tab="(record, isExecute) => openApiTab({ apiInfo: record, isCopy: false, isExecute })"
@open-copy-api-tab="openApiTab({ apiInfo: $event, isCopy: true })" @open-copy-api-tab="openApiTab({ apiInfo: $event, isCopy: true })"
@add-api-tab="addApiTab" @add-api-tab="addApiTab"
@import="emit('import')" @import="() => emit('import')"
@open-edit-api-tab="openApiTab" @open-edit-api-tab="openApiTab"
/> />
</div> </div>
@ -156,6 +156,7 @@
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { import {
RequestAuthType, RequestAuthType,
RequestBodyFormat,
RequestComposition, RequestComposition,
RequestDefinitionStatus, RequestDefinitionStatus,
RequestMethods, RequestMethods,
@ -348,6 +349,7 @@
loadedApiTab = { loadedApiTab = {
...loadedApiTab, ...loadedApiTab,
...(apiInfo as ApiDefinitionDetail).request, ...(apiInfo as ApiDefinitionDetail).request,
activeTab: (apiInfo as ApiDefinitionDetail).activeTab,
}; };
} }
// tabtab // tabtab
@ -385,6 +387,7 @@
request = { request = {
...res.request, ...res.request,
...(apiInfo as ApiDefinitionDetail).request, ...(apiInfo as ApiDefinitionDetail).request,
activeTab: (apiInfo as ApiDefinitionDetail).activeTab,
}; };
} }
addApiTab({ addApiTab({
@ -417,6 +420,16 @@
} }
async function openApiTabAndDebugMock(mock: MockDetail) { async function openApiTabAndDebugMock(mock: MockDetail) {
let activeTab = RequestComposition.BODY;
if (mock.mockMatchRule.body.bodyType !== RequestBodyFormat.NONE) {
activeTab = RequestComposition.BODY;
} else if (mock.mockMatchRule.header.matchRules.length > 0) {
activeTab = RequestComposition.HEADER;
} else if (mock.mockMatchRule.query.matchRules.length > 0) {
activeTab = RequestComposition.QUERY;
} else if (mock.mockMatchRule.rest.matchRules.length > 0) {
activeTab = RequestComposition.REST;
}
openApiTab({ openApiTab({
apiInfo: { apiInfo: {
id: mock.apiDefinitionId as string, id: mock.apiDefinitionId as string,
@ -465,6 +478,7 @@
value: e.value, value: e.value,
})) || [], })) || [],
}, },
activeTab,
} as unknown as ApiDefinitionDetail, } as unknown as ApiDefinitionDetail,
isExecute: true, isExecute: true,
isDebugMock: true, isDebugMock: true,

View File

@ -351,14 +351,12 @@
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
@apply flex-1 pt-0; @apply flex-1 pt-0;
.arco-tabs-content-item { .arco-tabs-content-item {
@apply px-0; @apply overflow-y-auto px-0;
.ms-scroll-bar();
} }
.arco-tabs-content-list { .arco-tabs-content-list {
@apply h-full; @apply h-full;
} }
.arco-tabs-content-list .arco-tabs-content-item:nth-of-type(1) .arco-tabs-pane {
@apply h-full overflow-hidden;
}
.arco-collapse { .arco-collapse {
height: calc(100% - 85px); height: calc(100% - 85px);
} }

View File

@ -59,7 +59,7 @@
</div> </div>
</template> </template>
</MsDetailCard> </MsDetailCard>
<a-form ref="mockForm" :model="mockDetail" :disabled="isReadOnly"> <a-form ref="mockFormRef" :model="mockDetail" :disabled="isReadOnly">
<a-form-item <a-form-item
class="hidden-item mb-[16px]" class="hidden-item mb-[16px]"
field="name" field="name"
@ -196,7 +196,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Message } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
@ -234,7 +234,7 @@
defaultHeaderParamsItem, defaultHeaderParamsItem,
defaultMatchRuleItem, defaultMatchRuleItem,
defaultRequestParamsItem, defaultRequestParamsItem,
makeDefaultParams, makeMockDefaultParams,
} from '@/views/api-test/components/config'; } from '@/views/api-test/components/config';
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils'; import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
@ -258,7 +258,7 @@
const loading = ref(false); const loading = ref(false);
const isEdit = ref(props.isEditMode); const isEdit = ref(props.isEditMode);
const mockDetail = ref<MockParams>(makeDefaultParams()); const mockDetail = ref<MockParams>(makeMockDefaultParams());
const isReadOnly = computed(() => !mockDetail.value.isNew && !isEdit.value); const isReadOnly = computed(() => !mockDetail.value.isNew && !isEdit.value);
const title = computed(() => { const title = computed(() => {
if (isReadOnly.value) { if (isReadOnly.value) {
@ -538,13 +538,17 @@
appendDefaultMatchRuleItem(); appendDefaultMatchRuleItem();
} }
isEdit.value = !!props.isEditMode; isEdit.value = !!props.isEditMode;
if (props.isEditMode) {
//
appendDefaultMatchRuleItem();
}
if (mockDetail.value.mockMatchRule.body.bodyType !== RequestBodyFormat.NONE) { if (mockDetail.value.mockMatchRule.body.bodyType !== RequestBodyFormat.NONE) {
activeTab.value = RequestComposition.BODY; activeTab.value = RequestComposition.BODY;
} else if (mockDetail.value.mockMatchRule.header.matchRules.length > 0) { } else if (mockDetail.value.mockMatchRule.header.matchRules.length > 0) {
activeTab.value = RequestComposition.HEADER; activeTab.value = RequestComposition.HEADER;
} else if (mockDetail.value.mockMatchRule.query) { } else if (mockDetail.value.mockMatchRule.query.matchRules.length > 0) {
activeTab.value = RequestComposition.QUERY; activeTab.value = RequestComposition.QUERY;
} else if (mockDetail.value.mockMatchRule.rest) { } else if (mockDetail.value.mockMatchRule.rest.matchRules.length > 0) {
activeTab.value = RequestComposition.REST; activeTab.value = RequestComposition.REST;
} }
} catch (error) { } catch (error) {
@ -609,7 +613,7 @@
} }
function handleCancel() { function handleCancel() {
mockDetail.value = makeDefaultParams(); mockDetail.value = makeMockDefaultParams();
isEdit.value = false; isEdit.value = false;
visible.value = false; visible.value = false;
} }
@ -624,7 +628,10 @@
handleCancel(); handleCancel();
} }
async function handleSave(isContinue = false) { const mockFormRef = ref<FormInstance>();
function handleSave(isContinue = false) {
mockFormRef.value?.validate(async (errors) => {
if (!errors) {
try { try {
loading.value = true; loading.value = true;
const { body } = mockDetail.value.mockMatchRule; const { body } = mockDetail.value.mockMatchRule;
@ -709,7 +716,8 @@
} }
emit('addDone'); emit('addDone');
if (isContinue) { if (isContinue) {
mockDetail.value = makeDefaultParams(); mockDetail.value.name = '';
mockDetail.value.tags = [];
} else { } else {
handleCancel(); handleCancel();
} }
@ -720,6 +728,8 @@
loading.value = false; loading.value = false;
} }
} }
});
}
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -163,7 +163,7 @@
<template v-else-if="currentSelectedDefinitionResponse"> <template v-else-if="currentSelectedDefinitionResponse">
<MsTab <MsTab
v-model:active-key="definitionActiveTab" v-model:active-key="definitionActiveTab"
:content-tab-list="responseCompositionTabList.filter((e) => e.value !== 'DELAY')" :content-tab-list="responseCompositionTabList"
class="no-content relative my-[8px] border-b" class="no-content relative my-[8px] border-b"
:show-badge="false" :show-badge="false"
/> />
@ -286,6 +286,19 @@
disabled disabled
@change="() => emit('change')" @change="() => emit('change')"
/> />
<a-input-number
v-else
v-model:model-value="mockResponse.delay"
:disabled="props.disabled"
mode="button"
:step="100"
:precision="0"
:max="600000"
:min="0"
class="w-[200px]"
>
<template #suffix> ms </template>
</a-input-number>
</div> </div>
</template> </template>
</a-spin> </a-spin>

View File

@ -299,7 +299,7 @@
}; };
}); });
const moduleKeyword = ref(''); const moduleKeyword = ref(''); //
const folderTree = ref<ModuleTreeNode[]>([]); const folderTree = ref<ModuleTreeNode[]>([]);
const focusNodeKey = ref<string | number>(''); const focusNodeKey = ref<string | number>('');
const selectedKeys = ref<Array<string | number>>([props.activeModule]); const selectedKeys = ref<Array<string | number>>([props.activeModule]);
@ -371,7 +371,7 @@
const isExpandApi = ref(false); const isExpandApi = ref(false);
const lastModuleCountParam = ref<ApiDefinitionGetModuleParams>({ const lastModuleCountParam = ref<ApiDefinitionGetModuleParams>({
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
keyword: moduleKeyword.value, keyword: '',
protocol: moduleProtocol.value, protocol: moduleProtocol.value,
moduleIds: [], moduleIds: [],
}); });
@ -427,7 +427,7 @@
if (props.trash) { if (props.trash) {
res = await getTrashModuleTree({ res = await getTrashModuleTree({
// //
keyword: moduleKeyword.value, keyword: '',
protocol: moduleProtocol.value, protocol: moduleProtocol.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: [], moduleIds: [],
@ -435,7 +435,7 @@
} else if (isExpandApi.value && !props.readOnly) { } else if (isExpandApi.value && !props.readOnly) {
// //
res = await getModuleTree({ res = await getModuleTree({
keyword: moduleKeyword.value, keyword: '',
protocol: moduleProtocol.value, protocol: moduleProtocol.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: [], moduleIds: [],
@ -443,7 +443,7 @@
} else { } else {
res = await getModuleTreeOnlyModules({ res = await getModuleTreeOnlyModules({
// //
keyword: moduleKeyword.value, keyword: '',
protocol: moduleProtocol.value, protocol: moduleProtocol.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: [], moduleIds: [],

View File

@ -27,8 +27,8 @@
:default-param-item="defaultNormalParamItem" :default-param-item="defaultNormalParamItem"
:draggable="false" :draggable="false"
:selectable="false" :selectable="false"
@change="handleParamTableChange" @change="handleCommonVariablesChange"
@batch-add="batchAddKeyValVisible = true" @batch-add="() => (batchAddKeyValVisible = true)"
/> />
<paramTable <paramTable
v-else v-else
@ -37,12 +37,19 @@
:default-param-item="defaultCsvParamItem" :default-param-item="defaultCsvParamItem"
:draggable="false" :draggable="false"
:selectable="false" :selectable="false"
@change="handleParamTableChange" @change="handleCsvVariablesChange"
@batch-add="batchAddKeyValVisible = true" @batch-add="() => (batchAddKeyValVisible = true)"
> >
<template #operationPre="{ record }"> <template #operationPre="{ record }">
<a-trigger trigger="click" position="br" class="scenario-csv-trigger"> <a-trigger
<MsButton type="text" class="!mr-0">{{ t('apiScenario.params.config') }}</MsButton> v-model:popup-visible="record.settingVisible"
trigger="click"
position="br"
class="scenario-csv-trigger"
>
<MsButton type="text" class="!mr-0" @click="handleRecordConfig(record)">
{{ t('apiScenario.params.config') }}
</MsButton>
<template #content> <template #content>
<div class="scenario-csv-trigger-content"> <div class="scenario-csv-trigger-content">
<div class="mb-[16px] flex items-center"> <div class="mb-[16px] flex items-center">
@ -50,32 +57,32 @@
<!-- <div class="text-[var(--color-text-4)]">({{ record.key }})</div> --> <!-- <div class="text-[var(--color-text-4)]">({{ record.key }})</div> -->
</div> </div>
<div class="scenario-csv-trigger-content-scroll"> <div class="scenario-csv-trigger-content-scroll">
<a-form ref="paramFormRef" :model="record" layout="vertical"> <a-form ref="paramFormRef" :model="paramForm" layout="vertical">
<a-form-item <a-form-item
field="key" field="name"
:label="t('apiScenario.params.csvName')" :label="t('apiScenario.params.csvName')"
:rules="[{ required: true, message: t('apiScenario.params.csvNameNotNull') }]" :rules="[{ required: true, message: t('apiScenario.params.csvNameNotNull') }]"
asterisk-position="end" asterisk-position="end"
class="mb-[16px]" class="mb-[16px]"
> >
<a-input v-model:model-value="record.key" :max-length="255"></a-input> <a-input v-model:model-value="paramForm.name" :max-length="255"></a-input>
</a-form-item> </a-form-item>
<a-form-item field="variableNames" :label="t('apiScenario.params.csvParamName')" class="mb-[16px]"> <a-form-item field="variableNames" :label="t('apiScenario.params.csvParamName')" class="mb-[16px]">
<a-input <a-input
v-model:model-value="record.variableNames" v-model:model-value="paramForm.variableNames"
:placeholder="t('apiScenario.params.csvParamNamePlaceholder')" :placeholder="t('apiScenario.params.csvParamNamePlaceholder')"
></a-input> ></a-input>
</a-form-item> </a-form-item>
<a-form-item field="encoding" :label="t('apiScenario.params.csvFileCode')" class="mb-[16px]"> <a-form-item field="encoding" :label="t('apiScenario.params.csvFileCode')" class="mb-[16px]">
<a-select <a-select
v-model:model-value="record.encoding" v-model:model-value="paramForm.encoding"
:options="encodingOptions" :options="encodingOptions"
class="w-[120px]" class="w-[120px]"
></a-select> ></a-select>
</a-form-item> </a-form-item>
<a-form-item field="delimiter" :label="t('apiScenario.params.csvSplitChar')" class="mb-[16px]"> <a-form-item field="delimiter" :label="t('apiScenario.params.csvSplitChar')" class="mb-[16px]">
<a-input <a-input
v-model:model-value="record.delimiter" v-model:model-value="paramForm.delimiter"
:placeholder="t('common.pleaseInput')" :placeholder="t('common.pleaseInput')"
:max-length="64" :max-length="64"
class="w-[120px]" class="w-[120px]"
@ -86,37 +93,41 @@
:label="t('apiScenario.params.csvIgnoreFirstLine')" :label="t('apiScenario.params.csvIgnoreFirstLine')"
class="mb-[16px]" class="mb-[16px]"
> >
<a-radio-group v-model:model-value="record.ignoreFirstLine"> <a-radio-group v-model:model-value="paramForm.ignoreFirstLine">
<a-radio :value="false">False</a-radio> <a-radio :value="false">False</a-radio>
<a-radio :value="true">True</a-radio> <a-radio :value="true">True</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item field="random" :label="t('apiScenario.params.csvIsRandom')" class="mb-[16px]"> <a-form-item field="random" :label="t('apiScenario.params.csvIsRandom')" class="mb-[16px]">
<a-radio-group v-model:model-value="record.random"> <a-radio-group v-model:model-value="paramForm.random">
<a-radio :value="false">False</a-radio> <a-radio :value="false">False</a-radio>
<a-radio :value="true">True</a-radio> <a-radio :value="true">True</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item field="allowQuotedData" :label="t('apiScenario.params.csvQuoteAllow')" class="mb-[16px]"> <a-form-item field="allowQuotedData" :label="t('apiScenario.params.csvQuoteAllow')" class="mb-[16px]">
<a-radio-group v-model:model-value="record.allowQuotedData"> <a-radio-group v-model:model-value="paramForm.allowQuotedData">
<a-radio :value="false">False</a-radio> <a-radio :value="false">False</a-radio>
<a-radio :value="true">True</a-radio> <a-radio :value="true">True</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item field="recycleOnEof" :label="t('apiScenario.params.csvRecycle')" class="mb-[16px]"> <a-form-item field="recycleOnEof" :label="t('apiScenario.params.csvRecycle')" class="mb-[16px]">
<a-radio-group v-model:model-value="record.recycleOnEof"> <a-radio-group v-model:model-value="paramForm.recycleOnEof">
<a-radio :value="false">False</a-radio> <a-radio :value="false">False</a-radio>
<a-radio :value="true">True</a-radio> <a-radio :value="true">True</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item field="stopThreadOnEof" :label="t('apiScenario.params.csvStop')" class="mb-[16px]"> <a-form-item field="stopThreadOnEof" :label="t('apiScenario.params.csvStop')" class="mb-[16px]">
<a-radio-group v-model:model-value="record.stopThreadOnEof"> <a-radio-group v-model:model-value="paramForm.stopThreadOnEof">
<a-radio :value="false">False</a-radio> <a-radio :value="false">False</a-radio>
<a-radio :value="true">True</a-radio> <a-radio :value="true">True</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
<div class="flex items-center justify-end gap-[8px]">
<a-button type="secondary" @click="cancelConfig">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="applyConfig">{{ t('ms.paramsInput.apply') }}</a-button>
</div>
</div> </div>
</template> </template>
</a-trigger> </a-trigger>
@ -134,6 +145,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { FormInstance } from '@arco-design/web-vue'; import { FormInstance } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue'; import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
@ -215,7 +227,7 @@
}, },
]; ];
function handleParamTableChange(resultArr: any[], isInit?: boolean) { function handleCommonVariablesChange(resultArr: any[], isInit?: boolean) {
commonVariables.value = [...resultArr]; commonVariables.value = [...resultArr];
if (!isInit) { if (!isInit) {
emit('change'); emit('change');
@ -223,6 +235,14 @@
} }
} }
function handleCsvVariablesChange(resultArr: any[], isInit?: boolean) {
csvVariables.value = [...resultArr];
if (!isInit) {
emit('change');
firstSearch.value = true;
}
}
// //
function handleSearch() { function handleSearch() {
if (firstSearch.value) { if (firstSearch.value) {
@ -255,8 +275,8 @@
const csvColumns: ParamTableColumn[] = [ const csvColumns: ParamTableColumn[] = [
{ {
title: 'apiScenario.params.csvName', title: 'apiScenario.params.csvName',
dataIndex: 'key', dataIndex: 'name',
slotName: 'key', slotName: 'name',
needValidRepeat: true, needValidRepeat: true,
}, },
{ {
@ -277,11 +297,11 @@
titleSlotName: 'typeTitle', titleSlotName: 'typeTitle',
typeTitleTooltip: [t('apiScenario.params.csvScopedTip1'), t('apiScenario.params.csvScopedTip2')], typeTitleTooltip: [t('apiScenario.params.csvScopedTip1'), t('apiScenario.params.csvScopedTip2')],
}, },
// { {
// title: 'apiScenario.params.file', title: 'apiScenario.params.file',
// dataIndex: 'file', dataIndex: 'file',
// slotName: 'file', slotName: 'file',
// }, },
{ {
title: 'apiScenario.table.columns.status', title: 'apiScenario.table.columns.status',
dataIndex: 'enable', dataIndex: 'enable',
@ -295,7 +315,8 @@
}, },
]; ];
const configFormRef = ref<FormInstance>(); const paramFormRef = ref<FormInstance>();
const paramForm = ref<CsvVariable>(cloneDeep(defaultCsvParamItem));
const encodingOptions = [ const encodingOptions = [
{ {
label: 'UTF-8', label: 'UTF-8',
@ -305,6 +326,10 @@
label: 'UTF-16', label: 'UTF-16',
value: 'UTF-16', value: 'UTF-16',
}, },
{
label: 'GBK',
value: 'GBK',
},
{ {
label: 'ISO-8859-15', label: 'ISO-8859-15',
value: 'ISO-8859-15', value: 'ISO-8859-15',
@ -314,6 +339,31 @@
value: 'US-ASCII', value: 'US-ASCII',
}, },
]; ];
function handleRecordConfig(record: CsvVariable) {
paramForm.value = cloneDeep(record);
}
function cancelConfig() {
paramFormRef.value?.resetFields();
}
function applyConfig() {
paramFormRef.value?.validate((errors) => {
if (!errors) {
csvVariables.value = csvVariables.value.map((e) => {
if (e.id === paramForm.value.id) {
return {
...paramForm.value,
settingVisible: false,
};
}
return e;
});
emit('change');
}
});
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -273,7 +273,7 @@
try { try {
loading.value = true; loading.value = true;
const res = await getModuleTree({ const res = await getModuleTree({
keyword: moduleKeyword.value, keyword: '',
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
moduleIds: [], moduleIds: [],
}); });

View File

@ -50,7 +50,7 @@
v-on="propsEvent" v-on="propsEvent"
@batch-action="handleTableBatch" @batch-action="handleTableBatch"
@change="changeHandler" @change="changeHandler"
@module-change="initData()" @module-change="initData"
@cell-click="handleCellClick" @cell-click="handleCellClick"
> >
<template #num="{ record }"> <template #num="{ record }">
@ -216,7 +216,7 @@
</MsTag> </MsTag>
</div> </div>
</div> </div>
<div class="h-[calc(100%-32px)]"> <div class="mt-[16px] h-[calc(100%-32px)] border-t border-[var(--color-text-n8)]">
<!-- 脑图开始 --> <!-- 脑图开始 -->
<MsMinder minder-type="FeatureCase" :import-json="importJson" @node-click="handleNodeClick" /> <MsMinder minder-type="FeatureCase" :import-json="importJson" @node-click="handleNodeClick" />
<MsDrawer v-model:visible="visible" :width="480" :mask="false"> <MsDrawer v-model:visible="visible" :width="480" :mask="false">

View File

@ -103,6 +103,7 @@
import { getFirstRouteNameByPermission, routerNameHasPermission } from '@/utils/permission'; import { getFirstRouteNameByPermission, routerNameHasPermission } from '@/utils/permission';
import type { LoginData } from '@/models/user'; import type { LoginData } from '@/models/user';
import { SettingRouteEnum } from '@/enums/routeEnum';
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'; import { ValidatedError } from '@arco-design/web-vue/es/form/interface';
@ -176,8 +177,11 @@
const { username, password } = values; const { username, password } = values;
loginConfig.value.username = rememberPassword ? username : ''; loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : ''; loginConfig.value.password = rememberPassword ? password : '';
if (!appStore.currentProjectId || appStore.currentProjectId === 'no_such_project') { if (
// &/ (!appStore.currentProjectId || appStore.currentProjectId === 'no_such_project') &&
!(router.currentRoute.value as unknown as string).startsWith(SettingRouteEnum.SETTING)
) {
// &/访
router.push({ router.push({
name: NO_PROJECT_ROUTE_NAME, name: NO_PROJECT_ROUTE_NAME,
}); });