소스 검색

学生端增加错题手册导出功能更新

dengshaobo 1 개월 전
부모
커밋
51421e9b56
5개의 변경된 파일과 2662개의 추가작업 그리고 3개의 파일을 삭제
  1. BIN
      src/assets/bg/target_pdf_header.png
  2. 5 0
      src/http/api/target.js
  3. 278 1
      src/styles/common.scss
  4. 2347 0
      src/views/targetImprove/consolidate/export.vue
  5. 32 2
      src/views/targetImprove/consolidate/index.vue

BIN
src/assets/bg/target_pdf_header.png


+ 5 - 0
src/http/api/target.js

@@ -77,6 +77,11 @@ const targetImprove = {
   waitExerciseNum(data) {
     return get( base.prefix+ `/api/v1/studentTarge/wait_exercise_num`, data)
   },
+
+  //获取学生端错题导出信息接口
+  getTargetErrorExportData(data) {
+    return post( base.prefix+ `/api/v1/studentError/query_error_question`, data)
+  },
 }
 
 export default targetImprove

+ 278 - 1
src/styles/common.scss

@@ -11392,4 +11392,281 @@ body {
           padding-top: 220px;
         }
       }
-}
+}
+
+
+//学生报告打印区域
+.print_slip
+{
+  width: 100%;
+  height: calc(100% - 40px);
+  
+  margin: auto;
+  overflow-y: auto;
+  display: flex;
+  justify-content: center;
+  .slip_content
+  {
+    width: auto;
+
+    height: auto;
+  }
+  .slip_page
+  {
+    // width: 595px;
+    // height: 842px;
+    margin: auto;
+    background: #fff;
+    border-radius: 0px 0px 0px 0px;
+    padding: 20px;
+    box-sizing: border-box;
+    margin-bottom: 20px;
+  }
+
+  .area_page
+  {
+    margin: auto;
+    background: #fff;
+    border-radius: 0px 0px 0px 0px;
+    padding: 20px;
+    box-sizing: border-box;
+    margin-bottom: 20px;
+  }
+  .student_item
+  {
+    width: 100%;
+    // height: calc((100% - 40px)/3);
+    height: auto;
+    // border-bottom: 1px dashed #E6E6E6;
+    // margin-bottom: 20px;
+
+    &:last-child
+    {
+      margin-bottom:0;//移除最后一个的底部边距
+    }
+    .item_header
+    {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      height: 25px;
+      .header_left
+      {
+        font-weight: 500;
+        font-size: 12px;
+        color:#000;
+        display: flex;
+        justify-content: flex-start;
+        gap: 10px;
+
+      }
+
+      .header_right
+      {
+        font-weight: 400;
+        font-size: 12px;
+        color:#333;
+        span
+        {
+          font-weight: 500;
+          font-size: 12px;
+          color: #000;
+        }
+      }
+    }
+    .parent_signature{
+      width: 100%;
+      height: 50px;
+      padding-top: 10px;
+      box-sizing: border-box;
+      line-height: 30px;
+      display: flex;
+      justify-content:space-between;
+      align-items: center;
+      span{
+        &:nth-child(1){
+          font-weight: 400;
+          font-size: 12px;
+          color: #999999;
+        }
+        &:nth-child(2){
+          font-weight: 400;
+          font-size: 12px;
+          color: #000000;
+          width: 30%;
+        }
+      }
+    }
+    .item_line
+    {
+      width: 100%;
+      height: 30px;
+     
+
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      .line_left
+      {
+        width: 20px;
+        height: 20px;
+        background-image: url("@/assets/report/scissors_left.webp");
+        background-size:16px auto;
+        background-position: 50% 50%;
+        background-repeat: no-repeat;
+
+      }
+
+      .line_right
+      {
+        width: 20px;
+        height: 20px;
+        background-image: url("@/assets/report/scissors_right.webp");
+        background-size:16px auto;
+        background-position: 50% 50%;
+        background-repeat: no-repeat;
+      }
+
+      .line_content
+      {
+        width: calc(100% - 40px);
+        height: 1px;
+        border-bottom: 1px dashed #E6E6E6;
+      }
+    }
+  }
+
+  .item_table
+  {
+      width: 100%;
+      height: auto;
+      // min-height: 200px;
+      border-radius: 5px;
+      border:1px solid #EBEEF5;
+      border-bottom: 0px solid #EBEEF5;
+      display: flex;
+      justify-content:space-between;
+  
+      .table_td_header
+      {
+          width: 100%;
+          background-color: #F4F6F8;
+          height:22px;
+          line-height: 22px;
+          text-align: center;
+          border-left: 1px solid #EBEEF5;
+          border-bottom: 1px solid #EBEEF5;
+  
+          font-size: 10px;
+          color:#000;
+          font-weight: 500;
+
+          white-space: nowrap;     /* 禁止换行 */
+          overflow: hidden;        /* 隐藏溢出内容 */
+          text-overflow: ellipsis; /* 溢出部分显示省略号 */
+      }
+  
+      .table_td
+      {
+          width: 100%;
+          height:22px;
+          line-height: 22px;
+          text-align: center;
+          border-left: 1px solid #EBEEF5;
+          border-bottom: 1px solid #EBEEF5;
+          font-size: 10px;
+          color:#333;
+          font-weight: 400;
+      }
+  
+      .table_left
+      {
+          width: 39px;
+          height: auto;
+          display: flex;
+          flex-direction: column;
+          flex-wrap: wrap;
+          .table_tr
+          {
+              width:100%;
+              height: auto;
+  
+              // &:last-child
+              // {
+              //     .table_td {
+  
+              //         &:last-child
+              //         {
+              //             border-bottom: none; // 移除最后一个 .table_tr 的底部边框
+              //         }
+              //     }
+              // }
+              .table_td_header
+              {
+                  width: 100%;
+                  background-color: #F4F6F8;
+                  height:22px;
+                  line-height: 22px;
+                  text-align: center;
+                  border-left: 1px solid #F4F6F8;
+                  border-bottom: 1px solid #EBEEF5;
+              }
+  
+              .table_td
+              {
+                  width: 100%;
+                  height:22px;
+                  line-height: 22px;
+                  text-align: center;
+                  border-left: 1px solid #fff;
+                  border-bottom: 1px solid #EBEEF5;
+              }
+  
+  
+          }
+  
+      }
+      .table_right
+      {
+          width: calc(100% - 39px);
+          display: flex;
+          justify-content:space-between;
+          flex-wrap: wrap;
+          .table_tr
+          {
+              width:calc(100%/12);
+              height: auto;
+          }
+      }
+  }
+  
+  .td_icon_yes
+  {
+      width: 100%;
+      height: 100%;
+      background-image: url("@/assets/report/score_yes_icon.webp");
+      background-size:12px auto;
+      background-position: 50% 50%;
+      background-repeat: no-repeat;
+  }
+  
+  .td_icon_no
+  {
+      width: 100%;
+      height: 100%;
+      background-image: url("@/assets/report/score_no_icon.webp");
+      background-size:12px auto;
+      background-position: 50% 50%;
+      background-repeat: no-repeat;
+  }
+  
+  .td_icon_dimidiate
+  {
+      width: 100%;
+      height: 100%;
+      background-image: url("@/assets/report/score_dimidiate_icon.webp");
+      background-size:12px auto;
+      background-position: 50% 50%;
+      background-repeat: no-repeat;
+  }
+}

+ 2347 - 0
src/views/targetImprove/consolidate/export.vue

