Vue环境下数据导出PDF的全面指南

news2025/5/20 5:21:07

在这里插入图片描述

文章目录

    • 1. 前言
    • 2. 原生浏览器打印方案
      • 2.1 使用window.print()实现
      • 2.2 使用CSS Paged Media模块
    • 3. 常用第三方库方案
      • 3.1 使用jsPDF
      • 3.2 使用html2canvas + jsPDF
      • 3.3 使用pdfmake
      • 3.4 使用vue-pdf
    • 4. 服务器端导出方案
      • 4.1 前端请求服务器生成PDF
      • 4.2 使用无头浏览器生成PDF
    • 5. 方法对比与选择指南
      • 5.1 功能对比表
      • 5.2 选择建议
      • 5.3 性能优化建议
    • 6. 最佳实践示例
      • 6.1 完整的Vue PDF导出组件

在这里插入图片描述

1. 前言

在现代Web应用开发中,将数据导出为PDF是一项常见且重要的功能需求。PDF作为一种通用的文档格式,具有跨平台、保持格式一致、易于打印等优势。Vue.js作为当前流行的前端框架,提供了多种实现PDF导出的方法。本文将全面探讨Vue环境下实现PDF导出的7种主要方法,包括原生浏览器API、常用第三方库方案以及服务器端导出方案,每种方法都将提供详细的代码示例和优劣分析。

2. 原生浏览器打印方案

2.1 使用window.print()实现

这种方法利用浏览器自带的打印功能,通过CSS媒体查询控制打印样式。

实现原理

  1. 创建专门用于打印的组件或视图
  2. 使用@media print定义打印样式
  3. 调用window.print()触发浏览器打印对话框

代码示例

<template>
  <div>
    <button @click="print">打印PDF</button>
    
    <div ref="printContent" class="print-container">
      <h1>销售报表</h1>
      <table>
        <thead>
          <tr>
            <th>日期</th>
            <th>产品</th>
            <th>数量</th>
            <th>金额</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="item in salesData" :key="item.id">
            <td>{{ item.date }}</td>
            <td>{{ item.product }}</td>
            <td>{{ item.quantity }}</td>
            <td>{{ formatCurrency(item.amount) }}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      salesData: [
        { id: 1, date: '2023-01-01', product: '产品A', quantity: 10, amount: 1000 },
        { id: 2, date: '2023-01-02', product: '产品B', quantity: 5, amount: 2500 }
      ]
    }
  },
  methods: {
    print() {
      window.print()
    },
    formatCurrency(value) {
      return '¥' + value.toLocaleString()
    }
  }
}
</script>

<style>
@media print {
  body * {
    visibility: hidden;
  }
  .print-container, .print-container * {
    visibility: visible;
  }
  .print-container {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
  }
  
  /* 打印分页控制 */
  table {
    page-break-inside: auto;
  }
  tr {
    page-break-inside: avoid;
    page-break-after: auto;
  }
  thead {
    display: table-header-group;
  }
  tfoot {
    display: table-footer-group;
  }
}

/* 屏幕样式与打印样式分离 */
.print-container {
  display: none;
}
@media print {
  .print-container {
    display: block;
  }
}
</style>

优点

  • 零依赖,不增加项目体积
  • 实现简单,适合简单打印需求
  • 用户可以选择"另存为PDF"(大多数现代浏览器支持)

缺点

  • 依赖用户操作,无法自动化
  • 打印样式控制有限
  • 无法生成复杂的PDF文档
  • 不同浏览器表现可能不一致

2.2 使用CSS Paged Media模块

对于更专业的打印需求,可以使用CSS Paged Media模块定义分页、页眉页脚等。

代码示例

@page {
  size: A4;
  margin: 1cm;
  
  @top-left {
    content: "公司名称";
    font-size: 10pt;
  }
  
  @top-right {
    content: "页码 " counter(page) " / " counter(pages);
    font-size: 10pt;
  }
  
  @bottom-center {
    content: "机密文件";
    font-size: 8pt;
    color: #999;
  }
}

@media print {
  h1 {
    page-break-before: always;
  }
  
  table {
    page-break-inside: avoid;
  }
  
  .page-break {
    page-break-after: always;
  }
}

优点

  • 更专业的打印控制
  • 支持页码、页眉页脚等高级功能
  • 仍然是纯CSS方案,无JavaScript依赖

缺点

  • 浏览器支持不一致(特别是复杂特性)
  • 学习曲线较陡
  • 仍然依赖浏览器打印功能

3. 常用第三方库方案

3.1 使用jsPDF

jsPDF是最流行的JavaScript PDF生成库之一,可以直接在浏览器中生成PDF。

安装

npm install jspdf

基础实现

import jsPDF from 'jspdf'

