feat(报告): 测试计划独立报告导出 pdf 终版

This commit is contained in:
baiqi 2024-09-10 14:56:08 +08:00 committed by Craftsman
parent 7ed7a70e84
commit 40bdb9fe2e
12 changed files with 403 additions and 245 deletions

View File

@ -3,4 +3,5 @@
dist
postcss.config.js
*.md
/src/assets/icon-font/iconfont.js
/src/assets/icon-font/iconfont.js
/src/assets/fonts/AlibabaPuHuiTi-3-55-Regular-normal.js

View File

@ -66,6 +66,7 @@
"json-schema-traverse": "^1.0.0",
"jsonpath-plus": "^8.1.0",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.8.3",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"lossless-json": "^4.0.1",
@ -128,9 +129,9 @@
"less": "^4.2.0",
"less-loader": "^11.1.4",
"lint-staged": "^13.3.0",
"postcss": "^8.4.39",
"postcss": "^8.4.45",
"postcss-html": "^1.7.0",
"postcss-import": "^15.1.0",
"postcss-import": "^16.1.0",
"postcss-less": "^6.0.0",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.3.0",

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
<template>
<div v-if="props.executeResult" class="flex items-center">
<!-- <MsIcon
<MsIcon
:type="lastExecuteResultMap[props.executeResult]?.icon || ''"
class="mr-1"
:size="16"
:style="{ color: lastExecuteResultMap[props.executeResult]?.color }"
></MsIcon> -->
<span class="text-[14px]">{{ lastExecuteResultMap[props.executeResult]?.statusText || '-' }}</span>
></MsIcon>
<span class="text-[14px]">{{ t(lastExecuteResultMap[props.executeResult]?.statusText || '-') }}</span>
</div>
</template>
@ -15,7 +15,9 @@
import { useI18n } from '@/hooks/useI18n';
import { LastExecuteResults, StatusType } from '@/enums/caseEnum';
import { LastExecuteResults } from '@/enums/caseEnum';
import { lastExecuteResultMap } from './utils';
const { t } = useI18n();
@ -23,33 +25,6 @@
executeResult?: LastExecuteResults;
}>();
const lastExecuteResultMap = {
PENDING: {
label: 'PENDING',
icon: StatusType.PENDING,
statusText: t('common.unExecute'),
color: 'var(--color-text-brand)',
},
SUCCESS: {
label: 'SUCCESS',
icon: StatusType.SUCCESS,
statusText: t('common.success'),
color: '',
},
BLOCKED: {
label: 'BLOCKED',
icon: StatusType.BLOCKED,
statusText: t('common.block'),
color: 'var(--color-fill-p-3)',
},
ERROR: {
label: 'ERROR',
icon: StatusType.ERROR,
statusText: t('common.fail'),
color: '',
},
};
// const status = computed(() => {
// if (props.executeResult) {
// const config = lastExecuteResultMap[props.executeResult];

View File

