VUE中通过DOM导出PDF

最终效果

在这里插入图片描述

前端导出PDF的核心在于样式的绘制上,这里其实直接使用CSS进行绘制和布局就行,只不过需要计算好每页DIV盒子的大小,防止一页放不下造成样式错乱。

项目依赖

项目是Vue3 + TS

npm i html2canvas@1.4.1
npm i jspdf@3.0.1

工具类(htmlToPdf.ts)

import {App} from 'vue';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';declare module '@vue/runtime-core' {interface ComponentCustomProperties {htmlToPdfInstall: (name: string, title: string) => Promise<void>;htmlToPdfPrint: (name: string, title: string) => Promise<void>;}
}export default {// 下载install(app: App) {app.config.globalProperties.htmlToPdfInstall = async function (name: string, title: string): Promise<void> {const ele = document.querySelector(`#${name}`) as HTMLElement;if (!ele) {console.error(`Element with id '${name}' not found.`);return;}const eleW = ele.offsetWidth;const eleH = ele.offsetHeight;const eleOffsetTop = ele.offsetTop;const eleOffsetLeft = ele.offsetLeft;const canvas = document.createElement('canvas');let abs = 0;const win_in = document.documentElement.clientWidth || document.body.clientWidth;const win_out = window.innerWidth;if (win_out > win_in) {abs = (win_out - win_in) / 2;}canvas.width = eleW * 2;canvas.height = eleH * 2;const context = canvas.getContext('2d');if (!context) return;context.scale(2, 2);context.translate(-eleOffsetLeft - abs, -eleOffsetTop);const pdf = new jsPDF('', 'pt', 'a4');const childrenBox = Array.from(ele.children);const pdfItem = []for (let i = 0; i < childrenBox.length; i++) {pdfItem.push(...Array.from(childrenBox[i].children))}for (let i = 0; i < pdfItem.length; i++) {const res = await html2canvas(pdfItem[i] as HTMLElement, {dpi: 300,useCORS: true,scale: 4,});const pageData = res.toDataURL('image/jpeg', 1.0);const contentWidth = res.width;const contentHeight = res.height;const imgWidth = 555.28;const imgHeight = (552.28 / contentWidth) * contentHeight;pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight);// 添加页码(居中底部)const pageNum = i + 1;const pageCount = pdfItem.length;pdf.setFontSize(8);pdf.setTextColor(96, 98, 102); // 设置字体颜色为 #606266 (RGB: 96, 98, 102)pdf.text(`- ${pageNum} / ${pageCount} -`, pdf.internal.pageSize.getWidth() / 2, pdf.internal.pageSize.getHeight() - 10, {align: 'center'});if (i < pdfItem.length - 1) {pdf.addPage();}}pdf.save(`${title}.pdf`);};app.config.globalProperties.htmlToPdfPrint = async function (name: string, title: string): Promise<void> {const ele = document.querySelector(`#${name}`) as HTMLElement;if (!ele) {console.error(`Element with id '${name}' not found.`);return;}const eleW = ele.offsetWidth;const eleH = ele.offsetHeight;const eleOffsetTop = ele.offsetTop;const eleOffsetLeft = ele.offsetLeft;const canvas = document.createElement('canvas');let abs = 0;const win_in = document.documentElement.clientWidth || document.body.clientWidth;const win_out = window.innerWidth;if (win_out > win_in) {abs = (win_out - win_in) / 2;}canvas.width = eleW * 2;canvas.height = eleH * 2;const context = canvas.getContext('2d');if (!context) return;context.scale(2, 2);context.translate(-eleOffsetLeft - abs, -eleOffsetTop);const pdf = new jsPDF('', 'pt', 'a4');const childrenBox = Array.from(ele.children);const pdfItem = [];for (let i = 0; i < childrenBox.length; i++) {pdfItem.push(...Array.from(childrenBox[i].children));}for (let i = 0; i < pdfItem.length; i++) {const res = await html2canvas(pdfItem[i] as HTMLElement, {dpi: 300,useCORS: true,scale: 4,});const pageData = res.toDataURL('image/jpeg', 1.0);const contentWidth = res.width;const contentHeight = res.height;const imgWidth = 555.28;const imgHeight = (552.28 / contentWidth) * contentHeight;pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight);// 添加页码(居中底部)const pageNum = i + 1;const pageCount = pdfItem.length;pdf.setFontSize(8);pdf.setTextColor(96, 98, 102); // 设置字体颜色为 #606266 (RGB: 96, 98, 102)pdf.text(`- ${pageNum} / ${pageCount} -`, pdf.internal.pageSize.getWidth() / 2, pdf.internal.pageSize.getHeight() - 10, {align: 'center'});if (i < pdfItem.length - 1) {pdf.addPage();}}// 👉 自动打开打印预览并调用 print()const blob = pdf.output('blob');const blobUrl = URL.createObjectURL(blob);// 👉 创建隐藏 iframeconst iframe = document.createElement('iframe');iframe.style.display = 'none';iframe.src = blobUrl;document.body.appendChild(iframe);iframe.onload = function () {setTimeout(() => {iframe.contentWindow?.focus();iframe.contentWindow?.print();}, 300);}}}
}

