前端开发攻略---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。

相关推荐
nibabaoo3 小时前
前端开发攻略---在 Vue 3 项目中使用 vue-i18n 实现国际化多语言
前端·javascript·国际化·i18n·vue3
沙振宇1 天前
【Web】使用Vue3+PlayCanvas开发3D游戏(四)3D障碍物躲避游戏2-模型加载
游戏·3d·vue3·vite·playcanvas
我命由我123451 天前
Vue Router - 记录一下 2 种路由写法
前端·javascript·vue.js·前端框架·html·html5·js
SuperEugene2 天前
Vue3 中后台实战:VXE Table 从基础表格到复杂业务表格全攻略 | Vue生态精选篇
前端·vue.js·状态模式·vue3·vxetable
p5l2m9n4o6q2 天前
Vue3后台管理系统布局实战:从零搭建Element Plus左右布局(含Pinia状态管理)
vue3·pinia·element plus·viewui·后台管理系统
梵得儿SHI2 天前
Vue3 生态工具实战进阶:API 请求封装 + 样式解决方案全攻略(Axios/Sass/CSS Modules)
前端·css·vue3·sass·api请求·样式解决方案·组合式api管理
我命由我123453 天前
React - state、state 的简写方式、props、props 的简写方式、类式组件中的构造器与 props、函数式组件使用 props
前端·javascript·react.js·前端框架·html·html5·js
我命由我123453 天前
React - React 初识、创建虚拟 DOM 的两种方式、jsx 语法规则、React 定义组件
前端·javascript·react.js·前端框架·html·html5·js
行者-全栈开发3 天前
43 篇系统实战:uni-app 从入门到架构师成长之路
前端·typescript·uni-app·vue3·最佳实践·企业级架构