基于 pdfjs-dist实现PDF 转 Canvas 增加水印批量打印实战

0 阅读12分钟

一、前言

在前端开发中,批量 PDF 打印场景(如工单打印、文档批量导出打印、图纸批量打印)十分常见,但实际开发中往往会遇到一系列痛点:频繁创建 Canvas 导致浏览器卡顿、大量 PDF 并行处理引发内存溢出、打印内容无法添加定制化水印/二维码、不同浏览器打印样式错乱、PDF 渲染模糊等。

本文基于指定版本 pdfjs-dist,以“实战落地”为核心,手把手实现 PDF 转 Canvas 批量打印方案,重点解决性能瓶颈与复用性问题,所有代码可直接拷贝集成到 Vue 项目,兼顾高性能、高定制性与可复用性,帮你快速搞定批量 PDF 打印需求。

二、核心技术栈与依赖配置

本文方案聚焦“可复用、易集成”,选用成熟稳定的技术栈,所有依赖版本固定,避免版本冲突,具体配置如下:

2.1 依赖清单与安装命令

核心依赖为 pdfjs-dist(PDF 解析与渲染核心),辅助依赖用于时间格式化、二维码生成与 UI 提示,安装命令直接拷贝执行即可:

# 核心依赖:pdfjs-dist(指定版本2.2.228,稳定无兼容bug)
npm install "pdfjs-dist": "2.2.228" --save

# 辅助依赖(按需安装,项目已存在则无需重复)
# moment:时间格式化(水印时间生成)
# qrcode:生成二维码(打印内容标识)
# element-ui:加载提示与错误提示(Vue项目常用)
npm install moment@2.29.4 qrcode@1.5.3 element-ui@2.15.13 --save

2.2 依赖引入与基础配置

在 Vue 组件或入口文件中引入依赖,重点配置 pdfjs-dist 的 worker 路径与字体路径,避免中文渲染乱码,代码直接拷贝可用:

// 1. pdfjsLib 核心依赖引入(适配npm安装的2.2.228版本)
import pdfjsLib from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";

// 关键配置:设置worker路径,避免渲染报错(npm安装后默认路径)
pdfjsLib.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.entry");

// 2. moment.js 引入与配置(中文时间格式)
import moment from "moment";
import "moment/locale/zh-cn";
moment.locale("zh-cn"); // 时间格式化默认中文(如:2026-02-12 15:30:00)

// 3. 二维码生成依赖引入与方法封装
import QRCode from "qrcode";

// 二维码生成方法(与打印逻辑联动,直接拷贝无需修改)
async function generateQrcode(text) {
  return new Promise((resolve, reject) => {
    QRCode.toDataURL(
      text,
      {
        width: 50,
        margin: 1,
        color: { dark: "#000000", light: "#ffffff" },
      },
      (err, url) => {
        if (err) reject(err);
        resolve(url);
      }
    );
  });
}

// 4. Element UI 引入(用于加载提示、错误提示,Vue项目适配)
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);

三、核心样式封装

批量打印的样式适配是关键,需兼顾“浏览器打印兼容性”与“定制化需求”(水印、二维码、页面分页),以下样式直接拷贝到 Vue 组件的 style 标签或项目样式文件,无需修改:

/* 批量打印专用样式 - 水印样式(工单号+产品名+时间) */
.watermark {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  opacity: 0.1;
  user-select: none;
  overflow: hidden;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}
.watermark-item {
  display: flex;
  align-items: center;
  justify-content: center;
  transform: rotate(-45deg);
  font-size: 16px;
  color: #000;
  white-space: nowrap;
  overflow: hidden;
}

