mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-11-29 18:48:13 +08:00
feat(报告): 测试计划独立报告导出 pdf 终版
This commit is contained in:
parent
7ed7a70e84
commit
40bdb9fe2e
@ -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
|
@ -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
@ -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];
|
||||
|
@ -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: '',
|
||||
},
|
||||
};
|
||||
|
169
frontend/src/hooks/useExportPDF.ts
Normal file
169
frontend/src/hooks/useExportPDF.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -57,4 +57,6 @@ export default {
|
||||
'report.detail.systemInternalTooltip': '系统内置,不可编辑',
|
||||
'report.detail.reportNameNotEmpty': '报告名称不能为空',
|
||||
'report.detail.manualGenReportSuccess': '自定义生成报告成功',
|
||||
'report.detail.exportingPdf': 'PDF报告导出中...',
|
||||
'report.detail.exportPdfSuccess': 'PDF报告导出成功',
|
||||
};
|
||||
|
@ -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;
|
@ -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: {},
|
||||
},
|
||||
|
@ -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, // 允许编译器编译JS,JSX文件
|
||||
|
Loading…
Reference in New Issue
Block a user