一.实现效果
页面中点击预览陈诉,实现将vue组件变成pdf文件进行弹窗展示
定义的方法文件
- import html2canvas from "html2canvas";
- import jsPDF, { RGBAData } from "jspdf";
- /** a4纸的尺寸[595.28,841.89], 单位毫米 */
- const [PAGE_WIDTH, PAGE_HEIGHT] = [595.28, 841.89];
- const PAPER_CONFIG: any = {
- /** 竖向 */
- portrait: {
- height: PAGE_HEIGHT,
- width: PAGE_WIDTH,
- contentWidth: 560,
- },
- /** 横向 */
- landscape: {
- height: PAGE_WIDTH,
- width: PAGE_HEIGHT,
- contentWidth: 800,
- },
- };
- // 将元素转化为canvas元素
- // 通过 放大 提高清晰度
- // width为内容宽度
- async function toCanvas(element: HTMLElement, width: number) {
- if (!element) return { width, height: 0 };
- // canvas元素
- const canvas = await html2canvas(element, {
- allowTaint: true, // 允许渲染跨域图片
- scale: window.devicePixelRatio * 2, // 增加清晰度
- useCORS: true, // 允许跨域
- });
- // 获取canvas转化后的宽高
- const { width: canvasWidth, height: canvasHeight } = canvas;
- // html页面生成的canvas在pdf中的高度
- const height = (width / canvasWidth) * canvasHeight;
- // 转化成图片Data
- const canvasData = canvas.toDataURL("image/jpeg", 1.0);
- return { width, height, data: canvasData };
- }
- /**
- * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
- * @param param0
- * @returns
- */
- export async function outputPDF({
- /** pdf内容的dom元素 */
- element,
- /** 页脚dom元素 */
- footer,
- /** 页眉dom元素 */
- header,
- /** pdf文件名 */
- filename,
- /** a4值的方向: portrait or landscape */
- orientation = "portrait" as "portrait" | "landscape",
- }: any) {
- if (!(element instanceof HTMLElement)) {
- return;
- }
- if (!["portrait", "landscape"].includes(orientation)) {
- return Promise.reject(
- new Error(
- `Invalid Parameters: the parameter {orientation} is assigned wrong value, you can only assign it with {portrait} or {landscape}`
- )
- );
- }
- const [A4_WIDTH, A4_HEIGHT] = [
- PAPER_CONFIG[orientation].width,
- PAPER_CONFIG[orientation].height,
- ];
- /** 一页pdf的内容宽度, 左右预设留白 */
- const { contentWidth } = PAPER_CONFIG[orientation];
- // eslint-disable-next-line new-cap
- const pdf = new jsPDF({
- unit: "pt",
- format: "a4",
- orientation,
- });
- // 一页的高度, 转换宽度为一页元素的宽度
- const { width, height, data } = await toCanvas(element, contentWidth);
- // 添加
- function addImage(
- _x: number,
- _y: number,
- pdfInstance: jsPDF,
- base_data:
- | string
- | HTMLImageElement
- | HTMLCanvasElement
- | Uint8Array
- | RGBAData,
- _width: number,
- _height: number
- ) {
- pdfInstance.addImage(base_data, "JPEG", _x, _y, _width, _height);
- }
- // 增加空白遮挡
- function addBlank(x: number, y: number, _width: number, _height: number) {
- pdf.setFillColor(255, 255, 255);
- pdf.rect(x, y, Math.ceil(_width), Math.ceil(_height), "F");
- }
- // 页脚元素 经过转换后在PDF页面的高度
- const { height: tFooterHeight, data: headerData } = footer
- ? await toCanvas(footer, contentWidth)
- : { height: 0, data: undefined };
- // 页眉元素 经过转换后在PDF的高度
- const { height: tHeaderHeight, data: footerData } = header
- ? await toCanvas(header, contentWidth)
- : { height: 0, data: undefined };
- // 添加页脚
- async function addHeader(_headerElement: HTMLElement) {
- headerData &&
- pdf.addImage(headerData, "JPEG", 0, 0, contentWidth, tHeaderHeight);
- }
- // 添加页眉
- async function addFooter(
- _pageNum: number,
- _now: number,
- _footerElement: HTMLElement
- ) {
- if (footerData) {
- pdf.addImage(
- footerData,
- "JPEG",
- 0,
- A4_HEIGHT - tFooterHeight,
- contentWidth,
- tFooterHeight
- );
- }
- }
- // 距离PDF左边的距离,/ 2 表示居中
- const baseX = (A4_WIDTH - contentWidth) / 2; // 预留空间给左边
- // 距离PDF 页眉和页脚的间距, 留白留空
- const baseY = 15;
- // 除去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
- const originalPageHeight =
- A4_HEIGHT - tFooterHeight - tHeaderHeight - 2 * baseY;
- // 元素在网页页面的宽度
- const elementWidth = element.offsetWidth;
- // PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度 转化为 距离Canvas顶部的高度
- const rate = contentWidth / elementWidth;
- // 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离
- const pages = [rate * getElementTop(element)];
- // 获取该元素到页面顶部的高度(注意滑动scroll会影响高度)
- function getElementTop(contentElement: any) {
- if (contentElement.getBoundingClientRect) {
- const rect = contentElement.getBoundingClientRect() || {};
- const topDistance = rect.top;
- return topDistance;
- }
- }
- // 遍历正常的元素节点
- function traversingNodes(nodes: any) {
- for (const element of nodes) {
- const one = element;
- /** */
- /** 注意: 可以根据业务需求,判断其他场景的分页,本代码只判断表格的分页场景 */
- /** */
- // table的每一行元素也是深度终点
- const isTableRow =
- one.classList && one.classList.contains("ant4-table-row");
- // 需要判断跨页且内部存在跨页的元素
- const isDivideInside =
- one.classList && one.classList.contains("divide-inside");
- // 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断
- const { offsetHeight } = one;
- // 计算出最终高度
- const offsetTop = getElementTop(one);
- // dom转换后距离顶部的高度
- // 转换成canvas高度
- const top = rate * offsetTop;
- const rateOffsetHeight = rate * offsetHeight;
- // 对于深度终点元素进行处理
- if (isTableRow || isDivideInside) {
- // dom高度转换成生成pdf的实际高度
- // 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
- updateTablePos(rateOffsetHeight, top);
- }
- // // 对于需要进行分页且内部存在需要分页(即不属于深度终点)的元素进行处理
- // 对于普通元素,则判断是否高度超过分页值,并且深入
- else {
- // 执行位置更新操作
- updateNormalElPos(top);
- // 遍历子节点
- traversingNodes(one.childNodes);
- }
- updatePos();
- }
- }
- // 普通元素更新位置的方法
- // 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点
- function updateNormalElPos(top: any) {
- if (
- top - (pages.length > 0 ? pages[pages.length - 1] : 0) >=
- originalPageHeight
- ) {
- pages.push(
- (pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight
- );
- }
- }
- // 可能跨页元素位置更新的方法
- // 需要考虑分页元素,则需要考虑两种情况
- // 1. 普通达顶情况,如上
- // 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点
- function updateTablePos(eHeight: number, top: number) {
- // 如果高度已经超过当前页,则证明可以分页了
- if (
- top - (pages.length > 0 ? pages[pages.length - 1] : 0) >=
- originalPageHeight
- ) {
- pages.push(
- (pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight
- );
- }
- // 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置
- else if (
- top + eHeight - (pages.length > 0 ? pages[pages.length - 1] : 0) >
- originalPageHeight &&
- top !== (pages.length > 0 ? pages[pages.length - 1] : 0)
- ) {
- pages.push(top);
- }
- }
- // 深度遍历节点的方法
- traversingNodes(element.childNodes);
- function updatePos() {
- while (pages[pages.length - 1] + originalPageHeight < height) {
- pages.push(pages[pages.length - 1] + originalPageHeight);
- }
- }
- function dataURLtoFile(dataurl: any, filename: any) {
- var arr = dataurl.split(","),
- mime = arr[0].match(/:(.*?);/)[1],
- bstr = atob(arr[1]),
- n = bstr.length,
- u8arr = new Uint8Array(n);
- while (n--) {
- u8arr[n] = bstr.charCodeAt(n);
- }
- return new File([u8arr], filename, { type: mime });
- }
- // 对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,
- // 所以要把它修正,让其值是以真实的打印元素顶部节点为准
- const newPages = pages.map((item) => item - pages[0]);
- // 根据分页位置 开始分页
- for (let i = 0; i < newPages.length; ++i) {
- // 根据分页位置新增图片
- addImage(
- baseX,
- baseY + tHeaderHeight - newPages[i],
- pdf,
- data!,
- width,
- height
- );
- // 将 内容 与 页眉之间留空留白的部分进行遮白处理
- addBlank(0, tHeaderHeight, A4_WIDTH, baseY);
- // 将 内容 与 页脚之间留空留白的部分进行遮白处理
- addBlank(0, A4_HEIGHT - baseY - tFooterHeight, A4_WIDTH, baseY);
- // 对于除最后一页外,对 内容 的多余部分进行遮白处理
- if (i < newPages.length - 1) {
- // 获取当前页面需要的内容部分高度
- const imageHeight = newPages[i + 1] - newPages[i];
- // 对多余的内容部分进行遮白
- addBlank(
- 0,
- baseY + imageHeight + tHeaderHeight,
- A4_WIDTH,
- A4_HEIGHT - imageHeight
- );
- }
- // 添加页眉
- if (header) {
- await addHeader(header);
- }
- // 添加页脚
- if (footer) {
- await addFooter(newPages.length, i + 1, footer);
- }
- // 若不是最后一页,则分页
- if (i !== newPages.length - 1) {
- // 增加分页
- pdf.addPage();
- }
- }
- let pdfDataTemp = pdf.output("datauristring"); //获取base64Pdf
- let myfileTemp = dataURLtoFile(pdfDataTemp, "选址评测报告" + ".pdf"); //调用一下下面的转文件流函数
- pdf.save(filename);
- return {
- pdfDataTemp,
- myfileTemp,
- };
- }
- const htmlToPdf = {
- async getPdf() {
- const element = document.querySelector(".pdf-panel");
- const { pdfDataTemp, myfileTemp }: any =await outputPDF({
- element,
- filename: `选址评测报告`,
- orientation: "portrait",
- });
- return {
- pdfBase64: pdfDataTemp,
- files: myfileTemp,
- };
- },
- };
- export default htmlToPdf;
复制代码 定义必要变成pdf的组件文件 ExportReportPDF .vue
- <div class="ctn">
- <div class="pdf-ctn">
- <div class="pdf-panel">
-
- 需要生成pdf的内容
- </div>
- </div>
- <div>
- </div>
- </div>
复制代码- <style lang="scss" scoped>
- .ctn {
- position: fixed;
- top: 0;
- left: 0;
- z-index: -1;
- overflow: scroll;
- position: relative;
- .pdf-ctn {
- width: 1300px;
- .pdf-panel {
- position: relative;
- }
- }
- }
- </style>
复制代码 引入到预览陈诉的页面中
- import ExportReportPDF from "./exportReportPDF/index.vue";
- import htmlToPdf from "./exportReportPDF/pdf-print.ts";
复制代码 利用,生成 fileUrl
- const getPdfObj :any= await htmlToPdf.getPdf();
- files.value = getPdfObj.files
- pdfBase64.value = getPdfObj.pdfBase64
- let blob = dataURLtoBlob(getPdfObj.pdfBase64)
- fileUrl.value = window.URL.createObjectURL(blob)
复制代码 在iframe利用
- <iframe width="90%" height="90%" :src="`${fileUrl}`"></iframe>
复制代码 题目点:
- 会出现 185ms Unable to clone canvas as it is tainted 题目导致白屏
- 生成的base64过大,iframe显示不出来
办理方法:
1.pdf方法进行将每一个生成一个图片canvas然后组合成为整体一个canvas
2.利用dataURLtoBlob和createObjectURL生成url显示到iframe上,不能直接用base64文件
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |