fix: 修复缺陷管理&用例管理&报告相关问题

This commit is contained in:
xinxin.wu 2024-04-02 22:14:35 +08:00 committed by Craftsman
parent 890429bb6e
commit 1b4b6f0076
25 changed files with 514 additions and 307 deletions

View File

@ -51,7 +51,8 @@
<div>{{ t('apiTestDebug.expressionTip2') }}</div> <div>{{ t('apiTestDebug.expressionTip2') }}</div>
<div>{{ t('apiTestDebug.expressionTip3') }}</div> <div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template> </template>
<!-- <MsIcon <MsIcon
v-if="props.showExtraction"
:disabled="props.disabled" :disabled="props.disabled"
type="icon-icon_flashlamp" type="icon-icon_flashlamp"
:size="15" :size="15"
@ -61,7 +62,7 @@
: 'ms-params-input-suffix-icon' : 'ms-params-input-suffix-icon'
" "
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)" @click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)"
/> --> />
</a-tooltip> </a-tooltip>
</template> </template>
</a-input> </a-input>
@ -146,7 +147,8 @@
<div>{{ t('apiTestDebug.expressionTip2') }}</div> <div>{{ t('apiTestDebug.expressionTip2') }}</div>
<div>{{ t('apiTestDebug.expressionTip3') }}</div> <div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template> </template>
<!-- <MsIcon <MsIcon
v-if="props.showExtraction"
type="icon-icon_flashlamp" type="icon-icon_flashlamp"
:disabled="props.disabled" :disabled="props.disabled"
:size="15" :size="15"
@ -156,7 +158,7 @@
: 'ms-params-input-suffix-icon' : 'ms-params-input-suffix-icon'
" "
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.X_PATH)" @click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.X_PATH)"
/> --> />
</a-tooltip> </a-tooltip>
</template> </template>
</a-input> </a-input>
@ -295,6 +297,7 @@
<div>{{ t('apiTestDebug.expressionTip3') }}</div> <div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template> </template>
<MsIcon <MsIcon
v-if="props.showExtraction"
type="icon-icon_flashlamp" type="icon-icon_flashlamp"
:size="15" :size="15"
:class=" :class="
@ -349,7 +352,7 @@
import { TableColumnData, TableData } from '@arco-design/web-vue'; import { TableColumnData, TableData } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { statusCodeOptions } from '@/components/pure/ms-advance-filter'; import { EQUAL, statusCodeOptions } from '@/components/pure/ms-advance-filter';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { TableOperationColumn } from '../../ms-user-group-comp/authTable.vue'; import { TableOperationColumn } from '../../ms-user-group-comp/authTable.vue';
import fastExtraction from '@/views/api-test/components/fastExtraction/index.vue'; import fastExtraction from '@/views/api-test/components/fastExtraction/index.vue';
@ -389,11 +392,17 @@
[key: string]: any; [key: string]: any;
} }
const props = defineProps<{ const props = withDefaults(
data: Param; defineProps<{
response?: string; data: Param;
disabled?: boolean; response?: string;
}>(); disabled?: boolean;
showExtraction?: boolean;
}>(),
{
showExtraction: false,
}
);
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:data', data: ExecuteConditionProcessor): void; (e: 'update:data', data: ExecuteConditionProcessor): void;
@ -486,11 +495,9 @@
// json // json
const jsonPathDefaultParamItem = { const jsonPathDefaultParamItem = {
expression: '', expression: '',
condition: '', condition: EQUAL.value,
expectedValue: '', expectedValue: '',
enable: true, enable: true,
moreSettingPopoverVisible: false,
disable: true,
}; };
// xpath // xpath
const xPathDefaultParamItem = { const xPathDefaultParamItem = {

View File

@ -9,6 +9,7 @@
:disabled="props.disabled" :disabled="props.disabled"
:step="100" :step="100"
:min="0" :min="0"
:precision="0"
mode="button" mode="button"
@blur=" @blur="
emit('change', { emit('change', {

View File

@ -105,6 +105,7 @@
v-model:data="getCurrentItemState" v-model:data="getCurrentItemState"
:disabled="props.disabled" :disabled="props.disabled"
:response="props.response" :response="props.response"
:show-extraction="props.showExtraction"
@change="handleChange" @change="handleChange"
/> />
<!-- 响应时间 --> <!-- 响应时间 -->
@ -176,6 +177,7 @@
assertionConfig?: ExecuteAssertionConfig; // assertionConfig?: ExecuteAssertionConfig; //
response?: string; // response?: string; //
disabled?: boolean; disabled?: boolean;
showExtraction?: boolean; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -8,6 +8,7 @@
* @description 用于自己扩展功能的form-create * @description 用于自己扩展功能的form-create
*/ */
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useVModel } from '@vueuse/core';
import { FieldTypeFormRules } from '@/components/pure/ms-form-create/form-create'; import { FieldTypeFormRules } from '@/components/pure/ms-form-create/form-create';
import JiraKey from './comp/jiraKey.vue'; import JiraKey from './comp/jiraKey.vue';
@ -36,7 +37,7 @@
api: any; // api: any; //
}>(); }>();
const emit = defineEmits(['update:api', 'update', 'update:form-item']); const emit = defineEmits(['update:api', 'update', 'update:formItem', 'change']);
const fApi = computed({ const fApi = computed({
get() { get() {
@ -47,6 +48,8 @@
}, },
}); });
const innerFormItem = useVModel(props, 'formItem', emit);
// //
const formItems = ref<FormItem[]>([...props.formRule]); const formItems = ref<FormItem[]>([...props.formRule]);
// //
@ -281,15 +284,16 @@
}, },
}; };
function changeHandler(value: any) { function changeHandler(value: any, defaultValue: any, formRuleItem: FormRuleItem, api: any) {
fApi.value.validateField(value); fApi.value.validateField(value);
emit('change', defaultValue, formRuleItem, api);
} }
watch( watch(
() => formRuleList.value, () => formRuleList.value,
(val) => { (val) => {
if (val) { if (val) {
emit('update:form-item', formRuleList.value); innerFormItem.value = val;
} }
}, },
{ {

View File

@ -93,12 +93,14 @@
} }
} }
); );
const isInit = ref<boolean>(true);
watch( watch(
() => selectValue.value, () => selectValue.value,
(val) => { (val) => {
selectValue.value = val; selectValue.value = val;
emit('update:model-value', val); if (!isInit.value) {
emit('update:model-value', val);
}
} }
); );
@ -115,6 +117,7 @@
if (props.inputSearch && props.optionMethod) { if (props.inputSearch && props.optionMethod) {
getLinksItem(); getLinksItem();
} }
isInit.value = false;
}); });
</script> </script>

