vue3导出一个将tab页签总和,包含表单和表格的复杂页面

vue3中一个带tab页的弹框,我需要将三个tab页导出为一个pdf,第一三个tab页是表单,第二个tab页是表格,表格需要设置超出页面高度自动换行
这是封装的ts方法
export const downloadPDFBook = async (
domName: string,
title: string,
flag: boolean,
onProgress?: (progress: { current: number; total: number }) => void
): Promise => {
// 滚动到页面顶部,确保DOM内容完整可见
window.scrollTo(0, 0);

// 获取根DOM元素,添加TS类型断言
const rootEl = document.getElementById(domName);
if (!rootEl) {
console.error([PDF] Element #${domName} not found);
return Promise.reject(new Error(Element #${domName} not found));
}

// 提取封面和内容卡片(添加TS类型标注)
const cover = rootEl.querySelector('.cover');
const cardElements = Array.from(rootEl.querySelectorAll('.content'));

// === 计算总任务数(每个 block 算 1 个任务)===
const totalBlocks = (cover ? 1 : 0) + cardElements.length;
let completedBlocks = 0;

// 进度更新辅助函数(优化类型判断)
const updateProgress = () => {
completedBlocks++;
if (typeof onProgress === 'function') {
// 严格类型的进度对象
onProgress({ current: completedBlocks, total: totalBlocks });
}
};

// === 步骤3:创建 PDF 并逐块渲染 ===
// 明确 jsPDF 实例类型
const pdf: JsPDFType = new jsPDF('p', 'mm', 'a4');
let firstPage = true;

/**

  • 单个区块渲染函数(提取为内部辅助函数,添加严格类型)
  • @param element - 要渲染的DOM元素
  • @param pdf - jsPDF 实例
  • @param isFirst - 是否为第一页
  • @returns Promise - 渲染是否成功
    */
    const renderBlock = async (
    element: HTMLElement | null,
    pdf: JsPDFType,
    isFirst: boolean
    ): Promise => {
    // 边界判断:元素不存在或无高度则直接更新进度并返回
    if (!element || !element.offsetHeight) {
    updateProgress();
    return false;
    }
// 确保当前区块可见
element.style.display = '';
element.style.visibility = 'visible';

try {
  // 渲染 DOM 为 canvas(明确 html2canvas 配置类型)
  const canvas = await html2canvas(element, {
    scale: 2,
    useCORS: true,
    allowTaint: true,
    backgroundColor: '#ffffff',
    logging: false,
  });

  // 转换 canvas 为图片数据
  const imgData = canvas.toDataURL('image/jpeg', 0.92);
  const pdfWidth = 200; // mm (A4 宽 210 - 10mm 边距)
  const pageHeightMM = 287; // mm (A4 高 297 - 10mm 边距)

  // 计算 Canvas 在 PDF 中的缩放后高度(单位:mm)
  const imgHeightMM = Math.floor((pdfWidth * canvas.height) / canvas.width);

  // 情况1:高度 ≤ 一页,直接添加
  if (imgHeightMM <= pageHeightMM) {
    if (!isFirst) pdf.addPage();
    pdf.addImage(imgData, 'JPEG', 5, 5, pdfWidth, imgHeightMM);
    updateProgress();
    return true;
  }

  // 情况2:高度 > 一页,需要分页处理
  const ctx = canvas.getContext('2d');
  if (!ctx) {
    updateProgress();
    return false;
  }

  const canvasWidth = canvas.width;
  const canvasHeight = canvas.height;
  const pageHeightPx = Math.floor((pageHeightMM * canvasWidth) / pdfWidth);
  let renderedHeight = 0;

  // 遍历分页
  while (renderedHeight < canvasHeight) {
    // 计算当前页的高度
    let currentPageHeight = Math.min(pageHeightPx, canvasHeight - renderedHeight);
    const pageBottom = renderedHeight + currentPageHeight;

    // 检查是否有表格行被截断
    const tableRows = element.querySelectorAll('tr');
    const elementRect = element.getBoundingClientRect();

    for (const row of tableRows) {
      const rowRect = row.getBoundingClientRect();
      // 计算行在canvas中的位置(考虑scale=2)
      const rowTop = Math.floor((rowRect.top - elementRect.top) * 2);
      const rowBottom = rowTop + Math.floor(rowRect.height * 2);

      // 如果行跨越当前页底部,调整页高
      if (rowTop < pageBottom && rowBottom > pageBottom) {
        currentPageHeight = rowTop - renderedHeight;
        break;
      }
    }

    // 创建临时画布用于截取当前页
    const pageCanvas = document.createElement('canvas');
    pageCanvas.width = canvasWidth;
    pageCanvas.height = currentPageHeight;

    // 截取当前页内容
    const imageData = ctx.getImageData(0, renderedHeight, canvasWidth, currentPageHeight);
    const pageCtx = pageCanvas.getContext('2d');
    if (pageCtx) {
      pageCtx.putImageData(imageData, 0, 0);

      // 计算当前页在PDF中的高度
      const currentPageHeightMM = Math.floor((pdfWidth * currentPageHeight) / canvasWidth);

      // 添加到PDF
      if (!isFirst || renderedHeight > 0) pdf.addPage();
      pdf.addImage(pageCanvas.toDataURL('image/jpeg', 0.92), 'JPEG', 5, 5, pdfWidth, currentPageHeightMM);
    }

    // 更新已渲染高度
    renderedHeight += currentPageHeight;
    isFirst = false;

    // 释放临时画布
    pageCanvas.remove();
  }

  updateProgress();
  return true;

} catch (err) {
  updateProgress();
  return false;
} finally {
}

};

try {
// 初始化进度回调
if (typeof onProgress === 'function' && totalBlocks > 0) {
onProgress({ current: 0, total: totalBlocks });
}

// 1. 渲染封面(如果存在)
if (cover) {
  await renderBlock(cover, pdf, firstPage);
  firstPage = false;
}

// 2. 批量渲染所有内容卡片
for (const card of cardElements) {
  const rendered = await renderBlock(card, pdf, firstPage);
  if (rendered) firstPage = false;
}

// === 输出 PDF(预览/下载)===
if (flag) {
  // 预览:生成 blob 链接并新开窗口打开
  const blobUrl = pdf.output('bloburl');
  window.open(blobUrl, '_blank');
} else {
  // 下载:直接保存为 PDF 文件
  pdf.save(`${title}.pdf`);
}

} finally {
// 最终进度回调(标记完成)
if (typeof onProgress === 'function' && totalBlocks > 0) {
onProgress({ current: totalBlocks, total: totalBlocks });
}
}
};

///////////////////////////////////////////////////
这是调用的地方,为三个tab项外面添加一个div,类名设置为print-container
另外额外加了只在打印时显示的文字,类名为el_upload_title,查看时隐藏,打印时显示,打印完成后恢复隐藏
const printDetail = async (row: any) => {
dialog.loading = true;
// 1. 临时调整:打印前强制显示所有tab页(默认仅激活的tab显示,其他隐藏)
const printContainer = document.querySelector('.print-container');
if (printContainer) {
// 取消tab的隐藏样式,确保所有tab内容都能被打印
const tabPanes = printContainer.querySelectorAll('.el-tab-pane');
console.log('tabPanes', tabPanes);
tabPanes.forEach((pane) => {
(pane as HTMLElement).style.display = 'block';
// (pane as HTMLElement).style.visibility = "visible";
});
const titles = printContainer.querySelectorAll('.el_upload_title');
titles.forEach((title) => {
(title as HTMLElement).style.display = 'block';
});
}

try {
await downloadPDFBook('print-container', '应急演练记录详情', true, ({ current, total }) => {
// 进度回调
const percent = Math.min(100, Math.round((current / total) * 100));
const msg = 正在生成 PDF... ${current}/${total} (${percent}%);
if (dialog.loading) {
// this.loadingInstance.setText(msg);
dialog.loadingText = msg;
}
});
} finally {
// 3. 打印后恢复:还原tab的隐藏样式(仅保留激活的tab显示)
if (printContainer) {
const titles = printContainer.querySelectorAll('.el_upload_title');
titles.forEach((title) => {
(title as HTMLElement).style.display = 'none';
});
const tabPanes = printContainer.querySelectorAll('.el-tab-pane');
tabPanes.forEach((pane) => {
const paneName = (pane as HTMLElement).getAttribute('name');
if (paneName != activeName.value) {
console.log('paneName', paneName);
(pane as HTMLElement).style.display = 'none';
// (pane as HTMLElement).style.visibility = "hidden";
}
});
(document.querySelector(.print-container #pane-${activeName.value}) as HTMLElement).style.display = 'block';

}
dialog.loading = false;

}
};

posted @ 2026-04-08 11:41  小王不要404  阅读(0)  评论(0)    收藏  举报