上面工具类中有两个方法:htmlToPdfInstall 是将html转化成PDF并在浏览器下载;htmlToPdfPrint 是将html转化成PDF然后实现打印预览,这两个方法的转化PDF都是 html2canvas 进行转化,区别是一个是浏览器下载,一个是预览打印,并且两个都添加了分页效果。

全局注册htmlToPdf工具

main.ts中注册函数

// html转PDF
import htmlToPdf from '@/utils/htmlToPdf';
const app = createApp(App)
app.use(htmlToPdf)
app.mount('#app1')

global.d.ts中添加如下代码:

import { ComponentCustomProperties } from 'vue';declare module '@vue/runtime-core' {interface ComponentCustomProperties {htmlToPdfInstall: (name: string, title: string) => Promise<void>;htmlToPdfPrint: (name: string, title: string) => Promise<void>;}
}

编写页面样式

html代码

<!--  报名信息导出预览  --><div v-if="isRegisterInfo" class="register_pdf_view_warp"><div class="register_pdf_view_title">报名信息预览</div><div class="register_pdf_view_btn"><el-buttontype="warning"plainsize="small"@click="printPDF":loading="exportLoading"><Icon icon="ep:view" class="mr-5px"/>打印预览</el-button><el-buttontype="primary"plainsize="small"@click="exportPDF":loading="exportLoading"><Icon icon="ep:download" class="mr-5px"/>下载</el-button><Icon icon="ep:circle-close" color="#303133" @click="closeRegisterInfo" size="20" class="preview_close_icon"/></div><div ref="registerPdfView" class="register_pdf_view_con"><div id="contentToExport"><div v-for="(item,index) in teamAndMemberInfo" :key="index" class="register_pdf_view_item"><div v-for="(member, i) in item.memberTotalPages" :key="i" class="register_pdf_view_item_table"><div class="register_pdf_view_item_title"><el-row :gutter="10"><el-col :span="12"><span class="register_pdf_team_info_label">&nbsp;&nbsp;队:</span><span>{{item.teamName}}</span></el-col></el-row><el-row :gutter="10"><el-col :span="12"><span class="register_pdf_team_info_label">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;队:</span><span>{{item.leader}}</span></el-col><el-col :span="12"><span class="register_pdf_team_info_label">联系电话:</span><span>{{item.leaderPhone}}</span></el-col></el-row><el-row :gutter="10"><el-col :span="12"><span class="register_pdf_team_info_label">&nbsp;&nbsp;练:</span><span>{{item.coach}}</span></el-col><el-col :span="12"><span class="register_pdf_team_info_label">联系电话:</span><span>{{item.coachPhone}}</span></el-col></el-row><el-row :gutter="10"><el-col :span="12"><span class="register_pdf_team_info_label">助理教练:</span><span>{{item.assistantCoach}}</span></el-col><el-col :span="12"><span class="register_pdf_team_info_label">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;医:</span><span>{{item.teamDoctor}}</span></el-col></el-row></div><el-table border:cell-style="{ textAlign: 'center' }":header-cell-style="{textAlign: 'center',background: 'var(--el-table-row-hover-bg-color)',color: 'var(--el-text-color-primary)'}":data="item['memberList'+ i]":stripe="true":show-overflow-tooltip="true"><el-table-column type="index" label="序号" width="60"/><el-table-column prop="role" label="身份"/><el-table-column prop="name" label="姓名" width="100"/><el-table-column prop="sex" label="性别" width="60"/><el-table-column prop="hbw" label="身高/体重" width="120"/><el-table-column prop="raceNum" label="比赛号码" width="100"/><el-table-column prop="idCard" label="身份证号" width="180"/><el-table-column prop="clothNum" label="服装号码" width="90"/></el-table></div><div v-for="(photo, i) in item.photoTotalPages" :key="i" class="register_pdf_view_item_list_warp"><div class="register_pdf_view_item_list_title">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;名:{{item.teamName}}</div><div class="register_pdf_view_item_list"><div v-for="item1 in item['photoInfoList' + i]" :key="item1" class="register_pdf_view_item_list_item_warp"><el-image class="list_item_img" fit="contain":src="item1.imagePath" :preview-src-list="[item1.imagePath]" /><div class="list_item_info_warp"><span class="list_item_role">{{item1.role}}:</span><span class="list_item_name">{{item1.name}}</span></div><div class="list_item_info_id_card">{{item1.idCard}}</div><div v-if="item1.raceNumber" class="list_item_info_race_number">号码:{{item1.raceNumber}}</div></div></div></div></div></div></div></div>