export function generatePDF(title, data, columns, filename = 'export.pdf') {
  const doc = new jsPDF()
  
  // 添加标题
  doc.setFontSize(18)
  doc.text(title, 14, 15)
  
  // 添加表头
  doc.setFontSize(12)
  let y = 30
  columns.forEach((col, index) => {
    doc.text(col.label, 14 + index * 40, y)
  })
  
  // 添加数据行
  doc.setFontSize(10)
  data.forEach((row, rowIndex) => {
    y = 40 + rowIndex * 10
    if (y > 280) { // 接近页面底部
      doc.addPage()
      y = 20
    }
    columns.forEach((col, colIndex) => {
      doc.text(String(row[col.field]), 14 + colIndex * 40, y)
    })
  })
  
  // 保存文件
  doc.save(filename)
}

// Vue组件中使用
methods: {
  exportPDF() {
    const data = [
      { id: 1, name: '产品A', price: 100, stock: 50 },
      { id: 2, name: '产品B', price: 200, stock: 30 }
    ]
    const columns = [
      { label: 'ID', field: 'id' },
      { label: '产品名称', field: 'name' },
      { label: '价格', field: 'price' },
      { label: '库存', field: 'stock' }
    ]
    generatePDF('产品列表', data, columns)
  }
}

高级功能示例

function generateAdvancedPDF() {
  const doc = new jsPDF({
    orientation: 'landscape',
    unit: 'mm',
    format: 'a4'
  })
  
  // 设置元数据
  doc.setProperties({
    title: '高级报表',
    subject: '2023年度销售数据',
    author: '销售系统',
    keywords: '销售, 报表, 2023',
    creator: '公司销售系统'
  })
  
  // 添加公司logo
  const imgData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...'
  doc.addImage(imgData, 'PNG', 10, 10, 50, 15)
  
  // 添加标题
  doc.setFont('helvetica', 'bold')
  doc.setFontSize(22)
  doc.setTextColor(40, 40, 40)
  doc.text('2023年度销售报表', 105, 20, { align: 'center' })
  
  // 添加表格
  const headers = [['产品', 'Q1', 'Q2', 'Q3', 'Q4', '总计']]
  const salesData = [
    ['产品A', 1000, 1500, 1200, 1800, 5500],
    ['产品B', 800, 900, 1000, 1200, 3900],
    ['产品C', 500, 600, 700, 900, 2700]
  ]
  
  doc.autoTable({
    head: headers,
    body: salesData,
    startY: 40,
    theme: 'grid',
    headStyles: {
      fillColor: [22, 160, 133],
      textColor: 255,
      fontStyle: 'bold'
    },
    alternateRowStyles: {
      fillColor: [240, 240, 240]
    },
    margin: { top: 40 },
    didDrawPage: function(data) {
      // 页脚
      doc.setFontSize(10)
      doc.setTextColor(150)
      const pageCount = doc.internal.getNumberOfPages()
      doc.text(`${doc.internal.getCurrentPageInfo().pageNumber} 页 / 共 ${pageCount}`, 
        data.settings.margin.left, 
        doc.internal.pageSize.height - 10
      )
    }
  })
  
  // 添加折线图(使用jsPDF的简单绘图功能)
  doc.setDrawColor(100, 100, 255)
  doc.setFillColor(100, 100, 255)
  
  const points = [
    { x: 60, y: 150, q: 'Q1' },
    { x: 90, y: 130, q: 'Q2' },
    { x: 120, y: 140, q: 'Q3' },
    { x: 150, y: 120, q: 'Q4' }
  ]
  
  // 绘制折线
  points.forEach((point, i) => {
    if (i > 0) {
      doc.line(points[i-1].x, points[i-1].y, point.x, point.y)
    }
    doc.circle(point.x, point.y, 2, 'F')
    doc.text(point.q, point.x, point.y + 10)
  })
  
  doc.save('advanced_report.pdf')
}

优点

  • 纯前端实现,不依赖服务器
  • 功能丰富,支持文本、表格、简单图形等
  • 支持自定义字体
  • 活跃的社区和良好的文档

缺点

  • 复杂布局实现较困难
  • 原生不支持复杂表格样式(需要插件)
  • 中文支持需要额外配置字体

3.2 使用html2canvas + jsPDF

这种方案先将HTML转换为canvas,再将canvas转为PDF,适合需要精确复制页面样式的场景。

安装

npm install html2canvas jspdf

基础实现

import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'

export async function exportHTMLToPDF(element, filename = 'export.pdf', options = {}) {
  const canvas = await html2canvas(element, {
    scale: 2, // 提高分辨率
    logging: false,
    useCORS: true,
    allowTaint: true,
    ...options
  })
  
  const imgData = canvas.toDataURL('image/png')
  const pdf = new jsPDF({
    orientation: canvas.width > canvas.height ? 'landscape' : 'portrait',
    unit: 'mm'
  })
  
  const pageWidth = pdf.internal.pageSize.getWidth()
  const pageHeight = pdf.internal.pageSize.getHeight()
  
  const imgWidth = pageWidth
  const imgHeight = (canvas.height * imgWidth) / canvas.width
  
  pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
  pdf.save(filename)
}

