8 changed files with 577 additions and 262 deletions
@ -0,0 +1,86 @@ |
|||
import {getPrintTemplate} from '@/api/DesignForm/requestapi' |
|||
import {fieldTree,PageConfig} from './printHtmlRender.vue' |
|||
import printHtmlRender from './printHtmlRender.vue' |
|||
import {printWithSmartPagination} from "@/views/sysworkflow/lowcodepage/appPage/appPageForm/printHtmlDom.js" |
|||
|
|||
const printRenderTree: Ref<any[]> = ref([]); |
|||
export const printHtmlPage = async (pformid:string,pappkey:string,pversionid:string,pformkey:string,row: any) => { |
|||
let data:any[]=[] |
|||
let _pageConfig:PageConfig |
|||
let title:string="表单"; |
|||
await getPrintTemplate({"versionid":pversionid,"formkey":pappkey}).then(resp=>{ |
|||
title=resp.data.title |
|||
if(resp.data.formtemplatejson!=""){ |
|||
data=JSON.parse(resp.data.formtemplatejson) |
|||
}else{ |
|||
alert("请先创建打印模板!") |
|||
return |
|||
} |
|||
|
|||
if(resp.data.pageconfigjson!=""){ |
|||
_pageConfig=JSON.parse(resp.data.pageconfigjson) |
|||
if(_pageConfig.founder!=""){ |
|||
_pageConfig.founder=row[_pageConfig.founder] |
|||
} |
|||
if(_pageConfig.founderTime!=""){ |
|||
_pageConfig.founderTime=row[_pageConfig.founderTime] |
|||
} |
|||
if(_pageConfig.deptOrg!=""){ |
|||
_pageConfig.deptOrg=row[_pageConfig.deptOrg] |
|||
} |
|||
_pageConfig.masters_key=row.masters_key |
|||
} |
|||
}) |
|||
|
|||
let qrstr=`https://wab.hxgk.group/#/form_table/taskInfo?id=${pformid}&key=${pappkey}&
|
|||
formid=${pversionid}&formKey=${pformkey}&state=2` |
|||
data.forEach(node=>{ |
|||
deepLoopForm(node,row) |
|||
}) |
|||
printRenderTree.value=data |
|||
|
|||
ElMessageBox({ |
|||
message: () => h('div',{style:{ width:'1200px',display:'flex','flex-direction':'column'}},[ |
|||
h(ElButton, { |
|||
type:"primary", |
|||
style: "margin:10px 10px 5px auto;", |
|||
onClick: () => { |
|||
printWithSmartPagination("printPageAll",{ |
|||
paperSize: _pageConfig.pagesize, |
|||
landscape: _pageConfig.horizontal=="htal"? true : false , //默认竖向
|
|||
margin: 5, |
|||
}); |
|||
} |
|||
},'打印表单'), |
|||
h('div',{style:{ border: '1px solid black', width: 'fit-content', margin: '5px','align-self': 'center'}},[ |
|||
h(printHtmlRender,{ |
|||
name:title, |
|||
fieldTree:printRenderTree.value, |
|||
pageConfig:_pageConfig, |
|||
qrcode:qrstr, |
|||
}) |
|||
]) |
|||
]), |
|||
showConfirmButton:false, |
|||
customStyle: { '--el-messagebox-width':'1300px',padding:'10px'}, |
|||
}).then(() => { |
|||
}) |
|||
}; |
|||
|
|||
const deepLoopForm=(node:fieldTree, row: Record<string, any>)=>{ |
|||
if(Array.isArray(node)){ |
|||
node.forEach(item=>{deepLoopForm(item,row)}) |
|||
return |
|||
} |
|||
|
|||
if(node.field!=""){ |
|||
let rnode:Object; |
|||
if (row.hasOwnProperty(node.field!)){// 有这个字段
|
|||
if (node.type=="table" || node.type=="tabs"){ |
|||
node.data=row[node.field!] |
|||
}else{ |
|||
node.field=row[node.field!] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,73 +1,414 @@ |
|||
export function printElement(elementId) { |
|||
/** |
|||
* ═══════════════════════════════════════════════════════════════ |
|||
* 智能分页打印解决方案 - 标准表格版(支持 caption) |
|||
* 新增:表格分页时保留 caption 在第一页 |
|||
* ═══════════════════════════════════════════════════════════════ |
|||
*/ |
|||
|
|||
export function printWithSmartPagination(elementId, options = {}) { |
|||
const { |
|||
paperSize = 'A4', |
|||
pageHeight= 0, |
|||
margin = 15, |
|||
debug = false, |
|||
landscape = false, |
|||
repeatHeader = true |
|||
} = options; |
|||
|
|||
const element = document.getElementById(elementId); |
|||
if (!element) { |
|||
console.error('元素未找到'); |
|||
console.error('❌ 未找到元素:', elementId); |
|||
return; |
|||
} |
|||
|
|||
console.log('📄 开始分析页面结构...'); |
|||
const structure = analyzePageStructure(element); |
|||
const pages = paginateContent(structure, { paperSize,pageHeight, margin, landscape, repeatHeader }); |
|||
console.log(`✅ 分页完成,共 ${pages.length} 页`); |
|||
|
|||
const printContent = buildPrintPages(pages, structure, { repeatLogo: true }); |
|||
printViaIframe(printContent, { paperSize, margin, debug, landscape }); |
|||
} |
|||
|
|||
function analyzePageStructure(container) { |
|||
const header = container.querySelector('#printHeader'); |
|||
const mainContent = container.querySelector('#printContainer'); |
|||
|
|||
return { |
|||
hasHeader: !!header, |
|||
hasBottom: false, |
|||
header: header?.cloneNode(true), |
|||
bottom: "", |
|||
main: mainContent, |
|||
tables: Array.from(mainContent.querySelectorAll('table')) |
|||
}; |
|||
} |
|||
|
|||
function paginateContent(structure, options) { |
|||
const { paperSize,pageHeight, margin,landscape } = options; |
|||
let _pageH=pageHeight |
|||
//处理页高的问题
|
|||
if(pageHeight==0){ |
|||
if(landscape){ |
|||
_pageH = { |
|||
'A5': 148,'A4': 210, 'A3': 297 |
|||
}[paperSize]; |
|||
}else{ |
|||
_pageH = { |
|||
'A5': 210,'A4': 297, 'A3': 420 |
|||
}[paperSize]; |
|||
} |
|||
} |
|||
|
|||
const paperHeight = _pageH * 3.78; |
|||
|
|||
const marginPx = margin * 3.78; |
|||
const printableHeight = paperHeight - marginPx * 2; |
|||
|
|||
const headerHeight = structure.hasHeader ? structure.header.getBoundingClientRect().height : 0; |
|||
const bottomHeight = structure.hasBottom ? structure.bottom.getBoundingClientRect().height : 0; |
|||
const availableHeight = printableHeight - headerHeight - bottomHeight-30; //50是页眉与内容的间距,
|
|||
|
|||
console.log(`📊 分页基础参数:`); |
|||
console.log(` 纸张高度: ${paperHeight.toFixed(0)}px, 可用高度: ${availableHeight.toFixed(0)}px`); |
|||
|
|||
const pages = []; |
|||
let currentPage = []; |
|||
let currentHeight = 0; |
|||
|
|||
Array.from(structure.main.children).forEach((child, index) => { |
|||
const childHeight = child.getBoundingClientRect().height; |
|||
const elementInfo = `${child.tagName}${child.className ? '.' + child.className.split(' ')[0] : ''}`; |
|||
const isTableContainer = child.querySelector('table'); |
|||
const remainingHeight = availableHeight - currentHeight; |
|||
|
|||
console.log(`\n🔍 [元素 ${index + 1}] ${elementInfo}`); |
|||
console.log(` 高度: ${childHeight.toFixed(1)}px, 累积: ${currentHeight.toFixed(1)}px`); |
|||
console.log(` 剩余空间: ${remainingHeight.toFixed(1)}px`); |
|||
|
|||
// ==================== 智能表格分割 ====================
|
|||
if (isTableContainer) { |
|||
if (currentHeight + childHeight <= availableHeight * 0.92) { |
|||
console.log(` ✅ 表格能完整放入当前页`); |
|||
currentPage.push(child.cloneNode(true)); |
|||
currentHeight += childHeight; |
|||
} else { |
|||
console.log(` ⚠️ 表格无法完整放入,剩余空间: ${remainingHeight.toFixed(1)}px`); |
|||
|
|||
// 对表格进行智能分页,第一页使用剩余空间
|
|||
const tablePages = paginateTableSmart(child, remainingHeight, availableHeight, options); |
|||
|
|||
if (tablePages.length > 0) { |
|||
// 第一页:部分表格 + 前面累积的普通元素(合并在一起)
|
|||
const firstPageContent = tablePages[0]; |
|||
if (firstPageContent.length > 0) { |
|||
currentPage.push(...firstPageContent); |
|||
pages.push([...currentPage]); |
|||
console.log(` ✅ 创建混合页: ${currentPage.length}个元素 (含caption和部分表格)`); |
|||
} |
|||
|
|||
// 剩余的表格页(纯表格)
|
|||
for (let i = 1; i < tablePages.length; i++) { |
|||
pages.push(tablePages[i]); |
|||
console.log(` 📊 添加纯表格页 ${i} (${tablePages[i].length}个元素)`); |
|||
} |
|||
} |
|||
|
|||
currentPage = []; |
|||
currentHeight = 0; |
|||
} |
|||
return; |
|||
} |
|||
// ============================================================
|
|||
|
|||
// 普通元素处理
|
|||
if (currentHeight + childHeight > availableHeight * 0.92) { |
|||
if (currentPage.length > 0) { |
|||
console.log(` 📄 推入当前页 (${currentPage.length}个元素)`); |
|||
pages.push([...currentPage]); |
|||
currentPage = []; |
|||
currentHeight = 0; |
|||
} |
|||
} |
|||
|
|||
currentPage.push(child.cloneNode(true)); |
|||
currentHeight += (childHeight+20); //20margin
|
|||
}); |
|||
|
|||
// 处理剩余元素
|
|||
if (currentPage.length > 0) { |
|||
console.log(`\n📄 最后推入剩余元素 (${currentPage.length}个)`); |
|||
pages.push([...currentPage]); |
|||
} |
|||
|
|||
return pages; |
|||
} |
|||
|
|||
/** |
|||
* 智能表格分页函数(支持 caption) |
|||
* @returns {Array<Array>} 分页结果,每个元素是一个页面数组 |
|||
*/ |
|||
function paginateTableSmart(tableContainer, firstPageHeight, subsequentPageHeight, options) { |
|||
const table = tableContainer.querySelector('table'); |
|||
const caption = table?.querySelector('caption'); // 获取 caption
|
|||
const thead = table?.querySelector('thead'); |
|||
const tbody = table?.querySelector('tbody'); |
|||
|
|||
if (!thead || !tbody) { |
|||
console.warn('⚠️ 表格缺少 thead 或 tbody'); |
|||
return [[tableContainer.cloneNode(true)]]; |
|||
} |
|||
|
|||
const captionHeight = caption ? caption.getBoundingClientRect().height : 0; |
|||
const headerHeight = thead.getBoundingClientRect().height; |
|||
const rows = Array.from(tbody.querySelectorAll('tr')); |
|||
|
|||
console.log(` 📊 智能分页参数:`); |
|||
console.log(` caption高度: ${captionHeight.toFixed(1)}px, 表头高度: ${headerHeight.toFixed(1)}px`); |
|||
console.log(` 第一页可用: ${firstPageHeight.toFixed(1)}px, 后续页可用: ${subsequentPageHeight.toFixed(1)}px`); |
|||
console.log(` 总行数: ${rows.length}行`); |
|||
|
|||
// 如果整个表格能在第一页放下
|
|||
const totalTableHeight = tableContainer.getBoundingClientRect().height; |
|||
if (totalTableHeight <= firstPageHeight * 0.85) { |
|||
console.log(` ✅ 表格总高度 ${totalTableHeight.toFixed(1)}px 能放入第一页`); |
|||
// 包含 caption
|
|||
return [[createTablePage(tableContainer, thead, rows, true)]]; |
|||
} |
|||
|
|||
const pages = []; |
|||
let currentRows = []; |
|||
let currentHeight = captionHeight + headerHeight; // 包含 caption 高度
|
|||
let isFirstPage = true; |
|||
|
|||
// 克隆元素,避免影响原页面
|
|||
const printContent = element.cloneNode(true); |
|||
rows.forEach((row, rowIndex) => { |
|||
const rowHeight = row.getBoundingClientRect().height; |
|||
const pageHeight = isFirstPage ? firstPageHeight : subsequentPageHeight; |
|||
|
|||
// 创建隐藏iframe
|
|||
// 检查是否需要分页
|
|||
if (currentHeight + rowHeight > pageHeight * 0.85 && currentRows.length > 0) { |
|||
// 创建当前页(只有第一页包含 caption)
|
|||
pages.push([createTablePage(tableContainer, thead, currentRows, isFirstPage)]); |
|||
console.log(` 📊 分页${isFirstPage ? '(含caption)' : ''}: ${currentRows.length}行, ${currentHeight.toFixed(1)}px`); |
|||
|
|||
// 重置为新页面
|
|||
currentRows = [row]; |
|||
currentHeight = captionHeight + headerHeight + rowHeight; // 新页面也要包含 caption 高度
|
|||
isFirstPage = false; |
|||
} else { |
|||
currentRows.push(row); |
|||
currentHeight += rowHeight; |
|||
} |
|||
|
|||
// 最后一行
|
|||
if (rowIndex === rows.length - 1 && currentRows.length > 0) { |
|||
pages.push([createTablePage(tableContainer, thead, currentRows, isFirstPage)]); |
|||
console.log(` 📊 最后分页${isFirstPage ? '(含caption)' : ''}: ${currentRows.length}行, ${currentHeight.toFixed(1)}px`); |
|||
} |
|||
}); |
|||
|
|||
console.log(` 📊 表格总计分页: ${pages.length}页`); |
|||
return pages; |
|||
} |
|||
|
|||
/** |
|||
* 创建表格页 |
|||
* @param {boolean} includeCaption - 是否包含 caption(只有第一页为 true) |
|||
*/ |
|||
function createTablePage(originalContainer, thead, rows, includeCaption = false) { |
|||
const container = originalContainer.cloneNode(false); |
|||
const table = document.createElement('table'); |
|||
|
|||
const originalTable = originalContainer.querySelector('table'); |
|||
table.className = originalTable.className; |
|||
table.style.cssText = originalTable.style.cssText; |
|||
|
|||
// 只在第一页添加 caption
|
|||
if (includeCaption) { |
|||
const caption = originalTable.querySelector('caption'); |
|||
if (caption) { |
|||
table.appendChild(caption.cloneNode(true)); |
|||
} |
|||
} |
|||
|
|||
table.appendChild(thead.cloneNode(true)); |
|||
|
|||
const tbody = document.createElement('tbody'); |
|||
rows.forEach(row => tbody.appendChild(row.cloneNode(true))); |
|||
table.appendChild(tbody); |
|||
|
|||
container.appendChild(table); |
|||
return container; |
|||
} |
|||
|
|||
function buildPrintPages(pages, structure, options) { |
|||
const container = document.createElement('div'); |
|||
container.className = 'print-report-container'; |
|||
|
|||
pages.forEach((pageElements, index) => { |
|||
const pageDiv = document.createElement('div'); |
|||
pageDiv.className = 'print-page'; |
|||
|
|||
const elements = Array.isArray(pageElements) ? pageElements : [pageElements]; |
|||
|
|||
if (options.repeatLogo && structure.hasHeader) { |
|||
const headerClone = structure.header.cloneNode(true); |
|||
headerClone.style.borderBottom = '2px solid rgb(182, 181, 181)'; |
|||
headerClone.style.marginBottom = '20px'; |
|||
pageDiv.appendChild(headerClone); |
|||
} |
|||
|
|||
const contentDiv = document.createElement('div'); |
|||
contentDiv.className = 'print-page-content'; |
|||
|
|||
elements.forEach(el => { |
|||
if (el?.nodeType === Node.ELEMENT_NODE) { |
|||
contentDiv.appendChild(el); |
|||
} |
|||
}); |
|||
|
|||
pageDiv.appendChild(contentDiv); |
|||
|
|||
if (index === pages.length - 1 && structure.hasBottom) { |
|||
const bottomClone = structure.bottom.cloneNode(true); |
|||
bottomClone.style.marginTop = '30px'; |
|||
bottomClone.style.borderTop = '1px solid #ccc'; |
|||
bottomClone.style.paddingTop = '15px'; |
|||
pageDiv.appendChild(bottomClone); |
|||
} |
|||
|
|||
container.appendChild(pageDiv); |
|||
}); |
|||
|
|||
return container; |
|||
} |
|||
|
|||
function printViaIframe(content, options) { |
|||
const { paperSize, margin, debug,landscape } = options; |
|||
const iframe = document.createElement('iframe'); |
|||
iframe.style.display = 'none'; |
|||
document.body.appendChild(iframe); |
|||
|
|||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; |
|||
|
|||
// 写入样式和内容
|
|||
iframeDoc.open(); |
|||
iframeDoc.write(` |
|||
<html> |
|||
<head> |
|||
<title>打印</title> |
|||
<style> |
|||
/* 打印优化样式 */ |
|||
body { margin: 20px; font-size: 14px; } |
|||
@media print { |
|||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } |
|||
let originalStyles = ''; |
|||
try { |
|||
originalStyles = collectAllStyles(); |
|||
if (debug) console.log('📄 已收集原始样式,长度:', originalStyles.length); |
|||
} catch (e) { |
|||
console.warn('⚠️ 收集样式时出错:', e); |
|||
} |
|||
|
|||
#printContainer table{ |
|||
display: block; |
|||
border: 0px solid; |
|||
border-spacing: 1px; |
|||
border-collapse: collapse; |
|||
const orientation = landscape ? 'landscape' : 'portrait'; |
|||
const printStyles = ` |
|||
@page { |
|||
size: ${paperSize.toLowerCase()} ${orientation}; |
|||
margin: ${margin}mm; |
|||
} |
|||
#printContainer .el-table th.el-table__cell{ |
|||
background: none !important; |
|||
body { |
|||
margin: 0; |
|||
padding: 0; |
|||
font-family: "SimSun", "Microsoft YaHei", serif; |
|||
background: white; |
|||
} |
|||
|
|||
#printContainer table th{ |
|||
border-right: 1px solid; |
|||
.print-page { |
|||
page-break-after: always; |
|||
padding:${margin}mm ${margin+5}mm; |
|||
} |
|||
.print-page:last-child { |
|||
page-break-after: avoid; |
|||
} |
|||
table { |
|||
width: 100% !important; |
|||
border-collapse: collapse !important; |
|||
} |
|||
#printContainer table td{ |
|||
border: 1px solid; |
|||
th, td { |
|||
border: 1px solid rgb(182, 181, 181) !important; |
|||
page-break-inside: avoid !important; |
|||
text-align: center; |
|||
line-height: 30px; |
|||
} |
|||
thead { |
|||
display: table-header-group !important; |
|||
} |
|||
table caption{ |
|||
text-align: center; |
|||
caption-side: top; |
|||
color: black; |
|||
font-weight: bold; |
|||
margin: 5px; |
|||
} |
|||
h3{ |
|||
width: 100%; |
|||
font-size: 25px; |
|||
font-weight: bold; |
|||
text-align:center; |
|||
} |
|||
|
|||
/* 复制原页面样式 */ |
|||
${Array.from(document.styleSheets) |
|||
.map(sheet => { |
|||
try { |
|||
return Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n'); |
|||
} catch (e) { |
|||
return ''; |
|||
h5{ |
|||
font-weight: bold; |
|||
width: 100%; |
|||
margin: 5px; |
|||
text-align:center; |
|||
} |
|||
.page_brow{ |
|||
padding: 0 !important; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
}) |
|||
.join('\n')} |
|||
</style> |
|||
`;
|
|||
|
|||
const allStyles = `${originalStyles}\n${printStyles}`; |
|||
|
|||
iframeDoc.open(); |
|||
iframeDoc.write(` |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<title>检验日报打印</title> |
|||
<style>${allStyles}</style> |
|||
</head> |
|||
<body style="background:white !important;"> |
|||
${printContent.outerHTML} |
|||
<body> |
|||
${content.outerHTML} |
|||
${debug ? '<div style="position: fixed; top: 10px; right: 10px; background: red; color: white; padding: 10px; z-index: 9999;">🔍 调试模式</div>' : ''} |
|||
</body> |
|||
</html> |
|||
`);
|
|||
iframeDoc.close(); |
|||
|
|||
// 触发打印
|
|||
iframe.onload = function() { |
|||
iframe.onload = () => { |
|||
setTimeout(() => { |
|||
if (debug) { |
|||
iframe.style.display = 'block'; |
|||
iframe.style.position = 'fixed'; |
|||
iframe.style.inset = '0'; |
|||
iframe.style.width = '100%'; |
|||
iframe.style.height = '100%'; |
|||
iframe.style.zIndex = '99999'; |
|||
iframe.style.background = 'white'; |
|||
console.log('✅ 调试模式已开启'); |
|||
} else { |
|||
iframe.contentWindow.print(); |
|||
// 延迟移除iframe
|
|||
setTimeout(() => document.body.removeChild(iframe), 1000); |
|||
setTimeout(() => iframe.remove(), 10000); |
|||
} |
|||
}, 300); |
|||
}; |
|||
} |
|||
|
|||
function collectAllStyles() { |
|||
const styles = []; |
|||
Array.from(document.styleSheets).forEach(sheet => { |
|||
try { |
|||
const rules = Array.from(sheet.cssRules).map(r => r.cssText).join('\n'); |
|||
if (rules) styles.push(rules); |
|||
} catch (e) { |
|||
console.warn('⚠️ 无法读取样式表:', e); |
|||
} |
|||
}); |
|||
document.querySelectorAll('style').forEach(style => { |
|||
if (style.textContent) styles.push(style.textContent); |
|||
}); |
|||
return styles.join('\n'); |
|||
} |
|||
Loading…
Reference in new issue