feat(接口测试): 新增编辑器diff对比功能&接口测试对比布局调整&请求体JSON对比

This commit is contained in:
xinxin.wu 2024-08-01 18:34:10 +08:00 committed by Craftsman
parent d90d060e6e
commit 55a33c6a0b
5 changed files with 245 additions and 130 deletions

View File

@ -93,6 +93,8 @@
const { t } = useI18n(); const { t } = useI18n();
// //
let editor: monaco.editor.IStandaloneCodeEditor; let editor: monaco.editor.IStandaloneCodeEditor;
// diffEditor diffMode
let diffEditor: monaco.editor.IStandaloneDiffEditor;
const codeContainerRef = ref(); const codeContainerRef = ref();
// ref // ref
@ -266,16 +268,17 @@
codeheight.value = props.height; codeheight.value = props.height;
return; return;
} }
const editorElement = editor.getDomNode(); if (editor) {
const editorElement = editor.getDomNode();
if (!editorElement) { if (!editorElement) {
return; return;
}
} }
// //
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); const lineHeight = editor?.getOption(monaco.editor.EditorOption.lineHeight);
// //
const lineCount = editor.getModel()?.getLineCount() || 10; const lineCount = editor?.getModel()?.getLineCount() || 10;
// @desc 3 2412px // @desc 3 2412px
const height = (lineCount + 3) * lineHeight; const height = (lineCount + 3) * lineHeight;
codeheight.value = height > 300 ? `${height + 24}px` : '300px'; codeheight.value = height > 300 ? `${height + 24}px` : '300px';
@ -343,11 +346,50 @@
{ deep: true } { deep: true }
); );
// diffEditor
const initDiffEditor = (originalValue: string, modifiedValue: string) => {
diffEditor = monaco.editor.createDiffEditor(codeContainerRef.value, {
automaticLayout: true,
padding: {
top: 12,
bottom: 12,
},
minimap: {
enabled: false,
},
contextmenu: !props.readOnly,
...props,
theme: currentTheme.value,
lineNumbersMinChars: 3,
lineDecorationsWidth: 0,
scrollBeyondLastLine: false,
});
const originalModel = monaco.editor.createModel(originalValue, props.language.toLowerCase());
const modifiedModel = monaco.editor.createModel(modifiedValue, props.language.toLowerCase());
diffEditor.setModel({
original: originalModel,
modified: modifiedModel,
});
handleEditorMount();
};
watch( watch(
() => props.language, () => props.language,
(newValue) => { (newValue) => {
currentLanguage.value = newValue; currentLanguage.value = newValue;
monaco.editor.setModelLanguage(editor.getModel()!, newValue.toLowerCase()); // ENUM monaco monaco.editor.setModelLanguage(editor.getModel()!, newValue.toLowerCase()); // ENUM monaco
//
if (diffEditor) {
const originalModel = diffEditor.getModel()?.original;
const modifiedModel = diffEditor.getModel()?.modified;
if (originalModel && modifiedModel) {
monaco.editor.setModelLanguage(originalModel, newValue.toLowerCase());
monaco.editor.setModelLanguage(modifiedModel, newValue.toLowerCase());
}
}
} }
); );
@ -355,15 +397,28 @@
() => props.readOnly, () => props.readOnly,
(val) => { (val) => {
editor.updateOptions({ readOnly: val }); editor.updateOptions({ readOnly: val });
if (diffEditor) {
diffEditor.updateOptions({ readOnly: val });
}
} }
); );
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.dispose(); if (editor) {
editor.dispose();
}
if (diffEditor) {
diffEditor.dispose();
}
}); });
onMounted(() => { onMounted(() => {
init(); if (props.diffMode) {
initDiffEditor(props.originalValue, props.modelValue);
} else {
init();
}
setEditBoxBg(); setEditBoxBg();
if (props.readOnly) { if (props.readOnly) {
format(); format();

View File

@ -135,4 +135,14 @@ export const editorProps = {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false, default: false,
}, },
// diff对比模式
diffMode: {
type: Boolean as PropType<boolean>,
default: false,
},
// 原来值
originalValue: {
type: String as PropType<string>,
default: null,
},
}; };

View File

@ -39,7 +39,7 @@
</div> </div>
<div v-if="showDiff(RequestComposition.REST)" class="title">REST</div> <div v-if="showDiff(RequestComposition.REST)" class="title">REST</div>
<div <div
v-if="showDiff(RequestComposition.REST)" v-if="showDiff(RequestComposition.REST) && hiddenEmptyTable(RequestComposition.REST)"
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.REST)}px` }" :style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.REST)}px` }"
> >
<MsFormTable <MsFormTable
@ -56,20 +56,11 @@
> >
{{ t('case.notSetData') }} {{ t('case.notSetData') }}
</div> </div>
<!-- 请求体 -->
<div class="title flex items-center justify-between"> <div class="title flex items-center justify-between">
<div class="detail-item-title-text"> <div class="detail-item-title-text">
{{ `${t('apiTestManagement.requestBody')}-${previewDetail?.body?.bodyType}` }} {{ `${t('apiTestManagement.requestBody')}-${previewDetail?.body?.bodyType}` }}
</div> </div>
<a-radio-group
v-if="previewDetail?.body?.bodyType === RequestBodyFormat.JSON && props.isApi"
v-model:model-value="bodyShowType"
type="button"
size="mini"
>
<a-radio value="schema">Schema</a-radio>
<a-radio value="json">JSON</a-radio>
</a-radio-group>
</div> </div>
<div <div
v-if=" v-if="
@ -96,52 +87,13 @@
> >
{{ t('case.notSetData') }} {{ t('case.notSetData') }}
</div> </div>
<template
v-else-if="
[RequestBodyFormat.JSON, RequestBodyFormat.RAW, RequestBodyFormat.XML].includes(previewDetail?.body?.bodyType)
"
>
<MsJsonSchema
v-if="previewDetail?.body?.bodyType === RequestBodyFormat.JSON && bodyShowType === 'schema' && props.isApi"
:data="previewDetail.body.jsonBody.jsonSchemaTableData"
disabled
/>
<MsCodeEditor
v-else
:model-value="bodyCode"
theme="vs"
height="200px"
:language="bodyCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(bodyCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue'; import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsJsonSchema from '@/components/pure/ms-json-schema/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -150,7 +102,6 @@
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const { copy, isSupported } = useClipboard({ legacy: true });
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
@ -259,8 +210,6 @@
}, },
]; ];
const bodyShowType = ref('schema');
/** /**
* 请求体 * 请求体
*/ */
@ -376,42 +325,6 @@
} }
}); });
const bodyCode = computed(() => {
switch (previewDetail.value?.body?.bodyType) {
case RequestBodyFormat.FORM_DATA:
return previewDetail.value.body.formDataBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.WWW_FORM:
return previewDetail.value.body.wwwFormBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.RAW:
return previewDetail.value.body.rawBody?.value;
case RequestBodyFormat.JSON:
return previewDetail.value.body.jsonBody?.jsonValue;
case RequestBodyFormat.XML:
return previewDetail.value.body.xmlBody?.value;
default:
return '';
}
});
const bodyCodeLanguage = computed(() => {
if (previewDetail.value?.body?.bodyType === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (previewDetail.value?.body?.bodyType === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
function copyScript(val: string) {
if (isSupported) {
copy(val);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
const typeKey = computed(() => (props.isApi ? 'api' : 'case')); const typeKey = computed(() => (props.isApi ? 'api' : 'case'));
// //

View File

@ -0,0 +1,118 @@
<template>
<template
v-if="
[RequestBodyFormat.JSON, RequestBodyFormat.RAW, RequestBodyFormat.XML].includes(
previewDefinedDetail?.body?.bodyType
)
"
>
<MsCodeEditor
v-if="previewDefinedDetail?.body?.bodyType === RequestBodyFormat.JSON"
:model-value="bodyCaseCode"
theme="vs"
height="200px"
:language="bodyCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
is-adaptive
diff-mode
:original-value="bodyDefinedCode"
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(bodyDefinedCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</template>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteBody } from '@/models/apiTest/common';
import { RequestBodyFormat } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const MsCodeEditor = defineAsyncComponent(() => import('@/components/pure/ms-code-editor/index.vue'));
const { copy, isSupported } = useClipboard({ legacy: true });
const { t } = useI18n();
const props = defineProps<{
definedDetail: RequestParam;
caseDetail: RequestParam;
}>();
const previewDefinedDetail = ref<RequestParam>(props.definedDetail);
const previewCaseDetail = ref<RequestParam>(props.caseDetail);
const bodyCodeLanguage = computed(() => {
if (previewDefinedDetail.value?.body?.bodyType === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (previewDefinedDetail.value?.body?.bodyType === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
function copyScript(val: string) {
if (isSupported) {
copy(val);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
const getBodyCode = (body: ExecuteBody) => {
switch (body?.bodyType) {
case RequestBodyFormat.FORM_DATA:
return body.formDataBody?.formValues?.map((item: any) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.WWW_FORM:
return body.wwwFormBody?.formValues?.map((item: any) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.RAW:
return body.rawBody?.value;
case RequestBodyFormat.JSON:
return body.jsonBody?.jsonValue;
case RequestBodyFormat.XML:
return body.xmlBody?.value;
default:
return '';
}
};
// Code
const bodyDefinedCode = computed(() => getBodyCode(previewDefinedDetail.value?.body));
// Code
const bodyCaseCode = computed(() => getBodyCode(previewCaseDetail.value?.body));
watchEffect(() => {
if (props.definedDetail) {
previewDefinedDetail.value = cloneDeep(props.definedDetail);
}
if (props.caseDetail) {
previewCaseDetail.value = cloneDeep(props.caseDetail);
}
});
</script>
<style scoped></style>

View File

@ -67,14 +67,27 @@
</div> </div>
<!-- 对比 --> <!-- 对比 -->
<div class="diff-container"> <div class="diff-container">
<div class="diff-item ml-[16px] mr-[8px]"> <a-spin class="h-full w-full" :loading="loading">
<div class="title-type"> [{{ apiDetailInfo?.num }}] {{ apiDetailInfo?.name }} </div> <div class="diff-normal">
<DiffItem :diff-distance-map="diffDistanceMap" mode="add" is-api :detail="apiDefinedRequest as RequestParam" /> <div class="diff-item">
</div> <div class="title-type"> [{{ apiDetailInfo?.num }}] {{ apiDetailInfo?.name }} </div>
<div class="diff-item ml-[8px] mr-[16px]"> <DiffItem
<div class="title-type"> [{{ caseDetail?.num }}] {{ caseDetail?.name }} </div> :diff-distance-map="diffDistanceMap"
<DiffItem :diff-distance-map="diffDistanceMap" mode="delete" :detail="caseDetail as RequestParam" /> mode="add"
</div> is-api
:detail="apiDefinedRequest as RequestParam"
/>
</div>
<div class="diff-item ml-[24px]">
<div class="title-type"> [{{ caseDetail?.num }}] {{ caseDetail?.name }} </div>
<DiffItem :diff-distance-map="diffDistanceMap" mode="delete" :detail="caseDetail as RequestParam" />
</div>
</div>
<DiffRequestBody
:defined-detail="apiDefinedRequest as RequestParam"
:case-detail="caseDetail as RequestParam"
/>
</a-spin>
</div> </div>
</MsDrawer> </MsDrawer>
</template> </template>
@ -86,6 +99,7 @@
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types'; import { TabItem } from '@/components/pure/ms-editable-tab/types';
import DiffItem from './diffItem.vue'; import DiffItem from './diffItem.vue';
import DiffRequestBody from './diffRequestBody.vue';
import { getCaseDetail, getDefinitionDetail } from '@/api/modules/api-test/management'; import { getCaseDetail, getDefinitionDetail } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -323,13 +337,16 @@
console.log(error); console.log(error);
} }
} }
const loading = ref<boolean>(false);
async function getRequestDetail(definedId: string, apiCaseId: string) { async function getRequestDetail(definedId: string, apiCaseId: string) {
loading.value = true;
try { try {
await Promise.all([getApiDetail(definedId), getCaseDetailInfo(apiCaseId)]); await Promise.all([getApiDetail(definedId), getCaseDetailInfo(apiCaseId)]);
processData(); processData();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
loading.value = false;
} }
} }
@ -361,31 +378,33 @@
} }
} }
.diff-container { .diff-container {
@apply flex; padding: 16px;
.diff-item { min-height: calc(100vh - 110px);
overflow-y: auto; border-radius: 12px;
padding: 16px; background: white;
min-height: calc(100vh - 110px); box-shadow: 0 0 10px rgba(120 56 135/ 5%);
border-radius: 12px; @apply mx-4;
background: white; .diff-normal {
box-shadow: 0 0 10px rgba(120 56 135/ 5%); @apply flex;
@apply flex-1; .diff-item {
.title-type { @apply flex-1;
color: var(--color-text-1); .title-type {
@apply font-medium;
}
.title {
color: var(--color-text-1);
@apply my-4;
}
.detail-item-title {
margin-bottom: 8px;
gap: 16px;
@apply flex items-center justify-between;
.detail-item-title-text {
@apply font-medium;
color: var(--color-text-1); color: var(--color-text-1);
@apply font-medium;
}
.title {
color: var(--color-text-1);
@apply my-4;
}
.detail-item-title {
margin-bottom: 8px;
gap: 16px;
@apply flex items-center justify-between;
.detail-item-title-text {
@apply font-medium;
color: var(--color-text-1);
}
} }
} }
} }