// Vue组件中使用
methods: {
  async exportPage() {
    const element = this.$refs.pdfContent
    await exportHTMLToPDF(element, 'page_export.pdf', {
      scale: 3 // 更高分辨率
    })
  }
}

高级功能示例

async function exportMultiPagePDF() {
  const elements = document.querySelectorAll('.report-page')
  const pdf = new jsPDF('p', 'mm', 'a4')
  
  for (let i = 0; i < elements.length; i++) {
    const element = elements[i]
    const canvas = await html2canvas(element, {
      scale: 2,
      logging: true
    })
    
    const imgData = canvas.toDataURL('image/png')
    const imgWidth = pdf.internal.pageSize.getWidth()
    const imgHeight = (canvas.height * imgWidth) / canvas.width
    
    if (i > 0) {
      pdf.addPage()
    }
    
    pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
    
    // 添加页脚
    pdf.setFontSize(10)
    pdf.setTextColor(150)
    pdf.text(
      `${i + 1}`, 
      pdf.internal.pageSize.getWidth() / 2,
      pdf.internal.pageSize.getHeight() - 10,
      { align: 'center' }
    )
  }
  
  pdf.save('multi_page_report.pdf')
}

优点

  • 精确复制页面样式和布局
  • 支持复杂HTML结构和CSS样式
  • 可以处理动态生成的内容
  • 支持多页导出

缺点

  • 生成的PDF本质是图片,文字不可选择
  • 大页面可能导致内存问题
  • 性能较差,特别是复杂页面
  • 分页控制较困难

3.3 使用pdfmake

pdfmake是一个声明式的PDF生成库,使用JSON格式定义文档结构。

安装

npm install pdfmake

基础实现

import pdfMake from 'pdfmake/build/pdfmake'
import pdfFonts from 'pdfmake/build/vfs_fonts'

pdfMake.vfs = pdfFonts.pdfMake.vfs

export function generatePDFWithPdfMake(data, filename = 'document.pdf') {
  const docDefinition = {
    content: [
      { text: '销售报表', style: 'header' },
      '\n\n',
      {
        table: {
          headerRows: 1,
          widths: ['*', 'auto', 'auto', 'auto'],
          body: [
            ['产品', '季度1', '季度2', '季度3'],
            ...data.map(item => [
              item.product,
              { text: item.q1, alignment: 'right' },
              { text: item.q2, alignment: 'right' },
              { text: item.q3, alignment: 'right' }
            ])
          ]
        }
      }
    ],
    styles: {
      header: {
        fontSize: 18,
        bold: true,
        alignment: 'center'
      }
    },
    defaultStyle: {
      font: 'SimSun'
    }
  }
  
  pdfMake.createPdf(docDefinition).download(filename)
}

// Vue组件中使用
methods: {
  exportData() {
    const data = [
      { product: '产品A', q1: 1000, q2: 1500, q3: 1200 },
      { product: '产品B', q1: 800, q2: 900, q3: 1000 }
    ]
    generatePDFWithPdfMake(data, 'sales_report.pdf')
  }
}

高级功能示例

