优化打印模板行高估算逻辑,新增文本换行估算功能,调整行高计算以适应不同内容类型,确保打印预览准确性和用户体验。增加防裁切余量和行高微调系数,改善分页处理。
This commit is contained in:
@@ -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)。过大则本页明明还能放 1~2 行却提前换页导致大块留白;过小易压分页缝 */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user