最近粉丝咨询最多的问题莫过于 pxcharts 多维表是否能导出PDF的能力了。

说实话,我回避了很久。浏览器打印引擎差异大,中文渲染、分页断行、复杂表格适配...每个都是坑。
直到上个月,一个做财务的朋友跟我吐槽:月底导报表,调格式调到凌晨2点。我决定,这功能必须上。
于是在1周的设计和研究下,终于实现了多维表导出PDF的功能。
演示如下:

导出后的PDF文件预览效果:

演示地址:pxcharts.com
接下来和大家分享一下详细的功能技术实现。
Pxcharts多维表导出PDF功能技术实现
支持将表格数据导出为 PDF 格式,便于用户打印、存档和分享,核心需求包括:
- 保持表格结构和样式
- 支持分页(避免行被截断)
- 支持封面页(统计信息)
- 状态标签着色
- 横向/纵向布局可选
技术选型
为了实现这个方案,我们的核心依赖如下:
| 依赖 | 版本 | 用途 |
|---|---|---|
jspdf |
latest | 生成 PDF 文件 |
html2canvas |
latest | 将 HTML 渲染为 Canvas 图像 |
选型理由
为什么选择 html2canvas + jsPDF?原因如下:
- 纯前端实现无需后端服务,保护数据隐私
- 样式可控通过 CSS 精确控制 PDF 外观
- 兼容性好支持现代浏览器
- 生态成熟社区活跃,文档完善
为什么不直接用 jsPDF 的表格 API?
- jsPDF 的
autoTable插件对复杂样式支持有限 - 自定义样式(状态标签着色、交替行背景)实现困难
- html2canvas 可以复用现有的 HTML/CSS 样式
实现架构
整体流程我这里设计如下:
css
表格数据
↓
生成 HTML(按页)
↓
html2canvas 渲染为 Canvas
↓
Canvas 转 PNG 图像
↓
jsPDF 写入 PDF(每页一张图)
↓
下载 PDF 文件
分页策略
关键问题:如何避免表格行在分页时被截断?
我的解决方案:按行预分页
- 估算每行高度(约 36px)
- 计算每页可容纳行数:
rowsPerPage = floor((pageHeight - headerHeight) / rowHeight) - 按行数切分数据,每页独立渲染
- 每页都包含表头,方便阅读
ini
const estimateRowHeight = 36// 每行大约 36px
const headerHeight = 60// 表头高度
const pageContentHeightPx = Math.round(contentHeight / scale)
const rowsPerPage = Math.floor((pageContentHeightPx - headerHeight) / estimateRowHeight)
// 分页
for (let i = 0; i < records.length; i += rowsPerPage) {
const pageRecords = records.slice(i, i + rowsPerPage)
pages.push(renderDataPage(pageRecords, i))
}
核心代码解析
1. 动态导入(SSR 兼容):
go
const [{ default: jsPDF }, { default: html2canvas }] = awaitPromise.all([
import("jspdf"),
import("html2canvas"),
])
原因 :jspdf 和 html2canvas 依赖浏览器 API(如 document、window),在 Next.js SSR 阶段会报错。使用动态导入确保只在客户端执行。
2. 页面尺寸计算:
arduino
const pageDimensions = {
a4: { width: 595, height: 842 }, // pt 单位
a3: { width: 842, height: 1191 },
}
const pdfWidth = orientation === "landscape"
? pageDimensions[pageSize].height
: pageDimensions[pageSize].width
注意 :jsPDF 使用 pt(点)作为单位,1pt = 1/72 英寸。
3. HTML 生成
数据页结构这里我预设如下:
javascript
<divstyle="width:1122px;padding:32px;box-sizing:border-box;background:#fff">
<tablestyle="width:100%;border-collapse:collapse">
<thead><!-- 表头 --></thead>
<tbody><!-- 数据行 --></tbody>
</table>
</div>
关键样式:
width:1122px固定 canvas 宽度(A4 横向像素)border-collapse:collapse合并表格边框white-space:nowrap防止文本换行
4. Canvas 渲染
php
const canvas = awaithtml2canvas(element, {
scale: 2, // 2倍缩放,提高清晰度
useCORS: true, // 允许跨域图片
allowTaint: true, // 允许污染 canvas
backgroundColor: "#ffffff",
logging: false,
})
参数说明:
| 参数 | 说明 |
|---|---|
scale: 2 |
2倍分辨率,PDF 更清晰 |
useCORS |
处理跨域图片(如附件预览图) |
allowTaint |
允许 canvas 被污染(某些图片需要) |
5. PDF 写入
arduino
const imgData = canvas.toDataURL("image/png", 1.0)
const imgWidth = contentWidth
const imgHeight = (canvas.height * imgWidth) / canvas.width
pdf.addImage(imgData, "PNG", margin, margin, imgWidth, imgHeight)
图像格式选择:
- PNG无损,清晰度高,适合文字
- JPEG有损压缩,文件小,但不适合文字
样式处理技巧
状态标签着色这里我做了一层数据映射,方便精准还原样式:
vbnet
constcolorMap: Record<string, string> = {
"已完成": "#dcfce7;color:#16a34a",
"进行中": "#dbeafe;color:#2563eb",
"待开始": "#fef3c7;color:#d97706",
"已停滞": "#f3f4f6;color:#6b7280",
"重要紧急": "#fee2e2;color:#dc2626",
}
交替行背景我采用的逻辑判断来动态渲染:
css
<tr style="background:${idx % 2 === 0 ? "#fff" : "#f8fafc"}">
如果文本出现截断换行,用canvas很难处理,这里我采用如下方案截断处理:
css
// 方案1:省略号截断(适合固定宽度列)
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px">
// 方案2:完全显示(适合自动宽度列)
<spanstyle="white-space:nowrap">
当然还有很多细节的处理,这里就不一一介绍了。我们可以基于这个方案,继续扩展出如下场景:
- 水印支持添加企业 Logo 或水印
- 页码在页脚添加 "第 X 页 / 共 Y 页"
- 图表嵌入将图表大屏的图表嵌入 PDF
- 批量导出支持同时导出多个表格
今天就分享到这,后续我们还会持续迭代和更新,打造最强大的多维表格和文档协同系统。
演示地址:pxcharts.com