Переглянути джерело

错题分析打印pdf、成绩单打印pdf问题修改

liurongli 3 тижнів тому
батько
коміт
fb285df6a0

+ 467 - 31
src/components/ErrorsTablePdf.vue

@@ -1,28 +1,44 @@
 <template>
-  <div class="report_module error">
+  <div class="report_module error" ref="rootRef">
+    <!-- 标题高度测量块(不参与导出) -->
     <div class="area_page">
-      <div class="area_title">
-        <div class="page_num">第1页(共10页)</div>
+      <div class="area_title" ref="pageTitle">
+        <div class="page_num">第1页(共{{ totalPageNum }}页)</div>
         <div class="title_text">
           【{{ `${examName}错题名单(${className})` }}】
         </div>
         <div class="print_time">打印时间:{{ getNowTime() }}</div>
       </div>
     </div>
-    <div class="area_page print_page">
-      <div class="area_title">
-        <div class="page_num">第1页(共10页)</div>
+    <!-- 分页打印块:第 1 页含标题 + 页码 + 打印时间(页头高度动态获取),
+         其余页页头仅显示页码 + 打印时间(固定高度 30px),表格数据分页续排 -->
+    <div
+      v-for="(pageData, pIndex) in tablePageData"
+      :key="pIndex"
+      class="area_page print_page"
+    >
+      <div class="area_title" v-if="pIndex === 0">
+        <div class="page_num">
+          第{{ pIndex + 1 }}页(共{{ totalPageNum }}页)
+        </div>
         <div class="title_text">
           【{{ `${examName}错题名单(${className})` }}】
         </div>
         <div class="print_time">打印时间:{{ getNowTime() }}</div>
       </div>
+      <div class="area_title mini_area_title" v-else>
+        <div class="page_num">
+          第{{ pIndex + 1 }}页(共{{ totalPageNum }}页)
+        </div>
+        <div class="print_time">打印时间:{{ getNowTime() }}</div>
+      </div>
       <div class="module_table table_42">
         <el-table
-          :data="tablePageData"
+          :data="pageData"
           border
           stripe
           row-key="questionId"
+          :show-header="pIndex === 0"
           :span-method="SpanMethod"
           :row-class-name="ErrorTableRowClassName"
           :cell-class-name="ErrorTableCellClassName"
@@ -30,7 +46,7 @@
           <el-table-column
             prop="questionName"
             label="小题名称"
-            width="60"
+            width="59"
             align="center"
           />
           <el-table-column
@@ -91,7 +107,7 @@
               :key="key"
               label=""
               :prop="key"
-              :min-width="[28, 50, 30, 320][index]"
+              :width="[28, 50, 30, 360][index]"
             >
               <template #default="{ row: org }">
                 <span v-if="key == 'registrationCodeList'">{{ org[key] }}</span>
@@ -131,6 +147,8 @@ interface TableItem {
   registrationCodeList?: string;
   hiddenRatio?: boolean;
   hiddenStu?: boolean;
+  rowspan?: number;
+  colspan?: number;
 }
 
 // 定义 Props
@@ -153,28 +171,388 @@ const props = defineProps({
   },
 });
 const tablePageData = ref<TableItem[][]>([]);
+const totalPageNum = ref(0);
 // 定义 Emits
 const emit = defineEmits(["PdfLoadEnd"]);
 
 // Refs & 常量
-const pageTitle = ref<HTMLElement | null>(null);
-const titleHeight = ref(80);
-const questionHeight = 30;
-const lineHeight = 16;
-const printPageHeight = 1205;
+const rootRef = ref<HTMLElement | null>(null); // 组件根节点
+const pageTitle = ref<HTMLElement | null>(null); // 标题测量块
+const printPageHeight = 1285; // 打印页固定高度(含上下内边距)
+const lineHeight = 23; // 单元格单行行高(与样式 .cell line-height:23px 一致)
+const rowBorder = 1; // 行下边框
+// 学号名单列每行容纳的条目数:行高 = 行数 × 23 + 1,行数 = ceil(条目数 / 每行条目数)。
+// 初始值 3.0,首次渲染后按浏览器实测行高自动校准(实测约 2.9)
+let codeItemsPerLine = 3.0;
+const headerHeight = 24; // 表头高度(23px 行高 + 1px 边框;td padding 已在样式清零,实测 24px)
+const miniHeaderHeight = 30; // 非首页页头固定高度(第几页总页数 + 打印时间)
+
+const LoadData = () => {
+  nextTick(() => {
+    TablePagesData();
+  });
+};
+
+// 学号名单列文本行数估算:浏览器按顿号等边界分词换行,
+// 初始按每行 3.0 个条目估算,首次渲染后依据真实行高自动校准 codeItemsPerLine,
+// 使估算行数与真实换行完全一致
+const getCodeLines = (code: string): number => {
+  const items = String(code).split("、").filter(Boolean).length;
+  return Math.max(1, Math.ceil(items / codeItemsPerLine));
+};
 
