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

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('');
return `<tr style="height:${element.rowHeight}mm;">${cells}</tr>`;
// 使用 min-height长文本/二维码撑高行时以内容为准,避免固定 height 与实高不一致导致分页估算偶发偏差
return `<tr style="min-height:${element.rowHeight}mm;">${cells}</tr>`;
}),
)
).join('');
@@ -673,6 +674,46 @@ function resolveDetailTableChunkTopMm(
const MM_TO_CSS_PX = 96 / 25.4;
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
* 若仍按固定 rowHeight 分页会导致整表落在同一 chunk、页数为 1 且预览溢出灰底。
@@ -681,6 +722,8 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
const baseRow = Math.max(0.01, Number(element.rowHeight || 8));
let maxH = baseRow;
const padHmm = 4; // 与单元格 padding:2mm 上下大致相当
// 图片类内容略加一点,避免略估;过多会整体抬高行高、加重页底留白
const mediaExtraHmm = 1.25;
for (const column of columns) {
const fieldKey = column.bindField || column.field;
@@ -697,15 +740,15 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
if (contentType === 'qrcode' || contentType === 'barcode') {
const fillCell = column?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100)));
const sideMm = fillCell ? colWMm * 0.92 : colWMm * (scale / 100) * 0.92;
maxH = Math.max(maxH, sideMm + padHmm);
const sideMm = fillCell ? colWMm * 0.93 : colWMm * (scale / 100) * 0.93;
maxH = Math.max(maxH, sideMm + padHmm + mediaExtraHmm);
continue;
}
if (contentType === 'image') {
const fillCell = column?.fillCell !== false;
const scale = Math.max(10, Math.min(100, Number(column?.contentScale || 100)));
const hMm = fillCell ? colWMm * 0.62 : colWMm * (scale / 100) * 0.62;
maxH = Math.max(maxH, hMm + padHmm);
const hMm = fillCell ? colWMm * 0.65 : colWMm * (scale / 100) * 0.65;
maxH = Math.max(maxH, hMm + padHmm + mediaExtraHmm * 0.6);
continue;
}
if (nowrap) {
@@ -718,10 +761,8 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
continue;
}
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 charsPerLine = Math.max(4, Math.floor(innerWpx / charWpx));
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
const lines = estimateTextWrapLines(text, innerWpx, fontSize);
const lineHeightPx = fontSize * 1.3;
const textHmm = (lines * lineHeightPx) / CSS_PX_PER_MM;
maxH = Math.max(maxH, textHmm + padHmm, baseRow);
@@ -729,17 +770,23 @@ function estimateTableBodyRowHeightMm(element: NativeTableElement, row: Record<s
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;
const end = Math.min(from + count, heights.length);
for (let j = from; j < end; j += 1) {
s += heights[j];
s += autoPagePessimisticRowHeightMm(heights[j]);
}
return s;
}
/**
* autoPage 明细表:按版心高度、表头顶、行高与页脚占位拆分数据行(与浏览器打印分页接近)
* 画布上每个 table/detailTable含多个不同 source 的明细表)均独立调用,避免仅首张表分页合理、其余表跨缝裁切。
*/
export function computeAutoPageRowChunks(
element: NativeTableElement,
@@ -781,7 +828,7 @@ export function computeAutoPageRowChunks(
: repeatFreeConstrains
? Math.max(rowH, innerH - y0 - headerH)
: 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;
if (needFooterEveryPage) {
@@ -790,13 +837,13 @@ export function computeAutoPageRowChunks(
let take = 0;
let used = 0;
while (take < remaining && used + rowHeights[i + take] <= maxBodyMm + 0.02) {
used += rowHeights[i + take];
while (take < remaining && used + autoPagePessimisticRowHeightMm(rowHeights[i + take]) <= maxBodyMm + 0.02) {
used += autoPagePessimisticRowHeightMm(rowHeights[i + take]);
take += 1;
}
if (needFooterLastOnly && !needFooterEveryPage && remaining <= take) {
const bodyMm = sumRowHeightsMm(rowHeights, i, remaining);
const bodyMm = sumPessimisticRowHeightsMm(rowHeights, i, remaining);
if (bodyMm + footerMm <= safeAvail + 0.02) {
chunks.push(rows.slice(i, i + remaining));
break;
@@ -804,8 +851,8 @@ export function computeAutoPageRowChunks(
maxBodyMm = Math.max(rowH, safeAvail - footerMm);
take = 0;
used = 0;
while (take < remaining && used + rowHeights[i + take] <= maxBodyMm + 0.02) {
used += rowHeights[i + take];
while (take < remaining && used + autoPagePessimisticRowHeightMm(rowHeights[i + take]) <= maxBodyMm + 0.02) {
used += autoPagePessimisticRowHeightMm(rowHeights[i + take]);
take += 1;
}
}