Răsfoiți Sursa

成绩打印、错题打印

liurongli 2 săptămâni în urmă
părinte
comite
4e08037717

+ 67 - 29
src/components/ErrorsTablePdf.vue

@@ -197,6 +197,18 @@ const miniHeaderHeight = 30; // 非首页页头固定高度(第几页总页数
 // 行高实测对锚点行(带 rowspan)是安全的:浏览器实测锚点行高度 = 自身内容高度
 // 行高实测对锚点行(带 rowspan)是安全的:浏览器实测锚点行高度 = 自身内容高度
 const measuredRowHeights = new Map<string, number>();
 const measuredRowHeights = new Map<string, number>();
 
 
+// 分页收敛信号:tableData 变化时重置为 pending;correctPages 最后一轮不再重排时 resolve。
+// DownloadPdf 开头 await 它,保证截图时 DOM 已稳定,不会截到重建中的死节点
+// (死节点 scrollWidth=0 → 空 data:," 串 → jsPDF 报 UNKNOWN)
+let pagesReadyResolve: () => void = () => {};
+let pagesReadyPromise: Promise<void> = Promise.resolve();
+const resetPagesReady = () => {
+  pagesReadyPromise = new Promise<void>((res) => {
+    pagesReadyResolve = res;
+  });
+};
+const resolvePagesReady = () => pagesReadyResolve();
+
 const LoadData = () => {
 const LoadData = () => {
   nextTick(() => {
   nextTick(() => {
     TablePagesData();
     TablePagesData();
@@ -234,6 +246,8 @@ const getRowHeight = (row: TableItem): number => {
 // 分页后采集每行真实渲染高度重新分页,并按精确溢出像素校正内容预算,
 // 分页后采集每行真实渲染高度重新分页,并按精确溢出像素校正内容预算,
 // 直到页面不再溢出且分页稳定,避免估算偏差导致页面内容溢出或大面积留白
 // 直到页面不再溢出且分页稳定,避免估算偏差导致页面内容溢出或大面积留白
 const TablePagesData = () => {
 const TablePagesData = () => {
+  // 新一轮分页开始:重置收敛信号,等待 correctPages 全部轮次结束后再允许截图
+  resetPagesReady();
   // 数据更新后清空旧行高缓存,重新采集
   // 数据更新后清空旧行高缓存,重新采集
   measuredRowHeights.clear();
   measuredRowHeights.clear();
   // 每行条目数密度按本次数据重新学习,避免沿用上次数据的学习结果
   // 每行条目数密度按本次数据重新学习,避免沿用上次数据的学习结果
@@ -472,7 +486,10 @@ const TablePagesData = () => {
     setTimeout(() => {
     setTimeout(() => {
       nextTick(() => {
       nextTick(() => {
         const els = rootRef.value?.querySelectorAll(".print_page");
         const els = rootRef.value?.querySelectorAll(".print_page");
-        if (!els || !els.length) return;
+        if (!els || !els.length) {
+          resolvePagesReady();
+          return;
+        }
 
 
         // 1) 采集每行真实高度:行高只与学号名单列的换行有关,以该列文本为 key。
         // 1) 采集每行真实高度:行高只与学号名单列的换行有关,以该列文本为 key。
         //    锚点行(带 rowspan)实测高度 = 自身内容高度,不会被合并组撑大,
         //    锚点行(带 rowspan)实测高度 = 自身内容高度,不会被合并组撑大,
@@ -561,6 +578,9 @@ const TablePagesData = () => {
           correctPages(attempt + 1, cacheKey);
           correctPages(attempt + 1, cacheKey);
           return;
           return;
         }
         }
+
+        // 收敛:不再重排,通知等待中的 DownloadPdf 可以截图了
+        resolvePagesReady();
       });
       });
     }, 0);
     }, 0);
   };
   };
@@ -569,6 +589,11 @@ const TablePagesData = () => {
 
 
 // 下载 PDF (使用 dom-to-image)
 // 下载 PDF (使用 dom-to-image)
 const DownloadPdf = async () => {
 const DownloadPdf = async () => {
+  // 关键:等分页校正循环(最多 12 轮)彻底收敛后再截图。
+  // 切班级时 tableData 刚更新,correction 循环会反复重建 .print_page DOM,
+  // 提前截图会截到已卸载的死节点(scrollWidth=0 → 空图 → jsPDF 报 UNKNOWN)
+  await pagesReadyPromise;
+  await nextTick();
   const elements = rootRef.value?.querySelectorAll(".print_page") || [];
   const elements = rootRef.value?.querySelectorAll(".print_page") || [];
   if (elements.length === 0) {
   if (elements.length === 0) {
     ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
     ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
@@ -597,34 +622,47 @@ const DownloadPdf = async () => {
 
 
       await new Promise((resolve) => setTimeout(resolve, 100));
       await new Promise((resolve) => setTimeout(resolve, 100));
 
 
-      const dataUrl = await domtoimage.toJpeg(htmlElement, {
-        quality: 0.8,
-        width: htmlElement.scrollWidth * 2,
-        height: htmlElement.scrollHeight * 2,
-        canvasWidth: htmlElement.scrollWidth * 2,
-        canvasHeight: htmlElement.scrollHeight * 2,
-        style: {
-          // transform: "scale(2)",
-          // "transform-origin": "top left",
-          // "background-color": "#ffffff",
-          // overflow: "visible",
-          // "white-space": "normal",
-          // position: "relative",
-          // visibility: "visible",
-          transform: "scale(2)",
-          "transform-origin": "top left",
-          "background-color": "#ffffff",
-          // 关键:背景图尺寸适配原元素
-          "background-size": `${htmlElement.scrollWidth}px ${htmlElement.scrollHeight}px`, // 优先铺满且完整显示
-          "background-position": "top left",
-          "background-repeat": "no-repeat", // 防止背景图重复导致视觉不完整
-          overflow: "visible",
-          "white-space": "normal",
-          position: "relative",
-          visibility: "visible",
-        },
-        bgcolor: "#ffffff",
-      });
+      // 防御:分页收敛前若有残留的重建时序,节点可能暂时无布局(scrollWidth=0),
+      // 此时 toJpeg 会返回空 data:," 串,导致 jsPDF 报 UNKNOWN。失败则等待重试
+      let dataUrl = "";
+      for (let tryN = 0; tryN < 3; tryN++) {
+        if (htmlElement.scrollWidth === 0 || htmlElement.scrollHeight === 0) {
+          await new Promise((r) => setTimeout(r, 200));
+        }
+        dataUrl = await domtoimage.toJpeg(htmlElement, {
+          quality: 0.7,
+          width: htmlElement.scrollWidth * 2,
+          height: htmlElement.scrollHeight * 2,
+          canvasWidth: htmlElement.scrollWidth * 2,
+          canvasHeight: htmlElement.scrollHeight * 2,
+          style: {
+            // transform: "scale(2)",
+            // "transform-origin": "top left",
+            // "background-color": "#ffffff",
+            // overflow: "visible",
+            // "white-space": "normal",
+            // position: "relative",
+            // visibility: "visible",
+            transform: "scale(2)",
+            "transform-origin": "top left",
+            "background-color": "#ffffff",
+            // 关键:背景图尺寸适配原元素
+            "background-size": `${htmlElement.scrollWidth}px ${htmlElement.scrollHeight}px`, // 优先铺满且完整显示
+            "background-position": "top left",
+            "background-repeat": "no-repeat", // 防止背景图重复导致视觉不完整
+            overflow: "visible",
+            "white-space": "normal",
+            position: "relative",
+            visibility: "visible",
+          },
+          bgcolor: "#ffffff",
+        });
+        if (dataUrl && dataUrl.startsWith("data:image/jpeg")) break;
+        await new Promise((r) => setTimeout(r, 200));
+      }
+      if (!dataUrl || !dataUrl.startsWith("data:image/jpeg")) {
+        throw new Error(`第 ${pageIndex + 1} 页截图为空,页面尚未渲染完成`);
+      }
 
 
       const imgProps = pdf.getImageProperties(dataUrl);
       const imgProps = pdf.getImageProperties(dataUrl);
       const imgWidth = imgProps.width;
       const imgWidth = imgProps.width;

+ 280 - 82
src/components/ScorePdf.vue

@@ -4,27 +4,49 @@
     <template v-for="(item, page) in state.tableData">
     <template v-for="(item, page) in state.tableData">
       <div class="area_page print_page">
       <div class="area_page print_page">
         <div class="area_title" v-if="item.isFirstPage">
         <div class="area_title" v-if="item.isFirstPage">
-          <div class="page_num">第{{ page + 1 }}页(共{{ state.pageTotal }}页)</div>
-          <div class="title_text">【{{ `${examName}成绩单(${item?.className || ''})` }}】</div>
+          <div class="page_num">
+            第{{ page + 1 }}页(共{{ state.pageTotal }}页)
+          </div>
+          <div class="title_text">
+            【{{ `${examName}成绩单(${item?.className || ""})` }}】
+          </div>
           <div class="print_time">打印时间:{{ getNowTime() }}</div>
           <div class="print_time">打印时间:{{ getNowTime() }}</div>
         </div>
         </div>
         <div class="area_count" v-if="item.isFirstPage">
         <div class="area_count" v-if="item.isFirstPage">
           应考:{{ item?.fiveRateResult?.examAllNum || 0 }}人&nbsp;&nbsp;
           应考:{{ item?.fiveRateResult?.examAllNum || 0 }}人&nbsp;&nbsp;
           实考:{{ item?.fiveRateResult?.effectiveNum || 0 }}人&nbsp;&nbsp;
           实考:{{ item?.fiveRateResult?.effectiveNum || 0 }}人&nbsp;&nbsp;
-          缺考:{{ Number(item?.fiveRateResult?.examAllNum || 0) - Number(item?.fiveRateResult?.effectiveNum || 0)
-          }}人&nbsp;&nbsp;
-          满分:{{ item?.fullScore || 0 }}分&nbsp;&nbsp;
+          缺考:{{
+            Number(item?.fiveRateResult?.examAllNum || 0) -
+            Number(item?.fiveRateResult?.effectiveNum || 0)
+          }}人&nbsp;&nbsp; 满分:{{ item?.fullScore || 0 }}分&nbsp;&nbsp;
           最高分:{{ item?.fiveRateResult?.maxScore || 0 }}分&nbsp;&nbsp;
           最高分:{{ item?.fiveRateResult?.maxScore || 0 }}分&nbsp;&nbsp;
           最低分:{{ item?.fiveRateResult?.minScore || 0 }}分&nbsp;&nbsp;
           最低分:{{ item?.fiveRateResult?.minScore || 0 }}分&nbsp;&nbsp;
-          年级均分:{{ item?.fiveRateResult?.avgSchoolScore || 0 }}分&nbsp;&nbsp;
-          班级均分:{{ item?.fiveRateResult?.avgScore || 0 }}分
+          年级均分:{{
+            item?.fiveRateResult?.avgSchoolScore || 0
+          }}分&nbsp;&nbsp; 班级均分:{{ item?.fiveRateResult?.avgScore || 0 }}分
         </div>
         </div>
         <div class="module_table table_42">
         <div class="module_table table_42">
-          <el-table :data="item?.listData || []" border stripe :show-header="item.isFirstPage">
-            <el-table-column v-for="item in state.staticHeaderData" :key="item.prop" :prop="item.prop"
-              :label="item.label" min-width="5">
+          <el-table
+            :data="item?.listData || []"
+            border
+            stripe
+            :show-header="item.isFirstPage"
+          >
+            <el-table-column
+              v-for="item in state.staticHeaderData"
+              :key="item.prop"
+              :prop="item.prop"
+              :label="item.label"
+              min-width="5"
+            >
               <template #default="scope">
               <template #default="scope">
-                <span v-if="scope.row.score != '缺考' && scope.row.score != '违纪' && item.prop === 'studentUserName'">
+                <span
+                  v-if="
+                    scope.row.score != '缺考' &&
+                    scope.row.score != '违纪' &&
+                    item.prop === 'studentUserName'
+                  "
+                >
                   {{ scope.row[item.prop] }}
                   {{ scope.row[item.prop] }}
                 </span>
                 </span>
                 <template v-else>
                 <template v-else>
@@ -32,9 +54,16 @@
                 </template>
                 </template>
               </template>
               </template>
             </el-table-column>
             </el-table-column>
-            <el-table-column v-for="(item, index) in state.groupDataTitleData" :key="item.prop" :prop="`${index}_${item.prop}`"
-              :label="item.label" min-width="5">
-              <template #default="scope">{{ scope.row?.dynamicsData?.groupData?.[index] || "-" }}</template>
+            <el-table-column
+              v-for="(item, index) in state.groupDataTitleData"
+              :key="item.prop"
+              :prop="`${index}_${item.prop}`"
+              :label="item.label"
+              min-width="5"
+            >
+              <template #default="scope">{{
+                scope.row?.dynamicsData?.groupData?.[index] || "-"
+              }}</template>
             </el-table-column>
             </el-table-column>
           </el-table>
           </el-table>
         </div>
         </div>
@@ -44,10 +73,7 @@
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
-import {
-  studentTranscriptTitle,
-  queryStudentTranscript,
-} from "@/api/analysis";
+import { studentTranscriptTitle, queryStudentTranscript } from "@/api/analysis";
 import { useAnalysisStore } from "@/store/analysis";
 import { useAnalysisStore } from "@/store/analysis";
 import { getNowTime } from "@/utils/common";
 import { getNowTime } from "@/utils/common";
 import { reactive, computed, nextTick } from "vue";
 import { reactive, computed, nextTick } from "vue";
@@ -68,7 +94,6 @@ interface TableCount {
   missExamCount: number | string;
   missExamCount: number | string;
 }
 }
 
 
-
 interface State {
 interface State {
   keyWord: string;
   keyWord: string;
   selectClassList: string[];
   selectClassList: string[];
@@ -87,7 +112,7 @@ const examName = computed(() => {
   return analysisStore.analysisExamInfo.examName || "";
   return analysisStore.analysisExamInfo.examName || "";
 });
 });
 const state = reactive<State>({
 const state = reactive<State>({
-  keyWord: '',
+  keyWord: "",
   selectClassList: [],
   selectClassList: [],
   sortType: 1,
   sortType: 1,
   filterType: 1,
   filterType: 1,
@@ -100,7 +125,7 @@ const state = reactive<State>({
     normalCount: 0,
     normalCount: 0,
     missExamCount: 0,
     missExamCount: 0,
   },
   },
-  tableLoading: true
+  tableLoading: true,
 });
 });
 /** 获取表头 */
 /** 获取表头 */
 const GetStudentTranscriptTitle = async () => {
 const GetStudentTranscriptTitle = async () => {
@@ -113,7 +138,10 @@ const GetStudentTranscriptTitle = async () => {
     if (res.code === 200) {
     if (res.code === 200) {
       const staticHeaderData = res.data?.title?.staticHeaderData || [];
       const staticHeaderData = res.data?.title?.staticHeaderData || [];
       state.staticHeaderData = staticHeaderData.filter(
       state.staticHeaderData = staticHeaderData.filter(
-        (item: { display: any, prop: string }) => item.display && item.prop !== 'studentUserCode' && item.prop !== 'studentRegistrationTypeName',//移除学号和类型
+        (item: { display: any; prop: string }) =>
+          item.display &&
+          item.prop !== "studentUserCode" &&
+          item.prop !== "studentRegistrationTypeName", //移除学号和类型
       );
       );
       state.groupDataTitleData =
       state.groupDataTitleData =
         res.data?.title?.dynamicsHeaderData?.groupDataTitle || [];
         res.data?.title?.dynamicsHeaderData?.groupDataTitle || [];
@@ -134,11 +162,11 @@ const GetTableData = async () => {
       queryStr: state.keyWord,
       queryStr: state.keyWord,
       classIdCodes: state.selectClassList,
       classIdCodes: state.selectClassList,
       sortType: state.sortType,
       sortType: state.sortType,
-      filterType: state.filterType
+      filterType: state.filterType,
     });
     });
     if (res.code === 200) {
     if (res.code === 200) {
       const tableData = res?.data || [];
       const tableData = res?.data || [];
-      tablePageData(tableData);//分页处理(内部会设置 state.tableData 和 state.pageTotal)
+      tablePageData(tableData); //分页处理(内部会设置 state.tableData 和 state.pageTotal)
     }
     }
   } catch (err) {
   } catch (err) {
     console.error("获取表格数据失败:", err);
     console.error("获取表格数据失败:", err);
@@ -150,12 +178,12 @@ const GetTableData = async () => {
 };
 };
 //分页:超过一页自动换页,后续页不包含标题行和应考/实考/缺考统计行
 //分页:超过一页自动换页,后续页不包含标题行和应考/实考/缺考统计行
 const tablePageData = (list: any[]) => {
 const tablePageData = (list: any[]) => {
-  const trLineHeight = 21;//表格表头、tr行高
-  const printPageHeight = 1255;//打印页面高度 减掉padding 30px
+  const trLineHeight = 21; //表格表头、tr行高
+  const printPageHeight = 1255; //打印页面高度 减掉padding 30px
 
 
-  const rowNum = Math.floor(printPageHeight / trLineHeight);//每页可容纳行数 向下取整
-  const firstPageRows = rowNum - 3;//第一页数据行数:标题行+统计行+表头行占3行
-  const otherPageRows = rowNum;//后续页数据行数:无标题、无统计、无表头,数据行放满  原来是 rowNum - 1,后续页没有表头占位了,数据行放满
+  const rowNum = Math.floor(printPageHeight / trLineHeight); //每页可容纳行数 向下取整
+  const firstPageRows = rowNum - 3; //第一页数据行数:标题行+统计行+表头行占3行
+  const otherPageRows = rowNum; //后续页数据行数:无标题、无统计、无表头,数据行放满  原来是 rowNum - 1,后续页没有表头占位了,数据行放满
 
 
   const pageList: any[] = [];
   const pageList: any[] = [];
   list.forEach((item) => {
   list.forEach((item) => {
@@ -169,8 +197,7 @@ const tablePageData = (list: any[]) => {
     const pageNum =
     const pageNum =
       Math.ceil((dataList.length - firstPageRows) / otherPageRows) + 1;
       Math.ceil((dataList.length - firstPageRows) / otherPageRows) + 1;
     for (let page = 1; page <= pageNum; page++) {
     for (let page = 1; page <= pageNum; page++) {
-      const start =
-        page === 1 ? 0 : firstPageRows + (page - 2) * otherPageRows;
+      const start = page === 1 ? 0 : firstPageRows + (page - 2) * otherPageRows;
       const rows =
       const rows =
         page === 1
         page === 1
           ? Math.min(firstPageRows, dataList.length)
           ? Math.min(firstPageRows, dataList.length)
@@ -185,9 +212,14 @@ const tablePageData = (list: any[]) => {
   });
   });
   state.tableData = pageList;
   state.tableData = pageList;
   state.pageTotal = pageList.length;
   state.pageTotal = pageList.length;
-}
+};
 // 初始化
 // 初始化
-const PageInit = async (keyWord: string, selectClassList: string[], sortType: number, filterType: number) => {
+const PageInit = async (
+  keyWord: string,
+  selectClassList: string[],
+  sortType: number,
+  filterType: number,
+) => {
   state.keyWord = keyWord;
   state.keyWord = keyWord;
   state.selectClassList = selectClassList;
   state.selectClassList = selectClassList;
   state.sortType = sortType;
   state.sortType = sortType;
@@ -196,8 +228,131 @@ const PageInit = async (keyWord: string, selectClassList: string[], sortType: nu
 };
 };
 
 
 // 导出PDF并在浏览器新窗口打开
 // 导出PDF并在浏览器新窗口打开
+// const ExportPdf = async () => {
+//   const elements = document.querySelectorAll(".score_pdf .print_page");
+//   if (!elements.length) {
+//     ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
+//     return;
+//   }
+//   // 在用户手势内同步打开空白窗口,避免被浏览器拦截
+//   const win = window.open("", "_blank");
+//   if (!win) {
+//     ElMessage.warning("浏览器拦截了弹窗,请允许后重试");
+//     return;
+//   }
+//   win.document.write(
+//     "<p style='font-family:sans-serif;text-align:center;padding-top:40px'>成绩单 PDF 生成中…</p>",
+//   );
+
+//   const pdf = new jsPDF("p", "pt", "a4");
+//   const pdfWidth = pdf.internal.pageSize.getWidth();
+//   const pdfHeight = pdf.internal.pageSize.getHeight();
+
+//   for (const [pageIndex, element] of Array.from(elements).entries()) {
+//     await nextTick();
+//     const htmlElement = element as HTMLElement;
+//     htmlElement.style.visibility = "visible";
+//     htmlElement.offsetHeight;
+
+//     // 等待该页渲染完成
+//     await new Promise((resolve) => setTimeout(resolve, 150));
+
+//     const dataUrl = await domtoimage.toJpeg(htmlElement, {
+//       quality: 0.8,
+//       width: htmlElement.scrollWidth * 2,
+//       height: htmlElement.scrollHeight * 2,
+//       canvasWidth: htmlElement.scrollWidth * 2,
+//       canvasHeight: htmlElement.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;
+//     // 顶对齐:页面顶部与PDF页面上边缘对齐,避免标题被垂直居中往下推
+//     const yPosition = 0;
+
+//     if (pageIndex > 0) {
+//       pdf.addPage();
+//     }
+//     pdf.addImage(
+//       dataUrl,
+//       "JPEG",
+//       xPosition,
+//       yPosition,
+//       adjustWidth,
+//       adjustHeight,
+//     );
+//   }
+//   // // 直接打开生成的PDF,保留浏览器自带的下载/打印工具栏
+//   // const filename = `${examName.value}成绩单.pdf`;
+//   // // 通过 PDF 文档属性设置标题,浏览器查看器的标签页标题即显示该文件名
+//   // pdf.setProperties({ title: filename });
+//   // const blob = pdf.output("blob");
+//   // const url = URL.createObjectURL(
+//   //   new File([blob], filename, { type: "application/pdf" }),
+//   // );
+//   // win.location.replace(url);
+//   // 在浏览器新窗口打开生成的PDF(中转页方案:窗口标题、下载文件名均使用指定名称)
+//   const filename = `${examName.value}_${analysisStore?.filterObject?.subjectName || ''}_${analysisStore?.filterObject?.classGroupName || ''}成绩单.pdf`;
+//   const blob = pdf.output("blob");
+//   const url = URL.createObjectURL(
+//     new File([blob], filename, { type: "application/pdf" }),
+//   );
+//   // 转义文件名中的特殊字符,防止破坏 HTML
+//   const safeName = filename
+//     .replace(/&/g, "&amp;")
+//     .replace(/</g, "&lt;")
+//     .replace(/>/g, "&gt;")
+//     .replace(/"/g, "&quot;");
+//   // 清空"生成中"占位页,写入中转页
+//   win.document.open();
+//   win.document.write(
+//     `<!DOCTYPE html>
+// <html lang="zh-CN">
+// <head>
+// <meta charset="utf-8">
+// <title>${safeName}</title>
+// <style>
+//   body{margin:0;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
+//   .bar{height:48px;display:flex;align-items:center;gap:12px;padding:0 16px;background:#f5f6f7;border-bottom:1px solid #dcdfe6;box-sizing:border-box;}
+//   .bar .fname{flex:1;font-size:14px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
+//   .bar .btn{color:#fff;background:#2e64fa;border:0;padding:6px 16px;border-radius:4px;font-size:13px;cursor:pointer;flex-shrink:0;text-decoration:none;display:inline-block;}
+//   iframe{width:100%;height:calc(100vh - 48px);border:0;display:block;}
+//   @media print{
+//     .bar{display:none;}
+//     iframe{height:100vh;}
+//   }
+// </style>
+// </head>
+// <body>
+//   <div class="bar">
+//     <span class="fname">${safeName}</span>
+//     <a class="btn" href="${url}" download="${safeName}">下载PDF</a>
+//   </div>
+//   <iframe src="${url}"></iframe>
+// </body>
+// </html>`,
+//   );
+//   win.document.close();
+// };
 const ExportPdf = async () => {
 const ExportPdf = async () => {
-  const elements = document.querySelectorAll(".score_pdf .print_page");
+  const elements = Array.from(
+    document.querySelectorAll<HTMLElement>(".score_pdf .print_page"),
+  );
   if (!elements.length) {
   if (!elements.length) {
     ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
     ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
     return;
     return;
@@ -212,37 +367,86 @@ const ExportPdf = async () => {
     "<p style='font-family:sans-serif;text-align:center;padding-top:40px'>成绩单 PDF 生成中…</p>",
     "<p style='font-family:sans-serif;text-align:center;padding-top:40px'>成绩单 PDF 生成中…</p>",
   );
   );
 
 
+  // 1. 确保页面 DOM 渲染完成(只在最开始等一次,不再逐页等待)
+  await nextTick();
+
+  // 2. 一次性把所有页面设为可见,只触发一次重排
+  elements.forEach((el) => {
+    el.style.visibility = "visible";
+  });
+  void document.body.offsetHeight;
+
+  // 3. 等字体加载完成(替代原来每页 150ms 的固定等待)
+  if (document.fonts?.ready) {
+    await document.fonts.ready;
+  }
+
+  // 4. 预加载页面内尚未加载的图片(含 lazy 图片),避免截图时空白
+  const pendingImgs = elements.flatMap((el) =>
+    Array.from(el.querySelectorAll<HTMLImageElement>("img")).filter(
+      (img) => !img.complete,
+    ),
+  );
+  pendingImgs.forEach((img) => {
+    img.loading = "eager";
+  });
+  await Promise.all(
+    pendingImgs.map(
+      (img) =>
+        new Promise<void>((resolve) => {
+          const done = () => resolve();
+          img.addEventListener("load", done);
+          img.addEventListener("error", done);
+        }),
+    ),
+  );
+
+  // 5. 等两帧,确保样式/布局已应用
+  await new Promise<void>((resolve) =>
+    requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
+  );
+
+  // 6. 并发截图:多页并行,限制并发数防止内存峰值过高
+  //    机器内存大、页面多时可以调大到 6-8;页面很大时可降到 2
+  const CONCURRENCY = 4;
+  const dataUrls: string[] = new Array(elements.length);
+  let cursor = 0;
+  const worker = async () => {
+    while (cursor < elements.length) {
+      const pageIndex = cursor++;
+      const htmlElement = elements[pageIndex];
+      // 渲染参数与原来完全一致,保留 scale(2)
+      dataUrls[pageIndex] = await domtoimage.toJpeg(htmlElement, {
+        quality: 0.7,
+        width: htmlElement.scrollWidth * 2,
+        height: htmlElement.scrollHeight * 2,
+        canvasWidth: htmlElement.scrollWidth * 2,
+        canvasHeight: htmlElement.scrollHeight * 2,
+        style: {
+          transform: "scale(2)",
+          "transform-origin": "top left",
+          "background-color": "#ffffff",
+          overflow: "visible",
+          "white-space": "normal",
+          position: "relative",
+          visibility: "visible",
+        },
+        bgcolor: "#ffffff",
+      });
+    }
+  };
+  await Promise.all(
+    Array.from(
+      { length: Math.min(CONCURRENCY, elements.length) },
+      () => worker(),
+    ),
+  );
+
+  // 7. 组装 PDF(布局逻辑与原来一致:居中 + 顶对齐)
   const pdf = new jsPDF("p", "pt", "a4");
   const pdf = new jsPDF("p", "pt", "a4");
   const pdfWidth = pdf.internal.pageSize.getWidth();
   const pdfWidth = pdf.internal.pageSize.getWidth();
   const pdfHeight = pdf.internal.pageSize.getHeight();
   const pdfHeight = pdf.internal.pageSize.getHeight();
-
-  for (const [pageIndex, element] of Array.from(elements).entries()) {
-    await nextTick();
-    const htmlElement = element as HTMLElement;
-    htmlElement.style.visibility = "visible";
-    htmlElement.offsetHeight;
-
-    // 等待该页渲染完成
-    await new Promise((resolve) => setTimeout(resolve, 150));
-
-    const dataUrl = await domtoimage.toJpeg(htmlElement, {
-      quality: 0.8,
-      width: htmlElement.scrollWidth * 2,
-      height: htmlElement.scrollHeight * 2,
-      canvasWidth: htmlElement.scrollWidth * 2,
-      canvasHeight: htmlElement.scrollHeight * 2,
-      style: {
-        transform: "scale(2)",
-        "transform-origin": "top left",
-        "background-color": "#ffffff",
-        overflow: "visible",
-        "white-space": "normal",
-        position: "relative",
-        visibility: "visible",
-      },
-      bgcolor: "#ffffff",
-    });
-
+  dataUrls.forEach((dataUrl, pageIndex) => {
     const imgProps = pdf.getImageProperties(dataUrl);
     const imgProps = pdf.getImageProperties(dataUrl);
     const imgWidth = imgProps.width;
     const imgWidth = imgProps.width;
     const imgHeight = imgProps.height;
     const imgHeight = imgProps.height;
@@ -250,9 +454,7 @@ const ExportPdf = async () => {
     const adjustWidth = imgWidth * ratio;
     const adjustWidth = imgWidth * ratio;
     const adjustHeight = imgHeight * ratio;
     const adjustHeight = imgHeight * ratio;
     const xPosition = (pdfWidth - adjustWidth) / 2;
     const xPosition = (pdfWidth - adjustWidth) / 2;
-    // 顶对齐:页面顶部与PDF页面上边缘对齐,避免标题被垂直居中往下推
     const yPosition = 0;
     const yPosition = 0;
-
     if (pageIndex > 0) {
     if (pageIndex > 0) {
       pdf.addPage();
       pdf.addPage();
     }
     }
@@ -264,29 +466,19 @@ const ExportPdf = async () => {
       adjustWidth,
       adjustWidth,
       adjustHeight,
       adjustHeight,
     );
     );
-  }
-  // // 直接打开生成的PDF,保留浏览器自带的下载/打印工具栏
-  // const filename = `${examName.value}成绩单.pdf`;
-  // // 通过 PDF 文档属性设置标题,浏览器查看器的标签页标题即显示该文件名
-  // pdf.setProperties({ title: filename });
-  // const blob = pdf.output("blob");
-  // const url = URL.createObjectURL(
-  //   new File([blob], filename, { type: "application/pdf" }),
-  // );
-  // win.location.replace(url);
-  // 在浏览器新窗口打开生成的PDF(中转页方案:窗口标题、下载文件名均使用指定名称)
+  });
+
+  // 中转页方案保持不变
   const filename = `${examName.value}_${analysisStore?.filterObject?.subjectName || ''}_${analysisStore?.filterObject?.classGroupName || ''}成绩单.pdf`;
   const filename = `${examName.value}_${analysisStore?.filterObject?.subjectName || ''}_${analysisStore?.filterObject?.classGroupName || ''}成绩单.pdf`;
   const blob = pdf.output("blob");
   const blob = pdf.output("blob");
   const url = URL.createObjectURL(
   const url = URL.createObjectURL(
     new File([blob], filename, { type: "application/pdf" }),
     new File([blob], filename, { type: "application/pdf" }),
   );
   );
-  // 转义文件名中的特殊字符,防止破坏 HTML
   const safeName = filename
   const safeName = filename
     .replace(/&/g, "&amp;")
     .replace(/&/g, "&amp;")
     .replace(/</g, "&lt;")
     .replace(/</g, "&lt;")
     .replace(/>/g, "&gt;")
     .replace(/>/g, "&gt;")
     .replace(/"/g, "&quot;");
     .replace(/"/g, "&quot;");
-  // 清空"生成中"占位页,写入中转页
   win.document.open();
   win.document.open();
   win.document.write(
   win.document.write(
     `<!DOCTYPE html>
     `<!DOCTYPE html>
@@ -317,10 +509,14 @@ const ExportPdf = async () => {
   );
   );
   win.document.close();
   win.document.close();
 };
 };
-
 // 加载数据并导出PDF(供父组件一步调用)
 // 加载数据并导出PDF(供父组件一步调用)
-const LoadAndExportPdf = async (keyWord: string, selectClassList: string[], sortType: number, filterType: number) => {
-  await PageInit(keyWord, selectClassList, sortType, filterType); 
+const LoadAndExportPdf = async (
+  keyWord: string,
+  selectClassList: string[],
+  sortType: number,
+  filterType: number,
+) => {
+  await PageInit(keyWord, selectClassList, sortType, filterType);
   await nextTick();
   await nextTick();
   await ExportPdf();
   await ExportPdf();
 };
 };
@@ -329,7 +525,7 @@ const LoadAndExportPdf = async (keyWord: string, selectClassList: string[], sort
 defineExpose({
 defineExpose({
   PageInit,
   PageInit,
   ExportPdf,
   ExportPdf,
-  LoadAndExportPdf
+  LoadAndExportPdf,
 });
 });
 </script>
 </script>
 
 
@@ -344,7 +540,7 @@ defineExpose({
 
 
   .area_page {
   .area_page {
     width: 908px;
     width: 908px;
-    margin-top:20px;
+    margin-top: 20px;
     padding: 8px 15px 0 !important;
     padding: 8px 15px 0 !important;
     background-color: #fff;
     background-color: #fff;
     box-sizing: border-box;
     box-sizing: border-box;
@@ -352,6 +548,8 @@ defineExpose({
 
 
     &.print_page {
     &.print_page {
       height: 1285px;
       height: 1285px;
+      -webkit-print-color-adjust: exact !important;
+      print-color-adjust: exact !important;
     }
     }
 
 
     .area_title {
     .area_title {

+ 182 - 8
src/views/analysis/errorAnalysis.vue

@@ -138,28 +138,73 @@
   /> -->
   /> -->
   <ErrorsTablePdf
   <ErrorsTablePdf
     ref="errPdfReport"
     ref="errPdfReport"
-    :tableData="state.tableData"
+    :tableData="state.pdfTableData"
     :className="analysisStore.filterObject.classGroupName"
     :className="analysisStore.filterObject.classGroupName"
     :subjectName="analysisStore.filterObject.subjectName || ''"
     :subjectName="analysisStore.filterObject.subjectName || ''"
     :examName="getExamName"
     :examName="getExamName"
     @PdfLoadEnd="PdfLoadEnd"
     @PdfLoadEnd="PdfLoadEnd"
   />
   />
+  <div class="page_dialog padding_0">
+    <el-dialog title="选择班级" center v-model="showClassPop" width="480px">
+      <div class="dialog_center padding_20 page_search">
+        <div class="select_check_content score_class_popup">
+          <!-- <div class="item_option">
+            <el-checkbox
+              :indeterminate="isIndeterminate"
+              v-model="checkAll"
+              @change="HandleCheckAllChange"
+              >全部</el-checkbox
+            >
+          </div> -->
+          <!-- <el-checkbox-group
+            v-model="selectClassList"
+            @change="HandleCheckedClassChange"
+          >
+            <el-checkbox
+              :value="item.classIdCode"
+              v-for="item in classList"
+              :title="item.className"
+              >{{ TruncateMiddleText(item.className, 5, 3, 1) }}</el-checkbox
+            >
+          </el-checkbox-group> -->
+          <el-radio-group v-model="radioSelectClassList">
+            <el-radio
+              :value="item.classIdCode"
+              v-for="item in classList"
+              :title="item.className"
+              >{{ TruncateMiddleText(item.className, 5, 3, 1) }}</el-radio
+            >
+          </el-radio-group>
+        </div>
+      </div>
+      <div slot="footer" class="el-dialog__footer dialog-footer">
+        <el-button size="small" @click="showClassPop = false">取 消</el-button>
+        <el-button
+          size="small"
+          type="primary"
+          @click="ConfirmEnter()"
+          :loading="buttonLoading"
+          >确 定</el-button
+        >
+      </div>
+    </el-dialog>
+  </div>
 </template>
 </template>
 <script lang="ts" setup>
 <script lang="ts" setup>
 import ReportModule from "@/components/ReportModule.vue";
 import ReportModule from "@/components/ReportModule.vue";
 import { useAnalysisStore } from "@/store/analysis";
 import { useAnalysisStore } from "@/store/analysis";
 // import ErrorsPdf from "@/components/ErrorsPdf.vue";
 // import ErrorsPdf from "@/components/ErrorsPdf.vue";
 import ErrorsTablePdf from "@/components/ErrorsTablePdf.vue";
 import ErrorsTablePdf from "@/components/ErrorsTablePdf.vue";
-import { onMounted, reactive, computed, ref, watch } from "vue";
+import { onMounted, reactive, computed, ref, watch, nextTick } from "vue";
 import { errorQuestionAnalysis, exportErrorQuestion } from "@/api/analysis";
 import { errorQuestionAnalysis, exportErrorQuestion } from "@/api/analysis";
-import { loadingSvgIcon } from "@/utils/common";
+import { loadingSvgIcon, truncateMiddleText } from "@/utils/common";
 import {
 import {
   downloadExcel,
   downloadExcel,
   GetExcelFileName,
   GetExcelFileName,
   errorQuestionDataStaticHeaderData,
   errorQuestionDataStaticHeaderData,
   errorQuestionDataChildHeaderData,
   errorQuestionDataChildHeaderData,
 } from "@/utils/exportExcel";
 } from "@/utils/exportExcel";
-
+import { ElMessage } from "element-plus";
 // 定义表格行数据类型
 // 定义表格行数据类型
 interface TableDataItem {
 interface TableDataItem {
   questionId: string | number;
   questionId: string | number;
@@ -183,11 +228,15 @@ interface TableDataItem {
 }
 }
 
 
 const state = reactive<{
 const state = reactive<{
+  allTableData: any[];
   tableData: TableDataItem[];
   tableData: TableDataItem[];
+  pdfTableData: TableDataItem[];
   tableLoading: boolean;
   tableLoading: boolean;
   loadingText: string;
   loadingText: string;
 }>({
 }>({
+  allTableData: [],
   tableData: [],
   tableData: [],
+  pdfTableData: [],
   tableLoading: true,
   tableLoading: true,
   loadingText: "加载中,请稍后...",
   loadingText: "加载中,请稍后...",
 });
 });
@@ -199,7 +248,24 @@ const errPdfReport = ref<any>(null);
 const getExamName = computed(() => {
 const getExamName = computed(() => {
   return analysisStore.analysisExamInfo.examName || "";
   return analysisStore.analysisExamInfo.examName || "";
 });
 });
-
+const buttonLoading = ref<boolean>(false); //按钮loading
+const showClassPop = ref<boolean>(false); //选择班级弹框
+const radioSelectClassList = ref<string>(""); //选中的班级
+const classList = computed(() => {
+  //班级列表
+  const classNameList =
+    analysisStore.filtersData.find((item: any) => item.type === "className")
+      ?.list || [];
+  return classNameList.filter((item: any) => item.classLevel == 2);
+});
+const TruncateMiddleText = (
+  text: string,
+  num: number,
+  startStr: number,
+  endStr: number,
+) => {
+  return truncateMiddleText(text, num, startStr, endStr);
+};
 // 错题分析
 // 错题分析
 const GetErrorQuestionAnalysis = async () => {
 const GetErrorQuestionAnalysis = async () => {
   state.tableLoading = true;
   state.tableLoading = true;
@@ -209,7 +275,9 @@ const GetErrorQuestionAnalysis = async () => {
 
 
   if (res.code === 200) {
   if (res.code === 200) {
     const allTableData = res.data || [];
     const allTableData = res.data || [];
+    state.allTableData = allTableData;
     state.tableData = [];
     state.tableData = [];
+    state.pdfTableData = [];
     const tableData = allTableData.filter((item: any) => {
     const tableData = allTableData.filter((item: any) => {
       if (analysisStore.filterObject.classLevel == 0) {
       if (analysisStore.filterObject.classLevel == 0) {
         return item.classIdCode.indexOf("school") > -1;
         return item.classIdCode.indexOf("school") > -1;
@@ -244,6 +312,15 @@ const GetErrorQuestionAnalysis = async () => {
               rowKey: index, // 行索引
               rowKey: index, // 行索引
               rowspanIndex: key, // 合并行之后的索引
               rowspanIndex: key, // 合并行之后的索引
             });
             });
+            state.pdfTableData.push({
+              ...item,
+              scoreRate: `${item.scoreRate}%`,
+              ...answer,
+              rowspan: rowspan, // 合并行
+              colspan: 1,
+              rowKey: index, // 行索引
+              rowspanIndex: key, // 合并行之后的索引
+            });
           } else {
           } else {
             state.tableData.push({
             state.tableData.push({
               ...item,
               ...item,
@@ -253,16 +330,71 @@ const GetErrorQuestionAnalysis = async () => {
               rowKey: index, // 行索引
               rowKey: index, // 行索引
               rowspanIndex: key, // 合并行之后的索引
               rowspanIndex: key, // 合并行之后的索引
             });
             });
+            state.pdfTableData.push({
+              ...item,
+              questionId: `${item.questionId}_${index}`,
+              scoreRate: `${item.scoreRate}%`,
+              ...answer,
+              rowKey: index, // 行索引
+              rowspanIndex: key, // 合并行之后的索引
+            });
           }
           }
         });
         });
       });
       });
     }
     }
   } else {
   } else {
+    state.allTableData = [];
     state.tableData = [];
     state.tableData = [];
+    state.pdfTableData = [];
   }
   }
   state.tableLoading = false;
   state.tableLoading = false;
 };
 };
+const GetErrorQuestionAnalysisPdf = (className: string) => {
+  state.pdfTableData = [];
+  const tableData = state.allTableData.filter((item: any) => {
+    return item.className == className;
+  });
 
 
+  if (tableData?.length) {
+    tableData[0].questionStatsList.forEach((item: any, key: number) => {
+      const answerListLen = item?.answerList?.length || 0;
+      if (answerListLen == 0) {
+        item.answerList = [
+          {
+            name: "-",
+            rate: "-",
+            registrationCodeList: "-",
+            score: "-",
+            studentNum: "-",
+          },
+        ];
+      }
+      const rowspan = item.answerList.length;
+      item.answerList.forEach((answer: any, index: number) => {
+        if (index == 0) {
+          state.pdfTableData.push({
+            ...item,
+            scoreRate: `${item.scoreRate}%`,
+            ...answer,
+            rowspan: rowspan, // 合并行
+            colspan: 1,
+            rowKey: index, // 行索引
+            rowspanIndex: key, // 合并行之后的索引
+          });
+        } else {
+          state.pdfTableData.push({
+            ...item,
+            questionId: `${item.questionId}_${index}`,
+            scoreRate: `${item.scoreRate}%`,
+            ...answer,
+            rowKey: index, // 行索引
+            rowspanIndex: key, // 合并行之后的索引
+          });
+        }
+      });
+    });
+  }
+};
 // 错题分析  合并单元格
 // 错题分析  合并单元格
 const SpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
 const SpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
   const props = [
   const props = [
@@ -333,7 +465,7 @@ const ExportExcel = () => {
   params.dynamicsHeaderData = [];
   params.dynamicsHeaderData = [];
 
 
   let dataList: any[] = [];
   let dataList: any[] = [];
-  state.tableData.forEach((item) => {
+  state.pdfTableData.forEach((item) => {
     const rowData: any[] = [];
     const rowData: any[] = [];
     staticHeaderData.forEach((header: any) => {
     staticHeaderData.forEach((header: any) => {
       if (header.prop == "rate") {
       if (header.prop == "rate") {
@@ -357,15 +489,57 @@ const ExportExcel = () => {
 
 
 // 导出PDF
 // 导出PDF
 const PrintPdf = () => {
 const PrintPdf = () => {
-  reportModuleRef.value?.SetPrintLoading?.(true);
-  errPdfReport.value?.DownloadPdf?.();
+  if (analysisStore?.filterObject?.classLevel === 0) {
+    //年级
+    radioSelectClassList.value = "";
+    showClassPop.value = true; //显示弹框
+  } else {
+    reportModuleRef.value?.SetPrintLoading?.(true);
+    errPdfReport.value?.DownloadPdf?.();
+  }
 };
 };
 
 
 // 导出PDF  加载完成
 // 导出PDF  加载完成
 const PdfLoadEnd = () => {
 const PdfLoadEnd = () => {
   reportModuleRef.value?.SetPrintLoading?.(false);
   reportModuleRef.value?.SetPrintLoading?.(false);
 };
 };
+//确定选择班级
+const ConfirmEnter = async () => {
+  // if (selectClassList.value.length == 0) {
+  //   ElMessage.warning("请至少选择一个班级!");
+  //   return;
+  // }
+  if (radioSelectClassList.value == "") {
+    ElMessage.warning("请选择班级!");
+    return;
+  }
+  buttonLoading.value = true;
+  const className =
+    classList.value.find(
+      (item) => item.classIdCode == radioSelectClassList.value,
+    )?.className || "";
 
 
+  GetErrorQuestionAnalysisPdf(className);
+  // 关键:等子组件按新数据开始分页后再调导出。
+  // DownloadPdf 内部会 await 分页校正循环(最多 12 轮)收敛后才截图,
+  // 否则会截到分页重建中的死节点(scrollWidth=0 → 空图 → jsPDF 报 UNKNOWN)
+  await nextTick();
+  setTimeout(() => {
+    showClassPop.value = false; //先关闭弹框,不等PDF生成完成
+  }, 500);
+  errPdfReport.value
+    ?.DownloadPdf?.() //加载并导出PDF
+    .then(() => {
+      // ElMessage.success("成绩单已在新页面打开,请去下载或打印");
+    })
+    .catch((err) => {
+      console.error("导出成绩单PDF失败:", err);
+      // ElMessage.error("导出PDF失败,请重试");
+    })
+    .finally(() => {
+      buttonLoading.value = false;
+    });
+};
 // 初始化
 // 初始化
 const pageInit = () => {
 const pageInit = () => {
   GetErrorQuestionAnalysis();
   GetErrorQuestionAnalysis();

+ 30 - 8
src/views/analysis/score.vue

@@ -151,15 +151,15 @@
     >
     >
       <div class="dialog_center padding_20 page_search">
       <div class="dialog_center padding_20 page_search">
         <div class="select_check_content score_class_popup">
         <div class="select_check_content score_class_popup">
-          <div class="item_option">
+          <!-- <div class="item_option">
             <el-checkbox
             <el-checkbox
               :indeterminate="isIndeterminate"
               :indeterminate="isIndeterminate"
               v-model="checkAll"
               v-model="checkAll"
               @change="HandleCheckAllChange"
               @change="HandleCheckAllChange"
               >全部</el-checkbox
               >全部</el-checkbox
             >
             >
-          </div>
-          <el-checkbox-group
+          </div> -->
+          <!-- <el-checkbox-group
             v-model="selectClassList"
             v-model="selectClassList"
             @change="HandleCheckedClassChange"
             @change="HandleCheckedClassChange"
           >
           >
@@ -169,7 +169,18 @@
               :title="item.className"
               :title="item.className"
               >{{ TruncateMiddleText(item.className, 5, 3, 1) }}</el-checkbox
               >{{ TruncateMiddleText(item.className, 5, 3, 1) }}</el-checkbox
             >
             >
-          </el-checkbox-group>
+          </el-checkbox-group> -->
+          <el-radio-group
+            v-model="radioSelectClassList"
+            @change="HandleCheckedClassChange"
+          >
+            <el-radio
+              :value="item.classIdCode"
+              v-for="item in classList"
+              :title="item.className"
+              >{{ TruncateMiddleText(item.className, 5, 3, 1) }}</el-radio
+            >
+          </el-radio-group>
         </div>
         </div>
         <div class="item_option">
         <div class="item_option">
           <el-radio-group v-model="sortType">
           <el-radio-group v-model="sortType">
@@ -304,6 +315,7 @@ const showClassPop = ref<boolean>(false); //选择班级弹框
 const isIndeterminate = ref<boolean>(false); //是否班级全选半选
 const isIndeterminate = ref<boolean>(false); //是否班级全选半选
 const checkAll = ref<boolean>(false); //班级全选
 const checkAll = ref<boolean>(false); //班级全选
 const selectClassList = ref<string[]>([]); //选中的班级
 const selectClassList = ref<string[]>([]); //选中的班级
+const radioSelectClassList = ref<string>(""); //选中的班级
 const sortType = ref<number>(1); //排序类型
 const sortType = ref<number>(1); //排序类型
 const filterType = ref<number>(1); //是否打印缺考学生
 const filterType = ref<number>(1); //是否打印缺考学生
 const buttonLoading = ref<boolean>(false); //按钮loading
 const buttonLoading = ref<boolean>(false); //按钮loading
@@ -438,6 +450,7 @@ const ExportExcel = () => {
 // 打印PDF
 // 打印PDF
 const PrintPdf = () => {
 const PrintPdf = () => {
   selectClassList.value = [];//清空选择的数据
   selectClassList.value = [];//清空选择的数据
+  radioSelectClassList.value = "";
   isIndeterminate.value = false;//默认不半选
   isIndeterminate.value = false;//默认不半选
   buttonLoading.value = false;
   buttonLoading.value = false;
   sortType.value = 1;
   sortType.value = 1;
@@ -456,6 +469,11 @@ const PrintPdf = () => {
       .finally(() => {
       .finally(() => {
         reportModuleRef.value?.SetPrintLoading?.(false);
         reportModuleRef.value?.SetPrintLoading?.(false);
       });//加载并导出PDF
       });//加载并导出PDF
+    radioSelectClassList.value = classIdCode;
+    scorePdfRef.value?.LoadAndExportPdf(state.keyWord, radioSelectClassList.value)
+      .finally(() => {
+        reportModuleRef.value?.SetPrintLoading?.(false);
+      });//加载并导出PDF  
   }
   }
 };
 };
 //班级全选
 //班级全选
@@ -490,17 +508,21 @@ const TruncateMiddleText = (
 };
 };
 //确定选择班级
 //确定选择班级
 const ConfirmEnter = () => {
 const ConfirmEnter = () => {
-  if (selectClassList.value.length == 0) {
-    ElMessage.warning("请至少选择一个班级!");
+  // if (selectClassList.value.length == 0) {
+  //   ElMessage.warning("请至少选择一个班级!");
+  //   return;
+  // }
+  if (radioSelectClassList.value == "") {
+    ElMessage.warning("请选择班级!");
     return;
     return;
   }
   }
   buttonLoading.value = true;
   buttonLoading.value = true;
   setTimeout(() => {
   setTimeout(() => {
     showClassPop.value = false;//先关闭弹框,不等PDF生成完成
     showClassPop.value = false;//先关闭弹框,不等PDF生成完成
   }, 500);
   }, 500);
-  scorePdfRef.value?.LoadAndExportPdf(state.keyWord, selectClassList.value, sortType.value, filterType.value)//加载并导出PDF
+  scorePdfRef.value?.LoadAndExportPdf(state.keyWord, [radioSelectClassList.value], sortType.value, filterType.value)//加载并导出PDF
     .then(() => {
     .then(() => {
-      ElMessage.success("成绩单已在新页面打开,请去下载或打印");
+      // ElMessage.success("成绩单已在新页面打开,请去下载或打印");
     })
     })
     .catch((err) => {
     .catch((err) => {
       console.error("导出成绩单PDF失败:", err);
       console.error("导出成绩单PDF失败:", err);