fix: 修复出库单打印预览空白 - iframe 改用 screen 隐藏/print 显示,并等待字体加载
This commit is contained in:
@@ -34,5 +34,7 @@ Thumbs.db
|
|||||||
# ===== 工作区记忆 / 调试产物 =====
|
# ===== 工作区记忆 / 调试产物 =====
|
||||||
.codebuddy/
|
.codebuddy/
|
||||||
server/scripts/*.pdf
|
server/scripts/*.pdf
|
||||||
|
server/scripts/_token.txt
|
||||||
|
server/scripts/print-preview.png
|
||||||
server/src/F9MES.Api/logs-err.txt
|
server/src/F9MES.Api/logs-err.txt
|
||||||
server/src/F9MES.Api/logs-out.txt
|
server/src/F9MES.Api/logs-out.txt
|
||||||
|
|||||||
@@ -57,3 +57,22 @@ canvas * {
|
|||||||
.n-input-number .n-input__input-el {
|
.n-input-number .n-input__input-el {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 静默打印 iframe:
|
||||||
|
- screen 下隐藏在屏幕下方,避免遮挡 UI
|
||||||
|
- print 下显示在左上角,确保 Chrome/Edge 打印对话框能正确渲染预览
|
||||||
|
(left:-9999px 会使部分浏览器不渲染屏幕外 iframe)。 */
|
||||||
|
@media screen {
|
||||||
|
.op-silent-print-frame {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.op-silent-print-frame {
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 999999;
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ function parsePaperSize(html: string): { w: string; h: string } {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 静默打印:瞬态 iframe + srcdoc + window.print(),完成后立即移除。
|
* 静默打印:瞬态 iframe + srcdoc + window.print(),完成后立即移除。
|
||||||
* 注意:iframe 必须带实际纸张尺寸并移出屏幕(不可 0×0,否则 Chrome/Edge
|
* 注意:iframe 必须有实际纸张尺寸;将其放在屏幕下方并通过 CSS 在 screen 下隐藏、
|
||||||
* 打印对话框以 0 视口渲染,预览为空白),
|
* print 下显示,避免 left:-9999px 导致某些 Chrome/Edge 版本的打印对话框预览空白。
|
||||||
*/
|
*/
|
||||||
function silentPrint(html: string): Promise<void> {
|
function silentPrint(html: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -173,21 +173,31 @@ function silentPrint(html: string): Promise<void> {
|
|||||||
const { w, h } = parsePaperSize(html)
|
const { w, h } = parsePaperSize(html)
|
||||||
const iframe = document.createElement('iframe')
|
const iframe = document.createElement('iframe')
|
||||||
iframe.setAttribute('aria-hidden', 'true')
|
iframe.setAttribute('aria-hidden', 'true')
|
||||||
// 移到屏幕外但保留纸张尺寸视口,保证打印对话框预览可见
|
// 通过 class + @media 在 screen 下隐藏、print 下显示,避免屏幕外渲染问题
|
||||||
iframe.style.cssText = `position:fixed;left:-9999px;top:0;width:${w};height:${h};border:0;`
|
iframe.className = 'op-silent-print-frame'
|
||||||
|
iframe.style.cssText = `position:fixed;left:0;top:100vh;width:${w};height:${h};border:0;`
|
||||||
let removed = false
|
let removed = false
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (removed) return
|
if (removed) return
|
||||||
removed = true
|
removed = true
|
||||||
setTimeout(() => iframe.remove(), 300)
|
setTimeout(() => iframe.remove(), 300)
|
||||||
}
|
}
|
||||||
iframe.onload = () => {
|
iframe.onload = async () => {
|
||||||
const cw = iframe.contentWindow
|
const cw = iframe.contentWindow
|
||||||
if (!cw) {
|
const cd = iframe.contentDocument
|
||||||
|
if (!cw || !cd) {
|
||||||
cleanup()
|
cleanup()
|
||||||
reject(new Error('iframe 无 contentWindow'))
|
reject(new Error('iframe 无 contentWindow'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 等待字体就绪,避免打印预览空白/缺字
|
||||||
|
if (cd.fonts && cd.fonts.ready) {
|
||||||
|
try {
|
||||||
|
await Promise.race([cd.fonts.ready, new Promise((r) => setTimeout(r, 2000))])
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
// onafterprint 在用户确认打印或取消时都会触发
|
// onafterprint 在用户确认打印或取消时都会触发
|
||||||
cw.onafterprint = () => {
|
cw.onafterprint = () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// 视觉验证:打开打印页,等待 iframe 就绪 + 字体加载,截图 iframe 区域
|
||||||
|
import { createRequire } from 'module'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
const PW = 'C:/Users/范先生/AppData/Roaming/npm/node_modules/@playwright/cli/node_modules/playwright-core'
|
||||||
|
const require = createRequire(PW + '/package.json')
|
||||||
|
const { chromium } = require(PW)
|
||||||
|
|
||||||
|
const B = 'http://localhost:5136'
|
||||||
|
const token = fs.readFileSync(new URL('_token.txt', import.meta.url), 'utf8').trim()
|
||||||
|
const url = `http://localhost:5227/?print=1&template=2&table=RawMaterial_OutStock&row=2&token=${encodeURIComponent(token)}&api=${B}`
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: true, channel: 'msedge' })
|
||||||
|
const page = await browser.newPage({ viewport: { width: 900, height: 500 } })
|
||||||
|
page.on('pageerror', (e) => console.log('[pageerror]', e.message))
|
||||||
|
page.on('console', (m) => { if (m.type() === 'error') console.log('[console]', m.text()) })
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 })
|
||||||
|
|
||||||
|
for (let i = 0; i < 40; i++) {
|
||||||
|
const ok = await page.evaluate(() => {
|
||||||
|
const f = document.querySelector('iframe')
|
||||||
|
return !!(f && f.srcdoc && f.srcdoc.length > 1000)
|
||||||
|
})
|
||||||
|
if (ok) break
|
||||||
|
await new Promise((r) => setTimeout(r, 500))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等字体与图片加载
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const f = document.querySelector('iframe')
|
||||||
|
if (f && f.contentDocument) {
|
||||||
|
await f.contentDocument.fonts.ready.catch(() => {})
|
||||||
|
await new Promise((r) => setTimeout(r, 800))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟浏览器进入打印预览/打印媒体,验证 iframe 在 @media print 下可见
|
||||||
|
await page.emulateMedia({ media: 'print' })
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await page.screenshot({ path: 'print-preview.png' })
|
||||||
|
const buf = fs.statSync('print-preview.png')
|
||||||
|
console.log('SHOT_SAVED size=' + buf.size)
|
||||||
|
|
||||||
|
// 用 canvas 判断内容像素比例(主页面同源 iframe 可访问)
|
||||||
|
const stats = await page.evaluate(async () => {
|
||||||
|
const f = document.querySelector('iframe')
|
||||||
|
if (!f || !f.contentDocument) return { err: 'no-iframe' }
|
||||||
|
const d = f.contentDocument
|
||||||
|
const w = 210 * 96 / 25.4 // 793px
|
||||||
|
const h = 99 * 96 / 25.4 // 374px
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = w
|
||||||
|
canvas.height = h
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.fillStyle = '#fff'
|
||||||
|
ctx.fillRect(0, 0, w, h)
|
||||||
|
// 将 iframe body 克隆到独立容器渲染后截图(drawWindow 不可用,改用 HTML 转图)
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.style.position = 'fixed'
|
||||||
|
container.style.left = '0'
|
||||||
|
container.style.top = '0'
|
||||||
|
container.style.zIndex = '99999'
|
||||||
|
container.appendChild(d.body.cloneNode(true))
|
||||||
|
document.body.appendChild(container)
|
||||||
|
// 用 SVG foreignObject 序列化渲染
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml">${d.body.innerHTML}</div></foreignObject></svg>`
|
||||||
|
const img = new Image()
|
||||||
|
const ok = await new Promise((resolve) => {
|
||||||
|
img.onload = () => resolve(true)
|
||||||
|
img.onerror = () => resolve(false)
|
||||||
|
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
|
||||||
|
})
|
||||||
|
document.body.removeChild(container)
|
||||||
|
if (!ok) return { err: 'svg-fail' }
|
||||||
|
ctx.drawImage(img, 0, 0, w, h)
|
||||||
|
const data = ctx.getImageData(0, 0, w, h).data
|
||||||
|
let nonWhite = 0
|
||||||
|
for (let i = 0; i < data.length; i += 40) {
|
||||||
|
const r = data[i], g = data[i + 1], b = data[i + 2]
|
||||||
|
if (r < 245 || g < 245 || b < 245) nonWhite++
|
||||||
|
}
|
||||||
|
const total = Math.floor(data.length / 4 / 10)
|
||||||
|
return { nonWhiteRatio: (nonWhite / total).toFixed(4) }
|
||||||
|
})
|
||||||
|
console.log('PIXEL=' + JSON.stringify(stats))
|
||||||
|
await browser.close()
|
||||||
|
console.log('DONE')
|
||||||
Reference in New Issue
Block a user