ts代码

// 报名信息预览
const closeRegisterInfo = () => {isRegisterInfo.value = false
}// 预览pdf
const printPDF = () => {message.warning("报名表打印预览生成中,请耐心等待...")htmlToPdfPrint?.('contentToExport', '【'+ currentCompetition.value.matchName +'】报名信息表');
}// 导出pdf
const exportPDF = () => {message.warning("报名表导出中,请耐心等待...")htmlToPdfInstall?.('contentToExport', '【'+ currentCompetition.value.matchName +'】报名信息表');
}const pageSize = 24; // 每页大小
// 预览报名信息
const viewRegisterInfo = async () => {try {if (currentEvent.value.id){teamAndMemberInfo.value = await SmallEventRegisterTeamApi.getRegisterTeamAndMemberInfo(currentEvent.value.id);// 分页teamAndMemberInfo.value.forEach(item => {// 人员信息分页const memberTotalPages = Math.ceil(item.memberList.length / pageSize);item['memberTotalPages'] = memberTotalPagesfor (let i = 0; i < memberTotalPages; i++) {const start = i * pageSize;const end = start + pageSize;item[`memberList${i}`] = item.memberList.slice(start, end);}// 头像信息分页const photoTotalPages = Math.ceil(item.photoInfoList.length / pageSize);item['photoTotalPages'] = photoTotalPagesfor (let i = 0; i < photoTotalPages; i++) {const start = i * pageSize;const end = start + pageSize;item[`photoInfoList${i}`] = item.photoInfoList.slice(start, end);}})}else {message.error("请先选中项目,然后进行导出操作!!!")}} finally {isRegisterInfo.value = !isRegisterInfo.value}
}

提示:上面获取预览信息的数据来自于后端,这里进行数据处理,默认每页是24条数据,如果超出24条就直接换到下一页,这个大家可以根据自己的数据大小和多少动态的计算

CSS代码

这里其实对专业前端来说,小菜一碟。

.register_pdf_view_warp {position: absolute;top: 10px;left: 10px;width: 880px;padding: 15px;background-color: #FAFAFA;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);z-index: 999;height: 80vh;
}.register_pdf_view_title {font-size: 18px;font-weight: 600;color: #303133;margin-bottom: 15px;
}.register_pdf_view_con {overflow-x: auto;height: 90%;
}.register_pdf_view_item_table, .register_pdf_view_item_list_warp {border: 1px solid #f2f2f2;padding: 10px;margin-bottom: 10px;background-color: #fff;position: relative;height: 1210px;
}.register_pdf_view_item_title {width: 90%;color: #303133;font-size: 16px;margin: 20px 0 20px;padding: 0 20px;
}.register_pdf_view_item_list {display: grid;justify-content: center;grid-template-columns: repeat(4, 1fr);align-items: center;column-gap: 10px;row-gap: 10px;
}.list_item_img {width: 80px;height: 110px;
}.register_pdf_view_btn {position: absolute;top: 10px;right: 10px;display: flex;gap: 20px;
}.register_pdf_view_item_list_item_warp {text-align: center;color: #303133;font-size: 14px;
}.register_pdf_team_info_label {display: inline-block;width: 94px;text-align: justify;text-align-last: justify;line-height: 35px;
}.register_pdf_view_item_list_title {font-size: 16px;margin: 20px 0;padding-left: 20px;
}/* Webkit 浏览器:Chrome / Edge / Safari */
.register_pdf_view_con::-webkit-scrollbar {width: 8px; /* 垂直滚动条宽度 */height: 8px; /* 水平滚动条高度 */
}.register_pdf_view_con::-webkit-scrollbar-thumb {background-color: rgba(0, 0, 0, 0.2); /* 滚动条拖动块颜色 */border-radius: 4px;
}.register_pdf_view_con::-webkit-scrollbar-track {background-color: transparent; /* 滚动条轨道颜色 */
}/* 鼠标悬浮时滚动条更明显 */
.register_pdf_view_con::-webkit-scrollbar-thumb:hover {background-color: rgba(0, 0, 0, 0.3);
}

最终页面效果如下

页面预览

在这里插入图片描述

打印预览

在这里插入图片描述

下载效果

在这里插入图片描述

有疑问的话可以留言,大家一起交流讨论,如有问题的话,可以指出,看到后修改。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/79907.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringAI框架中的RAG模块详解及应用示例

SpringAI框架中的RAG模块详解及应用示例 RAG&#xff08;Retrieval-Augmented Generation&#xff09;可以通过检索知识库&#xff0c;克服大模型训练完成后参数冻结的局限性&#xff0c;携带知识让大模型根据知识进行回答。SpringAI框架提供了模块化的API来支持RAG&#xff0…

MySQL-数据查询(测试)-05-(12-1)

1-数据准备&#xff1a; CREATE TABLE 员工信息表 (员工编号 VARCHAR(10) PRIMARY KEY,姓名 VARCHAR(20),学历 VARCHAR(20),出生日期 DATE,性别 INT,工作年限 INT,地址 VARCHAR(100),电话号码 VARCHAR(20),员工部门号 INT ); INSERT INTO 员工信息表 (员工编号, 姓名, 学历, 出…

5G网络:能源管理的“智能电网“革命,Python如何成为关键推手?

5G网络:能源管理的"智能电网"革命,Python如何成为关键推手? 大家好,我是Echo_Wish。今天咱们聊一个既硬核又接地气的话题——5G网络如何用Python代码重构全球能源管理。 不知道你们有没有注意过: • 家里装了智能电表后,电费突然变"聪明"了,谷时充…

AI背景下,如何重构你的产品?

当AI敲门时&#xff0c;你的产品准备好开门了吗&#xff1f; 最近和做产品的老张聊天&#xff0c;他愁眉苦脸地说&#xff1a;"现在AI这么火&#xff0c;我们的产品就像个老古董&#xff0c;用户都跑隔壁用AI产品去了。“这话让我想起三年前另一个朋友&#xff0c;当时区…

互联网大厂Java面试实战:从Spring Boot到微服务的技术问答与解析

&#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通 &#x1f601; 2. 毕业设计专栏&#xff0c;毕业季咱们不慌忙&#xff0c;几百款毕业设计等你选。 ❤️ 3. Python爬虫专栏…

Apollo学习——aem问题

执行aem指令出现一下问题 lxflxf:~/MYFile/apollo_v10.0 $aem enter permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.49/containers/json?filters%7B%22name%22%…

数字IC后端零基础入门基础理论(Day2)

数字IC后端零基础入门基础理论&#xff08;Day1&#xff09; Placement Blockage: cell摆放阻挡层。它是用来引导工具做placement的一种物理约束或手段&#xff0c;目的是希望工具按照我们的要求来做标准单元的摆放。 它主要有三种类型&#xff0c;分别是hard placement bloc…

如何远程执行脚本不留痕迹

通常我们在做远程维护的时候&#xff0c;会有这么一个需求&#xff0c;就是我想在远程主机执行一个脚本&#xff0c;但是这个脚本我又不想保留在远程主机上&#xff0c;那么有人就说了&#xff0c;那就复制过去再登录远程执行不就行了吗&#xff1f;嗯嗯&#xff0c;但是这还不…

【Lua】java 调用redis执行 lua脚本

【Lua】java 调用redis执行 lua脚本 public Object executeLuaScript(String script, List<String> keys, Object... args) {// 注意: 这里 Long.class 是返回值类型, 一定要指定清楚 不然会报错return this.redisTemplate.execute(RedisScript.of(j脚本, Long.class), k…

利用混合磁共振成像 - 显微镜纤维束成像技术描绘结构连接组|文献速递-深度学习医疗AI最新文献

Title 题目 Imaging the structural connectome with hybrid MRI-microscopy tractography 利用混合磁共振成像 - 显微镜纤维束成像技术描绘结构连接组 01 文献速递介绍 通过多种模态绘制大脑结构能够增进我们对大脑功能、发育、衰老以及疾病的理解&#xff08;汉森等人&am…

Shell脚本实践(修改文件,修改配置文件,执行jar包)

1、前言 需要编写一个shell脚本支持 1、修改.so文件名 2、修改配置文件 3、执行jar包 2、代码解析 2.1、修改.so文件名 so_file_dir="/opt/casb/xxx/lib" # 处理.so文件 cd "$so_file_dir" || { echo "错误: 无法进入目录 $so_file_dir"; exit …

基于GPUGEEK 平台进行深度学习

一、平台简介 GPUGEEK 是一个专注于提供 GPU 算力租赁服务的平台&#xff0c;在人工智能与深度学习领域为用户搭建起便捷的算力桥梁。它整合了丰富多样的 GPU 资源&#xff0c;涵盖 RTX - 4090、RTX - 3090、A100 - PCIE 等多种型号&#xff0c;满足不同用户在模型训练、数据处…

Android Framework学习五:APP启动过程原理及速度优化

文章目录 APP启动优化概述APP启动流程点击图片启动APP的过程启动触发Zygote 与应用进程创建Zygote进程的创建应用进程初始化 ApplicationActivity 启动与显示 优化启动时黑白屏现象可优化的阶段Application阶段相关优化 Activity阶段数据加载阶段 Framework学习系列文章 APP启动…

Web 实时通信技术:WebSocket 与 Server-Sent Events (SSE) 深入解析

一、WebSocket&#xff1a; &#xff08;一&#xff09;WebSocket 是什么&#xff1f; WebSocket 是一种网络通信协议&#xff0c;它提供了一种在单个 TCP 连接上进行全双工通信的方式。与传统的 HTTP 请求 - 响应模型不同&#xff0c;WebSocket 允许服务器和客户端在连接建立…

MySQL(8)什么是主键和外键?

主键&#xff08;Primary Key&#xff09;和外键&#xff08;Foreign Key&#xff09;是关系数据库中用于定义和维护表之间关系的重要概念。以下是详细的解释、示例代码和操作步骤。 主键&#xff08;Primary Key&#xff09; 定义 主键是表中的一个或多个字段&#xff0c;其…

任意复杂度的 JSON 数据转换为多个结构化的 Pandas DataFrame 表格

以下是一个 完整、结构清晰、可运行的 Python 工具&#xff0c;用于将任意复杂度的 JSON 数据转换为多个结构化的 Pandas DataFrame 表格。该工具支持嵌套对象、嵌套数组&#xff0c;并通过主键和外键建立表之间的关联关系。 if __name__ "__main__":# 示例 JSON 数…

【SSL部署与优化​】​​HTTP/2与HTTPS的协同效应

HTTP/2与HTTPS的协同效应&#xff1a;为何HTTP/2强制要求TLS 1.2&#xff1f; HTTP/2是HTTP协议的现代升级版&#xff0c;旨在通过多路复用、头部压缩等技术提升性能。然而&#xff0c;HTTP/2的设计与部署与HTTPS&#xff08;TLS加密&#xff09;紧密相关&#xff0c;甚至强制…

爬虫请求频率应控制在多少合适?

爬虫请求频率的控制是一个非常重要的问题&#xff0c;它不仅关系到爬虫的效率&#xff0c;还涉及到对目标网站服务器的影响以及避免被封禁的风险。合理的请求频率需要根据多个因素来综合考虑&#xff0c;以下是一些具体的指导原则和建议&#xff1a; 一、目标网站的政策 查看网…

使用Visual Studio将C#程序发布为.exe文件

说明 .exe 是可执行文件&#xff08;Executable File&#xff09;的扩展名。这类文件包含计算机可以直接运行的机器代码指令&#xff0c;通常由编程语言&#xff08;如 C、C、C#、Python 等&#xff09;编译或打包生成。可以用于执行自动化操作&#xff08;执行脚本或批处理操…

分布式1(cap base理论 锁 事务 幂等性 rpc)

目录 分布式系统介绍 一、定义与概念 二、分布式系统的特点 三、分布式系统面临的挑战 四、分布式系统的常见应用场景 CAP 定理 BASE 理论 BASE理论是如何保证最终一致性的 分布式锁的常见使用场景有哪些&#xff1f; 1. 防止多节点重复操作 2. 资源互斥访问 3. 分…