function generateAdvancedPdfMakeDocument() {
  const docDefinition = {
    pageSize: 'A4',
    pageMargins: [40, 60, 40, 60],
    header: function(currentPage, pageCount) {
      return {
        text: `${currentPage} 页 / 共 ${pageCount}`,
        alignment: 'right',
        margin: [0, 20, 20, 0]
      }
    },
    footer: function(currentPage, pageCount) {
      return {
        text: '公司机密 - 未经授权禁止复制',
        alignment: 'center',
        fontSize: 8,
        margin: [0, 0, 0, 20]
      }
    },
    content: [
      {
        columns: [
          {
            width: 100,
            image: 'logo',
            fit: [80, 80]
          },
          {
            width: '*',
            text: '2023年度财务报告',
            style: 'reportTitle'
          }
        ]
      },
      '\n\n',
      {
        text: '1. 销售概览',
        style: 'sectionHeader'
      },
      {
        text: '本年度公司整体销售额达到¥1,200万,同比增长15%。主要增长来自产品线A和B。',
        style: 'paragraph'
      },
      '\n',
      {
        style: 'tableExample',
        table: {
          widths: ['*', '*', '*', '*'],
          body: [
            [
              { text: '产品线', style: 'tableHeader' },
              { text: 'Q1', style: 'tableHeader' },
              { text: 'Q2', style: 'tableHeader' },
              { text: 'Q3', style: 'tableHeader' }
            ],
            ['产品A', '320万', '350万', '380万'],
            ['产品B', '280万', '300万', '320万'],
            ['产品C', '150万', '160万', '170万'],
            [
              { text: '总计', style: 'tableHeader' },
              { text: '750万', colSpan: 3, style: 'tableHeader' },
              {},
              {}
            ]
          ]
        }
      },
      '\n\n',
      {
        text: '2. 成本分析',
        style: 'sectionHeader',
        pageBreak: 'before'
      },
      {
        canvas: [
          {
            type: 'rect',
            x: 0, y: 0,
            w: 500, h: 20,
            color: '#eeeeee'
          },
          {
            type: 'rect',
            x: 0, y: 0,
            w: 350, h: 20,
            color: '#cc0000'
          }
        ]
      },
      {
        text: '成本构成示意图',
        alignment: 'center',
        italics: true,
        fontSize: 10,
        margin: [0, 5, 0, 0]
      }
    ],
    styles: {
      reportTitle: {
        fontSize: 24,
        bold: true,
        alignment: 'center',
        margin: [0, 20, 0, 0]
      },
      sectionHeader: {
        fontSize: 16,
        bold: true,
        margin: [0, 15, 0, 5]
      },
      paragraph: {
        fontSize: 12,
        lineHeight: 1.5,
        margin: [0, 0, 0, 10]
      },
      tableExample: {
        margin: [0, 5, 0, 15]
      },
      tableHeader: {
        bold: true,
        fontSize: 13,
        color: 'black',
        fillColor: '#dddddd'
      }
    },
    images: {
      logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...'
    }
  }
  
  pdfMake.createPdf(docDefinition).open()
}

优点

  • 声明式API,易于理解和使用
  • 强大的布局和样式控制能力
  • 内置分页、页眉页脚支持
  • 支持表格、列表、图片等多种元素

缺点

  • 中文支持需要额外配置字体
  • 学习曲线较陡(特别是复杂布局)
  • 文档结构可能变得复杂
  • 性能不如原生jsPDF

3.4 使用vue-pdf

vue-pdf是一个Vue专用的PDF生成和显示组件库。

安装

npm install @tato30/vue-pdf

基础实现

<template>
  <div>
    <button @click="generatePDF">生成PDF</button>
    
    <div ref="pdfContent">
      <h1>员工信息</h1>
      <table>
        <tr>
          <th>ID</th>
          <th>姓名</th>
          <th>部门</th>
        </tr>
        <tr v-for="emp in employees" :key="emp.id">
          <td>{{ emp.id }}</td>
          <td>{{ emp.name }}</td>
          <td>{{ emp.department }}</td>
        </tr>
      </table>
    </div>
  </div>
</template>

<script>
import VuePdf from '@tato30/vue-pdf'

export default {
  components: {
    VuePdf
  },
  data() {
    return {
      employees: [
        { id: 1, name: '张三', department: '研发' },
        { id: 2, name: '李四', department: '市场' }
      ]
    }
  },
  methods: {
    async generatePDF() {
      const pdf = new VuePdf()
      
      // 添加内容
      pdf.addText('员工信息', { fontSize: 18, align: 'center' })
      pdf.addBreak(20)
      
      // 添加表格
      pdf.addTable({
        headers: ['ID', '姓名', '部门'],
        rows: this.employees.map(emp => [emp.id, emp.name, emp.department]),
        styles: {
          cellPadding: 5,
          headerColor: '#eeeeee'
        }
      })
      
      // 生成并下载
      pdf.download('员工信息.pdf')
    }
  }
}
</script>

优点

  • 专为Vue设计,API更符合Vue习惯
  • 轻量级,易于集成
  • 支持基本的PDF生成功能

缺点

  • 功能相对有限
  • 社区支持不如jsPDF或pdfmake
  • 文档较少

4. 服务器端导出方案

4.1 前端请求服务器生成PDF

这种方案将PDF生成逻辑放在服务器端,前端只负责触发和下载。

前端代码

export function requestPDFExport(params) {
  return axios({
    url: '/api/export-pdf',
    method: 'POST',
    data: params,
    responseType: 'blob'
  }).then(response => {
    const blob = new Blob([response.data], { 
      type: 'application/pdf' 
    })
    const url = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    link.setAttribute('download', 'server_export.pdf')
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  })
}

// Vue组件中使用
methods: {
  async exportFromServer() {
    try {
      this.loading = true
      await requestPDFExport({
        reportType: 'sales',
        year: 2023,
        format: 'A4'
      })
    } catch (error) {
      console.error('导出失败:', error)
    } finally {
      this.loading = false
    }
  }
}

Node.js服务器端示例(使用pdfkit)

const express = require('express')
const PDFDocument = require('pdfkit')
const app = express()

