瀏覽代碼

修改:学生报告下载优化防止浏览器溢出、处理分页异常

liurongli 18 小時之前
父節點
當前提交
e47d1afe02

+ 3 - 0
src/styles/report.scss

@@ -2397,6 +2397,9 @@
       height: 36px;
       top: 148px;
       right: 20px;
+      @media screen and (max-width: 1440px){
+        width: 100px;
+      }
     }
     .area_page
     {

+ 158 - 71
src/views/analysisReport/studentPage/downloadPdf/components/bookFlip.vue

@@ -52,7 +52,7 @@
                             <div class="report_loading_icon">
                             <img src="@/assets/report/report_loading.png">
                             </div>
-                            <div class="report_loading_title">正在努力生成PDF中,请稍等...</div> 
+                            <div class="report_loading_title">{{ reportLoadingText }}</div> 
                             <div class="report_loading_progress">
                             <el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
                             </div>
@@ -77,7 +77,7 @@
                     <div class="report_loading_icon">
                     <img src="@/assets/report/report_loading.png">
                     </div>
-                    <div class="report_loading_title">正在努力生成PDF中,请稍等...</div> 
+                    <div class="report_loading_title">{{ reportLoadingText }}</div> 
                     <div class="report_loading_progress">
                     <el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
                     </div>
@@ -137,6 +137,7 @@ export default {
             currentPage: '', // 当前页码
             totalPages: 0, // 总页数(由 turn.js 计算)
             showReportLoading:false,
+            reportLoadingText: '正在努力生成PDF中,请稍等...',//下载loading文案
             targetProgress:0,
             loading:false,
             pdfLoading:false,
@@ -581,95 +582,181 @@ export default {
         //     this.$emit('PdfLoadEnd');
         // },
         async DownloadPdfNew() {
+            // 防止重复点击触发多次下载
+            if (this.pdfLoading) return;
             //获取所有的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;
+            if (elLens === 0) {
+                this.$message && this.$message.warning('暂无可下载的报告内容');
+                return;
+            }
             const elLensIndex = elLens - 1;
             this.pdfLoading = true;
             this.targetProgress = 0;
             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 pdfWidth = pdf.internal.pageSize.getWidth(); //获取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',
                         'background-color': '#ffffff',
-                        // 关键:背景图尺寸适配原元素
-                        'background-size': `${element.scrollWidth}px ${element.scrollHeight}px`, // 优先铺满且完整显示
+                        'background-size': `${width}px ${height}px`,
                         'background-position': 'top left',
-                        'background-repeat': 'no-repeat', // 防止背景图重复导致视觉不完整
+                        'background-repeat': 'no-repeat',
                         'overflow': 'visible',
                         'white-space': 'normal',
                         'position': 'relative',
                         '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);
         },
     }
 };

+ 69 - 48
src/views/analysisReport/studentPage/downloadPdf/studentReport.vue

@@ -422,7 +422,9 @@ export default {
         },//监听筛选数据变化
     },
     async created() {
-        this.setHtmlFontSize();
+        this.$nextTick(()=>{
+            this.setHtmlFontSize();
+        })
         // 监听窗口缩放,实时更新
         window.addEventListener('resize', this.setHtmlFontSize);
         // 可选:监听页面加载完成后再设置一次(避免初始渲染问题)
@@ -463,6 +465,9 @@ export default {
             // this.targetProgress = 1;
             // this.showReportLoading = true;
             this.pageCount = 15;
+            // moduleHeightData 先给兜底值,$nextTick 里再替换为真实标题高度,
+            // 避免与同步初始化的 modulePageData 出现时序性的长度不一致
+            this.moduleHeightData = [82];
             this.$nextTick(()=>{
                 const titleHeight = this.$refs?.areaReportTitle?.offsetHeight || 0;
                 const reportTitleHeight = titleHeight + 82;
@@ -618,7 +623,7 @@ export default {
                 ...this.reportParam,
                 subjectGroupType: 1, // 科目是否为组合
                 isTotal: 1, //是否为总分科目 1为总分 0为非总分
-            }).then(res => {
+            }).then(async res => {
                 if (res.code == 200 && res.data) {
                     const titleData = res?.data?.titleData || [];
                     this.multiSubjectData.studentOpenness = res?.data?.studentOpenness ?? '';
@@ -671,17 +676,15 @@ export default {
                     })
                     this.multiSubjectData.maxSubject = maxSubject.join('、')
                     this.multiSubjectData.minSubject = minSubject.join('、')
-                    //标准分分析图分页
-                    this.$nextTick(()=>{
-                        if((this.multiSubjectData.standardScoreAnalysisStatus === 0 && this.schoolType == 2) || this.schoolType == 1){
-                            const desHeight = this.$refs?.standardScoreChartDes?.offsetHeight || 0;
-                            const divHeight = 383 + desHeight;
-                            this.multiSubjectData.chartPagesNum = this.SingleChartPage(this.multiSubjectData.datay.length > 0 ? divHeight : 0);
-                        }else{
-                            this.multiSubjectData.chartPagesNum = this.SingleChartPage(0);
-                        }
-                        // console.log(divHeight,desHeight,this.multiSubjectData.chartPagesNum,13322133)
-                    })
+                    //标准分分析图分页(await 确保分页在函数返回前完成,避免与后续模块分页产生顺序竞争)
+                    await this.$nextTick();
+                    if((this.multiSubjectData.standardScoreAnalysisStatus === 0 && this.schoolType == 2) || this.schoolType == 1){
+                        const desHeight = this.$refs?.standardScoreChartDes?.offsetHeight || 0;
+                        const divHeight = 383 + desHeight;
+                        this.multiSubjectData.chartPagesNum = this.SingleChartPage(this.multiSubjectData.datay.length > 0 ? divHeight : 0);
+                    }else{
+                        this.multiSubjectData.chartPagesNum = this.SingleChartPage(0);
+                    }
                 } else {
                     this.multiSubjectData.studentOpenness = '';
                     this.multiSubjectData.showHistory = 0;
@@ -740,7 +743,7 @@ export default {
                 ...this.reportParam,
                 subjectGroupType: 1, // 科目是否为组合
                 isTotal: 1, //是否为总分科目 1为总分 0为非总分
-            }).then(res => {
+            }).then(async res => {
                 if (res.code == 200 && res.data) {
                     const data = res.data;
                     const upSubjectList = data.upSubjectData || [];
@@ -773,11 +776,10 @@ export default {
                 } else {
                     this.suggestionHtml = null
                 }
-                this.$nextTick(()=>{
-                    const suggestionHeight = this.$refs?.multiSuggestionModule?.offsetHeight || 0;
-                    // console.log(suggestionHeight,99888)
-                    this.multiSuggestionPageNum = this.SingleChartPage(this.suggestionHtml?suggestionHeight:0);
-                })
+                // 多科总结建议分页(await 确保分页在函数返回前完成,避免与后续模块分页产生顺序竞争)
+                await this.$nextTick();
+                const suggestionHeight = this.$refs?.multiSuggestionModule?.offsetHeight || 0;
+                this.multiSuggestionPageNum = this.SingleChartPage(this.suggestionHtml?suggestionHeight:0);
             })
         },
         //学生端查询单科-我的成绩
@@ -1025,7 +1027,7 @@ export default {
                 subjectGroupType: 0, // 科目是否为组合
                 isTotal: 0, //是否为总分科目 1为总分 0为非总分
                 subjectCode:subjectCode
-            }).then(res => {
+            }).then(async res => {
                 if (res.code == 200 && res.data) {
                     const data = res.data
                     //* 1-得分显示分数,小题分显示分数,2-得分显示分数,小题分显示对错
@@ -1057,11 +1059,10 @@ export default {
                     if (downSubjectData) {
                         this.singleSubjectData[index].suggestionHtml += `${downSubjectData},是你的劣势学科,建议先加强学习,熟练掌握薄弱知识点的基础,然后通过提分练习进行巩固和强化,争取下次考试获得更优异的成绩!`
                     }
-                    this.$nextTick(()=>{
-                        const suggestionHeight = this.$refs?.[`singleSubjectSuggestion_${index}`]?.[0]?.offsetHeight || 0;
-                        // console.log(suggestionHeight,8999111)
-                        this.singleSubjectData[index].suggestionPageNum = this.SingleChartPage(this.singleSubjectData[index].suggestionHtml?suggestionHeight:0);
-                    })
+                    // 单科总结建议分页(await 确保分页在函数返回前完成,答题卡分页 FindStudentCard 才能基于最终页码状态计算)
+                    await this.$nextTick();
+                    const suggestionHeight = this.$refs?.[`singleSubjectSuggestion_${index}`]?.[0]?.offsetHeight || 0;
+                    this.singleSubjectData[index].suggestionPageNum = this.SingleChartPage(this.singleSubjectData[index].suggestionHtml?suggestionHeight:0);
                 } else {
                     this.singleSubjectData[index].suggestionHtml = ''
                 }
@@ -1327,36 +1328,56 @@ export default {
         SingleChartPage(chartHeight){
             let chartPagesNum = '';//各分析图 高度
             const prevModuleLastPageNum = this.modulePageData.length > 0 ? this.modulePageData[this.modulePageData.length - 1] : 1;//上一模块 最后一页页码
-            //上一模块最后一页高度 和 当前模块 echart高度
-            let prevModuleLastPageHeight = [],moduleChartHeight = [];
+            // 上一模块最后一页的累计高度(同页码所有模块记录高度之和;|| 0 防御历史脏数据里的 NaN/undefined)
+            let lastPageHeightSum = 0;
             this.modulePageData.forEach((item,i)=>{
                 if(item == prevModuleLastPageNum){
-                    prevModuleLastPageHeight.push(this.moduleHeightData[i])
-                    moduleChartHeight.push(this.moduleHeightData[i]);//最后一页高度
+                    lastPageHeightSum += (this.moduleHeightData[i] || 0);
                 }
             })
-            // 将当前模块 echart高度
+            // 将当前模块 echart高度;与下方页码 push 严格 1:1,保证 modulePageData / moduleHeightData 长度恒等
             this.moduleHeightData.push(chartHeight)
-            moduleChartHeight.push(chartHeight)
-            //进行分页 满足一页的高度放在一起
-            const printPageGroup = this.groupByThreshold([...moduleChartHeight], this.printPageHeight);
-            //每个模块对应的页码
-            for (let i = 0; i < printPageGroup.length; i++) {
-                const arr = printPageGroup[i]
-                for (let j = 0; j < arr.length; j++) {
-                    if(i == 0){//第一页
-                        if(j > prevModuleLastPageHeight.length - 1){
-                            this.modulePageData.push(prevModuleLastPageNum)
-                            chartPagesNum = prevModuleLastPageNum
-                        }
-                    }else{
-                        this.modulePageData.push(prevModuleLastPageNum + i);
-                        chartPagesNum = prevModuleLastPageNum + i
-                    }
-                }
-            }
+            // 判断当前图表放哪一页:上一页放得下则并入上一页,放不下则新开一页。
+            // 页码永远只会是"上一页"或"上一页+1",不会跳号,从根本上杜绝空白页;
+            // 也不会因高度累计超页把上一页记录拆散、对旧记录重复 push 页码,杜绝长度失衡和多模块叠加到同一页
+            chartPagesNum = (lastPageHeightSum + chartHeight <= this.printPageHeight) ? prevModuleLastPageNum : prevModuleLastPageNum + 1;
+            this.modulePageData.push(chartPagesNum);
             return chartPagesNum
         },
+        // SingleChartPage(chartHeight){
+        //     let chartPagesNum = '';//各分析图 高度
+        //     const prevModuleLastPageNum = this.modulePageData.length > 0 ? this.modulePageData[this.modulePageData.length - 1] : 1;//上一模块 最后一页页码
+        //     //上一模块最后一页高度 和 当前模块 echart高度
+        //     let prevModuleLastPageHeight = [],moduleChartHeight = [];
+        //     this.modulePageData.forEach((item,i)=>{
+        //         if(item == prevModuleLastPageNum){
+        //             console.log(this.moduleHeightData[i],88888888)
+        //             prevModuleLastPageHeight.push(this.moduleHeightData?.[i] || 0)
+        //             moduleChartHeight.push(this.moduleHeightData?.[i] || 0);//最后一页高度
+        //         }
+        //     })
+        //     // 将当前模块 echart高度
+        //     this.moduleHeightData.push(chartHeight)
+        //     moduleChartHeight.push(chartHeight)
+        //     //进行分页 满足一页的高度放在一起
+        //     const printPageGroup = this.groupByThreshold([...moduleChartHeight], this.printPageHeight);
+        //     //每个模块对应的页码
+        //     for (let i = 0; i < printPageGroup.length; i++) {
+        //         const arr = printPageGroup[i]
+        //         for (let j = 0; j < arr.length; j++) {
+        //             if(i == 0){//第一页
+        //                 if(j > prevModuleLastPageHeight.length - 1){
+        //                     this.modulePageData.push(prevModuleLastPageNum)
+        //                     chartPagesNum = prevModuleLastPageNum
+        //                 }
+        //             }else{
+        //                 this.modulePageData.push(prevModuleLastPageNum + i);
+        //                 chartPagesNum = prevModuleLastPageNum + i
+        //             }
+        //         }
+        //     }
+        //     return chartPagesNum
+        // },
         GetDifficultyClass(val) {
             if (val == 1) {
                 return 'difficulty easy'