feat(报告): 报告导出 pdf 轻量版
This commit is contained in:
parent
7c8c7c4555
commit
c00303e863
|
@ -131,6 +131,7 @@ export interface MsTableProps<T> {
|
||||||
// 行选择器禁用配置
|
// 行选择器禁用配置
|
||||||
rowSelectionDisabledConfig?: MsTableRowSelectionDisabledConfig;
|
rowSelectionDisabledConfig?: MsTableRowSelectionDisabledConfig;
|
||||||
sorter?: Record<string, any>; // 排序
|
sorter?: Record<string, any>; // 排序
|
||||||
|
hoverable?: boolean; // 是否展示hover效果
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,12 @@ const A4_HEIGHT = 842;
|
||||||
const HEADER_HEIGHT = 16;
|
const HEADER_HEIGHT = 16;
|
||||||
const FOOTER_HEIGHT = 16;
|
const FOOTER_HEIGHT = 16;
|
||||||
const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
||||||
const pdfWidth = A4_WIDTH - 32; // 左右分别 16px 间距
|
const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
||||||
const totalWidth = 1370;
|
const CONTAINER_WIDTH = 1190;
|
||||||
const realPageHeight = Math.ceil(PAGE_HEIGHT * (totalWidth / pdfWidth) * 1.2); // 实际每页高度
|
export const SCALE_RATIO = 1.2;
|
||||||
const MAX_CANVAS_HEIGHT = realPageHeight * 23; // 一次截图最大高度是 25 页整
|
// 实际每页高度 = 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 页整(过长会无法截完整,出现空白)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 替换svg为base64
|
* 替换svg为base64
|
||||||
|
@ -72,7 +74,6 @@ async function replaceSvgWithBase64(container: HTMLElement) {
|
||||||
* (使用html2canvas截图时,因为插件有截图极限,超出极限部分会出现截图失败,所以这里设置了MAX_CANVAS_HEIGHT截图高度,然后根据这个截图高度分页截图,然后根据每个截图裁剪每页 pdf 的图片并添加到 pdf 内)
|
* (使用html2canvas截图时,因为插件有截图极限,超出极限部分会出现截图失败,所以这里设置了MAX_CANVAS_HEIGHT截图高度,然后根据这个截图高度分页截图,然后根据每个截图裁剪每页 pdf 的图片并添加到 pdf 内)
|
||||||
*/
|
*/
|
||||||
export default async function exportPDF(name: string, contentId: string) {
|
export default async function exportPDF(name: string, contentId: string) {
|
||||||
console.log('start', new Date().getMinutes(), new Date().getSeconds());
|
|
||||||
const element = document.getElementById(contentId);
|
const element = document.getElementById(contentId);
|
||||||
if (element) {
|
if (element) {
|
||||||
await replaceSvgWithBase64(element);
|
await replaceSvgWithBase64(element);
|
||||||
|
@ -86,52 +87,55 @@ export default async function exportPDF(name: string, contentId: string) {
|
||||||
pdf.setFontSize(10);
|
pdf.setFontSize(10);
|
||||||
// 计算pdf总页数
|
// 计算pdf总页数
|
||||||
let totalPages = 0;
|
let totalPages = 0;
|
||||||
let position = 0;
|
let position = 0; // 当前截图位置
|
||||||
let pageIndex = 1;
|
let pageIndex = 1;
|
||||||
let loopTimes = 0;
|
let loopTimes = 0;
|
||||||
const canvasList: HTMLCanvasElement[] = [];
|
const screenshotList: HTMLCanvasElement[] = [];
|
||||||
while (position < totalHeight) {
|
// 创建图片裁剪画布
|
||||||
|
const cropCanvas = document.createElement('canvas');
|
||||||
|
cropCanvas.width = CONTAINER_WIDTH * SCALE_RATIO; // 因为截图时放大了 SCALE_RATIO 倍,所以这里也要放大
|
||||||
|
cropCanvas.height = IMAGE_HEIGHT;
|
||||||
|
const tempContext = cropCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
// 这里是大的分页,也就是截图画布的分页
|
// 这里是大的分页,也就是截图画布的分页
|
||||||
const offscreenHeight = Math.min(MAX_CANVAS_HEIGHT, totalHeight - position);
|
while (position < totalHeight) {
|
||||||
|
// 截图高度
|
||||||
|
const screenshotHeight = Math.min(MAX_CANVAS_HEIGHT, totalHeight - position);
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const canvas = await html2canvas(element, {
|
const canvas = await html2canvas(element, {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: position,
|
y: position,
|
||||||
width: totalWidth,
|
width: CONTAINER_WIDTH,
|
||||||
height: offscreenHeight,
|
height: screenshotHeight,
|
||||||
backgroundColor: '#f9f9fe',
|
backgroundColor: '#f9f9fe',
|
||||||
scale: window.devicePixelRatio * 1.2, // 增加清晰度
|
scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
|
||||||
});
|
});
|
||||||
canvasList.push(canvas);
|
screenshotList.push(canvas);
|
||||||
position += offscreenHeight;
|
position += screenshotHeight;
|
||||||
totalPages += Math.ceil(canvas.height / realPageHeight);
|
totalPages += Math.ceil(canvas.height / IMAGE_HEIGHT);
|
||||||
loopTimes++;
|
loopTimes++;
|
||||||
}
|
}
|
||||||
totalPages -= loopTimes - 1; // 减去多余的页数
|
totalPages -= loopTimes - 1; // 减去多余的页数
|
||||||
// 生成 PDF
|
// 生成 PDF
|
||||||
canvasList.forEach((canvas) => {
|
screenshotList.forEach((_canvas) => {
|
||||||
const canvasWidth = canvas.width;
|
const canvasWidth = _canvas.width;
|
||||||
const canvasHeight = canvas.height;
|
const canvasHeight = _canvas.height;
|
||||||
const pages = Math.ceil(canvasHeight / realPageHeight);
|
const pages = Math.ceil(canvasHeight / IMAGE_HEIGHT);
|
||||||
for (let i = 1; i <= pages; i++) {
|
for (let i = 1; i <= pages; i++) {
|
||||||
// 这里是小的分页,是 pdf 的每一页
|
// 这里是小的分页,是 pdf 的每一页
|
||||||
const pagePosition = (i - 1) * realPageHeight;
|
const pagePosition = (i - 1) * IMAGE_HEIGHT;
|
||||||
// 创建临时画布
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
|
||||||
tempCanvas.width = canvasWidth;
|
|
||||||
tempCanvas.height = realPageHeight;
|
|
||||||
const tempContext = tempCanvas.getContext('2d', { willReadFrequently: true });
|
|
||||||
if (tempContext) {
|
if (tempContext) {
|
||||||
|
if (pageIndex === totalPages) {
|
||||||
// 填充背景颜色为白色
|
// 填充背景颜色为白色
|
||||||
tempContext.fillStyle = '#ffffff';
|
tempContext.fillStyle = '#ffffff';
|
||||||
tempContext.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
tempContext.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
|
||||||
// 将大分页的画布图片裁剪成pdf 页面内容大小,并渲染到临时画布上
|
|
||||||
tempContext.drawImage(canvas, 0, -pagePosition, canvasWidth, canvasHeight);
|
|
||||||
const tempCanvasData = tempCanvas.toDataURL('image/jpeg', 0.8);
|
|
||||||
// 将临时画布图片渲染到 pdf 上
|
|
||||||
pdf.addImage(tempCanvasData, 'jpeg', 16, 16, pdfWidth, PAGE_HEIGHT);
|
|
||||||
}
|
}
|
||||||
tempCanvas.remove();
|
// 将大分页的画布图片裁剪成pdf 页面内容大小,并渲染到临时画布上
|
||||||
|
tempContext.drawImage(_canvas, 0, -pagePosition, canvasWidth, canvasHeight);
|
||||||
|
const tempCanvasData = cropCanvas.toDataURL('image/jpeg', 0.8);
|
||||||
|
// 将临时画布图片渲染到 pdf 上
|
||||||
|
pdf.addImage(tempCanvasData, 'jpeg', 16, 16, PDF_WIDTH, PAGE_HEIGHT);
|
||||||
|
}
|
||||||
|
cropCanvas.remove();
|
||||||
pdf.text(
|
pdf.text(
|
||||||
`${pageIndex} / ${totalPages}`,
|
`${pageIndex} / ${totalPages}`,
|
||||||
pdf.internal.pageSize.width / 2 - 10,
|
pdf.internal.pageSize.width / 2 - 10,
|
||||||
|
@ -142,9 +146,8 @@ export default async function exportPDF(name: string, contentId: string) {
|
||||||
pageIndex++;
|
pageIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
canvas.remove();
|
_canvas.remove();
|
||||||
});
|
});
|
||||||
pdf.save(`${name}.pdf`);
|
pdf.save(`${name}.pdf`);
|
||||||
console.log('end', new Date().getMinutes(), new Date().getSeconds());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<a-spin :loading="loading" class="block">
|
<a-spin :loading="loading" class="report-detail-container">
|
||||||
<div id="report-detail" class="p-[16px]">
|
<div id="report-detail" class="report-detail">
|
||||||
<div class="report-header">
|
<div class="report-header">
|
||||||
<div class="flex-1 break-all">{{ detail.name }}</div>
|
<div class="flex-1 break-all">{{ detail.name }}</div>
|
||||||
<div class="one-line-text">
|
<div class="one-line-text">
|
||||||
|
@ -91,7 +91,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-[16px]">
|
<div class="mt-[16px]">
|
||||||
<div v-for="item of innerCardList" v-show="showItem(item)" :key="item.id" class="card-item mt-[16px]">
|
<div
|
||||||
|
v-for="item of innerCardList"
|
||||||
|
v-show="showItem(item)"
|
||||||
|
:key="item.id"
|
||||||
|
class="card-item mt-[16px]"
|
||||||
|
:class="`${item.value}`"
|
||||||
|
>
|
||||||
<div class="wrapper-preview-card">
|
<div class="wrapper-preview-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div v-if="item.value !== ReportCardTypeEnum.CUSTOM_CARD" class="mb-[8px] font-medium">
|
<div v-if="item.value !== ReportCardTypeEnum.CUSTOM_CARD" class="mb-[8px] font-medium">
|
||||||
|
@ -107,7 +113,8 @@
|
||||||
/>
|
/>
|
||||||
<div v-else-if="item.value === ReportCardTypeEnum.SUMMARY" v-html="getContent(item).content"></div>
|
<div v-else-if="item.value === ReportCardTypeEnum.SUMMARY" v-html="getContent(item).content"></div>
|
||||||
<MsBaseTable v-else-if="item.value === ReportCardTypeEnum.BUG_DETAIL" v-bind="bugTableProps"> </MsBaseTable>
|
<MsBaseTable v-else-if="item.value === ReportCardTypeEnum.BUG_DETAIL" v-bind="bugTableProps"> </MsBaseTable>
|
||||||
<MsBaseTable v-else-if="item.value === ReportCardTypeEnum.FUNCTIONAL_DETAIL" v-bind="caseTableProps">
|
<div v-else-if="item.value === ReportCardTypeEnum.FUNCTIONAL_DETAIL" id="functionalCase">
|
||||||
|
<MsBaseTable v-bind="caseTableProps">
|
||||||
<template #caseLevel="{ record }">
|
<template #caseLevel="{ record }">
|
||||||
<CaseLevel :case-level="record.priority" />
|
<CaseLevel :case-level="record.priority" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -115,6 +122,7 @@
|
||||||
<ExecuteResult :execute-result="record.executeResult" />
|
<ExecuteResult :execute-result="record.executeResult" />
|
||||||
</template>
|
</template>
|
||||||
</MsBaseTable>
|
</MsBaseTable>
|
||||||
|
</div>
|
||||||
<MsBaseTable
|
<MsBaseTable
|
||||||
v-else-if="item.value === ReportCardTypeEnum.API_CASE_DETAIL"
|
v-else-if="item.value === ReportCardTypeEnum.API_CASE_DETAIL"
|
||||||
v-bind="useApiTable.propsRes.value"
|
v-bind="useApiTable.propsRes.value"
|
||||||
|
@ -184,6 +192,7 @@
|
||||||
} from '@/config/testPlan';
|
} from '@/config/testPlan';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { addCommasToNumber } from '@/utils';
|
import { addCommasToNumber } from '@/utils';
|
||||||
|
import exportPdf, { MAX_CANVAS_HEIGHT, SCALE_RATIO } from '@/utils/exportPdf';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
configItem,
|
configItem,
|
||||||
|
@ -198,7 +207,7 @@
|
||||||
|
|
||||||
import { defaultGroupConfig, defaultSingleConfig } from './component/reportConfig';
|
import { defaultGroupConfig, defaultSingleConfig } from './component/reportConfig';
|
||||||
import { getSummaryDetail } from '@/views/test-plan/report/utils';
|
import { getSummaryDetail } from '@/views/test-plan/report/utils';
|
||||||
import exportPdf from '@/workers/exportPDF/exportPDFWorker';
|
import html2canvas from 'html2canvas-pro';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -496,6 +505,7 @@
|
||||||
scroll: { x: '100%', y: 'auto' },
|
scroll: { x: '100%', y: 'auto' },
|
||||||
columns: bugColumns,
|
columns: bugColumns,
|
||||||
showSelectorAll: false,
|
showSelectorAll: false,
|
||||||
|
hoverable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 用例明细 */
|
/** 用例明细 */
|
||||||
|
@ -565,11 +575,13 @@
|
||||||
propsRes: caseTableProps,
|
propsRes: caseTableProps,
|
||||||
loadList: loadCaseList,
|
loadList: loadCaseList,
|
||||||
setLoadListParams: setLoadCaseListParams,
|
setLoadListParams: setLoadCaseListParams,
|
||||||
|
setPagination: setCasePagination,
|
||||||
} = useTable(reportFeatureCaseList(), {
|
} = useTable(reportFeatureCaseList(), {
|
||||||
scroll: { x: '100%', y: 'auto' },
|
scroll: { x: '100%', y: 'auto' },
|
||||||
columns: caseColumns.value,
|
columns: caseColumns.value,
|
||||||
heightUsed: 20,
|
heightUsed: 20,
|
||||||
showSelectorAll: false,
|
showSelectorAll: false,
|
||||||
|
hoverable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 接口/场景明细 */
|
/** 接口/场景明细 */
|
||||||
|
@ -609,12 +621,14 @@
|
||||||
columns: apiColumns.value,
|
columns: apiColumns.value,
|
||||||
showSelectorAll: false,
|
showSelectorAll: false,
|
||||||
showSetting: false,
|
showSetting: false,
|
||||||
|
hoverable: false,
|
||||||
});
|
});
|
||||||
const useScenarioTable = useTable(getScenarioPage, {
|
const useScenarioTable = useTable(getScenarioPage, {
|
||||||
scroll: { x: '100%', y: 'auto' },
|
scroll: { x: '100%', y: 'auto' },
|
||||||
columns: apiColumns.value,
|
columns: apiColumns.value,
|
||||||
showSelectorAll: false,
|
showSelectorAll: false,
|
||||||
showSetting: false,
|
showSetting: false,
|
||||||
|
hoverable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getDetail() {
|
async function getDetail() {
|
||||||
|
@ -632,7 +646,7 @@
|
||||||
innerCardList.value = isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
|
innerCardList.value = isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
|
||||||
}
|
}
|
||||||
setLoadBugListParams({ reportId: reportId.value, shareId: shareId.value ?? undefined, pageSize: 500 });
|
setLoadBugListParams({ reportId: reportId.value, shareId: shareId.value ?? undefined, pageSize: 500 });
|
||||||
setLoadCaseListParams({ reportId: reportId.value, shareId: shareId.value ?? undefined, startPager: false });
|
setLoadCaseListParams({ reportId: reportId.value, shareId: shareId.value ?? undefined, pageSize: 500 });
|
||||||
useApiTable.setLoadListParams({
|
useApiTable.setLoadListParams({
|
||||||
reportId: reportId.value,
|
reportId: reportId.value,
|
||||||
shareId: shareId.value ?? undefined,
|
shareId: shareId.value ?? undefined,
|
||||||
|
@ -645,9 +659,29 @@
|
||||||
});
|
});
|
||||||
await Promise.all([loadBugList(), loadCaseList(), useApiTable.loadList(), useScenarioTable.loadList()]);
|
await Promise.all([loadBugList(), loadCaseList(), useApiTable.loadList(), useScenarioTable.loadList()]);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
nextTick(() => {
|
|
||||||
exportPdf(detail.value.name, 'report-detail');
|
exportPdf(detail.value.name, 'report-detail');
|
||||||
});
|
// nextTick(async () => {
|
||||||
|
// const element = document.getElementById('functionalCase');
|
||||||
|
// if (element) {
|
||||||
|
// while (caseTableProps.value.msPagination!.current * 500 < caseTableProps.value.msPagination!.total) {
|
||||||
|
// console.log('start html2canvas', new Date().getMinutes(), new Date().getSeconds());
|
||||||
|
// // eslint-disable-next-line no-await-in-loop
|
||||||
|
// const canvas = await html2canvas(element, {
|
||||||
|
// x: 0,
|
||||||
|
// y: 848,
|
||||||
|
// width: 1190,
|
||||||
|
// height: MAX_CANVAS_HEIGHT,
|
||||||
|
// backgroundColor: '#f9f9fe',
|
||||||
|
// scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
|
||||||
|
// });
|
||||||
|
// console.log('end html2canvas', new Date().getMinutes(), new Date().getSeconds());
|
||||||
|
// exportPdf(detail.value.name, 'report-detail', canvas);
|
||||||
|
// setCasePagination({ current: caseTableProps.value.msPagination!.current + 1 });
|
||||||
|
// // eslint-disable-next-line no-await-in-loop
|
||||||
|
// await loadCaseList();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
}, 0);
|
}, 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -663,6 +697,16 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.report-detail-container {
|
||||||
|
@apply flex justify-center;
|
||||||
|
.report-detail {
|
||||||
|
@apply overflow-x-auto;
|
||||||
|
|
||||||
|
padding: 16px;
|
||||||
|
width: 1190px;
|
||||||
|
.ms-scroll-bar();
|
||||||
|
}
|
||||||
|
}
|
||||||
.report-header {
|
.report-header {
|
||||||
@apply mb-4 flex items-center bg-white;
|
@apply mb-4 flex items-center bg-white;
|
||||||
|
|
||||||
|
@ -751,4 +795,7 @@
|
||||||
:deep(.arco-table-body) {
|
:deep(.arco-table-body) {
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
}
|
}
|
||||||
|
:deep(#ms-table-footer-wrapper) {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue