react+html2canvas+jspdf将页面导出pdf

news2025/5/21 4:37:51

主要使用html2canvas+jspdf
1.将前端页面导出为pdf
2.处理导出后图表的截断问题

export default function AIReport() {
  const handleExport = async () => {
    try {
      // 需要导出的内容id
      const element = document.querySelector('#AI-REPORT-CONTAINER');
      if (!element) {
        message.error('未找到要导出的内容');
        return;
      }

      message.loading({ content: '正在导出PDF...', key: 'export' });

      // 使用html2canvas生成整个报告的画布
      const canvas = await html2canvas(element as HTMLElement, {
        scale: 2, // 提高清晰度
        useCORS: true,
        allowTaint: true,
        backgroundColor: '#ffffff',
        logging: false,
      });

      // 创建PDF对象
      const pdf = new jsPDF({
        orientation: 'p', // 纵向
        unit: 'pt', // 使用点作为单位
        format: 'a4', // A4纸张
      });

      // 获取A4页面尺寸
      const a4Width = pdf.internal.pageSize.getWidth();
      const a4Height = pdf.internal.pageSize.getHeight();

      // 计算等比例下A4高度对应的canvas高度
      const a4HeightInCanvas = Math.floor((canvas.width / a4Width) * a4Height);

      // 获取canvas的总高度
      let leftHeight = canvas.height;

      // PDF页面偏移量
      let position = 0;

      // 创建临时canvas用于分页
      const tempCanvas = document.createElement('canvas');
      const ctx = tempCanvas.getContext('2d');

      // 递归处理每一页
      const processNextPage = () => {
        if (leftHeight <= 0) {
          // 完成所有页面处理,保存并下载PDF
          const pdfOutput = pdf.output('blob');
          const url = URL.createObjectURL(pdfOutput);
          const link = document.createElement('a');
          link.href = url;
          link.download = `AI基金报告_${data?.title || '未命名'}_${
            data?.date || ''
          }.pdf`;
          document.body.appendChild(link);
          link.click();
          document.body.removeChild(link);
          URL.revokeObjectURL(url);

          message.success({ content: 'PDF导出成功', key: 'export' });
          return;
        }

        // 计算当前页的高度
        let currentPageHeight;

        if (leftHeight > a4HeightInCanvas) {
          // 需要寻找合适的分页点
          let cutPoint = position + a4HeightInCanvas;
          let isFound = false;

          // 向上搜索连续的空白行作为分页点
          let checkCount = 0;
          for (let y = position + a4HeightInCanvas; y >= position; y--) {
            let isBlankLine = true;

            // 检查这一行的像素是否全为白色
            for (let x = 0; x < canvas.width; x += 10) {
              // 每10个像素采样一次提高性能
              const pixelData = canvas
                ?.getContext('2d')
                ?.getImageData(x, y, 1, 1).data;

              // 检查像素是否接近白色(允许一些误差)
              if (
                pixelData?.[0] < 250 ||
                pixelData?.[1] < 250 ||
                pixelData?.[2] < 250
              ) {
                isBlankLine = false;
                break;
              }
            }

            if (isBlankLine) {
              checkCount++;
              // 找到连续10行空白,确定为分页点
              if (checkCount >= 10) {
                cutPoint = y;
                isFound = true;
                break;
              }
            } else {
              checkCount = 0;
            }
          }

          // 如果没找到合适的分页点,就使用默认值
          currentPageHeight = isFound
            ? Math.round(cutPoint - position)
            : Math.min(leftHeight, a4HeightInCanvas);

          // 确保高度不为0
          if (currentPageHeight <= 0) {
            currentPageHeight = a4HeightInCanvas;
          }
        } else {
          // 最后一页,直接使用剩余高度
          currentPageHeight = leftHeight;
        }

        // 设置临时canvas的尺寸
        tempCanvas.width = canvas.width;
        tempCanvas.height = currentPageHeight;

        // 将原canvas对应部分绘制到临时canvas
        ctx?.drawImage(
          canvas,
          0,
          position,
          canvas.width,
          currentPageHeight,
          0,
          0,
          canvas.width,
          currentPageHeight,
        );

        // 从第二页开始添加新页面
        if (position > 0) {
          pdf.addPage();
        }

        // 将当前页添加到PDF
        pdf.addImage(
          tempCanvas.toDataURL('image/jpeg', 1.0),
          'JPEG',
          0,
          0,
          a4Width,
          (a4Width / tempCanvas.width) * currentPageHeight,
        );

        // 更新剩余高度和位置
        leftHeight -= currentPageHeight;
        position += currentPageHeight;

        // 处理下一页
        setTimeout(processNextPage, 100);
      };

      // 开始处理页面
      processNextPage();
    } catch {
      message.error({ content: '导出PDF失败,请稍后重试', key: 'export' });
    }
  };

  return (
    <Spin spinning={loading} wrapperClassName={styles.spinWrapper}>
      <div className={styles.exportBtn}>
        <Button type="primary" onClick={handleExport}>
          导出PDF
        </Button>
      </div>
      <div className={styles.container} id="AI-REPORT-CONTAINER">
       	这里为需要导出的页面内容,table,echart等
      </div>
    </Spin>
  );
}

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

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

