feat(接口测试): 用例列表同步&对比table抽屉Drawer
This commit is contained in:
parent
122c54691a
commit
1f77a8db41
|
@ -12,6 +12,7 @@
|
||||||
props.disabled ? 'ms-form-table--disabled' : '',
|
props.disabled ? 'ms-form-table--disabled' : '',
|
||||||
]"
|
]"
|
||||||
bordered
|
bordered
|
||||||
|
:row-class="rowClass"
|
||||||
v-on="propsEvent"
|
v-on="propsEvent"
|
||||||
@drag-change="tableChange"
|
@drag-change="tableChange"
|
||||||
@init-end="validateAndUpdateErrorMessageList"
|
@init-end="validateAndUpdateErrorMessageList"
|
||||||
|
@ -294,6 +295,7 @@
|
||||||
disabled?: boolean; // 是否禁用
|
disabled?: boolean; // 是否禁用
|
||||||
showSelectorAll?: boolean; // 是否显示全选
|
showSelectorAll?: boolean; // 是否显示全选
|
||||||
rowSelection?: TableRowSelection;
|
rowSelection?: TableRowSelection;
|
||||||
|
diffMode?: 'add' | 'delete';
|
||||||
spanMethod?: (data: {
|
spanMethod?: (data: {
|
||||||
record: TableData;
|
record: TableData;
|
||||||
column: TableColumnData | TableOperationColumn;
|
column: TableColumnData | TableOperationColumn;
|
||||||
|
@ -537,6 +539,18 @@
|
||||||
emit('selectAll', checked);
|
emit('selectAll', checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowClass(record: TableData, rowIndex: number) {
|
||||||
|
if (record.diff) {
|
||||||
|
if (props.diffMode === 'add') {
|
||||||
|
return 'add-row-class';
|
||||||
|
}
|
||||||
|
if (props.diffMode === 'delete') {
|
||||||
|
return 'delete-row-class';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
validateAndUpdateErrorMessageList,
|
validateAndUpdateErrorMessageList,
|
||||||
});
|
});
|
||||||
|
@ -719,4 +733,20 @@
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:deep(.add-row-class) {
|
||||||
|
.arco-table-td {
|
||||||
|
background: rgb(var(--success-1));
|
||||||
|
.arco-table-td-content {
|
||||||
|
background: rgb(var(--success-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.delete-row-class) {
|
||||||
|
.arco-table-td {
|
||||||
|
background: rgb(var(--danger-1));
|
||||||
|
.arco-table-td-content {
|
||||||
|
background: rgb(var(--danger-1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -47,9 +47,26 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #num="{ record }">
|
<template #num="{ record }">
|
||||||
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
|
<div class="flex items-center">
|
||||||
{{ record.num }}
|
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
|
||||||
</MsButton>
|
{{ record.num }}
|
||||||
|
</MsButton>
|
||||||
|
<!-- TODO 后台缺少字段 等待联调 -->
|
||||||
|
<a-tooltip v-if="record.apiChange" class="ms-tooltip-white">
|
||||||
|
<!-- 接口参数发生变更提示 -->
|
||||||
|
<MsIcon type="icon-icon_warning_colorful" size="16" />
|
||||||
|
<template #content>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<span class="text-[var(--color-text-1)]">
|
||||||
|
{{ t('case.apiParamsHasChange') }}
|
||||||
|
</span>
|
||||||
|
<MsButton class="ml-[8px]" @click="showDifferences(record)">
|
||||||
|
{{ t('case.changeDifferences') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #protocol="{ record }">
|
<template #protocol="{ record }">
|
||||||
<apiMethodName :method="record.protocol" />
|
<apiMethodName :method="record.protocol" />
|
||||||
|
@ -281,6 +298,17 @@
|
||||||
/>
|
/>
|
||||||
<!-- 执行结果抽屉 -->
|
<!-- 执行结果抽屉 -->
|
||||||
<caseAndScenarioReportDrawer v-model:visible="showExecuteResult" :report-id="activeReportId" />
|
<caseAndScenarioReportDrawer v-model:visible="showExecuteResult" :report-id="activeReportId" />
|
||||||
|
<!-- 同步抽屉 -->
|
||||||
|
<SyncModal v-model:visible="showSyncModal" :batch-params="batchParams" />
|
||||||
|
<!-- diff对比抽屉 -->
|
||||||
|
<DifferentDrawer
|
||||||
|
v-model:visible="showDifferentDrawer"
|
||||||
|
:detail="caseDetail as RequestParam"
|
||||||
|
:api-detail="apiDetail as RequestParam"
|
||||||
|
:active-api-case-id="activeApiCaseId"
|
||||||
|
:active-defined-id="activeDefinedId"
|
||||||
|
@close="closeDifferent"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -299,6 +327,8 @@
|
||||||
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
|
||||||
import caseDetailDrawer from './caseDetailDrawer.vue';
|
import caseDetailDrawer from './caseDetailDrawer.vue';
|
||||||
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
|
||||||
|
import DifferentDrawer from './differentDrawer.vue';
|
||||||
|
import SyncModal from './syncModal.vue';
|
||||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||||
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
|
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
|
||||||
|
@ -390,8 +420,7 @@
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 130,
|
width: 150,
|
||||||
showTooltip: true,
|
|
||||||
columnSelectorDisabled: true,
|
columnSelectorDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -568,6 +597,11 @@
|
||||||
eventTag: 'execute',
|
eventTag: 'execute',
|
||||||
permission: ['PROJECT_API_DEFINITION_CASE:READ+EXECUTE'],
|
permission: ['PROJECT_API_DEFINITION_CASE:READ+EXECUTE'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'case.apiSyncChange',
|
||||||
|
eventTag: 'sync',
|
||||||
|
permission: ['PROJECT_API_DEFINITION_CASE:READ+UPDATE'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'common.delete',
|
label: 'common.delete',
|
||||||
eventTag: 'delete',
|
eventTag: 'delete',
|
||||||
|
@ -820,9 +854,14 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchConditionParams = ref<any>();
|
const batchConditionParams = ref<any>();
|
||||||
|
|
||||||
|
const showSyncModal = ref<boolean>(false);
|
||||||
|
// 同步 TODO 等待联调
|
||||||
|
function syncParams() {
|
||||||
|
showSyncModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 处理表格选中后批量操作
|
// 处理表格选中后批量操作
|
||||||
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
|
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
|
||||||
tableSelected.value = params?.selectedIds || [];
|
tableSelected.value = params?.selectedIds || [];
|
||||||
|
@ -840,6 +879,9 @@
|
||||||
showBatchExecute.value = true;
|
showBatchExecute.value = true;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'sync':
|
||||||
|
syncParams();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -920,6 +962,23 @@
|
||||||
showExecuteResult.value = true;
|
showExecuteResult.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeApiCaseId = ref<string>('');
|
||||||
|
const activeDefinedId = ref<string>('');
|
||||||
|
const showDifferentDrawer = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 查看对比 TODO 等待联调
|
||||||
|
async function showDifferences(record: ApiCaseDetail) {
|
||||||
|
activeApiCaseId.value = record.id;
|
||||||
|
activeDefinedId.value = record.apiDefinitionId;
|
||||||
|
showDifferentDrawer.value = true;
|
||||||
|
}
|
||||||
|
// 关闭对比 TODO 等待联调
|
||||||
|
function closeDifferent() {
|
||||||
|
showDifferentDrawer.value = false;
|
||||||
|
activeApiCaseId.value = '';
|
||||||
|
activeDefinedId.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
loadCaseList,
|
loadCaseList,
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,470 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="showDiff(RequestComposition.HEADER)" class="title">{{ t('apiTestDebug.header') }}</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(RequestComposition.HEADER) && hiddenEmptyTable(RequestComposition.HEADER)"
|
||||||
|
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.HEADER)}px` }"
|
||||||
|
>
|
||||||
|
<MsFormTable
|
||||||
|
:columns="headerColumns"
|
||||||
|
:data="previewDetail?.headers?.filter((e) => e.key !== '') || []"
|
||||||
|
:selectable="false"
|
||||||
|
:diff-mode="props.mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(RequestComposition.HEADER) && !hiddenEmptyTable(RequestComposition.HEADER)"
|
||||||
|
class="not-setting-data"
|
||||||
|
:style="{ height: `${getBottomDistance(RequestComposition.HEADER, true)}px` }"
|
||||||
|
>
|
||||||
|
{{ t('case.notSetData') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="showDiff(RequestComposition.QUERY)" class="title">Query</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(RequestComposition.QUERY) && hiddenEmptyTable(RequestComposition.QUERY)"
|
||||||
|
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.QUERY)}px` }"
|
||||||
|
>
|
||||||
|
<MsFormTable
|
||||||
|
:columns="queryRestColumns"
|
||||||
|
:data="previewDetail?.query?.filter((e) => e.key !== '') || []"
|
||||||
|
:selectable="false"
|
||||||
|
:diff-mode="props.mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(RequestComposition.QUERY) && !hiddenEmptyTable(RequestComposition.QUERY)"
|
||||||
|
class="not-setting-data"
|
||||||
|
:style="{ height: `${getBottomDistance(RequestComposition.QUERY, true)}px` }"
|
||||||
|
>
|
||||||
|
{{ t('case.notSetData') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="showDiff(RequestComposition.REST)" class="title">REST</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(RequestComposition.REST)"
|
||||||
|
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.REST)}px` }"
|
||||||
|
>
|
||||||
|
<MsFormTable
|
||||||
|
:columns="queryRestColumns?.filter((e) => e.key !== '')"
|
||||||
|
:data="previewDetail?.rest || []"
|
||||||
|
:selectable="false"
|
||||||
|
:diff-mode="props.mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(RequestComposition.REST) && !hiddenEmptyTable(RequestComposition.REST)"
|
||||||
|
class="not-setting-data"
|
||||||
|
:style="{ height: `${getBottomDistance(RequestComposition.REST, true)}px` }"
|
||||||
|
>
|
||||||
|
{{ t('case.notSetData') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title flex items-center justify-between">
|
||||||
|
<div class="detail-item-title-text">
|
||||||
|
{{ `${t('apiTestManagement.requestBody')}-${previewDetail?.body?.bodyType}` }}
|
||||||
|
</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
|
||||||
|
v-if="
|
||||||
|
(previewDetail?.body?.bodyType === RequestBodyFormat.FORM_DATA ||
|
||||||
|
previewDetail?.body?.bodyType === RequestBodyFormat.WWW_FORM) &&
|
||||||
|
showDiff(previewDetail?.body?.bodyType) &&
|
||||||
|
hiddenEmptyTable(previewDetail?.body?.bodyType)
|
||||||
|
"
|
||||||
|
:style="{ 'padding-bottom': `${getBottomDistance(previewDetail.value?.body?.bodyType)}px` }"
|
||||||
|
>
|
||||||
|
<MsFormTable
|
||||||
|
:columns="bodyColumns"
|
||||||
|
:data="bodyTableData"
|
||||||
|
:selectable="false"
|
||||||
|
:show-setting="true"
|
||||||
|
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_DATA"
|
||||||
|
:diff-mode="props.mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showDiff(previewDetail?.body?.bodyType) && !hiddenEmptyTable(previewDetail?.body?.bodyType)"
|
||||||
|
class="not-setting-data"
|
||||||
|
:style="{ height: `${getBottomDistance(previewDetail?.body?.bodyType, true)}px` }"
|
||||||
|
>
|
||||||
|
{{ t('case.notSetData') }}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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 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 MsJsonSchema from '@/components/pure/ms-json-schema/index.vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { RequestBodyFormat, RequestComposition, RequestParamsType } from '@/enums/apiEnum';
|
||||||
|
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||||
|
|
||||||
|
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
|
|
||||||
|
const { copy, isSupported } = useClipboard({ legacy: true });
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
diffDistanceMap: Record<string, any>; // 距离填充
|
||||||
|
detail: RequestParam;
|
||||||
|
mode: 'add' | 'delete';
|
||||||
|
isApi?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const previewDetail = ref<RequestParam>(props.detail);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求头
|
||||||
|
*/
|
||||||
|
const headerColumns: FormTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramName',
|
||||||
|
dataIndex: 'key',
|
||||||
|
inputType: 'text',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramVal',
|
||||||
|
dataIndex: 'value',
|
||||||
|
inputType: 'text',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.desc',
|
||||||
|
dataIndex: 'description',
|
||||||
|
inputType: 'text',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query & Rest
|
||||||
|
*/
|
||||||
|
const queryRestColumns: FormTableColumn[] = [
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramName',
|
||||||
|
dataIndex: 'key',
|
||||||
|
inputType: 'text',
|
||||||
|
width: 220,
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.required',
|
||||||
|
dataIndex: 'required',
|
||||||
|
slotName: 'required',
|
||||||
|
inputType: 'text',
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
valueFormat: (record) => {
|
||||||
|
return record.required ? t('common.yes') : t('common.no');
|
||||||
|
},
|
||||||
|
width: 68,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.paramType',
|
||||||
|
dataIndex: 'paramType',
|
||||||
|
inputType: 'text',
|
||||||
|
width: 96,
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramVal',
|
||||||
|
dataIndex: 'value',
|
||||||
|
inputType: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.paramLengthRange',
|
||||||
|
dataIndex: 'lengthRange',
|
||||||
|
slotName: 'lengthRange',
|
||||||
|
inputType: 'text',
|
||||||
|
showInTable: false,
|
||||||
|
valueFormat: (record) => {
|
||||||
|
return [null, undefined].includes(record.minLength) && [null, undefined].includes(record.maxLength)
|
||||||
|
? '-'
|
||||||
|
: `${record.minLength} ${t('common.to')} ${record.maxLength}`;
|
||||||
|
},
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.encode',
|
||||||
|
dataIndex: 'encode',
|
||||||
|
slotName: 'encode',
|
||||||
|
inputType: 'text',
|
||||||
|
showInTable: false,
|
||||||
|
valueFormat: (record) => {
|
||||||
|
return record.encode ? t('common.yes') : t('common.no');
|
||||||
|
},
|
||||||
|
width: 68,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.desc',
|
||||||
|
dataIndex: 'description',
|
||||||
|
inputType: 'text',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'operation',
|
||||||
|
slotName: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const bodyShowType = ref('schema');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求体
|
||||||
|
*/
|
||||||
|
const bodyColumns = computed<FormTableColumn[]>(() => {
|
||||||
|
if ([RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(previewDetail.value?.body?.bodyType)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramName',
|
||||||
|
dataIndex: 'key',
|
||||||
|
inputType: 'text',
|
||||||
|
width: 220,
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.required',
|
||||||
|
dataIndex: 'required',
|
||||||
|
slotName: 'required',
|
||||||
|
inputType: 'text',
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
valueFormat: (record) => {
|
||||||
|
return record.required ? t('common.yes') : t('common.no');
|
||||||
|
},
|
||||||
|
width: 68,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramsType',
|
||||||
|
dataIndex: 'paramType',
|
||||||
|
inputType: 'text',
|
||||||
|
width: 96,
|
||||||
|
columnSelectorDisabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramVal',
|
||||||
|
dataIndex: 'value',
|
||||||
|
inputType: 'text',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.paramLengthRange',
|
||||||
|
dataIndex: 'lengthRange',
|
||||||
|
slotName: 'lengthRange',
|
||||||
|
inputType: 'text',
|
||||||
|
showInTable: false,
|
||||||
|
valueFormat: (record) => {
|
||||||
|
return [null, undefined].includes(record.minLength) && [null, undefined].includes(record.maxLength)
|
||||||
|
? '-'
|
||||||
|
: `${record.minLength} ${t('common.to')} ${record.maxLength}`;
|
||||||
|
},
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestDebug.encode',
|
||||||
|
dataIndex: 'encode',
|
||||||
|
slotName: 'encode',
|
||||||
|
inputType: 'text',
|
||||||
|
showInTable: false,
|
||||||
|
valueFormat: (record) => {
|
||||||
|
return record.encode ? t('common.yes') : t('common.no');
|
||||||
|
},
|
||||||
|
width: 68,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'common.desc',
|
||||||
|
dataIndex: 'description',
|
||||||
|
inputType: 'text',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'operation',
|
||||||
|
slotName: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'common.desc',
|
||||||
|
dataIndex: 'description',
|
||||||
|
inputType: 'text',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'apiTestManagement.paramVal',
|
||||||
|
dataIndex: 'value',
|
||||||
|
inputType: 'text',
|
||||||
|
showTooltip: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodyTableData = computed(() => {
|
||||||
|
switch (previewDetail.value?.body?.bodyType) {
|
||||||
|
case RequestBodyFormat.FORM_DATA:
|
||||||
|
return (previewDetail.value.body.formDataBody?.formValues || [])
|
||||||
|
.map((e) => ({
|
||||||
|
...e,
|
||||||
|
value: e.paramType === RequestParamsType.FILE ? e.files?.map((file) => file.fileName).join('、') : e.value,
|
||||||
|
}))
|
||||||
|
?.filter((e) => e.key !== '');
|
||||||
|
case RequestBodyFormat.WWW_FORM:
|
||||||
|
return previewDetail.value.body.wwwFormBody?.formValues?.filter((e) => e.key !== '') || [];
|
||||||
|
case RequestBodyFormat.BINARY:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
description: previewDetail.value.body.binaryBody.description,
|
||||||
|
value: previewDetail.value.body.binaryBody.file?.fileName,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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'));
|
||||||
|
|
||||||
|
// 设置非空间距确保行内容长度不同能够水平看齐对比
|
||||||
|
function getBottomDistance(type: string, isEmpty = false) {
|
||||||
|
const isEmptyDefaultDis = isEmpty ? 34 : 0;
|
||||||
|
if (props.diffDistanceMap[type] && props.diffDistanceMap[type][typeKey.value]) {
|
||||||
|
return props.diffDistanceMap[type][typeKey.value] * 32 + isEmptyDefaultDis;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否显示对比
|
||||||
|
function showDiff(type: string) {
|
||||||
|
return props.diffDistanceMap[type]?.display;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉空表
|
||||||
|
function hiddenEmptyTable(type: string) {
|
||||||
|
if (props.isApi) {
|
||||||
|
return props.diffDistanceMap[type]?.showEmptyApiTable;
|
||||||
|
}
|
||||||
|
return props.diffDistanceMap[type]?.showEmptyCaseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.detail) {
|
||||||
|
previewDetail.value = cloneDeep(props.detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.title-type {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.not-setting-data {
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,407 @@
|
||||||
|
<template>
|
||||||
|
<MsDrawer
|
||||||
|
v-model:visible="showDiffVisible"
|
||||||
|
:title="t('case.apiAndCaseDiff')"
|
||||||
|
width="100%"
|
||||||
|
class="diff-modal"
|
||||||
|
:footer="false"
|
||||||
|
no-content-padding
|
||||||
|
unmount-on-close
|
||||||
|
:closable="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<div>{{ t('case.apiAndCaseDiff') }}</div>
|
||||||
|
<div class="flex items-center text-[14px]">
|
||||||
|
<div class="-mt-[2px] mr-[8px]"> {{ t('case.syncItem') }}</div>
|
||||||
|
<a-checkbox-group v-model="form.checkType">
|
||||||
|
<a-checkbox v-for="item of checkList" :key="item.value" :value="item.value">
|
||||||
|
<div class="flex items-center"
|
||||||
|
>{{ item.label }}
|
||||||
|
|
||||||
|
<a-tooltip v-if="item.tooltip" :content="item.tooltip" position="top">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<icon-question-circle
|
||||||
|
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
<a-divider direction="vertical" :margin="0" class="!mr-[8px]"></a-divider>
|
||||||
|
<a-switch v-model:model-value="form.ignoreUpdate" size="small" />
|
||||||
|
<a-select
|
||||||
|
v-model="form.ignoreUpdateType"
|
||||||
|
class="ml-[8px] w-[160px]"
|
||||||
|
:placeholder="t('caseManagement.featureCase.PleaseSelect')"
|
||||||
|
:disabled="!form.ignoreUpdate"
|
||||||
|
@change="changeIgnoreType"
|
||||||
|
>
|
||||||
|
<a-option v-for="item of ignoreList" :key="item.value" :value="item.value">
|
||||||
|
{{ t(item.label) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||||
|
<a-switch v-model:model-value="form.deleteParams" size="small" />
|
||||||
|
<div class="ml-[8px] font-normal text-[var(--color-text-1)]">{{ t('case.deleteNotCorrespondValue') }}</div>
|
||||||
|
<a-divider direction="vertical" :margin="0" class="!ml-[8px]"></a-divider>
|
||||||
|
<a-button class="mx-[12px]" type="secondary" @click="cancel">{{ t('common.cancel') }}</a-button>
|
||||||
|
<a-button type="primary" :loading="syncLoading" :disabled="!form.checkType.length" @click="confirmBatchSync">
|
||||||
|
{{ t('case.apiSyncChange') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 图例 -->
|
||||||
|
<div class="legend-container">
|
||||||
|
<div class="item mr-[24px]">
|
||||||
|
<div class="legend add"></div>
|
||||||
|
{{ t('case.diffAdd') }}
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="legend delete"></div>
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 对比 -->
|
||||||
|
<div class="diff-container">
|
||||||
|
<div class="diff-item ml-[16px] mr-[8px]">
|
||||||
|
<div class="title-type"> [{{ apiDetailInfo?.num }}] {{ apiDetailInfo?.name }} </div>
|
||||||
|
<DiffItem :diff-distance-map="diffDistanceMap" mode="add" is-api :detail="apiDefinedRequest as RequestParam" />
|
||||||
|
</div>
|
||||||
|
<div class="diff-item ml-[8px] mr-[16px]">
|
||||||
|
<div class="title-type"> [{{ caseDetail?.num }}] {{ caseDetail?.name }} </div>
|
||||||
|
<DiffItem :diff-distance-map="diffDistanceMap" mode="delete" :detail="caseDetail as RequestParam" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MsDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import { TabItem } from '@/components/pure/ms-editable-tab/types';
|
||||||
|
import DiffItem from './diffItem.vue';
|
||||||
|
|
||||||
|
import { getCaseDetail, getDefinitionDetail } from '@/api/modules/api-test/management';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { EnableKeyValueParam, ExecuteRequestCommonParam } from '@/models/apiTest/common';
|
||||||
|
import { ApiDefinitionDetail } from '@/models/apiTest/management';
|
||||||
|
import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||||
|
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeApiCaseId: string;
|
||||||
|
activeDefinedId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showDiffVisible = defineModel<boolean>('visible', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkList = ref([
|
||||||
|
{
|
||||||
|
value: RequestComposition.HEADER,
|
||||||
|
label: t('apiTestDebug.header'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RequestComposition.BODY,
|
||||||
|
label: t('apiTestDebug.body'),
|
||||||
|
tooltip: t('case.onlySyncNewParamsOrValue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RequestComposition.QUERY,
|
||||||
|
label: RequestComposition.QUERY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RequestComposition.REST,
|
||||||
|
label: RequestComposition.REST,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const initForm = {
|
||||||
|
deleteParams: false,
|
||||||
|
checkType: [],
|
||||||
|
noticeApiCaseCreator: true,
|
||||||
|
noticeApiScenarioCreator: true,
|
||||||
|
ignoreUpdate: false,
|
||||||
|
ignoreUpdateType: ['THIS_TIME'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = ref({ ...initForm });
|
||||||
|
|
||||||
|
const ignoreList = ref([
|
||||||
|
{
|
||||||
|
value: 'THIS_TIME',
|
||||||
|
label: t('case.ignoreThisChange'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ALL',
|
||||||
|
label: t('case.ignoreAllChange'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 忽略更新
|
||||||
|
function changeIgnoreType() {}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
showDiffVisible.value = false;
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
const syncLoading = ref<boolean>(false);
|
||||||
|
// 同步
|
||||||
|
function confirmBatchSync() {}
|
||||||
|
|
||||||
|
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
|
||||||
|
const caseDetail = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const apiDetailInfo = ref<Record<string, any>>({});
|
||||||
|
const apiDefinedRequest = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const diffDistanceMap = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置对比
|
||||||
|
* @params apiValue 接口数据
|
||||||
|
* @params caseValue 用例数据
|
||||||
|
* @params typeKey 用于增加间距Map的对应KEY\HEADER\QUERY等
|
||||||
|
* @params nameKey 对应参数名称key
|
||||||
|
*/
|
||||||
|
function setDiff(
|
||||||
|
apiValue: Record<string, any>[],
|
||||||
|
caseValue: Record<string, any>[],
|
||||||
|
typeKey: string,
|
||||||
|
nameKey = 'key'
|
||||||
|
) {
|
||||||
|
const apiDefinedValue = (apiValue || []).filter((e) => e.key !== '') || [];
|
||||||
|
|
||||||
|
const apiCaseValue = (caseValue || []).filter((e) => e.key !== '') || [];
|
||||||
|
|
||||||
|
const caseValueMap = new Map();
|
||||||
|
const apiValueMap = new Map();
|
||||||
|
apiDefinedValue.forEach((item: any) => apiValueMap.set(item[nameKey], item));
|
||||||
|
apiCaseValue.forEach((item: any) => caseValueMap.set(item[nameKey], item));
|
||||||
|
|
||||||
|
const definedData: Record<string, any>[] = apiDefinedValue.map((item) => {
|
||||||
|
if (!caseValueMap.has(item[nameKey])) {
|
||||||
|
return {
|
||||||
|
...cloneDeep(item),
|
||||||
|
diff: 'change',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
const caseData: Record<string, any>[] = apiCaseValue.map((item) => {
|
||||||
|
if (!apiValueMap.has(item[nameKey])) {
|
||||||
|
return {
|
||||||
|
...cloneDeep(item),
|
||||||
|
diff: 'change',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置对比水平对齐间距绝对值
|
||||||
|
const disAbs = Math.abs(caseData.length - definedData.length);
|
||||||
|
|
||||||
|
diffDistanceMap.value[typeKey] = {
|
||||||
|
case: caseData.length < definedData.length ? disAbs : 0,
|
||||||
|
api: caseData.length > definedData.length ? disAbs : 0,
|
||||||
|
display: caseData.length !== 0 || definedData.length !== 0, // 如果都为空则不展示
|
||||||
|
showEmptyCaseTable: caseData.length !== 0, // 隐藏用例空表
|
||||||
|
showEmptyApiTable: definedData.length !== 0, // 隐藏api空表
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
caseData,
|
||||||
|
definedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对比数据
|
||||||
|
function processData() {
|
||||||
|
// 处理请求头
|
||||||
|
const headersObj = setDiff(
|
||||||
|
apiDefinedRequest.value?.headers as any,
|
||||||
|
caseDetail.value.headers,
|
||||||
|
RequestComposition.HEADER
|
||||||
|
);
|
||||||
|
if (apiDefinedRequest.value?.headers) {
|
||||||
|
apiDefinedRequest.value.headers = headersObj.definedData as EnableKeyValueParam[];
|
||||||
|
caseDetail.value.headers = headersObj.caseData;
|
||||||
|
}
|
||||||
|
// 处理query
|
||||||
|
const queryDiffObj = setDiff(
|
||||||
|
apiDefinedRequest.value?.query as any,
|
||||||
|
caseDetail.value.query,
|
||||||
|
RequestComposition.QUERY
|
||||||
|
);
|
||||||
|
if (apiDefinedRequest.value?.query) {
|
||||||
|
apiDefinedRequest.value.query = queryDiffObj.definedData as ExecuteRequestCommonParam[];
|
||||||
|
caseDetail.value.query = queryDiffObj.caseData;
|
||||||
|
}
|
||||||
|
// 处理rest
|
||||||
|
const restDiffObj = setDiff(apiDefinedRequest.value?.rest as any, caseDetail.value.rest, RequestComposition.REST);
|
||||||
|
if (apiDefinedRequest.value?.rest) {
|
||||||
|
apiDefinedRequest.value.rest = restDiffObj.definedData as ExecuteRequestCommonParam[];
|
||||||
|
caseDetail.value.rest = restDiffObj.caseData;
|
||||||
|
}
|
||||||
|
// 处理请求体
|
||||||
|
if (apiDefinedRequest.value?.body?.bodyType) {
|
||||||
|
switch (apiDefinedRequest.value?.body?.bodyType) {
|
||||||
|
// FORM_DATA格式
|
||||||
|
case RequestBodyFormat.FORM_DATA:
|
||||||
|
const bodyFormDataDiffObj = setDiff(
|
||||||
|
apiDefinedRequest.value.body.formDataBody?.formValues as any,
|
||||||
|
caseDetail.value.body.formDataBody?.formValues,
|
||||||
|
RequestBodyFormat.FORM_DATA
|
||||||
|
);
|
||||||
|
apiDefinedRequest.value.body.formDataBody.formValues =
|
||||||
|
bodyFormDataDiffObj.definedData as ExecuteRequestCommonParam[];
|
||||||
|
caseDetail.value.body.formDataBody.formValues = bodyFormDataDiffObj.caseData;
|
||||||
|
break;
|
||||||
|
// WWW_FORM格式
|
||||||
|
case RequestBodyFormat.WWW_FORM:
|
||||||
|
const bodyWwwFormDiffObj = setDiff(
|
||||||
|
apiDefinedRequest.value.body.wwwFormBody?.formValues as any,
|
||||||
|
caseDetail.value.body.wwwFormBody?.formValues,
|
||||||
|
RequestBodyFormat.WWW_FORM
|
||||||
|
);
|
||||||
|
apiDefinedRequest.value.body.wwwFormBody.formValues =
|
||||||
|
bodyWwwFormDiffObj.definedData as ExecuteRequestCommonParam[];
|
||||||
|
caseDetail.value.body.wwwFormBody.formValues = bodyWwwFormDiffObj.caseData;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取用例详情
|
||||||
|
async function getCaseDetailInfo(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await getCaseDetail(id);
|
||||||
|
let parseRequestBodyResult;
|
||||||
|
if (res.protocol === 'HTTP') {
|
||||||
|
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
|
||||||
|
}
|
||||||
|
caseDetail.value = {
|
||||||
|
...cloneDeep(defaultCaseParams as RequestParam),
|
||||||
|
...({
|
||||||
|
...res.request,
|
||||||
|
...res,
|
||||||
|
url: res.path,
|
||||||
|
...parseRequestBodyResult,
|
||||||
|
} as Partial<TabItem>),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getApiDetail(apiDefinitionId: string) {
|
||||||
|
try {
|
||||||
|
const detail = await getDefinitionDetail(apiDefinitionId);
|
||||||
|
apiDetailInfo.value = detail as ApiDefinitionDetail;
|
||||||
|
apiDefinedRequest.value = detail.request as unknown as RequestParam;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRequestDetail(definedId: string, apiCaseId: string) {
|
||||||
|
try {
|
||||||
|
await Promise.all([getApiDetail(definedId), getCaseDetailInfo(apiCaseId)]);
|
||||||
|
processData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.activeDefinedId && props.activeApiCaseId) {
|
||||||
|
getRequestDetail(props.activeDefinedId, props.activeApiCaseId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.legend-container {
|
||||||
|
padding: 8px 0;
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
.item {
|
||||||
|
@apply flex items-center;
|
||||||
|
.legend {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
&.add {
|
||||||
|
border: 0.5px solid rgb(var(--success-6));
|
||||||
|
}
|
||||||
|
&.delete {
|
||||||
|
border: 0.5px solid rgb(var(--danger-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.diff-container {
|
||||||
|
@apply flex;
|
||||||
|
.diff-item {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
|
||||||
|
@apply flex-1;
|
||||||
|
.title-type {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.arco-table-td-content) {
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
:deep(.ms-json-schema) .arco-table-td-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.diff-modal {
|
||||||
|
.ms-drawer-body {
|
||||||
|
background: var(--color-text-n9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="showBatchSyncModal"
|
||||||
|
title-align="start"
|
||||||
|
class="ms-modal-upload ms-modal-medium"
|
||||||
|
:width="600"
|
||||||
|
@close="cancel"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('case.apiSyncChange') }}
|
||||||
|
<div class="text-[var(--color-text-4)]">
|
||||||
|
{{
|
||||||
|
t('common.selectedCount', {
|
||||||
|
count: props.batchParams.currentSelectCount || 0,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-alert class="mb-[16px]" type="warning">{{ t('case.apiSyncModalAlert') }}</a-alert>
|
||||||
|
<div class="mb-[8px]">
|
||||||
|
{{ t('case.syncItem') }}
|
||||||
|
</div>
|
||||||
|
<a-checkbox-group v-model="form.checkType">
|
||||||
|
<a-checkbox v-for="item of checkList" :key="item.value" :value="item.value">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{ item.label }}
|
||||||
|
<a-tooltip v-if="item.tooltip" :content="item.tooltip" position="top">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<icon-question-circle
|
||||||
|
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
<div class="my-[16px] flex items-center">
|
||||||
|
<a-switch v-model:model-value="form.deleteParams" size="small" />
|
||||||
|
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('case.deleteNotCorrespondValue') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-[16px] flex items-center">
|
||||||
|
{{ t('case.changeNotice') }}
|
||||||
|
<a-tooltip :content="t('case.confirmMessageStatusEnable')" position="bl">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<icon-question-circle
|
||||||
|
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||||
|
size="16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="my-[16px] flex items-center">
|
||||||
|
<a-switch v-model:model-value="form.noticeApiCaseCreator" size="small" />
|
||||||
|
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('case.NoticeApiCaseCreator') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-[16px] flex items-center">
|
||||||
|
<a-switch v-model:model-value="form.noticeApiScenarioCreator" size="small" />
|
||||||
|
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('case.NoticeApiScenarioCreator') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<a-button type="secondary" @click="cancel">{{ t('common.cancel') }}</a-button>
|
||||||
|
<a-button type="primary" :loading="syncLoading" :disabled="!form.checkType.length" @click="confirmBatchSync">
|
||||||
|
{{ t('case.apiSyncChange') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
|
||||||
|
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
import { RequestComposition } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
batchParams: BatchActionQueryParams;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showBatchSyncModal = defineModel<boolean>('visible', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const initForm = {
|
||||||
|
deleteParams: false,
|
||||||
|
checkType: [],
|
||||||
|
noticeApiCaseCreator: true,
|
||||||
|
noticeApiScenarioCreator: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = ref({ ...initForm });
|
||||||
|
|
||||||
|
const checkList = ref([
|
||||||
|
{
|
||||||
|
value: RequestComposition.HEADER,
|
||||||
|
label: t('apiTestDebug.header'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RequestComposition.BODY,
|
||||||
|
label: t('apiTestDebug.body'),
|
||||||
|
tooltip: t('case.onlySyncNewParamsOrValue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RequestComposition.QUERY,
|
||||||
|
label: RequestComposition.QUERY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RequestComposition.REST,
|
||||||
|
label: RequestComposition.REST,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const syncLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
showBatchSyncModal.value = false;
|
||||||
|
}
|
||||||
|
// 同步 TODO 等待联调
|
||||||
|
function confirmBatchSync() {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -205,6 +205,26 @@ export default {
|
||||||
'case.recycle.confirmRecovery': 'Confirm Recovery',
|
'case.recycle.confirmRecovery': 'Confirm Recovery',
|
||||||
'case.createCase': 'Create Case',
|
'case.createCase': 'Create Case',
|
||||||
'case.updateCase': 'Update Case',
|
'case.updateCase': 'Update Case',
|
||||||
|
'case.changeDifferences': 'Interface versus use case differences',
|
||||||
|
'case.apiParamsHasChange': 'Interface parameter is changed',
|
||||||
|
'case.apiSyncChange': 'Sync',
|
||||||
|
'case.apiSyncModalAlert':
|
||||||
|
'Interface definition request parameter synchronization to the interface use case, may result in abnormal interfaces to perform case execution!',
|
||||||
|
'case.syncItem': 'Sync item',
|
||||||
|
'case.onlySyncNewParamsOrValue':
|
||||||
|
'JSON format, only the synchronous interface definition of new parameters and parameter values',
|
||||||
|
'case.deleteNotCorrespondValue':
|
||||||
|
'Remove parameters in the use case that cannot correspond to the interface definition',
|
||||||
|
'case.changeNotice': 'Change notice',
|
||||||
|
'case.confirmMessageStatusEnable':
|
||||||
|
'Verify that the "CASE Update "event in the Message Managementinterface test is configured for receivers and is on',
|
||||||
|
'case.NoticeApiCaseCreator': 'Notifies the creator of the interface use case',
|
||||||
|
'case.NoticeApiScenarioCreator': 'Notify the founder of citing the use case scenario',
|
||||||
|
'case.apiAndCaseDiff': 'Interface vs. use case differences',
|
||||||
|
'case.ignoreThisChange': 'Ignore this change',
|
||||||
|
'case.ignoreAllChange': 'Ignore all changes',
|
||||||
|
'case.diffAdd': 'Add',
|
||||||
|
'case.notSetData': 'No data has been set',
|
||||||
'case.saveContinueText': 'Save and Continue Creating',
|
'case.saveContinueText': 'Save and Continue Creating',
|
||||||
'case.detail.changeHistoryTip':
|
'case.detail.changeHistoryTip':
|
||||||
"View and compare historical changes. According to the administrator's settings, historical data will be automatically deleted",
|
"View and compare historical changes. According to the administrator's settings, historical data will be automatically deleted",
|
||||||
|
|
|
@ -196,6 +196,22 @@ export default {
|
||||||
'case.recycle.confirmRecovery': '确认恢复',
|
'case.recycle.confirmRecovery': '确认恢复',
|
||||||
'case.createCase': '创建用例',
|
'case.createCase': '创建用例',
|
||||||
'case.updateCase': '更新用例',
|
'case.updateCase': '更新用例',
|
||||||
|
'case.changeDifferences': '接口与用例差异对比',
|
||||||
|
'case.apiParamsHasChange': '接口参数发生变更',
|
||||||
|
'case.apiSyncChange': '同步',
|
||||||
|
'case.apiSyncModalAlert': '接口定义请求参数同步到接口用例中,可能导致待执行的接口用例执行异常!',
|
||||||
|
'case.syncItem': '同步项',
|
||||||
|
'case.onlySyncNewParamsOrValue': 'JSON 格式,仅同步接口定义新增的参数与参数值',
|
||||||
|
'case.deleteNotCorrespondValue': '删除用例中无法与接口定义对应的参数',
|
||||||
|
'case.changeNotice': '变更通知',
|
||||||
|
'case.confirmMessageStatusEnable': '请确认消息管理-接口测试中的"CASE更新"事件已配置接收人且状态为开启',
|
||||||
|
'case.NoticeApiCaseCreator': '通知接口用例的创建人',
|
||||||
|
'case.NoticeApiScenarioCreator': '通知引用该用例的场景创建人',
|
||||||
|
'case.apiAndCaseDiff': '接口与用例差异对比',
|
||||||
|
'case.ignoreThisChange': '忽略本次变更差异',
|
||||||
|
'case.ignoreAllChange': '忽略全部变更差异',
|
||||||
|
'case.diffAdd': '新增',
|
||||||
|
'case.notSetData': '暂未设置数据',
|
||||||
'case.saveContinueText': '保存并继续创建',
|
'case.saveContinueText': '保存并继续创建',
|
||||||
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
|
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
|
||||||
'case.detail.noReminders': '不再提醒',
|
'case.detail.noReminders': '不再提醒',
|
||||||
|
|
Loading…
Reference in New Issue