TS 导出 PDF:解决表头乱码,实现表格内添加可点击链接

前言

在前端项目中,使用 TypeScript 结合 jsPDF 和 jspdf-autotable 导出含可点击链接的表格 PDF 时,需解决字体适配、类型报错和链接配置等问题。本文记录具体实现步骤和常见问题解决方案。

一、字体文件转换:TTF 转 TS 格式

为确保 PDF 正常显示中文,需将本地字体文件转换为项目可导入的格式:

  1. 准备字体文件

    下载 simhei.ttf(黑体)字体文件,放入项目 src/assets/fonts 目录。

  2. 创建转换脚本

    src 目录下新建 convert.js,用于将 TTF 字体转换为 Base64 编码的 TS 文件:

    js 复制代码
    import 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. 文件是否损坏');
    }
  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.2
  • jspdf-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 类型报错和字体适配问题。

相关推荐
安琪吖4 分钟前
微前端:qiankun框架在开发中遇到的问题
前端·vue·element-ui
不爱说话郭德纲7 分钟前
🔥产品:"这功能很常见,不用原型,参考竞品就行!" 你会怎么做
前端·产品经理·产品
wordbaby15 分钟前
React 异步请求数据处理优化经验总结
前端·react.js
拉不动的猪17 分钟前
回顾 pinia VS vuex
前端·vue.js·面试
Warren9822 分钟前
Java异常讲解
java·开发语言·前端·javascript·vue.js·ecmascript·es6
超级土豆粉42 分钟前
Taro Hooks 完整分类详解
前端·javascript·react.js·taro
iphone10844 分钟前
从零开始学网页开发:HTML、CSS和JavaScript的基础知识
前端·javascript·css·html·网页开发·网页
2503_928411561 小时前
7.31 CSS-2D效果
前端·css·css3
辰九九1 小时前
Vue响应式原理
前端·javascript·vue.js
中等生1 小时前
为什么现在的前端项目都要'启动'?新手必懂的开发环境变迁
前端·javascript·react.js