app.post('/api/export-pdf', async (req, res) => {
  try {
    const { reportType, year, format } = req.body
    
    // 创建PDF文档
    const doc = new PDFDocument({ size: format || 'A4' })
    
    // 设置响应头
    res.setHeader('Content-Type', 'application/pdf')
    res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"')
    
    // 管道传输到响应
    doc.pipe(res)
    
    // 添加内容
    doc.fontSize(25).text(`${year}年度${getReportTitle(reportType)}`, {
      align: 'center'
    })
    
    doc.moveDown()
    doc.fontSize(12).text('生成日期: ' + new Date().toLocaleDateString())
    
    // 添加表格数据
    const data = await getReportData(reportType, year)
    drawTable(doc, data)
    
    // 结束并发送
    doc.end()
  } catch (error) {
    console.error('PDF生成错误:', error)
    res.status(500).send('PDF生成失败')
  }
})

function drawTable(doc, data) {
  const startY = 150
  const rowHeight = 30
  const colWidth = 150
  const headers = ['产品', 'Q1', 'Q2', 'Q3', 'Q4', '总计']
  
  // 表头
  doc.font('Helvetica-Bold')
  headers.forEach((header, i) => {
    doc.text(header, 50 + i * colWidth, startY, {
      width: colWidth,
      align: 'center'
    })
  })
  
  // 表格内容
  doc.font('Helvetica')
  data.forEach((row, rowIndex) => {
    const y = startY + (rowIndex + 1) * rowHeight
    
    // 绘制行背景
    if (rowIndex % 2 === 0) {
      doc.fillColor('#f5f5f5')
        .rect(50, y - 10, colWidth * headers.length, rowHeight)
        .fill()
      doc.fillColor('black')
    }
    
    // 绘制单元格文本
    Object.values(row).forEach((value, colIndex) => {
      doc.text(String(value), 50 + colIndex * colWidth, y, {
        width: colWidth,
        align: colIndex > 0 ? 'right' : 'left'
      })
    })
  })
}

app.listen(3000, () => console.log('Server running on port 3000'))

优点

  • 处理大数据量更高效
  • 减轻前端压力
  • 可以复用服务器端数据处理逻辑
  • 更安全,业务逻辑不暴露在客户端
  • 支持更复杂的PDF生成(如使用专业PDF库)

缺点

  • 增加服务器负载
  • 需要网络请求,可能有延迟
  • 实现复杂度更高

4.2 使用无头浏览器生成PDF

对于需要精确复制网页样式的场景,可以在服务器端使用无头浏览器(如Puppeteer)生成PDF。

Node.js服务器示例(使用Puppeteer)

const express = require('express')
const puppeteer = require('puppeteer')
const app = express()

app.post('/api/export-pdf', async (req, res) => {
  let browser = null
  try {
    const { url, options } = req.body
    
    browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    })
    
    const page = await browser.newPage()
    
    // 如果是URL,直接导航;如果是HTML内容,设置内容
    if (url.startsWith('http')) {
      await page.goto(url, { waitUntil: 'networkidle2' })
    } else {
      await page.setContent(url)
    }
    
    // 生成PDF
    const pdf = await page.pdf({
      format: 'A4',
      printBackground: true,
      margin: {
        top: '20mm',
        right: '20mm',
        bottom: '20mm',
        left: '20mm'
      },
      ...options
    })
    
    // 发送PDF
    res.setHeader('Content-Type', 'application/pdf')
    res.setHeader('Content-Disposition', 'attachment; filename="export.pdf"')
    res.send(pdf)
  } catch (error) {
    console.error('PDF生成错误:', error)
    res.status(500).send('PDF生成失败')
  } finally {
    if (browser) {
      await browser.close()
    }
  }
})

app.listen(3000, () => console.log('Server running on port 3000'))

优点

  • 精确复制网页样式和布局
  • 支持所有现代CSS特性
  • 可以处理动态内容
  • 支持截图、PDF等多种输出

缺点

  • 资源消耗大(需要运行浏览器实例)
  • 性能较差
  • 部署复杂度高

5. 方法对比与选择指南

5.1 功能对比表

方法生成方式样式保真度复杂度性能适用场景
浏览器打印客户端中等简单打印需求
jsPDF客户端编程式生成简单PDF
html2canvas+jsPDF客户端精确复制页面样式
pdfmake客户端结构化文档生成
vue-pdf客户端简单Vue集成
服务器生成服务端可高可低依赖实现复杂/大数据量PDF
无头浏览器服务端极高很高精确复制复杂页面