/* 二维码+产品信息容器样式 */
.info-container {
  position: absolute;
  z-index: 1;
  background: transparent;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.info-container .qr-code {
  margin-bottom: 5px;
  padding: 2px;
  background: white;
  border-radius: 4px;
  border: 1px solid #ccc;
}
.info-container .product-info {
  text-align: center !important;
  font-size: 6px !important;
  line-height: 1.4;
  color: #333;
}
.info-container .product-info div {
  margin: 2px 0;
  font-size: 6px !important;
}

/* 打印适配核心样式(解决浏览器兼容性、空白页、分页问题) */
@media print {
  @page {
    margin: 0 !important;
    padding: 0 !important;
    size: auto;
  }
  html,
  body {
    margin: 0 !important;
    padding: 0 !important;
    height: 100%;
    width: 100%;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    overflow: visible !important;
  }
  .page-container {
    box-sizing: border-box;
    page-break-after: always;
    page-break-inside: avoid;
    min-height: 100vh;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 !important;
    padding: 0 !important;
    position: relative;
    background: white;
  }
  .page-container:last-child {
    page-break-after: avoid;
  }
  .page-content {
    width: 98%;
    height: 98%;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
  }
  .page-content img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    margin: 0 !important;
    padding: 0 !important;
    display: block;
  }
}

四、核心逻辑实战开发

本章节是全文核心,所有代码封装为 Vue 组件 methods 方法,直接拷贝到组件中即可使用,重点实现“PDF 转 Canvas”“批量分批处理”“性能优化”“定制化内容添加”四大核心功能,每一步都有详细注释,便于理解与修改。

4.1 数据格式定义(必看)

批量打印需传入选中的打印数据(selectTableData),需包含指定字段,用于水印、二维码、PDF 地址获取,格式如下(直接拷贝到 Vue 组件 data 中,替换为真实数据即可):

data() {
  return {
    // 选中的打印数据(批量打印的核心数据源)
    selectTableData: [
      {
        id: 1, // 可选:数据唯一标识,无实际业务作用
        productImageUrl: "https://example.com/product1.pdf", // 必选:产品PDF地址(真实可访问)
        workOrderNo: "WO2026021201", // 必选:工单号(用于水印、二维码内容)
        porUrl: "https://example.com/por1.pdf", // 可选:POR文件PDF地址,无则传null
        productName: "测试产品A", // 可选:产品名称(用于水印、页面标识)
        figureCode: "FIG-001" // 可选:图号(用于打印文档标题)
      },
      {
        id: 2,
        productImageUrl: "https://example.com/product2.pdf",
        workOrderNo: "WO2026021202",
        porUrl: null, // 无POR文件,传null即可
        productName: "测试产品B",
        figureCode: "FIG-002"
      }
    ]
  };
}

4.2 入口方法:批量打印触发

该方法为批量打印的总入口,负责初始化加载提示、Canvas 复用池、分批处理 PDF、调用打印执行方法,同时处理异常捕获与内存清理,直接拷贝可用:

/**
 * 批量打印处理 - 性能优化版(入口方法,直接调用此方法触发批量打印)
 * 核心功能:初始化配置、分批处理、异常捕获、内存清理
 */