相关文章

LWIP的Socket接口

Socket接口简介 类似于文件操作的一种网络连接接口&#xff0c;通常将其称之为“套接字”。lwIP的Socket接口兼容BSD Socket接口&#xff0c;但只实现完整Socket的部分功能 netconn是对RAW的封装 Socket是对netconn的封装 SOCKET结构体 struct sockaddr { u8_t sa_len; /* 长…

Better Faster Large Language Models via Multi-token Prediction 原理

目录 模型结构&#xff1a; Memory-efficient implementation&#xff1a; 实验&#xff1a; 1. 在大规模模型上效果显著&#xff1a; 2. 在不同类型任务上的效果&#xff1a; 为什么MLP对效果有提升的几点猜测&#xff1a; 1. 并非所有token对生成质量的影响相同 2. 关…

Spring的Validation,这是一套基于注解的权限校验框架

为了保证数据的正确性、完整性&#xff0c;作为一名后端开发工程师&#xff0c;不能仅仅依靠前端来校验数据&#xff0c;还需要对接口请求的参数进行后端的校验。 controller 全局异常处理器 在项目中添加一个全局异常处理器&#xff0c;处理校验异常 RestControllerAdvice p…

MySQL - 如何突破单库性能瓶颈

数据库服务器硬件优化 我们来看看对数据库所在的服务器是如何进行优化的&#xff0c;服务器是数据库的宿主&#xff0c;其性能直接影响了数据库的性能&#xff0c;所以服务器的优化也是数据库优化的第一步。 数据库服务器通常是从 CPU、内存、磁盘三个角度进行硬件优化的&…

apisix透传客户端真实IP(real-ip插件)

文章目录 apisix透传客户端真实IP需求和背景apisix real-ip插件为什么需要 trusted_addresses&#xff1f;安全架构的最佳实践 示例场景apisix界面配置 apisix透传客户端真实IP 需求和背景 当 APISIX 前端有其他反向代理&#xff08;如 Nginx、HAProxy、云厂商的 LB&#xff…

Oracle 数据库的默认隔离级别

Oracle 数据库的默认隔离级别 默认隔离级别&#xff1a;READ COMMITTED Oracle 默认使用 读已提交(READ COMMITTED) 隔离级别&#xff0c;这是大多数OLTP(在线事务处理)系统的标准选择。 官方文档 https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/da…

代码随想录算法训练营第六十四天| 图论9—卡码网47. 参加科学大会,94. 城市间货物运输 I

每日被新算法方式轰炸的一天&#xff0c;今天是dijkstra&#xff08;堆优化版&#xff09;以及Bellman_ford &#xff0c;尝试理解中&#xff0c;属于是只能照着代码大概说一下在干嘛。 47. 参加科学大会 https://kamacoder.com/problempage.php?pid1047 dijkstra&#xff08…

开启健康生活的多元养生之道

健康养生是一门值得终身学习的学问&#xff0c;在追求健康的道路上&#xff0c;除了常见方法&#xff0c;还有许多容易被忽视却同样重要的角度。掌握这些多元养生之道&#xff0c;能让我们的生活更健康、更有品质。​ 室内环境的健康不容忽视。定期清洁空调滤网&#xff0c;避…

【Vite】前端开发服务器的配置

