前端开发攻略---Vue3项目中实现指定区域的打印预览与 PDF 导出功能

一、整体思路

  1. 打印实现 :利用 window.print(),但只打印指定区域。通过创建一个隐藏的 <iframe>,将要打印的 DOM 克隆到其中,并复制页面样式,然后调用 iframe 的打印方法。

  2. PDF 导出 :浏览器打印对话框自带"另存为 PDF"功能,因此打印功能天然支持 PDF 导出。若需要更精细的 PDF 控制(如直接生成 PDF 文件),可使用 html2canvas + jspdf,但复杂内容易出现问题,建议优先使用打印方式生成 PDF。

  3. 问题解决

    • 指定区域:克隆目标 DOM,避免影响原页面。

    • 样式完整 :复制原页面所有样式(linkstyle、内联样式)。

    • 表格分页 :使用 CSS 打印属性 page-break-inside: avoidpage-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"按钮直接生成文件(不弹出打印对话框),可基于 html2canvasjspdf 实现,但需注意复杂内容的处理。以下是一个简化示例:

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-insidethead 重复 配合 @media print 设置
图表滚动条 展开容器或替换为图片 打印前修改样式或调用图表库的导出图片方法
样式丢失 复制原页面所有样式 复制 <link><style> 标签,scoped CSS 天然保留属性选择器
内容截断 page-break-inside: avoid 对关键元素(行、图表容器)设置避免分页

五、注意事项

  1. 资源加载 :在调用 print() 前,确保 iframe 内所有图片、字体加载完成,可通过监听 load 事件或延迟执行。

  2. 异步内容:若打印区域包含异步渲染的数据(如从 API 获取),需先确保数据已渲染再克隆。

  3. 样式隔离:某些 UI 库(如 Ant Design Vue)的样式可能依赖于特定上下文,克隆后可能部分样式失效,可额外引入库的 CSS 文件。

  4. 移动端兼容:在移动设备上,打印功能可能有限,需测试。

  5. 性能:大量 DOM 克隆可能导致内存占用,注意及时移除 iframe。

相关推荐
可问春风_ren1 天前
HTML零基础进阶教程:解锁表单、多媒体与语义化实战
前端·git·html·ecmascript·reactjs·js
儒雅的烤地瓜1 天前
Vue | 一文详解Vue3中的Setup()函数
vue.js·vue3·vue2·组合式api·setup函数·option api
Irene19912 天前
ElementPlus 与成熟后台框架对比:vue-element-plus-admin、vue-pure-admin等
前端·ui·框架·vue3
我命由我123453 天前
HTML 开发 - HTML 描述列表标签(<dl>、<dt>、<dd>)
前端·javascript·css·html·css3·html5·js
终端鹿3 天前
Vue3 与第三方组件库联动:Element Plus 按需引入与二次封装
vue3·element plus·二次封装
我命由我123454 天前
React - 类组件 setState 的 2 种写法、LazyLoad、useState
前端·javascript·react.js·html·ecmascript·html5·js
酉鬼女又兒4 天前
零基础快速入门前端深入掌握箭头函数、Promise 与 Fetch API —— 蓝桥杯 Web 考点全解析(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·css·职场和发展·蓝桥杯·es6·js
我命由我123454 天前
React - useEffect、useRef、Fragment
开发语言·前端·javascript·react.js·前端框架·ecmascript·js
酉鬼女又兒4 天前
零基础快速入门前端蓝桥杯 Web 备考:AJAX 与 XMLHttpRequest 核心知识点及实战(可用于备赛蓝桥杯Web应用开发)
前端·ajax·职场和发展·蓝桥杯·css3·js
我命由我123455 天前
React - React Redux 数据共享、Redux DevTools、React Redux 最终优化
前端·javascript·react.js·前端框架·ecmascript·html5·js