async handleBatchPrint() {
  const { selectTableData } = this;
  // 校验:无选中数据则提示,直接返回
  if (!selectTableData || selectTableData.length === 0) {
    this.$message.warning("请选择需要打印的数据");
    return;
  }

  // 加载提示(Element UI),提升用户体验
  const loading = this.$loading({
    lock: true,
    text: "准备打印数据...",
    spinner: "el-icon-loading",
    background: "rgba(0, 0, 0, 0.7)",
  });

  // Canvas复用池 - 核心性能优化:避免频繁创建/销毁Canvas,降低性能损耗
  let canvasPool = {
    canvas: null, // 主Canvas(PDF渲染)
    context: null, // 主Canvas上下文
    tempCanvas: null, // 临时Canvas(图片增强)
    tempContext: null // 临时Canvas上下文
  };

  try {
    // 1. 初始化Canvas复用池(仅创建一次,后续复用)
    canvasPool.canvas = document.createElement("canvas");
    canvasPool.context = canvasPool.canvas.getContext("2d", {
      alpha: false, // 关闭透明通道,提升渲染性能
      willReadFrequently: true, // 允许频繁读取像素,适配高分辨率渲染
      desynchronized: true // 解除渲染同步,提升帧率
    });
    canvasPool.tempCanvas = document.createElement("canvas");
    canvasPool.tempContext = canvasPool.tempCanvas.getContext("2d", {
      alpha: false,
      willReadFrequently: true,
      desynchronized: true
    });

    // 2. 分批处理PDF(核心性能优化:降低内存峰值,避免卡顿)
    const BATCH_SIZE = 5; // 每批处理5个PDF,可根据项目调整(建议5-10个)
    const totalItems = selectTableData.length;
    const allPages = []; // 存储所有处理完成的打印页面

    // 分批循环处理
    for (let batchStart = 0; batchStart < totalItems; batchStart += BATCH_SIZE) {
      const batchEnd = Math.min(batchStart + BATCH_SIZE, totalItems);
      const batch = selectTableData.slice(batchStart, batchEnd);

      // 更新进度提示,让用户感知处理状态
      loading.text = `处理中... (${batchStart + 1}-${batchEnd}/${totalItems})`;

      // 并行处理当前批次的PDF,提升处理效率(不阻塞后续批次)
      await Promise.all(
        batch.map(async (item, batchIndex) => {
          const index = batchStart + batchIndex;
          const { productImageUrl, workOrderNo, porUrl, productName, figureCode } = item;

          // 跳过无效数据(避免影响整体流程)
          if (!productImageUrl || !workOrderNo) {
            console.warn(`跳过无效打印数据(第${index + 1}条):`, item);
            this.$message.warning(`跳过无效数据(第${index + 1}条):缺少工单号或PDF地址`);
            return;
          }

          try {
            // 生成二维码(工单号为二维码内容,用于打印标识)
            const qrCode = await generateQrcode(workOrderNo);

            // 处理产品PDF(第一页显示二维码,核心:PDF转Canvas)
            const productPages = await this.processPdfWithOptimization(
              productImageUrl,
              workOrderNo,
              productName,
              qrCode,
              canvasPool,
              true // 产品PDF第一页显示二维码
            );
            allPages.push(...productPages);

            // 处理POR PDF(若有,不显示二维码)
            if (porUrl) {
              const porPages = await this.processPdfWithOptimization(
                porUrl,
                workOrderNo,
                productName,
                null,
                canvasPool,
                false // POR PDF不显示二维码
              );
              allPages.push(...porPages);
            }

            // 批次最后一条数据处理完成后,触发垃圾回收(辅助释放内存)
            if (batchIndex === batch.length - 1 && window.gc) {
              window.gc();
            }
          } catch (error) {
            // 单PDF处理失败,不阻塞整体流程,仅提示错误
            console.error(`处理第 ${index + 1} 个PDF时出错:`, error);
            this.$message.error(`处理第 ${index + 1} 个PDF时出错: ${error.message}`);
          }
        })
      );
    }

    // 校验:无可用打印页面,抛出异常并提示
    if (allPages.length === 0) {
      throw new Error("没有可打印的页面,请检查PDF地址是否有效");
    }

    // 生成打印文档并执行打印
    loading.text = "生成打印文档...";
    await this.executePrint(allPages, selectTableData);

  } catch (error) {
    // 整体打印失败,提示用户并打印错误日志
    console.error("批量打印失败:", error);
    this.$message.error(`打印失败: ${error.message}`);
  } finally {
    // 核心内存优化:清理Canvas池,释放内存,避免内存泄漏
    if (canvasPool.canvas) {
      canvasPool.canvas.width = 0;
      canvasPool.canvas.height = 0;
      canvasPool.canvas = null;
    }
    if (canvasPool.tempCanvas) {
      canvasPool.tempCanvas.width = 0;
      canvasPool.tempCanvas.height = 0;
      canvasPool.tempCanvas = null;
    }
    canvasPool.context = null;
    canvasPool.tempContext = null;

    // 重置文档标题,关闭加载提示
    document.title = "批量PDF打印";
    loading.close();
  }
}

4.3 核心方法:PDF 转 Canvas 优化处理

该方法是“PDF 转 Canvas”的核心,负责加载 PDF、高分辨率渲染、图片增强、页面清理,复用 Canvas 池提升性能,直接拷贝可用:

/**
 * 优化的PDF处理方法 - 核心:PDF转Canvas,复用Canvas池提升性能
 * @param {string} pdfUrl - PDF文件URL(必填)
 * @param {string} workOrderNo - 工单号(用于水印/二维码)
 * @param {string} productName - 产品名称(用于水印/标识)
 * @param {string} qrCode - 二维码Base64(可选)
 * @param {Object} canvasPool - Canvas复用池(直接传入,无需处理)
 * @param {boolean} showQrOnFirst - 是否在第一页显示二维码
 * @returns {Promise<Array>} 处理后的页面容器数组(供打印使用)
 */