@ -2,6 +2,8 @@
import { getModuleTreeCounts } from '@/api/modules/bug-management';
import { getCaseModulesCounts, getPublicLinkCaseModulesCounts } from '@/api/modules/case-management/featureCase';
import { StatusType } from '@/enums/caseEnum';
export enum RequestModuleEnum {
API_CASE = 'API_CASE',
CASE_MANAGEMENT = 'CASE_MANAGEMENT',
@ -21,4 +23,29 @@ export function initGetModuleCountFunc(type: RequestModuleEnum[keyof RequestModu
}
}
export default {};
export const lastExecuteResultMap: Record<string, any> = {
PENDING: {
label: 'PENDING',
icon: StatusType.PENDING,
statusText: 'common.unExecute',
color: 'var(--color-text-brand)',
},
SUCCESS: {
label: 'SUCCESS',
icon: StatusType.SUCCESS,
statusText: 'common.success',
color: '',
},
BLOCKED: {
label: 'BLOCKED',
icon: StatusType.BLOCKED,
statusText: 'common.block',
color: 'var(--color-fill-p-3)',
},
ERROR: {
label: 'ERROR',
icon: StatusType.ERROR,
statusText: 'common.fail',
color: '',
},
};

View File

@ -0,0 +1,169 @@
import '@/assets/fonts/AlibabaPuHuiTi-3-55-Regular-normal';
import { Canvg } from 'canvg';
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;
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 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);
const commonOdfTableConfig: Partial<UserOptions> = {
headStyles: {
fillColor: '#793787',
},
styles: {
font: 'AlibabaPuHuiTi-3-55-Regular',
},
rowPageBreak: 'avoid',
margin: { top: 16, left: 16, right: 16, bottom: 16 },
tableWidth: PDF_WIDTH,
};
export type PdfTableConfig = Pick<UserOptions, 'columnStyles' | 'columns' | 'body'>;
/**
* PDF
* @param name
* @param contentId DOM id
* @description html2canvas生成图片jsPDF生成pdf
* 使html2canvas截图时MAX_CANVAS_HEIGHT截图高度 pdf pdf
*/
export default async function exportPDF(
name: string,
contentId: string,
autoTableConfig: PdfTableConfig[],
doneCallback?: () => void
) {
const element = document.getElementById(contentId);
if (element) {
await replaceSvgWithBase64(element); // 替换截图容器内的svg为base64因为html2canvas无法截取url-link方式的svg
// jsPDF实例
const pdf = new JSPDF({
unit: 'pt',
format: 'a4',
orientation: 'p',
});
const canvas = await html2canvas(element, {
x: 0,
width: CONTAINER_WIDTH,
height: element.clientHeight,
backgroundColor: '#f9f9fe',
scale: window.devicePixelRatio * SCALE_RATIO, // 缩放增加清晰度
});
pdf.setFont('AlibabaPuHuiTi-3-55-Regular');
pdf.setFontSize(10);
// 创建图片裁剪画布
const cropCanvas = document.createElement('canvas');
cropCanvas.width = CONTAINER_WIDTH * SCALE_RATIO;
cropCanvas.height = IMAGE_HEIGHT;
const tempContext = cropCanvas.getContext('2d', { willReadFrequently: true });
// 生成 PDF
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const pages = Math.ceil(canvasHeight / IMAGE_HEIGHT);
for (let i = 1; i <= pages; i++) {
// 这里是小的分页,是 pdf 的每一页
const pagePosition = (i - 1) * IMAGE_HEIGHT;
if (tempContext) {
if (i === pages) {
// 填充背景颜色为白色
tempContext.fillStyle = '#ffffff';
tempContext.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
}
// 将大分页的画布图片裁剪成pdf 页面内容大小,并渲染到临时画布上
tempContext.drawImage(canvas, 0, -pagePosition, canvasWidth, canvasHeight);
const tempCanvasData = cropCanvas.toDataURL('image/jpeg');
// 将临时画布图片渲染到 pdf 上
pdf.addImage(tempCanvasData, 'jpeg', 16, 16, PDF_WIDTH, PAGE_HEIGHT);
}
cropCanvas.remove();
if (i < pages) {
pdf.text(`${i}`, pdf.internal.pageSize.width / 2 - 10, pdf.internal.pageSize.height - 4);
pdf.addPage();
}
}
const lastImagePageUseHeight = (canvasHeight - IMAGE_HEIGHT) / PAGE_PDF_WIDTH_RATIO / SCALE_RATIO; // 最后一页带图片的pdf页面被图片占用的高度
autoTableConfig.forEach((config, index) => {
autoTable(pdf, {
...config,
startY: index === 0 && lastImagePageUseHeight > 0 ? lastImagePageUseHeight + 32 : undefined, // 第一页表格如果和图片同一页,则需要设置 startY 为当前图片占用高度+32以避免表格遮挡图片
...(commonOdfTableConfig as UserOptions),
didDrawPage: (data) => {
pdf.text(
`${data.doc.internal.getCurrentPageInfo().pageNumber}`,
pdf.internal.pageSize.width / 2 - 10,
pdf.internal.pageSize.height - 4
);
},
});
});
pdf.save(`${name}.pdf`);
nextTick(() => {
if (doneCallback) {
doneCallback();
}
});
}
}

View File