5.2 选择建议

  1. 简单打印需求:使用浏览器打印方案,成本最低
  2. 编程式生成简单PDF:选择jsPDF,纯前端实现
  3. 精确复制页面样式:html2canvas+jsPDF组合或服务器端无头浏览器方案
  4. 结构化文档生成:pdfmake提供更直观的声明式API
  5. Vue项目快速集成:考虑vue-pdf组件
  6. 大数据量或复杂处理:优先服务器端方案
  7. 最高样式保真度:无头浏览器方案(如Puppeteer)

5.3 性能优化建议

  1. 分页处理:对于大数据集,实现分页或分块生成
  2. 懒加载资源:只在需要时加载PDF生成库
  3. Web Worker:将耗时的生成过程放在Worker线程
  4. 服务器缓存:对频繁请求的相同内容缓存生成的PDF
  5. 按需生成:提供预览功能,只在用户确认后生成完整PDF
  6. 资源优化:压缩图片等资源减少PDF体积

6. 最佳实践示例

6.1 完整的Vue PDF导出组件

<template>
  <div class="pdf-export">
    <button 
      @click="showModal = true"
      class="export-button"
    >
      <i class="icon-pdf"></i> 导出PDF
    </button>
    
    <div v-if="showModal" class="export-modal">
      <div class="modal-content">
        <h3>PDF导出选项</h3>
        
        <div class="form-group">
          <label>文件名</label>
          <input v-model="filename" type="text" placeholder="请输入文件名">
        </div>
        
        <div class="form-group">
          <label>纸张大小</label>
          <select v-model="pageSize">
            <option value="A4">A4</option>
            <option value="A3">A3</option>
            <option value="Letter">Letter</option>
          </select>
        </div>
        
        <div class="form-group">
          <label>方向</label>
          <select v-model="orientation">
            <option value="portrait">纵向</option>
            <option value="landscape">横向</option>
          </select>
        </div>
        
        <div class="form-group">
          <label>
            <input v-model="includeHeaderFooter" type="checkbox"> 包含页眉页脚
          </label>
        </div>
        
        <div class="form-group">
          <label>
            <input v-model="onlySelected" type="checkbox"> 仅导出选中项
          </label>
        </div>
        
        <div class="button-group">
          <button @click="showModal = false">取消</button>
          <button @click="exportPDF" :disabled="exporting">
            {{ exporting ? '导出中...' : '确认导出' }}
          </button>
        </div>
        
        <div v-if="progress > 0" class="progress-bar">
          <div class="progress" :style="{ width: progress + '%' }"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { jsPDF } from 'jspdf'
import html2canvas from 'html2canvas'

