# Vue 实现 PDF 预览与批量打印组件
Vue 使用PDFJS实现 PDF 批量预览与批量打印组件引言在现代 Web 应用中PDF 预览和打印功能是常见的需求尤其是在涉及文档管理、票据打印等场景。本文将介绍如何基于 Vue 2.0 和 PDF.js 实现一个功能完善的 PDF 预览组件支持本地文件上传、网络 PDF 加载、批量预览与打印、页面导航、缩放等功能。功能特性本组件具有以下核心功能✅ PDF 预览支持本地文件上传和网络 PDF✅ 批量 PDF 预览和打印✅ 页面导航上一页、下一页、跳转到指定页✅ 缩放功能放大、缩小、自适应窗口✅ 打印功能单页和批量打印✅ 左侧树形结构可选✅ 响应式布局技术栈Vue 2.0Ant Design VueUI 组件库PDF.jsPDF 渲染库整体思路预览逻辑使用PDF.js插件加载文件地址。动态引入PDF.js插件通过url直接走PDF插件getDocument方法得到pdf文档对象或使用antd附件上传组件通过FileReader将文件读取为base64编码结构PDF插件提供的getDocument方法解析pdf的路径为promise对象解析后得到pdf文档对象。将解析完得到的pdf对象通过getPage方法和页数得到每一页的页面并转为canvas通过page.render()渲染到页面上。打印逻辑使用浏览器打印调用print()将打印当前可见页面。创建一个div并通过样式设置在屏幕最底层并通过pdfDoc.getPage()渲染为canvas为实现更好的打印效果canvas可能部分浏览器不兼容且分页不好掌控图片可以使用page-break-after样式进行分页将canvas转为blob通过img方式加载blob将img插入到打印模版html中再将打印模版转为blob通过iframe加载打印使用iframe创建隔离环境当前页面打印核心实现1. 组件结构组件采用模态框形式主要包含以下部分左侧树形结构可选工具栏上传、缩放、预览、打印、批量打印预览、页面导航等PDF 显示区域使用方法1. 引入组件import PDFDemo from /views/demo/ PDFDemo.vue; export default { components: { PDFDemo } };代码优化建议错误处理增强添加更多的错误处理和用户提示提高组件的健壮性。性能优化对于大型 PDF可以考虑使用虚拟滚动或分页加载优化渲染任务管理避免不必要的渲染用户体验添加 PDF 加载进度条优化打印预览体验添加更多的快捷键支持代码结构将打印相关逻辑抽取为独立的工具函数优化组件的状态管理完整代码完整的组件代码如下template a-modal v-modelvisible titlePDF预览 width70% :bodyStyle{ height: 500px, overflow: auto } :footernull cancelhandleCancel div classpdf-container div v-ifisLeftTree classtree-container KeyTree v-modeltree.value refkeyTree modemof changetreeChange/KeyTree /div div classcontent-container !-- 工具栏 -- div classpdf-controls a-upload namefile :showUploadListfalse :beforeUploadbeforeUpload stylemargin-right: 10px a-button iconupload选择本地PDF/a-button /a-upload a-button clickhandleZoomOut :disabledscale 0.5 stylemargin-right: 8px a-icon typeminus / 缩小 /a-button a-button clickhandleZoomIn :disabledscale 3 stylemargin-right: 8px a-icon typeplus / 放大 /a-button a-button clickhandleFit stylemargin-right: 8px a-icon typefullscreen / 自适应 /a-button a-button clickhandlePrint stylemargin-right: 8px a-icon typeprinter / 打印 /a-button a-button v-ifpdfDocs.length 1 clickhandleBatchPrint stylemargin-right: 8px a-icon typeprinter / 批量打印 /a-button a-divider typevertical stylemargin: 0 10px / !-- PDF切换控件 -- template v-ifpdfDocs.length 1 a-button clickprevPdf :disabledcurrentPdfIndex 0 stylemargin-right: 8px a-icon typeleft / 上一个PDF /a-button a-button clicknextPdf :disabledcurrentPdfIndex pdfDocs.length - 1 stylemargin-right: 8px 下一个PDF a-icon typeright / /a-button span stylemargin-right: 10pxPDF {{ currentPdfIndex 1 }} / {{ pdfDocs.length }}/span /template a-divider typevertical stylemargin: 0 10px / a-button clickprevPage :disabledcurrentPage 1 stylemargin-right: 8px a-icon typeleft / 上一页 /a-button a-button clicknextPage :disabledcurrentPage totalPages stylemargin-right: 8px 下一页 a-icon typeright / /a-button span stylemargin-right: 10px第 {{ currentPage }} / {{ totalPages }} 页/span a-input-number v-modelcurrentPage :min1 :maxtotalPages changegoToPage stylewidth: 80px / /div !-- PDF显示区域 -- div classpdf-canvas refpdfCanvasContainer canvas refpdfCanvas/canvas /div /div /div /a-modal /template script import KeyTree from /components/Key/KeyTree; export default { name: PDFDemo, components: { KeyTree }, data() { return { pdfUrl: , // 示例PDF文件 pdfDocs: [], // 存储多个PDF文档 currentPage: 1, totalPages: 0, scale: 1.5, pdfjsLoaded: false, tree: {}, visible: false, recordData: {}, apiPrefix: {}, isLeftTree: false, renderTask: null, // 用于存储当前正在进行的渲染任务 currentPdfIndex: 0, // 当前显示的PDF索引 batchRecords: [] // 批量预览的记录 }; }, beforeDestroy() { // 移除事件监听 window.removeEventListener(resize, this.handleResize); // 取消正在进行的渲染任务 if (this.renderTask) { this.renderTask.cancel(); } }, methods: { // 弹窗打开事件 show(record, api, isLeftTree false) { // 重置状态 this.isLeftTree isLeftTree; this.apiPrefix api; this.pdfDoc null; this.pdfDocs []; this.currentPage 1; this.totalPages 0; this.scale 1.5; this.renderTask null; this.currentPdfIndex 0; this.batchRecords record; this.recordData record; // 打开弹窗 this.visible true; this.$nextTick(() { this.loadPDFJS(); // 监听窗口 resize 事件 window.addEventListener(resize, this.handleResize); }); }, // 处理弹窗关闭 handleCancel() { this.visible false; }, // 初始化pdf.js插件 loadPDFJS() { // 检查PDF.js是否已经加载 if (window.pdfjsLib) { this.pdfjsLoaded true; // PDF.js已加载直接加载PDF this.loadPDF(); return; } // 创建script标签加载PDF.js const pdfScript document.createElement(script); pdfScript.src /pdf.min.js; pdfScript.onload () { // 设置worker路径 window.pdfjsLib.GlobalWorkerOptions.workerSrc /pdf.worker.min.js; this.pdfjsLoaded true; this.loadPDF(); }; pdfScript.onerror () { console.error(PDF.js加载失败); this.$message.error(PDF.js加载失败); }; document.head.appendChild(pdfScript); }, // 根据路径解析PDF文档 loadPDF() { if (!this.pdfjsLoaded) { this.$message.warning(PDF.js正在加载中请稍后); return; } if (this.batchRecords.length 0) { this.$message.warning(没有需要预览的记录); return; } // 批量加载PDF文档 const loadPromises this.batchRecords.map((record, index) { // 目前使用示例PDF文件实际项目中应该使用真实的URL const pdfUrl window._CONFIG[domianURL] / this.apiPrefix.unitbusiness /ticketBill/downTicketInfo ?ticketId0000000000000001tokennull; // 模拟测试数据 console.log(加载第${index 1}个PDF:, pdfUrl); const loadingTask window.pdfjsLib.getDocument(pdfUrl); return loadingTask.promise .then((pdf) { return { pdf, record }; }) .catch((error) { console.error(加载第${index 1}个PDF失败:, error); return null; }); }); Promise.all(loadPromises).then((results) { // 过滤掉加载失败的PDF this.pdfDocs results.filter((item) item ! null); if (this.pdfDocs.length 0) { this.$message.error(所有PDF加载失败); return; } // 显示第一个PDF this.currentPdfIndex 0; this.pdfDoc this.pdfDocs[0].pdf; this.totalPages this.pdfDoc.numPages; this.currentPage 1; // 先渲染页面然后在渲染完成后再调用 handleFit this.renderPage(this.currentPage); // 延迟调用 handleFit确保渲染完成 setTimeout(() { this.handleFit(); }, 100); }); }, // 本地上传pdf beforeUpload(file) { // 检查文件类型 const isPDF file.type application/pdf; if (!isPDF) { this.$message.error(只能上传PDF文件!); return false; } // 读取本地文件 const reader new FileReader(); reader.onload (e) { // 使用Data URL加载本地PDF this.pdfUrl e.target.result; this.loadPDF(); }; reader.readAsDataURL(file); // 阻止自动上传 return false; }, // 渲染指定页码的PDF页面 renderPage(num) { if (!this.pdfDoc) { return; } // 取消之前的渲染任务 if (this.renderTask) { this.renderTask.cancel(); } this.pdfDoc.getPage(num).then((page) { const canvas this.$refs.pdfCanvas; const ctx canvas.getContext(2d); const viewport page.getViewport({ scale: this.scale }); // 设置canvas尺寸 canvas.height viewport.height; canvas.width viewport.width; // 渲染PDF页面 const renderContext { canvasContext: ctx, viewport: viewport }; // 存储当前渲染任务并处理取消异常 this.$nextTick(() { this.renderTask page.render(renderContext); this.renderTask.promise.catch((error) { // 忽略渲染取消异常这是正常的 if (error.name ! RenderingCancelledException) { console.error(PDF渲染错误:, error); } }); }); }); }, // 上一页 prevPage() { if (this.currentPage 1) { this.currentPage--; this.renderPage(this.currentPage); } }, // 下一页 nextPage() { if (this.currentPage this.totalPages) { this.currentPage; this.renderPage(this.currentPage); } }, // 跳转到指定页 goToPage() { if (this.currentPage 1) { this.currentPage 1; } else if (this.currentPage this.totalPages) { this.currentPage this.totalPages; } this.renderPage(this.currentPage); }, // 放大 handleZoomIn() { this.scale 0.2; this.renderPage(this.currentPage); }, // 缩小 handleZoomOut() { this.scale - 0.2; this.renderPage(this.currentPage); }, // 自适应窗口 handleFit() { if (!this.pdfDoc) return; const container this.$refs.pdfCanvasContainer; const containerWidth container.clientWidth - 40; // 减去padding this.pdfDoc.getPage(this.currentPage).then((page) { const viewport page.getViewport({ scale: 1 }); const pageWidth viewport.width; // 计算自适应缩放比例 this.scale containerWidth / pageWidth; this.renderPage(this.currentPage); }); }, // 打印 handlePrint() { if (!this.pdfDoc) return; // 显示加载提示可选 const loadingToast this.$message.loading(正在准备打印..., 0); // 创建离屏容器来渲染 canvas不附加到 DOM const renderContainer document.createElement(div); renderContainer.style.position absolute; renderContainer.style.left -9999px; renderContainer.style.top -9999px; document.body.appendChild(renderContainer); const printPromises []; // 第一步并行渲染所有页面到 canvas但不转换为 dataURL for (let i 1; i this.totalPages; i) { printPromises.push( this.pdfDoc.getPage(i).then((page) { const canvas document.createElement(canvas); const viewport page.getViewport({ scale: 1.5 }); canvas.height viewport.height; canvas.width viewport.width; canvas.style.display none; // 隐藏 const ctx canvas.getContext(2d); const renderContext { canvasContext: ctx, viewport: viewport }; return page.render(renderContext).promise.then(() { // 不转换为 dataURL直接返回 canvas 元素 renderContainer.appendChild(canvas); return canvas; }); }) ); } // 第二步所有页面渲染完成后构建打印内容 Promise.all(printPromises) .then((canvases) { // 构建 HTML 字符串 let imagesHtml ; // 关键改进将 canvas 转换为 blob而不是 dataURL const blobPromises canvases.map((canvas, index) { return new Promise((resolve) { canvas.toBlob( (blob) { // 为每个 blob 创建对象 URL const url URL.createObjectURL(blob); const isLastPage index canvases.length - 1; // 生成 img 标签最后一页不加分页符 imagesHtml img src${url} styledisplay: block; width: 100%; ${!isLastPage ? page-break-after: always; : } /; // 清理 canvas 和 blob URL 的任务交给打印完成后 resolve(url); }, image/png, 1.0 ); // 使用最高质量 }); }); Promise.all(blobPromises).then((blobUrls) { // 构建完整的打印 HTML const printHtml html head titlePDF打印/title style body { margin: 0; background: white; } img { display: block; width: 100%; height: auto; page-break-after: always; margin: 0; padding: 0; } img:last-child { page-break-after: auto; } media print { body { margin: 0; } } /style /head body ${imagesHtml} /body /html ; // 创建 Blob 和 iframe const blob new Blob([printHtml], { type: text/html }); const blobUrl URL.createObjectURL(blob); // 创建隐藏的 iframe const iframe document.createElement(iframe); iframe.style.position absolute; iframe.style.width 0; iframe.style.height 0; iframe.style.border none; iframe.style.visibility hidden; // 加载完成后再打印 iframe.onload () { // 关闭加载提示 this.$message.destroy(); // 给浏览器一点时间渲染图片 setTimeout(() { try { iframe.contentWindow.print(); // 监听打印完成 const mediaQueryList iframe.contentWindow.matchMedia(print); const afterPrintHandler () { // 清理资源 document.body.removeChild(iframe); document.body.removeChild(renderContainer); URL.revokeObjectURL(blobUrl); blobUrls.forEach(URL.revokeObjectURL.bind(URL)); // 移除事件监听 if (mediaQueryList.removeListener) { mediaQueryList.removeListener(afterPrintHandler); } }; // 兼容不同浏览器 if (mediaQueryList.addListener) { mediaQueryList.addListener(afterPrintHandler); } iframe.contentWindow.onafterprint afterPrintHandler; } catch (e) { console.error(打印失败:, e); // 出错时也要清理 document.body.removeChild(iframe); document.body.removeChild(renderContainer); URL.revokeObjectURL(blobUrl); blobUrls.forEach(URL.revokeObjectURL.bind(URL)); } }, 500); // 给图片足够的时间渲染 }; iframe.src blobUrl; document.body.appendChild(iframe); }); }) .catch((error) { console.error(PDF渲染失败:, error); this.$message.destroy(); // 清理资源 if (renderContainer.parentNode) { document.body.removeChild(renderContainer); } }); }, // 窗口 resize 事件处理 handleResize() { // 当窗口大小变化时重新渲染当前页 this.renderPage(this.currentPage); }, // 树节点点击事件处理 treeChange(key, node) { // 绑定节点数据 // this.pdfUrl http://192.168.66.134:3129/api//unitbusiness/ticketBill/downTicketInfo?ticketId0000000000000001tokennull; this.loadPDF(); this.tree.node node; }, // 上一个PDF prevPdf() { if (this.currentPdfIndex 0) { this.currentPdfIndex--; this.pdfDoc this.pdfDocs[this.currentPdfIndex].pdf; this.totalPages this.pdfDoc.numPages; this.currentPage 1; this.renderPage(this.currentPage); setTimeout(() { this.handleFit(); }, 100); } }, // 下一个PDF nextPdf() { if (this.currentPdfIndex this.pdfDocs.length - 1) { this.currentPdfIndex; this.pdfDoc this.pdfDocs[this.currentPdfIndex].pdf; this.totalPages this.pdfDoc.numPages; this.currentPage 1; this.renderPage(this.currentPage); setTimeout(() { this.handleFit(); }, 100); } }, // 批量打印 handleBatchPrint() { if (this.pdfDocs.length 0) return; // 显示加载提示 this.$message.loading({ content: 正在准备打印请稍候..., duration: 0 }); // 创建渲染容器 const renderContainer document.createElement(div); renderContainer.style.position absolute; renderContainer.style.left -9999px; renderContainer.style.top -9999px; document.body.appendChild(renderContainer); const printPromises []; // 并行渲染所有PDF的所有页面到 canvas this.pdfDocs.forEach((item) { const pdf item.pdf; for (let i 1; i pdf.numPages; i) { printPromises.push( pdf.getPage(i).then((page) { const canvas document.createElement(canvas); const viewport page.getViewport({ scale: 1.5 }); canvas.height viewport.height; canvas.width viewport.width; canvas.style.display none; const ctx canvas.getContext(2d); const renderContext { canvasContext: ctx, viewport: viewport }; return page.render(renderContext).promise.then(() { renderContainer.appendChild(canvas); return canvas; }); }) ); } }); Promise.all(printPromises) .then((canvases) { // 第二步将所有 canvas 转换为图片使用 toBlob 避免内存问题 const blobPromises canvases.map((canvas, index) { return new Promise((resolve) { canvas.toBlob( (blob) { const url URL.createObjectURL(blob); const isLastPage index canvases.length - 1; resolve({ url, isLastPage }); }, image/png, 1.0 ); }); }); return Promise.all(blobPromises); }) .then((blobResults) { // 构建打印 HTML let imagesHtml ; const blobUrls []; blobResults.forEach((result) { blobUrls.push(result.url); imagesHtml img src${result.url} styledisplay: block; width: 100%; ${!result.isLastPage ? page-break-after: always; : } /; }); const printHtml html head titlePDF批量打印/title style body { margin: 0; background: white; } img { display: block; width: 100%; page-break-after: always; } img:last-child { page-break-after: avoid; } /style /head body ${imagesHtml} /body /html; // 创建 Blob 和 iframe const blob new Blob([printHtml], { type: text/html }); const blobUrl URL.createObjectURL(blob); // 创建隐藏的 iframe const iframe document.createElement(iframe); iframe.style.position absolute; iframe.style.width 0; iframe.style.height 0; iframe.style.border none; iframe.style.visibility hidden; // 加载完成后再打印 iframe.onload () { // 关闭加载提示 this.$message.destroy(); // 给浏览器一点时间渲染图片 setTimeout(() { try { iframe.contentWindow.print(); // 监听打印完成 const mediaQueryList iframe.contentWindow.matchMedia(print); const afterPrintHandler () { // 清理资源 document.body.removeChild(iframe); document.body.removeChild(renderContainer); URL.revokeObjectURL(blobUrl); blobUrls.forEach(URL.revokeObjectURL.bind(URL)); // 移除事件监听 if (mediaQueryList.removeListener) { mediaQueryList.removeListener(afterPrintHandler); } }; // 兼容不同浏览器 if (mediaQueryList.addListener) { mediaQueryList.addListener(afterPrintHandler); } iframe.contentWindow.onafterprint afterPrintHandler; } catch (e) { console.error(打印失败:, e); // 出错时也要清理 document.body.removeChild(iframe); document.body.removeChild(renderContainer); URL.revokeObjectURL(blobUrl); blobUrls.forEach(URL.revokeObjectURL.bind(URL)); } }, 500); }; iframe.src blobUrl; document.body.appendChild(iframe); }) .catch((error) { console.error(PDF渲染失败:, error); this.$message.destroy(); // 清理资源 if (renderContainer renderContainer.parentNode) { document.body.removeChild(renderContainer); } }); } } }; /script style scoped .pdf-container { display: flex; gap: 24px; } .pdf-controls { margin-bottom: 20px; } .pdf-canvas { border: 1px solid #e8e8e8; padding: 20px; background-color: #f5f5f5; min-height: 500px; } canvas { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } .tree-container { padding: 12px; border: 1px solid #e8e8e8; border-radius: 4px; min-width: 300px; overflow: auto; } .content-container { flex: 1; overflow: auto; } /style总结本文介绍了如何基于 Vue 2.0 和 PDF.js 实现一个功能完善的 PDF 预览组件支持本地文件上传、网络 PDF 加载、批量预览与打印、页面导航、缩放等功能。通过动态加载 PDF.js、优化渲染任务管理、使用 Blob 方式优化打印性能等技术手段实现了一个高性能、用户友好的 PDF 预览解决方案。该组件可以直接应用于需要 PDF 预览和打印功能的项目中如文档管理系统、票据打印系统等。通过简单的配置即可实现丰富的 PDF 处理功能为用户提供良好的使用体验。