@ -1,5 +1,5 @@
<template>
<a-spin :loading="loading" class="report-detail-container">
<a-spin :loading="loading" :tip="t('report.detail.exportingPdf')" class="report-detail-container">
<div id="report-detail" class="report-detail">
<div class="report-header">
<div class="flex-1 break-all">{{ detail.name }}</div>
@ -89,15 +89,8 @@
</div>
</div>
</div>
<div class="mt-[16px]">
<div
v-for="item of innerCardList"
v-show="showItem(item)"
:key="item.id"
class="card-item mt-[16px]"
:class="`${item.value}`"
>
<div v-for="item of innerCardList" :id="`${item.value}`" :key="item.id" class="card-item mt-[16px]">
<div class="wrapper-preview-card">
<div class="flex items-center justify-between">
<div v-if="item.value !== ReportCardTypeEnum.CUSTOM_CARD" class="mb-[8px] font-medium">
@ -111,43 +104,8 @@
:share-id="shareId"
is-preview
/>
<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>
<div v-else-if="item.value === ReportCardTypeEnum.FUNCTIONAL_DETAIL" id="functionalCase">
<MsBaseTable v-bind="caseTableProps">
<template #caseLevel="{ record }">
<CaseLevel :case-level="record.priority" />
</template>
<template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.executeResult" />
</template>
</MsBaseTable>
</div>
<MsBaseTable
v-else-if="item.value === ReportCardTypeEnum.API_CASE_DETAIL"
v-bind="useApiTable.propsRes.value"
>
<template #priority="{ record }">
<caseLevel :case-level="record.priority" />
</template>
<template #lastExecResult="{ record }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="record.executeResult" />
</template>
</MsBaseTable>
<MsBaseTable
v-else-if="item.value === ReportCardTypeEnum.SCENARIO_CASE_DETAIL"
v-bind="useScenarioTable.propsRes.value"
>
<template #priority="{ record }">
<caseLevel :case-level="record.priority" />
</template>
<template #lastExecResult="{ record }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="record.executeResult" />
</template>
</MsBaseTable>
<div v-else-if="item.value === ReportCardTypeEnum.CUSTOM_CARD" v-html="item.content"></div>
<div v-else-if="item.value === ReportCardTypeEnum.SUMMARY" v-html="detail.summary"></div>
</div>
</div>
</div>
@ -157,16 +115,13 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsChart from '@/components/pure/chart/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { lastExecuteResultMap } from '@/components/business/ms-case-associate/utils';
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/system-card/executeAnalysis.vue';
import ReportDetailTable from '@/views/test-plan/report/detail/component/system-card/reportDetailTable.vue';
@ -190,6 +145,7 @@
statusConfig,
toolTipConfig,
} from '@/config/testPlan';
import exportPDF, { PAGE_PDF_WIDTH_RATIO } from '@/hooks/useExportPDF';
import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils';
import exportPdf, { MAX_CANVAS_HEIGHT, SCALE_RATIO } from '@/utils/exportPdf';
@ -201,25 +157,21 @@
ReportMetricsItemModel,
StatusListType,
} from '@/models/testPlan/testPlanReport';
import { customValueForm } from '@/models/testPlan/testPlanReport';
import { ReportEnum } from '@/enums/reportEnum';
import { ReportCardTypeEnum } from '@/enums/testPlanReportEnum';
import { defaultGroupConfig, defaultSingleConfig } from './component/reportConfig';
import { getSummaryDetail } from '@/views/test-plan/report/utils';
import html2canvas from 'html2canvas-pro';
import { ColumnInput, RowInput } from 'jspdf-autotable';
const { t } = useI18n();
const route = useRoute();
const innerCardList = defineModel<configItem[]>('cardList', {
default: [],
});
const innerCardList = ref<configItem[]>([]);
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const reportId = ref<string>(route.query.id as string);
const isGroup = computed(() => route.query.type === 'GROUP');
const loading = ref<boolean>(false);
const loading = ref<boolean>(true);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '',
});
@ -399,19 +351,6 @@
];
});
function showItem(item: configItem) {
switch (item.value) {
case ReportCardTypeEnum.FUNCTIONAL_DETAIL:
return functionalCaseTotal.value > 0;
case ReportCardTypeEnum.API_CASE_DETAIL:
return apiCaseTotal.value > 0;
case ReportCardTypeEnum.SCENARIO_CASE_DETAIL:
return scenarioCaseTotal.value > 0;
default:
return true;
}
}
const cardCount = computed(() => {
const totalList = [functionalCaseTotal.value, apiCaseTotal.value, scenarioCaseTotal.value];
let count = 2;
@ -422,25 +361,24 @@
});
return count;
});
const originLayoutInfo = ref([]);
const currentMode = ref<string>('drawer');
async function getDefaultLayout() {
try {
const res = await getReportLayout(detail.value.id, shareId.value);
const result = res.map((item: any) => {
return {
id: item.id,
value: item.name,
label: item.label,
content: item.value || '',
type: item.type,
enableEdit: false,
richTextTmpFileIds: item.richTextTmpFileIds,
};
});
innerCardList.value = result;
originLayoutInfo.value = cloneDeep(result);
innerCardList.value = res
.filter((e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value))
.map((item: any) => {
return {
id: item.id,
value: item.name,
label: item.label,
content: item.value || '',
type: item.type,
enableEdit: false,
richTextTmpFileIds: item.richTextTmpFileIds,
};
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -449,24 +387,6 @@
const isDefaultLayout = ref<boolean>(false);
//
function getContent(item: configItem): customValueForm {
if (isDefaultLayout.value) {
return {
content: richText.value.summary || '',
label: t(item.label),
richTextTmpFileIds: [],
};
}
return {
content: item.content || '',
label: t(item.label),
richTextTmpFileIds: item.richTextTmpFileIds,
};
}
const currentMode = ref<string>('drawer');
/** 缺陷明细 */
const bugColumns: MsTableColumn = [
{
@ -497,16 +417,6 @@
const reportBugList = () => {
return !shareId.value ? getReportBugList : getReportShareBugList;
};
const {
propsRes: bugTableProps,
loadList: loadBugList,
setLoadListParams: setLoadBugListParams,
} = useTable(reportBugList(), {
scroll: { x: '100%', y: 'auto' },
columns: bugColumns,
showSelectorAll: false,
hoverable: false,
});
/** 用例明细 */
const staticColumns: MsTableColumn = [
@ -571,18 +481,6 @@
const reportFeatureCaseList = () => {
return !shareId.value ? getReportFeatureCaseList : getReportShareFeatureCaseList;
};
const {
propsRes: caseTableProps,
loadList: loadCaseList,
setLoadListParams: setLoadCaseListParams,
setPagination: setCasePagination,
} = useTable(reportFeatureCaseList(), {
scroll: { x: '100%', y: 'auto' },
columns: caseColumns.value,
heightUsed: 20,
showSelectorAll: false,
hoverable: false,
});
/** 接口/场景明细 */
const apiStaticColumns: MsTableColumn = [
@ -605,31 +503,72 @@
title: 'common.executionResult',
dataIndex: 'executeResult',
slotName: 'lastExecResult',
width: 80,
width: 100,
},
];
const apiColumns = computed(() => {
if (isGroup.value) {
return [...apiStaticColumns, ...testPlanNameColumns, ...lastStaticColumns];
return [
...apiStaticColumns,
...testPlanNameColumns,
...lastStaticColumns.filter((e) => e.dataIndex !== 'priority'),
];
}
return [...apiStaticColumns, ...lastStaticColumns];
return [...apiStaticColumns, ...lastStaticColumns.filter((e) => e.dataIndex !== 'priority')];
});
const useApiTable = useTable(getApiPage, {
scroll: { x: '100%', y: 'auto' },
columns: apiColumns.value,
showSelectorAll: false,
showSetting: false,
hoverable: false,
});
const useScenarioTable = useTable(getScenarioPage, {
scroll: { x: '100%', y: 'auto' },
columns: apiColumns.value,
showSelectorAll: false,
showSetting: false,
hoverable: false,
});
const fullCaseList = ref<any>([]);
async function initCaseList() {
fullCaseList.value = (
await reportFeatureCaseList()({
current: 1,
pageSize: 500,
reportId: reportId.value,
shareId: shareId.value ?? undefined,
startPager: false,
})
).list;
}
const fullBugList = ref<any>([]);
async function initBugList() {
fullBugList.value = (
await reportBugList()({
current: 1,
pageSize: 500,
reportId: reportId.value,
shareId: shareId.value ?? undefined,
startPager: false,
})
).list;
}
const fullApiList = ref<any>([]);
async function initApiList() {
fullApiList.value = (
await getApiPage({
current: 1,
pageSize: 500,
reportId: reportId.value,
shareId: shareId.value ?? undefined,
startPager: false,
})
).list;
}
const fullScenarioList = ref<any>([]);
async function initScenarioList() {
fullScenarioList.value = (
await getScenarioPage({
current: 1,
pageSize: 500,
reportId: reportId.value,
shareId: shareId.value ?? undefined,
startPager: false,
})
).list;
}
async function getDetail() {
try {
@ -643,50 +582,104 @@
if (!defaultLayout && id) {
getDefaultLayout();
} else {
innerCardList.value = isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig);
innerCardList.value = (isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig)).filter(
(e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value)
);
}
setLoadBugListParams({ reportId: reportId.value, shareId: shareId.value ?? undefined, pageSize: 500 });
setLoadCaseListParams({ reportId: reportId.value, shareId: shareId.value ?? undefined, pageSize: 500 });
useApiTable.setLoadListParams({
reportId: reportId.value,
shareId: shareId.value ?? undefined,
pageSize: 500,
await Promise.all([initBugList(), initCaseList(), initApiList(), initScenarioList()]);
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'));
}
);
});
useScenarioTable.setLoadListParams({
reportId: reportId.value,
shareId: shareId.value ?? undefined,
pageSize: 500,
});
await Promise.all([loadBugList(), loadCaseList(), useApiTable.loadList(), useScenarioTable.loadList()]);
setTimeout(() => {
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);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
@ -696,6 +689,12 @@
});
</script>
<style lang="less">
.arco-spin-mask-icon {
@apply !fixed;
}
</style>
<style lang="less" scoped>
.report-detail-container {
@apply flex justify-center;

View File

@ -57,4 +57,6 @@ export default {
'report.detail.systemInternalTooltip': 'System built-in, not editable',
'report.detail.reportNameNotEmpty': 'The report name cannot be empty',
'report.detail.manualGenReportSuccess': 'The custom generated report was successful',
'report.detail.exportingPdf': 'Exporting PDF report...',
'report.detail.exportPdfSuccess': 'PDF report exported successfully',
};

View File

@ -57,4 +57,6 @@ export default {
'report.detail.systemInternalTooltip': '系统内置,不可编辑',
'report.detail.reportNameNotEmpty': '报告名称不能为空',
'report.detail.manualGenReportSuccess': '自定义生成报告成功',
'report.detail.exportingPdf': 'PDF报告导出中...',
'report.detail.exportPdfSuccess': 'PDF报告导出成功',
};

View File

@ -1,27 +0,0 @@
// eslint-disable-next-line import/default
import exportPDFWorker from './exportPDFWorker?worker';
import html2canvas from 'html2canvas-pro';
// eslint-disable-next-line new-cap
const worker = new exportPDFWorker();
worker.onmessage = (event: MessageEvent) => {
const { name, pdfBlob } = event.data;
const url = URL.createObjectURL(pdfBlob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const exportPdf = async (name: string, contentId: string) => {
const element = document.getElementById(contentId);
if (element) {
// await replaceSvgWithBase64(element);
}
};
export default exportPdf;

View File

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
content: ['./index.html', './src/**/*.{vue,ts,jsx,tsx}'],
theme: {
extend: {},
},

View File

@ -12,7 +12,8 @@
"build/**/*.d.ts",
"mock/**/*.ts",
"__test__/**/*.ts",
"node_modules/monaco-editor/monaco.d.ts"
"node_modules/monaco-editor/monaco.d.ts",
"src/views/test-plan/report/detail/alibabapuhuiti.js"
], // TS
"compilerOptions": {
"allowJs": true, // JSJSX