async processPdfWithOptimization(
  pdfUrl,
  workOrderNo,
  productName,
  qrCode,
  canvasPool,
  showQrOnFirst
) {
  const pages = [];

  try {
    // 加载PDF文档(适配pdfjs-dist,配置cMap避免中文乱码)
    const pdfDoc = await pdfjsLib.getDocument({
      url: pdfUrl,
      cMapUrl: 'pdfjs-dist/cmaps/', // npm安装后默认字体路径,无需修改
      cMapPacked: true,
      standardFontDataUrl: 'pdfjs-dist/standard_fonts/', // 标准字体路径
      enableXfa: true,
      disableFontFace: false,
      useSystemFonts: true,
      maxCanvasPixels: 33554432 // 限制Canvas最大像素,避免内存溢出
    }).promise;

    const numPages = pdfDoc.numPages; // PDF总页数

    // 循环处理PDF的每一页
    for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
      const pageNum = pageIndex + 1;
      const page = await pdfDoc.getPage(pageNum);

      // 复用Canvas池中的元素,动态调整Canvas尺寸(适配当前PDF页面)
      const { canvas, context, tempCanvas, tempContext } = canvasPool;
      const viewport = page.getViewport({ scale: 8.0 }); // 高分辨率渲染(保证打印清晰)
      canvas.width = viewport.width;
      canvas.height = viewport.height;
      tempCanvas.width = viewport.width;
      tempCanvas.height = viewport.height;

      // 设置渲染质量,启用图片增强(提升打印清晰度)
      context.imageSmoothingEnabled = true;
      context.imageSmoothingQuality = "high";
      context.fillStyle = "white";
      context.fillRect(0, 0, canvas.width, canvas.height);

      // 渲染PDF页面到Canvas(核心:启用WebGL加速,提升渲染速度)
      await page.render({
        canvasContext: context,
        viewport: viewport,
        intent: "print", // 打印模式渲染,提升打印质量
        renderInteractiveForms: true,
        background: "white", // 背景设为白色,避免打印透明
        enableWebGL: true, // WebGL加速渲染,核心性能优化
        disableRange: false,
        disableStream: false,
        disableAutoFetch: false,
      }).promise;

      // 应用图片增强滤镜(锐化、对比度调整),进一步提升打印质量
      tempContext.filter = "contrast(1.3) saturate(1.2) brightness(1.05)";
      tempContext.drawImage(canvas, 0, 0);

      // 转换为高质量PNG图片(质量1.0,无损打印)
      const imgDataUrl = tempCanvas.toDataURL("image/png", 1.0);
      const img = await this.createImageFromDataUrl(imgDataUrl);

      // 创建页面容器(添加水印、二维码、产品信息)
      const pageContainer = this.createPageContainer(
        img,
        workOrderNo,
        productName,
        qrCode,
        showQrOnFirst && pageNum === 1,
        viewport
      );

      pages.push(pageContainer);

      // 清理页面对象,释放内存(核心优化)
      page.cleanup();
    }

    // 清理PDF文档,释放内存
    pdfDoc.cleanup();
    pdfDoc.destroy();

  } catch (error) {
    console.error("PDF处理失败:", error);
    throw new Error(`PDF处理失败(${pdfUrl}): ${error.message}`);
  }

  return pages;
}

4.4 工具方法:辅助功能封装

以下方法为辅助功能,负责图片创建、页面容器创建、水印创建、信息容器创建,直接拷贝到 methods 中即可,无需修改:

/**
 * 工具方法:从DataURL创建图片元素(适配Canvas转图片渲染)
 * @param {string} dataUrl - 图片DataURL
 * @returns {Promise<HTMLImageElement>} 图片元素
 */
createImageFromDataUrl(dataUrl) {
  return new Promise((resolve, reject) => {
    const img = document.createElement("img");
    img.onload = () => resolve(img); // 图片加载完成回调
    img.onerror = () => reject(new Error("图片加载失败")); // 加载失败回调
    img.src = dataUrl;
  });
},