定义一些开发服务器的行为和代理规则 服务器的基本配置 server: {host: true, // 监听所有网络地址port: 8081, // 使用8081端口open: true, // 启动时自动打开浏览器cors: true // 启用CORS跨域支持 } 代理配置 proxy: {/api: {target: https://…

Spring Security与Spring Boot集成原理

Spring Security依赖的是过滤器机制&#xff0c;首先是web容器例如tomcat作为独立的产品&#xff0c;本身有自己的一套过滤器机制用来处理请求&#xff0c;那么如何将tomcat接收到的请求转入到Spring Security的处理逻辑呢&#xff1f;spring充分采用了tomcat的拓展机制提供了t…

VScode各文件转化为PDF的方法

文章目录 代码.py文件.ipynb文本和代码夹杂的文件方法 1:使用 VS Code 插件(推荐)步骤 1:安装必要插件步骤 2:安装 `nbconvert`步骤 3:间接导出(HTML → PDF)本文遇见了系列错误:解决方案:问题原因步骤 1:降级 Jinja2 至兼容版本步骤 2:确保 nbconvert 版本兼容替代…

Vue3学习(组合式API——Watch侦听器、watchEffect()详解)

目录 一、Watch侦听器。 &#xff08;1&#xff09;侦听单个数据。 &#xff08;2&#xff09;侦听多个数据。&#xff08;数组写法&#xff1f;&#xff01;&#xff09; &#xff08;3&#xff09;immediate参数。(立即执行回调) &#xff08;3&#xff09;deep参数。(深层监…

【node.js】安装与配置

个人主页&#xff1a;Guiat 归属专栏&#xff1a;node.js 文章目录 1. Node.js简介1.1 Node.js的特点1.2 Node.js架构 2. Node.js安装2.1 下载和安装方法2.1.1 Windows安装2.1.2 macOS安装2.1.3 Linux安装 2.2 使用NVM安装和管理Node.js版本2.2.1 安装NVM2.2.2 使用NVM管理Node…

《AI大模型应知应会100篇》第62篇:TypeChat——类型安全的大模型编程框架

第62篇&#xff1a;TypeChat——类型安全的大模型编程框架 摘要 在构建 AI 应用时&#xff0c;一个常见的痛点是大语言模型&#xff08;LLM&#xff09;输出的不确定性与格式不一致问题。开发者往往需要手动解析、校验和处理模型返回的内容&#xff0c;这不仅增加了开发成本&a…

EdgeShard:通过协作边缘计算实现高效的 LLM 推理

(2024-05-23) EdgeShard: Efficient LLM Inference via Collaborative Edge Computing (EdgeShard:通过协作边缘计算实现高效的 LLM 推理) 作者: Mingjin Zhang; Jiannong Cao; Xiaoming Shen; Zeyang Cui;期刊: (发表日期: 2024-05-23)期刊分区:本地链接: Zhang 等 - 2024 …

火山 RTC 引擎9 ----集成 appkey

一、集成 appkey 1、网易RTC 初始化过程 1&#xff09;、添加头文件 实现互动直播 - 互动直播 2.0网易云信互动直播产品的基本功能包括音视频通话和连麦直播&#xff0c;当您成功初始化 SDK 之后&#xff0c;您可以简单体验本产品的基本业务流程&#xff0c;例如主播加入房间…

Adminer:一个基于Web的轻量级数据库管理工具

Adminer 是一个由单个 PHP 文件实现的免费数据库管理工具&#xff0c;支持 MySQL、MariaDB、PostgreSQL、CockroachDB、SQLite、SQL Server、Oracle、Elasticsearch、SimpleDB、MongoDB、Firebird、Clickhouse 等数据库。 Adminer 支持的主要功能如下&#xff1a; 连接数据库服…

RK3568下QT实现按钮切换tabWidget

运行效果: 在 Qt 应用程序开发过程中,TabWidget 是一种非常实用的 UI 组件,它能够以选项卡的形式展示多个页面内容,帮助我们有效组织和管理复杂的界面布局。而在实际使用时,常常会有通过按钮点击来切换 TabWidget 页面的需求,本文将通过一个完整的示例,详细介绍如何在 Q…

2025 OceanBase 开发者大会全议程指南

5 月 17 日&#xff0c;第三届 OceanBase 开发者大会将在广州举办。 我们邀请数据库领军者与AI实践先锋&#xff0c;与开发者一起探讨数据库与 AI 协同创新的技术趋势&#xff0c;面对面交流 OceanBase 在 TP、AP、KV 及 AI 能力上的最新进展&#xff0c;深度体验“打破技术栈…

day017-磁盘管理-实战

文章目录 1. 硬盘命名规则2. 添加硬盘2.1 查看硬盘名称 3. 硬盘分区3.1 分区命名规则&#xff1a;mbr分区表格式3.2 创建分区&#xff1a;fdisk3.2.1 fdisk -l&#xff1a;查看硬盘及分区信息3.2.2 fdisk /dev/sdc :为该硬盘分区3.2.3 创建扩展分区和逻辑分区3.2.4 保存设置并退…