|
|
@@ -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');
|