View File

@ -198,8 +198,8 @@ export interface ResponseDocumentAssertion {
} }
// 断言-断言列表的断言子项 // 断言-断言列表的断言子项
export interface ResponseAssertionItem { export interface ResponseAssertionItem {
condition: RequestAssertionConditionType; condition?: RequestAssertionConditionType;
expectedValue: string; expectedValue?: string;
expression: string; expression: string;
enable?: boolean; enable?: boolean;
} }

View File

@ -5,9 +5,11 @@ import {
ExecuteRequestFormBodyFormValue, ExecuteRequestFormBodyFormValue,
KeyValueParam, KeyValueParam,
RequestTaskResult, RequestTaskResult,
ResponseAssertionItem,
ResponseDefinition, ResponseDefinition,
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import { import {
RequestAssertionCondition,
RequestBodyFormat, RequestBodyFormat,
RequestCaseStatus, RequestCaseStatus,
RequestContentTypeEnum, RequestContentTypeEnum,
@ -157,3 +159,17 @@ export const caseStatusOptions = [
{ label: 'apiTestManagement.deprecate', value: RequestCaseStatus.DEPRECATED }, { label: 'apiTestManagement.deprecate', value: RequestCaseStatus.DEPRECATED },
{ label: 'apiTestManagement.done', value: RequestCaseStatus.DONE }, { label: 'apiTestManagement.done', value: RequestCaseStatus.DONE },
]; ];
// 断言 参数表格默认行的值
export const defaultAssertParamsItem: ResponseAssertionItem = {
expression: '',
condition: RequestAssertionCondition.EQUALS,
expectedValue: '',
enable: true,
};
// 断言xpath & reg
export const defaultAssertXpathParamsItem: ResponseAssertionItem = {
expression: '',
enable: true,
};

View File

@ -88,6 +88,7 @@
v-model:params="requestVModel.children[0].assertionConfig.assertions" v-model:params="requestVModel.children[0].assertionConfig.assertions"
:disabled="props.disabledExceptParam" :disabled="props.disabledExceptParam"
:is-definition="false" :is-definition="false"
:show-extraction="true"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
/> />
<auth <auth

View File

@ -264,6 +264,7 @@
:is-definition="props.isDefinition" :is-definition="props.isDefinition"
:response="requestVModel.response?.requestResults[0]?.responseResult.body" :response="requestVModel.response?.requestResults[0]?.responseResult.body"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
:show-extraction="true"
/> />
<auth <auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH" v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
@ -580,6 +581,8 @@
import { import {
casePriorityOptions, casePriorityOptions,
caseStatusOptions, caseStatusOptions,
defaultAssertParamsItem,
defaultAssertXpathParamsItem,
defaultBodyParamsItem, defaultBodyParamsItem,
defaultHeaderParamsItem, defaultHeaderParamsItem,
defaultKeyValueParamItem, defaultKeyValueParamItem,
@ -1210,6 +1213,39 @@
requestName = requestVModel.value.isNew ? saveModalForm.value.name : requestVModel.value.name; requestName = requestVModel.value.isNew ? saveModalForm.value.name : requestVModel.value.name;
requestModuleId = requestVModel.value.isNew ? saveModalForm.value.moduleId : requestVModel.value.moduleId; requestModuleId = requestVModel.value.isNew ? saveModalForm.value.moduleId : requestVModel.value.moduleId;
} }
//
const { assertionConfig } = requestVModel.value.children[0];
const assertionList = assertionConfig.assertions.map((assertItem: any) => {
const bodyAssertionDataByTypeList = filterKeyValParams(
assertItem.bodyAssertionDataByType.assertions,
defaultAssertParamsItem,
isExecute
).validParams;
return {
...assertItem,
bodyAssertionDataByType: {
...assertItem.bodyAssertionDataByType,
assertions: bodyAssertionDataByTypeList,
},
regexAssertion: {
...assertItem.regexAssertion,
assertions: filterKeyValParams(assertItem.regexAssertion.assertions, defaultAssertXpathParamsItem, isExecute)
.validParams,
},
xpathAssertion: {
...assertItem.xpathAssertion,
assertions: filterKeyValParams(assertItem.xpathAssertion.assertions, defaultAssertXpathParamsItem, isExecute)
.validParams,
},
jsonPathAssertion: {
...assertItem.jsonPathAssertion,
assertions: filterKeyValParams(assertItem.jsonPathAssertion.assertions, defaultAssertParamsItem, isExecute)
.validParams,
},
};
});
return { return {
id: requestVModel.value.id.toString(), id: requestVModel.value.id.toString(),
reportId: reportId.value, reportId: reportId.value,
@ -1226,7 +1262,10 @@
children: [ children: [
{ {
polymorphicName: 'MsCommonElement', // MsCommonElement polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: requestVModel.value.children[0].assertionConfig, assertionConfig: {
...assertionConfig,
assertions: assertionList,
},
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig), postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig), preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
}, },
@ -1658,15 +1697,10 @@
} }
} }
.url-input-tip { .url-input-tip {
display: flex; margin-top: 2px 0 250px;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-start;
margin-left: 250px;
color: rgb(var(--danger-6));
font-size: 12px; font-size: 12px;
color: rgb(var(--danger-6));
line-height: 16px; line-height: 16px;
margin-top: 2px; @apply flex flex-col flex-nowrap items-center justify-start;
} }
</style> </style>

View File

