<!doctype html>
|
<html>
|
<head>
|
<meta charset="UTF-8" />
|
<style>
|
/* 页面基础设置 */
|
* {
|
margin: 0;
|
padding: 0;
|
box-sizing: border-box;
|
}
|
|
body {
|
font-family:
|
'SimSun',
|
'Microsoft YaHei',
|
-apple-system,
|
BlinkMacSystemFont,
|
sans-serif;
|
font-size: 3mm;
|
line-height: 1.6;
|
}
|
|
/* 主表格 */
|
.main-table {
|
width: 100%;
|
border-collapse: collapse;
|
table-layout: fixed;
|
}
|
|
.main-table th,
|
.main-table td {
|
border: 1px solid #333;
|
padding: 1.5mm 2mm;
|
text-align: center;
|
vertical-align: middle;
|
}
|
|
/* Logo 单元格 */
|
.logo-cell {
|
width: 25mm;
|
padding: 0.5mm;
|
text-align: center;
|
vertical-align: middle;
|
}
|
|
.logo-placeholder {
|
width: 22mm;
|
height: 22mm;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin: 0 auto;
|
}
|
|
.logo-placeholder img {
|
width: 100%;
|
height: 100%;
|
object-fit: contain;
|
}
|
|
/* 公司名称 */
|
.company-name {
|
font-size: 4.5mm;
|
font-weight: bold;
|
padding: 1.5mm 2mm;
|
}
|
|
/* 报告名称 */
|
.report-name {
|
font-size: 3.75mm;
|
font-weight: bold;
|
padding: 1.5mm 2mm;
|
}
|
|
/* 编号 */
|
.doc-number {
|
font-size: 3.25mm;
|
font-weight: bold;
|
padding: 1.5mm 2mm;
|
}
|
|
/* 报告书编号 */
|
.report-book-number {
|
font-size: 3mm;
|
text-align: right !important;
|
padding: 1mm 2mm;
|
border: none !important;
|
}
|
|
/* 依据说明 */
|
.basis-row td {
|
text-align: left !important;
|
padding: 1.5mm 2mm;
|
font-size: 3mm;
|
border: none !important;
|
}
|
|
/* 页码标签 */
|
.page-code-label {
|
font-size: 3.25mm;
|
width: 20mm;
|
padding: 1.5mm 2mm;
|
}
|
|
/* 页码值 */
|
.page-code-value {
|
font-size: 3.25mm;
|
font-weight: bold;
|
text-decoration: underline;
|
width: 20mm;
|
padding: 1.5mm 2mm;
|
}
|
|
/* 依据说明 */
|
.basis-row td {
|
text-align: left !important;
|
padding: 1.5mm 2mm;
|
font-size: 3mm;
|
}
|
|
/* 打印样式中确保依据行左对齐 */
|
@media print {
|
.basis-row td {
|
text-align: left !important;
|
}
|
}
|
|
/* 数据表头 */
|
.data-header {
|
font-weight: bold;
|
}
|
|
/* 内容单元格 */
|
.content-cell {
|
text-align: left !important;
|
font-size: 2.75mm;
|
}
|
|
/* 合并单元格的样式 */
|
.category-cell {
|
font-weight: bold;
|
}
|
|
/* 样品信息行 */
|
.sample-info-row {
|
font-size: 3mm;
|
}
|
|
/* 数据表头行 */
|
.data-header-row {
|
font-weight: bold;
|
background-color: #f5f5f5;
|
}
|
|
/* 打印按钮 */
|
.print-btn {
|
position: fixed;
|
top: 5mm;
|
right: 5mm;
|
padding: 3mm 5mm;
|
cursor: pointer;
|
z-index: 1000;
|
background: #4caf50;
|
color: white;
|
border: none;
|
border-radius: 1mm;
|
font-size: 3.5mm;
|
}
|
|
.print-btn:hover {
|
background: #45a049;
|
}
|
|
/* 隐藏测量容器 - 使用打印样式确保测量准确 */
|
.measure-container {
|
position: absolute;
|
visibility: hidden;
|
left: -9999px;
|
top: 0;
|
}
|
|
.measure-container .main-table {
|
width: 210mm;
|
border-collapse: collapse;
|
table-layout: fixed;
|
}
|
|
.measure-container .main-table th,
|
.measure-container .main-table td {
|
border: 1px solid #333;
|
padding: 1mm 1.5mm; /* 使用打印时的 padding */
|
font-size: 2.75mm; /* 使用打印时的 font-size */
|
text-align: center;
|
vertical-align: middle;
|
}
|
|
.measure-container .main-table td.category-cell {
|
font-weight: bold;
|
}
|
|
.measure-container .main-table td.content-cell {
|
text-align: left;
|
word-wrap: break-word;
|
}
|
|
/* 屏幕预览样式 */
|
@media screen {
|
body {
|
background: #f0f0f0;
|
padding: 10mm;
|
}
|
|
.screen-view {
|
display: block;
|
max-width: 210mm;
|
min-height: 297mm;
|
margin: 0 auto;
|
background: white;
|
padding: 5mm;
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
}
|
|
.print-pages {
|
display: none !important;
|
}
|
}
|
|
/* 打印样式 */
|
@media print {
|
/* 打印机页面设置 - 清除浏览器的默认边距 */
|
@page {
|
size: A4;
|
margin: 0; /* 清除所有边距,让代码完全控制 */
|
}
|
|
body {
|
background: white;
|
padding: 0;
|
margin: 0;
|
}
|
|
.no-print,
|
.screen-view {
|
display: none !important;
|
}
|
|
.print-pages {
|
display: block !important;
|
}
|
|
/* 确保依据行在打印时也是左对齐 */
|
.basis-row td {
|
text-align: left !important;
|
}
|
|
.page-section {
|
break-after: page;
|
page-break-after: always;
|
height: 297mm; /* 使用完整 A4 高度 */
|
width: 210mm; /* 使用完整 A4 宽度 */
|
overflow: hidden;
|
position: relative;
|
padding: 10mm 10mm 10mm 10mm; /* 统一边距 10mm */
|
box-sizing: border-box;
|
margin: 0;
|
}
|
|
.page-section:last-child {
|
break-after: auto;
|
page-break-after: auto;
|
}
|
|
.page-section .main-table {
|
border-collapse: collapse;
|
width: 100%;
|
table-layout: fixed;
|
}
|
|
.page-section .main-table th,
|
.page-section .main-table td {
|
border: 1px solid #333;
|
padding: 1mm 1.5mm;
|
text-align: center;
|
vertical-align: middle;
|
font-size: 2.75mm;
|
overflow: visible;
|
word-wrap: break-word; /* 允许内容换行 */
|
word-break: break-all;
|
}
|
|
/* 确保表格底部有边框 */
|
.page-section .main-table tbody tr:last-child td,
|
.page-section .main-table tbody tr:last-child th {
|
border-bottom: 1px solid #333 !important;
|
}
|
|
/* 表格行不被截断 */
|
.page-section .main-table tbody tr {
|
break-inside: avoid !important;
|
page-break-inside: avoid !important;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<!-- 屏幕预览:连续表格 -->
|
<div class="screen-view">
|
<table class="main-table" id="source-table"></table>
|
</div>
|
|
<!-- 打印预览:分页容器 -->
|
<div class="print-pages no-screen" id="print-container"></div>
|
|
<script>
|
// ==================== 数据层 ====================
|
// 从 window.templateData 读取外部传入的数据
|
const templateData = window.templateData || {};
|
|
// 页面配置常量 - 全部使用毫米单位
|
const PAGE_CONFIG = {
|
width: 210, // A4 宽度 mm
|
height: 297, // A4 高度 mm
|
paddingTop: 10, // 上边距 mm
|
paddingBottom: 10, // 下边距 mm
|
sampleInfoHeight: 12, // 样品信息 + 报告书编号行的高度 mm
|
safetyMargin: 20, // 安全边距 mm(增大以防止截断)
|
|
// 毫米转像素(96 DPI)
|
mmToPx(mm) {
|
return mm * 3.779527559;
|
},
|
|
// 可用高度 = A4高度 - 上下边距 - 安全边距
|
get availableHeightPx() {
|
const availableMm = this.height - this.paddingTop - this.paddingBottom - this.safetyMargin;
|
return this.mmToPx(availableMm);
|
},
|
|
// 第一页额外减去样品信息高度
|
get firstPageExtraHeight() {
|
return this.mmToPx(this.sampleInfoHeight);
|
},
|
};
|
|
// 报告信息
|
const reportInfo = templateData.reportInfo || {
|
companyName: '',
|
reportName: '',
|
docNumber: '',
|
reportBookNumber: '',
|
basis: '',
|
};
|
|
// 样品信息
|
const sampleInfo = templateData.sampleInfo || [];
|
|
// 检测项目数据
|
const testItems = templateData.testItems || [];
|
|
// 结论和签名
|
const conclusionData = templateData.conclusionData || {
|
conclusion: '',
|
signatures: [],
|
remarks: [],
|
};
|
|
// HTML 转义函数(防止特殊字符破坏 HTML 结构)
|
function escapeHtml(text) {
|
if (text === null || text === undefined) return '';
|
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
}
|
|
// ==================== HTML 生成函数 ====================
|
|
// 生成表头 HTML(每页都需要)
|
function generateHeaderHTML(pageNum, totalPages) {
|
return (
|
'<tr>' +
|
'<td rowspan="3" class="logo-cell">' +
|
'<div class="logo-placeholder"><img src="/logo.png" alt="Logo"></div>' +
|
'</td>' +
|
'<td colspan="3" class="company-name">' +
|
reportInfo.companyName +
|
'</td>' +
|
'</tr>' +
|
'<tr>' +
|
'<td colspan="2" class="report-name">' +
|
reportInfo.reportName +
|
'</td>' +
|
'<td class="page-code-label">页码</td>' +
|
'</tr>' +
|
'<tr>' +
|
'<td colspan="2" class="doc-number">' +
|
reportInfo.docNumber +
|
'</td>' +
|
'<td class="page-code-value">' +
|
pageNum +
|
' / ' +
|
totalPages +
|
'</td>' +
|
'</tr>' +
|
'<tr>' +
|
'<td colspan="4" class="report-book-number">报告书编号:' +
|
reportInfo.reportBookNumber +
|
'</td>' +
|
'</tr>'
|
);
|
}
|
|
// 生成样品信息 HTML
|
function generateSampleInfoHTML() {
|
let html = '';
|
sampleInfo.forEach((row, index) => {
|
if (row.fullWidth) {
|
html +=
|
'<tr class="sample-info-row data-header">' +
|
'<td class="category-cell">' +
|
escapeHtml(row.label) +
|
'</td>' +
|
'<td colspan="3">' +
|
escapeHtml(row.value) +
|
'</td>' +
|
'</tr>';
|
} else {
|
html +=
|
'<tr class="sample-info-row data-header">' +
|
'<td class="category-cell">' +
|
escapeHtml(row.label) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(row.value) +
|
'</td>' +
|
'<td class="category-cell">' +
|
escapeHtml(row.label2) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(row.value2) +
|
'</td>' +
|
'</tr>';
|
}
|
});
|
// 在样品信息后添加依据行
|
html += '<tr class="basis-row">' + '<td colspan="4"><strong>依据:</strong>' + escapeHtml(reportInfo.basis) + '</td>' + '</tr>';
|
return html;
|
}
|
|
// 生成列标题 HTML
|
function generateColumnHeadersHTML() {
|
return '<tr class="data-header-row">' + '<td>质量属性</td>' + '<td>检验项目</td>' + '<td>放行标准</td>' + '<td>结果</td>' + '</tr>';
|
}
|
|
// 按分类对行进行分组(用于计算 rowspan)
|
function groupRowsByCategory(rows) {
|
const groups = [];
|
let currentGroup = null;
|
|
rows.forEach((row) => {
|
if (row.type !== 'test') {
|
// 非检测项目行(结论、签名、备注)单独成组
|
if (currentGroup) {
|
groups.push(currentGroup);
|
currentGroup = null;
|
}
|
groups.push({ category: row.category, rows: [row] });
|
} else {
|
// 检测项目行按分类分组
|
if (!currentGroup || currentGroup.category !== row.category) {
|
if (currentGroup) {
|
groups.push(currentGroup);
|
}
|
currentGroup = { category: row.category, rows: [] };
|
}
|
currentGroup.rows.push(row);
|
}
|
});
|
|
if (currentGroup) {
|
groups.push(currentGroup);
|
}
|
|
return groups;
|
}
|
|
// 将所有检测项目和相关数据(结论、签名、备注)扁平化为行数组
|
function flattenTestItems() {
|
const rows = [];
|
|
testItems.forEach((item) => {
|
if (item.noRowspan) {
|
// 无 rowspan 的单行 - 分类只显示在第一行
|
rows.push({
|
category: item.category,
|
isFirstRow: true,
|
totalRows: 1,
|
rowIndex: 0,
|
type: 'test',
|
name: item.name,
|
standard: item.standard,
|
result: item.result,
|
});
|
} else {
|
// 有 rowspan 的多行 - 但每行都保留分类信息用于跨页处理
|
item.items.forEach((subItem, index) => {
|
rows.push({
|
category: item.category,
|
isFirstRow: index === 0, // 只有第一行显示分类
|
totalRows: item.rowspan,
|
rowIndex: index,
|
type: 'test',
|
name: subItem.name,
|
standard: subItem.standard,
|
result: subItem.result,
|
});
|
});
|
}
|
});
|
|
// 添加结论行 - 作为独立行,不隶属于任何分类的 rowspan
|
if (conclusionData.conclusion) {
|
rows.push({
|
category: '结论',
|
isFirstRow: true,
|
totalRows: 1,
|
rowIndex: 0,
|
type: 'conclusion',
|
label: '结论',
|
content: conclusionData.conclusion,
|
});
|
}
|
|
// 添加签名行 - 每行都是独立的
|
conclusionData.signatures.forEach((sig) => {
|
rows.push({
|
category: sig.role,
|
isFirstRow: true,
|
totalRows: 1,
|
rowIndex: 0,
|
type: 'signature',
|
role: sig.role,
|
value: sig.value,
|
role2: sig.role2,
|
value2: sig.value2,
|
});
|
});
|
|
// 添加备注行 - 作为独立行
|
if (conclusionData.remarks.length > 0) {
|
rows.push({
|
category: '备注',
|
isFirstRow: true,
|
totalRows: 1,
|
rowIndex: 0,
|
type: 'remarks',
|
label: '备注',
|
content: conclusionData.remarks,
|
});
|
}
|
|
return rows;
|
}
|
|
// 生成单行 HTML - 每页独立渲染
|
// showCategory: 是否显示分类单元格(第一行或新页面的第一行需要显示)
|
// rowspan: 分类单元格需要跨的行数(仅在 showCategory 为 true 时有效)
|
function generateDataRowHTML(row, showCategory = false, rowspan = 1) {
|
// 处理结论行 - 结论单独一行,第一列显示"结论"
|
if (row.type === 'conclusion') {
|
return (
|
'<tr>' +
|
'<td class="category-cell">' +
|
row.label +
|
'</td>' +
|
'<td colspan="3" class="content-cell">' +
|
'<span style="font-size: 3.25mm;">' +
|
escapeHtml(row.content) +
|
'</span>' +
|
'</td>' +
|
'</tr>'
|
);
|
}
|
|
// 处理签名行 - 每行独立显示
|
if (row.type === 'signature') {
|
let html = '<tr>' +
|
'<td class="category-cell">' +
|
escapeHtml(row.role) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(row.value) +
|
'</td>';
|
if (row.role2 && row.value2) {
|
html += '<td class="category-cell">' + escapeHtml(row.role2) + '</td>' +
|
'<td>' + escapeHtml(row.value2) + '</td>';
|
} else {
|
html += '<td></td><td></td>';
|
}
|
html += '</tr>';
|
return html;
|
}
|
|
// 处理备注行 - 单独一行
|
if (row.type === 'remarks') {
|
const content = row.content.map(r => escapeHtml(r)).join('<br>');
|
return (
|
'<tr>' +
|
'<td class="category-cell">' +
|
row.label +
|
'</td>' +
|
'<td colspan="3" class="content-cell">' +
|
content +
|
'</td>' +
|
'</tr>'
|
);
|
}
|
|
// 处理检测项目行(默认类型)
|
// 只有分类的第一行显示分类单元格(使用 rowspan 合并)
|
if (showCategory) {
|
// 显示分类单元格(使用 rowspan 合并)
|
const rowspanAttr = rowspan > 1 ? ' rowspan="' + rowspan + '"' : '';
|
return (
|
'<tr>' +
|
'<td class="category-cell"' + rowspanAttr + '>' +
|
row.category +
|
'</td>' +
|
'<td class="content-cell">' +
|
row.name +
|
'</td>' +
|
'<td class="content-cell">' +
|
row.standard +
|
'</td>' +
|
'<td>' +
|
row.result +
|
'</td>' +
|
'</tr>'
|
);
|
} else {
|
// 非第一行,不显示分类单元格(已被 rowspan 合并)
|
return (
|
'<tr>' +
|
'<td class="content-cell">' +
|
row.name +
|
'</td>' +
|
'<td class="content-cell">' +
|
row.standard +
|
'</td>' +
|
'<td>' +
|
row.result +
|
'</td>' +
|
'</tr>'
|
);
|
}
|
}
|
|
// 生成结论和签名 HTML(用于测量高度)
|
function generateConclusionHTML() {
|
// 注意:结论、签名、备注现在已整合到 flattenTestItems 中
|
// 这个函数仅用于测量总高度
|
let html = '';
|
|
// 结论行
|
if (conclusionData.conclusion) {
|
html +=
|
'<tr class="sample-info-row data-header">' +
|
'<td class="category-cell">结论</td>' +
|
'<td colspan="3" class="content-cell">' +
|
'<span style="font-size: 3.25mm;">' +
|
escapeHtml(conclusionData.conclusion) +
|
'</span>' +
|
'</td>' +
|
'</tr>';
|
}
|
|
// 签名行
|
conclusionData.signatures.forEach((sig) => {
|
html +=
|
'<tr class="sample-info-row data-header">' +
|
'<td class="category-cell">' +
|
escapeHtml(sig.role) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(sig.value) +
|
'</td>';
|
if (sig.role2 && sig.value2) {
|
html +=
|
'<td class="category-cell">' +
|
escapeHtml(sig.role2) +
|
'</td>' +
|
'<td>' +
|
escapeHtml(sig.value2) +
|
'</td>' +
|
'</tr>';
|
} else {
|
html += '<td></td><td></td></tr>';
|
}
|
});
|
|
// 备注行
|
if (conclusionData.remarks.length > 0) {
|
html +=
|
'<tr class="sample-info-row data-header">' +
|
'<td class="category-cell">备注</td>' +
|
'<td colspan="3" class="content-cell">' +
|
conclusionData.remarks
|
.map(function (r) {
|
return escapeHtml(r);
|
})
|
.join('<br>') +
|
'</td>' +
|
'</tr>';
|
}
|
|
return html;
|
}
|
|
// ==================== 测量层 ====================
|
|
// 测量表头高度和每行高度
|
function measureHeights() {
|
// 创建测量容器
|
const measureDiv = document.createElement('div');
|
measureDiv.className = 'measure-container';
|
document.body.appendChild(measureDiv);
|
|
// 创建测量表格 - 包含完整表头(第一页表头)
|
const measureTable = document.createElement('table');
|
measureTable.className = 'main-table';
|
measureTable.innerHTML = `
|
<colgroup>
|
<col style="width: 22%;">
|
<col style="width: 24%;">
|
<col style="width: 28%;">
|
<col style="width: 26%;">
|
</colgroup>
|
<thead>${generateHeaderHTML(1, 1)}${generateSampleInfoHTML()}${generateColumnHeadersHTML()}</thead>
|
<tbody id="measure-tbody"></tbody>
|
`;
|
measureDiv.appendChild(measureTable);
|
|
// 测量第一页表头高度(包含样品信息)
|
const theadRows = measureTable.querySelectorAll('thead tr');
|
let firstPageHeaderHeight = 0;
|
theadRows.forEach((tr) => {
|
firstPageHeaderHeight += tr.offsetHeight;
|
});
|
|
// 测量后续页表头高度(不包含样品信息)- 样品信息约4行
|
let otherPageHeaderHeight = firstPageHeaderHeight;
|
const sampleInfoRows = Array.from(theadRows).filter((tr) => tr.classList.contains('sample-info-row'));
|
const sampleInfoHeight = sampleInfoRows.reduce((sum, tr) => sum + tr.offsetHeight, 0);
|
otherPageHeaderHeight = firstPageHeaderHeight - sampleInfoHeight;
|
|
// 测量每行高度
|
const tbody = measureTable.querySelector('#measure-tbody');
|
const rows = flattenTestItems();
|
const categoryGroups = groupRowsByCategory(rows);
|
const rowHeights = [];
|
|
categoryGroups.forEach((group) => {
|
group.rows.forEach((row, index) => {
|
const tr = document.createElement('tr');
|
const showCategory = index === 0 && row.type === 'test';
|
const rowspan = showCategory ? group.rows.length : 1;
|
tr.innerHTML = generateDataRowHTML(row, showCategory, rowspan)
|
.replace(/<tr[^>]*>/, '')
|
.replace('</tr>', '');
|
tbody.appendChild(tr);
|
rowHeights.push(tr.offsetHeight);
|
});
|
tbody.innerHTML = '';
|
});
|
|
// 清理测量容器
|
document.body.removeChild(measureDiv);
|
|
return {
|
firstPageHeaderHeight,
|
otherPageHeaderHeight,
|
rowHeights,
|
rows,
|
};
|
}
|
|
// ==================== 分页引擎 ====================
|
|
// 计算分页 - 简单逐行分页,不需要考虑 rowspan
|
function calculatePages(rowHeights, rows, availableHeight) {
|
const pages = [];
|
let currentPage = [];
|
let currentHeight = 0;
|
let isFirstPage = true; // 标记是否是第一页
|
|
function getCurrentLimit() {
|
// 第一页需要减去样品信息的高度
|
if (isFirstPage) {
|
return availableHeight - PAGE_CONFIG.firstPageExtraHeight;
|
}
|
// 后续页使用全部可用高度
|
return availableHeight;
|
}
|
|
rows.forEach((row, index) => {
|
const height = rowHeights[index];
|
const limit = getCurrentLimit();
|
|
// 检查当前页是否能容纳
|
if (currentHeight + height > limit && currentPage.length > 0) {
|
// 当前页放不下,新建一页
|
pages.push(currentPage);
|
currentPage = [index];
|
currentHeight = height;
|
isFirstPage = false; // 不再是第一页
|
} else {
|
// 当前页能容纳
|
currentPage.push(index);
|
currentHeight += height;
|
}
|
});
|
|
if (currentPage.length > 0) {
|
pages.push(currentPage);
|
}
|
|
return pages;
|
}
|
|
// ==================== 渲染层 ====================
|
|
// 渲染屏幕预览(连续表格)
|
function renderScreenView() {
|
const table = document.getElementById('source-table');
|
const rows = flattenTestItems();
|
|
// 按分类分组,计算每个分类的 rowspan
|
const categoryGroups = groupRowsByCategory(rows);
|
|
let tbodyContent = '';
|
categoryGroups.forEach((group) => {
|
group.rows.forEach((row, index) => {
|
// 分类内第一行显示分类单元格(使用 rowspan)
|
const showCategory = index === 0 && row.type === 'test';
|
const rowspan = showCategory ? group.rows.length : 1;
|
tbodyContent += generateDataRowHTML(row, showCategory, rowspan);
|
});
|
});
|
|
table.innerHTML =
|
'<colgroup>' +
|
'<col style="width: 22%;">' +
|
'<col style="width: 24%;">' +
|
'<col style="width: 28%;">' +
|
'<col style="width: 26%;">' +
|
'</colgroup>' +
|
'<thead>' +
|
generateHeaderHTML(1, 1) +
|
generateSampleInfoHTML() +
|
generateColumnHeadersHTML() +
|
'</thead>' +
|
'<tbody>' +
|
tbodyContent +
|
'</tbody>';
|
}
|
|
// 渲染分页打印内容
|
function renderPrintPages() {
|
const container = document.getElementById('print-container');
|
container.innerHTML = '';
|
|
// 测量
|
const { firstPageHeaderHeight, otherPageHeaderHeight, rowHeights, rows } = measureHeights();
|
|
// 使用统一的可用高度(已包含安全边距)
|
const availableHeight = PAGE_CONFIG.availableHeightPx;
|
|
// 计算分页(逐行)
|
const pages = calculatePages(rowHeights, rows, availableHeight);
|
const totalPages = pages.length;
|
|
// 渲染每页
|
pages.forEach((pageRowIndices, pageIndex) => {
|
const pageNum = pageIndex + 1;
|
const pageSection = document.createElement('div');
|
pageSection.className = 'page-section';
|
|
// 创建表格
|
const table = document.createElement('table');
|
table.className = 'main-table';
|
|
// 第一页包含样品信息,后续页只有列标题
|
const theadContent =
|
pageIndex === 0
|
? generateHeaderHTML(pageNum, totalPages) + generateSampleInfoHTML() + generateColumnHeadersHTML()
|
: generateHeaderHTML(pageNum, totalPages) + generateColumnHeadersHTML();
|
|
// 生成 tbody 内容
|
let tbodyContent = '';
|
|
// 按分类组处理当前页的行
|
let currentCategory = null;
|
let categoryRowCount = 0;
|
let categoryStartIdx = -1;
|
|
// 先统计当前页中每个分类的行数
|
const pageCategoryGroups = [];
|
let tempGroup = null;
|
|
pageRowIndices.forEach((rowIdx) => {
|
const row = rows[rowIdx];
|
if (row.type !== 'test') {
|
// 非检测项目单独成组
|
if (tempGroup) {
|
pageCategoryGroups.push(tempGroup);
|
tempGroup = null;
|
}
|
pageCategoryGroups.push({ category: row.category, rows: [row] });
|
} else {
|
if (!tempGroup || tempGroup.category !== row.category) {
|
if (tempGroup) {
|
pageCategoryGroups.push(tempGroup);
|
}
|
tempGroup = { category: row.category, rows: [] };
|
}
|
tempGroup.rows.push(row);
|
}
|
});
|
if (tempGroup) {
|
pageCategoryGroups.push(tempGroup);
|
}
|
|
// 按组渲染
|
pageCategoryGroups.forEach((group) => {
|
group.rows.forEach((row, index) => {
|
// 组内第一行显示分类单元格(使用 rowspan)
|
const showCategory = index === 0 && row.type === 'test';
|
const rowspan = showCategory ? group.rows.length : 1;
|
tbodyContent += generateDataRowHTML(row, showCategory, rowspan);
|
});
|
});
|
|
table.innerHTML =
|
'<colgroup>' +
|
'<col style="width: 22%;">' +
|
'<col style="width: 24%;">' +
|
'<col style="width: 28%;">' +
|
'<col style="width: 26%;">' +
|
'</colgroup>' +
|
'<thead>' +
|
theadContent +
|
'</thead>' +
|
'<tbody>' +
|
tbodyContent +
|
'</tbody>';
|
|
pageSection.appendChild(table);
|
container.appendChild(pageSection);
|
});
|
}
|
|
// ==================== 初始化 ====================
|
|
// 页面加载时初始化
|
window.addEventListener('load', function () {
|
renderScreenView();
|
// 延迟执行分页渲染,确保字体等资源加载完成
|
setTimeout(renderPrintPages, 100);
|
});
|
|
// 打印前重新计算分页
|
window.addEventListener('beforeprint', function () {
|
renderPrintPages();
|
});
|
|
// 窗口大小变化时重新计算(用于调试)
|
let resizeTimer;
|
window.addEventListener('resize', function () {
|
clearTimeout(resizeTimer);
|
resizeTimer = setTimeout(renderPrintPages, 250);
|
});
|
</script>
|
</body>
|
</html>
|