一、整体思路
-
打印实现 :利用
window.print(),但只打印指定区域。通过创建一个隐藏的<iframe>,将要打印的 DOM 克隆到其中,并复制页面样式,然后调用iframe的打印方法。 -
PDF 导出 :浏览器打印对话框自带"另存为 PDF"功能,因此打印功能天然支持 PDF 导出。若需要更精细的 PDF 控制(如直接生成 PDF 文件),可使用
html2canvas+jspdf,但复杂内容易出现问题,建议优先使用打印方式生成 PDF。 -
问题解决:
-
指定区域:克隆目标 DOM,避免影响原页面。
-
样式完整 :复制原页面所有样式(
link、style、内联样式)。 -
表格分页 :使用 CSS 打印属性
page-break-inside: avoid、page-break-after等,并配合<thead>、<tfoot>重复表头。 -
图表滚动条 :在打印前将图表容器高度设为自适应(
overflow: visible),或替换为图片(如 ECharts 提供getDataURL方法)。 -
内容截断 :利用
@media print样式控制分页,确保关键元素完整显示。
-
二、实现步骤
1. 创建打印工具函数(组合式函数)
在 src/composables/usePrint.js 中编写一个可复用的打印逻辑。
javascript
// usePrint.js
import { nextTick } from 'vue'
export function usePrint() {
// 复制样式到 iframe
const copyStyles = (iframeDoc) => {
// 复制所有 link 和 style 标签
const styles = document.querySelectorAll('link[rel="stylesheet"], style')
styles.forEach(style => {
iframeDoc.head.appendChild(style.cloneNode(true))
})
// 可选:复制内联样式(已在 DOM 中体现,通常不需要额外操作)
}
// 处理图表等特殊元素(根据项目需求定制)
const prepareContent = (cloneNode) => {
// 示例:将 echarts 容器替换为图片,或强制展开滚动区域
// 这里假设需要将所有 .echarts-container 替换为 canvas 导出的图片
// 具体实现需要结合 echarts 实例
return cloneNode
}
// 打印指定区域
const printArea = async (selector, options = {}) => {
const element = typeof selector === 'string'
? document.querySelector(selector)
: selector
if (!element) {
console.error('打印区域不存在')
return
}
// 克隆目标元素(深克隆,包含所有子节点)
const clone = element.cloneNode(true)
// 预处理内容(如图表转图片)
const preparedClone = prepareContent(clone)
// 创建隐藏 iframe
const iframe = document.createElement('iframe')
iframe.style.position = 'absolute'
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.border = 'none'
document.body.appendChild(iframe)
const iframeDoc = iframe.contentWindow.document
// 写入基本结构
iframeDoc.open()
iframeDoc.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>打印预览</title>
${options.title ? `<title>${options.title}</title>` : ''}
</head>
<body>${preparedClone.outerHTML}</body>
</html>
`)
iframeDoc.close()
// 复制页面样式
copyStyles(iframeDoc)
// 等待资源加载(如图片、字体)
await nextTick()
// 如果有图片等资源,可能需要等待 load 事件
await new Promise(resolve => {
iframe.contentWindow.onload = resolve
// 若 iframe 已加载完成,立即 resolve
if (iframeDoc.readyState === 'complete') resolve()
})
// 调用打印
iframe.contentWindow.focus()
iframe.contentWindow.print()
// 移除 iframe(部分浏览器打印后不会立即触发,可延迟移除)
setTimeout(() => {
document.body.removeChild(iframe)
}, 500)
}
return { printArea }
}
2. 在 Vue 组件中使用
javascript
<template>
<div>
<div ref="printSection" class="print-area">
<!-- 复杂表格、图表等内容 -->
<h2>销售报表</h2>
<table class="data-table">
<thead>
<tr><th>...</th></tr>
</thead>
<tbody>
<!-- 大量行数据 -->
</tbody>
</table>
<div class="chart-container" id="myChart"></div>
</div>
<button @click="handlePrint">打印/导出PDF</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { usePrint } from '@/composables/usePrint'
import * as echarts from 'echarts'
const printSection = ref(null)
const { printArea } = usePrint()
// 初始化图表(仅作示例)
onMounted(() => {
const chart = echarts.init(document.getElementById('myChart'))
chart.setOption({ /* 配置项 */ })
})
const handlePrint = async () => {
await printArea(printSection.value, {
title: '打印报表'
})
}
</script>
<style scoped>
/* 打印样式需额外定义全局样式,或在 usePrint 中复制 */
</style>
3. 关键问题处理
3.1 复杂表格分页控制
在全局样式(或打印专属样式)中添加以下规则:
css
@media print {
/* 避免表格行内分页 */
tr {
page-break-inside: avoid;
}
/* 表格头部重复 */
thead {
display: table-header-group;
}
/* 表格尾部重复 */
tfoot {
display: table-footer-group;
}
/* 强制分页(按需) */
.page-break {
page-break-after: always;
}
}
3.2 图表滚动条问题
方案 A:展开容器
在克隆 DOM 前,动态修改图表容器的样式,使其高度自适应,并隐藏滚动条:
javascript
const prepareContent = (cloneNode) => {
// 查找所有图表容器,移除固定高度和 overflow
const charts = cloneNode.querySelectorAll('.chart-container')
charts.forEach(el => {
el.style.height = 'auto'
el.style.overflow = 'visible'
// 如果图表是 ECharts,可能需要重新调整尺寸或转图片(见方案B)
})
return cloneNode
}
方案 B:替换为图片
对于 ECharts,可在打印前调用 getDataURL 生成图片,然后将容器替换为 <img>:
javascript
const prepareContent = async (cloneNode) => {
const charts = cloneNode.querySelectorAll('.echarts-container')
for (const container of charts) {
const chartId = container.id
const originalChart = echarts.getInstanceByDom(document.getElementById(chartId))
if (originalChart) {
const url = originalChart.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff'
})
// 创建 img 替换原容器
const img = document.createElement('img')
img.src = url
img.style.width = '100%'
container.parentNode.replaceChild(img, container)
}
}
return cloneNode
}
3.3 确保样式完整
在 copyStyles 函数中,我们复制了 <link> 和 <style> 标签。但若项目使用了 CSS 模块或 scoped 样式,由于克隆保留了原有的 data-v-xxx 属性,样式依然生效。
若样式由 JavaScript 动态生成(如 styled-components),则需额外处理。
3.4 内容截断与分页
使用 page-break-inside: avoid 避免重要元素(如表格行、图表)被截断到两页。若某个区块必须完整展示,可包裹 <div> 并设置该属性。
三、增强功能:直接生成 PDF 文件
若需要"导出PDF"按钮直接生成文件(不弹出打印对话框),可基于 html2canvas 和 jspdf 实现,但需注意复杂内容的处理。以下是一个简化示例:
javascript
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
const exportToPDF = async (element) => {
const canvas = await html2canvas(element, {
scale: 2, // 提高清晰度
logging: false,
useCORS: true, // 允许跨域图片
allowTaint: false
})
const imgData = canvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'px',
format: [canvas.width, canvas.height] // 动态尺寸
})
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height)
pdf.save('document.pdf')
}
问题与对策:
-
内容截断 :
html2canvas会完整渲染内容,但 PDF 一页放不下时,需要手动分页。可监听滚动高度,分段截取 canvas 并添加到 PDF。 -
表格分页 :
html2canvas本身无法感知分页,需预先拆分 DOM 或使用 CSS 多列布局。 -
复杂图表:同打印,建议预处理为图片。
| 功能点 | 实现方式 | 关键技巧 |
|---|---|---|
| 指定区域打印 | 克隆 DOM 到 iframe,调用 print() |
深克隆 + 样式复制 |
| PDF 导出 | 利用打印对话框另存为 PDF | 无需额外代码 |
| 直接生成 PDF | html2canvas + jspdf |
注意分页和清晰度,可考虑将长内容拆分为多页 |
| 表格分页 | CSS 打印属性 page-break-inside、thead 重复 |
配合 @media print 设置 |
| 图表滚动条 | 展开容器或替换为图片 | 打印前修改样式或调用图表库的导出图片方法 |
| 样式丢失 | 复制原页面所有样式 | 复制 <link> 和 <style> 标签,scoped CSS 天然保留属性选择器 |
| 内容截断 | page-break-inside: avoid |
对关键元素(行、图表容器)设置避免分页 |
五、注意事项
-
资源加载 :在调用
print()前,确保 iframe 内所有图片、字体加载完成,可通过监听load事件或延迟执行。 -
异步内容:若打印区域包含异步渲染的数据(如从 API 获取),需先确保数据已渲染再克隆。
-
样式隔离:某些 UI 库(如 Ant Design Vue)的样式可能依赖于特定上下文,克隆后可能部分样式失效,可额外引入库的 CSS 文件。
-
移动端兼容:在移动设备上,打印功能可能有限,需测试。
-
性能:大量 DOM 克隆可能导致内存占用,注意及时移除 iframe。