优化打印模板行高估算逻辑,新增文本换行估算功能,调整行高计算以适应不同内容类型,确保打印预览准确性和用户体验。增加防裁切余量和行高微调系数,改善分页处理。
This commit is contained in:
@@ -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)。过大则本页明明还能放 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,
|
* 估算表格数据行在纸面上的高度(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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user