|
@@ -52,7 +52,7 @@
|
|
|
<div class="report_loading_icon">
|
|
<div class="report_loading_icon">
|
|
|
<img src="@/assets/report/report_loading.png">
|
|
<img src="@/assets/report/report_loading.png">
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="report_loading_title">正在努力生成PDF中,请稍等...</div>
|
|
|
|
|
|
|
+ <div class="report_loading_title">{{ reportLoadingText }}</div>
|
|
|
<div class="report_loading_progress">
|
|
<div class="report_loading_progress">
|
|
|
<el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
|
|
<el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
|
|
|
</div>
|
|
</div>
|
|
@@ -77,7 +77,7 @@
|
|
|
<div class="report_loading_icon">
|
|
<div class="report_loading_icon">
|
|
|
<img src="@/assets/report/report_loading.png">
|
|
<img src="@/assets/report/report_loading.png">
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="report_loading_title">正在努力生成PDF中,请稍等...</div>
|
|
|
|
|
|
|
+ <div class="report_loading_title">{{ reportLoadingText }}</div>
|
|
|
<div class="report_loading_progress">
|
|
<div class="report_loading_progress">
|
|
|
<el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
|
|
<el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
|
|
|
</div>
|
|
</div>
|
|
@@ -137,6 +137,7 @@ export default {
|
|
|
currentPage: '', // 当前页码
|
|
currentPage: '', // 当前页码
|
|
|
totalPages: 0, // 总页数(由 turn.js 计算)
|
|
totalPages: 0, // 总页数(由 turn.js 计算)
|
|
|
showReportLoading:false,
|
|
showReportLoading:false,
|
|
|
|
|
+ reportLoadingText: '正在努力生成PDF中,请稍等...',//下载loading文案
|
|
|
targetProgress:0,
|
|
targetProgress:0,
|
|
|
loading:false,
|
|
loading:false,
|
|
|
pdfLoading:false,
|
|
pdfLoading:false,
|
|
@@ -581,95 +582,181 @@ export default {
|
|
|
// this.$emit('PdfLoadEnd');
|
|
// this.$emit('PdfLoadEnd');
|
|
|
// },
|
|
// },
|
|
|
async DownloadPdfNew() {
|
|
async DownloadPdfNew() {
|
|
|
|
|
+ // 防止重复点击触发多次下载
|
|
|
|
|
+ if (this.pdfLoading) return;
|
|
|
//获取所有的area_page 元素
|
|
//获取所有的area_page 元素
|
|
|
- const elements = document.querySelectorAll(".web_mode .web_area_page");
|
|
|
|
|
|
|
+ const elements = Array.from(document.querySelectorAll(".web_mode .web_area_page"));
|
|
|
const elLens = elements.length;
|
|
const elLens = elements.length;
|
|
|
|
|
+ if (elLens === 0) {
|
|
|
|
|
+ this.$message && this.$message.warning('暂无可下载的报告内容');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const elLensIndex = elLens - 1;
|
|
const elLensIndex = elLens - 1;
|
|
|
this.pdfLoading = true;
|
|
this.pdfLoading = true;
|
|
|
this.targetProgress = 0;
|
|
this.targetProgress = 0;
|
|
|
this.showReportLoading = true;
|
|
this.showReportLoading = true;
|
|
|
- const itemProgress = elLens > 0 ? Math.ceil(100 / elLens) : 0;
|
|
|
|
|
|
|
+ // this.reportLoadingText = '正在努力生成PDF中,请稍等...';
|
|
|
|
|
+ const itemProgress = Math.ceil(90 / elLens); // 截图与写入阶段占90%进度,剩余10%留给文件生成
|
|
|
const pdf = new jsPDF("p", "pt", "a4"); // 'p'表示纵向,'a4'表示A4纸张尺寸
|
|
const pdf = new jsPDF("p", "pt", "a4"); // 'p'表示纵向,'a4'表示A4纸张尺寸
|
|
|
const pdfWidth = pdf.internal.pageSize.getWidth(); //获取pdf的宽度
|
|
const pdfWidth = pdf.internal.pageSize.getWidth(); //获取pdf的宽度
|
|
|
const pdfHeight = pdf.internal.pageSize.getHeight(); //获取pdf的高度
|
|
const pdfHeight = pdf.internal.pageSize.getHeight(); //获取pdf的高度
|
|
|
- // 遍历每个 area_page 元素并按顺序处理
|
|
|
|
|
- for (const [pageIndex, element] of Array.from(elements).entries()) {
|
|
|
|
|
- // 确保页面元素存在并可见
|
|
|
|
|
- await this.$nextTick();
|
|
|
|
|
- if (element) {
|
|
|
|
|
- // 确保元素可见并已渲染
|
|
|
|
|
- // 确保元素可见并已渲染,并设置为横向排列样式
|
|
|
|
|
- element.style.visibility = 'visible';
|
|
|
|
|
- element.offsetHeight; // 触发重排
|
|
|
|
|
-
|
|
|
|
|
- // 等待元素完全渲染
|
|
|
|
|
- await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
|
-
|
|
|
|
|
- // 使用 dom-to-image 生成图片
|
|
|
|
|
- const dataUrl = await domtoimage.toJpeg(element, {
|
|
|
|
|
- quality: 0.8,// 降低质量以减小文件大小
|
|
|
|
|
- // 获取完整尺寸
|
|
|
|
|
- width: element.scrollWidth*2,
|
|
|
|
|
- height: element.scrollHeight*2,
|
|
|
|
|
- // 提高画布分辨率以获得更清晰的图像
|
|
|
|
|
- canvasWidth: element.scrollWidth * 2,
|
|
|
|
|
- canvasHeight: element.scrollHeight * 2,
|
|
|
|
|
- style: {
|
|
|
|
|
- 'transform': 'scale(2)',
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 1.等待字体加载完成,避免截图缺字或字体相关报错
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (document.fonts && document.fonts.ready) {
|
|
|
|
|
+ await document.fonts.ready;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 2.预加载页面内的图片资源,降低截图失败概率
|
|
|
|
|
+ await this.preloadPageImages(elements);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // 准备阶段异常不阻断下载,继续走截图逻辑
|
|
|
|
|
+ }
|
|
|
|
|
+ // 3.等待两帧渲染稳定,替代原先每页固定的100ms等待
|
|
|
|
|
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
|
|
|
+
|
|
|
|
|
+ // 单页截图:优先 dom-to-image,失败自动重试并降级 html2canvas 兜底
|
|
|
|
|
+ const capturePage = async (element, pageIndex) => {
|
|
|
|
|
+ const originVisibility = element.style.visibility; // 记录原可见性,截图后恢复
|
|
|
|
|
+ element.style.visibility = 'visible';
|
|
|
|
|
+ const width = element.scrollWidth || 908;
|
|
|
|
|
+ const height = element.scrollHeight || 1285;
|
|
|
|
|
+ const captureOptions = {
|
|
|
|
|
+ // quality: 0.8, // 降低质量以减小文件大小
|
|
|
|
|
+ // width: width * 2, // 2倍分辨率输出,保证清晰度
|
|
|
|
|
+ // height: height * 2,
|
|
|
|
|
+ // canvasWidth: width * 2,
|
|
|
|
|
+ // canvasHeight: height * 2,
|
|
|
|
|
+ // bgcolor: '#ffffff',
|
|
|
|
|
+ // style: {
|
|
|
|
|
+ // 'background-color': '#ffffff',
|
|
|
|
|
+ // // 关键:背景图尺寸适配原元素
|
|
|
|
|
+ // 'background-size': `${width}px ${height}px`, // 优先铺满且完整显示
|
|
|
|
|
+ // 'background-position': 'top left',
|
|
|
|
|
+ // 'background-repeat': 'no-repeat', // 防止背景图重复导致视觉不完整
|
|
|
|
|
+ // 'overflow': 'visible',
|
|
|
|
|
+ // 'white-space': 'normal',
|
|
|
|
|
+ // 'position': 'relative',
|
|
|
|
|
+ // 'visibility': 'visible'
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ quality: 0.78,
|
|
|
|
|
+ width: width * 1.5,
|
|
|
|
|
+ height: height * 1.5,
|
|
|
|
|
+ canvasWidth: width * 1.5,
|
|
|
|
|
+ canvasHeight: height * 1.5,
|
|
|
|
|
+ style: {
|
|
|
|
|
+ 'transform': 'scale(1.5)',
|
|
|
'transform-origin': 'top left',
|
|
'transform-origin': 'top left',
|
|
|
'background-color': '#ffffff',
|
|
'background-color': '#ffffff',
|
|
|
- // 关键:背景图尺寸适配原元素
|
|
|
|
|
- 'background-size': `${element.scrollWidth}px ${element.scrollHeight}px`, // 优先铺满且完整显示
|
|
|
|
|
|
|
+ 'background-size': `${width}px ${height}px`,
|
|
|
'background-position': 'top left',
|
|
'background-position': 'top left',
|
|
|
- 'background-repeat': 'no-repeat', // 防止背景图重复导致视觉不完整
|
|
|
|
|
|
|
+ 'background-repeat': 'no-repeat',
|
|
|
'overflow': 'visible',
|
|
'overflow': 'visible',
|
|
|
'white-space': 'normal',
|
|
'white-space': 'normal',
|
|
|
'position': 'relative',
|
|
'position': 'relative',
|
|
|
'visibility': 'visible'
|
|
'visibility': 'visible'
|
|
|
- },
|
|
|
|
|
- bgcolor: '#ffffff'
|
|
|
|
|
- });
|
|
|
|
|
- // 获取图像属性并计算缩放比例
|
|
|
|
|
- const imgProps = pdf.getImageProperties(dataUrl);
|
|
|
|
|
- const imgWidth = imgProps.width;
|
|
|
|
|
- const imgHeight = imgProps.height;
|
|
|
|
|
-
|
|
|
|
|
- // 计算缩放比例(保持宽高比)
|
|
|
|
|
- const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight);
|
|
|
|
|
- const adjustWidth = imgWidth * ratio;
|
|
|
|
|
- const adjustHeight = imgHeight * ratio;
|
|
|
|
|
-
|
|
|
|
|
- // 居中放置图像
|
|
|
|
|
- const xPosition = (pdfWidth - adjustWidth) / 2;
|
|
|
|
|
- const yPosition = (pdfHeight - adjustHeight) / 2;
|
|
|
|
|
-
|
|
|
|
|
- // console.log("打印pdf图片尺寸", imgWidth, imgHeight);
|
|
|
|
|
- // console.log("打印pdf页面尺寸", pdfWidth, pdfHeight);
|
|
|
|
|
- // console.log("打印pdf缩放比例", ratio);
|
|
|
|
|
- // console.log("打印pdf缩放后尺寸",adjustWidth, adjustHeight);
|
|
|
|
|
- // console.log("打印pdf图片位置", xPosition, yPosition);
|
|
|
|
|
- // 如果不是第一页,添加新页面
|
|
|
|
|
- if (pageIndex > 0)
|
|
|
|
|
- {
|
|
|
|
|
- pdf.addPage();
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ bgcolor: '#ffffff',
|
|
|
|
|
+ skipFonts: false,
|
|
|
|
|
+ skipCss: false,
|
|
|
|
|
+ cacheBust: true,
|
|
|
|
|
+ };
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 第一次 dom-to-image
|
|
|
|
|
+ return await domtoimage.toJpeg(element, captureOptions);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // 失败多为图片/字体资源未就绪,短暂等待后重试一次
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
+ try {
|
|
|
|
|
+ return await domtoimage.toJpeg(element, captureOptions);
|
|
|
|
|
+ } catch (err2) {
|
|
|
|
|
+ // html2canvas 兜底,最大限度保证出图成功
|
|
|
|
|
+ try {
|
|
|
|
|
+ const canvas = await html2canvas(element, {
|
|
|
|
|
+ scale: 2,
|
|
|
|
|
+ useCORS: true,
|
|
|
|
|
+ logging: false,
|
|
|
|
|
+ backgroundColor: '#ffffff',
|
|
|
|
|
+ letterRendering: true
|
|
|
|
|
+ });
|
|
|
|
|
+ return canvas.toDataURL('image/jpeg', 0.8);
|
|
|
|
|
+ } catch (err3) {
|
|
|
|
|
+ throw new Error(`第 ${pageIndex + 1} 页截图失败:${err3 && err3.message ? err3.message : err3}`);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- //处理封面图铺满屏
|
|
|
|
|
- let pdfAdjustHeight = adjustHeight;
|
|
|
|
|
- if(pageIndex == 0 || pageIndex == elLensIndex){
|
|
|
|
|
- pdfAdjustHeight = adjustHeight + 6
|
|
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ element.style.visibility = originVisibility; // 恢复原样式,避免影响页面展示
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 4.分批并发截图(每批2页):并发缩短耗时,分批控制内存峰值,防止浏览器内存溢出
|
|
|
|
|
+ const batchSize = 2;
|
|
|
|
|
+ for (let start = 0; start < elLens; start += batchSize) {
|
|
|
|
|
+ const batch = elements.slice(start, start + batchSize);
|
|
|
|
|
+ const dataUrlList = await Promise.all(batch.map((el, idx) => capturePage(el, start + idx))); // 结果顺序与页码一致
|
|
|
|
|
+ for (let j = 0; j < dataUrlList.length; j++) {
|
|
|
|
|
+ const dataUrl = dataUrlList[j];
|
|
|
|
|
+ const pageIndex = start + j;
|
|
|
|
|
+ // 获取图像属性并计算缩放比例
|
|
|
|
|
+ const imgProps = pdf.getImageProperties(dataUrl);
|
|
|
|
|
+ const imgWidth = imgProps.width;
|
|
|
|
|
+ const imgHeight = imgProps.height;
|
|
|
|
|
+ // 计算缩放比例(保持宽高比)
|
|
|
|
|
+ const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight);
|
|
|
|
|
+ const adjustWidth = imgWidth * ratio;
|
|
|
|
|
+ const adjustHeight = imgHeight * ratio;
|
|
|
|
|
+ // 居中放置图像
|
|
|
|
|
+ const xPosition = (pdfWidth - adjustWidth) / 2;
|
|
|
|
|
+ const yPosition = (pdfHeight - adjustHeight) / 2;
|
|
|
|
|
+ // 如果不是第一页,添加新页面
|
|
|
|
|
+ if (pageIndex > 0) {
|
|
|
|
|
+ pdf.addPage();
|
|
|
|
|
+ }
|
|
|
|
|
+ // 处理封面图铺满屏(上下各补6pt,消除白边)
|
|
|
|
|
+ const pdfAdjustHeight = (pageIndex === 0 || pageIndex === elLensIndex) ? adjustHeight + 6 : adjustHeight;
|
|
|
|
|
+ pdf.addImage(dataUrl, "JPEG", xPosition, yPosition - 6, adjustWidth, pdfAdjustHeight);
|
|
|
|
|
+ // 更新进度(不超过100%)
|
|
|
|
|
+ this.targetProgress = Math.min(this.targetProgress + itemProgress, 99);
|
|
|
|
|
+ dataUrlList[j] = null; // 及时释放引用,避免内存累积
|
|
|
}
|
|
}
|
|
|
- pdf.addImage(dataUrl, "JPEG", xPosition, yPosition - 6, adjustWidth, pdfAdjustHeight);
|
|
|
|
|
- this.targetProgress += itemProgress;
|
|
|
|
|
- // 边界处理:进度值不超过100%
|
|
|
|
|
- this.targetProgress = Math.min(this.targetProgress, 100);
|
|
|
|
|
|
|
+ // 让出主线程,保证进度条/loading正常刷新
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
}
|
|
}
|
|
|
|
|
+ // 5.文件生成阶段:预留进度并刷新UI,避免进度条先到100%但下载框迟迟不弹出
|
|
|
|
|
+ // this.reportLoadingText = '正在生成文件,请稍候...';
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 50)); // 让出主线程,刷新90%进度
|
|
|
|
|
+ // 保存pdf文件(同步生成文件并触发浏览器下载)
|
|
|
|
|
+ pdf.save(`${this.reportTitle}_${this.coverTitle}.pdf`);
|
|
|
|
|
+ this.targetProgress = 100; // 文件已生成、下载框弹出时进度才到100%
|
|
|
|
|
+ this.$emit('PdfLoadEnd');
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('DownloadPdfNew 生成PDF失败:', err);
|
|
|
|
|
+ this.$message && this.$message.error((err && err.message) || 'PDF 生成失败,请重试');
|
|
|
|
|
+ this.$emit('PdfLoadEnd');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // 无论成功失败都复位loading状态,防止按钮/弹窗卡死
|
|
|
|
|
+ this.showReportLoading = false;
|
|
|
|
|
+ this.targetProgress = 0;
|
|
|
|
|
+ this.pdfLoading = false;
|
|
|
}
|
|
}
|
|
|
- // 保存pdf文件
|
|
|
|
|
- pdf.save(`${this.reportTitle}_${this.coverTitle}.pdf`);
|
|
|
|
|
- this.showReportLoading = false; //关闭加载loading
|
|
|
|
|
- this.targetProgress = 0;
|
|
|
|
|
- this.pdfLoading = false;
|
|
|
|
|
- this.$emit('PdfLoadEnd');
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ // 预加载页面内的图片资源,降低 dom-to-image 截图失败概率
|
|
|
|
|
+ async preloadPageImages(elements) {
|
|
|
|
|
+ const tasks = [];
|
|
|
|
|
+ elements.forEach(el => {
|
|
|
|
|
+ el.querySelectorAll('img').forEach(img => {
|
|
|
|
|
+ const src = img.currentSrc || img.src;
|
|
|
|
|
+ if (!src) return;
|
|
|
|
|
+ tasks.push(new Promise(resolve => {
|
|
|
|
|
+ const image = new Image();
|
|
|
|
|
+ image.onload = () => resolve(true);
|
|
|
|
|
+ image.onerror = () => resolve(false);
|
|
|
|
|
+ image.src = src;
|
|
|
|
|
+ }));
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ await Promise.all(tasks);
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|