/**
 * 工具方法:创建页面容器(添加水印、二维码、产品信息)
 * @param {HTMLImageElement} img - 图片元素(PDF转Canvas后的图片)
 * @param {string} workOrderNo - 工单号
 * @param {string} productName - 产品名称
 * @param {string} qrCode - 二维码
 * @param {boolean} showQr - 是否显示二维码
 * @param {Object} viewport - 视口对象
 * @returns {HTMLElement} 页面容器(供打印使用)
 */
createPageContainer(img, workOrderNo, productName, qrCode, showQr, viewport) {
  // 页面容器(适配打印分页,每一页一个容器)
  const pageContainer = document.createElement("div");
  pageContainer.style.position = "relative";
  pageContainer.style.width = "100%";
  pageContainer.style.height = "100vh";
  pageContainer.style.margin = "0";
  pageContainer.style.padding = "0";
  pageContainer.style.pageBreakAfter = "always";

  // 添加水印(工单号+产品名+当前时间,避免打印内容混淆)
  const watermark = this.createWatermark(workOrderNo, productName);
  pageContainer.appendChild(watermark);

  // 添加PDF转换后的图片(核心打印内容)
  img.style.width = "98%";
  img.style.height = "98%";
  img.style.maxWidth = "98%";
  img.style.maxHeight = "98%";
  img.style.objectFit = "contain";
  pageContainer.appendChild(img);

  // 第一页添加二维码和产品信息(打印标识)
  if (showQr && qrCode) {
    const infoContainer = this.createInfoContainer(
      qrCode,
      workOrderNo,
      productName,
      viewport
    );
    pageContainer.appendChild(infoContainer);
  }

  return pageContainer;
},

/**
 * 工具方法:创建水印元素(工单号+产品名+时间)
 * @param {string} workOrderNo - 工单号
 * @param {string} productName - 产品名称
 * @returns {HTMLElement} 水印容器
 */
createWatermark(workOrderNo, productName) {
  const watermark = document.createElement("div");
  watermark.className = "watermark";
  // 水印内容:工单号+产品名+当前时间(精确到分秒,便于追溯)
  const watermarkText = `${workOrderNo} ${productName || ""} ${moment().format("YYYY-MM-DD HH:mm:ss")}`;

  // 创建4x4网格水印(16个元素,均匀分布在页面,避免遮挡核心内容)
  for (let i = 0; i < 16; i++) {
    const watermarkItem = document.createElement("div");
    watermarkItem.className = "watermark-item";
    watermarkItem.textContent = watermarkText;
    watermark.appendChild(watermarkItem);
  }

  return watermark;
},

/**
 * 工具方法:创建信息容器(二维码+产品信息)
 * @param {string} qrCode - 二维码Base64
 * @param {string} workOrderNo - 工单号
 * @param {string} productName - 产品名称
 * @param {Object} viewport - 视口对象
 * @returns {HTMLElement} 信息容器
 */
createInfoContainer(qrCode, workOrderNo, productName, viewport) {
  const qrSize = 50; // 二维码尺寸
  const qrPadding = 2; // 二维码内边距
  const scale = viewport.scale || 1;
  const safeMargin = 3 * scale; // 安全边距,避免内容被浏览器裁剪

  const infoContainer = document.createElement("div");
  infoContainer.className = "info-container";
  infoContainer.style.top = `${safeMargin}px`;
  infoContainer.style.left = `${safeMargin}px`;

  // 二维码容器(添加边框和背景,提升识别度)
  const qrContainer = document.createElement("div");
  qrContainer.className = "qr-code";
  qrContainer.style.padding = `${qrPadding}px`;
  qrContainer.style.background = "white";
  qrContainer.style.borderRadius = "4px";
  qrContainer.style.border = "1px solid #ccc";

  // 二维码图片
  const qrImage = document.createElement("img");
  qrImage.src = qrCode;
  qrImage.style.width = `${qrSize}px`;
  qrImage.style.height = `${qrSize}px`;
  qrImage.style.display = "block";

  qrContainer.appendChild(qrImage);
  infoContainer.appendChild(qrContainer);

  // 产品信息容器(显示工单号和产品名,便于识别)
  const productInfo = document.createElement("div");
  productInfo.className = "product-info";

  const workOrderNoDiv = document.createElement("div");
  workOrderNoDiv.textContent = `${workOrderNo}`;
  productInfo.appendChild(workOrderNoDiv);

  const productNameDiv = document.createElement("div");
  productNameDiv.textContent = `${productName || "-"}`;
  productInfo.appendChild(productNameDiv);

  infoContainer.appendChild(productInfo);

  return infoContainer;
}

