feat(接口测试): 报告详情_报告详情联调100%

This commit is contained in:
xinxin.wu 2024-03-23 18:18:45 +08:00 committed by Craftsman
parent 1b95d82b6a
commit 6736cc5f17
41 changed files with 2373 additions and 1711 deletions

View File

@ -1,7 +1,7 @@
import MSR from '@/api/http';
import * as reportUrl from '@/api/requrls/api-test/report';
import type { ReportDetail, ReportStepDetail } from '@/models/apiTest/report';
import type { GetShareId, ReportDetail, ReportStepDetail } from '@/models/apiTest/report';
import type { TableQueryParams } from '@/models/common';
import { ReportEnum } from '@/enums/reportEnum';
@ -37,22 +37,68 @@ export function reportBathDelete(moduleType: string, data: TableQueryParams) {
return MSR.post({ url: reportUrl.ApiBatchDeleteUrl, data });
}
// Api报告详情
export function reportDetail(reportId: string) {
// 报告详情
export function reportScenarioDetail(reportId: string, shareId?: string | undefined) {
if (shareId) {
return MSR.get<ReportDetail>({ url: `${reportUrl.ScenarioReportShareDetailUrl}/${shareId}/${reportId}` });
}
return MSR.get<ReportDetail>({ url: `${reportUrl.ScenarioReportDetailUrl}/${reportId}` });
}
// 报告步骤详情
export function reportStepDetail(reportId: string, stepId: string) {
export function reportStepDetail(reportId?: string, stepId?: string, shareId?: string | undefined) {
if (shareId) {
return MSR.get<ReportStepDetail>({
url: `${reportUrl.ScenarioReportShareDetailStepUrl}/${shareId}/${reportId}/${stepId}`,
});
}
return MSR.get<ReportStepDetail>({ url: `${reportUrl.ScenarioReportDetailStepUrl}/${reportId}/${stepId}` });
}
// 用例报告详情
export function reportCaseDetail(reportId: string) {
export function reportCaseDetail(reportId: string, shareId?: string | undefined) {
if (shareId) {
return MSR.get<ReportDetail>({ url: `${reportUrl.CaseReportShareDetailUrl}/${shareId}/${reportId}` });
}
return MSR.get<ReportDetail>({ url: `${reportUrl.CaseReportDetailUrl}/${reportId}` });
}
// 报告步骤详情
export function reportCaseStepDetail(reportId: string, stepId: string) {
export function reportCaseStepDetail(reportId: string, stepId: string, shareId?: string | undefined) {
if (shareId) {
return MSR.get<ReportStepDetail[]>({
url: `${reportUrl.CaseStepDetailShareStepUrl}/${shareId}/${reportId}/${stepId}`,
});
}
return MSR.get<ReportStepDetail[]>({ url: `${reportUrl.CaseStepDetailStepUrl}/${reportId}/${stepId}` });
}
// 生成分享id
export function getShareInfo(data: GetShareId) {
return MSR.post({ url: `${reportUrl.getShareIdUrl}`, data });
}
// 获取场景分享详情
export function getScenarioReportShareDetail(data: { shareId: string; reportId: string }) {
return MSR.get<ReportStepDetail>({
url: `${reportUrl.ScenarioReportDetailUrl}/${data.shareId}/${data.reportId}`,
});
}
// 获取用例分享详情
export function getCaseReportShareDetail(data: { shareId: string; reportId: string }) {
return MSR.get<ReportStepDetail>({
url: `${reportUrl.CaseReportDetailUrl}/${data.shareId}/${data.reportId}`,
});
}
// 报告分享(场景)
export function reportScenarioShare(shareId: string, stepId: string) {
return MSR.get<ReportStepDetail[]>({ url: `${reportUrl.reportScenarioShareUrl}/${shareId}/${stepId}` });
}
// 报告分享(用例)
export function reportCaseShare(shareId: string, stepId: string) {
return MSR.get<ReportStepDetail[]>({ url: `${reportUrl.reportCaseShareUrl}/${shareId}/${stepId}` });
}
// 获取分享报告id
export function getShareReportInfo(shareId: string) {
return MSR.get<ReportDetail>({ url: `${reportUrl.getShareReportInfoUrl}/${shareId}` });
}
export default {};

View File

@ -18,9 +18,29 @@ export const ApiBatchDeleteUrl = '/api/report/case/batch/delete';
// 场景报告详情(API)
export const ScenarioReportDetailUrl = '/api/report/scenario/get';
// 场景报告详情分享(API)
export const ScenarioReportShareDetailUrl = '/api/report/scenario/share';
// 场景报告详情(用例)
export const CaseReportDetailUrl = '/api/report/case/get';
// 场景报告详情分享(用例)
export const CaseReportShareDetailUrl = '/api/report/case/share';
// 报告详情步骤(API)
export const ScenarioReportDetailStepUrl = '/api/report/scenario/get/detail';
// 报告详情步骤分享(API)
export const ScenarioReportShareDetailStepUrl = '/api/report/scenario/share/detail';
// 报告详情步骤(用例)
export const CaseStepDetailStepUrl = '/api/report/case/get/detail';
// 报告详情步骤分享(用例)
export const CaseStepDetailShareStepUrl = '/api/report/case/share/detail';
// 报告分享(API)
export const reportScenarioShareUrl = '/api/report/scenario/share';
// 报告分享(CASE)
export const reportCaseShareUrl = '/api/report/case/share';
// 获取分享id
export const getShareIdUrl = '/api/report/share/gen';
export const getShareReportInfoUrl = '/api/report/share/get';

View File

@ -6,6 +6,7 @@ export enum ApiTestRouteEnum {
API_TEST_SCENARIO = 'apiTestScenario',
API_TEST_SCENARIO_RECYCLE = 'apiTestScenarioRecycle',
API_TEST_REPORT = 'apiTestReport',
API_TEST_REPORT_SHARE = 'apiTestReportShare',
}
export enum BugManagementRouteEnum {
@ -99,6 +100,12 @@ export enum SettingRouteEnum {
SETTING_ORGANIZATION_TASK_CENTER = 'settingOrganizationTaskCenter',
}
export enum ShareEnum {
SHARE = 'share',
SHARE_REPORT_SCENARIO = 'shareReportScenario',
SHARE_REPORT_CASE = 'shareReportCase',
}
export const RouteEnum = {
...ApiTestRouteEnum,
...SettingRouteEnum,
@ -109,4 +116,5 @@ export const RouteEnum = {
...TestPlanRouteEnum,
...UITestRouteEnum,
...WorkbenchRouteEnum,
...ShareEnum,
};

View File

@ -0,0 +1,25 @@
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in" appear>
<!-- transition内必须有且只有一个根元素不然会导致二级路由的组件无法渲染 -->
<div v-show="true" class="page-content">
<!-- TODO 实验性组件以后优化 -->
<keep-alive>
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</div>
</transition>
</router-view>
</template>
<script lang="ts" setup>
import MsCard from '@/components/pure/ms-card/index.vue';
</script>
<style lang="less" scoped>
.page-content {
@apply h-full overflow-y-auto;
min-height: 500px;
}
</style>

View File

@ -406,6 +406,7 @@ export interface RequestResult {
url: string;
method: string;
responseResult: ResponseResult;
[key: string]: any;
}
export interface RequestTaskResult {
requestResults: RequestResult[]; // 请求结果

View File

@ -1,3 +1,5 @@
import { RequestResult } from '@/models/apiTest/common';
export interface LegendData {
label: string;
value: string;
@ -36,6 +38,8 @@ export interface ResponseResult {
assertions: AssertionItem[];
}
export type resultType = RequestResult & ResponseResult;
export interface StepContent {
resourceId: string;
projectId: string;
@ -54,7 +58,7 @@ export interface StepContent {
method: string;
assertionTotal: number;
passAssertionsTotal: number;
subRequestResults: any;
subRequestResults: ResponseResult[];
responseResult: ResponseResult;
isSuccessful: boolean;
fakeErrorCode: string;
@ -62,6 +66,7 @@ export interface StepContent {
[key: string]: any;
}
export type StepContentType = StepContent & RequestResult;
// 步骤详情
export interface ReportStepDetailItem {
id: string;
@ -74,7 +79,7 @@ export interface ReportStepDetailItem {
code: string;
responseSize: number;
scriptIdentifier: string;
content: StepContent;
content: StepContentType;
[key: string]: any;
}
@ -96,6 +101,7 @@ export interface ScenarioItemType {
responseSize: number; // 响应内容大小
scriptIdentifier: string; // 脚本标识
fold: boolean; // 是否展示折叠
expanded: boolean; // 是否展开折叠树节点
children: ScenarioItemType[];
level?: number;
stepDetail: ReportStepDetailItem;
@ -145,3 +151,9 @@ export interface ReportDetail {
}
export type ReportDetailPartial = Partial<ScenarioItemType>;
// 获取分享id
export interface GetShareId {
reportId: string;
projectId: string;
}

View File

@ -3,6 +3,31 @@ export const WHITE_LIST = [
{ name: 'notFound', path: '/notFound', children: [] },
{ name: 'invite', path: '/invite', children: [] },
{ name: 'index', path: '/index', children: [] },
{
name: 'share',
path: '/share',
children: [
{
name: 'shareReportScenario',
path: '/shareReportScenario',
},
{
name: 'shareReportCase',
path: '/shareReportCase',
children: [],
},
],
},
{
name: 'shareReportScenario',
path: '/shareReportScenario',
children: [],
},
{
name: 'shareReportCase',
path: '/shareReportCase',
children: [],
},
];
// 左侧菜单底部对齐的菜单数组数组项为一级路由的name

View File

@ -5,6 +5,7 @@ import type { RouteRecordRaw } from 'vue-router';
export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue');
export const PAGE_LAYOUT = () => import('@/layout/page-layout.vue');
export const NO_PERMISSION_LAYOUT = () => import('@/layout/no-permission-layout.vue');
export const SHARE_LAYOUT = () => import('@/layout/share-layout.vue');
export const INDEX_ROUTE: RouteRecordRaw = {
path: '/index',

View File

@ -0,0 +1,42 @@
import { ShareEnum } from '@/enums/routeEnum';
import { SHARE_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const ShareRoute: AppRouteRecordRaw = {
path: '/share',
name: ShareEnum.SHARE,
component: SHARE_LAYOUT,
meta: {
locale: 'menu.testPlan',
icon: 'icon-icon_test-tracking_filled',
hideChildrenInMenu: true,
roles: ['*'],
hideInMenu: true,
},
children: [
// 测试计划
{
path: 'shareReportScenario',
name: ShareEnum.SHARE_REPORT_SCENARIO,
component: () => import('@/views/api-test/report/shareSceneIndex.vue'),
meta: {
locale: '',
roles: ['*'],
isTopMenu: false,
},
},
{
path: 'shareReportCase',
name: ShareEnum.SHARE_REPORT_CASE,
component: () => import('@/views/api-test/report/shareCaseIndex.vue'),
meta: {
locale: '',
roles: ['*'],
isTopMenu: false,
},
},
],
};
export default ShareRoute;

View File

@ -16,7 +16,7 @@
v-else
type="icon"
status="secondary"
class="!mr-0 !rounded-full"
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
size="small"
@click="emit('changeExpand', true)"
>

View File

@ -4,58 +4,18 @@
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
</a-tabs>
<div class="response-container">
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
show-language-change
show-charset-change
read-only
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyScript">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
<MsCodeEditor
v-else-if="activeTab === ResponseComposition.CONSOLE"
:model-value="props.console?.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
</MsCodeEditor>
<div
<ResBody v-if="activeTab === ResponseComposition.BODY" :request-result="props.requestResult" @copy="copyScript" />
<ResConsole v-else-if="activeTab === ResponseComposition.CONSOLE" :console="props.console?.trim()" />
<ResValueScript
v-else-if="
activeTab === ResponseComposition.HEADER ||
activeTab === ResponseComposition.REAL_REQUEST ||
activeTab === ResponseComposition.EXTRACT
"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<pre class="response-header-pre">{{ getResponsePreContent(activeTab) }}</pre>
</div>
<MsBaseTable v-else-if="activeTab === 'ASSERTION'" v-bind="propsRes" v-on="propsEvent">
<template #status="{ record }">
<MsTag :type="record.pass === true ? 'success' : 'danger'" theme="light">
{{ record.pass === true ? t('common.success') : t('common.fail') }}
</MsTag>
</template>
</MsBaseTable>
:active-tab="activeTab"
:request-result="props.requestResult"
/>
<ResAssertion v-else-if="activeTab === 'ASSERTION'" :request-result="props.requestResult" />
</div>
</div>
<a-empty
@ -85,13 +45,10 @@
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import ResAssertion from './result/assertionTable.vue';
import ResBody from './result/body.vue';
import ResConsole from './result/console.vue';
import ResValueScript from './result/resValueScript.vue';
import { useI18n } from '@/hooks/useI18n';
@ -147,23 +104,6 @@
default: ResponseComposition.BODY,
});
//
const responseLanguage = computed(() => {
if (props.requestResult) {
const { contentType } = props.requestResult.responseResult;
if (contentType.includes('json')) {
return LanguageEnum.JSON;
}
if (contentType.includes('html')) {
return LanguageEnum.HTML;
}
if (contentType.includes('xml')) {
return LanguageEnum.XML;
}
}
return LanguageEnum.PLAINTEXT;
});
const { copy, isSupported } = useClipboard();
async function copyScript() {
@ -174,58 +114,6 @@
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.requestResult?.responseResult?.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.requestResult?.body
? `${t('apiTestDebug.requestUrl')}:\n${props.requestResult.url}\n${t('apiTestDebug.header')}:\n${
props.requestResult.headers
}\nBody:\n${props.requestResult.body.trim()}`
: '';
case ResponseComposition.EXTRACT:
return props.requestResult?.responseResult?.vars?.trim();
default:
return '';
}
}
const columns: MsTableColumn = [
{
title: 'apiTestDebug.content',
dataIndex: 'content',
showTooltip: true,
},
{
title: 'apiTestDebug.status',
dataIndex: 'pass',
slotName: 'status',
width: 80,
},
{
title: 'apiTestDebug.reason',
dataIndex: 'message',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(undefined, {
scroll: { x: '100%' },
columns,
});
watch(
() => props.requestResult?.responseResult.assertions,
(val) => {
if (val) {
propsRes.value.data = props.requestResult?.responseResult.assertions || [];
}
},
{
immediate: true,
}
);
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,64 @@
<template>
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #status="{ record }">
<MsTag :type="record.pass === true ? 'success' : 'danger'" theme="light">
{{ record.pass === true ? t('common.success') : t('common.fail') }}
</MsTag>
</template>
</MsBaseTable>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestResult } from '@/models/apiTest/common';
const { t } = useI18n();
const props = defineProps<{
requestResult?: RequestResult;
}>();
const columns: MsTableColumn = [
{
title: 'apiTestDebug.content',
dataIndex: 'content',
showTooltip: true,
},
{
title: 'apiTestDebug.status',
dataIndex: 'pass',
slotName: 'status',
width: 80,
},
{
title: 'apiTestDebug.reason',
dataIndex: 'message',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(undefined, {
scroll: { x: '100%' },
columns,
});
watch(
() => props.requestResult?.responseResult.assertions,
(val) => {
if (val) {
propsRes.value.data = props.requestResult?.responseResult.assertions || [];
}
},
{
immediate: true,
}
);
</script>
<style scoped></style>

View File

@ -0,0 +1,63 @@
<template>
<MsCodeEditor
ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="[LanguageEnum.JSON, LanguageEnum.HTML, LanguageEnum.XML, LanguageEnum.PLAINTEXT]"
:show-full-screen="false"
:show-theme-change="false"
show-language-change
show-charset-change
read-only
>
<template #rightTitle>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="emits('copy')">
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { RequestResult } from '@/models/apiTest/common';
const props = defineProps<{
requestResult?: RequestResult;
requestUrl?: string;
isHttpProtocol?: boolean;
isDefinition?: boolean;
}>();
const emits = defineEmits<{
(e: 'copy'): void;
}>();
//
const responseLanguage = computed(() => {
if (props.requestResult) {
const { contentType } = props.requestResult.responseResult;
if (contentType.includes('json')) {
return LanguageEnum.JSON;
}
if (contentType.includes('html')) {
return LanguageEnum.HTML;
}
if (contentType.includes('xml')) {
return LanguageEnum.XML;
}
}
return LanguageEnum.PLAINTEXT;
});
</script>
<style scoped></style>

View File

@ -0,0 +1,27 @@
<template>
<MsCodeEditor
:model-value="props.console?.trim()"
:language="LanguageEnum.PLAINTEXT"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
read-only
>
</MsCodeEditor>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
const props = defineProps<{
console?: string;
}>();
</script>
<style scoped></style>

View File

@ -0,0 +1,298 @@
<template>
<div class="mt-4 flex w-full items-center justify-between rounded bg-[var(--color-text-n9)] p-4">
<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 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>
</template>
</a-popover>
<a-popover position="left" content-class="w-[400px]">
<div class="one-line-text text-[rgb(var(--success-7))]"> {{ timingInfo?.responseTime || 0 }} ms </div>
<template #content>
<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-[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>
</template>
</a-popover>
<span v-if="props.showType && props.showType !== 'CASE'">{{ props.environmentName }}</span>
</div>
</div>
<div v-if="activeType === 'SubRequest'" class="my-4 flex justify-start">
<MsPagination
v-model:page-size="pageSize"
v-model:current="current"
:total="total"
size="mini"
@change="loadLoop"
/>
</div>
<!-- 平铺 -->
<TiledDisplay
v-if="props.mode === 'tiled'"
: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-[300px] w-full' : 'h-[calc(100%-35px)] w-full px-[18px] pb-[18px]']"
>
<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>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
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 responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
import { reportCaseStepDetail, reportStepDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import type { ReportStepDetail, ReportStepDetailItem, ScenarioItemType } from '@/models/apiTest/report';
import { ResponseComposition } from '@/enums/apiEnum';
const props = defineProps<{
mode: 'tiled' | 'tab'; // | tab
stepItem?: ScenarioItemType; //
console?: string;
isPriorityLocalExec?: boolean;
requestUrl?: string;
isHttpProtocol?: boolean;
isDefinition?: boolean;
showType: 'API' | 'CASE';
environmentName?: string; //
isResponseModel?: boolean;
reportId?: string;
}>();
const { t } = useI18n();
const responseCompositionTabList = [
{
label: t('apiTestDebug.responseBody'),
value: ResponseComposition.BODY,
},
{
label: t('apiTestDebug.responseHeader'),
value: ResponseComposition.HEADER,
},
{
label: t('apiTestDebug.realRequest'),
value: ResponseComposition.REAL_REQUEST,
},
{
label: t('apiTestDebug.console'),
value: ResponseComposition.CONSOLE,
},
...(props.isDefinition
? [
{
label: t('apiTestDebug.extract'),
value: ResponseComposition.EXTRACT,
},
{
label: t('apiTestDebug.assertion'),
value: ResponseComposition.ASSERTION,
},
]
: []),
];
const activeTab = ref(ResponseComposition.BODY);
const activeStepDetail = ref<ReportStepDetailItem>();
const activeStepDetailCopy = ref<ReportStepDetail>();
const route = useRoute();
const reportDetailMap = {
API: {
stepDetail: reportStepDetail,
},
CASE: {
stepDetail: reportCaseStepDetail,
},
};
const activeIndex = ref<number>(0);
const total = computed(() => (activeStepDetail.value?.content.subRequestResults || []).length);
const current = ref(1);
const pageSize = ref(1);
const activeType = ref<'ResContent' | 'SubRequest'>('ResContent');
const subRequestResults = ref<any[]>([]);
/**
* 加载子请求列表
*/
async function loadLoop() {
if (activeStepDetail.value?.content) {
const { content } = activeStepDetail.value;
activeStepDetailCopy.value = {
...activeStepDetail.value,
content: {
...content,
responseResult: {
...subRequestResults.value[current.value - 1],
},
},
};
}
}
const stepDetailInfo = ref<ReportStepDetailItem[]>([]);
/**
* 响应时间信息
*/
const timingInfo = computed(() => {
if (activeStepDetail.value && activeStepDetail.value.content.responseResult) {
const {
dnsLookupTime,
downloadTime,
latency,
responseTime,
socketInitTime,
sslHandshakeTime,
tcpHandshakeTime,
transferStartTime,
} = activeStepDetail.value.content.responseResult;
return {
dnsLookupTime,
tcpHandshakeTime,
sslHandshakeTime,
socketInitTime,
latency,
downloadTime,
transferStartTime,
responseTime,
};
}
return null;
});
/**
* 响应状态码对应颜色
*/
const statusCodeColor = computed(() => {
if (activeStepDetail.value?.content?.requestResult) {
const code = Number(activeStepDetail.value?.content?.responseResult.responseCode);
if (code >= 200 && code < 300) {
return 'rgb(var(--success-7)';
}
if (code >= 300 && code < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
}
return '';
});
const loading = ref<boolean>(false);
//
async function getStepDetail() {
try {
loading.value = true;
if (props.stepItem) {
const res = await reportDetailMap[props.showType].stepDetail(
(props.stepItem?.reportId || props.reportId) as string,
props?.stepItem?.stepId,
route.query.shareId as string | undefined
);
stepDetailInfo.value = cloneDeep(res) as any;
// TODO --
activeStepDetail.value = stepDetailInfo.value[activeIndex.value];
subRequestResults.value = stepDetailInfo.value[activeIndex.value].content.subRequestResults;
if (activeType.value === 'ResContent') {
activeStepDetailCopy.value = cloneDeep(activeStepDetail.value);
} else {
loadLoop();
}
}
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
/**
* 设置激活类型是响应内容还是子请求
*/
function setActiveType(type: 'ResContent' | 'SubRequest') {
activeType.value = type;
if (type === 'SubRequest') {
loadLoop();
} else {
activeStepDetailCopy.value = { ...activeStepDetail.value };
}
}
const originStepId = ref<string | undefined>('');
watchEffect(() => {
if (props?.stepItem?.stepId && props.stepItem.stepId !== originStepId.value) {
getStepDetail();
}
});
onMounted(() => {
originStepId.value = props.stepItem?.stepId;
});
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,43 @@
<template>
<div class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<pre class="response-header-pre">{{ getResponsePreContent(props.activeTab) }}</pre>
</div>
</template>
<script setup lang="ts">
/**
* @description (共用) 请求头 || 提取 || 真实请求
*/
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum';
const { t } = useI18n();
const props = defineProps<{
requestResult?: RequestResult;
activeTab: keyof typeof ResponseComposition;
}>();
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.requestResult?.responseResult?.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.requestResult?.body
? `${t('apiTestDebug.requestUrl')}:\n${props.requestResult.url}\n${t('apiTestDebug.header')}:\n${
props.requestResult.headers
}\nBody:\n${props.requestResult.body.trim()}`
: '';
case ResponseComposition.EXTRACT:
return props.requestResult?.responseResult?.vars?.trim();
default:
return '';
}
}
</script>
<style scoped></style>

View File

@ -0,0 +1,146 @@
<template>
<div class="resContent">
<!-- 展开折叠列表 -->
<div class="tiledList">
<div v-for="item of props.menuList" :key="item.value" class="menu-list-wrapper">
<div class="menu-list">
<div class="flex items-center">
<MsButton
v-if="expandIds.includes(item.value)"
type="icon"
class="!mr-2 !rounded-full bg-[rgb(var(--primary-1))]"
size="small"
@click="changeExpand(item.value)"
>
<icon-down :size="8" />
</MsButton>
<MsButton
v-else
type="icon"
status="secondary"
class="!mr-2 !rounded-full !bg-[var(--color-text-n8)]"
size="small"
@click="changeExpand(item.value)"
>
<icon-right :size="8" />
</MsButton>
<span class="menu-title">{{ item.label }}</span>
</div>
</div>
<transition name="fade">
<div v-show="expandIds.includes(item.value)" class="expandContent">
<div v-if="item.value === ResponseComposition.BODY" class="res-item">
<ResBody :request-result="props.requestResult" @copy="copyScript" />
</div>
<div v-if="expandIds.includes(item.value) && item.value === ResponseComposition.CONSOLE" class="res-item">
<ResConsole :console="props.console?.trim()" />
</div>
<div v-if="expandIds.includes(item.value) && item.value === ResponseComposition.HEADER" class="res-item">
<ResValueScript :active-tab="item.value" :request-result="props.requestResult" />
</div>
<div v-if="expandIds.includes(item.value) && item.value === ResponseComposition.REAL_REQUEST">
<ResValueScript :active-tab="item.value" :request-result="props.requestResult" />
</div>
<div v-if="expandIds.includes(item.value) && item.value === ResponseComposition.EXTRACT">
<ResValueScript :active-tab="item.value" :request-result="props.requestResult" />
</div>
<div v-if="expandIds.includes(item.value) && item.value === ResponseComposition.ASSERTION">
<ResAssertion :request-result="props.requestResult" />
</div>
</div>
</transition>
<a-divider type="dashed" :margin="0" class="!mb-4"></a-divider>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import ResAssertion from './assertionTable.vue';
import ResBody from './body.vue';
import ResConsole from './console.vue';
import ResValueScript from './resValueScript.vue';
import { useI18n } from '@/hooks/useI18n';
import type { RequestResult } from '@/models/apiTest/common';
import { ResponseComposition } from '@/enums/apiEnum';
const { t } = useI18n();
const props = defineProps<{
isExpanded?: boolean;
requestResult?: RequestResult;
console?: string;
environmentName?: string;
menuList: { label: string; value: keyof typeof ResponseComposition }[];
reportId?: string;
}>();
const expandIds = ref<string[]>([]);
function changeExpand(value: string) {
const isExpand = expandIds.value.indexOf(value) > -1;
if (isExpand) {
expandIds.value = expandIds.value.filter((item) => item !== value);
} else {
expandIds.value.push(value);
}
}
const { copy, isSupported } = useClipboard();
async function copyScript() {
if (isSupported) {
await copy(props.requestResult?.responseResult.body || '');
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
</script>
<style lang="less">
.response-popover-content {
padding: 4px 8px;
.arco-popover-content {
@apply mt-0;
font-size: 14px;
line-height: 22px;
}
}
.resContentWrapper {
position: relative;
border: 1px solid var(--color-text-n8);
border-top: none;
border-radius: 0 0 6px 6px;
@apply mb-4 bg-white p-4;
.resContent {
height: 38px;
border-radius: 6px;
}
}
.tiledList {
@apply px-4;
.menu-list-wrapper {
@apply mt-4;
.menu-list {
height: 32px;
// border-bottom: 1px dashed var(--color-text-n8);
@apply flex items-start justify-between px-4;
.menu-title {
@apply font-medium;
}
}
.expandContent {
background: var(--color-text-n9);
.res-item {
height: 210px;
}
}
}
}
</style>

View File

@ -0,0 +1,388 @@
<template>
<div class="report-container h-full">
<!-- 报告参数开始 -->
<div class="report-header flex items-center justify-between">
<span>
{{ detail.poolName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.requestDuration || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.creatUserName || '-' }}
</span>
<span>
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
{{ dayjs(detail.startTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
{{ dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
</span>
</div>
<!-- 报告参数结束 -->
<!-- 报告分析开始 -->
<div class="analyze mb-1">
<!-- 请求分析 -->
<div class="request-analyze min-h-[110px]">
<div class="block-title mb-4">{{ t('report.detail.api.requestAnalysis') }}</div>
<!-- 独立报告 -->
<SetReportChart :legend-data="legendData" :options="charOptions" />
<!-- 集合报告 -->
<!-- </div> -->
</div>
<!-- 耗时分析 -->
<div class="time-analyze">
<div class="time-card mb-2 mt-[16px] h-[40px] flex-1 gap-4">
<div class="time-card-item flex h-full">
<MsIcon type="icon-icon_time_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
<span class="time-card-item-title">{{ t('report.detail.api.totalTime') }}</span>
<span class="count">{{ getTotalTime }}</span
><span class="time-card-item-title">s</span>
</div>
<div class="time-card-item h-full">
<MsIcon type="icon-icon_time_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
<span class="time-card-item-title"> {{ t('report.detail.api.requestTotalTime') }}</span>
<span class="count">{{ detail.requestDuration || '-' }}</span
><span class="time-card-item-title">s</span>
</div>
</div>
<div class="time-card flex-1 gap-4">
<!-- 执行率 -->
<div v-if="detail.integrated" class="time-card-item-rote">
<div class="time-card-item-rote-title">
<MsIcon type="icon-icon_yes_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.executionRate') }}
</div>
<div class="flex items-center">
<span class="count"> {{ getExcuteRate() }} %</span>
<a-divider direction="vertical" class="!h-[16px]" :margin="8"></a-divider>
<span>{{ getRequestEacuteCount }}</span>
<span class="mx-1 text-[var(--color-text-4)]">/ {{ getRequestTotalCount || 0 }}</span>
</div>
</div>
<div class="time-card-item-rote">
<div class="time-card-item-rote-title">
<MsIcon type="icon-icon_yes_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.assertPass') }}
</div>
<div class="flex items-center">
<span class="count"
>{{ detail.assertionPassRate === 'Calculating' ? '-' : detail.assertionPassRate || '0.00' }}%</span
>
<a-divider direction="vertical" class="!h-[16px]" :margin="8"></a-divider>
<span>{{ addCommasToNumber(detail.assertionSuccessCount || 0) }}</span>
<span class="mx-1 text-[var(--color-text-4)]">/ {{ detail.assertionCount || 0 }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 报告步骤分析结束 -->
<!-- 报告明细开始 -->
<div class="report-info">
<div class="mb-4 flex h-[36px] items-center justify-between">
<div class="flex items-center">
<div class="mr-2 font-medium leading-[36px]">{{ t('report.detail.api.reportDetail') }}</div>
<a-radio-group v-model:model-value="activeTab" type="button" size="small">
<a-radio v-for="item of methods" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-radio>
</a-radio-group>
</div>
<a-select v-model="condition" class="w-[240px]" :placeholder="t('report.detail.api.filterPlaceholder')">
<a-option :key="1" :value="1"> 1 </a-option>
</a-select>
</div>
<TiledList show-type="CASE" :active-type="activeTab" :report-detail="detail || []" />
</div>
<!-- 报告明细结束 -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs';
import SetReportChart from './case/setReportChart.vue';
import TiledList from './tiledList.vue';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type { LegendData, ReportDetail } from '@/models/apiTest/report';
const { t } = useI18n();
const props = defineProps<{
detailInfo?: ReportDetail;
}>();
const detail = ref<ReportDetail>({
id: '',
name: '', //
testPlanId: '',
createUser: '',
deleteTime: 0,
deleteUser: '',
deleted: false,
updateUser: '',
updateTime: 0,
startTime: 0, // /
endTime: 0, // /
requestDuration: 0, //
status: '', // /SUCCESS/ERROR
triggerMode: '', //
runMode: '', //
poolId: '', //
poolName: '', //
versionId: '',
integrated: false, //
projectId: '',
environmentId: '', // id
environmentName: '', //
errorCount: 0, //
fakeErrorCount: 0, //
pendingCount: 0, //
successCount: 0, //
assertionCount: 0, //
assertionSuccessCount: 0, //
requestErrorRate: '', //
requestPendingRate: '', //
requestFakeErrorRate: '', //
requestPassRate: '', //
assertionPassRate: '', //
scriptIdentifier: '', //
children: [], //
stepTotal: 0, //
console: '',
});
const getTotalTime = computed(() => {
if (detail.value) {
const { endTime, startTime } = detail.value;
if (endTime && startTime && endTime !== 0 && startTime !== 0) {
return endTime - startTime;
}
return '-';
}
return '-';
});
const methods = ref([
{
label: t('report.detail.api.tiledDisplay'),
value: 'tiled',
},
{
label: t('report.detail.api.tabDisplay'),
value: 'tab',
},
]);
const legendData = ref<LegendData[]>([]);
const charOptions = ref({
tooltip: {
trigger: 'item',
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['65%', '80%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{
value: 0,
name: t('report.detail.api.pass'),
itemStyle: {
color: '#00C261',
},
},
{
value: 0,
name: t('report.detail.api.misstatement'),
itemStyle: {
color: '#FFC14E',
},
},
{
value: 0,
name: t('report.detail.api.error'),
itemStyle: {
color: '#ED0303',
},
},
{
value: 0,
name: t('report.detail.api.pending'),
itemStyle: {
color: '#D4D4D8',
},
},
],
},
});
const activeTab = ref<'tiled' | 'tab'>('tiled');
const condition = ref('');
function getExcuteRate() {
return 100 - Number(detail.value.requestPendingRate)
? (100 - Number(detail.value.requestPendingRate)).toFixed(2)
: '0.00';
}
//
const getRequestEacuteCount = computed(() => {
const { errorCount, successCount, fakeErrorCount } = detail.value;
return addCommasToNumber(errorCount + successCount + fakeErrorCount);
});
const getRequestTotalCount = computed(() => {
const { errorCount, successCount, fakeErrorCount, pendingCount } = detail.value;
return addCommasToNumber(errorCount + successCount + fakeErrorCount + pendingCount);
});
function initOptionsData() {
const tempArr = [
{
label: 'report.detail.api.pass',
value: 'successCount',
color: '#00C261',
class: 'bg-[rgb(var(--success-6))]',
rateKey: 'requestPassRate',
},
{
label: 'report.detail.api.misstatement',
value: 'fakeErrorCount',
color: '#FFC14E',
class: 'bg-[rgb(var(--warning-6))]',
rateKey: 'requestFakeErrorRate',
},
{
label: 'report.detail.api.error',
value: 'errorCount',
color: '#ED0303',
class: 'bg-[rgb(var(--danger-6))]',
rateKey: 'requestErrorRate',
},
{
label: 'report.detail.api.pending',
value: 'pendingCount',
color: '#D4D4D8',
class: 'bg-[var(--color-text-input-border)]',
rateKey: 'requestPendingRate',
},
];
const validArr = props?.detailInfo?.integrated ? tempArr : tempArr.slice(0, 1);
charOptions.value.series.data = validArr.map((item: any) => {
return {
value: detail.value[item.value] || 0,
name: t(item.label),
itemStyle: {
color: item.color,
},
};
});
legendData.value = validArr.map((item: any) => {
return {
...item,
label: t(item.label),
count: detail.value[item.value] || 0,
rote: detail.value[item.rateKey] === 'Calculating' ? '-' : detail.value[item.rateKey],
};
});
}
watchEffect(() => {
if (props.detailInfo) {
detail.value = props.detailInfo;
initOptionsData();
}
});
</script>
<style scoped lang="less">
.report-container {
padding: 16px;
height: calc(100vh - 56px);
background: var(--color-text-n9);
.report-header {
padding: 0 16px;
height: 54px;
border-radius: 4px;
background: white;
@apply mb-4 bg-white;
}
.analyze {
height: 196px;
border-radius: 4px;
@apply mb-4 flex justify-between bg-white;
.request-analyze {
@apply flex h-full flex-1 flex-col p-4;
.chart-legend {
.chart-legend-item {
@apply grid grid-cols-3 gap-2;
}
.chart-flag {
@apply flex items-center;
.count {
color: var(--color-text-1);
}
}
}
}
.time-analyze {
@apply flex h-full flex-1 flex-col p-4;
.time-card {
@apply flex items-center justify-between;
.time-card-item {
border-radius: 6px;
background: var(--color-text-n9);
@apply mt-4 flex flex-1 flex-grow items-center px-4;
.time-card-item-title {
color: var(--color-text-4);
}
.count {
font-size: 18px;
@apply mx-2 font-medium;
}
}
.time-card-item-rote {
border-radius: 6px;
background: var(--color-text-n9);
@apply mt-4 flex flex-1 flex-grow flex-col p-4;
.time-card-item-rote-title {
color: var(--color-text-4);
@apply mb-2;
}
.count {
font-size: 18px;
@apply mx-2 font-medium;
}
}
}
}
}
.report-info {
padding: 16px;
border-radius: 4px;
@apply bg-white;
}
}
.block-title {
font-size: 14px;
@apply font-medium;
}
</style>

View File

@ -28,7 +28,7 @@
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
{{ t('common.share') }}
</MsButton>
<MsButton
<!-- <MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
@ -38,147 +38,33 @@
>
<MsIcon type="icon-icon_move_outlined" class="mr-2 font-[16px]" />
{{ t('common.export') }}
</MsButton>
</MsButton> -->
</div>
</template>
<template #default="{ detail }">
<div class="report-container h-full">
<!-- 报告参数开始 -->
<div class="report-header flex items-center justify-between">
<span>
{{ detail.environmentName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.poolName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.requestDuration || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.creatUserName || '-' }}
</span>
<span>
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
{{ dayjs(detail.startTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
{{ dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
</span>
</div>
<!-- 报告参数结束 -->
<!-- 报告分析开始 -->
<div class="analyze mb-1">
<!-- 请求分析 -->
<div class="request-analyze min-h-[110px]">
<div class="block-title mb-4">{{ t('report.detail.api.requestAnalysis') }}</div>
<!-- 独立报告 -->
<IndepReportChart
v-if="props.showType === 'INDEPENDENT'"
:legend-data="legendData"
:options="charOptions"
/>
<SetReportChart v-else :legend-data="legendData" :options="charOptions" />
<!-- 集合报告 -->
<!-- </div> -->
</div>
<!-- 耗时分析 -->
<div class="time-analyze">
<div class="time-card mb-2 mt-[16px] h-[40px] flex-1 gap-4">
<div class="time-card-item flex h-full">
<MsIcon type="icon-icon_time_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
<span class="time-card-item-title">{{ t('report.detail.api.totalTime') }}</span>
<span class="count">{{ getTotalTime }}</span
><span class="time-card-item-title">s</span>
</div>
<div class="time-card-item h-full">
<MsIcon type="icon-icon_time_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
<span class="time-card-item-title"> {{ t('report.detail.api.requestTotalTime') }}</span>
<span class="count">{{ detail.requestDuration || '-' }}</span
><span class="time-card-item-title">s</span>
</div>
</div>
<div class="time-card flex-1 gap-4">
<!-- 执行率 -->
<div v-if="props.showType === 'INTEGRATED'" class="time-card-item-rote">
<div class="time-card-item-rote-title">
<MsIcon type="icon-icon_yes_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.executionRate') }}
</div>
<div class="flex items-center">
<span class="count"> {{ getExcuteRate(detail) }} %</span>
<a-divider direction="vertical" class="!h-[16px]" :margin="8"></a-divider>
<span>{{ getRequestEacuteCount }}</span>
<span class="mx-1 text-[var(--color-text-4)]">/ {{ getRequestTotalCount || 0 }}</span>
</div>
</div>
<div class="time-card-item-rote">
<div class="time-card-item-rote-title">
<MsIcon type="icon-icon_yes_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.assertPass') }}
</div>
<div class="flex items-center">
<span class="count"
>{{ detail.assertionPassRate === 'Calculating' ? '-' : detail.assertionPassRate || '0.00' }}%</span
>
<a-divider direction="vertical" class="!h-[16px]" :margin="8"></a-divider>
<span>{{ addCommasToNumber(detail.assertionSuccessCount || 0) }}</span>
<span class="mx-1 text-[var(--color-text-4)]">/ {{ detail.assertionCount || 0 }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 报告步骤分析结束 -->
<!-- 报告明细开始 -->
<div class="report-info">
<div class="mb-4 flex h-[36px] items-center justify-between">
<div class="flex items-center">
<div class="mr-2 font-medium leading-[36px]">{{ t('report.detail.api.reportDetail') }}</div>
<a-radio-group v-model:model-value="activeTab" type="button" size="small">
<a-radio v-for="item of methods" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-radio>
</a-radio-group>
</div>
<a-select v-model="condition" class="w-[240px]" :placeholder="t('report.detail.api.filterPlaceholder')">
<a-option :key="1" :value="1"> 1 </a-option>
</a-select>
</div>
<!-- 平铺模式 -->
<TiledList
v-show="activeTab === 'tiled'"
show-type="CASE"
:active-type="activeTab"
:report-detail="detail || []"
/>
<!-- tab展示 -->
<TiledList
v-show="activeTab === 'tab'"
show-type="CASE"
:active-type="activeTab"
:report-detail="detail || []"
/>
</div>
<!-- 报告明细结束 -->
</div>
<CaseReportCom :detail-info="detail" />
</template>
</MsDetailDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import IndepReportChart from './case/IndepReportChart.vue';
import SetReportChart from './case/setReportChart.vue';
import TiledList from './tiledList.vue';
import CaseReportCom from './caseReportCom.vue';
import { reportCaseDetail } from '@/api/modules/api-test/report';
import { getShareInfo, reportCaseDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import { useAppStore } from '@/store';
import type { LegendData, ReportDetail } from '@/models/apiTest/report';
import type { ReportDetail } from '@/models/apiTest/report';
import { RouteEnum } from '@/enums/routeEnum';
const appStore = useAppStore();
const { t } = useI18n();
@ -250,146 +136,45 @@
* 分享share
*/
const shareLoading = ref<boolean>(false);
function shareHandler() {}
const shareLink = ref<string>('');
/**
* 导出
*/
const exportLoading = ref<boolean>(false);
function exportHandler() {}
const charOptions = ref({
tooltip: {
trigger: 'item',
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['65%', '80%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{
value: 0,
name: t('report.detail.api.pass'),
itemStyle: {
color: '#00C261',
},
},
],
},
async function shareHandler() {
try {
const res = await getShareInfo({
reportId: reportStepDetail.value.id,
projectId: appStore.currentProjectId,
});
const shareId = res.shareUrl;
const methods = ref([
{
label: t('report.detail.api.tiledDisplay'),
value: 'tiled',
const { origin } = window.location;
shareLink.value = `${origin}/#/${RouteEnum.SHARE}/${RouteEnum.SHARE_REPORT_CASE}${shareId}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(shareLink.value).then(
() => {
Message.info(t('bugManagement.detail.shareTip'));
},
{
label: t('report.detail.api.tabDisplay'),
value: 'tab',
},
]);
const legendData = ref<LegendData[]>([]);
const activeTab = ref('tiled');
const condition = ref('');
const getRequestTotalCount = computed(() => {
const { errorCount, successCount, fakeErrorCount, pendingCount } = reportStepDetail.value;
return addCommasToNumber(errorCount + successCount + fakeErrorCount + pendingCount);
});
//
const getRequestEacuteCount = computed(() => {
const { errorCount, successCount, fakeErrorCount } = reportStepDetail.value;
return addCommasToNumber(errorCount + successCount + fakeErrorCount);
});
function getExcuteRate(detail: ReportDetail) {
return 100 - Number(detail.requestPendingRate) ? (100 - Number(detail.requestPendingRate)).toFixed(2) : '0.00';
(e) => {
Message.error(e);
}
const getTotalTime = computed(() => {
const { endTime, startTime } = reportStepDetail.value;
if (endTime && startTime && endTime !== 0 && startTime !== 0) {
return endTime - startTime;
);
} else {
const input = document.createElement('input');
input.value = shareLink.value;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
Message.info(t('bugManagement.detail.shareTip'));
}
} catch (error) {
console.log(error);
}
return '-';
});
function initOptionsData() {
const tempArr = [
{
label: 'report.detail.api.pass',
value: 'successCount',
color: '#00C261',
class: 'bg-[rgb(var(--success-6))]',
rateKey: 'requestPassRate',
},
{
label: 'report.detail.api.misstatement',
value: 'fakeErrorCount',
color: '#FFC14E',
class: 'bg-[rgb(var(--warning-6))]',
rateKey: 'requestFakeErrorRate',
},
{
label: 'report.detail.api.error',
value: 'errorCount',
color: '#ED0303',
class: 'bg-[rgb(var(--danger-6))]',
rateKey: 'requestErrorRate',
},
{
label: 'report.detail.api.pending',
value: 'pendingCount',
color: '#D4D4D8',
class: 'bg-[var(--color-text-input-border)]',
rateKey: 'requestPendingRate',
},
];
const validArr = props.showType === 'INTEGRATED' ? tempArr : tempArr.slice(0, 1);
charOptions.value.series.data = validArr.map((item: any) => {
return {
value: reportStepDetail.value[item.value] || 0,
name: t(item.label),
itemStyle: {
color: item.color,
},
};
});
legendData.value = validArr.map((item: any) => {
return {
...item,
label: t(item.label),
count: reportStepDetail.value[item.value] || 0,
rote: reportStepDetail.value[item.rateKey] === 'Calculating' ? '-' : reportStepDetail.value[item.rateKey],
};
});
}
//
function loadedReport(detail: ReportDetail) {
innerReportId.value = detail.id;
reportStepDetail.value = cloneDeep(detail);
initOptionsData();
}
</script>
@ -406,12 +191,11 @@
@apply mb-4 bg-white;
}
.analyze {
min-height: 196px;
max-height: 200px;
height: 196px;
border-radius: 4px;
@apply mb-2 flex justify-between bg-white;
@apply mb-4 flex justify-between bg-white;
.request-analyze {
@apply flex flex-1 flex-col p-4;
@apply flex h-full flex-1 flex-col p-4;
.chart-legend {
.chart-legend-item {
@apply grid grid-cols-3 gap-2;
@ -425,7 +209,7 @@
}
}
.time-analyze {
@apply flex flex-1 flex-col p-4;
@apply flex h-full flex-1 flex-col p-4;
.time-card {
@apply flex items-center justify-between;
.time-card-item {

View File

@ -26,9 +26,9 @@
[ScenarioStepType.IF_CONTROLLER]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONCE_ONLY_CONTROLLER]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
[ScenarioStepType.API_CASE]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' },
[ScenarioStepType.CUSTOM_REQUEST]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' },
[ScenarioStepType.API]: { label: 'report.detail.api', color: 'rgb(var(--link-4))' },
};
const getClass = computed(() => {

View File

@ -4,10 +4,10 @@
v-model:visible="showDrawer"
:width="1200"
:footer="false"
:title="t('project.fileManagement.detail')"
:title="reportStepDetail.name"
:detail-id="props.reportId"
:detail-index="props.activeReportIndex"
:get-detail-func="reportDetail"
:get-detail-func="reportScenarioDetail"
:pagination="props.pagination"
:table-data="props.tableData"
:page-change="props.pageChange"
@ -28,7 +28,8 @@
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
{{ t('common.share') }}
</MsButton>
<MsButton
<!-- TODO 这个版本不上导出 -->
<!-- <MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
@ -38,187 +39,33 @@
>
<MsIcon type="icon-icon_move_outlined" class="mr-2 font-[16px]" />
{{ t('common.export') }}
</MsButton>
</MsButton> -->
</div>
</template>
<template #default="{ detail }">
<div class="report-container h-full">
<!-- 报告参数开始 -->
<div class="report-header flex items-center justify-between">
<!-- TODO 虚拟数据替换接口后边 -->
<span>
{{ detail.environmentName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.poolName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.requestDuration || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.createUser || '-' }}
</span>
<span>
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
{{ dayjs(detail.startTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
{{ dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
</span>
</div>
<!-- 报告参数结束 -->
<!-- 报告步骤分析和请求分析开始 -->
<div class="analyze mb-1">
<div class="step-analyze min-w-[522px]">
<div class="block-title">{{ t('report.detail.api.stepAnalysis') }}</div>
<div class="mb-2 flex items-center">
<!-- 总数 -->
<div class="countItem">
<span class="mr-2 text-[var(--color-text-4)]"> {{ t('report.detail.stepTotal') }}</span>
{{ detail.stepTotal || 0 }}
</div>
<!-- 通过 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.successCount') }}</div>
{{ detail.successCount || 0 }}
</div>
<!-- 误报 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.fakeErrorCount') }}</div>
{{ detail.fakeErrorCount || 0 }}
</div>
<!-- 失败 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.errorCount') }}</div>
{{ detail.errorCount || 0 }}
</div>
<!-- 未执行 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-input-border)]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.pendingCount') }}</div>
{{ detail.pendingCount || 0 }}
</div>
</div>
<StepProgress :report-detail="detail" height="8px" radius="var(--border-radius-mini)" />
<div class="card">
<div class="timer-card mr-2">
<div class="text-[var(--color-text-4)]">
<MsIcon type="icon-icon_time_outlined" class="text-[var(--color-text-4)]x mr-[4px]" size="16" />
{{ t('report.detail.api.totalTime') }}
</div>
<div>
<span class="ml-4 text-[18px] font-medium">{{ getTotalTime }}</span
>s
</div>
</div>
<div class="timer-card mr-2">
<div class="text-[var(--color-text-4)]">
<MsIcon type="icon-icon_time_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.requestTotalTime') }}
</div>
<div>
<span class="ml-4 text-[18px] font-medium">{{ detail.requestDuration || '-' }}</span
>s
</div>
</div>
<div class="timer-card min-w-[200px]">
<div class="text-[var(--color-text-4)]">
<MsIcon type="icon-icon_yes_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.assertPass') }}
</div>
<div class="flex items-center">
<span class="text-[18px] font-medium text-[var(--color-text-1)]"
>{{ detail.assertionPassRate || 0 }} <span>%</span></span
>
<a-divider direction="vertical" :margin="0" class="!mx-2 h-[16px]"></a-divider>
<span class="text-[var(--color-text-1)]">{{
addCommasToNumber(detail.assertionSuccessCount || 0)
}}</span>
<span class="text-[var(--color-text-4)]"
><span class="mx-1">/</span> {{ addCommasToNumber(detail.assertionCount) || 0 }}</span
>
</div>
</div>
</div>
</div>
<div class="request-analyze">
<div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div>
<div class="flex min-h-[110px] items-center">
<div class="relative mr-4">
<div class="absolute bottom-0 left-[30%] top-[35%] text-center">
<div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div>
<div class="text-[18px] font-medium">4</div>
</div>
<MsChart width="110px" height="110px" :options="charOptions" />
</div>
<div class="chart-legend grid flex-1 gap-y-3">
<!-- 图例开始 -->
<div v-for="item of legendData" :key="item.value" class="chart-legend-item">
<div class="chart-flag">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full" :class="item.class"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ item.label }}</div>
</div>
<div class="count">{{ item.count || 0 }}</div>
<div class="count">{{ item.rote || 0 }}%</div>
</div>
</div>
</div>
</div>
</div>
<!-- 报告步骤分析和请求分析结束 -->
<!-- 报告明细开始 -->
<div class="report-info">
<div class="mb-4 flex h-[36px] items-center justify-between">
<div class="flex items-center">
<div class="mr-2 font-medium leading-[36px]">{{ t('report.detail.api.reportDetail') }}</div>
<a-radio-group v-model:model-value="activeTab" type="button" size="small">
<a-radio v-for="item of methods" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-radio>
</a-radio-group>
</div>
<a-select v-model="condition" class="w-[240px]" :placeholder="t('report.detail.api.filterPlaceholder')">
<a-option :key="1" :value="1"> 1 </a-option>
</a-select>
</div>
<!-- 平铺模式 -->
<TiledList
v-show="activeTab === 'tiled'"
show-type="API"
:active-type="activeTab"
:report-detail="detail || []"
/>
<!-- tab展示 -->
<TiledList
v-show="activeTab === 'tab'"
show-type="API"
:active-type="activeTab"
:report-detail="detail || []"
/>
</div>
<!-- 报告明细结束 -->
</div>
<ScenarioCom :detail-info="detail" />
</template>
</MsDetailDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsChart from '@/components/pure/chart/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import StepProgress from './stepProgress.vue';
import TiledList from './tiledList.vue';
import ScenarioCom from './scenarioCom.vue';
import { reportDetail } from '@/api/modules/api-test/report';
import { getShareInfo, reportScenarioDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import { useAppStore } from '@/store';
import type { LegendData, ReportDetail } from '@/models/apiTest/report';
import type { ReportDetail } from '@/models/apiTest/report';
import { RouteEnum } from '@/enums/routeEnum';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
@ -229,6 +76,7 @@
pagination: MsPaginationI;
pageChange: (page: number) => Promise<void>;
showType: string; //
isShare?: boolean;
}>();
const emit = defineEmits<{
@ -243,6 +91,7 @@
emit('update:visible', val);
},
});
const innerReportId = ref(props.reportId);
const reportStepDetail = ref<ReportDetail>({
@ -285,161 +134,57 @@
console: '',
});
const charOptions = ref({
tooltip: {
trigger: 'item',
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['65%', '80%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{
value: 0,
name: t('report.detail.api.pass'),
itemStyle: {
color: '#00C261',
},
},
{
value: 0,
name: t('report.detail.api.misstatement'),
itemStyle: {
color: '#FFC14E',
},
},
{
value: 0,
name: t('report.detail.api.error'),
itemStyle: {
color: '#ED0303',
},
},
{
value: 0,
name: t('report.detail.api.pending'),
itemStyle: {
color: '#D4D4D8',
},
},
],
},
});
const getTotalTime = computed(() => {
const { endTime, startTime } = reportStepDetail.value;
if (endTime && startTime && endTime !== 0 && startTime !== 0) {
return endTime - startTime;
}
return '-';
});
const legendData = ref<LegendData[]>([]);
function initOptionsData() {
const tempArr = [
{
label: 'report.detail.api.pass',
value: 'successCount',
color: '#00C261',
class: 'bg-[rgb(var(--success-6))]',
rateKey: 'requestPassRate',
},
{
label: 'report.detail.api.misstatement',
value: 'fakeErrorCount',
color: '#FFC14E',
class: 'bg-[rgb(var(--warning-6))]',
rateKey: 'requestFakeErrorRate',
},
{
label: 'report.detail.api.error',
value: 'successCount',
color: '#ED0303',
class: 'bg-[rgb(var(--danger-6))]',
rateKey: 'requestErrorRate',
},
{
label: 'report.detail.api.pending',
value: 'pendingCount',
color: '#D4D4D8',
class: 'bg-[var(--color-text-input-border)]',
rateKey: 'requestPendingRate',
},
];
charOptions.value.series.data = tempArr.map((item: any) => {
return {
value: reportStepDetail.value[item.value] || 0,
name: t(item.label),
itemStyle: {
color: item.color,
},
};
});
legendData.value = tempArr.map((item: any) => {
return {
...item,
label: t(item.label),
count: reportStepDetail.value[item.value] || 0,
rote: reportStepDetail.value[item.rateKey],
};
});
}
//
/**
* 详情
*/
function loadedReport(detail: ReportDetail) {
innerReportId.value = detail.id;
reportStepDetail.value = cloneDeep(detail);
initOptionsData();
}
/**
* 分享share
*/
const shareLink = ref<string>('');
const shareId = ref<string>('');
const shareLoading = ref<boolean>(false);
function shareHandler() {}
async function shareHandler() {
try {
const res = await getShareInfo({
reportId: reportStepDetail.value.id,
projectId: appStore.currentProjectId,
});
shareId.value = res.shareUrl;
const { origin } = window.location;
shareLink.value = `${origin}/#/${RouteEnum.SHARE}/${RouteEnum.SHARE_REPORT_SCENARIO}${shareId.value}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(shareLink.value).then(
() => {
Message.info(t('bugManagement.detail.shareTip'));
},
(e) => {
Message.error(e);
}
);
} else {
const input = document.createElement('input');
input.value = shareLink.value;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
Message.info(t('bugManagement.detail.shareTip'));
}
} catch (error) {
console.log(error);
}
}
/**
* 导出
*/
const exportLoading = ref<boolean>(false);
function exportHandler() {}
const activeTab = ref('tiled');
const condition = ref('');
const methods = ref([
{
label: t('report.detail.api.tiledDisplay'),
value: 'tiled',
},
{
label: t('report.detail.api.tabDisplay'),
value: 'tab',
},
]);
onMounted(() => {
initOptionsData();
});
</script>
<style scoped lang="less">
@ -455,11 +200,13 @@
@apply mb-4 bg-white;
}
.analyze {
min-height: 196px;
height: 196px;
border-radius: 4px;
@apply mb-2 flex justify-between;
@apply mb-4 flex justify-between;
.step-analyze {
padding: 16px;
width: 60%;
height: 196px;
border-radius: 4px;
@apply h-full bg-white;
.countItem {
@ -476,6 +223,8 @@
}
.request-analyze {
padding: 16px;
width: 40%;
height: 100%;
border-radius: 4px;
@apply ml-4 h-full flex-grow bg-white;
.chart-legend {

View File

@ -69,9 +69,9 @@
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
icon: 'icon-icon_block_filled',
label: 'report.status.pending',
color: '!text-[rgb(var(--link-6))]',
color: '!text-[var(--color-text-input-border)]',
},
},
[ReportEnum.API_SCENARIO_REPORT]: {
@ -105,7 +105,7 @@
PENDING: {
icon: 'icon-icon_wait',
label: 'report.status.pending',
color: '!text-[rgb(var(--link-6))]',
color: '!text-[var(--color-text-input-border)]',
},
},
});

View File

@ -1,217 +0,0 @@
<template>
<div class="resContentWrapper">
<!-- 循环计数器 -->
<div v-if="detailItem.stepType === 'LOOP_CONTROLLER'" class="mb-4 flex justify-start">
<MsPagination
v-model:page-size="pageNation.pageSize"
v-model:current="pageNation.current"
:total="pageNation.total"
size="mini"
@change="loadLoop"
@page-size-change="loadLoop"
/>
</div>
<div class="resContent">
<div class="flex h-full w-full items-center justify-between rounded bg-[var(--color-text-n9)] px-4">
<div class="font-medium">
<span>{{ t('report.detail.api.resContent') }}</span>
<span class="text-[rgb(var(--primary-5))]">
<a-divider direction="vertical" :margin="8"></a-divider>
子请求</span
>
</div>
<div class="grid grid-cols-4 gap-2 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 || '') }"
>
{{ 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 || '') }">
{{ activeStepDetail?.content?.responseResult.responseCode || '-' }}
</div>
</div>
</template>
</a-popover>
<span class="text-[rgb(var(--success-6))]"
>{{ activeStepDetail?.content?.responseResult?.responseTime }}ms</span
>
<span class="text-[rgb(var(--success-6))]"
>{{ activeStepDetail?.content?.responseResult?.responseSize }}bytes</span
>
<!-- <span>Mock</span> -->
<span>{{ props.environmentName }}</span>
</div>
</div>
</div>
<!-- 子请求开始 -->
<div>
<!-- TODO 最后写 看看能不能使用其他的代替 -->
</div>
<!-- 子请求结束 -->
<!-- 响应内容tab开始 -->
<div>
<a-tabs v-model:active-key="showTab" class="no-content">
<a-tab-pane v-for="it of tabList" :key="it.key" :title="t(it.title)" />
</a-tabs>
<a-divider :margin="0"></a-divider>
<div v-if="showTab !== 'assertions'">
<ResContent :script="showContent || ''" language="JSON" show-charset-change
/></div>
<div v-else>
<assertTable :data="showContent" />
</div>
</div>
<!-- 响应内容tab结束 -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsPagination from '@/components/pure/ms-pagination/index';
import assertTable from './step/assertTable.vue';
import ResContent from './step/resContent.vue';
import { reportCaseStepDetail, reportStepDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import type { ReportStepDetail, ScenarioItemType } from '@/models/apiTest/report';
const { t } = useI18n();
const props = defineProps<{
detailItem: ScenarioItemType; //
showType: 'API' | 'CASE'; // |
console?: string; //
environmentName?: string; //
}>();
const pageNation = ref({
total: 1000,
pageSize: 10,
current: 1,
});
//
async function loadLoop() {}
const tabList = ref([
{
key: 'body',
title: 'report.detail.api.resBody',
},
{
key: 'headers',
title: 'report.detail.api.resHeader',
},
{
key: 'realReq',
title: 'report.detail.api.realReq',
},
{
key: 'console',
title: 'report.detail.api.console',
},
{
key: 'extract',
title: 'report.detail.api.extract',
},
{
key: 'assertions',
title: 'report.detail.api.assert',
},
]);
/**
* 响应状态码对应颜色
*/
function statusCodeColor(code: string) {
if (code) {
const resCode = Number(code);
if (resCode >= 200 && resCode < 300) {
return 'rgb(var(--success-7)';
}
if (resCode >= 300 && resCode < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
}
return '';
}
const showTab = ref('body');
const reportDetailMap = {
API: {
stepDetail: reportStepDetail,
},
CASE: {
stepDetail: reportCaseStepDetail,
},
};
const activeIndex = ref<number>(0);
const activeStepDetail = ref<ReportStepDetail>({});
/**
* 获取步骤详情
*/
const stepDetailInfo = ref<ReportStepDetail[]>([]);
async function getStepDetail() {
try {
const result = await reportDetailMap[props.showType].stepDetail(
props.detailItem.reportId,
props.detailItem.stepId
);
stepDetailInfo.value = cloneDeep(result) as ReportStepDetail[];
activeStepDetail.value = stepDetailInfo.value[activeIndex.value];
} catch (error) {
console.log(error);
}
}
/**
* 获取请求内容
*/
const showContent = computed(() => {
if (showTab.value === 'console') {
return props.console;
}
if (showTab.value === 'realReq') {
return activeStepDetail.value.content?.body
? `${t('apiTestDebug.requestUrl')}:\n${activeStepDetail.value.content.url}\n${t('apiTestDebug.header')}:\n${
activeStepDetail.value.content.headers
}\nBody:\n${activeStepDetail.value.content.body.trim()}`
: '';
}
if (showTab.value === 'extract') {
return activeStepDetail.value.content?.responseResult.vars?.trim();
}
return activeStepDetail.value.content?.responseResult[showTab.value];
});
onMounted(() => {
if (!props.detailItem.fold) {
getStepDetail();
}
});
</script>
<style scoped lang="less">
.resContentWrapper {
position: relative;
border: 1px solid var(--color-text-n8);
border-top: none;
border-radius: 0 0 6px 6px;
@apply mb-4 bg-white p-4;
.resContent {
height: 38px;
border-radius: 6px;
}
}
</style>

View File

@ -0,0 +1,407 @@
<template>
<div class="report-container h-full">
<!-- 报告参数开始 -->
<div class="report-header flex items-center justify-between">
<!-- TODO 虚拟数据替换接口后边 -->
<span>
{{ detail.environmentName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.poolName || '-' }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.requestDuration }}
<a-divider direction="vertical" :margin="4"></a-divider>
{{ detail.creatUserName || '-' }}
</span>
<span>
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTime') }}</span>
{{ dayjs(detail.startTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
<span class="text-[var(--color-text-4)]">{{ t('report.detail.api.executionTimeTo') }}</span>
{{ dayjs(detail.endTime).format('YYYY-MM-DD HH:mm:ss') || '-' }}
</span>
</div>
<!-- 报告参数结束 -->
<!-- 报告步骤分析和请求分析开始 -->
<div class="analyze mb-1">
<div class="step-analyze min-w-[522px]">
<div class="block-title">{{ t('report.detail.api.stepAnalysis') }}</div>
<div class="mb-2 flex items-center">
<!-- 总数 -->
<div class="countItem">
<span class="mr-2 text-[var(--color-text-4)]"> {{ t('report.detail.stepTotal') }}</span>
{{ detail.stepTotal || 0 }}
</div>
<!-- 通过 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.successCount') }}</div>
{{ detail.successCount || 0 }}
</div>
<!-- 误报 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.fakeErrorCount') }}</div>
{{ detail.fakeErrorCount || 0 }}
</div>
<!-- 失败 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.errorCount') }}</div>
{{ detail.errorCount || 0 }}
</div>
<!-- 未执行 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-input-border)]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.pendingCount') }}</div>
{{ detail.pendingCount || 0 }}
</div>
</div>
<StepProgress :report-detail="detail" height="8px" radius="var(--border-radius-mini)" />
<div class="card">
<div class="timer-card mr-2">
<div class="text-[var(--color-text-4)]">
<MsIcon type="icon-icon_time_outlined" class="text-[var(--color-text-4)]x mr-[4px]" size="16" />
{{ t('report.detail.api.totalTime') }}
</div>
<div>
<span class="ml-4 text-[18px] font-medium">{{ getTotalTime }}</span
>s
</div>
</div>
<div class="timer-card mr-2">
<div class="text-[var(--color-text-4)]">
<MsIcon type="icon-icon_time_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.requestTotalTime') }}
</div>
<div>
<span class="ml-4 text-[18px] font-medium">{{ detail.requestDuration }}</span
>s
</div>
</div>
<div class="timer-card min-w-[200px]">
<div class="text-[var(--color-text-4)]">
<MsIcon type="icon-icon_yes_outlined" class="mr-[4px] text-[var(--color-text-4)]" size="16" />
{{ t('report.detail.api.assertPass') }}
</div>
<div class="flex items-center">
<span class="text-[18px] font-medium text-[var(--color-text-1)]"
>{{ detail.assertionPassRate || 0 }} <span>%</span></span
>
<a-divider direction="vertical" :margin="0" class="!mx-2 h-[16px]"></a-divider>
<span class="text-[var(--color-text-1)]">{{ addCommasToNumber(detail.assertionSuccessCount || 0) }}</span>
<span class="text-[var(--color-text-4)]"
><span class="mx-1">/</span> {{ addCommasToNumber(detail.assertionCount) || 0 }}</span
>
</div>
</div>
</div>
</div>
<div class="request-analyze">
<div class="block-title">{{ t('report.detail.api.requestAnalysis') }}</div>
<div class="flex min-h-[110px] items-center">
<div class="relative mr-4">
<div class="absolute bottom-0 left-[30%] top-[35%] text-center">
<div class="text-[12px] text-[(var(--color-text-4))]">{{ t('report.detail.api.total') }}</div>
<div class="text-[18px] font-medium">4</div>
</div>
<MsChart width="110px" height="110px" :options="charOptions" />
</div>
<div class="chart-legend grid flex-1 gap-y-3">
<!-- 图例开始 -->
<div v-for="item of legendData" :key="item.value" class="chart-legend-item">
<div class="chart-flag">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full" :class="item.class"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ item.label }}</div>
</div>
<div class="count">{{ item.count || 0 }}</div>
<div class="count">{{ item.rote || 0 }}%</div>
</div>
</div>
</div>
</div>
</div>
<!-- 报告步骤分析和请求分析结束 -->
<!-- 报告明细开始 -->
<div class="report-info">
<div class="mb-4 flex h-[36px] items-center justify-between">
<div class="flex items-center">
<div class="mr-2 font-medium leading-[36px]">{{ t('report.detail.api.reportDetail') }}</div>
<a-radio-group v-model:model-value="activeTab" type="button" size="small">
<a-radio v-for="item of methods" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-radio>
</a-radio-group>
</div>
<a-select v-model="condition" class="w-[240px]" :placeholder="t('report.detail.api.filterPlaceholder')">
<a-option :key="1" :value="1"> 1 </a-option>
</a-select>
</div>
<TiledList show-type="API" :active-type="activeTab" :report-detail="detail || []" />
</div>
<!-- 报告明细结束 -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs';
import MsChart from '@/components/pure/chart/index.vue';
import StepProgress from './stepProgress.vue';
import TiledList from './tiledList.vue';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import type { LegendData, ReportDetail } from '@/models/apiTest/report';
const { t } = useI18n();
const props = defineProps<{
detailInfo?: ReportDetail;
}>();
const detail = ref<ReportDetail>({
id: '',
name: '', //
testPlanId: '',
createUser: '',
deleteTime: 0,
deleteUser: '',
deleted: false,
updateUser: '',
updateTime: 0,
startTime: 0, // /
endTime: 0, // /
requestDuration: 0, //
status: '', // /SUCCESS/ERROR
triggerMode: '', //
runMode: '', //
poolId: '', //
poolName: '', //
versionId: '',
integrated: false, //
projectId: '',
environmentId: '', // id
environmentName: '', //
errorCount: 0, //
fakeErrorCount: 0, //
pendingCount: 0, //
successCount: 0, //
assertionCount: 0, //
assertionSuccessCount: 0, //
requestErrorRate: '', //
requestPendingRate: '', //
requestFakeErrorRate: '', //
requestPassRate: '', //
assertionPassRate: '', //
scriptIdentifier: '', //
children: [], //
stepTotal: 0, //
console: '',
});
const getTotalTime = computed(() => {
if (detail.value) {
const { endTime, startTime } = detail.value;
if (endTime && startTime && endTime !== 0 && startTime !== 0) {
return endTime - startTime;
}
return '-';
}
return '-';
});
const methods = ref([
{
label: t('report.detail.api.tiledDisplay'),
value: 'tiled',
},
{
label: t('report.detail.api.tabDisplay'),
value: 'tab',
},
]);
const legendData = ref<LegendData[]>([]);
const charOptions = ref({
tooltip: {
trigger: 'item',
},
legend: {
show: false,
},
series: {
name: '',
type: 'pie',
radius: ['65%', '80%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{
value: 0,
name: t('report.detail.api.pass'),
itemStyle: {
color: '#00C261',
},
},
{
value: 0,
name: t('report.detail.api.misstatement'),
itemStyle: {
color: '#FFC14E',
},
},
{
value: 0,
name: t('report.detail.api.error'),
itemStyle: {
color: '#ED0303',
},
},
{
value: 0,
name: t('report.detail.api.pending'),
itemStyle: {
color: '#D4D4D8',
},
},
],
},
});
const activeTab = ref<'tiled' | 'tab'>('tiled');
const condition = ref('');
function initOptionsData() {
const tempArr = [
{
label: 'report.detail.api.pass',
value: 'successCount',
color: '#00C261',
class: 'bg-[rgb(var(--success-6))]',
rateKey: 'requestPassRate',
},
{
label: 'report.detail.api.misstatement',
value: 'fakeErrorCount',
color: '#FFC14E',
class: 'bg-[rgb(var(--warning-6))]',
rateKey: 'requestFakeErrorRate',
},
{
label: 'report.detail.api.error',
value: 'errorCount',
color: '#ED0303',
class: 'bg-[rgb(var(--danger-6))]',
rateKey: 'requestErrorRate',
},
{
label: 'report.detail.api.pending',
value: 'pendingCount',
color: '#D4D4D8',
class: 'bg-[var(--color-text-input-border)]',
rateKey: 'requestPendingRate',
},
];
charOptions.value.series.data = tempArr.map((item: any) => {
return {
value: detail.value[item.value] || 0,
name: t(item.label),
itemStyle: {
color: item.color,
},
};
});
legendData.value = tempArr.map((item: any) => {
return {
...item,
label: t(item.label),
count: detail.value[item.value] || 0,
rote: detail.value[item.rateKey] === 'Calculating' ? '-' : detail.value[item.rateKey],
};
});
}
watchEffect(() => {
if (props.detailInfo) {
detail.value = props.detailInfo;
initOptionsData();
}
});
</script>
<style scoped lang="less">
.report-container {
padding: 16px;
height: calc(100vh - 56px);
background: var(--color-text-n9);
.report-header {
padding: 0 16px;
height: 54px;
border-radius: 4px;
background: white;
@apply mb-4 bg-white;
}
.analyze {
height: 196px;
border-radius: 4px;
@apply mb-4 flex justify-between;
.step-analyze {
padding: 16px;
width: 60%;
height: 196px;
border-radius: 4px;
@apply h-full bg-white;
.countItem {
@apply mr-6 flex items-center;
}
.card {
@apply mt-4 flex items-center justify-between;
.timer-card {
border-radius: 6px;
background-color: var(--color-text-n9);
@apply flex flex-1 flex-col p-4;
}
}
}
.request-analyze {
padding: 16px;
width: 40%;
height: 100%;
border-radius: 4px;
@apply ml-4 h-full flex-grow bg-white;
.chart-legend {
.chart-legend-item {
@apply grid grid-cols-3;
}
.chart-flag {
@apply flex items-center;
.count {
color: var(--color-text-1);
}
}
}
}
}
.report-info {
padding: 16px;
border-radius: 4px;
@apply bg-white;
}
}
.block-title {
@apply mb-4 font-medium;
}
</style>

View File

@ -1,288 +0,0 @@
<template>
<template v-for="item in list" :key="item.stepId">
<div
:style="{
'padding-left': `${16 * (item.level as number)}px`,
}"
>
<div
class="scenario-class cursor-pointer rounded-t-md px-8"
:class="[...getBorderAndRadius(item), ...getBorderClass(item)]"
@click="showDetail(item)"
>
<div class="flex h-[46px] items-center">
<!-- 序号 -->
<span class="index mr-2 text-[var(--color-text-4)]">{{ item.sort }}</span>
<!-- 展开折叠控制器 -->
<div v-if="getShowExpand(item)" class="mx-2">
<span
v-if="item.fold"
class="collapsebtn flex items-center justify-center"
@click.stop="expandHandler(item)"
>
<icon-right class="text-[var(--color-text-4)]" :style="{ 'font-size': '12px' }" />
</span>
<span v-else class="expand flex items-center justify-center" @click.stop="expandHandler(item)">
<icon-down class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }" />
</span>
</div>
<MsIcon
v-if="props.showType === 'API'"
type="icon-icon_split_turn-down_arrow"
class="mx-[4px] text-[var(--color-text-4)]"
size="16"
/>
<!-- 场景count -->
<span v-if="props.showType === 'API'" class="mr-2 text-[var(--color-text-4)]">{{
(item.children || []).length
}}</span>
<!-- 循环控制器 -->
<ConditionStatus v-if="props.showType === 'API'" :status="item.stepType || ''" />
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text max-w-[200px]">
{{ item.name || '-' }}
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="max-w-[300px]">
{{ item.name || '-' }}
</div>
</div>
</template>
</a-popover>
</div>
<div class="flex">
<MsTag class="cursor-pointer" :type="item.status === 'SUCCESS' ? 'success' : 'danger'" theme="light">
{{ item.status === 'SUCCESS' ? t('report.detail.api.pass') : t('report.detail.api.resError') }}
</MsTag>
<span class="statusCode mx-2">
<div class="mr-2"> {{ t('report.detail.api.statusCode') }}</div>
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor(item.code) }">
{{ item.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(item.code) }">
{{ item.code || '-' }}
</div>
</div>
</template>
</a-popover>
</span>
<span class="resTime">
{{ t('report.detail.api.responseTime') }}
<span class="resTimeCount ml-2">{{ item.requestTime || 0 }}ms</span></span
>
<span class="resSize">
{{ t('report.detail.api.responseSize') }}
<span class="resTimeCount ml-2">{{ item.responseSize || 0 }} bytes</span></span
>
</div>
</div>
</div>
<a-divider
v-if="item.level === 0 && props.showType !== 'CASE'"
:margin="0"
class="!mb-4"
:class="props.showType === 'API' ? '!mb-4' : '!mb-0'"
></a-divider>
<!-- 响应内容开始 -->
<div
v-if="showResContent(item)"
:style="{
'padding-left': `${16 * (item.level as number)}px`,
}"
>
<ResponseContent
:detail-item="item"
:show-type="props.showType"
:console="props.console"
:environment-name="props.environmentName"
/>
</div>
<!-- </div> -->
<!-- 响应内容结束 -->
<ScenarioItem
v-if="'children' in item"
:list="item.children"
:active-type="props.activeType"
:show-type="props.showType"
:console="props.console"
:environment-name="props.environmentName"
@detail="showDetail"
/>
</template>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import ConditionStatus from './conditionStatus.vue';
import ResponseContent from './responseContent.vue';
import { useI18n } from '@/hooks/useI18n';
import type { ScenarioItemType } from '@/models/apiTest/report';
const { t } = useI18n();
const props = withDefaults(
defineProps<{
showBorder?: boolean;
hasBottomMargin?: boolean;
list: ScenarioItemType[];
activeType: string;
showType: 'API' | 'CASE';
console?: string; //
environmentName?: string; //
}>(),
{
showBorder: true,
hasBottomMargin: true,
}
);
const emit = defineEmits(['expand', 'detail']);
const activeItem = ref();
function showDetail(item: ScenarioItemType) {
if (props.activeType === 'tab') {
return;
}
activeItem.value = item;
emit('detail', activeItem.value);
}
const showApiType = ref<string[]>(['API', 'API_CASE', 'CUSTOM_API', 'LOOP_CONTROLLER']);
async function expandHandler(item: ScenarioItemType) {
item.fold = !item.fold;
}
function getBorderAndRadius(item: ScenarioItemType) {
if (props.showType === 'API') {
if (props.activeType === 'tab') {
if (!item.fold && showApiType.value.includes(item.stepType)) {
return ['rounded-b-none', 'mb-0'];
}
return ['mb-1', 'rounded-[4px]'];
}
} else {
return ['mb-1', 'rounded-[4px]'];
}
return ['mb-1', 'rounded-[4px]'];
}
function getBorderClass(item: ScenarioItemType) {
if (props.showType === 'API') {
return item.level !== 0 ? ['border', 'border-solid', 'border-[var(--color-text-n8)]'] : [''];
}
return ['border', 'border-solid', 'border-[var(--color-text-n8)]'];
}
function getShowExpand(item: ScenarioItemType) {
if (props.showType === 'API') {
return item.level !== 0 && showApiType.value.includes(item.stepType) && props.activeType === 'tab';
}
return props.activeType === 'tab';
}
function showResContent(item: ScenarioItemType) {
if (props.showType === 'API') {
return showApiType.value.includes(item.stepType) && props.activeType === 'tab' && !item.fold;
}
return props.activeType === 'tab' && !item.fold;
}
//
function statusCodeColor(code: string) {
if (code) {
const resCode = Number(code);
if (resCode >= 200 && resCode < 300) {
return 'rgb(var(--success-7)';
}
if (resCode >= 300 && resCode < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
}
return '';
}
</script>
<style scoped lang="less">
.scenario-class {
@apply flex items-center justify-between px-2;
.index {
width: 16px;
height: 16px;
line-height: 16px;
border-radius: 50%;
color: white;
background: var(--color-text-brand);
@apply inline-block text-center;
}
.resTime,
.resSize,
.statusCode {
margin-right: 8px;
color: var(--color-text-4);
@apply flex;
.resTimeCount {
color: rgb(var(--success-6));
}
.code {
display: inline-block;
max-width: 60px;
text-overflow: ellipsis;
word-break: keep-all;
white-space: nowrap;
}
}
}
.resContentWrapper {
position: relative;
border: 1px solid var(--color-text-n8);
border-top: none;
border-radius: 0 0 6px 6px;
@apply mb-4 bg-white p-4;
.resContent {
height: 38px;
border-radius: 6px;
}
}
:deep(.expand) {
width: 16px;
height: 16px;
border-radius: 50%;
background: rgb(var(--primary-1));
}
:deep(.collapsebtn) {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-text-n8) !important;
@apply bg-white;
}
:deep(.arco-table-expand-btn) {
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background: var(--color-text-n8) !important;
}
:deep(.no-content) {
.arco-tabs-content {
display: none;
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
</style>

View File

@ -1,84 +0,0 @@
<template>
<ms-base-table ref="tableRef" v-bind="propsRes" no-disable :indent-size="0" v-on="propsEvent">
<template #pass="{ record }">
<MsTag theme="light" :type="record.pass ? 'success' : 'danger'">
{{ record.pass ? t('report.detail.api.resSuccess') : t('report.detail.api.resError') }}
</MsTag>
</template>
<template #script="{ record }">
{{ record.script }}
</template>
</ms-base-table>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import type { AssertionItem } from '@/models/apiTest/report';
const { t } = useI18n();
const props = defineProps<{
data: AssertionItem[];
}>();
const columns: MsTableColumn = [
{
title: 'report.detail.api.content',
dataIndex: 'content',
slotName: 'content',
showTooltip: true,
headerCellClass: 'assertTitleClass',
bodyCellClass: 'assertCellClass',
},
{
title: 'report.detail.api.assertStatus',
dataIndex: 'pass',
slotName: 'pass',
showTooltip: true,
},
{
title: '',
dataIndex: 'script',
slotName: 'script',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(undefined, {
columns,
scroll: { x: 'auto' },
showPagination: false,
hoverable: false,
showExpand: true,
rowKey: 'id',
rowClass: (record: any) => {
if (record.children) {
return 'gray-td-bg';
}
},
});
watchEffect(() => {
propsRes.value.data = props.data;
});
</script>
<style scoped lang="less">
:deep(.arco-table-th) {
background: var(--color-text-n9) !important;
}
:deep(.assertTitleClass.arco-table-th) {
padding-left: 36px;
}
:deep(.arco-table-td.assertCellClass) {
padding-left: 36px;
}
</style>

View File

@ -1,36 +0,0 @@
<template>
<MsCodeEditor
v-model:model-value="innerValue"
:show-theme-change="false"
title=""
width="100%"
height="208px"
:show-language-change="props.language ? true : false"
theme="MS-text"
:language="props.language"
:show-charset-change="props.showCharsetChange"
:read-only="false"
:show-full-screen="false"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { Language } from '@/components/pure/ms-code-editor/types';
const props = defineProps<{
script: string;
language?: Language;
showCharsetChange: boolean;
}>();
const emit = defineEmits<{
(e: 'update:script', value: string): void;
}>();
const innerValue = useVModel(props, 'script', emit);
</script>
<style scoped></style>

View File

@ -4,107 +4,48 @@
v-model:visible="showDrawer"
:width="960"
:footer="false"
:title="t('步骤名称')"
:title="props.scenarioDetail?.name"
show-full-screen
:unmount-on-close="true"
>
<template #headerLeft>
<div class="scene-type"> API </div>
<ConditionStatus
v-if="props.scenarioDetail?.stepType && showCondition.includes(props.scenarioDetail?.stepType)"
class="ml-2"
:status="props.scenarioDetail?.stepType"
/>
</template>
<div>
<div class="mb-4 flex justify-start">
<MsPagination
v-if="props.scenarioDetail.stepType === 'LOOP_CONTROLLER'"
v-model:page-size="pageNation.pageSize"
v-model:current="pageNation.current"
:total="pageNation.total"
size="mini"
@change="loadLoop"
@page-size-change="loadLoop"
/></div>
<ms-base-table
ref="tableRef"
v-bind="propsRes"
v-model:expandedKeys="expandedKeys"
no-disable
:indent-size="0"
v-on="propsEvent"
>
<template #titleName>
<div class="flex w-full justify-between">
<div class="font-medium">{{ t('report.detail.api.resContent') }}</div>
<div class="grid grid-cols-4 gap-2 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 || '') }"
>
{{ 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 || '') }"
>
{{ activeStepDetail?.content?.responseResult.responseCode || '-' }}
</div>
</div>
</template>
</a-popover>
<span class="text-[rgb(var(--success-6))]"
>{{ activeStepDetail.content?.responseResult.responseTime }}ms</span
>
<span class="text-[rgb(var(--success-6))]">
{{ activeStepDetail.content?.responseResult.responseSize }} bytes</span
>
<!-- <span>Mock</span> -->
<span>{{ props.environmentName }}</span>
</div>
</div>
</template>
<template #name="{ record }">
<span class="font-medium">
{{ record.name }}
</span>
<div v-if="record.showScript && !record.isAssertion" class="w-full">
<ResContent :script="record.script || ''" language="JSON" :show-charset-change="record.showScript" />
</div>
<div v-if="record.isAssertion" class="w-full">
<assertTable :data="record.assertions" />
</div>
</template>
</ms-base-table>
<StepDetailContent
mode="tiled"
:show-type="props.showType"
:step-item="props.scenarioDetail"
:console="props.console"
:is-definition="true"
:environment-name="props.environmentName"
:report-id="props.scenarioDetail?.reportId"
/>
</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 MsPagination from '@/components/pure/ms-pagination/index';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import assertTable from './assertTable.vue';
import ResContent from './resContent.vue';
import ConditionStatus from '../conditionStatus.vue';
import StepDetailContent from '@/views/api-test/components/requestComposition/response/result/index.vue';
import { reportCaseStepDetail, reportStepDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId } from '@/utils';
import type { ReportStepDetail, ScenarioDetailItem } from '@/models/apiTest/report';
import type { ScenarioItemType } from '@/models/apiTest/report';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
stepId: string;
activeStepIndex: number;
scenarioDetail: ScenarioDetailItem;
scenarioDetail?: ScenarioItemType;
showType: 'API' | 'CASE'; // |
console?: string; //
environmentName?: string; //
@ -122,245 +63,15 @@
emit('update:visible', val);
},
});
const innerFileId = ref(props.stepId);
function loadedStep(detail: Record<string, any>) {
innerFileId.value = detail.id;
}
const pageNation = ref({
total: 1000,
pageSize: 10,
current: 1,
});
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
const expandedKeys = ref<string[]>([]);
const columns: MsTableColumn = [
{
title: 'report.detail.api.resContent',
dataIndex: 'name',
slotName: 'name',
titleSlotName: 'titleName',
fixed: 'left',
headerCellClass: 'titleClass',
bodyCellClass: (record) => {
if (record.children) {
return '';
}
return 'cellClassWrapper';
},
},
];
const { propsRes, propsEvent } = useTable(undefined, {
columns,
scroll: { x: 'auto' },
showPagination: false,
hoverable: false,
showExpand: true,
rowKey: 'id',
rowClass: (record: any) => {
if (record.children) {
return 'gray-td-bg';
}
},
});
async function loadLoop() {}
/**
* 响应状态码对应颜色
*/
function statusCodeColor(code: string) {
if (code) {
const resCode = Number(code);
if (resCode >= 200 && resCode < 300) {
return 'rgb(var(--success-7)';
}
if (resCode >= 300 && resCode < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
}
return '';
}
const stepDetailInfo = ref<ReportStepDetail[]>([]);
//
function getRequestItem(item: ReportStepDetail) {
const headers = item.content?.responseResult.headers;
const body = item.content?.responseResult.body;
const realRequest = item.content?.body
? `${t('apiTestDebug.requestUrl')}:\n${item.content?.url}\n${t('apiTestDebug.header')}:\n${
item.content?.headers
}\nBody:\n${item.content?.body.trim()}`
: '';
const assertionList = item.content?.responseResult.assertions;
const extractValue = item.content?.responseResult.vars?.trim();
return {
headers,
body,
realRequest,
assertionList,
extractValue,
};
}
const tempStepMap = ref<Record<string, any>>({});
function setTableMaps(currentStepId: string, paramsObj: Record<string, any>) {
const { headers, body, realRequest, assertionList, extractValue } = paramsObj;
tempStepMap.value[currentStepId] = [
{
id: getGenerateId(),
name: '响应体',
script: '',
showScript: false,
isAssertion: false,
assertions: [],
children: [
{
showScript: true,
script: body,
isAssertion: false,
assertions: [],
},
],
},
{
id: getGenerateId(),
name: '响应头',
script: '',
showScript: false,
isAssertion: false,
assertions: [],
children: [
{
script: headers,
showScript: true,
isAssertion: false,
assertions: [],
},
],
},
{
id: getGenerateId(),
name: '实际请求',
script: '',
showScript: false,
isAssertion: false,
assertions: [],
children: [
{
script: realRequest,
showScript: true,
isAssertion: false,
assertions: [],
},
],
},
{
id: getGenerateId(),
name: '控制台',
script: '',
isAssertion: false,
showScript: false,
assertions: [],
children: [
{
script: props.console,
showScript: true,
isAssertion: false,
assertions: [],
},
],
},
{
id: getGenerateId(),
name: '提取',
script: '',
isAssertion: false,
showScript: false,
assertions: [],
children: [
{
script: extractValue,
showScript: true,
isAssertion: false,
assertions: [],
},
],
},
{
id: getGenerateId(),
name: '断言',
script: '',
showScript: false,
isAssertion: false,
assertions: [],
children: [
{
script: '',
showScript: false,
isAssertion: true,
assertions: assertionList,
},
],
},
];
}
function setResValue(list: ReportStepDetail[]) {
for (let i = 0; i < list.length; i++) {
const currentStepId = list[i].id as string;
const paramsObj = getRequestItem(list[i]);
setTableMaps(currentStepId, paramsObj);
if (list[i].content?.subRequestResults && list[i].content?.subRequestResults.length) {
setResValue(list[i].content?.subRequestResults);
}
}
}
const reportDetailMap = {
API: {
stepDetail: reportStepDetail,
},
CASE: {
stepDetail: reportCaseStepDetail,
},
};
/**
* 获取步骤详情
*/
const activeStepId = ref<string>('');
const activeStepDetail = ref<ReportStepDetail>({});
const activeIndex = ref<number>(0);
async function getStepDetail() {
try {
const result = await reportDetailMap[props.showType].stepDetail(
props.scenarioDetail.reportId as string,
props.scenarioDetail.stepId as string
);
stepDetailInfo.value = cloneDeep(result) as ReportStepDetail[];
activeStepId.value = stepDetailInfo.value[0].id as string;
activeStepDetail.value = stepDetailInfo.value.find((item) => item.id === activeStepId.value) || {};
setResValue(stepDetailInfo.value);
propsRes.value.data = tempStepMap.value[activeStepId.value];
} catch (error) {
console.log(error);
}
}
watchEffect(() => {
if (props.scenarioDetail.reportId && props.scenarioDetail.stepId) {
getStepDetail();
}
});
const showCondition = ref<string[]>([
'API',
'API_CASE',
' CUSTOM_REQUEST',
' LOOP_CONTROLLER',
'IF_CONTROLLER',
'ONCE_ONLY_CONTROLLER',
]);
</script>
<style scoped lang="less">

View File

@ -0,0 +1,395 @@
<template>
<div class="flex h-full flex-col gap-[16px]">
<a-spin class="max-h-[calc(100%-46px)] w-full" :loading="loading">
<MsTree
ref="treeRef"
v-model:selected-keys="selectedKeys"
v-model:data="steps"
:expand-all="props.expandAll"
:field-names="{ title: 'name', key: 'stepId', children: 'children' }"
:virtual-list-props="{
height: `calc(100vh - 406px)`,
threshold: 20,
fixedSize: true,
buffer: 15,
}"
title-class="step-tree-node-title"
node-highlight-class="step-tree-node-focus"
action-on-node-click="expand"
disabled-title-tooltip
block-node
@select="(selectedKeys, node) => handleStepSelect(selectedKeys, node as ScenarioItemType)"
@expand="handleStepExpand"
@more-actions-close="() => setFocusNodeKey('')"
>
<template #title="step">
<div class="flex w-full items-center gap-[8px]">
<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>
<div class="step-node-content flex justify-between">
<div class="flex items-center">
<!-- 步骤展开折叠按钮 -->
<a-tooltip
v-if="step.children?.length > 0"
:content="
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
: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>
</a-tooltip>
<!-- 展开折叠控制器 -->
<div v-if="getShowExpand(step)" class="mx-1">
<span
v-if="step.fold"
class="collapsebtn flex items-center justify-center"
@click.stop="expandHandler(step)"
>
<icon-right class="text-[var(--color-text-4)]" :style="{ 'font-size': '12px' }" />
</span>
<span v-else class="expand flex items-center justify-center" @click.stop="expandHandler(step)">
<icon-down class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }" />
</span>
</div>
<ConditionStatus
v-if="props.showType === 'API' && showCondition.includes(step.stepType)"
class="mx-1"
:status="step.stepType || ''"
/>
<a-tooltip :content="step.name">
<div class="step-name-container" @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>
<div class="flex">
<MsTag class="cursor-pointer" :type="step.status === 'SUCCESS' ? 'success' : 'danger'" theme="light">
{{ step.status === 'SUCCESS' ? t('report.detail.api.pass') : t('report.detail.api.resError') }}
</MsTag>
<span class="statusCode mx-2">
<div class="mr-2"> {{ t('report.detail.api.statusCode') }}</div>
<a-popover position="left" content-class="response-popover-content">
<div 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 class="resTime">
{{ t('report.detail.api.responseTime') }}
<span class="resTimeCount ml-2">{{ step.requestTime || 0 }}ms</span></span
>
<span class="resSize">
{{ t('report.detail.api.responseSize') }}
<span class="resTimeCount ml-2">{{ step.responseSize || 0 }} bytes</span></span
>
</div>
</div>
<div v-if="!step.fold" class="line"></div>
</div>
<!-- 折叠展开内容 -->
<div v-if="showResContent(step)" class="mt-4 h-[210px] pl-2">
<a-scrollbar
:style="{
overflow: 'auto',
height: '210px',
width: '100%',
}"
>
<StepDetailContent
:mode="props.activeType"
:step-item="step"
:console="props.console"
:is-definition="true"
:environment-name="props.environmentName"
:show-type="props.showType"
:is-response-model="true"
:report-id="props?.reportId"
/>
</a-scrollbar>
</div>
</template>
</MsTree>
</a-spin>
</div>
</template>
<script setup lang="ts">
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 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 { findNodeByKey, mapTree } from '@/utils';
import type { ScenarioItemType } from '@/models/apiTest/report';
import { ScenarioStepType } from '@/enums/apiEnum';
const { t } = useI18n();
const props = defineProps<{
stepKeyword?: string;
expandAll?: boolean;
showType: 'API' | 'CASE';
activeType: 'tiled' | 'tab';
console?: string;
environmentName?: string;
reportId?: string;
}>();
const loading = ref(false);
const treeRef = ref<InstanceType<typeof MsTree>>();
const emit = defineEmits(['expand', 'detail']);
const steps = defineModel<ScenarioItemType[]>('steps', {
required: true,
});
/**
* 处理步骤展开折叠
*/
function handleStepExpand(data: MsTreeExpandedData) {
const realStep = findNodeByKey<ScenarioItemType>(steps.value, data.node?.id, 'id');
if (realStep) {
realStep.expanded = !realStep.expanded;
}
}
const selectedKeys = ref<(string | number)[]>([]);
const focusStepKey = ref<string>('');
function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioItemType) {
const offspringIds: string[] = [];
mapTree(step.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
selectedKeys.value = [step.stepId, ...offspringIds];
}
function setFocusNodeKey(id: string) {
focusStepKey.value = id || '';
}
function expandHandler(item: ScenarioItemType) {
const realStep = findNodeByKey<ScenarioItemType>(steps.value, item.stepId, 'stepId');
if (realStep) {
realStep.fold = !realStep.fold;
}
}
const showApiType = ref<string[]>([ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST]);
const showCondition = ref<string[]>([
ScenarioStepType.API,
ScenarioStepType.API_CASE,
ScenarioStepType.CUSTOM_REQUEST,
ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONCE_ONLY_CONTROLLER,
]);
function getShowExpand(item: ScenarioItemType) {
if (props.showType === 'API') {
return showApiType.value.includes(item.stepType) && props.activeType === 'tab';
}
return props.activeType === 'tab';
}
const activeItem = ref();
function showDetail(item: ScenarioItemType) {
if (props.activeType === 'tab') {
return;
}
if (!showApiType.value.includes(item.stepType)) {
return;
}
activeItem.value = item;
emit('detail', activeItem.value);
}
//
function statusCodeColor(code: string) {
if (code) {
const resCode = Number(code);
if (resCode >= 200 && resCode < 300) {
return 'rgb(var(--success-7)';
}
if (resCode >= 300 && resCode < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
}
return '';
}
function showResContent(item: ScenarioItemType) {
if (props.showType === 'API') {
return showApiType.value.includes(item.stepType) && props.activeType === 'tab' && !item.fold;
}
return props.activeType === 'tab' && !item.fold;
}
</script>
<style scoped lang="less">
// TODO:transform
.loop-levels(@index, @max) when (@index <= @max) {
:deep(.arco-tree-node[data-level='@{index}']) {
margin-left: @index * 32px;
}
.loop-levels(@index + 1, @max); //
}
.loop-levels(0, 99); //
:deep(.arco-tree-node) {
padding: 0 8px;
min-width: 1000px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-medium) !important;
&:not(:first-child) {
margin-top: 4px;
}
&:hover {
background-color: white !important;
.arco-tree-node-title {
background-color: white !important;
}
}
.arco-tree-node-title {
@apply !cursor-pointer bg-white;
padding: 12px 4px;
&:hover {
background-color: white !important;
}
.step-node-content {
@apply flex w-full flex-1 items-center;
gap: 8px;
margin-right: 6px;
}
.step-name-container {
@apply flex items-center;
margin-right: 16px;
&:hover {
.edit-script-name-icon {
@apply visible;
}
}
.edit-script-name-icon {
@apply invisible cursor-pointer;
color: rgb(var(--primary-5));
}
}
.arco-tree-node-title-text {
@apply flex-1;
}
}
.arco-tree-node-indent {
@apply hidden;
}
.arco-tree-node-switcher {
@apply hidden;
}
.arco-tree-node-drag-icon {
@apply hidden;
}
.ms-tree-node-extra {
gap: 4px;
background-color: white !important;
}
}
:deep(.arco-tree-node-selected) {
.arco-tree-node-title {
.step-tree-node-title {
font-weight: 400;
color: var(--color-text-1);
}
}
}
:deep(.step-tree-node-focus) {
background-color: white !important;
.arco-tree-node-title {
background-color: white;
}
}
.resTime,
.resSize,
.statusCode {
margin-right: 8px;
color: var(--color-text-4);
@apply flex;
.resTimeCount {
color: rgb(var(--success-6));
}
.code {
display: inline-block;
max-width: 60px;
text-overflow: ellipsis;
word-break: keep-all;
white-space: nowrap;
}
}
:deep(.expand) {
width: 16px;
height: 16px;
border-radius: 50%;
background: rgb(var(--primary-1));
}
:deep(.collapsebtn) {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-text-n8) !important;
@apply bg-white;
}
:deep(.arco-table-expand-btn) {
width: 16px;
height: 16px;
border: none;
border-radius: 50%;
background: var(--color-text-n8) !important;
}
.resContentWrapper {
border-top: 1px solid red;
border-radius: 0 0 6px 6px;
@apply mb-4 bg-white p-4;
.resContent {
height: 38px;
border-radius: 6px;
}
}
:deep(.ms-tree-container .ms-tree .arco-tree-node .arco-tree-node-title) {
background: white;
}
:deep(.ms-tree-container .ms-tree .arco-tree-node-selected) {
background: white;
}
.line {
position: absolute;
top: 48px;
width: 100%;
height: 1px;
background: var(--color-text-n8);
}
</style>

View File

@ -64,6 +64,11 @@
}>();
const { t } = useI18n();
const getCountTotal = computed(() => {
const { successCount, errorCount, fakeErrorCount, pendingCount } = props.reportDetail;
return successCount + errorCount + fakeErrorCount + pendingCount;
});
const colorData = computed(() => {
if (
props.reportDetail.status === 'ERROR' ||
@ -81,19 +86,19 @@
}
return [
{
percentage: (props.reportDetail.successCount / props.reportDetail.stepTotal) * 100,
percentage: (props.reportDetail.successCount / getCountTotal.value) * 100,
color: 'rgb(var(--success-6))',
},
{
percentage: (props.reportDetail.errorCount / props.reportDetail.stepTotal) * 100,
percentage: (props.reportDetail.errorCount / getCountTotal.value) * 100,
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.reportDetail.fakeErrorCount / props.reportDetail.stepTotal) * 100,
percentage: (props.reportDetail.fakeErrorCount / getCountTotal.value) * 100,
color: 'rgb(var(--warning-6))',
},
{
percentage: (props.reportDetail.pendingCount / props.reportDetail.stepTotal) * 100,
percentage: (props.reportDetail.pendingCount / getCountTotal.value) * 100,
color: 'var(--color-text-input-border)',
},
];

View File

@ -1,18 +0,0 @@
<template>
<div> </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const subRequestResults = ref([
{
requestName: '子请求001',
id: 1001,
reportId: '1001',
stepId: 'step1001',
},
]);
</script>
<style scoped></style>

View File

@ -1,29 +1,31 @@
<template>
<div
class="tiled-wrap"
class="tiled-wrap p-4"
:class="{
'border border-solid border-[var(--color-text-n8)]': props.showType === 'API',
}"
>
<a-scrollbar
<!-- <a-scrollbar
:style="{
overflow: 'auto',
height: 'calc(100vh - 424px)',
width: '100%',
}"
>
<ScenarioItem
v-if="tiledList.length > 0"
> -->
<!-- 步骤树 -->
<stepTree
ref="stepTreeRef"
v-model:steps="tiledList"
:show-type="props.showType"
:list="tiledList"
:show-border="true"
:active-type="props.activeType"
:expand-all="isExpandAll"
:console="props.reportDetail.console"
:environment-name="props.reportDetail.environmentName"
:report-id="props.reportDetail.id"
@detail="showDetail"
/>
<MsEmpty v-else />
</a-scrollbar>
<!-- </a-scrollbar> -->
<!-- 步骤抽屉 -->
<StepDrawer
v-model:visible="showStepDrawer"
:step-id="activeDetailId"
@ -32,6 +34,7 @@
:show-type="props.showType"
:console="props.reportDetail.console"
:environment-name="props.reportDetail.environmentName"
:report-id="props.reportDetail.id"
/>
</div>
</template>
@ -40,37 +43,33 @@
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsEmpty from '@/components/pure/ms-empty/index.vue';
import ScenarioItem from './scenarioItem.vue';
import StepDrawer from './step/stepDrawer.vue';
import StepTree from './step/stepTree.vue';
import { addLevelToTree } from '@/utils';
import type { ReportDetail, ScenarioDetailItem, ScenarioItemType } from '@/models/apiTest/report';
import type { ReportDetail, ScenarioItemType } from '@/models/apiTest/report';
import { addFoldField } from '../utils';
const props = defineProps<{
reportDetail: ReportDetail;
activeType: string; // |tab
activeType: 'tiled' | 'tab'; // |tab
showType: 'API' | 'CASE'; // |
}>();
const tiledList = ref<ScenarioItemType[]>([]);
watchEffect(() => {
if (props.reportDetail && props.reportDetail.children) {
tiledList.value = props.reportDetail.children || [];
tiledList.value = addLevelToTree<ScenarioItemType>(tiledList.value) as ScenarioItemType[];
tiledList.value.forEach((item) => {
addFoldField(item);
});
}
});
const isExpandAll = ref(false); //
const showStepDrawer = ref<boolean>(false);
const activeDetailId = ref<string>('');
const activeStepIndex = ref<number>(0);
const scenarioDetail = ref<ScenarioDetailItem>({});
const scenarioDetail = ref<ScenarioItemType>();
/**
* 步骤详情
*/
function showDetail(item: ScenarioItemType) {
showStepDrawer.value = true;
scenarioDetail.value = cloneDeep(item);
@ -81,6 +80,16 @@
onMounted(() => {
tiledList.value = addLevelToTree<ScenarioItemType>(tiledList.value) as ScenarioItemType[];
});
watchEffect(() => {
if (props.reportDetail && props.reportDetail.children) {
tiledList.value = props.reportDetail.children || [];
tiledList.value = addLevelToTree<ScenarioItemType>(tiledList.value) as ScenarioItemType[];
tiledList.value.forEach((item) => {
addFoldField(item);
});
}
});
</script>
<style scoped lang="less">

View File

@ -58,10 +58,12 @@ export default {
'report.detail.api.resSuccess': 'Success',
'report.detail.api.resError': 'Error',
'report.detail.api.apiCase': 'Api Case',
'report.detail.api': 'Api',
'report.detail.api.resBody': 'response body',
'report.detail.api.resHeader': 'headers',
'report.detail.api.realReq': 'real request',
'report.detail.api.console': 'console',
'report.detail.api.extract': 'extract',
'report.detail.api.assert': 'assertion',
'report.detail.api.subRequest': 'Sub-request',
};

View File

@ -16,7 +16,7 @@ export default {
'report.fake.error': '误报',
'report.status.running': '执行中',
'report.status.rerunning': '重跑中',
'report.status.pending': '排队中',
'report.status.pending': '未执行',
'report.stopped': '停止',
'report.trigger.scheduled': '定时执行',
'report.trigger.manual': '手工执行',
@ -57,10 +57,12 @@ export default {
'report.detail.api.resSuccess': '成功',
'report.detail.api.resError': '失败',
'report.detail.api.apiCase': '接口用例',
'report.detail.api': '接口定义',
'report.detail.api.resBody': '响应体',
'report.detail.api.resHeader': '响应头',
'report.detail.api.realReq': '实际请求',
'report.detail.api.console': '控制台',
'report.detail.api.extract': '提取',
'report.detail.api.assert': '断言',
'report.detail.api.subRequest': '子请求',
};

View File

@ -0,0 +1,108 @@
<template>
<caseReportCom :detail-info="detail" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import caseReportCom from './component/caseReportCom.vue';
import { getShareReportInfo, reportCaseDetail } from '@/api/modules/api-test/report';
import type { ReportDetail } from '@/models/apiTest/report';
const detail = ref<ReportDetail>();
const route = useRoute();
async function getDetail() {
try {
const res = await getShareReportInfo(route.query.shareId as string);
detail.value = await reportCaseDetail(res.reportId, route.query.shareId as string);
} catch (error) {
console.log(error);
}
}
watchEffect(() => {
if (route.query.shareId) {
getDetail();
}
});
</script>
<style scoped lang="less">
.report-container {
padding: 16px;
height: calc(100vh - 56px);
background: var(--color-text-n9);
.report-header {
padding: 0 16px;
height: 54px;
border-radius: 4px;
background: white;
@apply mb-4 bg-white;
}
.analyze {
min-height: 196px;
max-height: 200px;
border-radius: 4px;
@apply mb-2 flex justify-between bg-white;
.request-analyze {
@apply flex flex-1 flex-col p-4;
.chart-legend {
.chart-legend-item {
@apply grid grid-cols-3 gap-2;
}
.chart-flag {
@apply flex items-center;
.count {
color: var(--color-text-1);
}
}
}
}
.time-analyze {
@apply flex flex-1 flex-col p-4;
.time-card {
@apply flex items-center justify-between;
.time-card-item {
border-radius: 6px;
background: var(--color-text-n9);
@apply mt-4 flex flex-1 flex-grow items-center px-4;
.time-card-item-title {
color: var(--color-text-4);
}
.count {
font-size: 18px;
@apply mx-2 font-medium;
}
}
.time-card-item-rote {
border-radius: 6px;
background: var(--color-text-n9);
@apply mt-4 flex flex-1 flex-grow flex-col p-4;
.time-card-item-rote-title {
color: var(--color-text-4);
@apply mb-2;
}
.count {
font-size: 18px;
@apply mx-2 font-medium;
}
}
}
}
}
.report-info {
padding: 16px;
border-radius: 4px;
@apply bg-white;
}
}
.block-title {
font-size: 14px;
@apply font-medium;
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<ShareCom :detail-info="detail" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import ShareCom from './component/scenarioCom.vue';
import { getShareReportInfo, reportScenarioDetail } from '@/api/modules/api-test/report';
import type { ReportDetail } from '@/models/apiTest/report';
const detail = ref<ReportDetail>();
const route = useRoute();
async function getDetail() {
try {
const res = await getShareReportInfo(route.query.shareId as string);
detail.value = await reportScenarioDetail(res.reportId, route.query.shareId as string);
} catch (error) {
console.log(error);
}
}
watchEffect(() => {
if (route.query.shareId) {
getDetail();
}
});
</script>
<style scoped></style>

View File

@ -218,11 +218,12 @@
* 表格单行选中事件处理
*/
function handleRowSelectChange(key: string) {
const selectedData = currentTable.value.propsRes.value.data.find((e) => e.id === key);
const selectedData = currentTable.value.propsRes.value.data.find((e: any) => e.id === key);
if (tableSelectedKeys.value.includes(key)) {
//
tableSelectedData.value = tableSelectedData.value.filter((e) => e.id !== key);
} else if (selectedData) {
}
if (selectedData) {
tableSelectedData.value.push(selectedData);
}
emit('select', tableSelectedData.value);

View File

@ -54,9 +54,9 @@
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
icon: 'icon-icon_block_filled',
label: 'project.taskCenter.queuing',
color: '!text-[rgb(var(--link-6))]',
color: '!text-[var(--color-text-input-border)]',
},
},
[TaskCenterEnum.API_SCENARIO]: {
@ -88,9 +88,9 @@
color: '!text-[rgb(var(--link-6))]',
},
PENDING: {
icon: 'icon-icon_wait',
icon: 'icon-icon_block_filled',
label: 'project.taskCenter.queuing',
color: '!text-[rgb(var(--link-6))]',
color: '!text-[var(--color-text-input-border)]',
},
},
[TaskCenterEnum.LOAD_TEST]: {
@ -124,9 +124,9 @@
},
[TaskCenterEnum.UI_TEST]: {
PENDING: {
icon: 'icon-icon_wait',
icon: 'icon-icon_block_filled',
label: 'project.taskCenter.queuing',
color: '!text-[rgb(var(--link-6))]',
color: '!text-[var(--color-text-input-border)]',
},
RUNNING: {
icon: 'icon-icon_testing',
@ -154,9 +154,9 @@
},
[TaskCenterEnum.TEST_PLAN]: {
RUNNING: {
icon: 'icon-icon_testing',
icon: 'icon-icon_block_filled',
label: 'project.taskCenter.queuing',
color: '!text-[rgb(var(--link-6))]',
color: '!text-[var(--color-text-input-border)]',
},
SUCCESS: {
icon: 'icon-icon_succeed_colorful',

View File

@ -31,7 +31,7 @@ export default {
'project.taskCenter.falseAlarm': 'False alarm',
'project.taskCenter.inExecution': 'In execution',
'project.taskCenter.rerun': 'Rerun',
'project.taskCenter.queuing': 'Be queuing',
'project.taskCenter.queuing': 'pending',
'project.taskCenter.starting': 'Be starting',
'project.taskCenter.complete': 'complete',
'project.taskCenter.scheduledTask': 'Scheduled task',

View File

@ -31,7 +31,7 @@ export default {
'project.taskCenter.falseAlarm': '误报',
'project.taskCenter.inExecution': '执行中',
'project.taskCenter.rerun': '重跑中',
'project.taskCenter.queuing': '排队中',
'project.taskCenter.queuing': '未执行',
'project.taskCenter.starting': '启动中',
'project.taskCenter.complete': '完成',
'project.taskCenter.scheduledTask': '定时任务',