export default {
  name: 'PdfExport',
  props: {
    tableData: {
      type: Array,
      required: true
    },
    selectedRows: {
      type: Array,
      default: () => []
    },
    tableColumns: {
      type: Array,
      default: () => []
    },
    title: {
      type: String,
      default: '导出数据'
    }
  },
  data() {
    return {
      showModal: false,
      filename: 'export',
      pageSize: 'A4',
      orientation: 'portrait',
      includeHeaderFooter: true,
      onlySelected: false,
      exporting: false,
      progress: 0
    }
  },
  methods: {
    async exportPDF() {
      this.exporting = true
      this.progress = 0
      
      try {
        // 确定导出数据
        const exportData = this.onlySelected && this.selectedRows.length > 0
          ? this.selectedRows
          : this.tableData
        
        // 创建PDF
        const doc = new jsPDF({
          orientation: this.orientation,
          unit: 'mm',
          format: this.pageSize
        })
        
        // 添加标题
        doc.setFont('helvetica', 'bold')
        doc.setFontSize(16)
        doc.text(this.title, 105, 20, { align: 'center' })
        
        // 添加生成时间
        doc.setFont('helvetica', 'normal')
        doc.setFontSize(10)
        doc.text(`生成时间: ${new Date().toLocaleString()}`, 105, 30, { align: 'center' })
        
        // 添加表格
        await this.addTableToPDF(doc, exportData)
        
        // 添加页脚
        if (this.includeHeaderFooter) {
          const pageCount = doc.internal.getNumberOfPages()
          for (let i = 1; i <= pageCount; i++) {
            doc.setPage(i)
            doc.setFontSize(8)
            doc.text(
              `第 ${i} 页 / 共 ${pageCount} 页`,
              doc.internal.pageSize.getWidth() - 20,
              doc.internal.pageSize.getHeight() - 10
            )
          }
        }
        
        // 保存文件
        doc.save(`${this.filename}.pdf`)
        
        this.$emit('export-success')
        this.showModal = false
      } catch (error) {
        console.error('导出失败:', error)
        this.$emit('export-error', error)
      } finally {
        this.exporting = false
        this.progress = 0
      }
    },
    
    async addTableToPDF(doc, data) {
      const columns = this.tableColumns.filter(col => !col.hidden)
      const headers = columns.map(col => col.label)
      
      // 准备表格数据
      const tableData = data.map(row => {
        return columns.map(col => {
          let value = row[col.prop]
          if (col.formatter) {
            value = col.formatter(row, col)
          }
          return value !== undefined ? String(value) : ''
        })
      })
      
      // 自动调整列宽
      const pageWidth = doc.internal.pageSize.getWidth() - 20 // 左右边距
      const colWidth = pageWidth / columns.length
      const colWidths = columns.map(() => colWidth)
      
      // 分页添加表格
      let startY = 40
      let remainingData = tableData
      
      while (remainingData.length > 0) {
        // 计算当前页能容纳的行数
        const pageHeight = doc.internal.pageSize.getHeight()
        const rowHeight = 7
        const maxRows = Math.floor((pageHeight - startY - 20) / rowHeight)
        
        const chunk = remainingData.slice(0, maxRows)
        remainingData = remainingData.slice(maxRows)
        
        // 添加表格部分
        doc.autoTable({
          head: [headers],
          body: chunk,
          startY,
          margin: { left: 10, right: 10 },
          styles: {
            fontSize: 8,
            cellPadding: 2,
            overflow: 'linebreak'
          },
          columnStyles: columns.reduce((styles, col, index) => {
            styles[index] = { 
              cellWidth: colWidths[index],
              halign: col.align || 'left'
            }
            return styles
          }, {})
        })
        
        // 更新进度
        this.progress = ((tableData.length - remainingData.length) / tableData

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2379731.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux中的DNS的安装与配置

DNS简介 DNS&#xff08;DomainNameSystem&#xff09;是互联网上的一项服务&#xff0c;它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便的访问互联网。 DNS使用的是53端口 通常DNS是以UDP这个较快速的数据传输协议来查询的&#xff0c;但是没有查…

linux服务器与时间服务器同步时间

内网部署服务器&#xff0c;需要同步时间 使用系统内置的systemctl-timesyncd进行时间同步 1.编辑配置文件 sudo nano /etc/systemd/timesyncd.conf修改添加内容入下 [Time] NTP10.100.13.198 FallbackNTP#说明 #NTP10.100.13.198&#xff1a;你的主 NTP 时间服务器 IP #Fall…

【数据结构篇】排序1(插入排序与选择排序)

注&#xff1a;本文以排升序为例 常见的排序算法&#xff1a; 目录&#xff1a; 一 直接插入排序&#xff1a; 1.1 基本思想&#xff1a; 1.2 代码&#xff1a; 1.3 复杂度&#xff1a; 二 希尔排序&#xff08;直接插入排序的优化&#xff09;&#xff1a; 2.1 基本思想…

《Linux服务与安全管理》| DNS服务器安装和配置

《Linux服务与安全管理》| DNS服务器安装和配置 目录 《Linux服务与安全管理》| DNS服务器安装和配置 第一步&#xff1a;使用dnf命令安装BIND服务 第二步&#xff1a;查看服务器server01的网络配置 第三步&#xff1a;配置全局配置文件 第四步&#xff1a;修改bind的区域…

Notepad++ 学习(三)使用python插件编写脚本:实现跳转指定标签页(自主研发)

目录 一、先看成果二、安装Python Script插件三、配置Python脚本四、使用脚本跳转标签页方法一&#xff1a;通过菜单运行方法二&#xff1a;设置快捷键&#xff08;推荐&#xff09; 五、注意事项六、进阶使用 官网地址&#xff1a; https://notepad-plus-plus.org/Python Scri…

Stable Diffusion 学习笔记02

模型下载网站&#xff1a; 1&#xff0c;LiblibAI-哩布哩布AI - 中国领先的AI创作平台 2&#xff0c;Civitai: The Home of Open-Source Generative AI 模型的安装&#xff1a; 将下载的sd模型放置在sd1.5的文件内即可&#xff0c;重启客户端可用。 外挂VAE模型&#xff1a…

python:pymysql概念、基本操作和注入问题讲解

python&#xff1a;pymysql分享目录 一、概念二、数据准备三、安装pymysql四、pymysql使用&#xff08;一&#xff09;使用步骤&#xff08;二&#xff09;查询操作&#xff08;三&#xff09;增&#xff08;四&#xff09;改&#xff08;五&#xff09;删 五、关于pymysql注入…

机器学习-人与机器生数据的区分模型测试 - 模型融合与检验

模型融合 # 先用普通Pipeline训练 from sklearn.pipeline import Pipeline#from sklearn2pmml.pipeline import PMMLPipeline train_pipe Pipeline([(scaler, StandardScaler()),(ensemble, VotingClassifier(estimators[(rf, RandomForestClassifier(n_estimators200, max_de…

机器学习 day03

文章目录 前言一、特征降维1.特征选择2.主成分分析&#xff08;PCA&#xff09; 二、KNN算法三、模型的保存与加载 前言 通过今天的学习&#xff0c;我掌握了机器学习中的特征降维的概念以及用法&#xff0c;KNN算法的基本原理及用法&#xff0c;模型的保存和加载 一、特征降维…

嵌入式软件--stm32 DAY 6 USART串口通讯(下)

1.寄存器轮询_收发字符串 通过寄存器轮询方式实现了收发单个字节之后&#xff0c;我们趁热打铁&#xff0c;争上游&#xff0c;进阶到字符串。字符串就是多个字符。很明显可以循环收发单个字节实现。 然后就是接收字符串。如果接受单个字符的函数放在while里&#xff0c;它也可…

问题处理——在ROS2(humble)+Gazebo+rqt下,无法显示仿真无人机的相机图像

文章目录 前言一、问题展示二、解决方法&#xff1a;1.下载对应版本的PX42.下载对应版本的Gazebo3.启动 总结 前言 在ROS2的环境下&#xff0c;进行无人机仿真的过程中&#xff0c;有时需要调取无人机的相机图像信息&#xff0c;但是使用rqt&#xff0c;却发现相机图像无法显示…

朱老师,3518e系列,第六季

第一节&#xff1a;概述。 首先是 将 他写好的 rtsp 源码上传&#xff0c;用于分析。 已经拷贝完。 第二节&#xff1a; h264 编码概念。 编解码 可以用cpu, 也可以用 bsp cpu 编解码的效果不好。做控制比较好。 h264 由 VCL&#xff0c; NAL 组成。 NAL 关心的是 压缩…

ElasticSearch-集群

本篇文章依据ElasticSearch权威指南进行实操和记录 1&#xff0c;空集群 即不包含任何节点的集群 集群大多数分为两类&#xff0c;主节点和数据节点 主节点 职责&#xff1a;主节点负责管理集群的状态&#xff0c;例如分配分片、添加和删除节点、监控节点故障等。它们不直接…

一文掌握工业相机选型计算

目录 一、基本概念 1.1 物方和像方 1.2 工作距离和视场 1.3 放大倍率 1.4 相机芯片尺寸 二、公式计算 三、实例应用 一、基本概念 1.1 物方和像方 在光学领域&#xff0c;物方&#xff08;Object Space&#xff09;是与像方&#xff08;Image Space&#xff09;相对的…

LabVIEW机械振动信号分析与故障诊断

利用 LabVIEW 开发机械振动信号分析与故障诊断系统&#xff0c;融合小波变换、时频分布、高阶统计量&#xff08;双谱&#xff09;等先进信号处理技术&#xff0c;实现对齿轮、发动机等机械部件的非平稳非高斯振动信号的特征提取与故障诊断。系统通过虚拟仪器技术将理论算法转化…

【Spring】Spring中的适配器模式

欢迎来到啾啾的博客&#x1f431;。 记录学习点滴。分享工作思考和实用技巧&#xff0c;偶尔也分享一些杂谈&#x1f4ac;。 欢迎评论交流&#xff0c;感谢您的阅读&#x1f604;。 目录 适配器模式Spring MVC的适配器模式 适配器模式 适配器模式&#xff08;Adapter Pattern&a…

2、ubuntu系统配置OpenSSH | 使用vscode或pycharm远程连接

1、OpenSSH介绍 OpenSSH&#xff08;Open Secure Shell&#xff09;是一套基于SSH协议的开源工具&#xff0c;用于在计算机网络中提供安全的加密通信。它被广泛用于远程系统管理、文件传输和网络服务的安全隧道搭建&#xff0c;是保护网络通信免受窃听和攻击的重要工具。 1.1…

RPC与SOAP的区别

一.RPC&#xff08;远程过程调用&#xff09;和SOAP&#xff08;简单对象访问协议&#xff09;均用于实现分布式系统中的远程通信&#xff0c;但两者在设计理念、协议实现及应用场景上存在显著差异。 二.对比 1.设计理念 2.协议规范 3.技术特性 4.典型应用场景 5.总结 三.总结…

Day11-苍穹外卖(数据统计篇)

前言&#xff1a; 今天写day11的内容&#xff0c;主要讲了四个统计接口的制作。看起来内容较多&#xff0c;其实代码逻辑都是相似的&#xff0c;这里我们过一遍。 今日所学&#xff1a; Apache ECharts营业额统计用户统计订单统计销量排行统计 1. Apache ECharts 1.1 介绍 A…

Tomcat简述介绍

文章目录 Web服务器Tomcat的作用Tomcat分析目录结构 Web服务器 Web服务器的作用是接收客户端的请求&#xff0c;给客户端作出响应。 知名Java Web服务器 Tomcat&#xff08;Apache&#xff09;&#xff1a;用来开发学习使用&#xff1b;免费&#xff0c;开源JBoss&#xff0…