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

相关推荐
ANnianStriver1 天前
PetLumina-AI 驱动的宠物生活管理平台
java·生活·vue3·springboot·ai编程·宠物·全栈开发
雨季mo浅忆2 天前
记录Vue3项目中的各类问题
前端·bug·vue3
八目蛛5 天前
八目蛛网络(免费工具网站导航)
css·vue.js·开源·vue3·html5·ai编程
颂love5 天前
Vue3基础入门
前端·学习·vue3
海市公约6 天前
Vue3组合式API中watch传值生命周期与自定义Hook实战
vue3·生命周期·watch·props·组件通信·defineexpose·自定义hook
小妖6667 天前
console.log 显示内容不全怎么办
javascript·js·console.log
海市公约7 天前
Vue3组合式API与响应式系统核心机制详解
vue3·computed·reactive·ref·响应式系统·composition api·script setup
小茴香3538 天前
Vue3路由权限动态管理
前端·前端框架·vue3
暗冰ཏོ12 天前
《2026 Vue2 + Vue3 完整学习指南:基础语法、路由缓存、登录拦截、项目实战与面试题》
前端·vue.js·vue·vue3·vue2
padane2213 天前
gmssl编译wasm
ubuntu·html·密码学·wasm·js