前言
在前端项目中,使用 TypeScript 结合 jsPDF 和 jspdf-autotable 导出含可点击链接的表格 PDF 时,需解决字体适配、类型报错和链接配置等问题。本文记录具体实现步骤和常见问题解决方案。
一、字体文件转换:TTF 转 TS 格式
为确保 PDF 正常显示中文,需将本地字体文件转换为项目可导入的格式:
-
准备字体文件
下载
simhei.ttf
(黑体)字体文件,放入项目src/assets/fonts
目录。 -
创建转换脚本
在
src
目录下新建convert.js
,用于将 TTF 字体转换为 Base64 编码的 TS 文件:jsimport fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // 计算当前文件目录 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 配置字体路径和输出路径 const fontPath = path.resolve(__dirname, 'src/assets/fonts/simhei.ttf'); const outputPath = path.resolve(__dirname, 'src/assets/fonts/simhei.ts'); try { // 读取 TTF 文件并转换为 Base64 const fontContent = fs.readFileSync(fontPath, 'base64'); // 生成可导入的 TS 文件 const jsContent = `export default \`${fontContent}\`;`; fs.writeFileSync(outputPath, jsContent); console.log('✅ 字体转换成功!文件已生成:', outputPath); } catch (error) { console.error('❌ 转换失败:', error.message); console.error('请检查:1. 字体文件是否存在 2. 格式是否为.ttf 3. 文件是否损坏'); }
-
执行转换
运行
node convert.js
,在src/assets/fonts
目录下生成simhei.ts
文件,供项目导入使用。
二、自定义类型声明:解决 TS 类型报错
由于 jsPDF 原生类型定义中不含 autoTable
方法,需扩展接口消除类型报错:
在 src/types
目录下新建 jspdf-autotable.d.ts
:
ts
// 扩展 jsPDF 接口,添加 autoTable 方法的类型定义
import jsPDF from 'jspdf';
import { AutoTableOptions, AutoTableResult } from 'jspdf-autotable';
declare module 'jspdf' {
interface jsPDF {
autoTable: (options: AutoTableOptions) => AutoTableResult;
}
}
三、核心实现:导出含可点击链接的表格 PDF
1. 依赖版本说明
jspdf
: 2.5.2jspdf-autotable
: 5.0.2
2. 具体代码实现
ts
import { ref, toRefs } from 'vue';
import dayjs from 'dayjs';
import jsPDF from 'jspdf';
import { autoTable, type UserOptions } from 'jspdf-autotable';
import SimheiFont from '@/assets/fonts/simhei'; // 导入转换后的字体
// 手动绑定 autoTable 到 jsPDF 原型
jsPDF.prototype.autoTable = function(options: any) {
return autoTable(this, options);
};
// 字体配置:注册中文字体
const configureFonts = (pdf: jsPDF) => {
try {
// 清除字体缓存
if ((pdf as any).fontDictionary) (pdf as any).fontDictionary = {};
// 添加字体到虚拟文件系统
pdf.addFileToVFS('simhei.ttf', SimheiFont);
// 注册字体(名称:simhei,字重:normal)
pdf.addFont('simhei.ttf', 'simhei', 'normal');
// 设置默认字体
pdf.setFont('simhei');
console.log('字体注册成功');
} catch (error) {
console.error('字体注册失败:', error);
// 降级使用内置字体
pdf.setFont('helvetica');
console.warn('已切换到默认字体,可能无法正常显示中文');
}
};
// 处理 PDF 下载逻辑
const handleClickComfirm = async () => {
// 假设从 props 获取表格数据
const { tableData } = toRefs(props);
// 创建 PDF 实例(A4 纸张,纵向)
const pdf = new jsPDF('p', 'mm', 'a4');
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const margin = 15; // 页边距
let yPos = 40; // 内容起始 Y 坐标
// 配置字体
configureFonts(pdf);
// 添加标题
pdf.setFontSize(16);
pdf.text('企业报告', margin, 25);
// 表格导出核心逻辑
const clueTableData = tableData.value || [];
const hasClueData = clueTableData.length > 0;
// 添加表格标题
pdf.setFontSize(14);
pdf.text('利好线索列表', margin, yPos);
yPos += 12;
if (hasClueData) {
// 格式化表格数据
const formattedData = clueTableData.map((item: any) => ({
type: item.eventTypeName || item.tagCodeName || '未知类型',
content: item.eventTitle || '无内容',
time: item.eventPubTime ? dayjs(item.eventPubTime).format('YYYY-MM-DD') : '无时间',
operation: '查看详情',
detailUrl: `${window.location.origin}/report/detail/${item.tagCode}?id=${item.eventDataappId}`
}));
// 定义表格列配置
const columns = [
{ header: '类型', dataKey: 'type' },
{ header: '内容', dataKey: 'content' },
{ header: '时间', dataKey: 'time' },
{ header: '操作', dataKey: 'operation' }
];
// 表格样式配置
const tableOptions: UserOptions = {
columns,
body: formattedData,
startY: yPos,
margin: { left: margin, right: margin },
// 表头样式(关键:设置字重为 normal 避免乱码)
headStyles: {
font: 'simhei',
fontStyle: 'normal' // 覆盖默认 bold 样式
},
// 表格内容样式
bodyStyles: {
font: 'simhei',
fontSize: 10,
cellPadding: 5,
lineColor: 'gainsboro'
},
// 列宽调整
columnStyles: {
type: { cellWidth: 30 },
content: { cellWidth: 65 },
time: { cellWidth: 30 },
operation: { cellWidth: 25 }
},
// 为"操作"列添加可点击链接
didDrawCell: (data: any) => {
const cell = data.cell;
if (data.column.dataKey === 'operation') {
const rowData = data.row.raw;
if (rowData.detailUrl) {
// 计算链接区域坐标
const linkX = cell.x + 2;
const linkY = cell.y + 2;
const linkWidth = cell.width - 4;
const linkHeight = cell.height - 4;
// 添加 PDF 链接
pdf.link(linkX, linkY, linkWidth, linkHeight, { url: rowData.detailUrl });
// 绘制下划线增强视觉提示
pdf.setDrawColor(0, 0, 255);
pdf.setLineWidth(0.3);
pdf.line(linkX, linkY + cell.height - 3, linkX + linkWidth, linkY + cell.height - 3);
}
}
}
};
try {
// 渲染表格
autoTable(pdf, tableOptions);
// 获取表格底部 Y 坐标,用于后续内容排版
const finalY = (pdf as any).autoTable?.previous?.finalY;
yPos = typeof finalY === 'number' ? finalY + 10 : yPos + 50;
} catch (error) {
console.error('表格渲染失败:', error);
yPos += 50;
}
} else {
// 无数据时显示提示
pdf.setFontSize(12);
pdf.text('暂无数据', margin, yPos);
yPos += 10;
}
// 保存 PDF
pdf.save('企业报告.pdf');
return false; // 阻止模态框自动关闭
};
四、常见问题:表头乱码解决方案
表头乱码的核心原因是:
- jspdf-autotable 默认表头字体样式为
bold
(粗体),但导入的simhei
字体仅包含normal
(常规)字重,导致字体匹配失败。
解决方法:
在 headStyles
中显式设置 fontStyle: 'normal'
,覆盖默认粗体样式,确保表头使用可用的常规字体:
ts
headStyles: {
font: 'simhei',
fontStyle: 'normal' // 关键配置:使用正常字重
}
通过以上步骤,可实现 PDF 导出功能,支持中文显示和表格内可点击链接,同时解决 TypeScript 类型报错和字体适配问题。