feat(接口测试): 报告详情_报告详情联调100%
This commit is contained in:
parent
1b95d82b6a
commit
6736cc5f17
|
@ -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 {};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -406,6 +406,7 @@ export interface RequestResult {
|
|||
url: string;
|
||||
method: string;
|
||||
responseResult: ResponseResult;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface RequestTaskResult {
|
||||
requestResults: RequestResult[]; // 请求结果
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
|
@ -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)"
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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)',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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': '子请求',
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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': '定时任务',
|
||||
|
|
Loading…
Reference in New Issue