feat(报告): 场景报告导出 pdf
This commit is contained in:
parent
e2429d0c94
commit
76c33c015e
|
@ -33,7 +33,7 @@
|
|||
<slot name="tbutton"></slot>
|
||||
</div>
|
||||
</slot>
|
||||
<div class="right-operation-button-icon">
|
||||
<div class="ms-drawer-right-operation-button">
|
||||
<MsButton
|
||||
v-if="props.showFullScreen"
|
||||
type="icon"
|
||||
|
@ -286,17 +286,21 @@
|
|||
@apply w-full;
|
||||
|
||||
line-height: 24px;
|
||||
.right-operation-button-icon .ms-button-icon {
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-text-1);
|
||||
.arco-icon {
|
||||
margin-right: 8px;
|
||||
.ms-drawer-right-operation-button {
|
||||
.ms-button-icon,
|
||||
.ms-drawer-fullscreen-btn {
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
&:hover {
|
||||
color: rgb(var(--primary-5));
|
||||
.arco-icon {
|
||||
.arco-icon,
|
||||
.ms-drawer-fullscreen-btn-icon {
|
||||
margin-right: 8px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
&:hover {
|
||||
color: rgb(var(--primary-5));
|
||||
.arco-icon {
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,6 +115,8 @@ export enum ShareEnum {
|
|||
export enum FullPageEnum {
|
||||
FULL_PAGE = 'fullPage',
|
||||
FULL_PAGE_TEST_PLAN_EXPORT_PDF = 'fullPageTestPlanExportPDF',
|
||||
FULL_PAGE_SCENARIO_EXPORT_PDF = 'fullPageScenarioExportPDF',
|
||||
FULL_PAGE_CASE_EXPORT_PDF = 'fullPageCaseExportPDF',
|
||||
}
|
||||
|
||||
export const RouteEnum = {
|
||||
|
|
|
@ -1,62 +1,11 @@
|
|||
import '@/assets/fonts/AlibabaPuHuiTi-3-55-Regular-normal';
|
||||
|
||||
import { Canvg } from 'canvg';
|
||||
import { replaceSvgWithBase64 } from '@/utils/exportPdf';
|
||||
|
||||
import html2canvas from 'html2canvas-pro';
|
||||
import JSPDF from 'jspdf';
|
||||
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_HEIGHT = 842;
|
||||
const HEADER_HEIGHT = 16;
|
||||
|
@ -64,7 +13,7 @@ const FOOTER_HEIGHT = 16;
|
|||
export const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
||||
export const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
||||
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 宽度的比例
|
||||
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 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,
|
||||
height: element.clientHeight,
|
||||
backgroundColor: '#f9f9fe',
|
||||
scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
|
||||
scale: SCALE_RATIO, // 缩放增加清晰度
|
||||
});
|
||||
pdf.setFont('AlibabaPuHuiTi-3-55-Regular');
|
||||
pdf.setFontSize(10);
|
||||
|
|
|
@ -14,6 +14,11 @@ const FullPage: AppRouteRecordRaw = {
|
|||
name: FullPageEnum.FULL_PAGE_TEST_PLAN_EXPORT_PDF,
|
||||
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 PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
||||
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 倍的清晰度)
|
||||
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
|
||||
|
@ -53,7 +53,7 @@ async function convertSvgToBase64(svgElement: SVGSVGElement) {
|
|||
/**
|
||||
* 替换svg为base64
|
||||
*/
|
||||
async function replaceSvgWithBase64(container: HTMLElement) {
|
||||
export async function replaceSvgWithBase64(container: HTMLElement) {
|
||||
await inlineSvgUseElements(container);
|
||||
const svgElements = container.querySelectorAll('.c-icon');
|
||||
svgElements.forEach(async (svgElement) => {
|
||||
|
@ -107,7 +107,7 @@ export default async function exportPDF(name: string, contentId: string) {
|
|||
width: CONTAINER_WIDTH,
|
||||
height: screenshotHeight,
|
||||
backgroundColor: '#f9f9fe',
|
||||
scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
|
||||
scale: SCALE_RATIO, // 缩放增加清晰度
|
||||
});
|
||||
screenshotList.push(canvas);
|
||||
position += screenshotHeight;
|
||||
|
|
|
@ -9,24 +9,36 @@
|
|||
show-full-screen
|
||||
>
|
||||
<template #tbutton>
|
||||
<a-dropdown v-if="!props.doNotShowShare" position="br" @select="shareHandler">
|
||||
<div class="ms-drawer-right-operation-button flex items-center">
|
||||
<a-dropdown v-if="!props.doNotShowShare" position="br" @select="shareHandler">
|
||||
<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="shareHandler"
|
||||
>
|
||||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px] text-[var(--color-text-1)]" />
|
||||
{{ t('common.share') }}
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<a-doption>
|
||||
<span>{{ t('report.detail.api.copyLink') }}</span
|
||||
><span>{{ t('report.detail.api.copyLinkTimeEnd', { time: shareTime }) }}</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="mr-4 !rounded-[var(--border-radius-small)]"
|
||||
@click="shareHandler"
|
||||
class="mr-4 !rounded-[var(--border-radius-small)] text-[var(--color-text-1)]"
|
||||
@click="exportHandler"
|
||||
>
|
||||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
||||
{{ t('common.share') }}
|
||||
<MsIcon type="icon-icon_bottom-align_outlined" class="mr-2 font-[16px] text-[var(--color-text-1)]" />
|
||||
{{ t('common.export') }}
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<a-doption>
|
||||
<span>{{ t('report.detail.api.copyLink') }}</span
|
||||
><span>{{ t('report.detail.api.copyLinkTimeEnd', { time: shareTime }) }}</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<CaseReportCom
|
||||
v-if="!props.isScenario"
|
||||
|
@ -50,10 +62,11 @@
|
|||
|
||||
import { getShareInfo, getShareTime, reportCaseDetail, reportScenarioDetail } from '@/api/modules/api-test/report';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
import type { ReportDetail } from '@/models/apiTest/report';
|
||||
import { RouteEnum } from '@/enums/routeEnum';
|
||||
import { FullPageEnum, RouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
reportId: string;
|
||||
|
@ -67,6 +80,7 @@
|
|||
const { t } = useI18n();
|
||||
const { copy, isSupported } = useClipboard({ legacy: true });
|
||||
const route = useRoute();
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
const innerVisible = defineModel<boolean>('visible', {
|
||||
required: true,
|
||||
|
@ -188,6 +202,16 @@
|
|||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function exportHandler() {
|
||||
openNewPage(
|
||||
props.isScenario ? FullPageEnum.FULL_PAGE_SCENARIO_EXPORT_PDF : FullPageEnum.FULL_PAGE_CASE_EXPORT_PDF,
|
||||
{
|
||||
id: props.reportId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.doNotShowShare) {
|
||||
getTime();
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
@change="loadControlLoop"
|
||||
/>
|
||||
</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">
|
||||
<span
|
||||
:class="{ 'text-[rgb(var(--primary-5))]': activeType === 'ResContent' }"
|
||||
|
|
|
@ -257,7 +257,6 @@
|
|||
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
|
||||
.history-table-before {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -16,12 +16,12 @@
|
|||
@loaded="loadedReport"
|
||||
>
|
||||
<template #titleRight="{ loading }">
|
||||
<div class="rightButtons flex items-center">
|
||||
<div class="ms-drawer-right-operation-button flex items-center">
|
||||
<MsButton
|
||||
v-permission="['PROJECT_API_REPORT:READ+SHARE']"
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="mr-4 !rounded-[var(--border-radius-small)]"
|
||||
class="!rounded-[var(--border-radius-small)]"
|
||||
:disabled="loading"
|
||||
:loading="shareLoading"
|
||||
@click="shareHandler"
|
||||
|
@ -29,6 +29,16 @@
|
|||
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
|
||||
{{ t('common.share') }}
|
||||
</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>
|
||||
</template>
|
||||
<template #default="{ loading }">
|
||||
|
@ -52,14 +62,16 @@
|
|||
|
||||
import { getShareInfo, reportScenarioDetail } from '@/api/modules/api-test/report';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
import type { ReportDetail } from '@/models/apiTest/report';
|
||||
import { RouteEnum } from '@/enums/routeEnum';
|
||||
import { FullPageEnum, RouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
const { copy, isSupported } = useClipboard({ legacy: true });
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
|
@ -162,14 +174,16 @@
|
|||
Message.error(t('common.copyNotSupport'));
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
const exportLoading = ref<boolean>(false);
|
||||
function exportHandler() {}
|
||||
|
||||
function exportHandler() {
|
||||
openNewPage(FullPageEnum.FULL_PAGE_SCENARIO_EXPORT_PDF, {
|
||||
id: props.reportId,
|
||||
});
|
||||
}
|
||||
|
||||
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
|
||||
|
||||
|
@ -186,7 +200,6 @@
|
|||
<style scoped lang="less">
|
||||
.report-container {
|
||||
padding: 16px;
|
||||
height: calc(100vh - 56px);
|
||||
background: var(--color-text-n9);
|
||||
.report-header {
|
||||
padding: 0 16px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="report-container h-full">
|
||||
<div class="report-container">
|
||||
<!-- 报告参数开始 -->
|
||||
<ReportDetailHeader :detail="detail" show-type="API" />
|
||||
<!-- 报告参数结束 -->
|
||||
|
@ -35,13 +35,19 @@
|
|||
<!-- 报告分析,报告步骤分析和请求分析结束 -->
|
||||
<!-- 报告明细开始 -->
|
||||
<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
|
||||
:key-words="cascaderKeywords"
|
||||
show-type="API"
|
||||
:get-report-step-detail="props.getReportStepDetail"
|
||||
:active-type="activeTab"
|
||||
:report-detail="detail || []"
|
||||
:is-export="props.isExport"
|
||||
/>
|
||||
</div>
|
||||
<!-- 报告明细结束 -->
|
||||
|
@ -71,6 +77,7 @@
|
|||
const props = defineProps<{
|
||||
detailInfo?: ReportDetail;
|
||||
getReportStepDetail?: (...args: any) => Promise<any>; // 获取步骤的详情内容接口
|
||||
isExport?: boolean; // 是否是导出pdf预览
|
||||
}>();
|
||||
|
||||
const detail = ref<ReportDetail>({
|
||||
|
@ -170,6 +177,7 @@
|
|||
legend: {
|
||||
show: false,
|
||||
},
|
||||
animation: !props.isExport, // pdf预览需要关闭渲染动画
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
|
@ -317,7 +325,6 @@
|
|||
|
||||
<style scoped lang="less">
|
||||
.report-container {
|
||||
height: calc(100vh - 56px);
|
||||
background: var(--color-text-n9);
|
||||
.report-header {
|
||||
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>
|
||||
<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="mr-2 font-medium leading-[36px]">{{ t('report.detail.api.reportDetail') }}</div>
|
||||
<a-radio-group v-model:model-value="innerActiveTab" type="button" size="small">
|
||||
<div class="mr-2 font-medium">{{ t('report.detail.api.reportDetail') }}</div>
|
||||
<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">
|
||||
{{ t(item.label) }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="w-[240px]">
|
||||
<div v-if="!props.isExport" class="w-[240px]">
|
||||
<MsCascader
|
||||
v-model:model-value="innerKeyword"
|
||||
mode="native"
|
||||
|
@ -59,6 +59,7 @@
|
|||
activeTab: 'tiled' | 'tab';
|
||||
keyword: string;
|
||||
showType: 'API' | 'CASE';
|
||||
isExport?: boolean; // 是否是导出pdf预览
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:activeTab', 'update:keyword']);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class="tiled-wrap p-4"
|
||||
:class="{
|
||||
'border border-solid border-[var(--color-text-n8)]': props.showType === 'API',
|
||||
'!max-h-max': props.isExport,
|
||||
}"
|
||||
>
|
||||
<div v-if="isFailedRetry" class="mb-[8px]">
|
||||
|
@ -14,7 +15,19 @@
|
|||
/>
|
||||
</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
|
||||
v-else
|
||||
ref="stepTreeRef"
|
||||
v-model:steps="currentTiledList"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
|
@ -45,6 +58,7 @@
|
|||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||
import ReadOnlyStepTree from './step/readOnlyTree.vue';
|
||||
import StepDrawer from './step/stepDrawer.vue';
|
||||
import StepTree from './step/stepTree.vue';
|
||||
|
||||
|
@ -61,6 +75,7 @@
|
|||
showType: 'API' | 'CASE'; // 接口场景|用例
|
||||
keyWords: string;
|
||||
getReportStepDetail?: (...args: any) => Promise<any>; // 获取步骤的详情内容接口
|
||||
isExport?: boolean; // 是否是导出pdf预览
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
@ -209,7 +224,9 @@
|
|||
|
||||
<style scoped lang="less">
|
||||
.tiled-wrap {
|
||||
min-height: calc(100vh - 424px);
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 162px);
|
||||
border-radius: 4px;
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
</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,
|
||||
toolTipConfig,
|
||||
} 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 { addCommasToNumber } from '@/utils';
|
||||
|
||||
|
@ -668,95 +668,98 @@
|
|||
(e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value)
|
||||
);
|
||||
await Promise.all([initBugList(), initCaseList(), initApiList(), initScenarioList()]);
|
||||
const tableArr: PdfTableConfig[] = [];
|
||||
if (fullBugList.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
num: { cellWidth: 120 / PAGE_PDF_WIDTH_RATIO },
|
||||
title: { cellWidth: 600 / PAGE_PDF_WIDTH_RATIO },
|
||||
status: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
handleUserName: { cellWidth: 270 / PAGE_PDF_WIDTH_RATIO },
|
||||
relationCaseCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: bugColumns.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullBugList.value,
|
||||
});
|
||||
}
|
||||
if (fullCaseList.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
relationCaseCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: caseColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullCaseList.value.map((e: any) => ({
|
||||
...e,
|
||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||
executeUser: e.executeUser?.name || '-',
|
||||
})) as RowInput[],
|
||||
});
|
||||
}
|
||||
if (fullApiList.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
num: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 450 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: apiColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullApiList.value.map((e: any) => ({
|
||||
...e,
|
||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||
executeUser: e.executeUser?.name || '-',
|
||||
})) as RowInput[],
|
||||
});
|
||||
}
|
||||
if (apiColumns.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: apiColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullScenarioList.value.map((e: any) => ({
|
||||
...e,
|
||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||
executeUser: e.executeUser?.name || '-',
|
||||
})) as RowInput[],
|
||||
});
|
||||
}
|
||||
nextTick(async () => {
|
||||
exportPDF(
|
||||
name,
|
||||
'report-detail',
|
||||
[
|
||||
{
|
||||
columnStyles: {
|
||||
num: { cellWidth: 120 / PAGE_PDF_WIDTH_RATIO },
|
||||
title: { cellWidth: 600 / PAGE_PDF_WIDTH_RATIO },
|
||||
status: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
handleUserName: { cellWidth: 270 / PAGE_PDF_WIDTH_RATIO },
|
||||
relationCaseCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: bugColumns.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullBugList.value,
|
||||
},
|
||||
{
|
||||
columnStyles: {
|
||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
relationCaseCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: caseColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullCaseList.value.map((e: any) => ({
|
||||
...e,
|
||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||
executeUser: e.executeUser?.name || '-',
|
||||
})) as RowInput[],
|
||||
},
|
||||
{
|
||||
columnStyles: {
|
||||
num: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 450 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: apiColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullApiList.value.map((e: any) => ({
|
||||
...e,
|
||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||
executeUser: e.executeUser?.name || '-',
|
||||
})) as RowInput[],
|
||||
},
|
||||
{
|
||||
columnStyles: {
|
||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
columns: apiColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
dataKey: item.dataIndex,
|
||||
})) as ColumnInput[],
|
||||
body: fullScenarioList.value.map((e: any) => ({
|
||||
...e,
|
||||
executeResult: t(lastExecuteResultMap[e.executeResult]?.statusText || '-'),
|
||||
executeUser: e.executeUser?.name || '-',
|
||||
})) as RowInput[],
|
||||
},
|
||||
],
|
||||
() => {
|
||||
loading.value = false;
|
||||
Message.success(t('report.detail.exportPdfSuccess'));
|
||||
}
|
||||
);
|
||||
exportPDF(name, 'report-detail', tableArr, () => {
|
||||
loading.value = false;
|
||||
Message.success(t('report.detail.exportPdfSuccess'));
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
Loading…
Reference in New Issue