@ -1,114 +1,115 @@
<template> <template>
<div v-if="isShowLoopControl" class="my-4 flex items-center justify-start" @click.stop="() => {}"> <div class="flex h-[calc(100%-8px)] flex-col" @click.stop="() => {}">
<a-pagination <div v-if="isShowLoopControl" class="my-4 flex items-center justify-start" @click.stop="() => {}">
v-model:page-size="controlPageSize" <a-pagination
v-model:current="controlCurrent" v-model:page-size="controlPageSize"
:total="controlTotal" v-model:current="controlCurrent"
size="mini" :total="controlTotal"
show-total size="mini"
:show-jumper="controlTotal > 5" show-total
@change="loadControlLoop" :show-jumper="controlTotal > 5"
/> @change="loadControlLoop"
</div> />
<div class="mt-4 flex w-full items-center justify-between rounded bg-[var(--color-text-n9)] p-4"> <!-- <loopPagination v-model:current-loop="controlCurrent" :loop-total="controlTotal" /> -->
<div class="font-medium">
<span
:class="{ 'text-[rgb(var(--primary-5))]': activeType === 'ResContent' }"
@click.stop="setActiveType('ResContent')"
>{{ t('report.detail.api.resContent') }}</span
>
<span
v-if="total > 0"
:class="{ 'text-[rgb(var(--primary-5))]': activeType === 'SubRequest' }"
@click.stop="setActiveType('SubRequest')"
>
<a-divider direction="vertical" :margin="8"></a-divider>
{{ t('report.detail.api.subRequest') }}</span
>
</div> </div>
<div class="flex flex-row gap-6 text-center"> <div class="mt-4 flex w-full items-center justify-between rounded bg-[var(--color-text-n9)] p-4">
<a-popover position="left" content-class="response-popover-content"> <div class="font-medium">
<div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor }"> <span
{{ activeStepDetail?.content?.responseResult.responseCode || '-' }} :class="{ 'text-[rgb(var(--primary-5))]': activeType === 'ResContent' }"
</div> @click.stop="setActiveType('ResContent')"
<template #content> >{{ t('report.detail.api.resContent') }}</span
<div class="flex items-center gap-[8px] text-[14px]"> >
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div> <span
<div :style="{ color: statusCodeColor }"> v-if="total > 0"
{{ activeStepDetail?.content?.responseResult.responseCode || '-' }} :class="{ 'text-[rgb(var(--primary-5))]': activeType === 'SubRequest' }"
@click.stop="setActiveType('SubRequest')"
>
<a-divider direction="vertical" :margin="8"></a-divider>
{{ t('report.detail.api.subRequest') }}</span
>
</div>
<div class="flex flex-row gap-6 text-center">
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor }">
{{ activeStepDetail?.content?.responseResult.responseCode || '-' }}
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
<div :style="{ color: statusCodeColor }">
{{ activeStepDetail?.content?.responseResult.responseCode || '-' }}
</div>
</div> </div>
</div> </template>
</template> </a-popover>
</a-popover> <a-popover position="left" content-class="w-[400px]">
<a-popover position="left" content-class="w-[400px]"> <div class="one-line-text text-[rgb(var(--success-7))]"> {{ timingInfo?.responseTime || 0 }} ms </div>
<div class="one-line-text text-[rgb(var(--success-7))]"> {{ timingInfo?.responseTime || 0 }} ms </div> <template #content>
<template #content> <div class="mb-[8px] flex items-center gap-[8px] text-[14px]">
<div class="mb-[8px] flex items-center gap-[8px] text-[14px]"> <div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div>
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div> <div class="text-[rgb(var(--success-7))]"> {{ timingInfo?.responseTime }} ms </div>
<div class="text-[rgb(var(--success-7))]"> {{ timingInfo?.responseTime }} ms </div>
</div>
<responseTimeLine v-if="timingInfo" :response-timing="timingInfo" />
</template>
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ activeStepDetail?.content?.responseResult.responseSize || '-' }} bytes
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ activeStepDetail?.content?.responseResult.responseSize }} bytes
</div> </div>
<responseTimeLine v-if="timingInfo" :response-timing="timingInfo" />
</template>
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ activeStepDetail?.content?.responseResult.responseSize || '-' }} bytes
</div> </div>
</template> <template #content>
</a-popover> <div class="flex items-center gap-[8px] text-[14px]">
<a-popover position="left" content-class="response-popover-content"> <div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
<div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text max-w-[150px]">{{ <div class="one-line-text text-[rgb(var(--success-7))]">
props.environmentName {{ activeStepDetail?.content?.responseResult.responseSize }} bytes
}}</div> </div>
<template #content> </div>
<div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text">{{ </template>
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text max-w-[150px]">{{
props.environmentName props.environmentName
}}</div> }}</div>
</template> <template #content>
</a-popover> <div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text">{{
props.environmentName
}}</div>
</template>
</a-popover>
</div>
</div> </div>
</div> <div v-if="activeType === 'SubRequest'" class="my-4 flex justify-start">
<div v-if="activeType === 'SubRequest'" class="my-4 flex justify-start"> <MsPagination
<MsPagination v-model:page-size="pageSize"
v-model:page-size="pageSize" v-model:current="current"
v-model:current="current" :total="total"
:total="total" size="mini"
size="mini" @change="loadLoop"
@change="loadLoop" />
/> </div>
</div> <!-- 平铺 -->
<!-- 平铺 --> <TiledDisplay
<TiledDisplay v-if="props.mode === 'tiled'"
v-if="props.mode === 'tiled'" :menu-list="responseCompositionTabList"
:menu-list="responseCompositionTabList"
:request-result="activeStepDetailCopy?.content"
:console="props.console"
:is-definition="props.isDefinition"
:report-id="props.reportId"
/>
<!-- 响应内容tab -->
<a-spin
v-else
:loading="loading"
:class="[props.isResponseModel ? 'h-full w-full' : 'h-[calc(100%-35px)] w-full px-[18px] pb-[18px]']"
>
<result
v-model:active-tab="activeTab"
:request-result="activeStepDetailCopy?.content" :request-result="activeStepDetailCopy?.content"
:console="props.console" :console="props.console"
:is-http-protocol="false" :is-definition="props.isDefinition"
:request-url="activeStepDetail?.content.url" :report-id="props.reportId"
is-definition
:is-priority-local-exec="false"
/> />
</a-spin> <!-- 响应内容tab -->
<div v-else class="h-[calc(100%-8px)]">
<a-spin :loading="loading" class="h-[calc(100%-8px)] w-full pb-1">
<result
v-model:active-tab="activeTab"
:request-result="activeStepDetailCopy?.content"
:console="props.console"
:is-http-protocol="false"
:request-url="activeStepDetail?.content.url"
is-definition
:is-priority-local-exec="false"
/>
</a-spin>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -116,18 +117,19 @@
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsPagination from '@/components/pure/ms-pagination/index';
import TiledDisplay from './tiledDisplay.vue';
import result from '@/views/api-test/components/requestComposition/response/result.vue'; import result from '@/views/api-test/components/requestComposition/response/result.vue';
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue'; import loopPagination from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
import { reportCaseStepDetail, reportStepDetail } from '@/api/modules/api-test/report'; import { reportCaseStepDetail, reportStepDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { findNodeByKey, formatDuration } from '@/utils';
import type { ReportStepDetail, ReportStepDetailItem, ScenarioItemType } from '@/models/apiTest/report'; import type { ReportStepDetail, ReportStepDetailItem, ScenarioItemType } from '@/models/apiTest/report';
import { ResponseComposition, ScenarioStepType } from '@/enums/apiEnum'; import { ResponseComposition, ScenarioStepType } from '@/enums/apiEnum';
const TiledDisplay = defineAsyncComponent(() => import('./tiledDisplay.vue'));
const responseTimeLine = defineAsyncComponent(() => import('@/views/api-test/components/responseTimeLine.vue'));
const MsPagination = defineAsyncComponent(() => import('@/components/pure/ms-pagination/index'));
const props = defineProps<{ const props = defineProps<{
mode: 'tiled' | 'tab'; // | tab mode: 'tiled' | 'tab'; // | tab
stepItem?: ScenarioItemType; // stepItem?: ScenarioItemType; //
@ -312,10 +314,10 @@
); );
}); });
const controlCurrent = ref(1); const controlCurrent = ref<number>(1);
const controlTotal = computed(() => { const controlTotal = computed(() => {
if (props.stepItem?.children) { if (props.stepItem?.children) {
return props.stepItem.children.length; return props.stepItem.children.length || 0;
} }
return 0; return 0;
}); });

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex min-h-[110px] items-center"> <div class="flex min-h-[110px] items-center">
<div class="relative mr-4"> <div class="relative mr-4">
<div class="absolute bottom-0 left-[30%] top-[35%] text-center"> <div class="charts absolute text-center">
<div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div> <div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div>
<div class="text-[18px] font-medium">{{ props.requestTotal }}</div> <div class="text-[18px] font-medium">{{ props.requestTotal }}</div>
</div> </div>
@ -53,4 +53,11 @@
} }
} }
} }
.charts {
top: 30%;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
</style> </style>

View File

@ -249,6 +249,7 @@
color: '#00C261', color: '#00C261',
class: 'bg-[rgb(var(--success-6))]', class: 'bg-[rgb(var(--success-6))]',
rateKey: 'requestPassRate', rateKey: 'requestPassRate',
key: 'SUCCESS',
}, },
{ {
label: 'report.detail.api.misstatement', label: 'report.detail.api.misstatement',
@ -256,6 +257,7 @@
color: '#FFC14E', color: '#FFC14E',
class: 'bg-[rgb(var(--warning-6))]', class: 'bg-[rgb(var(--warning-6))]',
rateKey: 'requestFakeErrorRate', rateKey: 'requestFakeErrorRate',
key: 'FAKE_ERROR',
}, },
{ {
label: 'report.detail.api.error', label: 'report.detail.api.error',
@ -263,6 +265,7 @@
color: '#ED0303', color: '#ED0303',
class: 'bg-[rgb(var(--danger-6))]', class: 'bg-[rgb(var(--danger-6))]',
rateKey: 'requestErrorRate', rateKey: 'requestErrorRate',
key: 'ERROR',
}, },
{ {
label: 'report.detail.api.pending', label: 'report.detail.api.pending',
@ -270,14 +273,16 @@
color: '#D4D4D8', color: '#D4D4D8',
class: 'bg-[var(--color-text-input-border)]', class: 'bg-[var(--color-text-input-border)]',
rateKey: 'requestPendingRate', rateKey: 'requestPendingRate',
key: 'PENDING',
}, },
]; ];
let validArr; let validArr;
if (props?.detailInfo?.integrated) { if (props?.detailInfo?.integrated) {
validArr = cloneDeep(tempArr); validArr = cloneDeep(tempArr);
} else { } else {
validArr = props?.detailInfo?.status === 'SUCCESS' ? [tempArr[0]] : [tempArr[2]]; validArr = tempArr.filter((e) => e.key === props?.detailInfo?.status);
} }
charOptions.value.series.data = validArr.map((item: any) => { charOptions.value.series.data = validArr.map((item: any) => {
return { return {
value: detail.value[item.value] || 0, value: detail.value[item.value] || 0,
@ -287,6 +292,7 @@
}, },
}; };
}); });
legendData.value = validArr.map((item: any) => { legendData.value = validArr.map((item: any) => {
return { return {
...item, ...item,

View File

@ -50,8 +50,10 @@
</MsButton> --> </MsButton> -->
</div> </div>
</template> </template>
<template #default="{ detail }"> <template #default="{ loading }">
<CaseReportCom :detail-info="detail" /> <a-spin class="h-full w-full" :loading="loading">
<CaseReportCom :detail-info="reportStepDetail" />
</a-spin>
</template> </template>
</MsDetailDrawer> </MsDetailDrawer>
</template> </template>
@ -103,7 +105,7 @@
const innerReportId = ref(props.reportId); const innerReportId = ref(props.reportId);
const detailDrawerRef = ref(); const detailDrawerRef = ref();
const reportStepDetail = ref<ReportDetail>({ const initReportDetail = {
id: '', id: '',
name: '', // name: '', //
testPlanId: '', testPlanId: '',
@ -141,6 +143,10 @@
children: [], // children: [], //
stepTotal: 0, // stepTotal: 0, //
console: '', console: '',
};
const reportStepDetail = ref<ReportDetail>({
...initReportDetail,
}); });
/** /**
@ -185,8 +191,22 @@
// //
function loadedReport(detail: ReportDetail) { function loadedReport(detail: ReportDetail) {
innerReportId.value = detail.id; innerReportId.value = detail.id;
reportStepDetail.value = { ...initReportDetail };
reportStepDetail.value = cloneDeep(detail); reportStepDetail.value = cloneDeep(detail);
} }
onBeforeUnmount(() => {
detailDrawerRef.value?.destroy();
});
watch(
() => showDrawer.value,
(val) => {
if (!val) {
reportStepDetail.value = { ...initReportDetail };
}
}
);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -12,7 +12,7 @@
:table-data="props.tableData" :table-data="props.tableData"
:page-change="props.pageChange" :page-change="props.pageChange"
show-full-screen show-full-screen
:unmount-on-close="true" unmount-on-close
@loaded="loadedReport" @loaded="loadedReport"
> >
<template #titleRight="{ loading }"> <template #titleRight="{ loading }">
@ -51,8 +51,10 @@
</MsButton> --> </MsButton> -->
</div> </div>
</template> </template>
<template #default="{ detail }"> <template #default="{ loading }">
<ScenarioCom :detail-info="detail" /> <a-spin class="h-full w-full" :loading="loading">
<ScenarioCom :detail-info="reportStepDetail" />
</a-spin>
</template> </template>
</MsDetailDrawer> </MsDetailDrawer>
</template> </template>
@ -67,15 +69,13 @@
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue'; import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import ScenarioCom from './scenarioCom.vue'; import ScenarioCom from './scenarioCom.vue';
import { getShareInfo, getShareTime, reportScenarioDetail } from '@/api/modules/api-test/report'; import { getShareInfo, reportScenarioDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import type { ReportDetail } from '@/models/apiTest/report'; import type { ReportDetail } from '@/models/apiTest/report';
import { RouteEnum } from '@/enums/routeEnum'; import { RouteEnum } from '@/enums/routeEnum';
import * as constants from 'constants';
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
@ -105,7 +105,7 @@
const innerReportId = ref(props.reportId); const innerReportId = ref(props.reportId);
const reportStepDetail = ref<ReportDetail>({ const initReportStepDetail = {
id: '', id: '',
name: '', // name: '', //
testPlanId: '', testPlanId: '',
@ -143,6 +143,10 @@
children: [], // children: [], //
stepTotal: 0, // stepTotal: 0, //
console: '', console: '',
};
const reportStepDetail = ref<ReportDetail>({
...initReportStepDetail,
}); });
/** /**
@ -150,6 +154,7 @@
*/ */
function loadedReport(detail: ReportDetail) { function loadedReport(detail: ReportDetail) {
innerReportId.value = detail.id; innerReportId.value = detail.id;
reportStepDetail.value = { ...initReportStepDetail };
reportStepDetail.value = cloneDeep(detail); reportStepDetail.value = cloneDeep(detail);
} }
@ -195,6 +200,21 @@
*/ */
const exportLoading = ref<boolean>(false); const exportLoading = ref<boolean>(false);
function exportHandler() {} function exportHandler() {}
const detailDrawerRef = ref();
onBeforeUnmount(() => {
detailDrawerRef.value?.destroy();
});
watch(
() => showDrawer.value,
(val) => {
if (!val) {
reportStepDetail.value = { ...initReportStepDetail };
}
}
);
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -11,13 +11,19 @@
></div> ></div>
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">
<div class="one-line-text max-w-[150px]"> {{ props.detail.environmentName || '-' }}</div> <div class="one-line-text max-w-[150px]">
{{ props.detail.environmentName || t('report.detail.api.defaultEnv') }}</div
>
<a-divider direction="vertical" :margin="4" class="!mx-2"></a-divider> <a-divider direction="vertical" :margin="4" class="!mx-2"></a-divider>
<template #content> <template #content>
<div class="max-w-[400px] items-center gap-[8px] text-[14px]"> <div class="max-w-[400px] items-center gap-[8px] text-[14px]">
<div class="flex-shrink-0 text-[var(--color-text-4)]">{{ t('report.detail.api.executeEnv') }}</div> <div class="flex-shrink-0 text-[var(--color-text-4)]">{{ t('report.detail.api.executeEnv') }}</div>
<div> <div>
{{ props.detail.environmentName || '-' }} {{
props.detail.environmentName || props.showType === 'CASE'
? t('report.detail.api.caseSaveEnv')
: t('report.detail.api.scenarioSavedEnv')
}}
</div> </div>
</div> </div>
</template> </template>
@ -50,10 +56,10 @@
</template> </template>
</a-popover> </a-popover>
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">
<span v-if="showRunMode"> <span v-if="props.detail.integrated">
{{ props.detail.runMode === 'SERIAL' ? t('case.execute.serial') : t('case.execute.parallel') }}</span {{ props.detail.runMode === 'SERIAL' ? t('case.execute.serial') : t('case.execute.parallel') }}</span
> >
<a-divider v-if="showRunMode" direction="vertical" :margin="4" class="!mx-2"></a-divider> <a-divider v-if="props.detail.integrated" direction="vertical" :margin="4" class="!mx-2"></a-divider>
<template #content> <template #content>
<div class="items-center gap-[8px] text-[14px]"> <div class="items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('report.detail.api.runMode') }}</div> <div class="text-[var(--color-text-4)]">{{ t('report.detail.api.runMode') }}</div>
@ -99,10 +105,6 @@
detail: ReportDetail; detail: ReportDetail;
showType: 'API' | 'CASE'; showType: 'API' | 'CASE';
}>(); }>();
const showRunMode = computed(() => {
return props.showType === 'API' ? props.detail.runMode : props.detail.runMode && props.detail.integrated;
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -38,6 +38,21 @@
{{ record.integrated ? t('report.collection') : t('report.independent') }} {{ record.integrated ? t('report.collection') : t('report.independent') }}
</MsTag> </MsTag>
</template> </template>
<template #integratedFilter="{ columnConfig }">
<TableFilter
v-model:visible="reportTypeVisible"
v-model:status-filters="integratedFiltersMap[showType]"
:title="(columnConfig.title as string)"
:list="reportTypeList"
@search="initData()"
>
<template #item="{ item }">
<MsTag theme="light" :type="item.value === 'INTEGRATED' ? 'primary' : undefined">
{{ item.value === 'INTEGRATED' ? t('report.collection') : t('report.independent') }}
</MsTag>
</template>
</TableFilter>
</template>
<!-- 报告触发方式筛选 --> <!-- 报告触发方式筛选 -->
<template #triggerModeFilter="{ columnConfig }"> <template #triggerModeFilter="{ columnConfig }">
<a-trigger <a-trigger
@ -45,7 +60,11 @@
trigger="click" trigger="click"
@popup-visible-change="handleFilterHidden" @popup-visible-change="handleFilterHidden"
> >
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="triggerModeFilterVisible = true"> <a-button
type="text"
class="arco-btn-text--secondary p-[8px_4px]"
@click.stop="triggerModeFilterVisible = true"
>
<div class="font-medium"> <div class="font-medium">
{{ t(columnConfig.title as string) }} {{ t(columnConfig.title as string) }}
</div> </div>
@ -79,7 +98,7 @@
trigger="click" trigger="click"
@popup-visible-change="handleFilterHidden" @popup-visible-change="handleFilterHidden"
> >
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="statusFilterVisible = true"> <a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click.stop="statusFilterVisible = true">
<div class="font-medium"> <div class="font-medium">
{{ t(columnConfig.title as string) }} {{ t(columnConfig.title as string) }}
</div> </div>
@ -170,6 +189,7 @@
import CaseReportDrawer from './caseReportDrawer.vue'; import CaseReportDrawer from './caseReportDrawer.vue';
import ReportDetailDrawer from './reportDetailDrawer.vue'; import ReportDetailDrawer from './reportDetailDrawer.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue'; import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import TableFilter from '@/views/case-management/caseManagementFeature/components/tableFilter.vue';
import { import {
getShareTime, getShareTime,
@ -230,6 +250,7 @@
title: 'report.type', title: 'report.type',
slotName: 'integrated', slotName: 'integrated',
dataIndex: 'integrated', dataIndex: 'integrated',
titleSlotName: 'integratedFilter',
width: 150, width: 150,
showDrag: true, showDrag: true,
}, },
@ -321,11 +342,48 @@
const allListFilters = ref<string[]>([]); const allListFilters = ref<string[]>([]);
const independentListFilters = ref<string[]>([]); const independentListFilters = ref<string[]>([]);
const integratedListFilters = ref<string[]>([]); const integratedListFilters = ref<string[]>([]);
const statusListFiltersMap = ref<Record<string, string[]>>({ const statusListFiltersMap = ref<Record<string, string[]>>({
all: allListFilters.value, All: allListFilters.value,
INDEPENDENT: independentListFilters.value, INDEPENDENT: independentListFilters.value,
INTEGRATED: integratedListFilters.value, INTEGRATED: integratedListFilters.value,
}); });
//
const allIntegratedFilters = ref<string[]>([]);
const independentIntegratedFilters = ref<string[]>([]);
const integratedIntegratedFilters = ref<string[]>([]);
const reportTypeVisible = ref<boolean>(false);
const integratedFiltersMap = ref<Record<string, string[]>>({
All: allIntegratedFilters.value,
INDEPENDENT: independentIntegratedFilters.value,
INTEGRATED: integratedIntegratedFilters.value,
});
const reportTypeList = ref([
{
value: 'INDEPENDENT',
label: t('report.independent'),
},
{
value: 'INTEGRATED',
label: t('report.collection'),
},
]);
const integratedFilters = computed(() => {
if (showType.value === 'All') {
if (integratedFiltersMap.value[showType.value].length === 1) {
return integratedFiltersMap.value[showType.value].includes('INDEPENDENT') ? [false] : [true];
}
return undefined;
}
if (showType.value === 'INTEGRATED') {
return [true];
}
return [false];
});
function initData() { function initData() {
setLoadListParams({ setLoadListParams({
@ -334,7 +392,7 @@
moduleType: props.moduleType, moduleType: props.moduleType,
filter: { filter: {
status: statusListFiltersMap.value[showType.value], status: statusListFiltersMap.value[showType.value],
integrated: showType.value === 'All' ? undefined : Array.of((showType.value === 'INTEGRATED').toString()), integrated: integratedFilters.value,
triggerMode: triggerModeListFilters.value, triggerMode: triggerModeListFilters.value,
}, },
}); });
@ -366,7 +424,7 @@
condition: { condition: {
filter: { filter: {
status: statusListFiltersMap.value[showType.value], status: statusListFiltersMap.value[showType.value],
integrated: showType.value === 'All' ? undefined : Array.of((showType.value === 'INTEGRATED').toString()), integrated: integratedFilters.value,
triggerMode: triggerModeListFilters.value, triggerMode: triggerModeListFilters.value,
}, },
keyword: keyword.value, keyword: keyword.value,
@ -433,7 +491,7 @@
}); });
const statusFilters = computed(() => { const statusFilters = computed(() => {
return Object.keys(ReportStatus[props.moduleType]); return Object.keys(ReportStatus[props.moduleType]) || [];
}); });
function handleFilterHidden(val: boolean) { function handleFilterHidden(val: boolean) {
@ -470,13 +528,13 @@
const showCaseDetailDrawer = ref<boolean>(false); const showCaseDetailDrawer = ref<boolean>(false);
function showReportDetail(id: string, rowIndex: number) { function showReportDetail(id: string, rowIndex: number) {
activeDetailId.value = id;
activeReportIndex.value = rowIndex - 1;
if (props.moduleType === ReportEnum.API_SCENARIO_REPORT) { if (props.moduleType === ReportEnum.API_SCENARIO_REPORT) {
showDetailDrawer.value = true; showDetailDrawer.value = true;
} else { } else {
showCaseDetailDrawer.value = true; showCaseDetailDrawer.value = true;
} }
activeDetailId.value = id;
activeReportIndex.value = rowIndex - 1;
} }
const shareTime = ref<string>(''); const shareTime = ref<string>('');

View File

@ -94,7 +94,7 @@
<div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div> <div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div>
<div class="flex min-h-[110px] items-center"> <div class="flex min-h-[110px] items-center">
<div class="relative mr-4"> <div class="relative mr-4">
<div class="absolute bottom-0 left-[30%] top-[35%] text-center"> <div class="charts absolute text-center">
<div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div> <div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div>
<div class="text-[18px] font-medium">{{ getIndicators(detail.requestTotal) }}</div> <div class="text-[18px] font-medium">{{ getIndicators(detail.requestTotal) }}</div>
</div> </div>
@ -388,4 +388,11 @@
.block-title { .block-title {
@apply mb-4 font-medium; @apply mb-4 font-medium;
} }
.charts {
top: 30%;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
</style> </style>

View File

@ -1,12 +1,6 @@
<template> <template>
<div class="flex h-full flex-col gap-[16px]"> <div class="flex h-full flex-col gap-[16px]">
<a-spin class="w-full" :loading="loading"> <a-spin class="w-full" :loading="loading">
<!-- 不做虚拟滚动 :virtual-list-props="{
height: `calc(100vh - 454px)`,
threshold: 20,
fixedSize: true,
buffer: 15,
}" -->
<MsTree <MsTree
ref="treeRef" ref="treeRef"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
@ -16,6 +10,14 @@
:field-names="{ title: 'name', key: 'stepId', children: 'children' }" :field-names="{ title: 'name', key: 'stepId', children: 'children' }"
title-class="step-tree-node-title" title-class="step-tree-node-title"
node-highlight-class="step-tree-node-focus" node-highlight-class="step-tree-node-focus"
:virtual-list-props="{
height: 'calc(100vh - 200px)',
threshold: 200,
fixedSize: true,
buffer: 15, // 10 padding
isStaticItemHeight: true,
estimatedSize: 48,
}"
action-on-node-click="expand" action-on-node-click="expand"
disabled-title-tooltip disabled-title-tooltip
block-node block-node
@ -24,142 +26,138 @@
@more-actions-close="() => setFocusNodeKey('')" @more-actions-close="() => setFocusNodeKey('')"
> >
<template #title="step"> <template #title="step">
<div class="flex w-full items-center gap-[8px]"> <div class="flex flex-col">
<div <div class="flex w-full items-center gap-[8px]">
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white" <div
> class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
{{ step.sort }} >
</div> {{ step.sort }}
<div class="step-node-content flex justify-between"> </div>
<div class="flex flex-1 items-center"> <div class="step-node-content flex justify-between">
<!-- 步骤展开折叠按钮 --> <div class="flex flex-1 items-center">
<a-tooltip <!-- 步骤展开折叠按钮 -->
v-if="step.children?.length > 0" <a-tooltip
:content=" v-if="step.children?.length > 0"
t(step.expanded ? 'apiScenario.collapseStepTip' : 'apiScenario.expandStepTip', { :content="
count: step.children.length, t(step.expanded ? 'apiScenario.collapseStepTip' : 'apiScenario.expandStepTip', {
}) count: step.children.length,
" })
> "
<div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-4)]"> >
<MsIcon <div class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-4)]">
:type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'" <MsIcon
:size="14" :type="step.expanded ? 'icon-icon_split_turn-down_arrow' : 'icon-icon_split-turn-down-left'"
/> :size="14"
<span class="mx-1"> {{ step.children?.length || 0 }}</span> />
</div> <span class="mx-1"> {{ step.children?.length || 0 }}</span>
</a-tooltip>
<!-- 展开折叠控制器 -->
<div v-if="getShowExpand(step)" class="mx-1" @click.stop="expandHandler(step)">
<span v-if="step.fold" class="collapsebtn flex items-center justify-center">
<icon-right class="text-[var(--color-text-4)]" :style="{ 'font-size': '12px' }" />
</span>
<span v-else class="expand flex items-center justify-center">
<icon-down class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }" />
</span>
</div>
<div v-if="props.showType === 'API' && showCondition.includes(step.stepType)" class="flex-shrink-0">
<ConditionStatus class="mx-1" :status="step.stepType || ''" />
</div>
<a-tooltip :content="step.name" position="tl">
<div class="step-name-container w-full flex-grow" @click.stop="showDetail(step)">
<div class="one-line-text mx-[4px] max-w-[150px] text-[var(--color-text-1)]">
{{ step.name }}
</div> </div>
</a-tooltip>
<!-- 展开折叠控制器 -->
<div v-show="getShowExpand(step)" class="mx-1" @click.stop="expandHandler(step)">
<span v-if="step.fold" class="collapsebtn flex items-center justify-center">
<icon-right class="text-[var(--color-text-4)]" :style="{ 'font-size': '12px' }" />
</span>
<span v-else class="expand flex items-center justify-center">
<icon-down class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }" />
</span>
</div>
<div v-if="props.showType === 'API' && showCondition.includes(step.stepType)" class="flex-shrink-0">
<ConditionStatus class="mx-1" :status="step.stepType || ''" />
</div> </div>
</a-tooltip>
</div>
<div class="flex">
<stepStatus :status="step.status || 'PENDING'" />
<!-- 脚本报错 -->
<a-popover position="left" content-class="response-popover-content">
<MsTag
v-if="step.scriptIdentifier"
type="primary"
theme="light"
:self-style="{
color: 'rgb(var(--primary-3))',
background: 'rgb(var(--primary-1))',
}"
>
<template #icon>
<MsIcon type="icon-icon_info_outlined" class="mx-1 !text-[rgb(var(--primary-3))]" size="16" />
<span class="!text-[rgb(var(--primary-3))]">{{ t('report.detail.api.scriptErrorTip') }}</span>
</template>
</MsTag>
<template #content>
<div class="max-w-[400px]">{{ step.scriptIdentifier }}</div>
</template>
</a-popover>
<div v-show="showStatus(step)" class="flex">
<span class="statusCode mx-2">
<div v-if="step.code" class="mr-2"> {{ t('report.detail.api.statusCode') }}</div>
<a-popover position="left" content-class="response-popover-content">
<div
v-if="step.code"
class="one-line-text max-w-[200px]"
:style="{ color: statusCodeColor(step.code) }"
>
{{ step.code || '-' }}
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
<div :style="{ color: statusCodeColor(step.code) }">
{{ step.code || '-' }}
</div>
</div>
</template>
</a-popover>
</span>
<span v-if="step.requestTime !== null" class="resTime"> <a-tooltip :content="step.name" position="tl">
{{ t('report.detail.api.responseTime') }} <div class="step-name-container w-full flex-grow" @click.stop="showDetail(step)">
<a-popover position="left" content-class="response-popover-content"> <div class="one-line-text mx-[4px] max-w-[150px] text-[var(--color-text-1)]">
<span class="resTimeCount ml-2" {{ step.name }}
>{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[0] : '-' </div>
}}{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[1] : 'ms' }}</span </div>
> </a-tooltip>
<template #content> </div>
<span v-if="step.requestTime !== null" class="resTime"> <div class="flex">
{{ t('report.detail.api.responseTime') }} <stepStatus :status="step.status || 'PENDING'" />
<span class="resTimeCount ml-2" <!-- 脚本报错 -->
>{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[0] : '-' <a-popover position="left" content-class="response-popover-content">
}}{{ <MsTag
step.requestTime !== null ? formatDuration(step.requestTime).split('-')[1] : 'ms' v-if="step.scriptIdentifier"
}}</span type="primary"
></span theme="light"
> :self-style="{
color: 'rgb(var(--primary-3))',
background: 'rgb(var(--primary-1))',
}"
>
<template #icon>
<MsIcon type="icon-icon_info_outlined" class="mx-1 !text-[rgb(var(--primary-3))]" size="16" />
<span class="!text-[rgb(var(--primary-3))]">{{ t('report.detail.api.scriptErrorTip') }}</span>
</template> </template>
</a-popover></span </MsTag>
> <template #content>
<span v-if="step.responseSize !== null" class="resSize"> <div class="max-w-[400px] break-words">{{ step.scriptIdentifier }}</div>
{{ t('report.detail.api.responseSize') }} </template>
<a-popover position="left" content-class="response-popover-content"> </a-popover>
<span class="resTimeCount ml-2">{{ step.responseSize || 0 }} bytes</span> <div v-show="showStatus(step)" class="flex">
<template #content> <span class="statusCode mx-2">
<span class="resSize"> <div v-show="step.code" class="mr-2"> {{ t('report.detail.api.statusCode') }}</div>
{{ t('report.detail.api.responseSize') }} <a-popover position="left" content-class="response-popover-content">
<span class="resTimeCount ml-2">{{ step.responseSize || 0 }} bytes</span></span <div
v-show="step.code"
class="one-line-text max-w-[200px]"
:style="{ color: statusCodeColor(step.code) }"
> >
</template> {{ step.code || '-' }}
</a-popover></span </div>
> <template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
<div :style="{ color: statusCodeColor(step.code) }">
{{ step.code || '-' }}
</div>
</div>
</template>
</a-popover>
</span>
<span v-show="step.requestTime !== null" class="resTime">
{{ t('report.detail.api.responseTime') }}
<a-popover position="left" content-class="response-popover-content">
<span class="resTimeCount ml-2"
>{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[0] : '-'
}}{{
step.requestTime !== null ? formatDuration(step.requestTime).split('-')[1] : 'ms'
}}</span
>
<template #content>
<span v-show="step.requestTime !== null" class="resTime">
{{ t('report.detail.api.responseTime') }}
<span class="resTimeCount ml-2"
>{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[0] : '-'
}}{{
step.requestTime !== null ? formatDuration(step.requestTime).split('-')[1] : 'ms'
}}</span
></span
>
</template>
</a-popover></span
>
<span v-show="step.responseSize !== null" class="resSize">
{{ t('report.detail.api.responseSize') }}
<a-popover position="left" content-class="response-popover-content">
<span class="resTimeCount ml-2">{{ step.responseSize || 0 }} bytes</span>
<template #content>
<span class="resSize">
{{ t('report.detail.api.responseSize') }}
<span class="resTimeCount ml-2">{{ step.responseSize || 0 }} bytes</span></span
>
</template>
</a-popover></span
>
</div>
</div> </div>
</div> </div>
<div v-if="!step.fold" class="line"></div>
</div> </div>
<div v-if="!step.fold" class="line"></div> <!-- 折叠展开内容 -->
</div> <div v-show="showResContent(step)" class="foldContent mt-4 pl-2">
<!-- 折叠展开内容 v-if="showResContent(step)" -->
<div v-if="showResContent(step)" class="foldContent mt-4 pl-2">
<a-scrollbar
:style="{
overflow: 'auto',
height: 'calc(100vh - 540px)',
width: '100%',
}"
>
<StepDetailContent <StepDetailContent
:mode="props.activeType" :mode="props.activeType"
:step-item="step" :step-item="step"
@ -171,7 +169,14 @@
:report-id="props?.reportId" :report-id="props?.reportId"
:steps="steps" :steps="steps"
/> />
</a-scrollbar> </div>
</div>
</template>
<template v-if="steps.length === 0" #empty>
<div
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-center text-[12px] leading-[16px] text-[var(--color-text-4)]"
>
{{ t('common.noData') }}
</div> </div>
</template> </template>
</MsTree> </MsTree>
@ -182,12 +187,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeExpandedData } from '@/components/business/ms-tree/types'; import { MsTreeExpandedData } from '@/components/business/ms-tree/types';
import stepStatus from './stepStatus.vue';
import StepDetailContent from '@/views/api-test/components/requestComposition/response/result/index.vue';
import ConditionStatus from '@/views/api-test/report/component/conditionStatus.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { findNodeByKey, formatDuration, mapTree } from '@/utils'; import { findNodeByKey, formatDuration, mapTree } from '@/utils';
@ -195,6 +195,14 @@
import type { ScenarioItemType } from '@/models/apiTest/report'; import type { ScenarioItemType } from '@/models/apiTest/report';
import { ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepType } from '@/enums/apiEnum';
const StepDetailContent = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/result/index.vue')
);
const stepStatus = defineAsyncComponent(() => import('./stepStatus.vue'));
const ConditionStatus = defineAsyncComponent(() => import('@/views/api-test/report/component/conditionStatus.vue'));
const MsTag = defineAsyncComponent(() => import('@/components/pure/ms-tag/ms-tag.vue'));
const MsTree = defineAsyncComponent(() => import('@/components/business/ms-tree/index.vue'));
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
stepKeyword?: string; stepKeyword?: string;
@ -430,7 +438,6 @@
background: var(--color-text-n8) !important; background: var(--color-text-n8) !important;
} }
.resContentWrapper { .resContentWrapper {
border-top: 1px solid red;
border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px;
@apply mb-4 bg-white p-4; @apply mb-4 bg-white p-4;
.resContent { .resContent {
@ -451,4 +458,8 @@
height: 1px; height: 1px;
background: var(--color-text-n8); background: var(--color-text-n8);
} }
.foldContent {
height: 100%;
height: calc(100vh);
}
</style> </style>

View File

@ -3,6 +3,7 @@
v-model:params="assertionConfig.assertions" v-model:params="assertionConfig.assertions"
:is-definition="false" :is-definition="false"
:assertion-config="assertionConfig" :assertion-config="assertionConfig"
:show-extraction="true"
@change="emit('change')" @change="emit('change')"
/> />
</template> </template>

View File

@ -246,6 +246,7 @@
is-definition is-definition
:disabled="!isEditableApi" :disabled="!isEditableApi"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
:show-extraction="true"
/> />
<auth <auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH" v-else-if="requestVModel.activeTab === RequestComposition.AUTH"

View File

@ -192,6 +192,7 @@
is-definition is-definition
:disabled="!isEditableApi" :disabled="!isEditableApi"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
:show-extraction="true"
/> />
<auth <auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH" v-else-if="requestVModel.activeTab === RequestComposition.AUTH"

View File

@ -199,7 +199,7 @@ export function initFormCreate(customFields: CustomAttributes[], permission: str
currentDefaultValue = item.type === 'MEMBER' ? item.defaultValue : JSON.parse(item.defaultValue); currentDefaultValue = item.type === 'MEMBER' ? item.defaultValue : JSON.parse(item.defaultValue);
} }
} else if (multipleInputType.includes(item.type)) { } else if (multipleInputType.includes(item.type)) {
currentDefaultValue = JSON.parse(item.defaultValue); currentDefaultValue = Array.isArray(item.defaultValue) ? item.defaultValue : JSON.parse(item.defaultValue);
} else if (singleType.includes(item.type)) { } else if (singleType.includes(item.type)) {
const optionsIds = optionsValue.map((e: any) => e.value); const optionsIds = optionsValue.map((e: any) => e.value);
currentDefaultValue = (optionsIds || []).find((e: any) => item.defaultValue === e) || ''; currentDefaultValue = (optionsIds || []).find((e: any) => item.defaultValue === e) || '';

View File

@ -200,6 +200,7 @@
showInTable: true, showInTable: true,
width: 200, width: 200,
showDrag: true, showDrag: true,
showTooltip: true,
}, },
{ {
title: 'project.commonScript.createTime', title: 'project.commonScript.createTime',

View File

@ -1,5 +1,5 @@
<template> <template>
<MsAssertion v-model:params="params" /> <MsAssertion v-model:params="params" :show-extraction="false" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -18,6 +18,7 @@
v-model:model-value="form.requestTimeout" v-model:model-value="form.requestTimeout"
:min="0" :min="0"
:step="100" :step="100"
:precision="0"
class="w-[180px]" class="w-[180px]"
:disabled="isDisabled" :disabled="isDisabled"
> >
@ -29,6 +30,7 @@
v-model:model-value="form.responseTimeout" v-model:model-value="form.responseTimeout"
:min="0" :min="0"
:step="100" :step="100"
:precision="0"
class="w-[180px]" class="w-[180px]"
:disabled="isDisabled" :disabled="isDisabled"
> >