|
@@ -107,7 +107,7 @@
|
|
|
:key="key"
|
|
:key="key"
|
|
|
label=""
|
|
label=""
|
|
|
:prop="key"
|
|
:prop="key"
|
|
|
- :width="[28, 50, 30, 360][index]"
|
|
|
|
|
|
|
+ :width="[28, 50, 30, 348][index]"
|
|
|
>
|
|
>
|
|
|
<template #default="{ row: org }">
|
|
<template #default="{ row: org }">
|
|
|
<span v-if="key == 'registrationCodeList'">{{ org[key] }}</span>
|
|
<span v-if="key == 'registrationCodeList'">{{ org[key] }}</span>
|
|
@@ -182,8 +182,12 @@ const printPageHeight = 1285; // 打印页固定高度(含上下内边距)
|
|
|
const lineHeight = 23; // 单元格单行行高(与样式 .cell line-height:23px 一致)
|
|
const lineHeight = 23; // 单元格单行行高(与样式 .cell line-height:23px 一致)
|
|
|
const rowBorder = 1; // 行下边框
|
|
const rowBorder = 1; // 行下边框
|
|
|
// 学号名单列每行容纳的条目数:仅用于首轮估算行数(行数 = ceil(条目数 / 每行条目数)),
|
|
// 学号名单列每行容纳的条目数:仅用于首轮估算行数(行数 = ceil(条目数 / 每行条目数)),
|
|
|
-// 首轮渲染后会按每行真实渲染高度重新分页,因此该值无需精确
|
|
|
|
|
-let codeItemsPerLine = 3.0;
|
|
|
|
|
|
|
+// 该列宽 348px 内,3 字姓名实测约 8 个/行;首轮按 7.0 保守估算(低于真实容量,
|
|
|
|
|
+// 防止估算偏密导致页面内容溢出),首轮渲染后由 correctPages 按实测行高反推更准的密度。
|
|
|
|
|
+// 注意:旧值 3.0 按每行仅 3 个条目估算行数,行内拆分时一次只切下 3 个/行的片段,
|
|
|
|
|
+// 与真实渲染(约 8 个/行)相差 2.6 倍,页面可排内容被严重低估、过早换页,
|
|
|
|
|
+// 是各页出现 87~763px 不规则底部留白的根因之一
|
|
|
|
|
+let codeItemsPerLine = 7.0;
|
|
|
const headerHeight = 24; // 表头高度(23px 行高 + 1px 边框;td padding 已在样式清零,实测 24px)
|
|
const headerHeight = 24; // 表头高度(23px 行高 + 1px 边框;td padding 已在样式清零,实测 24px)
|
|
|
const miniHeaderHeight = 30; // 非首页页头固定高度(第几页总页数 + 打印时间)
|
|
const miniHeaderHeight = 30; // 非首页页头固定高度(第几页总页数 + 打印时间)
|
|
|
|
|
|
|
@@ -200,7 +204,8 @@ const LoadData = () => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 学号名单列文本行数估算(仅首轮用):浏览器按顿号等边界分词换行,
|
|
// 学号名单列文本行数估算(仅首轮用):浏览器按顿号等边界分词换行,
|
|
|
-// 首轮按每行 3.0 个条目估算,渲染后以实测行高重新分页,估算不准会被实测修正
|
|
|
|
|
|
|
+// 首轮按每行 7.0 个条目估算(实测列宽约容纳 8 个 3 字姓名,7.0 留安全余量),
|
|
|
|
|
+// 渲染后以实测行高重新分页,估算不准会被实测修正
|
|
|
const getCodeLines = (code: string): number => {
|
|
const getCodeLines = (code: string): number => {
|
|
|
const items = String(code).split("、").filter(Boolean).length;
|
|
const items = String(code).split("、").filter(Boolean).length;
|
|
|
return Math.max(1, Math.ceil(items / codeItemsPerLine));
|
|
return Math.max(1, Math.ceil(items / codeItemsPerLine));
|
|
@@ -231,11 +236,13 @@ const getRowHeight = (row: TableItem): number => {
|
|
|
const TablePagesData = () => {
|
|
const TablePagesData = () => {
|
|
|
// 数据更新后清空旧行高缓存,重新采集
|
|
// 数据更新后清空旧行高缓存,重新采集
|
|
|
measuredRowHeights.clear();
|
|
measuredRowHeights.clear();
|
|
|
|
|
+ // 每行条目数密度按本次数据重新学习,避免沿用上次数据的学习结果
|
|
|
|
|
+ codeItemsPerLine = 7.0;
|
|
|
const data = props.tableData as TableItem[];
|
|
const data = props.tableData as TableItem[];
|
|
|
// 第 1 页页头实测高度(标题可换行,高度动态获取)
|
|
// 第 1 页页头实测高度(标题可换行,高度动态获取)
|
|
|
const titleBlockHeight = pageTitle.value?.offsetHeight || 30;
|
|
const titleBlockHeight = pageTitle.value?.offsetHeight || 30;
|
|
|
// 打印页内容区高度:总高 - 上下内边距(校正过程中按需缩小)
|
|
// 打印页内容区高度:总高 - 上下内边距(校正过程中按需缩小)
|
|
|
- const baseContentHeight = printPageHeight - 20;
|
|
|
|
|
|
|
+ const baseContentHeight = printPageHeight - 30;
|
|
|
let contentBudget = baseContentHeight;
|
|
let contentBudget = baseContentHeight;
|
|
|
|
|
|
|
|
const buildPages = (contentHeight: number): TableItem[][] => {
|
|
const buildPages = (contentHeight: number): TableItem[][] => {
|
|
@@ -452,11 +459,13 @@ const TablePagesData = () => {
|
|
|
tablePageData.value = pages;
|
|
tablePageData.value = pages;
|
|
|
totalPageNum.value = pages.length;
|
|
totalPageNum.value = pages.length;
|
|
|
|
|
|
|
|
- // 后续轮次:渲染后按实际渲染情况修正分页,直到收敛(最多 8 轮):
|
|
|
|
|
|
|
+ // 后续轮次:渲染后按实际渲染情况修正分页,直到收敛(最多 12 轮):
|
|
|
// 1) 采集每行真实渲染高度(key = 学号名单列文本),用实测高度重新分页,
|
|
// 1) 采集每行真实渲染高度(key = 学号名单列文本),用实测高度重新分页,
|
|
|
// 保证每页排满——估算按"每行条目数"换算行数,与实际换行存在偏差
|
|
// 保证每页排满——估算按"每行条目数"换算行数,与实际换行存在偏差
|
|
|
// (名单长短不同、条目宽度不同),实测是最准确的校正依据;
|
|
// (名单长短不同、条目宽度不同),实测是最准确的校正依据;
|
|
|
- // 2) 仍有页面溢出时,按精确溢出像素安全缩减预算(每轮最多 40px、下限 60%),
|
|
|
|
|
|
|
+ // 2) 按实测行高反推每行条目数密度(codeItemsPerLine),使行内拆分的
|
|
|
|
|
+ // 片段大小贴合真实换行,减少因估算偏疏/偏密导致的反复重排;
|
|
|
|
|
+ // 3) 仍有页面溢出时,按精确溢出像素安全缩减预算(每轮最多 40px、下限 60%),
|
|
|
// 防止出现"每页只有一行 + 大面积留白"的坍缩
|
|
// 防止出现"每页只有一行 + 大面积留白"的坍缩
|
|
|
const correctPages = (attempt: number, lastCacheKey: string) => {
|
|
const correctPages = (attempt: number, lastCacheKey: string) => {
|
|
|
// 等 el-table 完成内部布局后再测量
|
|
// 等 el-table 完成内部布局后再测量
|
|
@@ -481,6 +490,26 @@ const TablePagesData = () => {
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // 1.5) 用实测行高反推"每行条目数"密度,供后续轮次的行内拆分/行数估算使用:
|
|
|
|
|
+ // 小片段(行数 < 4)因取整误差大且方向偏小,会拉低密度,故只统计足够高的行;
|
|
|
|
|
+ // 取观测上界 × 0.9(行尾不满的行密度偏低,上界才接近真实容量,0.9 留余量),
|
|
|
|
|
+ // 上限 7.0 保证对未知数据保守(3 字姓名实测约 8 个/行,7.0 低于真实容量,
|
|
|
|
|
+ // 估算行数只会偏多、不会偏少,杜绝页面内容溢出),长姓名等密集数据
|
|
|
|
|
+ // 的观测密度较低,会自动学到更小的值
|
|
|
|
|
+ let maxPerLine = 0;
|
|
|
|
|
+ measuredRowHeights.forEach((h, text) => {
|
|
|
|
|
+ const items = String(text).split("、").filter(Boolean).length;
|
|
|
|
|
+ if (items > 1) {
|
|
|
|
|
+ const lines = Math.max(1, Math.round(h / lineHeight));
|
|
|
|
|
+ if (lines >= 4) {
|
|
|
|
|
+ maxPerLine = Math.max(maxPerLine, items / lines);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (maxPerLine > 0) {
|
|
|
|
|
+ codeItemsPerLine = Math.max(1.5, Math.min(7.0, maxPerLine * 0.9));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 2) 检查每页表格内容是否超出内容预算
|
|
// 2) 检查每页表格内容是否超出内容预算
|
|
|
let maxOverflow = 0;
|
|
let maxOverflow = 0;
|
|
|
els.forEach((el, i) => {
|
|
els.forEach((el, i) => {
|
|
@@ -503,10 +532,10 @@ const TablePagesData = () => {
|
|
|
|
|
|
|
|
// 3) 有溢出:按精确溢出像素缩减预算重新分页(每轮最多减 40px,
|
|
// 3) 有溢出:按精确溢出像素缩减预算重新分页(每轮最多减 40px,
|
|
|
// 避免一次大幅缩减导致各页出现大面积底部留白;下限 60% 防坍缩)
|
|
// 避免一次大幅缩减导致各页出现大面积底部留白;下限 60% 防坍缩)
|
|
|
- if (maxOverflow > 1 && attempt < 8) {
|
|
|
|
|
|
|
+ if (maxOverflow > 1 && attempt < 12) {
|
|
|
const shrink = Math.min(maxOverflow, 40);
|
|
const shrink = Math.min(maxOverflow, 40);
|
|
|
contentBudget -= Math.ceil(shrink);
|
|
contentBudget -= Math.ceil(shrink);
|
|
|
- const floor = Math.max(100, (printPageHeight - 20) * 0.6);
|
|
|
|
|
|
|
+ const floor = Math.max(100, (printPageHeight - 30) * 0.6);
|
|
|
if (contentBudget < floor) contentBudget = floor;
|
|
if (contentBudget < floor) contentBudget = floor;
|
|
|
console.log(
|
|
console.log(
|
|
|
"[CORRECT]",
|
|
"[CORRECT]",
|
|
@@ -525,7 +554,7 @@ const TablePagesData = () => {
|
|
|
|
|
|
|
|
// 4) 无溢出但实测行高有更新(首次采集或拆分片段变化):用真实行高重新分页,
|
|
// 4) 无溢出但实测行高有更新(首次采集或拆分片段变化):用真实行高重新分页,
|
|
|
// 把每页排满;同一文本实测高度稳定,重排一次即收敛
|
|
// 把每页排满;同一文本实测高度稳定,重排一次即收敛
|
|
|
- if (cacheKey !== lastCacheKey && attempt < 8) {
|
|
|
|
|
|
|
+ if (cacheKey !== lastCacheKey && attempt < 12) {
|
|
|
const p = buildPages(contentBudget);
|
|
const p = buildPages(contentBudget);
|
|
|
tablePageData.value = p;
|
|
tablePageData.value = p;
|
|
|
totalPageNum.value = p.length;
|
|
totalPageNum.value = p.length;
|
|
@@ -582,17 +611,17 @@ const DownloadPdf = async () => {
|
|
|
// "white-space": "normal",
|
|
// "white-space": "normal",
|
|
|
// position: "relative",
|
|
// position: "relative",
|
|
|
// visibility: "visible",
|
|
// visibility: "visible",
|
|
|
- 'transform': 'scale(2)',
|
|
|
|
|
- 'transform-origin': 'top left',
|
|
|
|
|
- 'background-color': '#ffffff',
|
|
|
|
|
|
|
+ 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'
|
|
|
|
|
|
|
+ "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",
|
|
bgcolor: "#ffffff",
|
|
|
});
|
|
});
|
|
@@ -601,12 +630,23 @@ const DownloadPdf = async () => {
|
|
|
const imgWidth = imgProps.width;
|
|
const imgWidth = imgProps.width;
|
|
|
const imgHeight = imgProps.height;
|
|
const imgHeight = imgProps.height;
|
|
|
|
|
|
|
|
|
|
+ // 打印安全边距(单位 pt,1pt ≈ 0.353mm)。
|
|
|
|
|
+ // 打印机存在不可打印边距(常见 4.5~6.3mm),若图片铺满整页,
|
|
|
|
|
+ // 表格左右边框距页面边缘仅约 3.5mm,打印时会被打印机物理裁掉
|
|
|
|
|
+ // (中间列竖线在页面内部不受影响)。故四周留出安全边距:
|
|
|
|
|
+ // 14pt 时左右边框距页面边缘约 6.6mm、底部约 6.9mm、顶部约 13.7mm,
|
|
|
|
|
+ // 均超过打印机极限边距 6.3mm,可放心打印,同时留白最小。
|
|
|
|
|
+ // 若个别打印机边距更大,打印时选择"适应页面/缩小以适合可打印区域"兜底。
|
|
|
|
|
+ // const margin = 14; // 14pt ≈ 4.9mm
|
|
|
|
|
+ // const availWidth = pdfWidth - margin * 2;
|
|
|
|
|
+ // const availHeight = pdfHeight - margin * 2;
|
|
|
|
|
+ // const ratio = Math.min(availWidth / imgWidth, availHeight / imgHeight);
|
|
|
const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight);
|
|
const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight);
|
|
|
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;
|
|
|
- const yPosition = (pdfHeight - adjustHeight) / 2;
|
|
|
|
|
|
|
+ const yPosition = 0;//(pdfHeight - adjustHeight) / 2;
|
|
|
|
|
|
|
|
if (pageIndex > 0) {
|
|
if (pageIndex > 0) {
|
|
|
pdf.addPage();
|
|
pdf.addPage();
|
|
@@ -751,7 +791,7 @@ defineExpose({
|
|
|
.area_page {
|
|
.area_page {
|
|
|
width: 908px;
|
|
width: 908px;
|
|
|
margin-top: 20px;
|
|
margin-top: 20px;
|
|
|
- padding: 10px !important;
|
|
|
|
|
|
|
+ padding: 8px 15px 0 !important;
|
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
@@ -765,6 +805,8 @@ defineExpose({
|
|
|
line-height: 30px;
|
|
line-height: 30px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
gap: 5px;
|
|
gap: 5px;
|
|
|
|
|
+ padding: 0 5px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
|
|
|
.page_num {
|
|
.page_num {
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
@@ -800,11 +842,32 @@ defineExpose({
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
border-collapse: collapse;
|
|
border-collapse: collapse;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
:deep(.el-table) {
|
|
:deep(.el-table) {
|
|
|
|
|
+ // --el-table-border-color: #e9e9e9;
|
|
|
|
|
+ // --el-table-header-border-color: #e9e9e9;
|
|
|
|
|
+ // border-left: 1px solid #e9e9e9;
|
|
|
|
|
+ // border-right: 1px solid #e9e9e9;
|
|
|
|
|
+ // border-top: 1px solid #e9e9e9;
|
|
|
|
|
+ // border-radius: 6px;
|
|
|
|
|
+ // box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ /* 重置表格边框渲染规则,避免伪元素干扰 */
|
|
|
|
|
+ --el-table-border-color: #e9e9e9 !important;
|
|
|
|
|
+ --el-table-header-border-color: #e9e9e9;
|
|
|
|
|
+ border-collapse: collapse !important;
|
|
|
|
|
+ border: 1px solid #e9e9e9 !important;
|
|
|
border-radius: 6px;
|
|
border-radius: 6px;
|
|
|
- border-left: 0px;
|
|
|
|
|
- border-right: 0px;
|
|
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ // element-plus 的左右/顶部边框用伪元素绘制,dom-to-image 截图不渲染伪元素,
|
|
|
|
|
+ // 故禁用伪元素边框,由上面真实边框替代,保证 PDF/打印中左右边框可见
|
|
|
|
|
+ &::before,
|
|
|
|
|
+ &::after,
|
|
|
|
|
+ .el-table__inner-wrapper::before,
|
|
|
|
|
+ .el-table__inner-wrapper::after {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
thead {
|
|
thead {
|
|
|
height: 23px !important;
|
|
height: 23px !important;
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|