@@ -0,0 +1,2347 @@
+<template>
+  <div :key="dialogKey">
+    <el-dialog title="导出个性化错题手册" class="page_full_dialog"  :visible.sync="showDialog"  fullscreen height="100%" >
+      <div class="score_slip">
+        <div class="page_jg_20"></div>
+        <div class="print_slip" ref="printSlip" >
+          <div class="slip_content" v-loading="loading" element-loading-text="正在努力加载中,请稍候……" element-loading-spinner="el-icon-loading" element-loading-background="#ffffff">
+            <div class="area_page" v-for="(page,pageIndex) in pageList" :key="'slip_'+pageIndex"  :style="{width:pageInfo.width+'px',height:pageInfo.height+'px'}">
+              <!-- 页眉 每页都有 -->
+              <div class="page_header_info">
+                <span>{{studentName}}  {{className}}</span>
+                <span>{{schoolName}}</span>
+              </div>
+              
+              <!-- 固定内容区域 -->
+              <div class="page_area_content">
+                <!-- 一栏布局 -->
+                <template v-if="columnRadio==1">
+                  <template v-for="(questionItem,index) in page.list">
+                    <!-- 切片包裹层:超高块切片时按切片高度裁剪 正常块不受影响 -->
+                    <div :key="'w_'+index" :style="questionItem.isSlice ? {height: questionItem.sliceHeight + 'px', overflow: 'hidden'} : {}">
+                    <div :style="questionItem.isSlice ? {'margin-top': -questionItem.offsetTop + 'px'} : {}">
+                    <!-- 错题本顶部标题 区域 -->
+                    <div class="page_title_info" v-if="questionItem.areaType=='pageTitle'">
+                      {{ questionItem.title }}
+                      
+                    </div>
+                    <!-- 标题下方间距:用实体间隔块(高度计入测量与切片) 不用margin-bottom(PDF截图/切片包裹层会丢失) -->
+                    <div class="page_jg_10" v-if="questionItem.areaType=='pageTitle'"></div>
+                    <!-- 知识点标题区 -->
+                    <div class="page_knowledge_info" v-if="questionItem.areaType=='knowledge'">
+                      <span class="knowledge_name">{{questionItem.knowledgeName}}</span>
+                    </div>
+                    <div class="page_jg_10" v-if="questionItem.areaType=='knowledge'"></div>
+                    <!-- 题目内容区 -->
+                    <div class="question_item" v-if="questionItem.areaType=='question'" >
+                      <div class="item_header_one">
+                        <div class="header_left">
+                          <span class="left_question_num">错题{{questionItem.questionNum}}</span>
+                          <span class="left_question_source">来源:{{questionItem.sourceName}}</span>
+                        </div>
+                        <div class="header_right">
+                          <span v-if="questionItem.answerScore">
+                            满分:{{questionItem.answerScore}}
+                          </span>
+                          <span v-if="questionItem.score">
+                            得分:{{questionItem.score}}
+                          </span>
+                          <span>
+                            得分率:{{questionItem.scoreRate}}%
+                          </span>
+                        </div>
+                      </div>
+                      <div class="page_jg_10">
+                      </div>
+                      <div class="item_question_title" v-html="questionItem.title">
+                      </div>
+                      <div class="page_jg_10">
+                      </div>
+                      <div class="item_question_knowledge">
+                        <span class="knowledge_title">考查知识点:</span><span class="knowledge_list">{{(questionItem.knowledgeList || []).map(k => k.knowName).filter(Boolean).join('、')}}</span>
+                      </div>
+                      <div class="page_jg_10">
+                      </div>
+                      
+                      
+                    </div>
+                    <!-- 学生作答区 -->
+                    <div v-if="questionItem.areaType=='studentAnswer'">
+                      <div class="item_question_answer" >
+                        <div class="answer_title">我的作答</div>
+                        <!-- 客观题 直接显示学生选择的答案 -->
+                        <div class="answer_content" v-html="questionItem.studentAnswer" v-if="questionItem.isObjective==1">
+
+                        </div>
+                        <!-- 主观题 显示学生作答的图片 -->
+                        <!-- <div class="answer_images" v-if="questionItem.isObjective==0">
+                          <img v-for="(image, imageIndex) in questionItem.studentAnswer.split(';')" :key="'image_'+imageIndex" :src="image" alt="">
+                        </div> -->
+                        <!-- 主观题:只有真正的图片地址才渲染成 img;文本作答(如 isObjective 未标对但内容是"C")按文本显示,避免塞进 img src 产生 404 -->
+                        <div class="answer_images" v-if="questionItem.isObjective==0">
+                            <img v-for="(image, imageIndex) in AnswerImages(questionItem.studentAnswer)" :key="'image_'+imageIndex" :src="image" alt="">
+                            <div class="answer_content" v-if="AnswerText(questionItem.studentAnswer)" v-html="AnswerText(questionItem.studentAnswer)"></div>
+                        </div>
+                      </div>
+                      <div class="page_jg_10">
+                      </div>
+                    </div>
+                    <!-- 答案解析区 -->
+                    <div v-if="questionItem.areaType=='answerAnalysis'">
+                        <div class="item_question_answer" v-if="questionItem.parse">
+                          <div class="answer_title">答案解析</div>
+                          <div class="answer_content" >
+                            <span>【答案】:</span>
+                            <span v-html="questionItem.answer"></span>
+                          </div>
+                          <div class="answer_content" v-html="questionItem.parse">
+
+                          </div>
+                        </div>
+                        <div class="page_jg_10">
+                        </div>
+                    </div>
+                    </div>
+                    </div>
+                  </template>
+                </template>
+                <!-- 两栏布局:一页分两半 先填左栏再填右栏 -->
+                <template v-else>
+                  <!-- 通栏标题区(错题本顶部标题 / 卷底答案解析标题) -->
+                  <template v-for="(fullItem,fullIdx) in page.fullList">
+                    <div class="page_title_info" v-if="fullItem.areaType=='pageTitle'" :key="'full_'+fullIdx">
+                      {{ fullItem.title }}
+                    </div>
+                    <div class="page_jg_10" v-if="fullItem.areaType=='pageTitle'" :key="'full_gap_'+fullIdx"></div>
+                  </template>
+                  <div class="page_columns_wrap">
+                    <div class="page_column" v-for="(colList,colIdx) in page.columns" :key="'col_'+colIdx">
+                      <template v-for="(questionItem,index) in colList">
+                        <!-- 切片包裹层:超高块切片时按切片高度裁剪 正常块不受影响 -->
+                        <div :key="'w_'+index" :style="questionItem.isSlice ? {height: questionItem.sliceHeight + 'px', overflow: 'hidden'} : {}">
+                        <div :style="questionItem.isSlice ? {'margin-top': -questionItem.offsetTop + 'px'} : {}">
+                        <!-- 知识点标题区 -->
+                        <div class="page_knowledge_info" v-if="questionItem.areaType=='knowledge'" :key="'k_'+index">
+                          <span class="knowledge_name">{{questionItem.knowledgeName}}</span>
+                        </div>
+                        <div class="page_jg_10" v-if="questionItem.areaType=='knowledge'" :key="'kg_'+index"></div>
+                        <!-- 题目内容区 -->
+                        <div class="question_item" v-if="questionItem.areaType=='question'" :key="'q_'+index">
+                          <div class="item_header_two">
+                            <div class="header_left">
+                              <span class="left_question_num">错题{{questionItem.questionNum}}</span>
+                              <span class="left_question_source">
+                                <span v-if="questionItem.answerScore">
+                                满分:{{questionItem.answerScore}}
+                              </span>
+                              <span v-if="questionItem.score">
+                                得分:{{questionItem.score}}
+                              </span>
+                              <span>
+                                得分率:{{questionItem.scoreRate}}%
+                              </span>
+                              </span>
+                              
+                            </div>
+                            <div class="header_right">
+                              <span class="left_question_source">来源:{{questionItem.sourceName}}</span>
+                            </div>
+                          </div> 
+                          <div class="page_jg_10">
+                          </div>
+                          <div class="item_question_title" v-html="questionItem.title">
+                          </div>
+                          <div class="page_jg_10">
+                          </div>
+                          <div class="item_question_knowledge">
+                            <span class="knowledge_title">考查知识点:</span><span class="knowledge_list">{{(questionItem.knowledgeList || []).map(k => k.knowName).filter(Boolean).join('、')}}</span>
+                          </div>
+                          <div class="page_jg_10">
+                          </div>
+                        </div>
+                        <!-- 学生作答区:作答为空也要显示"我的作答"空框(与单栏/测量容器一致 不加空值判断,否则实测高度比测量少一个空框高导致页高失真) -->
+                        <div v-if="questionItem.areaType=='studentAnswer'" :key="'a_'+index">
+                          <div class="item_question_answer">
+                            <div class="answer_title">我的作答</div>
+                            <!-- 客观题 直接显示学生选择的答案 -->
+                            <div class="answer_content" v-html="questionItem.studentAnswer" v-if="questionItem.isObjective==1">
+
+                            </div>
+                            <!-- 主观题 显示学生作答的图片 -->
+                            <!-- <div class="answer_images" v-if="questionItem.isObjective==0">
+                              <img v-for="(image, imageIndex) in questionItem.studentAnswer.split(';')" :key="'image_'+imageIndex" :src="image" alt="">
+                            </div> -->
+                            <!-- 主观题:只有真正的图片地址才渲染成 img;文本作答按文本显示,避免塞进 img src 产生 404 -->
+                            <div class="answer_images" v-if="questionItem.isObjective==0">
+                              <img v-for="(image, imageIndex) in AnswerImages(questionItem.studentAnswer)" :key="'image_'+imageIndex" :src="image" alt="">
+                              <div class="answer_content" v-if="AnswerText(questionItem.studentAnswer)" v-html="AnswerText(questionItem.studentAnswer)"></div>
+                            </div>
+                          </div>
+                          <div class="page_jg_10">
+                          </div>
+                        </div>
+                        <!-- 答案解析区 -->
+                        <div v-if="questionItem.areaType=='answerAnalysis'" :key="'p_'+index">
+                            <div class="item_question_answer" v-if="questionItem.parse">
+                              <div class="answer_title">答案解析</div>
+                              <div class="answer_content" >
+                                <span>【答案】:</span>
+                                <span v-html="questionItem.answer"></span>
+                              </div>
+                              <div class="answer_content" v-html="questionItem.parse">
+
+                              </div>
+                            </div>
+                            <div class="page_jg_10">
+                            </div>
+                        </div>
+                        </div>
+                        </div>
+                      </template>
+                    </div>
+                  </div>
+                </template>
+              </div>
+              
+              <!-- 页脚 每页都有 -->
+              <div class="page_footer_info">
+                <span>第 {{pageIndex+1}} 页</span>
+                <span>共 {{pageList.length}} 页</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        
+        <div class="slip_set_box">
+          <div class="set slip_set">
+            <el-radio-group v-model="answerRadio" @change="HandleAnswerCheckChange">
+              <el-radio :label="0">答案解析在卷底</el-radio>
+              <el-radio :label="1">答案解析在每题后</el-radio>
+            </el-radio-group>
+          </div>
+          <div class="set slip_sets">
+            <el-radio-group v-model="columnRadio" @change="HandleAnswerCheckChange">
+              <el-radio :label="1">一栏</el-radio>
+              <el-radio :label="2">二栏</el-radio>
+            </el-radio-group>
+          </div>
+          <div class="set slip_set_rank">
+            <el-checkbox-group v-model="answerCheck" @change="HandleAnswerCheckChange">
+              <el-checkbox :label="1">我的作答</el-checkbox>
+              <el-checkbox :label="2">答案解析</el-checkbox>
+              
+            </el-checkbox-group>
+          </div>
+        </div>
+        <div class="slip_close" @click="showDialog=false">
+          关闭
+        </div>
+        <div class="slip_print" @click="DownloadPdfs()">
+          下载PDF (共{{pageList.length}}页)
+        </div>
+      </div>
+      <!-- 离线测量容器:与正式页等宽同样式,但不可见;两栏时按单栏宽度测量 -->
+      <div ref="measureBox" class="measure_box" :style="{width: measureBoxWidth+'px'}">
+        <template v-for="(mItem, mIdx) in measureList">
+          <!-- 错题本顶部标题 区域 外包一层:标题+间距块整体作为测量容器的一个子元素 保持与 measureList 一一对应 -->
+          <div v-if="mItem.areaType=='pageTitle'" :key="'mt_'+mIdx">
+            <div class="page_title_info">
+              {{ mItem.title }}
+            </div>
+            <div class="page_jg_10"></div>
+          </div>
+          <div v-if="mItem.areaType=='knowledge'" :key="'mk_'+mIdx">
+            <div class="page_knowledge_info">
+              <span>{{mItem.knowledgeName}}</span>
+            </div>
+            <div class="page_jg_10"></div>
+          </div>
+          <div class="question_item" v-if="mItem.areaType=='question'" :key="'mq_'+mIdx">
+            <!-- 测量容器必须与正式渲染用同一套标题结构与样式(单栏/两栏连 DOM 结构都不同),否则测量高度与实际渲染不一致导致页高失真 -->
+            <!-- 单栏结构:左侧错题号+来源 右侧满分/得分/得分率 -->
+            <div class="item_header_one" v-if="columnRadio != 2">
+              <div class="header_left">
+                <span class="left_question_num">错题{{mItem.questionNum}}</span>
+                <span class="left_question_source">来源:{{mItem.sourceName}}</span>
+              </div>
+              <div class="header_right">
+                <span v-if="mItem.answerScore">满分:{{mItem.answerScore}}</span>
+                <span v-if="mItem.score">得分:{{mItem.score}}</span>
+                <span>得分率:{{mItem.scoreRate}}%</span>
+              </div>
+            </div>
+            <!-- 两栏结构:第一行错题号+满分/得分/得分率 第二行来源(与两栏正式渲染一致) -->
+            <div class="item_header_two" v-else>
+              <div class="header_left">
+                <span class="left_question_num">错题{{mItem.questionNum}}</span>
+                <span class="left_question_source">
+                  <span v-if="mItem.answerScore">满分:{{mItem.answerScore}}</span>
+                  <span v-if="mItem.score">得分:{{mItem.score}}</span>
+                  <span>得分率:{{mItem.scoreRate}}%</span>
+                </span>
+              </div>
+              <div class="header_right">
+                <span class="left_question_source">来源:{{mItem.sourceName}}</span>
+              </div>
+            </div>
+            <div class="page_jg_10"></div>
+            <div class="item_question_title" v-html="mItem.title"></div>
+            <div class="page_jg_10"></div>
+            <div class="item_question_knowledge">
+              <span class="knowledge_title">考查知识点:</span>
+              <span class="knowledge_list">{{(mItem.knowledgeList || []).map(k => k.knowName).filter(Boolean).join('、')}}</span>
+            </div>
+            <div class="page_jg_10"></div>
+          </div>
+          <!-- 学生作答区 -->
+          <div v-if="mItem.areaType=='studentAnswer'">
+            <div class="item_question_answer" >
+              <div class="answer_title">我的作答</div>
+              <!-- 客观题 直接显示学生选择的答案 -->
+              <div class="answer_content" v-html="mItem.studentAnswer" v-if="mItem.isObjective==1">
+
+              </div>
+              <!-- 主观题 显示学生作答的图片 -->
+              <!-- <div class="answer_images" v-if="mItem.isObjective==0">
+                <img v-for="(image, imageIndex) in mItem.studentAnswer.split(';')" :key="'image_'+imageIndex" :src="image" alt="">
+              </div> -->
+              <!-- 主观题:只有真正的图片地址才渲染成 img;文本作答按文本显示,避免塞进 img src 产生 404 -->
+              <div class="answer_images" v-if="mItem.isObjective==0">
+                <img v-for="(image, imageIndex) in AnswerImages(mItem.studentAnswer)" :key="'image_'+imageIndex" :src="image" alt="">
+                <div class="answer_content" v-if="AnswerText(mItem.studentAnswer)" v-html="AnswerText(mItem.studentAnswer)"></div>
+              </div>
+            </div>
+            <div class="page_jg_10">
+            </div>
+          </div>
+          <!-- 答案解析区 -->
+          <div v-if="mItem.areaType=='answerAnalysis'">
+              <div class="item_question_answer">
+                <div class="answer_title">答案解析</div>
+                <div class="answer_content" >
+                  <span>【答案】:</span>
+                  <span v-html="mItem.answer"></span>
+                </div>
+                <div class="answer_content" v-html="mItem.parse">
+
+                </div>
+              </div>
+              <div class="page_jg_10">
+              </div>
+          </div>
+            
+        </template>
+      </div>
+    </el-dialog>
+    <div class="page_dialog">
+      <el-dialog title="下载PDF" class="page_dialog" :visible.sync="showReportLoading"   width="500px" top="15%" append-to-body>
+         <div class="report_loading">
+            <div class="report_loading_icon">
+               <img src="@/assets/report/report_loading.png">
+            </div>
+            <div class="report_loading_title">
+               
+               正在努力生成PDF中,请稍等...
+               
+            </div> 
+            <div class="report_loading_progress">
+               <el-progress :text-inside="true" color="#2E64FA" text-color="#ffffff" :stroke-width="16" :percentage="targetProgress"></el-progress>
+            </div>
+         </div>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+
+<script>
+import { jsPDF } from "jspdf";
+import domtoimage from 'dom-to-image';
+import { mapGetters } from "vuex";
+export default {
+  props: {
+    value: {
+      type: Boolean,
+      required: true,
+    },//是否显示弹窗
+
+
+
+  },
+  components: {
+    
+  },
+  data() {
+    return {
+      answerRadio:1,//设置 0答案解析在卷底  1答案解析在每题题后 
+      answerCheck:[1,2],//1 我的作答  2 答案解析
+      columnRadio:1,//设置列数 1 栏  2 栏
+
+      checkAverage:[],//均分
+      checkFullScore:[0],//显示满分
+      pageList:[],//导出pdf的页数据
+      questionList:[],//错题数据
+      pageInfo:{
+        width:794,
+        height:1123
+      },//导出pdf页面的比例配置
+
+      showReportLoading:false,//是否显示生成pdf进度弹窗
+
+      targetProgress:0,//目标进度值
+      progressInterval:null,//用于存储定时器
+      listData:[],//成绩小条数据list
+      rowCount: 3,//默认3行
+      examName:'',//考试名称
+      subjectName:'',//科目名称
+
+      dialogKey:1,//弹窗key值
+      tagActiveName:'',//试题组合名称
+      tagActive:100,//试题组合id
+
+      pageNum:1,//当前页数
+      pageSize:10,//每页显示数量
+      total:0,//总页数
+
+      measureList:[],//离线测量用的临时列表
+
+      allData:[],//所有数据 缓存
+      loading:false,//加载中
+
+    };
+  },
+
+  computed:{//分析报告公共参数变量
+
+    ...mapGetters(["userInfo"]),
+    showDialog: {
+      get() {
+        return this.value;
+      },
+      set(val) {
+        // 当外层弹窗关闭时,确保内层弹窗也关闭
+        if (!val) {
+          this.$nextTick(() => {
+            this.showReportLoading = false;
+          });
+        }
+        this.$emit("input", val);
+      },
+    },
+
+    showAnswer()
+    {
+
+    },//是否显示我的作答
+    
+
+    studentName(){
+      return this.userInfo.userName; // 学生名称
+    },
+
+    className()
+    {
+      return this.userInfo.className || "" ; // 班级名称
+    },
+
+    schoolName(){
+      return this.userInfo.schoolName; // 学校名称
+    },
+
+    //测量容器宽度:必须与页面里块的实际可用宽度完全一致,否则图片按 max-width:100% 等比缩放、
+    //文字按不同宽度换行,测出的高度与正式渲染不一致(此前单栏用 794 测量而页面实际只有 754,块高被高估约5%);
+    //全局样式 .print_slip .area_page 有 padding:20px + box-sizing:border-box,页面内容实际宽 = 794 - 40 = 754;
+    //两栏时按单栏宽度测量:单栏宽 = (内容宽754 - 两栏间距20) / 2 = 367(与 .page_column 固定宽一致)
+    measureBoxWidth()
+    {
+      if(this.columnRadio == 2)
+      {
+        return Math.floor((this.pageInfo.width - 40 - 20) / 2);
+      }
+      return this.pageInfo.width - 40;
+    },
+
+    //每页内容区域可用高度 = 页高 - 页眉区 - 页脚保留区(与模板实际布局严格对应):
+    pageContentHeight()
+    {
+      return 1023;//固定值 已经减去页眉区和页脚保留区
+    },
+
+
+  },
+  created() 
+  {
+  },
+  beforeDestroy() 
+  {
+    // 组件销毁前清除定时器
+    if (this.progressInterval) {
+      clearInterval(this.progressInterval);
+      this.progressInterval = null;
+    }
+  },
+ 
+  methods: {
+
+    //解析学生作答内容 分离图片地址与文本:
+    //部分后端数据 isObjective 不为1 但作答内容是纯文本(如单选答案"C"),旧逻辑直接 split 后全塞给 img src,
+    //文本被当相对路径请求(GET /C 404);现在只有真正的图片地址才渲染成 img,其余按文本显示;
+    //data:base64 内联图自身含 ; 和 , 先整体提取 避免被普通分隔符拆碎;返回 { images: [...], text: '...' }
+    ParseAnswerParts(studentAnswer)
+    {
+      const images = [];
+      const texts = [];
+      let rest = String(studentAnswer || '');
+      rest = rest.replace(/data:image\/[a-zA-Z.+-]+;base64,[A-Za-z0-9+/=\s]+/g, (m) => {
+        images.push(m.replace(/\s/g, ''));
+        return '\u0001';//占位符 防止被后续分隔符拆开
+      });
+      rest.split(/[;;,,]/).forEach(part => {
+        part = part.trim();
+        if(!part || part === '\u0001') return;
+        // http(s)/blob 开头 或 以常见图片扩展名结尾 才认定为图片地址
+        if(/^(https?:|blob:)/i.test(part) || /\.(png|jpe?g|gif|bmp|webp|svg)(\?|#|$)/i.test(part))
+        {
+          images.push(part);
+        }
+        else
+        {
+          texts.push(part);
+        }
+      });
+      return { images: images, text: texts.join(',') };
+    },
+    //学生作答中的图片地址列表(模板 v-for 用)
+    AnswerImages(studentAnswer)
+    {
+      return this.ParseAnswerParts(studentAnswer).images;
+    },
+    //学生作答中的文本部分(非图片地址的内容 按文本显示 避免塞进 img src 产生 404)
+    AnswerText(studentAnswer)
+    {
+      return this.ParseAnswerParts(studentAnswer).text;
+    },
+
+    //重新处理数据
+    HandleAnswerCheckChange()
+    {
+      this.loading=true;
+      //滚动条回到顶部
+      this.$nextTick(() => {
+        if (this.$refs.printSlip) {
+          this.$refs.printSlip.scrollTop = 0;
+        }
+      });
+      this.SetListData(this.allData).then(() => {
+       
+      });
+    },
+
+    //获取错题数据
+    GetListData(params) 
+    {
+      this.dialogKey += 1;
+      this.checkFullScore = [0];
+
+      return new Promise((resolve, reject) => {
+        let param = {
+          ...params
+        };
+       this.loading=true;
+       this.$api.target.getTargetErrorExportData(param).then(res=>{
+        console.log("获取错题列表",res);
+        
+        if(res.code==200)
+        {
+          this.allData = res.data;//缓存所有数据
+          this.SetListData(res.data).then(() => {
+            resolve(res);
+          });
+
+        }
+        resolve(res);
+      })
+       
+      }).catch((err) => {
+        reject(err);
+      });
+
+      
+    },
+
+    //根据原始数据构造 list 并测量每个区域高度,回写到 list[i].height
+    SetListData(rawData)
+    {
+      return new Promise((resolve) => {
+        let list=[];
+        let pageTitleObj={
+          areaType:'pageTitle',//标题区域
+          title:this.studentName+' 个性化错题',//标题
+          height:40,//高度
+        };//页面标题
+        list.push(pageTitleObj);
+        let questionNum=1;
+        (rawData || []).forEach(item=>{
+          let obj={
+            areaType:'knowledge',//知识点名称区域
+            knowledgeName:item.knowledgeName,
+            height:0,//高度
+          };
+          list.push(obj);
+          (item.questionList || []).forEach(questionItem=>{
+            let questionObj={
+              questionNum:questionNum,//题号 按顺序
+              title:questionItem.title,//题目内容
+              knowledgeList:questionItem.knowledgeList,//知识点列表
+              sourceName:questionItem.sourceName,//题目来源
+              answerScore:questionItem.answerScore,//满分
+              score:questionItem.score,//得分
+              scoreRate:questionItem.scoreRate,//得分率
+              areaType:'question',//区域类型 题目区域
+              height:0,//高度
+            };
+            questionNum++;
+            list.push(questionObj);//插入题目
+
+            //判断是否显示我的作答区域
+            if(this.answerCheck.includes(1))
+            {
+              console.log("插入我的作答区域");
+              let answerObj={
+                areaType:'studentAnswer',//区域类型 答案区域
+                isObjective:questionItem.isObjective,//是客观题 
+                studentAnswer:questionItem.studentAnswer,//我的作答
+                height:0,//高度
+              };
+              list.push(answerObj);//插入我的作答
+            }
+
+            //判断是否显示答案解析区域
+            if(this.answerCheck.includes(2))
+            {
+
+              //判断答案在卷底 还是在每题后
+              if(this.answerRadio==1)
+              {
+                let analysisObj={
+                  areaType:'answerAnalysis',//区域类型 答案区域
+                  answer:questionItem.answer,//答案
+                  parse:questionItem.parse,//解析区域
+                  height:0,//高度
+                };
+                list.push(analysisObj);//插入我的答案解析
+                console.log("插入答案解析区域");
+              }
+              
+            }
+          });
+        });
+
+        //判断是否显示答案解析区域
+        if(this.answerCheck.includes(2))
+        {
+
+            //判断答案在卷底 
+            if(this.answerRadio==0)
+            {
+              let pageTitleObj={
+                areaType:'pageTitle',//标题区域
+                title:'答案解析',//标题
+                height:40,//高度
+              };//页面标题
+              list.push(pageTitleObj);
+              let questionNum=1;
+              (rawData || []).forEach(item=>{
+                let obj={
+                  areaType:'knowledge',//知识点名称区域
+                  knowledgeName:item.knowledgeName,
+                  height:0,//高度
+                };
+                list.push(obj);
+                (item.questionList || []).forEach(questionItem=>{
+                  let questionObj={
+    
+                    areaType:'knowledge',//知识点名称区域
+                    knowledgeName:'错题'+questionNum,
+                    height:0,//高度
+                  };
+                  questionNum++;
+                  list.push(questionObj);//插入题目
+                  let analysisObj={
+                    areaType:'answerAnalysis',//区域类型 答案区域
+                    answer:questionItem.answer,//答案
+                    parse:questionItem.parse,//解析区域
+                    height:0,//高度
+                  };
+                  list.push(analysisObj);//插入我的答案解析
+                  console.log("插入答案解析区域");
+                });
+              });
+                
+            }
+              
+        }
+        console.log("打印所有需要加载到页面的list数据", list);
+
+        //先把所有数据丢进测量容器 等dom渲染完再读高度
+        this.measureList = list;
+        this.$nextTick(() => {
+          this.$nextTick(() => {
+            // 双重 nextTick,等 v-html 完成二次渲染;再轮询等测量容器内图片全部加载完成才测量。
+            // (img 设了 height:auto 后未加载完高度为0,若提前测量会测小 → 切片窗口与正式渲染错位,出现图片被裁/空白)
+            this.WaitForImagesLoaded().then(() => {
+              const heights = this.MeasureItemHeights();
+              console.log("打印每个子元素的高度", heights);
+              // 测量容器里的元素(与 list 一一对应)切片时用它找文字行边界,避免切断文字行/产生孤行标点
+              const measureEls = this.$refs.measureBox ? Array.from(this.$refs.measureBox.children) : [];
+              // 把测量出的高度(取整)回写到每个 list item 上
+              for(let i = 0; i < list.length; i++)
+              {
+                list[i].height = Math.round(heights[i] || 0);
+                list[i].srcIndex = i;//记录在测量容器中的下标,块级诊断用它回查测量容器当前高度
+              }
+              console.log("打印填充高度后的list", list);
+
+              // 每页内容区域可用高度 固定为1023  已经减去页眉区和页脚保留区  
+              const LATER_PAGE_HEIGHT = this.pageContentHeight;     // 内容区可用高度
+              if(this.columnRadio == 2)
+              {
+                this.BuildPageListTwoColumns(list, heights, LATER_PAGE_HEIGHT, measureEls);
+              }
+              else
+              {
+                this.BuildPageList(list, heights, LATER_PAGE_HEIGHT, measureEls);
+              }
+              setTimeout(() => {
+                this.loading=false;
+              }, 2000);
+              resolve(list);
+            });
+          });
+        });
+      });
+    },
+
+    //等待测量容器内所有图片加载完成后再测量(轮询检测 + 最少等待 + 超时兜底)
+    //避免图片未加载完成时测得的高度与正式渲染不一致,导致切片错位/图片被裁/空白;
+    //img.complete=true 但 naturalWidth=0 表示加载失败,也视为就绪(不再等待)
+    WaitForImagesLoaded()
+    {
+      return new Promise(resolve => {
+        const startTime = Date.now();
+        const MIN_WAIT = 300;      // 最少等待 保证 v-html/CSS 布局稳定
+        const MAX_WAIT = 8000;     // 超时兜底 防止坏图导致无限等待
+        const timer = setInterval(() => {
+          const box = this.$refs.measureBox;
+          const imgs = box ? box.querySelectorAll('img') : [];
+          let pending = 0;
+          for(let k = 0; k < imgs.length; k++)
+          {
+            if(!imgs[k].complete) pending++;
+          }
+          const elapsed = Date.now() - startTime;
+          if((pending === 0 && elapsed >= MIN_WAIT) || elapsed >= MAX_WAIT)
+          {
+            clearInterval(timer);
+            console.log('[测量等待] 未就绪图片数=' + pending + ' 耗时=' + elapsed + 'ms' + (elapsed >= MAX_WAIT ? '(超时兜底)' : ''));
+            // 双 rAF 确保浏览器完成重排后再测量
+            requestAnimationFrame(() => {
+              requestAnimationFrame(() => resolve());
+            });
+          }
+        }, 100);
+      });
+    },
+
+    //读取测量容器里每个子元素的高度(返回整数像素值)
+    MeasureItemHeights() 
+    {
+      const box = this.$refs.measureBox;
+      if(!box) return [];
+      const children = box.children;
+      const heights = [];
+      for(let i=0; i<children.length; i++)
+      {
+        heights.push(Math.round(children[i].getBoundingClientRect().height));
+      }
+      console.log("打印每个子元素的高度", heights); 
+      return heights;
+    },
+
+    //根据测量高度按页容量分页
+    //pageHeight 后续页可用高度(px);firstPageHeight 第一页可用高度(要减去标题块占用)
+    BuildPageList(list, heights, pageHeight = 1023, measureEls = [])
+    {
+      this.pageList = [];
+      let currentPage = {
+        pageIndex: 1,
+        knowledgeName: '',
+        list: [],
+      };
+      let used = 0;
+
+      // 知识点标题下方至少保留的最小高度(避免标题孤零零贴页底)
+      const KNOWLEDGE_RESERVE = 80;
+      // 剩余空间低于该值时不接首片 直接换新页开始切(避免页底只剩一条碎内容)
+      const MIN_FIRST_SLICE = 100;
+      // 切片边界重叠高度:切点未落在安全边界(如强切图片)时,仅前一片向下延伸多显示一段内容,
+      // 避免切线正好穿过一行内容导致上页底部显示半行看不清;下一页从切点正常开始不向上重叠(避免两页重复看着像重复内容)
+      const SLICE_OVERLAP = 10;
+
+      // 创建新页的工厂方法
+      const nextPage = (pageIndex) => ({
+        pageIndex: pageIndex,
+        knowledgeName: '',
+        list: [],
+      });
+      // 当前页存档并开启新页(仅在已有内容时调用)
+      const startNewPage = () => {
+        this.pageList.push(currentPage);
+        currentPage = nextPage(currentPage.pageIndex + 1);
+        used = 0;
+      };
+
+      for(let i = 0; i < list.length; i++)
+      {
+        const item = list[i];
+        // ★ 优先使用 list 上回写的整数高度,避免 heights 与 list[i].height 不一致
+        const h = item.height || Math.round(heights[i] || 0);
+        const areaType = item.areaType;
+        const remainSpace = pageHeight - used;
+        // 可拆分模块:题目、学生作答、答案解析(切片走安全边界:文字不切半行、图片不裁切);知识点标题/页面标题按剩余空间整块判断(放得下就插 放不下换新页)
+        const splittable = (areaType === 'question' || areaType === 'studentAnswer' || areaType === 'answerAnalysis');
+
+        // 1. 可拆分块(题目/学生作答/解析):当前页剩余空间放不下就切片,能塞多少塞多少(首片填满当前页剩余 其余换新页继续)
+        if(splittable && h > remainSpace)
+        {
+          const el = (measureEls && measureEls[i]) ? measureEls[i] : null;
+          // 收集块内图片位置(相对 el 顶部),切片时保证不裁剪/不切穿图片
+          const imgRects = el ? this.GetBlockImageRects(el) : [];
+          const isImageBlock = this.IsImageBlock(el, h, imgRects);
+          // 剩余空间太少不接首片 换新页从头切(避免页底只剩一条碎内容)
+          if(currentPage.list.length > 0 && remainSpace < MIN_FIRST_SLICE)
+          {
+            startNewPage();
+          }
+          console.warn('[分页] 可拆分块按剩余空间切片:', areaType, '高度', h, '剩余空间', pageHeight - used);
+          let offset = 0;
+          let left = h;
+          let isFirst = true;
+          let forcedTimes = 0;//连续强切兜底次数(防重叠挤占空间导致 -1 反复换页死循环)
+          while(left > 0)
+          {
+            // ★ 第一片填满当前页剩余空白 后续片用整页高度(能放多少放多少);
+            // 非尾片预留底部重叠量(不安全切点时前一片向下延伸多显示一段 下一页从切点正常开始不向上重叠),
+            // 避免"内容+重叠"总高超出页高(预留后剩不下则换新页重试)
+            const sliceMax = isFirst ? (pageHeight - used) : pageHeight;
+            const isLikelyLast = left <= sliceMax;
+            const contentMax = sliceMax - (isLikelyLast ? 0 : SLICE_OVERLAP);
+            const cut = this.CalcSafeSliceHeight(el, imgRects, isImageBlock, offset, contentMax, left);
+            let sliceH = cut.h;
+            if(sliceH === -1)
+            {
+              // 当前剩余连一张完整图片都放不下:当前页已有内容就换新页重试;
+              // 空页仍 -1(重叠预留挤占所致)或单图超页高 → 强切兜底(计为不安全切点 后续靠重叠兼容)
+              if(currentPage.list.length > 0 && forcedTimes === 0)
+              {
+                startNewPage();
+                continue;
+              }
+              console.warn('[分页] 强切兜底(可用高度不足/单图超页高):', areaType, 'offset', offset);
+              sliceH = Math.min(left, Math.max(contentMax, 1));
+              cut.cutSafe = false;
+              forcedTimes++;
+            }
+            else
+            {
+              forcedTimes = 0;
+            }
+            // 尾片无需底部重叠;切点落在安全边界(文字行顶/图片底边)也无需重叠(不会出现半行字)
+            const isLastSlice = (left - sliceH) <= 0;
+            const bottomOverlap = (isLastSlice || cut.cutSafe) ? 0 : SLICE_OVERLAP;
+            const finalH = sliceH + bottomOverlap;//实际渲染高度(含底部重叠区)
+            // 切片项:模板按 sliceHeight 裁剪、向上偏移 offsetTop 显示原块的对应区间(blockHeight 保留原块测量总高供诊断);
+            // 底部重叠仅通过窗口加高向下延伸实现,下一页切片仍从切点正常开始(不向上重叠 避免两页都显示重叠区看着像重复)
+            currentPage.list.push({ ...item, isSlice: true, sliceHeight: finalH, offsetTop: offset, height: finalH, blockHeight: h });
+            used += finalH;
+            offset += sliceH;
+            left = h - offset;
+            isFirst = false;
+            // 内容没切完 → 换新页继续放剩余切片(新页能放多少放多少)
+            if(left > 0) startNewPage();
+          }
+          continue;
+        }
+
+        // 2. 不可拆分块:按剩余空间判断 放得下就插入 放不下换新页插入(换页后仍超页高的整块放入 超出由内容区 overflow:hidden 裁掉)
+        if(areaType === 'knowledge' && currentPage.list.length > 0 && (pageHeight - used) - h < KNOWLEDGE_RESERVE)
+        {
+          // 知识点标题:放入后下方保留不足 KNOWLEDGE_RESERVE 就换页(避免标题贴页底)
+          startNewPage();
+        }
+        else if(currentPage.list.length > 0 && h > remainSpace)
+        {
+          if(h > pageHeight)
+          {
+            console.warn('[分页] 单块超页高且不可拆分:', areaType, '高度', h, ',整块换新页(超出部分裁掉)');
+          }
+          startNewPage();
+        }
+
+        currentPage.list.push(item);
+        used += h;
+      }
+
+      // 最后一页收尾
+      if(currentPage.list.length > 0)
+      {
+        this.pageList.push(currentPage);
+      }
+
+      // 末页防孤儿:内容不足 1/3 页时把上一页尾部整块移入末页平衡(不拆切片)
+      this.RebalanceLastPage(pageHeight);
+
+      console.log('[分页结果] 共', this.pageList.length, '页', this.pageList.map(p => ({
+        pageIndex: p.pageIndex,
+        itemCount: p.list.length,
+        //每页内容总高度(切片按 sliceHeight 计)不含页眉,用于与实际渲染高度对比
+        contentHeight: (p.list || []).reduce((s, it) => s + (it.isSlice ? it.sliceHeight : (it.height || 0)), 0),
+        //每页块构成:定位"哪页为什么空"(看尾部是什么块、剩余空间是多少)
+        blocks: (p.list || []).map(it => it.areaType + (it.isSlice ? '(切片' + it.sliceHeight + ')' : '(' + (it.height || 0) + ')')).join(' + '),
+      })));
+      // 渲染完成后对比 计算高度 vs 实际DOM高度(页级 + 块级)
+      this.DebugComparePageHeights();
+      this.DebugCompareBlockHeights();
+    },
+
+    //计算安全切片高度(图片与HTML文字统一逻辑 能放多少放多少):
+    //el 测量容器原块;imgRects 块内图片位置;offset 已切到的位置;sliceMax 本次内容可用高度;remain 剩余未切内容高度;
+    //返回值:{ h: 本次切片内容高度, cutSafe: 切点是否落在安全边界 };h=-1 表示可用高度较小且连一张完整图片都放不下 由调用方换新页重试;
+    //cutSafe=true(文字行边界/图片底边/最后一片)上下页边界内容完整无需重叠;false(裁图强切/兜底强切)切点穿过内容中间,
+    //由调用方给前一片底部延伸重叠多显示一段,避免上页底部显示半行看不清;
+    //切点规则:图片区优先切在图片底边(不裁图),但安全切点会留下过多空白(> IMG_GAP_TOLERANCE)时
+    //允许裁切图片填满剩余空间(填满优先 避免页底大片空白);文字区切在行边界(不切半行字/标点孤行推下页)
+    CalcSafeSliceHeight(el, imgRects, isImageBlock, offset, sliceMax, remain)
+    {
+      let sliceH = Math.min(remain, sliceMax);
+      // 最后一片不需要切 直接返回(不产生边界空白)
+      if(sliceH >= remain) return { h: sliceH, cutSafe: true };
+      // 当前切片区间内是否含图片(尾部纯文字区间走文字行边界逻辑)
+      const hasImgInSlice = isImageBlock && imgRects.some(r => r.bottom > offset + 1 && r.top < offset + sliceMax);
+      if(hasImgInSlice)
+      {
+        // 图片内容:可用高度内能完整放下的图片尽量塞满 切点落在图片底边
+        const imgCut = this.FindImageCut(imgRects, offset, offset + sliceMax);
+        const safeH = imgCut > offset ? (imgCut - offset) : 0;
+        // 安全切点留下的空白超过容忍值(或一张完整图都放不下但可用高度不小)→ 裁切图片填满剩余空间 避免页底大片空白(切点穿过图片 不安全)
+        const IMG_GAP_TOLERANCE = 150;
+        if(sliceMax - safeH > IMG_GAP_TOLERANCE)
+        {
+          console.warn('[切片] 图片安全切点仅放下', safeH, 'px,会留', sliceMax - safeH, 'px 空白 → 裁切图片填满剩余', sliceMax, 'px');
+          return { h: sliceH, cutSafe: false };
+        }
+        if(safeH > 0) return { h: safeH, cutSafe: true };
+        // 一张完整图都放不下且可用高度较小 → 换页(不产生裁图小碎条 页底留少量空白可接受)
+        return { h: -1, cutSafe: false };
+      }
+      if(el)
+      {
+        // 文字内容:把切点调整到文字行边界 标点孤行推到下一页(页底会留一点空白属正常)
+        const safe = this.AdjustSliceBoundary(el, offset + sliceH);
+        if(safe - offset > 60)
+        {
+          sliceH = safe - offset;
+        }
+      }
+      // 兜底:边界调整后切片高度异常 强制按最大高度推进 防止死循环(切点位置不可控 标记不安全)
+      if(sliceH <= 0)
+      {
+        return { h: Math.min(remain, sliceMax), cutSafe: false };
+      }
+      return { h: sliceH, cutSafe: true };
+    },
+
+    //末页防孤儿处理:最后一页内容太少(不足页高 1/3)时,从上一页尾部把整块依次移到末页,
+    //让最后两页内容均衡,避免出现只剩几十像素的"空页";切片不搬动(会破坏顺序);
+    //搬动后若上一页被搬空则移除该页;同时保证上一页不被搬得过空(至少保留页高 1/4)
+    RebalanceLastPage(pageHeight)
+    {
+      if(this.pageList.length < 2) return;
+      const sumH = p => (p.list || []).reduce((s, it) => s + (it.isSlice ? it.sliceHeight : (it.height || 0)), 0);
+      const last = this.pageList[this.pageList.length - 1];
+      let lastH = sumH(last);
+      // 末页内容已足够 → 不处理;末页含切片时无法搬动(会破坏切片顺序)→ 跳过并告警原因,交给切片时的尾片保护处理;
+      // 诊断日志:输出末页每个块的构成,方便定位孤儿页里到底是什么内容(切片/知识点标题/普通块)
+      console.log('[末页构成] 高度', Math.round(lastH), 'px,包含:', (last.list || []).map(it => ({
+        areaType: it.areaType,
+        isSlice: !!it.isSlice,
+        height: it.isSlice ? it.sliceHeight : (it.height || 0),
+      })));
+      if(lastH >= pageHeight / 3) return;
+      if((last.list || []).some(it => it.isSlice))
+      {
+        console.log('[防孤儿页] 末页含切片,无法通过搬运平衡(不改变切片顺序),保持现状');
+        return;
+      }
+      const prev = this.pageList[this.pageList.length - 2];
+      const targetH = Math.min(pageHeight / 2, 400); // 末页至少填到的高度目标(两者取小 避免超页)
+      console.log('[防孤儿页] 末页仅', Math.round(lastH), 'px,从前一页搬运整块平衡');
+      while(prev.list.length > 0 && lastH < targetH)
+      {
+        const item = prev.list[prev.list.length - 1];
+        if(item.isSlice) break; // 遇到切片停止 避免破坏切片顺序
+        const itemH = item.height || 0;
+        if(lastH + itemH > pageHeight) break; // 末页放不下也不搬(保持顺序 不能跳过它搬更前面的)
+        prev.list.pop();
+        last.list.unshift(item);
+        lastH += itemH;
+        if(sumH(prev) < pageHeight / 4) break; // 上一页不能被搬得过空 否则孤儿页只是转移了
+      }
+      // 上一页被搬空 → 移除该页;最后统一重排页码(页脚/日志里的页码保持一致)
+      if(prev.list.length === 0)
+      {
+        this.pageList.splice(this.pageList.length - 2, 1);
+      }
+      for(let i = 0; i < this.pageList.length; i++)
+      {
+        this.pageList[i].pageIndex = i + 1;
+      }
+    },
+
+    //两栏分页:一页分左/右两栏,先填左栏再填右栏,两栏都放不下才换页
+    //columnHeight 栏可用高度(px);通栏标题(pageTitle)强制换页并占据页顶,栏可用高度需减去它占的高度
+    BuildPageListTwoColumns(list, heights, columnHeight = 1023, measureEls = [])
+    {
+      this.pageList = [];
+
+      // 知识点标题下方至少保留的最小高度
+      const KNOWLEDGE_RESERVE = 80;
+
+      // 创建新页的工厂方法(通栏标题区 + 左右两栏)
+      const nextPage = (pageIndex) => ({
+        pageIndex: pageIndex,
+        fullList: [],//通栏区域(pageTitle)
+        topHeight: 0,//通栏标题已占用高度(栏可用高度要减去它)
+        columns: [[], []],//左栏 右栏
+      });
+
+      let page = nextPage(1);
+      let used = [0, 0];//左/右栏已用高度
+      let cur = 0;//当前正在填的栏 0左 1右
+
+      const pushPage = () => {
+        this.pageList.push(page);
+        page = nextPage(page.pageIndex + 1);
+        used = [0, 0];
+        cur = 0;
+      };
+
+      for(let i = 0; i < list.length; i++)
+      {
+        const item = list[i];
+        // ★ 优先使用 list 上写的整数高度,避免 heights 与 list[i].height 不一致
+        const h = item.height || Math.round(heights[i] || 0);
+        const areaType = item.areaType;
+
+        // 1. 通栏标题:强制换页并独占页顶(如卷底的"答案解析"标题)
+        if(areaType === 'pageTitle')
+        {
+          if(page.fullList.length > 0 || page.columns[0].length > 0 || page.columns[1].length > 0)
+          {
+            pushPage();
+          }
+          page.fullList.push(item);
+          page.topHeight += h + 10;//标题高度 + 底部外边距10px
+          continue;
+        }
+
+        // 当前页栏可用高度(减去通栏标题占用)
+        const maxH = columnHeight - page.topHeight;
+
+        // 超高块处理:避免被页容器 overflow:hidden 裁掉丢失内容;题目/学生作答/解析可拆分 其他模块整块换新页(放不下就放入 超出裁掉)
+        const splittable = (areaType === 'question' || areaType === 'studentAnswer' || areaType === 'answerAnalysis');
+        if(h > maxH && !splittable)
+        {
+          console.warn('[两栏分页] 单块超栏高且不可拆分:', areaType, '高度', h, ',整块换新页(超出部分裁掉)');
+          pushPage();
+          page.columns[0].push(item);
+          used[0] += h;
+          cur = 0;
+          continue;
+        }
+        // 可拆分块(题目/学生作答/解析):当前栏剩余空间放不下就切片 能塞多少塞多少(首片填当前栏 续片优先填同页右栏 右栏切完才换新页 避免右栏空白)
+        const colRemain = maxH - used[cur];
+        if(splittable && h > colRemain)
+        {
+          const el = (measureEls && measureEls[i]) ? measureEls[i] : null;
+          const imgRects = el ? this.GetBlockImageRects(el) : [];
+          const isImageBlock = this.IsImageBlock(el, h, imgRects);
+          // 当前栏剩余空白不足这么多时 就不在当前栏底塞碎片 优先整块挪到右栏/换新页处理;
+          // 注意 cur==1 时不能回退左栏(左栏内容已排在前面 会破坏阅读顺序)
+          const MIN_FIRST_SLICE = 100;
+          let sliceCol = cur;
+          let sliceRemain = colRemain;
+          if(sliceRemain < MIN_FIRST_SLICE)
+          {
+            if(cur === 0 && h <= maxH - used[1])
+            {
+              // 右栏空且整块放得下 → 不切片直接整块放右栏(旧逻辑这里会换页 导致左栏底部剩余空间留白)
+              page.columns[1].push(item);
+              used[1] += h;
+              cur = 1;
+              continue;
+            }
+            if(h <= columnHeight)
+            {
+              // 新页能放下:直接换页放入左栏(当前页放不下只是因为通栏标题占了空间)
+              pushPage();
+              page.columns[0].push(item);
+              used[0] += h;
+              cur = 0;
+              continue;
+            }
+            pushPage();
+            sliceCol = 0;
+            sliceRemain = columnHeight;
+          }
+          // 切成多个切片:首片填当前栏剩余 续片优先填同页右栏(阅读顺序仍先左后右)右栏切完才换新页左栏;
+          // 旧逻辑续片一律独占新页左栏,导致切片期间每页右栏全空白且总页数翻倍;
+          // 每片实际占栏高度都要计入容量(含底部重叠),否则栏内容总高超栏高会被 overflow:hidden 裁掉底部内容(右栏空白诱因之一)
+          console.warn('[两栏分页] 可拆分块按剩余空间切片:', areaType, '高度', h, '栏剩余', sliceRemain);
+          // 切片边界重叠:切点不安全(强切图片)时,仅前一片向下延伸多显示一段,避免切线穿过内容中间导致上页/上栏底部半行看不清;下一页从切点正常开始不向上重叠(与单栏 BuildPageList 同一策略)
+          const SLICE_OVERLAP = 10;
+          let offset = 0;
+          let left = h;
+          let advanceTimes = 0;//因 -1 向更大容量位置推进的次数(容量单调递增 最多两次:左栏→右栏→新页 防死循环)
+          while(left > 0)
+          {
+            // 非尾片预留底部重叠量(前一片向下延伸),避免"内容+重叠"总高超出栏高
+            const isLikelyLast = left <= sliceRemain;
+            const contentMax = sliceRemain - (isLikelyLast ? 0 : SLICE_OVERLAP);
+            const cut = this.CalcSafeSliceHeight(el, imgRects, isImageBlock, offset, contentMax, left);
+            let sliceH = cut.h;
+            if(sliceH === -1)
+            {
+              // 连一张完整图片都放不下:当前位置容量小于整栏高就推进到更大容量位置重试;否则强切兜底(防止死循环)
+              if(sliceRemain < columnHeight && advanceTimes < 2)
+              {
+                advanceTimes++;
+                if(sliceCol === 0)
+                {
+                  sliceCol = 1;
+                  sliceRemain = maxH - used[1];
+                }
+                else
+                {
+                  pushPage();
+                  sliceCol = 0;
+                  sliceRemain = columnHeight;
+                }
+                continue;
+              }
+              console.warn('[两栏分页] 强切兜底(可用高度不足/单图超栏高):', areaType, 'offset', offset);
+              sliceH = Math.min(left, Math.max(contentMax, 1));
+              cut.cutSafe = false;
+            }
+            // 尾片无需底部重叠;安全边界切点也无需重叠(不会出现半行内容)
+            const isLastSlice = (left - sliceH) <= 0;
+            const bottomOverlap = (isLastSlice || cut.cutSafe) ? 0 : SLICE_OVERLAP;
+            const finalH = sliceH + bottomOverlap;//实际渲染高度(含底部重叠区)
+            page.columns[sliceCol].push({ ...item, isSlice: true, sliceHeight: finalH, offsetTop: offset, height: finalH, blockHeight: h });
+            used[sliceCol] += finalH;
+            offset += sliceH;
+            left = h - offset;
+            // 内容没切完 → 推进下一位置:左栏切的优先进同页右栏(填满右栏避免空白),右栏切的换新页左栏继续;
+            // 尾片落在哪一栏,后续块就从哪一栏继续往下填(另一栏剩余空间留给后续块均衡)
+            if(left > 0)
+            {
+              if(sliceCol === 0)
+              {
+                sliceCol = 1;
+                sliceRemain = maxH - used[1];
+              }
+              else
+              {
+                pushPage();
+                sliceCol = 0;
+                sliceRemain = columnHeight;
+              }
+            }
+            else
+            {
+              cur = sliceCol;
+            }
+          }
+          continue;
+        }
+
+        // 2. 普通块放置:优先两栏中剩余空间较小的一栏(先填浅的 让两栏趋于均衡 避免一侧过早填满被迫换页另一侧留白);
+        // 阅读顺序先左栏后右栏:右栏已有内容后不能再放回左栏(会破坏顺序 只能继续填右栏)
+        const knowledge = (areaType === 'knowledge');
+        const limitOf = c => knowledge ? Math.max(maxH - KNOWLEDGE_RESERVE, 0) : maxH;
+        const fits = (c) => page.columns[c].length === 0 || used[c] + h <= limitOf(c);
+        const order = page.columns[1].length > 0 ? [1] : ((maxH - used[0]) <= (maxH - used[1]) ? [0, 1] : [1, 0]);
+        let placed = false;
+        for(let k = 0; k < order.length && !placed; k++)
+        {
+          const c = order[k];
+          if(fits(c))
+          {
+            page.columns[c].push(item);
+            used[c] += h;
+            cur = c;
+            placed = true;
+          }
+        }
+
+        // 3. 当前页两栏都放不下 → 换页放入新页左栏(进入此分支的块高必然 <= 栏高 能完整放下,
+        // 避免强塞当前页剩余较大栏导致尾部被栏容器裁掉丢失内容)
+        if(!placed)
+        {
+          pushPage();
+          page.columns[0].push(item);
+          used[0] += h;
+          cur = 0;
+        }
+      }
+
+      // 最后一页收尾
+      if(page.fullList.length > 0 || page.columns[0].length > 0 || page.columns[1].length > 0)
+      {
+        this.pageList.push(page);
+      }
+
+      console.log('[两栏分页结果] 共', this.pageList.length, '页', this.pageList.map(p => ({
+        pageIndex: p.pageIndex,
+        leftCount: p.columns[0].length,
+        rightCount: p.columns[1].length,
+        //每页内容总高度 = 通栏区 + 较高的一栏(不含页眉),用于与实际渲染高度对比
+        contentHeight: (p.fullList || []).reduce((s, it) => s + (it.height || 0), 0)
+          + Math.max(...p.columns.map(col => col.reduce((s, it) => s + (it.isSlice ? it.sliceHeight : (it.height || 0)), 0))),
+        //每页栏内块构成:定位"哪页为什么空"
+        left: (p.columns[0] || []).map(it => it.areaType + (it.isSlice ? '(切片' + it.sliceHeight + ')' : '(' + (it.height || 0) + ')')).join(' + '),
+        right: (p.columns[1] || []).map(it => it.areaType + (it.isSlice ? '(切片' + it.sliceHeight + ')' : '(' + (it.height || 0) + ')')).join(' + '),
+      })));
+      // 渲染完成后对比 计算高度 vs 实际DOM高度(页级 + 块级)
+      this.DebugComparePageHeights();
+      this.DebugCompareBlockHeights();
+    },
+
+    //汇总每页的“计算内容总高度”(不含页眉):单栏 = 块高之和;两栏 = 通栏区 + 较高一栏之和;切片按 sliceHeight 计
+    CalcPageExpectedHeights()
+    {
+      return this.pageList.map(p => {
+        if(p.columns)
+        {
+          const fullH = (p.fullList || []).reduce((s, it) => s + (it.height || 0), 0);
+          const colH = Math.max(...p.columns.map(col => col.reduce((s, it) => s + (it.isSlice ? it.sliceHeight : (it.height || 0)), 0)));
+          return fullH + colH;
+        }
+        return (p.list || []).reduce((s, it) => s + (it.isSlice ? it.sliceHeight : (it.height || 0)), 0);
+      });
+    },
+
+    //诊断:对比每页“分页计算的内容总高度”与“页面实际渲染高度”,定位高度失真的页/块;
+    //两者均不含页眉(页眉高度固定 直接扣除);差值 = 实际 - 计算,正数表示实际比算的多(内容被低估)
+    DebugComparePageHeights()
+    {
+      this.$nextTick(() => {
+        this.$nextTick(() => {
+          setTimeout(() => {
+            const expected = this.CalcPageExpectedHeights();
+            const pages = document.querySelectorAll('.area_page');
+            const rows = [];
+            for(let i = 0; i < pages.length; i++)
+            {
+              const pageEl = pages[i];
+              // 内容区是固定高度包裹层(.page_area_content height:1023 overflow:hidden):
+              // ① 基准线取包裹层顶部(紧随页眉),不能再像旧逻辑那样把页眉的下一个兄弟元素底边当基准(那个兄弟现在就是包裹层自己,会把基准拉到内容区底部,实际高度恒算成0)
+              // ② 包裹层自身高度固定,测它没意义,必须钻进包裹层内测子元素的最大底边(子元素 offsetTop 相对 .area_page,position:relative,与基准线可直接比较)
+              const contentBox = pageEl.querySelector('.page_area_content');
+              const scopeEl = contentBox || pageEl;
+              let baseTop = 0;
+              if(contentBox)
+              {
+                baseTop = contentBox.offsetTop;
+              }
+              else
+              {
+                const header = pageEl.querySelector('.page_header_info');
+                if(header) baseTop = header.offsetTop + header.offsetHeight;
+              }
+              // 实际内容底部 = 包裹层内子元素的最大底边(两栏时 page_columns_wrap 的高度即较高一栏);
+              // 跳过绝对/固定定位元素(如页脚 .page_footer_info 是 absolute 钉在页底,会把底部恒拉到整页高)
+              let maxBottom = 0;
+              for(let c = 0; c < scopeEl.children.length; c++)
+              {
+                const child = scopeEl.children[c];
+                const pos = window.getComputedStyle(child).position;
+                if(pos === 'absolute' || pos === 'fixed') continue;
+                maxBottom = Math.max(maxBottom, child.offsetTop + child.offsetHeight);
+              }
+              const actual = Math.round(Math.max(0, maxBottom - baseTop));
+              const exp = Math.round(expected[i] || 0);
+              rows.push({ 页码: i + 1, 计算高度: exp, 实际高度: actual, 差值: actual - exp });
+              // 差值异常时输出该页内容区每个子元素的高度明细,定位是哪个块失真(跳过定位元素)
+              if(Math.abs(actual - exp) > 20)
+              {
+                const details = [];
+                for(let c = 0; c < scopeEl.children.length; c++)
+                {
+                  const child = scopeEl.children[c];
+                  const pos = window.getComputedStyle(child).position;
+                  if(pos === 'absolute' || pos === 'fixed') continue;
+                  details.push({
+                    序号: c,
+                    类名: (child.className || '').toString().slice(0, 40),
+                    offsetTop: Math.round(child.offsetTop),
+                    高度: Math.round(child.offsetHeight),
+                    底边: Math.round(child.offsetTop + child.offsetHeight),
+                  });
+                }
+                console.log('[页高对比-明细] 第' + (i + 1) + '页 内容区子元素高度明细(基准线=' + Math.round(baseTop) + '):');
+                console.table(details);
+              }
+            }
+            console.log('[页高对比] 计算高度=分页时累加的块高;实际高度=渲染后DOM实测;差值≠0 的页就是高度失真点');
+            console.table(rows);
+          }, 800); // 等渲染与图片加载稳定后再测
+        });
+      });
+    },
+
+    //块级诊断:逐块比对"测量容器测得的高度"与"页面实际渲染的高度",定位哪些块被高估/低估;
+    //切片块额外检查切片窗口尾部空白(测量高度 > 实际块高时,尾片底部就会有一段空白);
+    //页高对比只能看到包裹层外壳高度(切片外壳恒等于 sliceHeight),看不到内部失真,所以要钻进块内比对;
+    //注意:该诊断在渲染后 1500ms 执行,若页面图片仍未加载完会出现假阳性(实际高度偏小),以稳定后的结果为准;
+    //只适用于一栏布局(两栏结构不同 跳过)
+    DebugCompareBlockHeights()
+    {
+      if(this.columnRadio == 2) return;
+      this.$nextTick(() => {
+        this.$nextTick(() => {
+          setTimeout(() => {
+            const rows = [];
+            const subRows = [];
+            // 下钻一层:把页面块与测量容器块的顶层子元素逐一对比宽/高,定位到底是哪类元素(图片/表格/文字段)两边渲染尺寸不同;
+            // 宽不同 → 可用宽度不一致(图片按比例缩放高度也跟着变);宽相同高不同 → 行高/字体/内部边距差异;
+            // 只递归首层差异子元素一次(通常是 .item_question_title 这类 v-html 容器),避免输出爆炸;
+            // 注意:页面侧被切片包裹的块与测量容器块的模板结构一致 子元素可按序对齐;若两边子元素数不同会在备注里标出(按序对齐可能错位)
+            const diffChildren = (pageBlock, mBlock, pageIdx, depth) => {
+              if(!pageBlock || !mBlock || depth > 1) return;
+              const pk = pageBlock.children;
+              const mk = mBlock.children;
+              if(pk.length === 0) return;
+              const note = pk.length !== mk.length ? '(子元素数不同' + pk.length + '/' + mk.length + ' 对齐可能错位)' : '';
+              const n = Math.min(pk.length, mk.length);
+              for(let c2 = 0; c2 < n; c2++)
+              {
+                const pr = pk[c2].getBoundingClientRect();
+                const mr = mk[c2].getBoundingClientRect();
+                if(Math.abs(pr.height - mr.height) <= 2 && Math.abs(pr.width - mr.width) <= 2) continue;
+                const name = ((pk[c2].className && pk[c2].className.toString) ? pk[c2].className.toString() : pk[c2].tagName).slice(0, 40);
+                subRows.push({
+                  页: pageIdx,
+                  层级: depth,
+                  子元素: name + note,
+                  测量宽: Math.round(mr.width),
+                  页面宽: Math.round(pr.width),
+                  测量高: Math.round(mr.height),
+                  页面高: Math.round(pr.height),
+                  高差: Math.round(pr.height - mr.height),
+                });
+                // 只对首个有差异的子元素下钻一层(v-html 容器内部)
+                diffChildren(pk[c2], mk[c2], pageIdx, depth + 1);
+                break;
+              }
+            };
+            const pages = document.querySelectorAll('.area_page');
+            for(let i = 0; i < pages.length && i < this.pageList.length; i++)
+            {
+              const contentBox = pages[i].querySelector('.page_area_content');
+              const list = this.pageList[i].list || [];
+              if(!contentBox || contentBox.children.length !== list.length) continue;
+              for(let k = 0; k < list.length; k++)
+              {
+                const it = list[k];
+                const wrap = contentBox.children[k];
+                const inner = wrap.firstElementChild;
+                if(!inner || !inner.firstElementChild) continue;
+                const block = inner.firstElementChild;//真正的块元素(题目/解析/作答/知识点...)
+                // 此刻测量容器里同一个块的高度:用于判决失真来源——
+                // 测量现在也变矮了 ≈ 实际 → 时序问题(测量完成后两边都变了,如字体延迟加载);
+                // 测量现在仍是高的 → 环境差异(测量容器与正式页渲染不一致)
+                const mBox = this.$refs.measureBox;
+                const mEl = (mBox && it.srcIndex != null && mBox.children[it.srcIndex]) ? mBox.children[it.srcIndex] : null;
+                const measureNowH = mEl ? Math.round(mEl.getBoundingClientRect().height) : -1;
+                if(it.isSlice)
+                {
+                  // 块实际渲染高度(切片把块整体上移 offsetTop,负 margin 不影响 offsetHeight 的取值)
+                  const renderedH = Math.round(block.offsetHeight);
+                  const storedTotal = it.blockHeight || it.height || 0;//原块测量总高(height 字段已被切片高度覆盖 优先用 blockHeight)
+                  // 尾片:切片窗口覆盖到块末尾(offsetTop + sliceHeight ≈ 块测量总高);
+                  // 若块实际高度 < 测量总高,窗口底部就会多出一段空白(即用户看到的"考查知识点下面空白")
+                  const isLastSlice = (it.offsetTop + it.sliceHeight) >= storedTotal - 1;
+                  const tailBlank = isLastSlice ? (it.offsetTop + it.sliceHeight) - renderedH : 0;
+                  if(Math.abs(renderedH - storedTotal) > 2 || tailBlank > 2)
+                  {
+                    rows.push({
+                      页: i + 1,
+                      类型: it.areaType + '(切片)',
+                      测量块总高: storedTotal,
+                      实际块总高: renderedH,
+                      差值: renderedH - storedTotal,
+                      测量容器当前高: measureNowH,
+                      切片区间: it.offsetTop + '~' + (it.offsetTop + it.sliceHeight),
+                      窗口尾部空白: Math.round(tailBlank),
+                      失真来源: Math.abs(measureNowH - renderedH) <= 2 ? '时序(测量后又变了)' : (Math.abs(measureNowH - storedTotal) <= 2 ? '环境差异(两容器渲染不同)' : '两者兼有'),
+                    });
+                    diffChildren(block, mEl, i + 1, 0);
+                  }
+                }
+                else
+                {
+                  const renderedH = Math.round(wrap.offsetHeight);
+                  const storedH = it.height || 0;
+                  if(Math.abs(renderedH - storedH) > 2)
+                  {
+                    rows.push({
+                      页: i + 1,
+                      类型: it.areaType,
+                      测量块总高: storedH,
+                      实际块总高: renderedH,
+                      差值: renderedH - storedH,
+                      测量容器当前高: measureNowH,
+                      失真来源: Math.abs(measureNowH - renderedH) <= 2 ? '时序(测量后又变了)' : (Math.abs(measureNowH - storedH) <= 2 ? '环境差异(两容器渲染不同)' : '两者兼有'),
+                    });
+                    diffChildren(block, mEl, i + 1, 0);
+                  }
+                }
+              }
+            }
+            if(rows.length === 0)
+            {
+              console.log('[块级对比] 所有块的测量高度与实际渲染一致,无失真块');
+            }
+            else
+            {
+              console.log('[块级对比] 差值=实际-测量:负数=测量高估(切片窗口尾部会多出这段空白)');
+              console.table(rows);
+            }
+            if(subRows.length > 0)
+            {
+              console.log('[块级对比-子元素明细] 定位两个容器里哪类元素尺寸不同:宽不同=可用宽度不一致;宽相同高不同=行高/字体/内部边距差异');
+              console.table(subRows);
+              // 祖先链宽度追踪:从失真的子元素逐级向上看每一层宽度,找到宽度变窄的那一层(即谁吃了 40px);
+              // 同时输出该层的 computed padding/margin/box-sizing,直接看到是哪条样式造成的;只追踪第一个失真块避免刷屏;
+              // 测量容器侧也同步输出对应链路的宽度作为对照;若两边在某一层宽度开始分岁,问题就在该层或其直接父级;
+              const firstSub = subRows[0];
+              const firstPage = document.querySelector('.area_page .page_area_content');
+              const firstBlock = firstPage && firstPage.children[0] && firstPage.children[0].firstElementChild
+                ? firstPage.children[0].firstElementChild.firstElementChild : null;
+              const walkUp = (el, label) => {
+                const chain = [];
+                let cur = el;
+                let hop = 0;
+                while(cur && hop < 8)
+                {
+                  const cs = window.getComputedStyle(cur);
+                  chain.push({
+                    来源: label,
+                    层级: hop,
+                    元素: (cur.className && cur.className.toString ? cur.className.toString().slice(0, 30) : cur.tagName),
+                    宽度: Math.round(cur.getBoundingClientRect().width),
+                    paddingLeft: cs.paddingLeft,
+                    paddingRight: cs.paddingRight,
+                    marginLeft: cs.marginLeft,
+                    marginRight: cs.marginRight,
+                    boxSizing: cs.boxSizing,
+                  });
+                  cur = cur.parentElement;
+                  hop++;
+                }
+                return chain;
+              };
+              if(firstBlock)
+              {
+                console.log('[祖先链宽度-页面侧] 从块往上逐层看宽度,找到变窄的那一层就是 40px 的去向:');
+                console.table(walkUp(firstBlock, '页面'));
+              }
+              const mBoxEl = this.$refs.measureBox;
+              const mFirstBlock = mBoxEl && mBoxEl.children[0] ? mBoxEl.children[0] : null;
+              if(mFirstBlock)
+              {
+                console.log('[祖先链宽度-测量侧] 对照:');
+                console.table(walkUp(mFirstBlock, '测量'));
+              }
+            }
+          }, 1500);
+        });
+      });
+    },
+
+    //收集块内所有图片的垂直位置(相对 el 顶部 按 top 升序),切片时用于按图片边界切分、避免裁剪图片
+    GetBlockImageRects(el)
+    {
+      const rects = [];
+      try
+      {
+        const elRect = el.getBoundingClientRect();
+        const imgs = el.querySelectorAll('img');
+        for(let k = 0; k < imgs.length; k++)
+        {
+          const r = imgs[k].getBoundingClientRect();
+          if(r.height <= 0) continue;
+          rects.push({ top: r.top - elRect.top, bottom: r.bottom - elRect.top });
+        }
+        rects.sort((a, b) => a.top - b.top);
+      }
+      catch(e)
+      {
+        console.warn('[图片位置收集失败]', e);
+      }
+      return rects;
+    },
+
+    //判断块是否以图片内容为主(图片总高度占比过半 → 按图片边界切分 否则按文字行边界切分)
+    IsImageBlock(el, blockHeight, imgRects)
+    {
+      if(!el || !imgRects || imgRects.length === 0 || blockHeight <= 0) return false;
+      let total = 0;
+      for(const r of imgRects) total += (r.bottom - r.top);
+      return total >= blockHeight * 0.5;
+    },
+
+    //为图片内容找安全切点:在 (offset, maxCut] 内返回最大可用切点,要求:
+    //① 切在图片底边(能完整放下的图片尽量塞满 如能放下2张就切在第2张底边)
+    //② 不得切穿任何图片中间(包括并排图片 只要某图 top < 切点 < bottom 即视为切穿)
+    //第一张图片都无法完整放下时返回 -1 由调用方决定换页 不返回部分高度 避免裁图;
+    //offset、maxCut、返回值均为相对 el 顶部的坐标
+    FindImageCut(imgRects, offset, maxCut)
+    {
+      const rects = imgRects || [];
+      // 候选切点:区间内能完整放下的图片底边(去重) 从大到小尝试(优先塞满)
+      const candidates = [];
+      for(const r of rects)
+      {
+        if(r.bottom > offset + 1 && r.bottom <= maxCut + 1 && candidates.indexOf(r.bottom) < 0)
+        {
+          candidates.push(r.bottom);
+        }
+      }
+      candidates.sort((a, b) => b - a);
+      for(const c of candidates)
+      {
+        // 检查该切点是否会切穿某张图片(并排图片常见)
+        let crossed = false;
+        for(const r of rects)
+        {
+          if(r.top < c && c < r.bottom)
+          {
+            crossed = true;
+            break;
+          }
+        }
+        if(!crossed) return c;
+      }
+      // 区间内没有能完整放下的图片 → 返回 -1 交由调用方换页处理(不裁图)
+      return -1;
+    },
+
+    //安全化切片边界:不切在文字行中间;若切点上方最后一行只剩标点(孤行)则把该行推到下一页
+    //el 测量容器里的原块元素;boundary 期望切点(相对 el 顶部 px);返回调整后的切点(只会上移不会下移)
+    AdjustSliceBoundary(el, boundary)
+    {
+      try
+      {
+        const elRect = el.getBoundingClientRect();
+        const range = document.createRange();
+        // 收集块内所有文字行位置(只统计文本节点 忽略图片等替换元素)
+        const rects = [];
+        const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
+        let node;
+        while((node = walker.nextNode()))
+        {
+          if(!(node.nodeValue || '').trim()) continue;
+          range.selectNodeContents(node);
+          const rs = range.getClientRects();
+          for(let r = 0; r < rs.length; r++)
+          {
+            if(rs[r].width > 0) rects.push(rs[r]);
+          }
+        }
+        if(rects.length === 0) return boundary;
+        // 按行聚合(top 容差3px)转成相对 el 顶部的坐标
+        const lines = [];
+        rects.sort((a, b) => a.top - b.top);
+        for(const r of rects)
+        {
+          const relTop = r.top - elRect.top;
+          const relBottom = r.bottom - elRect.top;
+          const last = lines[lines.length - 1];
+          if(last && Math.abs(last.top - relTop) <= 3)
+          {
+            last.bottom = Math.max(last.bottom, relBottom);
+          }
+          else
+          {
+            lines.push({ top: relTop, bottom: relBottom });
+          }
+        }
+        let cut = boundary;
+        // 0) 切点落在某张图片中间 → 上移到该图片顶部(不切断图片)
+        const imgList = el.querySelectorAll('img');
+        for(let g = 0; g < imgList.length; g++)
+        {
+          const ir = imgList[g].getBoundingClientRect();
+          if(ir.height <= 0) continue;
+          const iTop = ir.top - elRect.top;
+          const iBottom = ir.bottom - elRect.top;
+          if(iTop < cut && cut < iBottom)
+          {
+            cut = iTop;
+            break;
+          }
+        }
+        // 1) 切点落在某一行中间 → 上移到该行顶部(不切断文字行)
+        for(const ln of lines)
+        {
+          if(ln.top < cut && cut < ln.bottom)
+          {
+            cut = ln.top;
+            break;
+          }
+        }
+        // 2) 切点上方最后一行若只剩标点 → 再上移一行(最多回退3行 避免极端数据死循环)
+        for(let guard = 0; guard < 3; guard++)
+        {
+          let lastLine = null;
+          for(const ln of lines)
+          {
+            if(ln.bottom <= cut + 1) lastLine = ln;
+            else break;
+          }
+          if(!lastLine) break;
+          const text = this.GetLineText(el, elRect, lastLine);
+          // 非空且全是标点/空白 → 孤行标点 推到下一页
+          if(text !== '' && /^[\s,。、;:!?..,;:!?…·—-~~"'“”‘’()()《》【】〔〕%%]+$/.test(text))
+          {
+            cut = lastLine.top;
+          }
+          else
+          {
+            break;
+          }
+        }
+        return Math.max(cut, 0);
+      }
+      catch(e)
+      {
+        console.warn('[切片边界调整失败]', e);
+        return boundary;
+      }
+    },
+
+    //读取某一行(垂直区间)内的文字内容 用于判断是否为标点孤行(逐字测量所属行)
+    GetLineText(el, elRect, line)
+    {
+      let text = '';
+      const range = document.createRange();
+      const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
+      let node;
+      while((node = walker.nextNode()))
+      {
+        const value = node.nodeValue || '';
+        if(!value.trim()) continue;
+        range.selectNodeContents(node);
+        const nr = range.getBoundingClientRect();
+        // 与目标行垂直区间无交集则跳过 减少逐字测量次数
+        if(nr.bottom - elRect.top < line.top - 3 || nr.top - elRect.top > line.bottom + 3) continue;
+        for(let c = 0; c < value.length; c++)
+        {
+          range.setStart(node, c);
+          range.setEnd(node, c + 1);
+          const cr = range.getBoundingClientRect();
+          if(cr.width <= 0 && cr.height <= 0) continue;
+          const cTop = cr.top - elRect.top;
+          if(cTop >= line.top - 3 && cTop < line.bottom)
+          {
+            text += value.charAt(c);
+          }
+        }
+      }
+      return text;
+    },
+
+    //导出所有pdf domtoimage的方式
+    async DownloadPdfs()
+    {
+
+      
+      this.showReportLoading = true;
+     
+      // 创建PDF对象,使用自定义纸张尺寸
+      const pdf = new jsPDF("p", "pt", 'a4');//p 为纵向布局
+      const pdfWidth = pdf.internal.pageSize.getWidth();
+      const pdfHeight = pdf.internal.pageSize.getHeight();
+
+      //获取所有的area_page 元素
+      const elements = document.querySelectorAll(".area_page");
+
+      let fillCount=0;//完成数量
+      let pageCount=elements.length;//总数量
+      this.targetProgress=0;
+      let oneProgress=Math.round(1 / pageCount * 100);
+      console.log("打印每页的目标进度", oneProgress);
+      this.StartProgress(0,oneProgress);
+
+       console.log("开始导出",this.targetProgress);
+
+       // 遍历每个页面数据
+      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)',
+              'transform-origin': 'top left',
+              'background-color': '#ffffff',
+              '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();
+          }
+          pdf.addImage(dataUrl, "JPEG", xPosition, yPosition, adjustWidth, adjustHeight);
+          fillCount++;
+          // console.log("打印当前完成数量", fillCount);
+          
+          // 更新进度
+          const progress = Math.floor(fillCount / pageCount * 100);
+          const lastProgressprogress=progress-oneProgress;
+          // if(this.targetProgress<progress)
+          // {
+            // this.targetProgress=progress;
+          // }
+
+          this.StartProgress(lastProgressprogress,progress);
+         
+         
+
+          
+          // 页面间短暂延迟
+          // await new Promise(resolve => setTimeout(resolve, 50));
+        }
+      }
+      
+          // 保存PDF文件
+          pdf.save(this.studentName+'-'+this.className+"-个性化错题.pdf");
+      
+       this.showReportLoading = false; //关闭加载loading
+    },
+
+
+    // 开始进度条数值变化  最大增加进度值 maxIncrement
+    StartProgress(currentValue,targetValue)
+    {
+      console.log("开始进度动画,目标值:", targetValue);
+      
+      // 清除已存在的定时器
+      if (this.progressInterval) {
+        clearInterval(this.progressInterval);
+        this.progressInterval = null;
+      }
+      
+      // 获取当前进度值
+      if(this.targetProgress<currentValue)
+      {
+        this.targetProgress=currentValue;
+      }
+      const targetProgress = targetValue;
+      // 如果当前值大于等于目标值,则不需要动画(进度不能递减)
+      if (this.targetProgress >= targetProgress) {
+        console.log("当前进度已达到或超过目标进度,无需动画");
+        return;
+      }
+      
+      // 计算差值
+      const difference = targetProgress - currentValue;
+      console.log("差值:", difference);
+      
+      // 根据差值计算时间间隔(差值越大,时间间隔越小)
+      // 这里设置时间间隔在50ms到100ms之间变化
+      const minInterval = 50;
+      const maxInterval = 500;
+      // 限制差值在1-10范围内
+      // 限制差值在1-10范围内
+      const clampedDifference = Math.max(1, Math.min(difference, 10));
+      
+      // 计算时间间隔:差值1->500ms,差值10->50ms
+      let interval = maxInterval - (clampedDifference - 1) * ((maxInterval - minInterval) / 9);
+      
+      // 将时间间隔调整为10的倍数
+      interval = Math.round(interval / 10) * 10;
+      
+      // 确保时间间隔在有效范围内
+      interval = Math.max(minInterval, Math.min(interval, maxInterval));
+      // 计算每次增加的步长(差值越大,步长越大)
+      const step = Math.max(1, Math.floor(difference / 5)) || 1;
+      
+      console.log("动画参数:当前进度=", currentValue, "目标进度=", targetProgress, 
+                  "差值=", difference, "时间间隔=", interval, "步长=", step);
+      
+      // 创建新的定时器
+      this.progressInterval = setInterval(() => {
+        // 只能增加,不能减少
+        this.targetProgress = Math.min(this.targetProgress + step, targetProgress);
+        console.log("进度更新:", this.targetProgress);
+        
+        // 如果达到目标值,清除定时器
+        if (this.targetProgress >= targetProgress) {
+          this.targetProgress = targetProgress;
+          console.log("进度动画完成,最终值:", this.targetProgress);
+          clearInterval(this.progressInterval);
+          this.progressInterval = null;
+        }
+      }, interval);
+    },
+  },
+  mounted() {
+   
+  },
+};
+</script>
+<style scoped lang="scss">
+
+.slip_content
+{
+  min-width: 794px;
+}
+.score_slip
+{
+  width: 100%;
+  height: 100vh;
+  background: #F0F4FB;
+  position: relative;
+  overflow: hidden;
+
+  .slip_set_box{
+    min-width: 170px;
+    position: absolute;
+    top: 20px;
+    left: 20px;
+    display: flex;
+    flex-direction: column;
+    .set{
+      min-width: 170px;
+      background-color: #ffffff;
+      border-radius: 4px;
+      padding: 12px 12px 0;
+      box-sizing: border-box;
+      :deep(.el-radio-group){
+        display: flex;
+        flex-direction: column;
+      }
+      :deep(.el-checkbox-group){
+        display: flex;
+        flex-direction: column;
+      }
+      &.slip_sets{
+        margin-top: 16px;
+      }
+      &.slip_set_rank{
+        margin-top: 16px;
+      }
+      &.slip_set_average{
+        margin-top: 16px;
+      }
+      &.slip_set_fullScore{
+        margin-top: 16px;
+      }
+    }
+  }
+  .el-radio
+  {
+    line-height: 22px;
+    margin-bottom: 12px;
+  }
+  .el-checkbox
+  {
+    line-height: 22px;
+    margin-bottom: 12px;
+  }
+  .slip_close
+  {
+    position: absolute;
+    right:20px;
+    top:20px;
+    width: 160px;
+    height: 42px;
+    background: rgba(245,108,108,0.1);
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #F56C6C;
+    line-height: 42px;
+    text-align: center;
+    font-weight: 500;
+    font-size: 14px;
+    color: #F56C6C;
+    cursor: pointer;
+  }
+
+  .slip_print
+  {
+    position: absolute;
+    right:20px;
+    top:82px;
+    width: 160px;
+    height: 44px;
+    background: #2E64FA;
+    border-radius: 4px 4px 4px 4px;
+    line-height:42px;
+    font-weight: 500;
+    font-size: 14px;
+    color: #ffffff;
+    text-align: center;
+    cursor: pointer;
+  
+  }
+
+  .slip_down_pdf
+  {
+    position: absolute;
+    right:20px;
+    top:144px;
+    width: 160px;
+    height: 44px;
+    background: #2E64FA;
+    border-radius: 4px 4px 4px 4px;
+
+    line-height:42px;
+    
+    font-weight: 500;
+    font-size: 14px;
+    color: #ffffff;
+    text-align: center;
+    cursor: pointer;
+  
+  }
+
+  .score_icon
+  {
+    img
+    {
+      width: 12.5px;
+      height: 12px;
+    }
+  }
+}
+
+.area_page
+{
+  position: relative;
+}
+
+// 页面内容区域 固定高度
+.page_area_content
+{
+  width: 100%;
+  height: 1023px;
+  overflow: hidden; /* 内容超出部分隐藏 */
+}
+
+// 页面页眉信息
+.page_header_info
+{
+  width: 100%;
+  height: 30px;
+  border-bottom: 1px solid #DDDDDD;
+  margin-bottom: 10px;
+  display: flex;
+  justify-content: space-between;
+
+}
+
+// 页面页脚信息
+.page_footer_info
+{
+  position: absolute;
+  bottom: 0px;
+  left: 0px;
+  width: 100%;
+  display: flex;
+  justify-content:center;
+  align-items: center;
+  box-sizing: border-box;
+  line-height: 25px;
+  font-size: 10px;
+  color:#000000;
+  gap: 10px;
+  background-color: #fff;
+  padding-bottom: 10px;
+
+}
+
+.page_title_info
+{
+  width: 100%;
+  height: 40px;
+  line-height: 40px;
+  // font-family: Alibaba PuHuiTi 2.0, Alibaba PuHuiTi 20;
+  font-weight: 550;
+  font-size: 18px;
+  color: #000000;
+  text-align: center;
+
+  background-image: url('../../../assets/bg/target_pdf_header.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center;
+}
+
+.page_knowledge_info
+{
+  width: 100%;
+  height: 30px;
+
+  .knowledge_name
+  {
+    width:auto;
+    font-weight: 550;
+    font-size: 12px;
+    color: #000000;
+    background: #E2E2E2;
+    border-radius: 4px 4px 4px 4px;
+    line-height: 30px;
+    padding: 8px 8px;
+
+  }
+ 
+
+}
+
+.question_item
+{
+  width:100%;
+}
+
+//两栏布局:栏宽必须与测量容器宽度一致(367 = (794-40-20)/2),否则测量高度会失真
+.page_columns_wrap
+{
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+
+  .page_column
+  {
+    width: 367px;
+    box-sizing: border-box;
+  }
+}
+  .item_header
+  {
+    width: 100%;
+    height: auto;
+    min-height: 30px;
+    background: #F3F3F3;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #CCCCCC;
+    padding: 4px 8px;
+    box-sizing: border-box;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .header_left
+    {
+
+      display: flex;
+      align-items: center;
+      justify-content:flex-start;
+      gap: 8px;
+      
+      .left_question_num
+      {
+        background: #E2E2E2;
+        border-radius: 4px 4px 4px 4px;
+        padding: 4px 10px;
+        
+        font-weight: 550;
+        font-size: 12px;
+        color: #000000;
+      }
+
+      .left_question_source
+      {
+        font-weight: 400;
+        font-size: 10px;
+        color: #999999;
+      }
+    }
+
+    .header_right
+    {
+      display:flex;
+      justify-content: flex-end;
+      align-items: center;
+      gap: 8px;
+      font-size: 10px;
+      color: #000000;
+    }
+  }
+
+  //一栏的题目内容标题区域
+  .item_header_one
+  {
+    width: 100%;
+    height: 30px;
+
+    background: #F3F3F3;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #CCCCCC;
+    padding: 4px 8px;
+    box-sizing: border-box;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .header_left
+    {
+
+      display: flex;
+      align-items: center;
+      justify-content:flex-start;
+      gap: 8px;
+      
+      .left_question_num
+      {
+        background: #E2E2E2;
+        border-radius: 4px 4px 4px 4px;
+        padding: 4px 10px;
+        
+        font-weight: 550;
+        font-size: 12px;
+        color: #000000;
+      }
+
+      .left_question_source
+      {
+        font-weight: 400;
+        font-size: 10px;
+        color: #999999;
+      }
+    }
+
+    .header_right
+    {
+      display:flex;
+      justify-content: flex-end;
+      align-items: center;
+      gap: 8px;
+      font-size: 10px;
+      color: #000000;
+    }
+  }
+
+  //两栏的题目内容标题区域
+  .item_header_two
+  {
+    width: 100%;
+    height: auto;
+ 
+    background: #F3F3F3;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #CCCCCC;
+    padding: 4px 8px;
+    box-sizing: border-box;
+
+
+    .header_left
+    {
+
+      display: flex;
+      align-items: center;
+      justify-content:space-between;
+      gap: 8px;
+      
+      .left_question_num
+      {
+        background: #E2E2E2;
+        border-radius: 4px 4px 4px 4px;
+        padding: 4px 10px;
+        
+        font-weight: 550;
+        font-size: 12px;
+        color: #000000;
+      }
+
+      .left_question_source
+      {
+        font-weight: 400;
+        font-size: 10px;
+        color: #000000;
+        display: flex;
+        justify-content: flex-end;
+        gap: 8px;
+      }
+    }
+
+    .header_right
+    {
+      display:flex;
+      justify-content:flex-start;
+      align-items: center;
+      gap: 8px;
+      font-size: 10px;
+      color: #999999;
+      padding-top: 8px;
+    }
+  }
+
+  //题目内容区域
+  .item_question_title
+  {
+    line-height: 20px;
+    color: #000000;
+    font-size: 10px;
+    overflow-x:hidden;
+
+    ::v-deep table
+    {
+      max-width: 100% !important;
+    }
+    ::v-deep img
+    {
+      max-width: 100% !important;
+      height: auto !important;
+    }
+  }
+
+  .item_question_knowledge
+  {
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #CCCCCC;
+    padding: 8px 10px;
+    box-sizing: border-box;
+    .knowledge_title
+    {
+      font-weight: 550;
+      font-size: 10px;
+      color: #000000;
+    }
+
+    .knowledge_list
+    {
+      font-weight: 400;
+      font-size: 10px;
+      color: #000000;
+    }
+  }
+
+  //答案解析区域
+  .item_question_answer
+  {
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #CCCCCC;
+    padding: 5px 10px;
+    box-sizing: border-box;
+    overflow-x:hidden;
+    .answer_title
+    {
+      font-weight: 550;
+      font-size: 10px;
+      color: #000000;
+      line-height: 20px;
+    }
+
+    .answer_content
+    {
+      font-weight: 400;
+      font-size: 10px;
+      color: #000000;
+      line-height: 20px;
+    }
+
+    //图片不可超过容器的宽度 高度自适应
+    .answer_images
+    {
+      width: 100%;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 6px;
+      align-items: flex-start;
+
+      img
+      {
+        display: block;
+        max-width: 100%;
+        height: auto;
+        object-fit: contain;
+        border-radius: 2px;
+        border: 1px solid #e0e0e0;
+        background-color: #ffffff;
+      }
+    }
+  }
+
+// 离线测量容器:宽度与正式页一致,定位到屏外不可见
+.measure_box
+{
+  position: fixed;
+  left: -9999px;
+  top: 0;
+  width: 794px;   // 与正式 PDF 页宽保持一致(A4 96dpi 约 794px)
+  visibility: hidden;
+  pointer-events: none;
+  z-index: -1;
+  // ★ 强制浏览器参与布局计算(部分浏览器 visibility:hidden 元素布局会懒加载)
+  contain: layout;
+}
+</style>

+ 32 - 2
src/views/targetImprove/consolidate/index.vue

@@ -144,6 +144,7 @@
         </div>
       </div>
     </div>
+    <DialogExport ref="dialogExport" v-model="showExportDialog"></DialogExport>
   </div>
 </template>
 <script>
@@ -152,6 +153,7 @@ import QuestionItem from "@/views/targetImprove/components/QuestionItem.vue"//
 import CommonTree from '@//views/targetImprove/components/CommonTree.vue';//树形组件
 import AnswerAnalysis from '@/views/targetImprove/components/AnswerAnalysis';//答案解析组件
 import StudentAnswerDetail from '@/views/targetImprove/components/StudentAnswerDetail.vue'//学生作答组件
+import DialogExport from '@/views/targetImprove/consolidate/export.vue';//导出错题本pdf组件
 
 import aiIcon from '@/assets/knowledgeGraph/aiIcon.png'
 import answerIcon from '@/assets/practiceResult/broken_line.svg'
@@ -166,7 +168,8 @@ export default {
     QuestionItem,
     CommonTree,
     AnswerAnalysis,
-    StudentAnswerDetail
+    StudentAnswerDetail,
+    DialogExport
 
   },
   inject: ['openAiRobot'],//注入Ai机器人组件调用方法
@@ -291,7 +294,34 @@ export default {
     //导出错题本pdf
     ExportPDF()
     {
-      this.$message.warning('功能开发中,敬请期待');
+      if(this.questionList.length==0)
+      {
+        this.$message.warning("还没有错题哦!");
+        return;
+      }
+      
+      let params={
+
+        "knowledgeIds": this.knowledgeIds,//知识点Id
+        "courseCode": this.subjectCode,
+        "gradeCodes":this.checkGrades,//年级code
+        "types": this.checkDimension,//维度  1 考试 2 练习
+      };
+      if(this.startTime!='')
+      {
+        params.beginAt=this.startTime+" 00:00:00";
+        params.endAt=this.endTime+' 23:59:59';
+      }
+      this.$refs.dialogExport.GetListData(params).then(res=>{
+        console.log("获取到数据", res);
+        if(res.code==200)
+        {
+         this.showExportDialog=true;
+
+        }else{
+          this.showExportDialog=false;
+        }
+      });
     },
 
     //分页切换