feat(报告): 场景报告导出 pdf
This commit is contained in:
parent
e2429d0c94
commit
76c33c015e
|
@ -33,7 +33,7 @@
|
||||||
<slot name="tbutton"></slot>
|
<slot name="tbutton"></slot>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
<div class="right-operation-button-icon">
|
<div class="ms-drawer-right-operation-button">
|
||||||
<MsButton
|
<MsButton
|
||||||
v-if="props.showFullScreen"
|
v-if="props.showFullScreen"
|
||||||
type="icon"
|
type="icon"
|
||||||
|
@ -286,10 +286,13 @@
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
.right-operation-button-icon .ms-button-icon {
|
.ms-drawer-right-operation-button {
|
||||||
|
.ms-button-icon,
|
||||||
|
.ms-drawer-fullscreen-btn {
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
.arco-icon {
|
.arco-icon,
|
||||||
|
.ms-drawer-fullscreen-btn-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
}
|
}
|
||||||
|
@ -301,6 +304,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.arco-drawer-close-btn {
|
.arco-drawer-close-btn {
|
||||||
@apply flex items-center;
|
@apply flex items-center;
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,8 @@ export enum ShareEnum {
|
||||||
export enum FullPageEnum {
|
export enum FullPageEnum {
|
||||||
FULL_PAGE = 'fullPage',
|
FULL_PAGE = 'fullPage',
|
||||||
FULL_PAGE_TEST_PLAN_EXPORT_PDF = 'fullPageTestPlanExportPDF',
|
FULL_PAGE_TEST_PLAN_EXPORT_PDF = 'fullPageTestPlanExportPDF',
|
||||||
|
FULL_PAGE_SCENARIO_EXPORT_PDF = 'fullPageScenarioExportPDF',
|
||||||
|
FULL_PAGE_CASE_EXPORT_PDF = 'fullPageCaseExportPDF',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RouteEnum = {
|
export const RouteEnum = {
|
||||||
|
|
|
@ -1,62 +1,11 @@
|
||||||
import '@/assets/fonts/AlibabaPuHuiTi-3-55-Regular-normal';
|
import '@/assets/fonts/AlibabaPuHuiTi-3-55-Regular-normal';
|
||||||
|
|
||||||
import { Canvg } from 'canvg';
|
import { replaceSvgWithBase64 } from '@/utils/exportPdf';
|
||||||
|
|
||||||
import html2canvas from 'html2canvas-pro';
|
import html2canvas from 'html2canvas-pro';
|
||||||
import JSPDF from 'jspdf';
|
import JSPDF from 'jspdf';
|
||||||
import autoTable, { UserOptions } from 'jspdf-autotable';
|
import autoTable, { UserOptions } from 'jspdf-autotable';
|
||||||
|
|
||||||
/**
|
|
||||||
* 替换svg为base64
|
|
||||||
*/
|
|
||||||
async function inlineSvgUseElements(container: HTMLElement) {
|
|
||||||
const useElements = container.querySelectorAll('use');
|
|
||||||
useElements.forEach((useElement) => {
|
|
||||||
const href = useElement.getAttribute('xlink:href') || useElement.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const symbolId = href.substring(1);
|
|
||||||
const symbol = document.getElementById(symbolId);
|
|
||||||
if (symbol) {
|
|
||||||
const svgElement = useElement.closest('svg');
|
|
||||||
if (svgElement) {
|
|
||||||
svgElement.innerHTML = symbol.innerHTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将svg转换为base64
|
|
||||||
*/
|
|
||||||
async function convertSvgToBase64(svgElement: SVGSVGElement) {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const svgString = new XMLSerializer().serializeToString(svgElement);
|
|
||||||
if (ctx) {
|
|
||||||
const v = Canvg.fromString(ctx, svgString);
|
|
||||||
canvas.width = svgElement.clientWidth;
|
|
||||||
canvas.height = svgElement.clientHeight;
|
|
||||||
await v.render();
|
|
||||||
}
|
|
||||||
return canvas.toDataURL('image/png');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 替换svg为base64
|
|
||||||
*/
|
|
||||||
async function replaceSvgWithBase64(container: HTMLElement) {
|
|
||||||
await inlineSvgUseElements(container);
|
|
||||||
const svgElements = container.querySelectorAll('.c-icon');
|
|
||||||
svgElements.forEach(async (svgElement) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = await convertSvgToBase64(svgElement as SVGSVGElement);
|
|
||||||
img.width = svgElement.clientWidth;
|
|
||||||
img.height = svgElement.clientHeight;
|
|
||||||
img.style.marginRight = '8px';
|
|
||||||
svgElement.parentNode?.replaceChild(img, svgElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const A4_WIDTH = 595;
|
const A4_WIDTH = 595;
|
||||||
const A4_HEIGHT = 842;
|
const A4_HEIGHT = 842;
|
||||||
const HEADER_HEIGHT = 16;
|
const HEADER_HEIGHT = 16;
|
||||||
|
@ -64,7 +13,7 @@ const FOOTER_HEIGHT = 16;
|
||||||
export const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
export const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
||||||
export const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
export const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
||||||
export const CONTAINER_WIDTH = 1190;
|
export const CONTAINER_WIDTH = 1190;
|
||||||
export const SCALE_RATIO = 1.5;
|
export const SCALE_RATIO = window.devicePixelRatio * 1.5;
|
||||||
export const PAGE_PDF_WIDTH_RATIO = CONTAINER_WIDTH / PDF_WIDTH; // 页面容器宽度与 pdf 宽度的比例
|
export const PAGE_PDF_WIDTH_RATIO = CONTAINER_WIDTH / PDF_WIDTH; // 页面容器宽度与 pdf 宽度的比例
|
||||||
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 SCALE_RATIO 倍的清晰度)
|
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 SCALE_RATIO 倍的清晰度)
|
||||||
export const IMAGE_HEIGHT = Math.ceil(PAGE_HEIGHT * PAGE_PDF_WIDTH_RATIO * SCALE_RATIO);
|
export const IMAGE_HEIGHT = Math.ceil(PAGE_HEIGHT * PAGE_PDF_WIDTH_RATIO * SCALE_RATIO);
|
||||||
|
@ -110,7 +59,7 @@ export default async function exportPDF(
|
||||||
width: CONTAINER_WIDTH,
|
width: CONTAINER_WIDTH,
|
||||||
height: element.clientHeight,
|
height: element.clientHeight,
|
||||||
backgroundColor: '#f9f9fe',
|
backgroundColor: '#f9f9fe',
|
||||||
scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
|
scale: SCALE_RATIO, // 缩放增加清晰度
|
||||||
});
|
});
|
||||||
pdf.setFont('AlibabaPuHuiTi-3-55-Regular');
|
pdf.setFont('AlibabaPuHuiTi-3-55-Regular');
|
||||||
pdf.setFontSize(10);
|
pdf.setFontSize(10);
|
||||||
|
|
|
@ -14,6 +14,11 @@ const FullPage: AppRouteRecordRaw = {
|
||||||
name: FullPageEnum.FULL_PAGE_TEST_PLAN_EXPORT_PDF,
|
name: FullPageEnum.FULL_PAGE_TEST_PLAN_EXPORT_PDF,
|
||||||
component: () => import('@/views/test-plan/report/detail/exportPDF.vue'),
|
component: () => import('@/views/test-plan/report/detail/exportPDF.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'scenarioExportPDF',
|
||||||
|
name: FullPageEnum.FULL_PAGE_SCENARIO_EXPORT_PDF,
|
||||||
|
component: () => import('@/views/api-test/report/exportPDF.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ const FOOTER_HEIGHT = 16;
|
||||||
const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
||||||
const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
||||||
const CONTAINER_WIDTH = 1190;
|
const CONTAINER_WIDTH = 1190;
|
||||||
export const SCALE_RATIO = 1.2;
|
export const SCALE_RATIO = window.devicePixelRatio * 1.2;
|
||||||
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 SCALE_RATIO 倍的清晰度)
|
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 SCALE_RATIO 倍的清晰度)
|
||||||
export const IMAGE_HEIGHT = Math.ceil(PAGE_HEIGHT * (CONTAINER_WIDTH / PDF_WIDTH) * SCALE_RATIO);
|
export const IMAGE_HEIGHT = Math.ceil(PAGE_HEIGHT * (CONTAINER_WIDTH / PDF_WIDTH) * SCALE_RATIO);
|
||||||
export const MAX_CANVAS_HEIGHT = IMAGE_HEIGHT * 25; // 一次截图最大高度是 20 页整(过长会无法截完整,出现空白)
|
export const MAX_CANVAS_HEIGHT = IMAGE_HEIGHT * 20; // 一次截图最大高度是 20 页整(过长会无法截完整,出现空白)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 替换svg为base64
|
* 替换svg为base64
|
||||||
|
@ -53,7 +53,7 @@ async function convertSvgToBase64(svgElement: SVGSVGElement) {
|
||||||
/**
|
/**
|
||||||
* 替换svg为base64
|
* 替换svg为base64
|
||||||
*/
|
*/
|
||||||
async function replaceSvgWithBase64(container: HTMLElement) {
|
export async function replaceSvgWithBase64(container: HTMLElement) {
|
||||||
await inlineSvgUseElements(container);
|
await inlineSvgUseElements(container);
|
||||||
const svgElements = container.querySelectorAll('.c-icon');
|
const svgElements = container.querySelectorAll('.c-icon');
|
||||||
svgElements.forEach(async (svgElement) => {
|
svgElements.forEach(async (svgElement) => {
|
||||||
|
@ -107,7 +107,7 @@ export default async function exportPDF(name: string, contentId: string) {
|
||||||
width: CONTAINER_WIDTH,
|
width: CONTAINER_WIDTH,
|
||||||
height: screenshotHeight,
|
height: screenshotHeight,
|
||||||
backgroundColor: '#f9f9fe',
|
backgroundColor: '#f9f9fe',
|
||||||
scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
|
scale: SCALE_RATIO, // 缩放增加清晰度
|
||||||
});
|
});
|
||||||
screenshotList.push(canvas);
|
screenshotList.push(canvas);
|
||||||
position += screenshotHeight;
|
position += screenshotHeight;
|
||||||
|
|
|
@ -9,15 +9,16 @@
|
||||||
show-full-screen
|
show-full-screen
|
||||||
>
|
>
|
||||||
<template #tbutton>
|
<template #tbutton>
|
||||||
|
<div class="ms-drawer-right-operation-button flex items-center">
|
||||||
<a-dropdown v-if="!props.doNotShowShare" position="br" @select="shareHandler">
|
<a-dropdown v-if="!props.doNotShowShare" position="br" @select="shareHandler">
|
||||||
<MsButton
|
<MsButton
|
||||||
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||||
type="icon"
|
type="icon"
|
||||||
status="secondary"
|
status="secondary"
|
||||||
class="mr-4 !rounded-[var(--border-radius-small)]"
|
class="mr-4 !rounded-[var(--border-radius-small)] text-[var(--color-text-1)]"
|
||||||
@click="shareHandler"
|
@click="shareHandler"
|
||||||
>
|
>
|
||||||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px] text-[var(--color-text-1)]" />
|
||||||
{{ t('common.share') }}
|
{{ t('common.share') }}
|
||||||
</MsButton>
|
</MsButton>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
@ -27,6 +28,17 @@
|
||||||
</a-doption>
|
</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
|
<MsButton
|
||||||
|
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="mr-4 !rounded-[var(--border-radius-small)] text-[var(--color-text-1)]"
|
||||||
|
@click="exportHandler"
|
||||||
|
>
|
||||||
|
<MsIcon type="icon-icon_bottom-align_outlined" class="mr-2 font-[16px] text-[var(--color-text-1)]" />
|
||||||
|
{{ t('common.export') }}
|
||||||
|
</MsButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<CaseReportCom
|
<CaseReportCom
|
||||||
v-if="!props.isScenario"
|
v-if="!props.isScenario"
|
||||||
|
@ -50,10 +62,11 @@
|
||||||
|
|
||||||
import { getShareInfo, getShareTime, reportCaseDetail, reportScenarioDetail } from '@/api/modules/api-test/report';
|
import { getShareInfo, getShareTime, reportCaseDetail, reportScenarioDetail } from '@/api/modules/api-test/report';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
import type { ReportDetail } from '@/models/apiTest/report';
|
import type { ReportDetail } from '@/models/apiTest/report';
|
||||||
import { RouteEnum } from '@/enums/routeEnum';
|
import { FullPageEnum, RouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reportId: string;
|
reportId: string;
|
||||||
|
@ -67,6 +80,7 @@
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { copy, isSupported } = useClipboard({ legacy: true });
|
const { copy, isSupported } = useClipboard({ legacy: true });
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { openNewPage } = useOpenNewPage();
|
||||||
|
|
||||||
const innerVisible = defineModel<boolean>('visible', {
|
const innerVisible = defineModel<boolean>('visible', {
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -188,6 +202,16 @@
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportHandler() {
|
||||||
|
openNewPage(
|
||||||
|
props.isScenario ? FullPageEnum.FULL_PAGE_SCENARIO_EXPORT_PDF : FullPageEnum.FULL_PAGE_CASE_EXPORT_PDF,
|
||||||
|
{
|
||||||
|
id: props.reportId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!props.doNotShowShare) {
|
if (!props.doNotShowShare) {
|
||||||
getTime();
|
getTime();
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
@change="loadControlLoop"
|
@change="loadControlLoop"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full items-center justify-between rounded bg-[var(--color-text-n9)] p-4">
|
<div class="flex w-full items-center justify-between rounded bg-[var(--color-text-n9)] px-[16px] py-[8px]">
|
||||||
<div class="font-medium">
|
<div class="font-medium">
|
||||||
<span
|
<span
|
||||||
:class="{ 'text-[rgb(var(--primary-5))]': activeType === 'ResContent' }"
|
:class="{ 'text-[rgb(var(--primary-5))]': activeType === 'ResContent' }"
|
||||||
|
|
|
@ -257,7 +257,6 @@
|
||||||
|
|
||||||
.ms-scroll-bar();
|
.ms-scroll-bar();
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-table-before {
|
.history-table-before {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
@loaded="loadedReport"
|
@loaded="loadedReport"
|
||||||
>
|
>
|
||||||
<template #titleRight="{ loading }">
|
<template #titleRight="{ loading }">
|
||||||
<div class="rightButtons flex items-center">
|
<div class="ms-drawer-right-operation-button flex items-center">
|
||||||
<MsButton
|
<MsButton
|
||||||
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||||
type="icon"
|
type="icon"
|
||||||
status="secondary"
|
status="secondary"
|
||||||
class="mr-4 !rounded-[var(--border-radius-small)]"
|
class="!rounded-[var(--border-radius-small)]"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:loading="shareLoading"
|
:loading="shareLoading"
|
||||||
@click="shareHandler"
|
@click="shareHandler"
|
||||||
|
@ -29,6 +29,16 @@
|
||||||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
||||||
{{ t('common.share') }}
|
{{ t('common.share') }}
|
||||||
</MsButton>
|
</MsButton>
|
||||||
|
<MsButton
|
||||||
|
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||||
|
type="icon"
|
||||||
|
status="secondary"
|
||||||
|
class="mr-4 !rounded-[var(--border-radius-small)]"
|
||||||
|
@click="exportHandler"
|
||||||
|
>
|
||||||
|
<MsIcon type="icon-icon_bottom-align_outlined" class="mr-2 font-[16px]" />
|
||||||
|
{{ t('common.export') }}
|
||||||
|
</MsButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ loading }">
|
<template #default="{ loading }">
|
||||||
|
@ -52,14 +62,16 @@
|
||||||
|
|
||||||
import { getShareInfo, reportScenarioDetail } from '@/api/modules/api-test/report';
|
import { getShareInfo, reportScenarioDetail } from '@/api/modules/api-test/report';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
|
||||||
import type { ReportDetail } from '@/models/apiTest/report';
|
import type { ReportDetail } from '@/models/apiTest/report';
|
||||||
import { RouteEnum } from '@/enums/routeEnum';
|
import { FullPageEnum, RouteEnum } from '@/enums/routeEnum';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { copy, isSupported } = useClipboard({ legacy: true });
|
const { copy, isSupported } = useClipboard({ legacy: true });
|
||||||
|
const { openNewPage } = useOpenNewPage();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -162,14 +174,16 @@
|
||||||
Message.error(t('common.copyNotSupport'));
|
Message.error(t('common.copyNotSupport'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 导出
|
function exportHandler() {
|
||||||
*/
|
openNewPage(FullPageEnum.FULL_PAGE_SCENARIO_EXPORT_PDF, {
|
||||||
const exportLoading = ref<boolean>(false);
|
id: props.reportId,
|
||||||
function exportHandler() {}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
|
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
|
||||||
|
|
||||||
|
@ -186,7 +200,6 @@
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.report-container {
|
.report-container {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
height: calc(100vh - 56px);
|
|
||||||
background: var(--color-text-n9);
|
background: var(--color-text-n9);
|
||||||
.report-header {
|
.report-header {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="report-container h-full">
|
<div class="report-container">
|
||||||
<!-- 报告参数开始 -->
|
<!-- 报告参数开始 -->
|
||||||
<ReportDetailHeader :detail="detail" show-type="API" />
|
<ReportDetailHeader :detail="detail" show-type="API" />
|
||||||
<!-- 报告参数结束 -->
|
<!-- 报告参数结束 -->
|
||||||
|
@ -35,13 +35,19 @@
|
||||||
<!-- 报告分析,报告步骤分析和请求分析结束 -->
|
<!-- 报告分析,报告步骤分析和请求分析结束 -->
|
||||||
<!-- 报告明细开始 -->
|
<!-- 报告明细开始 -->
|
||||||
<div class="report-info">
|
<div class="report-info">
|
||||||
<reportInfoHeader v-model:keyword="cascaderKeywords" v-model:active-tab="activeTab" show-type="API" />
|
<reportInfoHeader
|
||||||
|
v-model:keyword="cascaderKeywords"
|
||||||
|
v-model:active-tab="activeTab"
|
||||||
|
show-type="API"
|
||||||
|
:is-export="props.isExport"
|
||||||
|
/>
|
||||||
<TiledList
|
<TiledList
|
||||||
:key-words="cascaderKeywords"
|
:key-words="cascaderKeywords"
|
||||||
show-type="API"
|
show-type="API"
|
||||||
:get-report-step-detail="props.getReportStepDetail"
|
:get-report-step-detail="props.getReportStepDetail"
|
||||||
:active-type="activeTab"
|
:active-type="activeTab"
|
||||||
:report-detail="detail || []"
|
:report-detail="detail || []"
|
||||||
|
:is-export="props.isExport"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 报告明细结束 -->
|
<!-- 报告明细结束 -->
|
||||||
|
@ -71,6 +77,7 @@
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
detailInfo?: ReportDetail;
|
detailInfo?: ReportDetail;
|
||||||
getReportStepDetail?: (...args: any) => Promise<any>; // 获取步骤的详情内容接口
|
getReportStepDetail?: (...args: any) => Promise<any>; // 获取步骤的详情内容接口
|
||||||
|
isExport?: boolean; // 是否是导出pdf预览
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const detail = ref<ReportDetail>({
|
const detail = ref<ReportDetail>({
|
||||||
|
@ -170,6 +177,7 @@
|
||||||
legend: {
|
legend: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
|
animation: !props.isExport, // pdf预览需要关闭渲染动画
|
||||||
series: {
|
series: {
|
||||||
name: '',
|
name: '',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
|
@ -317,7 +325,6 @@
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.report-container {
|
.report-container {
|
||||||
height: calc(100vh - 56px);
|
|
||||||
background: var(--color-text-n9);
|
background: var(--color-text-n9);
|
||||||
.report-header {
|
.report-header {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
|
@ -0,0 +1,338 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col gap-[16px]">
|
||||||
|
<MsTree
|
||||||
|
ref="treeRef"
|
||||||
|
v-model:selected-keys="selectedKeys"
|
||||||
|
v-model:expanded-keys="innerExpandedKeys"
|
||||||
|
v-model:data="steps"
|
||||||
|
:field-names="{ title: 'name', key: 'stepId', children: 'children' }"
|
||||||
|
title-class="step-tree-node-title"
|
||||||
|
node-highlight-class="step-tree-node-focus"
|
||||||
|
:animation="false"
|
||||||
|
action-on-node-click="expand"
|
||||||
|
disabled-title-tooltip
|
||||||
|
block-node
|
||||||
|
hide-switcher
|
||||||
|
>
|
||||||
|
<template #title="step">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<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">
|
||||||
|
<div class="flex flex-1 items-center">
|
||||||
|
<!-- 步骤展开折叠按钮 -->
|
||||||
|
<!-- <div
|
||||||
|
v-if="step.children && step.children.length"
|
||||||
|
class="flex cursor-pointer items-center gap-[2px] text-[var(--color-text-4)]"
|
||||||
|
>
|
||||||
|
<MsIcon :type="'icon-icon_split_turn-down_arrow'" :size="14" />
|
||||||
|
<span class="mx-1"> {{ step.children?.length || 0 }}</span>
|
||||||
|
</div> -->
|
||||||
|
<!-- 展开折叠控制器 -->
|
||||||
|
<div v-show="getShowExpand(step)" class="mx-1">
|
||||||
|
<span v-if="step.fold" class="collapsebtn flex items-center justify-center">
|
||||||
|
<icon-right class="text-[var(--color-text-4)]" :style="{ 'font-size': '12px' }" />
|
||||||
|
</span>
|
||||||
|
<span v-else class="expand flex items-center justify-center">
|
||||||
|
<icon-down class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.showType === 'API' && showCondition.includes(step.stepType)" class="flex-shrink-0">
|
||||||
|
<ConditionStatus class="mx-1" :status="step.stepType || ''" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="step-name-container"
|
||||||
|
:class="{
|
||||||
|
'w-full flex-grow': showApiType.includes(step.stepType),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="mx-[4px] max-w-[400px] break-all text-[var(--color-text-1)]">
|
||||||
|
{{ step.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<stepStatus :status="step.status || 'PENDING'" />
|
||||||
|
<!-- 脚本报错 -->
|
||||||
|
<MsTag
|
||||||
|
v-if="step.scriptIdentifier"
|
||||||
|
type="primary"
|
||||||
|
theme="light"
|
||||||
|
:self-style="{
|
||||||
|
color: 'rgb(var(--primary-3))',
|
||||||
|
background: 'rgb(var(--primary-1))',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<MsIcon type="icon-icon_info_outlined" class="mx-1 !text-[rgb(var(--primary-3))]" size="16" />
|
||||||
|
<span class="!text-[rgb(var(--primary-3))]">{{ t('report.detail.api.scriptErrorTip') }}</span>
|
||||||
|
</template>
|
||||||
|
</MsTag>
|
||||||
|
<div v-show="showStatus(step)" class="flex">
|
||||||
|
<div class="mx-2 flex items-center">
|
||||||
|
<div v-show="step.code" class="mr-2 text-[var(--color-text-4)]">
|
||||||
|
{{ t('report.detail.api.statusCode') }}</div
|
||||||
|
>
|
||||||
|
<div v-show="step.code" class="max-w-[200px]" :style="{ color: statusCodeColor(step.code) }">
|
||||||
|
{{ step.code || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="step.requestTime !== null" class="mr-2 flex items-center text-[var(--color-text-4)]">
|
||||||
|
{{ t('report.detail.api.responseTime') }}
|
||||||
|
<span class="ml-2 text-[rgb(var(--success-6))]">
|
||||||
|
{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[0] : '-' }}
|
||||||
|
{{ step.requestTime !== null ? formatDuration(step.requestTime).split('-')[1] : 'ms' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="step.responseSize !== null" class="flex items-center text-[var(--color-text-4)]">
|
||||||
|
{{ t('report.detail.api.responseSize') }}
|
||||||
|
<span class="ml-2 text-[rgb(var(--success-6))]"> {{ step.responseSize || 0 }} bytes </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="steps.length === 0" #empty>
|
||||||
|
<div
|
||||||
|
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-center text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
||||||
|
>
|
||||||
|
{{ t('common.noData') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsTree>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { formatDuration } from '@/utils';
|
||||||
|
|
||||||
|
import type { ScenarioItemType } from '@/models/apiTest/report';
|
||||||
|
import { ScenarioStepType } from '@/enums/apiEnum';
|
||||||
|
|
||||||
|
const stepStatus = defineAsyncComponent(() => import('./stepStatus.vue'));
|
||||||
|
const ConditionStatus = defineAsyncComponent(() => import('@/views/api-test/report/component/conditionStatus.vue'));
|
||||||
|
const MsTag = defineAsyncComponent(() => import('@/components/pure/ms-tag/ms-tag.vue'));
|
||||||
|
const MsTree = defineAsyncComponent(() => import('@/components/business/ms-tree/index.vue'));
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps<{
|
||||||
|
stepKeyword?: string;
|
||||||
|
expandAll?: boolean;
|
||||||
|
showType: 'API' | 'CASE';
|
||||||
|
activeType: 'tiled' | 'tab';
|
||||||
|
console?: string;
|
||||||
|
reportId?: string;
|
||||||
|
expandedKeys: (string | number)[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||||
|
|
||||||
|
const steps = defineModel<ScenarioItemType[]>('steps', {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const innerExpandedKeys = defineModel<(string | number)[]>('expandedKeys', {
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
const showApiType = ref<string[]>([
|
||||||
|
ScenarioStepType.API,
|
||||||
|
ScenarioStepType.API_CASE,
|
||||||
|
ScenarioStepType.CUSTOM_REQUEST,
|
||||||
|
ScenarioStepType.SCRIPT,
|
||||||
|
ScenarioStepType.TEST_PLAN_API_CASE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedKeys = ref<(string | number)[]>([]);
|
||||||
|
|
||||||
|
const showCondition = ref<string[]>([
|
||||||
|
ScenarioStepType.API,
|
||||||
|
ScenarioStepType.API_CASE,
|
||||||
|
ScenarioStepType.CUSTOM_REQUEST,
|
||||||
|
ScenarioStepType.LOOP_CONTROLLER,
|
||||||
|
ScenarioStepType.IF_CONTROLLER,
|
||||||
|
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||||
|
ScenarioStepType.TEST_PLAN_API_CASE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getShowExpand(item: ScenarioItemType) {
|
||||||
|
if (props.showType === 'API') {
|
||||||
|
return showApiType.value.includes(item.stepType) && props.activeType === 'tab';
|
||||||
|
}
|
||||||
|
return props.activeType === 'tab';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应状态码对应颜色
|
||||||
|
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 showStatus(item: ScenarioItemType) {
|
||||||
|
if (showApiType.value.includes(item.stepType) && item.status && item.status !== 'PENDING') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return item.children && item.children.length > 0 && item.status && item.status !== 'PENDING';
|
||||||
|
}
|
||||||
|
</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: 8px 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;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: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-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: 40px;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--color-text-n8);
|
||||||
|
}
|
||||||
|
:deep(.step-tree-node-title) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-[36px] items-center justify-between">
|
<div class="mb-[16px] flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="mr-2 font-medium leading-[36px]">{{ t('report.detail.api.reportDetail') }}</div>
|
<div class="mr-2 font-medium">{{ t('report.detail.api.reportDetail') }}</div>
|
||||||
<a-radio-group v-model:model-value="innerActiveTab" type="button" size="small">
|
<a-radio-group v-if="!props.isExport" v-model:model-value="innerActiveTab" type="button" size="small">
|
||||||
<a-radio v-for="item of methods" :key="item.value" :value="item.value">
|
<a-radio v-for="item of methods" :key="item.value" :value="item.value">
|
||||||
{{ t(item.label) }}
|
{{ t(item.label) }}
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[240px]">
|
<div v-if="!props.isExport" class="w-[240px]">
|
||||||
<MsCascader
|
<MsCascader
|
||||||
v-model:model-value="innerKeyword"
|
v-model:model-value="innerKeyword"
|
||||||
mode="native"
|
mode="native"
|
||||||
|
@ -59,6 +59,7 @@
|
||||||
activeTab: 'tiled' | 'tab';
|
activeTab: 'tiled' | 'tab';
|
||||||
keyword: string;
|
keyword: string;
|
||||||
showType: 'API' | 'CASE';
|
showType: 'API' | 'CASE';
|
||||||
|
isExport?: boolean; // 是否是导出pdf预览
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['update:activeTab', 'update:keyword']);
|
const emit = defineEmits(['update:activeTab', 'update:keyword']);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class="tiled-wrap p-4"
|
class="tiled-wrap p-4"
|
||||||
:class="{
|
:class="{
|
||||||
'border border-solid border-[var(--color-text-n8)]': props.showType === 'API',
|
'border border-solid border-[var(--color-text-n8)]': props.showType === 'API',
|
||||||
|
'!max-h-max': props.isExport,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="isFailedRetry" class="mb-[8px]">
|
<div v-if="isFailedRetry" class="mb-[8px]">
|
||||||
|
@ -14,7 +15,19 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 步骤树 -->
|
<!-- 步骤树 -->
|
||||||
|
<ReadOnlyStepTree
|
||||||
|
v-if="props.isExport"
|
||||||
|
v-model:steps="currentTiledList"
|
||||||
|
v-model:expandedKeys="expandedKeys"
|
||||||
|
:show-type="props.showType"
|
||||||
|
:active-type="props.activeType"
|
||||||
|
:expand-all="isExpandAll"
|
||||||
|
:console="props.reportDetail.console"
|
||||||
|
:report-id="props.reportDetail.id"
|
||||||
|
:get-report-step-detail="props.getReportStepDetail"
|
||||||
|
/>
|
||||||
<StepTree
|
<StepTree
|
||||||
|
v-else
|
||||||
ref="stepTreeRef"
|
ref="stepTreeRef"
|
||||||
v-model:steps="currentTiledList"
|
v-model:steps="currentTiledList"
|
||||||
v-model:expandedKeys="expandedKeys"
|
v-model:expandedKeys="expandedKeys"
|
||||||
|
@ -45,6 +58,7 @@
|
||||||
import { cloneDeep, debounce } from 'lodash-es';
|
import { cloneDeep, debounce } from 'lodash-es';
|
||||||
|
|
||||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||||
|
import ReadOnlyStepTree from './step/readOnlyTree.vue';
|
||||||
import StepDrawer from './step/stepDrawer.vue';
|
import StepDrawer from './step/stepDrawer.vue';
|
||||||
import StepTree from './step/stepTree.vue';
|
import StepTree from './step/stepTree.vue';
|
||||||
|
|
||||||
|
@ -61,6 +75,7 @@
|
||||||
showType: 'API' | 'CASE'; // 接口场景|用例
|
showType: 'API' | 'CASE'; // 接口场景|用例
|
||||||
keyWords: string;
|
keyWords: string;
|
||||||
getReportStepDetail?: (...args: any) => Promise<any>; // 获取步骤的详情内容接口
|
getReportStepDetail?: (...args: any) => Promise<any>; // 获取步骤的详情内容接口
|
||||||
|
isExport?: boolean; // 是否是导出pdf预览
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -209,7 +224,9 @@
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.tiled-wrap {
|
.tiled-wrap {
|
||||||
min-height: calc(100vh - 424px);
|
overflow: auto;
|
||||||
|
max-height: calc(100vh - 162px);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
.ms-scroll-bar();
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<a-spin :loading="loading" :tip="t('report.detail.exportingPdf')" class="report-detail-container">
|
||||||
|
<div id="report-detail" class="report-detail">
|
||||||
|
<div class="mb-[16px] rounded-[var(--border-radius-small)] bg-white p-[16px]">{{ reportStepDetail?.name }}</div>
|
||||||
|
<ScenarioCom :detail-info="reportStepDetail" is-export />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
import ScenarioCom from './component/scenarioCom.vue';
|
||||||
|
|
||||||
|
import { reportScenarioDetail } from '@/api/modules/api-test/report';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import exportPDF from '@/utils/exportPdf';
|
||||||
|
|
||||||
|
import { ReportDetail } from '@/models/apiTest/report';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const reportStepDetail = ref<ReportDetail>();
|
||||||
|
|
||||||
|
async function initReportDetail() {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
reportStepDetail.value = await reportScenarioDetail(route.query.id as string);
|
||||||
|
setTimeout(() => {
|
||||||
|
nextTick(async () => {
|
||||||
|
await exportPDF(reportStepDetail.value?.name || '', 'report-detail');
|
||||||
|
loading.value = false;
|
||||||
|
Message.success(t('report.detail.exportPdfSuccess'));
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (route.query.id) {
|
||||||
|
initReportDetail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.arco-spin-mask-icon {
|
||||||
|
@apply !fixed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.report-detail-container {
|
||||||
|
@apply flex justify-center;
|
||||||
|
.report-detail {
|
||||||
|
@apply overflow-x-auto;
|
||||||
|
|
||||||
|
padding: 16px;
|
||||||
|
width: 1190px;
|
||||||
|
.ms-scroll-bar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -148,7 +148,7 @@
|
||||||
statusConfig,
|
statusConfig,
|
||||||
toolTipConfig,
|
toolTipConfig,
|
||||||
} from '@/config/testPlan';
|
} from '@/config/testPlan';
|
||||||
import exportPDF, { PAGE_PDF_WIDTH_RATIO } from '@/hooks/useExportPDF';
|
import exportPDF, { PAGE_PDF_WIDTH_RATIO, PdfTableConfig } from '@/hooks/useExportPDF';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { addCommasToNumber } from '@/utils';
|
import { addCommasToNumber } from '@/utils';
|
||||||
|
|
||||||
|
@ -668,12 +668,9 @@
|
||||||
(e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value)
|
(e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value)
|
||||||
);
|
);
|
||||||
await Promise.all([initBugList(), initCaseList(), initApiList(), initScenarioList()]);
|
await Promise.all([initBugList(), initCaseList(), initApiList(), initScenarioList()]);
|
||||||
nextTick(async () => {
|
const tableArr: PdfTableConfig[] = [];
|
||||||
exportPDF(
|
if (fullBugList.value.length > 0) {
|
||||||
name,
|
tableArr.push({
|
||||||
'report-detail',
|
|
||||||
[
|
|
||||||
{
|
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
num: { cellWidth: 120 / PAGE_PDF_WIDTH_RATIO },
|
num: { cellWidth: 120 / PAGE_PDF_WIDTH_RATIO },
|
||||||
title: { cellWidth: 600 / PAGE_PDF_WIDTH_RATIO },
|
title: { cellWidth: 600 / PAGE_PDF_WIDTH_RATIO },
|
||||||
|
@ -687,8 +684,10 @@
|
||||||
dataKey: item.dataIndex,
|
dataKey: item.dataIndex,
|
||||||
})) as ColumnInput[],
|
})) as ColumnInput[],
|
||||||
body: fullBugList.value,
|
body: fullBugList.value,
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
|
if (fullCaseList.value.length > 0) {
|
||||||
|
tableArr.push({
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||||
|
@ -708,8 +707,10 @@
|
||||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||||
executeUser: e.executeUser?.name || '-',
|
executeUser: e.executeUser?.name || '-',
|
||||||
})) as RowInput[],
|
})) as RowInput[],
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
|
if (fullApiList.value.length > 0) {
|
||||||
|
tableArr.push({
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
num: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
num: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||||
name: { cellWidth: 450 / PAGE_PDF_WIDTH_RATIO },
|
name: { cellWidth: 450 / PAGE_PDF_WIDTH_RATIO },
|
||||||
|
@ -729,8 +730,10 @@
|
||||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||||
executeUser: e.executeUser?.name || '-',
|
executeUser: e.executeUser?.name || '-',
|
||||||
})) as RowInput[],
|
})) as RowInput[],
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
|
if (apiColumns.value.length > 0) {
|
||||||
|
tableArr.push({
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||||
|
@ -750,13 +753,13 @@
|
||||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||||
executeUser: e.executeUser?.name || '-',
|
executeUser: e.executeUser?.name || '-',
|
||||||
})) as RowInput[],
|
})) as RowInput[],
|
||||||
},
|
});
|
||||||
],
|
}
|
||||||
() => {
|
nextTick(async () => {
|
||||||
|
exportPDF(name, 'report-detail', tableArr, () => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
Message.success(t('report.detail.exportPdfSuccess'));
|
Message.success(t('report.detail.exportPdfSuccess'));
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
Loading…
Reference in New Issue