在前端开发中,尤其是在使用 Vue 与 ElementUI 构建管理后台时,表格数据的打印与导出为PDF是常见的需求。本文将分享两种实践方案,分别解决直接浏览器打印的样式问题,以及将HTML内容高质量转换为PDF文件的技术要点。
一、项目背景与核心问题
假设你在一个 Vue 项目中使用了 ElementUI 的表格组件,并希望实现以下功能:
- 功能需求:用户点击“打印”按钮时,能将当前表格内容以整齐的样式、固定的列宽完整打印出来。
- 技术实现:通过
:style 动态控制表格宽度(打印时固定为 200mm,页面显示时为 100%)。
- 遇到问题:
- 直接设置打印宽度可能影响页面正常显示的美观性。
- 表格的渲染是异步的,即使使用
nextTick 或 MutationObserver,也可能在列宽计算完成前就触发了打印,导致打印内容显示不全。
二、方案一:浏览器直接打印优化
本方案的核心思想是:动态切换打印样式 -> 监听表格渲染完成 -> 构建独立的打印DOM -> 调用打印API -> 恢复页面状态。
1. HTML 结构
在模板中,我们通过 isPrint 状态控制表格的宽度模式。
<el-button type="primary" icon="el-icon-printer" @click="print">打 印</el-button>
<el-table
:style="{ width: isPrint ? '200mm' : '100%' }"
ref="refTable"
:data="tableList"
border
>
<el-table-column prop="name" label="第一列" width="100px" align="center"></el-table-column>
<el-table-column prop="age" label="年龄" width="90px" align="center"></el-table-column>
<!-- 更多列... -->
</el-table>
2. JavaScript 逻辑与解析
打印功能的关键在于确保表格完全渲染后再执行打印操作。
export default {
data() {
return {
tableList: [],
isPrint: false,
};
},
methods: {
print() {
this.isPrint = true; // 1. 切换到打印宽度模式
const el = this.$refs.refTable.$el;
const body = el.querySelector('.el-table__body');
const originWidth = body.offsetWidth; // 2. 记录初始宽度
let setIntervalClear = undefined;
// 3. 启动定时器监听宽度变化
setIntervalClear = setInterval(() => {
const width = body.offsetWidth;
if (originWidth !== width) { // 4. 宽度变化,说明渲染完成
// 5. 构建独立的打印DOM
const dom = document.createElement('div');
const style = `<style>@media print { @page { size: 210mm 297mm !important; margin: 2mm; } }</style>`;
const html = el.outerHTML;
dom.innerHTML = `<div>${style}${html}</div>`;
// 6. 执行打印 (假设使用了this.$print插件或window.print())
this.$print(dom);
// 7. 清理并恢复状态
clearInterval(setIntervalClear);
this.isPrint = false;
}
}, 16); // 约60fps的频率检测,平衡性能与及时性
},
}
}
代码解析与要点:
- 切换模式:
this.isPrint = true 触发表格宽度变为固定值,为打印做准备。
- 监听渲染:ElementUI表格的列宽计算是异步的。这里采用定时器循环检测表格内容区域(
.el-table__body)的offsetWidth,当其发生变化时,表明内部布局已渲染稳定。这是解决打印不全的关键,单纯的 nextTick 无法捕获此变化。
- 构建打印DOM:创建一个新的
div,将表格HTML和专为打印设计的 @page CSS规则注入其中,确保打印输出独立于页面布局,且纸张尺寸正确。
- 恢复状态:打印指令发出后,立即清除定时器并将
isPrint 置回 false,表格恢复自适应宽度,不影响用户继续操作。
3. 方案一处理流程图
下图清晰地展示了上述浏览器打印方案的核心流程与决策点:

三、方案二:HTML 转 PDF 生成与下载
如果你需要生成PDF文件供用户下载,而不仅仅是调用打印机,可以结合 html2canvas 和 jsPDF 库来实现。
1. 安装依赖
npm install html2canvas jspdf
2. 核心实现代码
此方案同样需要等待表格渲染完成,再进行截图和PDF生成。
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
export default {
methods: {
async createAndUploadPDF() {
// 显示加载提示
const loading = this.$loading({ lock: true, text: '生成PDF中...' });
const el = document.getElementById('idMain');
const body = el.querySelector('.el-table__body');
const originWidth = body.offsetWidth;
let setIntervalClear = undefined;
el.style.width = '210mm'; // 切换为PDF宽度
// 监听渲染完成
setIntervalClear = setInterval(async () => {
const width = body.offsetWidth;
if (originWidth === width) return; // 宽度未变,继续等待
clearInterval(setIntervalClear);
// 使用html2canvas将DOM转换为Canvas
const canvas = await html2canvas(el, {
scale: window.devicePixelRatio || 2, // 提升清晰度
useCORS: true, // 处理图片跨域
backgroundColor: '#FFFFFF',
});
// 使用jsPDF处理Canvas
const pdf = new jsPDF('p', 'mm', 'a4');
const imgData = canvas.toDataURL('image/png');
const imgWidth = 210; // A4纸宽
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 动态生成文件名(处理IOS时区问题)
let fileName = Date.now() + 8 * 60 * 60 * 1000; // 增加8小时(北京时间偏移)
fileName = new Date(fileName).toISOString(); // 转为ISO字符串
fileName = fileName.replace(/[-:Z]/g, '').replace(/[T.]/g, '_'); // 格式化
// 添加图片到PDF,处理多页情况
let position = 0;
let heightLeft = imgHeight;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
while (heightLeft > 297) { // A4纸高297mm
position -= 297;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= 297;
}
// 保存PDF文件
pdf.save(`${fileName}.pdf`);
// 恢复状态
el.style.width = '100%';
loading.close();
}, 16);
}
}
}
技术要点说明:
- 清晰度优化:
html2canvas 配置中的 scale 参数使用 devicePixelRatio,可以在高分辨率屏幕上获得更清晰的图片。
- IOS时间格式化:在生成文件名时,对
Date.now() 增加了8小时偏移并调用 toISOString(),是为了避免在IOS设备上因时区处理差异导致的时间戳错误,这是一种常见的兼容性处理技巧。
- 多页PDF处理:当转换的内容高度超过单页A4纸时,通过循环
addPage() 和 addImage() 实现内容分页。
3. 相关样式
为目标区域添加一些基础内边距,使PDF内容看起来更舒适。
.pdf_area {
padding: 18px;
box-sizing: border-box;
}
.pdf_area h2 {
margin-bottom: 18px;
}
四、总结
两种方案各有适用场景:
- 方案一(直接打印):适合需要快速调用系统打印服务的场景,通过巧妙的异步监听确保了打印内容的完整性。
- 方案二(HTML转PDF):适合需要生成文件供用户下载、分享或存档的场景,提供了更好的跨平台一致性,但依赖于前端库。
无论哪种方案,其核心挑战都在于如何确保动态渲染的组件(如ElementUI表格)在输出前已完全布局稳定。文中使用的定时器检测宽度变化的方法,是在 JavaScript 异步渲染环境下一种简单有效的解决方案。掌握这些技巧,能帮助你更从容地应对复杂的前端打印与导出需求,构建更健壮的 Node.js 全栈或前端应用。