Ver Fonte

fix(seal): 修复渲染竞态及超大画布导出PDF失败

yusheng há 2 semanas atrás
pai
commit
b5b891dee2
1 ficheiros alterados com 87 adições e 12 exclusões
  1. 87 12
      src/components/addDoc/seal.vue

+ 87 - 12
src/components/addDoc/seal.vue

@@ -119,6 +119,12 @@
       };
     },
     created() {
+      // 竞态控制字段(非响应式,挂在实例上避免响应式开销)
+      this._renderSeq = 0; // 渲染序号,用于竞态控制
+      this._lastRenderPromise = null; // 最近一次渲染完成的 Promise
+      this._renderResolve = null; // 最近一次渲染的 resolve 回调
+      this._renderReject = null; // 最近一次渲染的 reject 回调
+
       // 获取公用章列表
       getSealPage({
         size: 999,
@@ -811,9 +817,17 @@
 
       // 渲染Canvas核心函数
       async renderToCanvas(type) {
+        // 竞态保护:记录本次渲染序号与 Promise,供 exportToPDF 等待最新一次渲染完成
+        const mySeq = ++this._renderSeq;
+        this._lastRenderPromise = new Promise((resolve, reject) => {
+          this._renderResolve = resolve;
+          this._renderReject = reject;
+        });
+
         const targetElement = document.getElementById('captureTarget');
         if (!targetElement) {
           console.warn('未找到目标元素');
+          this._renderReject && this._renderReject(new Error('未找到目标元素'));
           return;
         }
 
@@ -837,6 +851,13 @@
         try {
           // 使用 html2canvas 进行转换,scale: 2 提高清晰度
           const scale = 2;
+          // captureTarget 是隐藏元素(left: -100000px),scrollWidth/Height 偶现为 0,
+          // 用 getBoundingClientRect 兜底获取真实宽高,避免渲染出 0 尺寸 canvas
+          const rect = targetElement.getBoundingClientRect();
+          const targetWidth =
+            targetElement.scrollWidth || Math.ceil(rect.width) || 1;
+          const targetHeight =
+            targetElement.scrollHeight || Math.ceil(rect.height) || 1;
           const canvas = await html2canvas(targetElement, {
             scale: scale, // 2倍分辨率,提高清晰度
             backgroundColor: '#ffffff',
@@ -844,8 +865,8 @@
             logging: false,
             allowTaint: false,
             imageTimeout: 5000,
-            windowWidth: targetElement.scrollWidth,
-            windowHeight: targetElement.scrollHeight
+            windowWidth: targetWidth,
+            windowHeight: targetHeight
           });
 
           // 移除加载提示
@@ -875,6 +896,11 @@
             this.redrawCanvas();
           }
 
+          // 仅当这是最新一次渲染时才 resolve,避免旧渲染覆盖新状态
+          if (mySeq === this._renderSeq && this._renderResolve) {
+            this._renderResolve();
+          }
+
           // 显示成功提示
           // this.showMessage('✅ 渲染成功!可下载图片', 'success');
         } catch (error) {
@@ -882,34 +908,83 @@
           const temp = document.getElementById('tempLoadingMsg');
           if (temp) temp.remove();
           this.canvasGenerated = false;
+          // 仅当这是最新一次渲染时才 reject
+          if (mySeq === this._renderSeq && this._renderReject) {
+            this._renderReject(error);
+          }
           // this.showMessage('⚠️ 渲染失败,请检查HTML结构或外部资源', 'error');
         }
       },
 
       // 导出为 PDF(使用 jsPDF)
-      exportToPDF() {
+      async exportToPDF() {
+        // 竞态保护:如果正在渲染,等待最新一次渲染完成(最多等 15s),
+        // 避免 html2canvas 尚未绘制完成就导出导致偶现失败
+        if (this._lastRenderPromise) {
+          try {
+            await Promise.race([
+              this._lastRenderPromise,
+              new Promise((_, reject) =>
+                setTimeout(() => reject(new Error('等待渲染完成超时')), 15000)
+              )
+            ]);
+          } catch (err) {
+            console.error('等待渲染完成失败,跳过导出:', err);
+            this.$message && this.$message.error('渲染尚未完成,请稍后重试');
+            return;
+          }
+        }
+
         const outputCanvas = document.getElementById('outputCanvas');
         if (
           !this.canvasGenerated ||
           !outputCanvas ||
           outputCanvas.width === 0
         ) {
-          // this.showMessage(
-          //   '请先点击"渲染至Canvas"生成图像后再导出PDF',
-          //   'error'
-          // );
+          this.$message && this.$message.error('内容尚未渲染,无法导出PDF');
           return;
         }
 
         try {
           // 使用 Canvas 的 CSS 尺寸(显示尺寸)
-          const cssWidth =
+          let cssWidth =
             parseFloat(outputCanvas.style.width) || outputCanvas.width;
-          const cssHeight =
+          let cssHeight =
             parseFloat(outputCanvas.style.height) || outputCanvas.height;
 
-          // 获取高清图片数据,使用最高质量
-          const imgData = outputCanvas.toDataURL('image/png', 1.0);
+          // 防止 css 尺寸异常(比如渲染竞态时 style 未设置),兜底用 canvas 实际像素
+          if (!cssWidth || !cssHeight || isNaN(cssWidth) || isNaN(cssHeight)) {
+            cssWidth = outputCanvas.width;
+            cssHeight = outputCanvas.height;
+          }
+
+          // 计算需要的降采样比例:
+          // jsPDF 最大页面尺寸约 5080mm,按 96dpi 换算约 19200px,留 10% 余量用 17000px
+          const MAX_SIDE = 17000;
+          let scale = 1;
+          const maxSide = Math.max(outputCanvas.width, outputCanvas.height);
+          if (maxSide > MAX_SIDE) {
+            scale = MAX_SIDE / maxSide;
+          }
+
+          // 降采样后从临时 canvas 取图,避免超大图 toDataURL OOM / 超出 jsPDF 限制
+          let imgData;
+          if (scale < 1) {
+            const tempCanvas = document.createElement('canvas');
+            tempCanvas.width = Math.round(outputCanvas.width * scale);
+            tempCanvas.height = Math.round(outputCanvas.height * scale);
+            const tempCtx = tempCanvas.getContext('2d');
+            tempCtx.drawImage(
+              outputCanvas,
+              0,
+              0,
+              tempCanvas.width,
+              tempCanvas.height
+            );
+            imgData = tempCanvas.toDataURL('image/jpeg', 0.95);
+          } else {
+            imgData = outputCanvas.toDataURL('image/jpeg', 0.95);
+          }
 
           // 创建 PDF 实例,横向或纵向根据 Canvas 尺寸决定
           const orientation = cssWidth > cssHeight ? 'l' : 'p';
@@ -923,7 +998,7 @@
           const pdf = new jsPDF(orientation, 'mm', [pdfWidth, pdfHeight]);
 
           // 将 Canvas 图片添加到 PDF
-          pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
+          pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight);
 
           // 获取 PDF 文件流并触发事件
           const pdfBlob = pdf.output('blob');