feat(接口测试): 请求参数的参数名称唯一的校验(请求头,请求体,Query,REST)

This commit is contained in:
teukkk 2024-04-12 11:52:31 +08:00 committed by Craftsman
parent ce975229c9
commit 087bfb417f
12 changed files with 290 additions and 134 deletions

View File

@ -1,148 +1,168 @@
<template>
<MsBaseTable
v-bind="propsRes"
:hoverable="false"
is-simple-setting
:span-method="props.spanMethod"
:class="!props.selectable && !props.draggable ? 'ms-form-table-no-left-action' : ''"
v-on="propsEvent"
@drag-change="tableChange"
>
<!-- 展开行-->
<template #expand-icon="{ expanded, record }">
<div class="flex items-center gap-[2px] text-[var(--color-text-4)]">
<MsIcon :type="expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'" />
<div v-if="record.children">{{ record.children.length }}</div>
</div>
</template>
<template
v-for="item of props.columns.filter((e) => e.slotName !== undefined)"
#[item.slotName!]="{ record, rowIndex, column }"
<a-form ref="formRef" :model="propsRes">
<MsBaseTable
v-bind="propsRes"
:hoverable="false"
is-simple-setting
:span-method="props.spanMethod"
:class="!props.selectable && !props.draggable ? 'ms-form-table-no-left-action' : ''"
v-on="propsEvent"
@drag-change="tableChange"
@init-end="validateAndUpdateErrorMessageList"
>
<slot :name="item.slotName" v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }">
<a-tooltip
v-if="item.hasRequired"
:content="t(record.required ? 'msFormTable.paramRequired' : 'msFormTable.paramNotRequired')"
<!-- 展开行-->
<template #expand-icon="{ expanded, record }">
<div class="flex items-center gap-[2px] text-[var(--color-text-4)]">
<MsIcon :type="expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'" />
<div v-if="record.children">{{ record.children.length }}</div>
</div>
</template>
<template
v-for="item of props.columns.filter((e) => e.slotName !== undefined)"
:key="item.toString()"
#[item.slotName!]="{ record, rowIndex, column }"
>
<a-form-item
:field="`data[${rowIndex}].${item.dataIndex}`"
label=""
:rules="{
validator: (value, callback) => {
validRepeat(rowIndex, item.dataIndex as string, value, callback);
},
}"
>
<MsButton
type="icon"
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
size="mini"
@click="toggleRequired(record, rowIndex, item)"
<slot
:name="item.slotName"
v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }"
>
<div>*</div>
</MsButton>
</a-tooltip>
<a-input
v-if="item.inputType === 'input'"
v-model:model-value="record[item.dataIndex as string]"
:placeholder="t(item.locale)"
class="ms-form-table-input"
:max-length="255"
size="mini"
@input="() => handleFormChange(record, rowIndex, item)"
/>
<a-select
v-else-if="item.inputType === 'select'"
v-model:model-value="record[item.dataIndex as string]"
:options="item.typeOptions || []"
class="ms-form-table-input w-full"
size="mini"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<MsTagsInput
v-else-if="item.inputType === 'tags'"
v-model:model-value="record[item.dataIndex as string]"
:max-tag-count="1"
input-class="ms-form-table-input"
size="mini"
@change="() => handleFormChange(record, rowIndex, item)"
@clear="() => handleFormChange(record, rowIndex, item)"
/>
<a-switch
v-else-if="item.inputType === 'switch'"
v-model:model-value="record[item.dataIndex as string]"
size="small"
class="ms-form-table-input-switch"
type="line"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<a-checkbox
v-else-if="item.inputType === 'checkbox'"
v-model:model-value="record[item.dataIndex as string]"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<template v-else-if="item.inputType === 'autoComplete'">
<a-auto-complete
v-model:model-value="record[item.dataIndex as string]"
:data="item.autoCompleteParams?.filter((e) => e.isShow === true)"
class="ms-form-table-input"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
:filter-option="false"
size="small"
@search="(val) => handleSearchParams(val, item)"
@change="() => handleFormChange(record, rowIndex, item)"
@select="(val) => selectAutoComplete(val, record, item)"
>
<template #option="{ data: opt }">
<div class="w-[350px]">
{{ opt.raw.value }}
<a-tooltip :content="t(opt.raw.desc)" position="bl" :mouse-enter-delay="300">
<div class="one-line-text max-w-full text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t(opt.raw.desc) }}
<a-tooltip
v-if="item.hasRequired"
:content="t(record.required ? 'msFormTable.paramRequired' : 'msFormTable.paramNotRequired')"
>
<MsButton
type="icon"
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
size="mini"
@click="toggleRequired(record, rowIndex, item)"
>
<div>*</div>
</MsButton>
</a-tooltip>
<a-input
v-if="item.inputType === 'input'"
v-model:model-value="record[item.dataIndex as string]"
:placeholder="t(item.locale)"
class="ms-form-table-input"
:max-length="255"
size="mini"
@input="() => handleFormChange(record, rowIndex, item)"
/>
<a-select
v-else-if="item.inputType === 'select'"
v-model:model-value="record[item.dataIndex as string]"
:options="item.typeOptions || []"
class="ms-form-table-input w-full"
size="mini"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<MsTagsInput
v-else-if="item.inputType === 'tags'"
v-model:model-value="record[item.dataIndex as string]"
:max-tag-count="1"
input-class="ms-form-table-input"
size="mini"
@change="() => handleFormChange(record, rowIndex, item)"
@clear="() => handleFormChange(record, rowIndex, item)"
/>
<a-switch
v-else-if="item.inputType === 'switch'"
v-model:model-value="record[item.dataIndex as string]"
size="small"
class="ms-form-table-input-switch"
type="line"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<a-checkbox
v-else-if="item.inputType === 'checkbox'"
v-model:model-value="record[item.dataIndex as string]"
@change="() => handleFormChange(record, rowIndex, item)"
/>
<template v-else-if="item.inputType === 'autoComplete'">
<a-auto-complete
v-model:model-value="record[item.dataIndex as string]"
:data="item.autoCompleteParams?.filter((e) => e.isShow === true)"
class="ms-form-table-input"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
:filter-option="false"
size="small"
@search="(val) => handleSearchParams(val, item)"
@change="() => handleFormChange(record, rowIndex, item)"
@select="(val) => selectAutoComplete(val, record, item)"
>
<template #option="{ data: opt }">
<div class="w-[350px]">
{{ opt.raw.value }}
<a-tooltip :content="t(opt.raw.desc)" position="bl" :mouse-enter-delay="300">
<div class="one-line-text max-w-full text-[12px] leading-[16px] text-[var(--color-text-4)]">
{{ t(opt.raw.desc) }}
</div>
</a-tooltip>
</div>
</a-tooltip>
</template>
</a-auto-complete>
</template>
<template v-else-if="item.inputType === 'text'">
{{
typeof item.valueFormat === 'function'
? item.valueFormat(record)
: record[item.dataIndex as string] || '-'
}}
</template>
<template v-else-if="item.dataIndex === 'action'">
<div
:key="item.dataIndex"
class="flex flex-row items-center"
:class="{ 'justify-end': item.align === 'right' }"
>
<slot
name="operationPre"
v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }"
></slot>
<MsTableMoreAction
v-if="item.moreAction"
:list="getMoreActionList(item.moreAction, record)"
@select="(e) => handleMoreActionSelect(e, record, item, rowIndex)"
/>
<icon-minus-circle
v-if="dataLength > 1 && rowIndex !== dataLength - 1"
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(record, rowIndex)"
/>
</div>
</template>
</a-auto-complete>
</template>
<template v-else-if="item.inputType === 'text'">
{{
typeof item.valueFormat === 'function' ? item.valueFormat(record) : record[item.dataIndex as string] || '-'
}}
</template>
<template v-else-if="item.dataIndex === 'action'">
<div
:key="item.dataIndex"
class="flex flex-row items-center"
:class="{ 'justify-end': item.align === 'right' }"
>
<slot
name="operationPre"
v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }"
></slot>
<MsTableMoreAction
v-if="item.moreAction"
:list="getMoreActionList(item.moreAction, record)"
@select="(e) => handleMoreActionSelect(e, record, item, rowIndex)"
/>
<icon-minus-circle
v-if="dataLength > 1 && rowIndex !== dataLength - 1"
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(record, rowIndex)"
/>
</div>
</template>
</slot>
</template>
<template
v-for="item of props.columns.filter((e) => e.titleSlotName !== undefined)"
#[item.titleSlotName!]="{ record, rowIndex, column }"
>
<slot
:name="item.titleSlotName"
v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }"
</slot>
</a-form-item>
</template>
<template
v-for="item of props.columns.filter((e) => e.titleSlotName !== undefined)"
#[item.titleSlotName!]="{ record, rowIndex, column }"
>
</slot>
</template>
</MsBaseTable>
<slot
:name="item.titleSlotName"
v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }"
>
</slot>
</template>
</MsBaseTable>
</a-form>
</template>
<script setup lang="ts">
import { FormInstance } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -167,6 +187,7 @@
required?: boolean; //
inputType?: 'input' | 'select' | 'tags' | 'switch' | 'text' | 'checkbox' | 'autoComplete'; //
autoCompleteParams?: SelectOptionData[]; //
needValidRepeat?: boolean; //
valueFormat?: (record: Record<string, any>) => string; // inputTypetext
[key: string]: any; //
}
@ -258,6 +279,35 @@
const dataLength = computed(() => propsRes.value.data.length);
//
const formRef = ref<FormInstance>();
async function validRepeat(rowIndex: number, dataIndex: string, value: any, callback: (error?: string) => void) {
const currentColumn = props.columns.find((item) => item.dataIndex === dataIndex);
if (!currentColumn?.needValidRepeat) {
callback();
return;
}
propsRes.value.data?.forEach((row, index) => {
if (row[dataIndex].length && index !== rowIndex && row[dataIndex] === value) {
callback(`${t(currentColumn?.title as string)}${t('msFormTable.paramRepeatMessage')}`);
}
});
callback();
}
//
const setErrorMessageList: ((params: string[]) => void) | undefined = inject('setErrorMessageList', undefined);
const errorMessageList = ref<string[]>([]); //
async function validateAndUpdateErrorMessageList() {
if (typeof setErrorMessageList === 'function' && props.columns.some((item) => item.needValidRepeat)) {
await nextTick();
formRef.value?.validate((errors) => {
errorMessageList.value = !errors ? [] : [...new Set(Object.values(errors).map(({ message }) => message))];
setErrorMessageList(errorMessageList.value);
});
}
}
watch(
() => selectedKeys.value,
(arr) => {
@ -276,6 +326,7 @@
() => props.data,
(arr) => {
propsRes.value.data = arr as any[];
validateAndUpdateErrorMessageList();
},
{
immediate: true,
@ -468,4 +519,20 @@
line-height: 16px;
color: var(--color-text-1);
}
:deep(.arco-form-item) {
margin-bottom: 0;
.arco-form-item-label-col {
display: none;
}
.arco-form-item-content,
.arco-form-item-wrapper-col {
min-height: auto;
}
.arco-form-item-message {
margin-bottom: 0;
}
.arco-form-item-content-flex {
flex-wrap: nowrap;
}
}
</style>

View File

@ -1,4 +1,5 @@
export default {
'msFormTable.paramRequired': '必填',
'msFormTable.paramNotRequired': '非必填',
'msFormTable.paramRepeatMessage': '唯一,不能重复',
};

View File

@ -331,6 +331,7 @@
(e: 'clearSelector'): void;
(e: 'filterChange', dataIndex: string, value: (string | number)[], multiple: boolean, isCustomParam: boolean): void;
(e: 'moduleChange'): void;
(e: 'initEnd'): void;
}>();
const attrs = useAttrs();
@ -408,6 +409,7 @@
scrollObj.value = {};
currentColumns.value = arr || tmpArr;
}
emit('initEnd');
} catch (error) {
// eslint-disable-next-line no-console
console.error('InitColumn failed', error);

View File

@ -208,6 +208,7 @@
{
title: 'apiTestDebug.paramName',
dataIndex: 'key',
needValidRepeat: true,
slotName: 'key',
width: 240,
},

View File

@ -59,6 +59,7 @@
dataIndex: 'key',
slotName: 'key',
inputType: 'autoComplete',
needValidRepeat: true,
autoCompleteParams: responseHeaderOption,
},
{

View File

@ -496,6 +496,12 @@
() => import('@/views/api-test/management/components/addDependencyDrawer.vue')
);
export interface TabErrorMessage {
value: string;
label: string;
messageList: string[];
}
export interface RequestCustomAttr {
type: 'api' | 'case' | 'mock' | 'doc'; // tab api
isNew: boolean;
@ -505,6 +511,9 @@
executeLoading: boolean; // loading
isCopy?: boolean; //
isExecute?: boolean; //
errorMessageInfo?: {
[key: string]: Record<string, any>;
};
}
export type RequestParam = ExecuteApiRequestFullParams & {
responseDefinition?: ResponseItem[];
@ -1346,6 +1355,59 @@
}
}
function initErrorMessageInfoItem(key) {
if (requestVModel.value.errorMessageInfo && !requestVModel.value.errorMessageInfo[key]) {
requestVModel.value.errorMessageInfo[key] = {};
}
}
function changeTabErrorMessageList(tabKey: string, formErrorMessageList: string[]) {
if (!requestVModel.value.errorMessageInfo) return;
const label = contentTabList.value.find((item) => item.value === tabKey)?.label ?? '';
const listItem: TabErrorMessage = {
value: tabKey,
label,
messageList: formErrorMessageList,
};
// TODO: sql sql
if (requestVModel.value.activeTab === RequestComposition.BODY) {
initErrorMessageInfoItem(RequestComposition.BODY);
requestVModel.value.errorMessageInfo[RequestComposition.BODY][requestVModel.value.body.bodyType] =
cloneDeep(listItem);
} else {
requestVModel.value.errorMessageInfo[requestVModel.value.activeTab] = cloneDeep(listItem);
}
}
const setErrorMessageList = debounce((list: string[]) => {
changeTabErrorMessageList(requestVModel.value.activeTab, list);
}, 300);
provide('setErrorMessageList', setErrorMessageList);
//
function getFlattenedMessages() {
if (!requestVModel.value.errorMessageInfo) return;
const flattenedMessages: { label: string; messageList: string[] }[] = [];
const { errorMessageInfo } = requestVModel.value;
Object.values(errorMessageInfo).forEach((item) => {
const label = item.label || (Object.values(item)[0] && Object.values(item)[0].label);
const messageList: string[] =
item.messageList || [...new Set(Object.values(item).flatMap((bodyType) => bodyType.messageList))] || [];
if (messageList.length) {
flattenedMessages.push({ label, messageList: [...new Set(messageList)] });
}
});
return flattenedMessages;
}
function showMessage() {
getFlattenedMessages()?.forEach(({ label, messageList }) => {
messageList?.forEach((message) => {
Message.error(`${label}${message}`);
});
});
}
function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
@ -1368,6 +1430,11 @@
//
await fApi.value?.validate();
}
//
if (getFlattenedMessages()?.length) {
showMessage();
return;
}
if (!requestVModel.value.isNew) {
//
updateRequest();
@ -1487,6 +1554,11 @@
if (errors) {
requestVModel.value.activeTab = RequestComposition.BASE_INFO;
} else {
//
if (getFlattenedMessages()?.length) {
showMessage();
return;
}
switch (value) {
case 'save':
handleSaveShortcut();
@ -1565,6 +1637,8 @@
execute,
makeRequestParams,
changeVerticalExpand,
getFlattenedMessages,
showMessage,
});
</script>

View File

@ -68,6 +68,7 @@
title: 'apiTestDebug.paramName',
dataIndex: 'key',
slotName: 'key',
needValidRepeat: true,
},
{
title: 'apiTestDebug.paramType',

View File

@ -69,6 +69,7 @@
title: 'apiTestDebug.paramName',
dataIndex: 'key',
slotName: 'key',
needValidRepeat: true,
},
{
title: 'apiTestDebug.paramType',

View File

@ -212,6 +212,7 @@
response: cloneDeep(defaultResponse),
isNew: true,
executeLoading: false,
errorMessageInfo: {},
};
const debugTabs = ref<RequestParam[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<RequestParam>(debugTabs.value[0]);

View File

@ -275,6 +275,7 @@
executeLoading: false,
preDependency: [], //
postDependency: [], //
errorMessageInfo: {},
};
function addApiTab(defaultProps?: Partial<TabItem>) {

View File

@ -237,6 +237,11 @@
function handleDrawerConfirm(isContinue: boolean) {
formRef.value?.validate(async (errors) => {
if (!errors) {
//
if (requestCompositionRef.value?.getFlattenedMessages()?.length) {
requestCompositionRef.value?.showMessage();
return;
}
drawerLoading.value = true;
//
if (!requestCompositionRef.value?.makeRequestParams()) return;

View File

@ -205,6 +205,7 @@
executeLoading: false,
preDependency: [], //
postDependency: [], //
errorMessageInfo: {},
};
// id