4.5 执行打印方法:触发浏览器打印

该方法负责创建打印容器(iframe)、拼接打印内容、触发浏览器打印,打印完成后清理 iframe 释放内存,直接拷贝可用:

/**
 * 执行打印操作(核心方法,触发浏览器打印)
 * @param {Array} allPages - 所有页面容器(processPdfWithOptimization返回)
 * @param {Array} selectTableData - 选中的表格数据(用于设置打印标题)
 */
async executePrint(allPages, selectTableData) {
  // 创建隐藏iframe作为打印容器(避免污染主页面DOM,提升兼容性)
  const printFrame = document.createElement("iframe");
  printFrame.style.display = "none";
  document.body.appendChild(printFrame);

  // 拼接打印内容(包含所有页面+打印样式,确保打印格式正确)
  const printContent = `
<!DOCTYPE html>
    批量打印
        ${allPages
          .map((page) => {
            const pageContent = document.createElement("div");
            pageContent.className = "page-content";
            while (page.firstChild) {
              pageContent.appendChild(page.firstChild);
            }
            page.appendChild(pageContent);
            page.className = "page-container";
            return page.outerHTML;
          })
          .join("")}

  `;

  // 写入iframe并执行打印
  const frameDoc = printFrame.contentWindow.document;
  frameDoc.open();
  frameDoc.write(printContent);
  frameDoc.close();

  // 等待内容加载完成(避免打印空白,适配不同浏览器加载速度)
  await new Promise((resolve) => setTimeout(resolve, 1000));

  // 设置打印文档标题(工单号+产品名,便于识别打印任务)
  const productName = selectTableData[0].productName || "批量打印";
  const figureCode = selectTableData[0].figureCode || "";
  document.title = `${figureCode}_${productName}`;

  // 触发浏览器打印
  printFrame.contentWindow.print();

  // 打印完成后,移除iframe,释放内存(核心优化)
  printFrame.contentWindow.onafterprint = () => {
    document.body.removeChild(printFrame);
  };
}

五、调用方式

所有代码拷贝完成后,仅需添加一个触发按钮,调用 handleBatchPrint 方法即可实现批量打印,直接拷贝到 Vue 模板中:

<!-- Vue模板:批量打印触发按钮 -->
<!-- 批量打印按钮(Element UI样式,可替换为自身项目按钮) -->
<el-button type="primary" icon="el-icon-printer" @click="handleBatchPrint">
  批量打印
</el-button>

六、常见问题排查

集成过程中可能遇到以下问题,整理了对应的排查方法,快速解决集成难题:

  • 问题 1:PDF 加载失败/中文渲染乱码 → 排查:1. PDF URL 是否真实可访问、跨域是否配置 CORS;2. pdfjs-dist 的 cMap 路径是否正确(本文配置无需修改);3. 确认 pdfjs-dist 版本为 2.2.228;

  • 问题 2:打印空白页 → 排查:1. iframe 内容是否写入成功(可临时注释 display: none 查看 iframe 内容);2. setTimeout 等待时间是否足够(可调整为 1500ms);3. Canvas 渲染是否正常;

  • 问题 3:二维码不显示 → 排查:1. generateQrcode 方法是否返回正确的 Base64 格式;2. showQrOnFirst 参数是否为 true;3. 二维码容器样式是否被遮挡;

  • 问题 4:浏览器卡顿/内存溢出 → 排查:1. 减小批次大小(如改为 3 个/批);2. 检查是否有未清理的 Canvas/iframe;3. 升级浏览器到最新版本(推荐 Chrome);

  • 问题 5:打印样式错乱 → 排查:1. 打印样式是否完整拷贝;2. 浏览器打印设置中是否勾选“背景图形”(部分浏览器默认不勾选,导致水印不显示);3. 确认@media print 样式未被覆盖。

原创不易,承蒙厚爱,感谢每一份认可与赞赏✨

3.jpg