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,4 +1,5 @@
<template>
<a-form ref="formRef" :model="propsRes">
<MsBaseTable
v-bind="propsRes"
:hoverable="false"
@ -7,6 +8,7 @@
:class="!props.selectable && !props.draggable ? 'ms-form-table-no-left-action' : ''"
v-on="propsEvent"
@drag-change="tableChange"
@init-end="validateAndUpdateErrorMessageList"
>
<!-- 展开行-->
<template #expand-icon="{ expanded, record }">
@ -17,9 +19,22 @@
</template>
<template
v-for="item of props.columns.filter((e) => e.slotName !== undefined)"
:key="item.toString()"
#[item.slotName!]="{ record, rowIndex, column }"
>
<slot :name="item.slotName" v-bind="{ record, rowIndex, column, dataIndex: item.dataIndex, columnConfig: item }">
<a-form-item
:field="`data[${rowIndex}].${item.dataIndex}`"
label=""
:rules="{
validator: (value, callback) => {
validRepeat(rowIndex, item.dataIndex as string, value, callback);
},
}"
>
<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')"
@ -101,7 +116,9 @@
</template>
<template v-else-if="item.inputType === 'text'">
{{
typeof item.valueFormat === 'function' ? item.valueFormat(record) : record[item.dataIndex as string] || '-'
typeof item.valueFormat === 'function'
? item.valueFormat(record)
: record[item.dataIndex as string] || '-'
}}
</template>
<template v-else-if="item.dataIndex === 'action'">
@ -128,6 +145,7 @@
</div>
</template>
</slot>
</a-form-item>
</template>
<template
v-for="item of props.columns.filter((e) => e.titleSlotName !== undefined)"
@ -140,9 +158,11 @@
</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