优化打印模板行高估算逻辑,新增文本换行估算功能,调整行高计算以适应不同内容类型,确保打印预览准确性和用户体验。增加防裁切余量和行高微调系数,改善分页处理。

This commit is contained in:
geht
2026-04-20 10:04:38 +08:00
parent ffb6f2bd0a
commit 5ce732fe72

View File

@@ -157,7 +157,8 @@ async function renderTablePage(
}), }),
) )
).join(''); ).join('');
return `<tr style="height:${element.rowHeight}mm;">${cells}</tr>`; // 使用 min-height长文本/二维码撑高行时以内容为准,避免固定 height 与实高不一致导致分页估算偶发偏差
return `<tr style="min-height:${element.rowHeight}mm;">${cells}</tr>`;
}), }),
) )
).join(''); ).join('');
@@ -673,6 +674,46 @@ function resolveDetailTableChunkTopMm(
const MM_TO_CSS_PX = 96 / 25.4; const MM_TO_CSS_PX = 96 / 25.4;
const CSS_PX_PER_MM = MM_TO_CSS_PX; const CSS_PX_PER_MM = MM_TO_CSS_PX;
/** 版心底部保留的防裁切余量mm。过大则本页明明还能放 12 行却提前换页导致大块留白;过小易压分页缝 */
const AUTO_PAGE_CHUNK_INSET_MM = 1.4;
/** 累计行高微调:略大于 1 防亚像素/线宽误差,不宜过大以免总行高膨胀、底部空白 */
const AUTO_PAGE_ROW_HEIGHT_FIT_FACTOR = 1.03;
/** 判断是否视为「全角级」占宽,用于换行行数估算(拉丁字母约 0.62em,汉字约 1em */
function isFullWidthWrapCodePoint(cp: number): boolean {
return (
(cp >= 0x2e80 && cp <= 0x9fff) || // CJK/CJK 部首等
(cp >= 0x3040 && cp <= 0x30ff) || // 假名
(cp >= 0xac00 && cp <= 0xd7af) || // 谚文音节
(cp >= 0xf900 && cp <= 0xfaff) || // 兼容汉字
(cp >= 0xff00 && cp <= 0xffef)
); // 全角ASCII
}
/**
* 按列宽与字体估算换行宽度单位拉丁≈0.62、CJK≈1避免纯用字符数/拉丁 charWidth 低估中文行数导致偶发跨页裁切。
*/
function estimateTextWrapLines(text: string, innerWpx: number, fontSize: number): number {
if (!text.length) {
return 1;
}
let units = 0;
for (const ch of text) {
const cp = ch.codePointAt(0)!;
if (isFullWidthWrapCodePoint(cp)) {
units += 1;
} else if (/\s/.test(ch)) {
units += 0.22;
} else {
units += 0.62;
}
}
units = Math.max(0.01, units);
// 每行可容纳的等效「全角」数0.97 在防中文低估与避免行数虚高之间折中
const unitsPerLine = Math.max(1.8, (innerWpx / Math.max(1, fontSize)) * 0.97);
return Math.max(1, Math.ceil(units / unitsPerLine));
}
/** /**
* 估算表格数据行在纸面上的高度mm。换行/二维码等会使实际行高远大于设计 rowHeight * 估算表格数据行在纸面上的高度mm。换行/二维码等会使实际行高远大于设计 rowHeight
* 若仍按固定 rowHeight 分页会导致整表落在同一 chunk、页数为 1 且预览溢出灰底。 * 若仍按固定 rowHeight 分页会导致整表落在同一 chunk、页数为 1 且预览溢出灰底。
@@ -681,6 +722,8 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
const baseRow = Math.max(0.01, Number(element.rowHeight || 8)); const baseRow = Math.max(0.01, Number(element.rowHeight || 8));
let maxH = baseRow; let maxH = baseRow;
const padHmm = 4; // 与单元格 padding:2mm 上下大致相当 const padHmm = 4; // 与单元格 padding:2mm 上下大致相当
// 图片类内容略加一点,避免略估;过多会整体抬高行高、加重页底留白
const mediaExtraHmm = 1.25;
for (const column of columns) { for (const column of columns) {
const fieldKey = column.bindField || column.field; const fieldKey = column.bindField || column.field;
@@ -697,15 +740,15 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
if (contentType === 'qrcode' || contentType === 'barcode') { if (contentType === 'qrcode' || contentType === 'barcode') {
const fillCell = column?.fillCell !== false; const fillCell = column?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100))); const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100)));
const sideMm = fillCell ? colWMm * 0.92 : colWMm * (scale / 100) * 0.92; const sideMm = fillCell ? colWMm * 0.93 : colWMm * (scale / 100) * 0.93;
maxH = Math.max(maxH, sideMm + padHmm); maxH = Math.max(maxH, sideMm + padHmm + mediaExtraHmm);
continue; continue;
} }
if (contentType === 'image') { if (contentType === 'image') {
const fillCell = column?.fillCell !== false; const fillCell = column?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100))); const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100)));
const hMm = fillCell ? colWMm * 0.62 : colWMm * (scale / 100) * 0.62; const hMm = fillCell ? colWMm * 0.65 : colWMm * (scale / 100) * 0.65;
maxH = Math.max(maxH, hMm + padHmm); maxH = Math.max(maxH, hMm + padHmm + mediaExtraHmm * 0.6);
continue; continue;
} }
if (nowrap) { if (nowrap) {
@@ -718,10 +761,8 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
continue; continue;
} }
const colWpx = colWMm * CSS_PX_PER_MM; const colWpx = colWMm * CSS_PX_PER_MM;
const charWpx = Math.max(1, fontSize * 0.62);
const innerWpx = Math.max(8, colWpx - 2 * CSS_PX_PER_MM * 2); const innerWpx = Math.max(8, colWpx - 2 * CSS_PX_PER_MM * 2);
const charsPerLine = Math.max(4, Math.floor(innerWpx / charWpx)); const lines = estimateTextWrapLines(text, innerWpx, fontSize);
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
const lineHeightPx = fontSize * 1.3; const lineHeightPx = fontSize * 1.3;
const textHmm = (lines * lineHeightPx) / CSS_PX_PER_MM; const textHmm = (lines * lineHeightPx) / CSS_PX_PER_MM;
maxH = Math.max(maxH, textHmm + padHmm, baseRow); maxH = Math.max(maxH, textHmm + padHmm, baseRow);
@@ -729,17 +770,23 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
return Math.max(baseRow, maxH); return Math.max(baseRow, maxH);
} }
function sumRowHeightsMm(heights: number[], from: number, count: number): number { /** 分页累加用行高:在估算值上乘以系数,使「能否放下整行」更贴近预览/打印 */
function autoPagePessimisticRowHeightMm(rawMm: number): number {
return Math.max(0.01, rawMm * AUTO_PAGE_ROW_HEIGHT_FIT_FACTOR);
}
function sumPessimisticRowHeightsMm(heights: number[], from: number, count: number): number {
let s = 0; let s = 0;
const end = Math.min(from + count, heights.length); const end = Math.min(from + count, heights.length);
for (let j = from; j < end; j += 1) { for (let j = from; j < end; j += 1) {
s += heights[j]; s += autoPagePessimisticRowHeightMm(heights[j]);
} }
return s; return s;
} }
/** /**
* autoPage 明细表:按版心高度、表头顶、行高与页脚占位拆分数据行(与浏览器打印分页接近) * autoPage 明细表:按版心高度、表头顶、行高与页脚占位拆分数据行(与浏览器打印分页接近)
* 画布上每个 table/detailTable含多个不同 source 的明细表)均独立调用,避免仅首张表分页合理、其余表跨缝裁切。
*/ */
export function computeAutoPageRowChunks( export function computeAutoPageRowChunks(
element: NativeTableElement, element: NativeTableElement,
@@ -781,7 +828,7 @@ export function computeAutoPageRowChunks(
: repeatFreeConstrains : repeatFreeConstrains
? Math.max(rowH, innerH - y0 - headerH) ? Math.max(rowH, innerH - y0 - headerH)
: Math.max(rowH, innerH - headerH - band); : Math.max(rowH, innerH - headerH - band);
const safeAvail = Math.max(rowH, avail); const safeAvail = Math.max(rowH, avail - AUTO_PAGE_CHUNK_INSET_MM);
let maxBodyMm = safeAvail; let maxBodyMm = safeAvail;
if (needFooterEveryPage) { if (needFooterEveryPage) {
@@ -790,13 +837,13 @@ export function computeAutoPageRowChunks(
let take = 0; let take = 0;
let used = 0; let used = 0;
while (take < remaining && used + rowHeights[i + take] <= maxBodyMm + 0.02) { while (take < remaining && used + autoPagePessimisticRowHeightMm(rowHeights[i + take]) <= maxBodyMm + 0.02) {
used += rowHeights[i + take]; used += autoPagePessimisticRowHeightMm(rowHeights[i + take]);
take += 1; take += 1;
} }
if (needFooterLastOnly && !needFooterEveryPage && remaining <= take) { if (needFooterLastOnly && !needFooterEveryPage && remaining <= take) {
const bodyMm = sumRowHeightsMm(rowHeights, i, remaining); const bodyMm = sumPessimisticRowHeightsMm(rowHeights, i, remaining);
if (bodyMm + footerMm <= safeAvail + 0.02) { if (bodyMm + footerMm <= safeAvail + 0.02) {
chunks.push(rows.slice(i, i + remaining)); chunks.push(rows.slice(i, i + remaining));
break; break;
@@ -804,8 +851,8 @@ export function computeAutoPageRowChunks(
maxBodyMm = Math.max(rowH, safeAvail - footerMm); maxBodyMm = Math.max(rowH, safeAvail - footerMm);
take = 0; take = 0;
used = 0; used = 0;
while (take < remaining && used + rowHeights[i + take] <= maxBodyMm + 0.02) { while (take < remaining && used + autoPagePessimisticRowHeightMm(rowHeights[i + take]) <= maxBodyMm + 0.02) {
used += rowHeights[i + take]; used += autoPagePessimisticRowHeightMm(rowHeights[i + take]);
take += 1; take += 1;
} }
} }