Vue環(huán)境下數(shù)據(jù)導(dǎo)出為PDF的最全指南
1. 前言
在現(xiàn)代Web應(yīng)用開發(fā)中,將數(shù)據(jù)導(dǎo)出為PDF是一項(xiàng)常見(jiàn)且重要的功能需求。PDF作為一種通用的文檔格式,具有跨平臺(tái)、保持格式一致、易于打印等優(yōu)勢(shì)。Vue.js作為當(dāng)前流行的前端框架,提供了多種實(shí)現(xiàn)PDF導(dǎo)出的方法。本文將全面探討Vue環(huán)境下實(shí)現(xiàn)PDF導(dǎo)出的7種主要方法,包括原生瀏覽器API、常用第三方庫(kù)方案以及服務(wù)器端導(dǎo)出方案,每種方法都將提供詳細(xì)的代碼示例和優(yōu)劣分析。
2. 原生瀏覽器打印方案
2.1 使用window.print()實(shí)現(xiàn)
這種方法利用瀏覽器自帶的打印功能,通過(guò)CSS媒體查詢控制打印樣式。
實(shí)現(xiàn)原理:
- 創(chuàng)建專門用于打印的組件或視圖
- 使用@media print定義打印樣式
- 調(diào)用window.print()觸發(fā)瀏覽器打印對(duì)話框
代碼示例:
<template>
<div>
<button @click="print">打印PDF</button>
<div ref="printContent" class="print-container">
<h1>銷售報(bào)表</h1>
<table>
<thead>
<tr>
<th>日期</th>
<th>產(chǎn)品</th>
<th>數(shù)量</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: '產(chǎn)品A', quantity: 10, amount: 1000 },
{ id: 2, date: '2023-01-02', product: '產(chǎn)品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%;
}
/* 打印分頁(yè)控制 */
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>優(yōu)點(diǎn):
- 零依賴,不增加項(xiàng)目體積
- 實(shí)現(xiàn)簡(jiǎn)單,適合簡(jiǎn)單打印需求
- 用戶可以選擇"另存為PDF"(大多數(shù)現(xiàn)代瀏覽器支持)
缺點(diǎn):
- 依賴用戶操作,無(wú)法自動(dòng)化
- 打印樣式控制有限
- 無(wú)法生成復(fù)雜的PDF文檔
- 不同瀏覽器表現(xiàn)可能不一致
2.2 使用CSS Paged Media模塊
對(duì)于更專業(yè)的打印需求,可以使用CSS Paged Media模塊定義分頁(yè)、頁(yè)眉頁(yè)腳等。
代碼示例:
@page {
size: A4;
margin: 1cm;
@top-left {
content: "公司名稱";
font-size: 10pt;
}
@top-right {
content: "頁(yè)碼 " counter(page) " / " counter(pages);
font-size: 10pt;
}
@bottom-center {
content: "機(jī)密文件";
font-size: 8pt;
color: #999;
}
}
@media print {
h1 {
page-break-before: always;
}
table {
page-break-inside: avoid;
}
.page-break {
page-break-after: always;
}
}優(yōu)點(diǎn):
- 更專業(yè)的打印控制
- 支持頁(yè)碼、頁(yè)眉頁(yè)腳等高級(jí)功能
- 仍然是純CSS方案,無(wú)JavaScript依賴
缺點(diǎn):
- 瀏覽器支持不一致(特別是復(fù)雜特性)
- 學(xué)習(xí)曲線較陡
- 仍然依賴瀏覽器打印功能
3. 常用第三方庫(kù)方案
3.1 使用jsPDF
jsPDF是最流行的JavaScript PDF生成庫(kù)之一,可以直接在瀏覽器中生成PDF。
安裝:
npm install jspdf
基礎(chǔ)實(shí)現(xiàn):
import jsPDF from 'jspdf'
export function generatePDF(title, data, columns, filename = 'export.pdf') {
const doc = new jsPDF()
// 添加標(biāo)題
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)
})
// 添加數(shù)據(jù)行
doc.setFontSize(10)
data.forEach((row, rowIndex) => {
y = 40 + rowIndex * 10
if (y > 280) { // 接近頁(yè)面底部
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: '產(chǎn)品A', price: 100, stock: 50 },
{ id: 2, name: '產(chǎn)品B', price: 200, stock: 30 }
]
const columns = [
{ label: 'ID', field: 'id' },
{ label: '產(chǎn)品名稱', field: 'name' },
{ label: '價(jià)格', field: 'price' },
{ label: '庫(kù)存', field: 'stock' }
]
generatePDF('產(chǎn)品列表', data, columns)
}
}高級(jí)功能示例:
function generateAdvancedPDF() {
const doc = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
})
// 設(shè)置元數(shù)據(jù)
doc.setProperties({
title: '高級(jí)報(bào)表',
subject: '2023年度銷售數(shù)據(jù)',
author: '銷售系統(tǒng)',
keywords: '銷售, 報(bào)表, 2023',
creator: '公司銷售系統(tǒng)'
})
// 添加公司logo
const imgData = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...'
doc.addImage(imgData, 'PNG', 10, 10, 50, 15)
// 添加標(biāo)題
doc.setFont('helvetica', 'bold')
doc.setFontSize(22)
doc.setTextColor(40, 40, 40)
doc.text('2023年度銷售報(bào)表', 105, 20, { align: 'center' })
// 添加表格
const headers = [['產(chǎn)品', 'Q1', 'Q2', 'Q3', 'Q4', '總計(jì)']]
const salesData = [
['產(chǎn)品A', 1000, 1500, 1200, 1800, 5500],
['產(chǎn)品B', 800, 900, 1000, 1200, 3900],
['產(chǎn)品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) {
// 頁(yè)腳
doc.setFontSize(10)
doc.setTextColor(150)
const pageCount = doc.internal.getNumberOfPages()
doc.text(`第 ${doc.internal.getCurrentPageInfo().pageNumber} 頁(yè) / 共 ${pageCount} 頁(yè)`,
data.settings.margin.left,
doc.internal.pageSize.height - 10
)
}
})
// 添加折線圖(使用jsPDF的簡(jiǎn)單繪圖功能)
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')
}優(yōu)點(diǎn):
- 純前端實(shí)現(xiàn),不依賴服務(wù)器
- 功能豐富,支持文本、表格、簡(jiǎn)單圖形等
- 支持自定義字體
- 活躍的社區(qū)和良好的文檔
缺點(diǎn):
- 復(fù)雜布局實(shí)現(xiàn)較困難
- 原生不支持復(fù)雜表格樣式(需要插件)
- 中文支持需要額外配置字體
3.2 使用html2canvas + jsPDF
這種方案先將HTML轉(zhuǎn)換為canvas,再將canvas轉(zhuǎn)為PDF,適合需要精確復(fù)制頁(yè)面樣式的場(chǎng)景。
安裝:
npm install html2canvas jspdf
基礎(chǔ)實(shí)現(xiàn):
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 // 更高分辨率
})
}
}高級(jí)功能示例:
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)
// 添加頁(yè)腳
pdf.setFontSize(10)
pdf.setTextColor(150)
pdf.text(
`第 ${i + 1} 頁(yè)`,
pdf.internal.pageSize.getWidth() / 2,
pdf.internal.pageSize.getHeight() - 10,
{ align: 'center' }
)
}
pdf.save('multi_page_report.pdf')
}優(yōu)點(diǎn):
- 精確復(fù)制頁(yè)面樣式和布局
- 支持復(fù)雜HTML結(jié)構(gòu)和CSS樣式
- 可以處理動(dòng)態(tài)生成的內(nèi)容
- 支持多頁(yè)導(dǎo)出
缺點(diǎn):
- 生成的PDF本質(zhì)是圖片,文字不可選擇
- 大頁(yè)面可能導(dǎo)致內(nèi)存問(wèn)題
- 性能較差,特別是復(fù)雜頁(yè)面
- 分頁(yè)控制較困難
3.3 使用pdfmake
pdfmake是一個(gè)聲明式的PDF生成庫(kù),使用JSON格式定義文檔結(jié)構(gòu)。
安裝:
npm install pdfmake
基礎(chǔ)實(shí)現(xiàn):
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: '銷售報(bào)表', style: 'header' },
'\n\n',
{
table: {
headerRows: 1,
widths: ['*', 'auto', 'auto', 'auto'],
body: [
['產(chǎn)品', '季度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: '產(chǎn)品A', q1: 1000, q2: 1500, q3: 1200 },
{ product: '產(chǎn)品B', q1: 800, q2: 900, q3: 1000 }
]
generatePDFWithPdfMake(data, 'sales_report.pdf')
}
}高級(jí)功能示例:
function generateAdvancedPdfMakeDocument() {
const docDefinition = {
pageSize: 'A4',
pageMargins: [40, 60, 40, 60],
header: function(currentPage, pageCount) {
return {
text: `第 ${currentPage} 頁(yè) / 共 ${pageCount} 頁(yè)`,
alignment: 'right',
margin: [0, 20, 20, 0]
}
},
footer: function(currentPage, pageCount) {
return {
text: '公司機(jī)密 - 未經(jīng)授權(quán)禁止復(fù)制',
alignment: 'center',
fontSize: 8,
margin: [0, 0, 0, 20]
}
},
content: [
{
columns: [
{
width: 100,
image: 'logo',
fit: [80, 80]
},
{
width: '*',
text: '2023年度財(cái)務(wù)報(bào)告',
style: 'reportTitle'
}
]
},
'\n\n',
{
text: '1. 銷售概覽',
style: 'sectionHeader'
},
{
text: '本年度公司整體銷售額達(dá)到¥1,200萬(wàn),同比增長(zhǎng)15%。主要增長(zhǎng)來(lái)自產(chǎn)品線A和B。',
style: 'paragraph'
},
'\n',
{
style: 'tableExample',
table: {
widths: ['*', '*', '*', '*'],
body: [
[
{ text: '產(chǎn)品線', style: 'tableHeader' },
{ text: 'Q1', style: 'tableHeader' },
{ text: 'Q2', style: 'tableHeader' },
{ text: 'Q3', style: 'tableHeader' }
],
['產(chǎn)品A', '320萬(wàn)', '350萬(wàn)', '380萬(wàn)'],
['產(chǎn)品B', '280萬(wàn)', '300萬(wàn)', '320萬(wàn)'],
['產(chǎn)品C', '150萬(wàn)', '160萬(wàn)', '170萬(wàn)'],
[
{ text: '總計(jì)', style: 'tableHeader' },
{ text: '750萬(wàn)', 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: '成本構(gòu)成示意圖',
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()
}優(yōu)點(diǎn):
- 聲明式API,易于理解和使用
- 強(qiáng)大的布局和樣式控制能力
- 內(nèi)置分頁(yè)、頁(yè)眉頁(yè)腳支持
- 支持表格、列表、圖片等多種元素
缺點(diǎn):
- 中文支持需要額外配置字體
- 學(xué)習(xí)曲線較陡(特別是復(fù)雜布局)
- 文檔結(jié)構(gòu)可能變得復(fù)雜
- 性能不如原生jsPDF
3.4 使用vue-pdf
vue-pdf是一個(gè)Vue專用的PDF生成和顯示組件庫(kù)。
安裝:
npm install @tato30/vue-pdf
基礎(chǔ)實(shí)現(xiàn):
<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: '研發(fā)' },
{ id: 2, name: '李四', department: '市場(chǎng)' }
]
}
},
methods: {
async generatePDF() {
const pdf = new VuePdf()
// 添加內(nèi)容
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>優(yōu)點(diǎn):
- 專為Vue設(shè)計(jì),API更符合Vue習(xí)慣
- 輕量級(jí),易于集成
- 支持基本的PDF生成功能
缺點(diǎn):
- 功能相對(duì)有限
- 社區(qū)支持不如jsPDF或pdfmake
- 文檔較少
4. 服務(wù)器端導(dǎo)出方案
4.1 前端請(qǐng)求服務(wù)器生成PDF
這種方案將PDF生成邏輯放在服務(wù)器端,前端只負(fù)責(zé)觸發(fā)和下載。
前端代碼:
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('導(dǎo)出失敗:', error)
} finally {
this.loading = false
}
}
}Node.js服務(wù)器端示例(使用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
// 創(chuàng)建PDF文檔
const doc = new PDFDocument({ size: format || 'A4' })
// 設(shè)置響應(yīng)頭
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"')
// 管道傳輸?shù)巾憫?yīng)
doc.pipe(res)
// 添加內(nèi)容
doc.fontSize(25).text(`${year}年度${getReportTitle(reportType)}`, {
align: 'center'
})
doc.moveDown()
doc.fontSize(12).text('生成日期: ' + new Date().toLocaleDateString())
// 添加表格數(shù)據(jù)
const data = await getReportData(reportType, year)
drawTable(doc, data)
// 結(jié)束并發(fā)送
doc.end()
} catch (error) {
console.error('PDF生成錯(cuò)誤:', error)
res.status(500).send('PDF生成失敗')
}
})
function drawTable(doc, data) {
const startY = 150
const rowHeight = 30
const colWidth = 150
const headers = ['產(chǎn)品', 'Q1', 'Q2', 'Q3', 'Q4', '總計(jì)']
// 表頭
doc.font('Helvetica-Bold')
headers.forEach((header, i) => {
doc.text(header, 50 + i * colWidth, startY, {
width: colWidth,
align: 'center'
})
})
// 表格內(nèi)容
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'))優(yōu)點(diǎn):
- 處理大數(shù)據(jù)量更高效
- 減輕前端壓力
- 可以復(fù)用服務(wù)器端數(shù)據(jù)處理邏輯
- 更安全,業(yè)務(wù)邏輯不暴露在客戶端
- 支持更復(fù)雜的PDF生成(如使用專業(yè)PDF庫(kù))
缺點(diǎn):
- 增加服務(wù)器負(fù)載
- 需要網(wǎng)絡(luò)請(qǐng)求,可能有延遲
- 實(shí)現(xiàn)復(fù)雜度更高
4.2 使用無(wú)頭瀏覽器生成PDF
對(duì)于需要精確復(fù)制網(wǎng)頁(yè)樣式的場(chǎng)景,可以在服務(wù)器端使用無(wú)頭瀏覽器(如Puppeteer)生成PDF。
Node.js服務(wù)器示例(使用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,直接導(dǎo)航;如果是HTML內(nèi)容,設(shè)置內(nèi)容
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
})
// 發(fā)送PDF
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'attachment; filename="export.pdf"')
res.send(pdf)
} catch (error) {
console.error('PDF生成錯(cuò)誤:', error)
res.status(500).send('PDF生成失敗')
} finally {
if (browser) {
await browser.close()
}
}
})
???????app.listen(3000, () => console.log('Server running on port 3000'))優(yōu)點(diǎn):
- 精確復(fù)制網(wǎng)頁(yè)樣式和布局
- 支持所有現(xiàn)代CSS特性
- 可以處理動(dòng)態(tài)內(nèi)容
- 支持截圖、PDF等多種輸出
缺點(diǎn):
- 資源消耗大(需要運(yùn)行瀏覽器實(shí)例)
- 性能較差
- 部署復(fù)雜度高
5. 方法對(duì)比與選擇指南
5.1 功能對(duì)比表
| 方法 | 生成方式 | 樣式保真度 | 復(fù)雜度 | 性能 | 適用場(chǎng)景 |
|---|---|---|---|---|---|
| 瀏覽器打印 | 客戶端 | 中等 | 低 | 高 | 簡(jiǎn)單打印需求 |
| jsPDF | 客戶端 | 低 | 中 | 高 | 編程式生成簡(jiǎn)單PDF |
| html2canvas+jsPDF | 客戶端 | 高 | 中 | 中 | 精確復(fù)制頁(yè)面樣式 |
| pdfmake | 客戶端 | 中 | 中 | 中 | 結(jié)構(gòu)化文檔生成 |
| vue-pdf | 客戶端 | 低 | 低 | 高 | 簡(jiǎn)單Vue集成 |
| 服務(wù)器生成 | 服務(wù)端 | 可高可低 | 高 | 依賴實(shí)現(xiàn) | 復(fù)雜/大數(shù)據(jù)量PDF |
| 無(wú)頭瀏覽器 | 服務(wù)端 | 極高 | 很高 | 低 | 精確復(fù)制復(fù)雜頁(yè)面 |
5.2 選擇建議
簡(jiǎn)單打印需求:使用瀏覽器打印方案,成本最低
編程式生成簡(jiǎn)單PDF:選擇jsPDF,純前端實(shí)現(xiàn)
精確復(fù)制頁(yè)面樣式:html2canvas+jsPDF組合或服務(wù)器端無(wú)頭瀏覽器方案
結(jié)構(gòu)化文檔生成:pdfmake提供更直觀的聲明式API
Vue項(xiàng)目快速集成:考慮vue-pdf組件
大數(shù)據(jù)量或復(fù)雜處理:優(yōu)先服務(wù)器端方案
最高樣式保真度:無(wú)頭瀏覽器方案(如Puppeteer)
5.3 性能優(yōu)化建議
分頁(yè)處理:對(duì)于大數(shù)據(jù)集,實(shí)現(xiàn)分頁(yè)或分塊生成
懶加載資源:只在需要時(shí)加載PDF生成庫(kù)
Web Worker:將耗時(shí)的生成過(guò)程放在Worker線程
服務(wù)器緩存:對(duì)頻繁請(qǐng)求的相同內(nèi)容緩存生成的PDF
按需生成:提供預(yù)覽功能,只在用戶確認(rèn)后生成完整PDF
資源優(yōu)化:壓縮圖片等資源減少PDF體積
6. 最佳實(shí)踐示例
完整的Vue PDF導(dǎo)出組件
<template>
<div class="pdf-export">
<button
@click="showModal = true"
class="export-button"
>
<i class="icon-pdf"></i> 導(dǎo)出PDF
</button>
<div v-if="showModal" class="export-modal">
<div class="modal-content">
<h3>PDF導(dǎo)出選項(xiàng)</h3>
<div class="form-group">
<label>文件名</label>
<input v-model="filename" type="text" placeholder="請(qǐng)輸入文件名">
</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"> 包含頁(yè)眉頁(yè)腳
</label>
</div>
<div class="form-group">
<label>
<input v-model="onlySelected" type="checkbox"> 僅導(dǎo)出選中項(xiàng)
</label>
</div>
<div class="button-group">
<button @click="showModal = false">取消</button>
<button @click="exportPDF" :disabled="exporting">
{{ exporting ? '導(dǎo)出中...' : '確認(rèn)導(dǎo)出' }}
</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: '導(dǎo)出數(shù)據(jù)'
}
},
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 {
// 確定導(dǎo)出數(shù)據(jù)
const exportData = this.onlySelected && this.selectedRows.length > 0
? this.selectedRows
: this.tableData
// 創(chuàng)建PDF
const doc = new jsPDF({
orientation: this.orientation,
unit: 'mm',
format: this.pageSize
})
// 添加標(biāo)題
doc.setFont('helvetica', 'bold')
doc.setFontSize(16)
doc.text(this.title, 105, 20, { align: 'center' })
// 添加生成時(shí)間
doc.setFont('helvetica', 'normal')
doc.setFontSize(10)
doc.text(`生成時(shí)間: ${new Date().toLocaleString()}`, 105, 30, { align: 'center' })
// 添加表格
await this.addTableToPDF(doc, exportData)
// 添加頁(yè)腳
if (this.includeHeaderFooter) {
const pageCount = doc.internal.getNumberOfPages()
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i)
doc.setFontSize(8)
doc.text(
`第 ${i} 頁(yè) / 共 ${pageCount} 頁(yè)`,
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('導(dǎo)出失敗:', 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)
// 準(zhǔn)備表格數(shù)據(jù)
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) : ''
})
})
// 自動(dòng)調(diào)整列寬
const pageWidth = doc.internal.pageSize.getWidth() - 20 // 左右邊距
const colWidth = pageWidth / columns.length
const colWidths = columns.map(() => colWidth)
// 分頁(yè)添加表格
let startY = 40
let remainingData = tableData
while (remainingData.length > 0) {
// 計(jì)算當(dāng)前頁(yè)能容納的行數(shù)
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
}, {})
})
// 更新進(jìn)度
this.progress = ((tableData.length - remainingData.length) / tableData
到此這篇關(guān)于Vue環(huán)境下數(shù)據(jù)導(dǎo)出為PDF的最全指南的文章就介紹到這了,更多相關(guān)Vue數(shù)據(jù)導(dǎo)出為PDF內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue如何限制只能輸入正負(fù)數(shù)及小數(shù)
這篇文章主要介紹了vue如何限制只能輸入正負(fù)數(shù)及小數(shù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07
vuejs使用遞歸組件實(shí)現(xiàn)樹形目錄的方法
本篇文章主要介紹了vuejs使用遞歸組件實(shí)現(xiàn)樹形目錄的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
axios中post請(qǐng)求json和application/x-www-form-urlencoded詳解
Axios是專注于網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求的庫(kù),相比于原生的XMLHttpRequest對(duì)象,axios簡(jiǎn)單易用,下面這篇文章主要給大家介紹了關(guān)于axios中post請(qǐng)求json和application/x-www-form-urlencoded的相關(guān)資料,需要的朋友可以參考下2022-10-10
Vue.js組件tabs實(shí)現(xiàn)選項(xiàng)卡切換效果
這篇文章主要為大家詳細(xì)介紹了Vue.js組件tabs實(shí)現(xiàn)選項(xiàng)卡切換效果的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
vue用ant design中table表格,點(diǎn)擊某行時(shí)觸發(fā)的事件操作
這篇文章主要介紹了vue用ant design中table表格,點(diǎn)擊某行時(shí)觸發(fā)的事件操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
vue項(xiàng)目打包后,由于html被緩存導(dǎo)致出現(xiàn)白屏的處理方案
這篇文章主要介紹了vue項(xiàng)目打包后,由于html被緩存導(dǎo)致出現(xiàn)白屏的處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03