-// 表格分页逻辑
-const TablePagesData = () => {};
+// 行高:单行 = 23px 行高 + 1px 边框 = 24px;多行 = 行数 × 23 + 1。
+// 与浏览器实测完全一致(td 的 padding 已在样式清零,见 .el-table__cell)
+const getRowHeight = (row: TableItem): number => {
+  const key =
+    row.registrationCodeList != null ? String(row.registrationCodeList) : "";
+  let lines = 1;
+  if (key && key !== "-" && key !== "") {
+    lines = getCodeLines(key);
+  }
+  return lines * lineHeight + rowBorder;
+};
+
+// 表格分页逻辑:按题组(rowKey == 0 起始)分页,保证合并单元格连续;
+// 第 1 页包含动态高度页头(标题 + 第几页总页数 + 打印时间)+ 表头;
+// 其余页包含固定 30px 页头(第几页总页数 + 打印时间),无表头;
+// 单行高度放不下时按名单内容行内拆分,前部分留在当前页、剩余内容换页续排,
+// 尽量填满每页、减少底部空白;
+// 分页后依据真实渲染行高校准"每行条目数"并校正内容预算,直到页面不再溢出,
+// 避免估算偏差导致页面内容溢出或大面积留白
+const TablePagesData = () => {
+  const data = props.tableData as TableItem[];
+  // 第 1 页页头实测高度(标题可换行,高度动态获取)
+  const titleBlockHeight = pageTitle.value?.offsetHeight || 30;
+  // 打印页内容区高度:总高 - 上下内边距(校正过程中按需缩小)
+  const baseContentHeight = printPageHeight - 20;
+  let contentBudget = baseContentHeight;
+
+  const buildPages = (contentHeight: number): TableItem[][] => {
+    const pages: TableItem[][] = [];
+    if (!data.length) return pages;
+
+    // 第 1 页预算 = 内容高度 - 动态页头 - 表头;后续页预算 = 内容高度 - 固定页头(30px)
+    const page1Budget = contentHeight - titleBlockHeight - headerHeight;
+    const otherPageBudget = contentHeight - miniHeaderHeight;
+
+    // 每组(题)起始行下标
+    const groupStarts: number[] = [];
+    data.forEach((row, idx) => {
+      if (Number(row.rowKey) === 0) groupStarts.push(idx);
+    });
+    if (!groupStarts.length || groupStarts[0] !== 0) groupStarts.unshift(0);
+    groupStarts.push(data.length);
+
+    let current: TableItem[] = [];
+    let currentHeight = 0;
+    let firstPage = true;
+
+    const getBudget = () => (firstPage ? page1Budget : otherPageBudget);
+
+    const pushPage = () => {
+      if (current.length) {
+        pages.push(current);
+        current = [];
+        currentHeight = 0;
+        firstPage = false;
+      }
+    };
+
+    // 放入页面的行一律克隆,避免改动原数据
+    const cloneRows = (rows: TableItem[]): TableItem[] =>
+      rows.map((r) => ({ ...r }));
+
+    // 行内拆分:整行放不下时,把学号名单内容按条目边界切分为
+    // [当前页部分, 续排部分];前部分填满当前页剩余空白,剩余部分在下一页继续。
+    // 只按条目边界切分,不截断人名/学号;保证前部分一定放得进剩余高度
+    const splitRow = (
+      row: TableItem,
+      availableHeight: number,
+    ): TableItem[] | null => {
+      const code = row.registrationCodeList;
+      if (!code || code === "-" || code === "") return null;
+      // 整行放得下则无需拆分;剩余高度不足一行时也无法拆分
+      if (getRowHeight(row) <= availableHeight) return null;
+      if (availableHeight < lineHeight + rowBorder) return null;
+      const items = String(code).split("、").filter(Boolean);
+      if (items.length < 2) return null;
+      // 剩余高度可容纳的完整显示行数
+      const fitLines = Math.floor((availableHeight - 1) / lineHeight);
+      if (fitLines < 1) return null;
+      // 每行约 codeItemsPerLine 个条目,且至少给续排部分保留 1 个条目
+      const fitItems = Math.min(
+        items.length - 1,
+        Math.floor(fitLines * codeItemsPerLine),
+      );
+      if (fitItems < 1) return null;
+      let cut = 0;
+      for (let i = 0; i < fitItems; i++) {
+        cut += items[i].length + 1; // + 顿号
+      }
+      const rowA = {
+        ...row,
+        registrationCodeList: String(code).slice(0, cut),
+        rowspan: 1,
+        colspan: 1,
+      };
+      const rowB = {
+        ...row,
+        registrationCodeList: String(code).slice(cut),
+        rowspan: 1,
+        colspan: 1,
+      };
+      // rowA 行数 = ceil(fitItems/codeItemsPerLine) <= fitLines,其高度必 <= availableHeight
+      return [rowA, rowB];
+    };
+
+    for (let g = 0; g < groupStarts.length - 1; g++) {
+      const gStart = groupStarts[g];
+      const gEnd = groupStarts[g + 1];
+      let cursor = gStart;
+      // 行内拆分后待续的行(仍是 data[cursor] 的内容,未推进 cursor)
+      let pending: TableItem | null = null;
+
+      while (cursor < gEnd) {
+        const row = pending ?? data[cursor];
+        const remain = getBudget() - currentHeight;
+        const rowHeight = getRowHeight(row);
+
+        // A. 整组剩余部分可直接放下(仅非 pending 时检查)
+        if (!pending) {
+          let restHeight = 0;
+          for (let j = cursor; j < gEnd; j++)
+            restHeight += getRowHeight(data[j]);
+          if (restHeight <= remain) {
+            const rows = cloneRows(data.slice(cursor, gEnd));
+            current.push(...rows);
+            rows.forEach((r) => {
+              currentHeight += getRowHeight(r);
+            });
+            cursor = gEnd;
+            continue;
+          }
+        }
+
+        // B. 行能整行放入当前页(尽量多放几行填满)
+        if (remain >= rowHeight) {
+          if (pending) {
+            current.push(pending);
+            currentHeight += rowHeight;
+            pending = null;
+            cursor++;
+            // 放完待续行后不立即换页,继续用剩余空间排后续行,避免出现只有
+            // 单个剩余片段的半页
+            continue;
+          } else {
+            let count = 1;
+            let used = rowHeight;
+            while (
+              cursor + count < gEnd &&
+              used + getRowHeight(data[cursor + count]) <= remain
+            ) {
+              used += getRowHeight(data[cursor + count]);
+              count++;
+            }
+            const rows = cloneRows(data.slice(cursor, cursor + count));
+            current.push(...rows);
+            currentHeight += used;
+            cursor += count;
+          }
+          // 放行后不立即换页:剩余空间不足整行时,由下方分支 C 拆分当前行填满
+          continue;
+        }
+
+        // C. 剩余空间不足一行:行内拆分填满当前页,剩余内容下页继续
+        if (pending) {
+          // 拆分剩余片段不可再次拆分(否则会产生过小碎片、页数爆炸):
+          // 直接换页,由下一页完整续排
+          if (current.length === 0) {
+            // 空页仍放不下(超长行片段):再次按整页预算拆分续排,
+            // 避免强制放入导致页面内容溢出裁切;无法再拆时才强制放入
+            const sp2 = splitRow(pending, getBudget());
+            if (sp2) {
+              current.push(sp2[0]);
+              currentHeight += getRowHeight(sp2[0]);
+              pending = sp2[1];
+              continue;
+            }
+            const single = cloneRows([pending])[0];
+            current.push(single);
+            currentHeight += rowHeight;
+            pending = null;
+            cursor++;
+            if (cursor < gEnd) pushPage();
+          } else {
+            pushPage();
+          }
+          continue;
+        }
+        const sp = splitRow(row, remain);
+        if (sp) {
+          current.push(sp[0]);
+          currentHeight += getRowHeight(sp[0]);
+          pending = sp[1];
+          // 拆分片段放入后不立即换页:继续用剩余空间排后续行/片段,填满后再换页
+          continue;
+        } else if (current.length === 0) {
+          // 空页仍放不下且无法拆分:强制放入单行(溢出由 overflow hidden 兜底),防止死循环
+          const single = cloneRows([row])[0];
+          current.push(single);
+          currentHeight += rowHeight;
+          if (pending) {
+            pending = null;
+            cursor++;
+          } else {
+            cursor++;
+          }
+          if (cursor < gEnd) pushPage();
+        } else {
+          // 当前页放不下 → 换页后重试
+          pushPage();
+        }
+      }
+    }
+
+    pushPage();
+
+    // 统一修正每页合并单元格:
+    // 每页第一行必须作为合并锚点,保证前 8 列(小题名称~正确答案)在每一页都可见;
+    // 页内遇到新的组起点(rowKey==0)或拆分片段(rowspan)时重新开启一个锚点,
+    // 锚点 rowspan 覆盖到下一个锚点前的所有行,其余行被合并隐藏
+    pages.forEach((page) => {
+      if (!page.length) return;
+      let anchorIdx = 0;
+      for (let i = 1; i < page.length; i++) {
+        if (Number(page[i].rowKey) === 0 || page[i].rowspan) {
+          page[anchorIdx].rowspan = i - anchorIdx;
+          page[anchorIdx].colspan = 1;
+          anchorIdx = i;
+        }
+      }
+      page[anchorIdx].rowspan = page.length - anchorIdx;
+      page[anchorIdx].colspan = 1;
+    });
+
+    return pages;
+  };
+
+  // 第一轮:按估算高度分页并渲染
+  let pages = buildPages(contentBudget);
+  tablePageData.value = pages;
+  totalPageNum.value = pages.length;
+
+  // 后续轮次:渲染后按实际渲染情况修正分页,直到收敛(最多 8 轮):
+  // 1) 采集每行真实高度反推"学号名单列每行条目数"(校准 codeItemsPerLine),
+  //    保证行高估算与真实渲染一致 → 页面能排满;
+  // 2) 仍有页面溢出时,按精确溢出像素安全缩减预算(每轮最多 40px、下限 60%),
+  //    防止出现"每页只有一行 + 大面积留白"的坍缩
+  const correctPages = (attempt: number) => {
+    // 等 el-table 完成内部布局后再测量
+    setTimeout(() => {
+      nextTick(() => {
+        const els = rootRef.value?.querySelectorAll(".print_page");
+        if (!els || !els.length) return;
+
+        // 1) 校准学号名单列每行条目数:行高 h → 显示行数 lines = (h-1)/23,
+        //    加权平均 items/lines 得到真实每行条目数
+        let totalItems = 0;
+        let totalLines = 0;
+        els.forEach((el) => {
+          el.querySelectorAll("tbody tr").forEach((tr) => {
+            const lastCell = tr.querySelector("td:last-child .cell");
+            const text = lastCell?.textContent?.trim() ?? "";
+            const items = String(text).split("、").filter(Boolean).length;
+            if (!items) return;
+            const h = tr.getBoundingClientRect().height;
+            const lines = Math.max(1, Math.round((h - rowBorder) / lineHeight));
+            totalItems += items;
+            totalLines += lines;
+          });
+        });
+        if (totalLines > 0) {
+          const measured = totalItems / totalLines;
+          if (
+            Math.abs(measured - codeItemsPerLine) > codeItemsPerLine * 0.05 &&
+            attempt < 8
+          ) {
+            codeItemsPerLine = Math.round(measured * 10) / 10;
+            console.log(
+              "[CALIBRATE]",
+              JSON.stringify({ attempt, itemsPerLine: codeItemsPerLine }),
+            );
+            const p = buildPages(contentBudget);
+            tablePageData.value = p;
+            totalPageNum.value = p.length;
+            correctPages(attempt + 1);
+            return;
+          }
+        }
+
+        // 2) 检查每页表格内容是否超出内容预算
+        let maxOverflow = 0;
+        els.forEach((el, i) => {
+          const body = el.querySelector(".el-table__body-wrapper");
+          const bodyH = body ? body.getBoundingClientRect().height : 0;
+          if (!bodyH) return;
+          // 实测页头高度:第 1 页为动态高度,其余页为固定 30px
+          const headerH =
+            el.querySelector(".area_title")?.getBoundingClientRect().height ??
+            0;
+          // 第 1 页含表头,其余页无表头
+          const budget =
+            i === 0
+              ? contentBudget - headerH - headerHeight
+              : contentBudget - headerH;
+          if (bodyH > budget + 1) {
+            maxOverflow = Math.max(maxOverflow, bodyH - budget);
+          }
+        });
+
+        // 3) 有溢出:按精确溢出像素缩减预算重新分页(每轮最多减 40px,
+        //    避免一次大幅缩减导致各页出现大面积底部留白;下限 60% 防坍缩)
+        if (maxOverflow > 1 && attempt < 8) {
+          const shrink = Math.min(maxOverflow, 40);
+          contentBudget -= Math.ceil(shrink);
+          const floor = Math.max(100, (printPageHeight - 20) * 0.6);
+          if (contentBudget < floor) contentBudget = floor;
+          console.log(
+            "[CORRECT]",
+            JSON.stringify({
+              attempt,
+              overflow: +maxOverflow.toFixed(1),
+              budget: +contentBudget.toFixed(1),
+            }),
+          );
+          const p = buildPages(contentBudget);
+          tablePageData.value = p;
+          totalPageNum.value = p.length;
+          correctPages(attempt + 1);
+          return;
+        }
+      });
+    }, 0);
+  };
+  correctPages(1);
+};
 
 // 下载 PDF (使用 dom-to-image)
 const DownloadPdf = async () => {
-  const elements = document.querySelectorAll(".page_report .print_page");
+  const elements = rootRef.value?.querySelectorAll(".print_page") || [];
   if (elements.length === 0) {
-    ElMessage.warning("未找到可导出的页面元素!");
+    ElMessage.warning("未找到可导出的页面内容,请先加载数据!");
     emit("PdfLoadEnd");
     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();
@@ -231,19 +609,62 @@ const DownloadPdf = async () => {
     }
   }
 
-  pdf.save(
-    `${props.examName}_${props.subjectName}_${props.className}错题名单.pdf`,
+  // pdf.save(
+  //   `${props.examName}_${props.subjectName}_${props.className}错题名单.pdf`,
+  // );
+  // // 直接打开生成的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 = `${props.examName}_${props.subjectName}_${props.className}错题名单.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();
   emit("PdfLoadEnd");
 };
-const LoadData = () => {
-  nextTick(() => {
-    const pageTitleHeight = pageTitle.value?.offsetHeight || 60;
-    titleHeight.value = pageTitleHeight + 20;
-    tablePageData.value = props.tableData;
-    TablePagesData();
-  });
-};
 // 错题分析  合并单元格
 const SpanMethod = ({ row, column }: any) => {
   const props = [
@@ -288,9 +709,12 @@ const ErrorTableCellClassName = ({ column }: any) => {
 // 监听筛选条件
 watch(
   () => props.tableData,
-  async () => {
+  () => {
     if (props.tableData.length) {
       LoadData();
+    } else {
+      tablePageData.value = [];
+      totalPageNum.value = 0;
     }
   },
   { deep: true },
@@ -306,7 +730,7 @@ defineExpose({
 .error {
   width: 908px;
   margin: 0 auto;
-  // position: absolute;
+  position: absolute;
   top: 0;
   left: -9999px;
   z-index: -10;
@@ -350,6 +774,14 @@ defineExpose({
       }
     }
 
+    // 非首页页头:仅页码 + 打印时间,固定高度 30px
+    .mini_area_title {
+      height: 30px;
+      flex-shrink: 0;
+      overflow: hidden;
+      white-space: nowrap;
+    }
+
     .module_table {
       width: 100%;
       box-sizing: border-box;
@@ -399,6 +831,10 @@ defineExpose({
 
         .el-table__cell {
           height: 23px !important;
+          // 关键:element-plus 默认给 td 保留上下各 8px 内边距(共 16px),
+          // 若不在此清零,真实行高 = 23+16+1 = 40px,与分页算法按 23+1=24px
+          // 估算不一致,导致页面溢出、预算坍缩、每页只剩 1 行
+          padding: 0 !important;
           &.white_space_normal {
             .cell {
               white-space: normal;

+ 8 - 7
src/components/ScorePdf.vue

@@ -22,7 +22,7 @@
         <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" :width="state.groupDataTitleData.length ? '70px' : 'auto'">
+              :label="item.label" min-width="5">
               <template #default="scope">
                 <span v-if="scope.row.score != '缺考' && scope.row.score != '违纪' && item.prop === 'studentUserName'">
                   {{ scope.row[item.prop] }}
@@ -32,8 +32,8 @@
                 </template>
               </template>
             </el-table-column>
-            <el-table-column v-for="(item, index) in state.groupDataTitleData" :key="item.prop" :prop="item.prop"
-              :label="item.label">
+            <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>
@@ -145,7 +145,7 @@ const GetTableData = async () => {
 //分页:超过一页自动换页,后续页不包含标题行和应考/实考/缺考统计行
 const tablePageData = (list: any[]) => {
   const trLineHeight = 21;//表格表头、tr行高
-  const printPageHeight = 1285;//打印页面高度
+  const printPageHeight = 1260;//打印页面高度 减掉padding 24px
 
   const rowNum = Math.floor(printPageHeight / trLineHeight);//每页可容纳行数 向下取整
   const firstPageRows = rowNum - 3;//第一页数据行数:标题行+统计行+表头行占3行
@@ -266,7 +266,7 @@ const ExportPdf = async () => {
   // );
   // win.location.replace(url);
   // 在浏览器新窗口打开生成的PDF(中转页方案:窗口标题、下载文件名均使用指定名称)
-  const filename = `${examName.value}成绩单.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" }),
@@ -289,7 +289,7 @@ const ExportPdf = async () => {
   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 button{color:#fff;background:#2e64fa;border:0;padding:6px 16px;border-radius:4px;font-size:13px;cursor:pointer;flex-shrink:0;}
+  .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;}
@@ -300,7 +300,7 @@ const ExportPdf = async () => {
 <body>
   <div class="bar">
     <span class="fname">${safeName}</span>
-    <button onclick="window.print()">下载PDF</button>
+    <a class="btn" href="${url}" download="${safeName}">下载PDF</a>
   </div>
   <iframe src="${url}"></iframe>
 </body>
@@ -432,6 +432,7 @@ defineExpose({
           white-space: nowrap;
           padding: 0 0 0 4px !important;
           color: #000000;
+          text-overflow: initial !important;
         }
       }
     }

+ 6 - 6
src/views/analysis/errorAnalysis.vue

@@ -127,28 +127,28 @@
       </el-table>
     </template>
   </ReportModule>
-  <ErrorsPdf
+  <!-- <ErrorsPdf
     ref="errPdfReport"
     :tableData="state.tableData"
     :className="analysisStore.filterObject.classGroupName"
     :subjectName="analysisStore.filterObject.subjectName || ''"
     :examName="getExamName"
     @PdfLoadEnd="PdfLoadEnd"
-  />
-  <!-- <ErrorsTablePdf
+  /> -->
+  <ErrorsTablePdf
     ref="errPdfReport"
     :tableData="state.tableData"
     :className="analysisStore.filterObject.classGroupName"
     :subjectName="analysisStore.filterObject.subjectName || ''"
     :examName="getExamName"
     @PdfLoadEnd="PdfLoadEnd"
-  /> -->
+  />
 </template>
 <script lang="ts" setup>
 import ReportModule from "@/components/ReportModule.vue";
 import { useAnalysisStore } from "@/store/analysis";
-import ErrorsPdf from "@/components/ErrorsPdf.vue";
-// import ErrorsTablePdf from "@/components/ErrorsTablePdf.vue";
+// import ErrorsPdf from "@/components/ErrorsPdf.vue";
+import ErrorsTablePdf from "@/components/ErrorsTablePdf.vue";
 import { onMounted, reactive, computed, ref, watch } from "vue";
 import { errorQuestionAnalysis, exportErrorQuestion } from "@/api/analysis";
 import { loadingSvgIcon } from "@/utils/common";

+ 1 - 1
src/views/analysis/reportDownload.vue

@@ -416,7 +416,7 @@ function groupAnalysisModule(): ExcelModule {
 function selectQuestionAnalysisModule(): ExcelModule {
   let excelsList: ExcelsItem[] = [
     {
-      label: "客观题分析表",
+      label: "选项分析表",
       value: "selectQuestionAnalysis",
     },
   ];

+ 1 - 1
src/views/analysis/score.vue

@@ -485,7 +485,7 @@ const ConfirmEnter = () => {
     })
     .catch((err) => {
       console.error("导出成绩单PDF失败:", err);
-      ElMessage.error("导出PDF失败,请重试");
+      // ElMessage.error("导出PDF失败,请重试");
     })
     .finally(() => {
       buttonLoading.value = false;