feat(oa): generate mobile OA approval PDF via html2canvas and jspdf

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-13 10:56:37 +08:00
1 parent 505bf29fdd
commit bf22b29d80
3 files changed
+391 -46

No files matched your search

+235
View File
@@ -9,6 +9,8 @@
"version": "0.1.0",
"dependencies": {
"@form-create/vant": "^3.2.14",
"html2canvas": "^1.4.1",
"jspdf": "^4.2.1",
"vant": "^4.9.21",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
@@ -51,6 +53,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.8",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz",
@@ -928,6 +939,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@vant/popperjs": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz",
@@ -1061,6 +1092,59 @@
"integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/core-js": {
"version": "3.50.0",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.50.0.tgz",
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"engines": {
"node": "*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
@@ -1073,6 +1157,16 @@
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@@ -1133,6 +1227,17 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmmirror.com/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
@@ -1151,6 +1256,12 @@
}
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"license": "MIT"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1166,6 +1277,42 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmmirror.com/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/jspdf": {
"version": "4.2.1",
"resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-4.2.1.tgz",
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -1193,6 +1340,29 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/pako": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.2.0.tgz",
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "(MIT AND Zlib)"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@@ -1241,6 +1411,33 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.62.4",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.4.tgz",
@@ -1296,6 +1493,35 @@
"node": ">=0.10.0"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -1313,6 +1539,15 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vant": {
"version": "4.10.0",
"resolved": "https://registry.npmmirror.com/vant/-/vant-4.10.0.tgz",
+2
View File
@@ -10,6 +10,8 @@
},
"dependencies": {
"@form-create/vant": "^3.2.14",
"html2canvas": "^1.4.1",
"jspdf": "^4.2.1",
"vant": "^4.9.21",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
+154 -46
View File
@@ -1,8 +1,10 @@
/**
* OA 审批单移动端打印(S6)
* 对齐 web-admin `oa-approval-print.ts` 字段/流程结构;优先 window.print(),无 jspdf 依赖。
* 对齐 web-admin `oa-approval-print.ts` 字段/流程结构;支持 html2canvas + jsPDF 导出。
* Spec: docs/superpowers/specs/2026-08-12-mobile-oa-task-centers-design.md §6.5
*/
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import { formatDeptDisplay } from '../kit/oa-dept-value.js'
import { formatPersonDisplay } from '../kit/oa-person-value.js'
import { formatIdNameDisplay } from '../../utils/id-name-value.js'
@@ -13,6 +15,7 @@ import {
import { toRmbUpper } from '../../utils/rmb-upper.js'
import { OA_STATUS_TEXT } from '../status-label.js'
import { resolveStamp } from '../oa-status-stamp.js'
import { buildOaPrintFilename } from './oa-print-filename.js'
export { resolveStamp }
@@ -597,42 +600,8 @@ function flowCellHtml(f) {
return html
}
function buildPrintHtml(model) {
const stampChars3 = model.stampText.length >= 3 ? ' is-chars3' : ''
const wmItems = Array.from({ length: 24 }, (_, i) => {
const row = Math.floor(i / 3)
const col = i % 3
const top = 40 + row * 100
const left = -20 + col * 240 + (row % 2) * 40
return `<span class="oa-print-wm__item" style="top:${top}px;left:${left}px">${escapeHtml(model.watermarkName)} ${escapeHtml(model.watermarkName)}</span>`
}).join('')
let rowsHtml = ''
for (const row of model.rows) {
rowsHtml += `<tr><th>${escapeHtml(row.label)}</th><td>${cellInnerHtml(row)}</td></tr>`
}
let flowHtml = ''
if (model.flowItems.length) {
model.flowItems.forEach((f, fi) => {
flowHtml += '<tr class="oa-print-flow">'
if (fi === 0) {
flowHtml += `<th class="oa-print-flow__label" rowspan="${model.flowItems.length}">审批流程</th>`
}
flowHtml += `<td class="oa-print-flow__cell">${flowCellHtml(f)}</td></tr>`
})
} else {
flowHtml =
'<tr class="oa-print-flow"><th class="oa-print-flow__label">审批流程</th><td class="oa-print-flow__cell"><span class="oa-print-text">-</span></td></tr>'
}
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>${escapeHtml(model.title)}</title>
<style>
function buildPrintStylesContent() {
return `
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1f2329;
font-family: "PingFang SC","Microsoft YaHei","Helvetica Neue",Arial,sans-serif; }
@@ -702,15 +671,39 @@ function buildPrintHtml(model) {
line-height: 1; color: currentColor; }
.oa-stamp .main.is-chars3 { font-size: 26px; letter-spacing: 1px; }
.oa-print-footer { display: flex; flex-wrap: wrap; justify-content: space-between;
gap: 8px; margin-top: 16px; font-size: 12px; color: #646464; }
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.oa-print-page { max-width: none; padding: 12mm 14mm; }
gap: 8px; margin-top: 16px; font-size: 12px; color: #646464; }`
}
function buildPrintPageContent(model) {
const stampChars3 = model.stampText.length >= 3 ? ' is-chars3' : ''
const wmItems = Array.from({ length: 24 }, (_, i) => {
const row = Math.floor(i / 3)
const col = i % 3
const top = 40 + row * 100
const left = -20 + col * 240 + (row % 2) * 40
return `<span class="oa-print-wm__item" style="top:${top}px;left:${left}px">${escapeHtml(model.watermarkName)} ${escapeHtml(model.watermarkName)}</span>`
}).join('')
let rowsHtml = ''
for (const row of model.rows) {
rowsHtml += `<tr><th>${escapeHtml(row.label)}</th><td>${cellInnerHtml(row)}</td></tr>`
}
</style>
</head>
<body>
<div class="oa-print-page">
let flowHtml = ''
if (model.flowItems.length) {
model.flowItems.forEach((f, fi) => {
flowHtml += '<tr class="oa-print-flow">'
if (fi === 0) {
flowHtml += `<th class="oa-print-flow__label" rowspan="${model.flowItems.length}">审批流程</th>`
}
flowHtml += `<td class="oa-print-flow__cell">${flowCellHtml(f)}</td></tr>`
})
} else {
flowHtml =
'<tr class="oa-print-flow"><th class="oa-print-flow__label">审批流程</th><td class="oa-print-flow__cell"><span class="oa-print-text">-</span></td></tr>'
}
return `<div class="oa-print-page">
<div class="oa-print-wm" aria-hidden="true">${wmItems}</div>
<div class="oa-print-inner">
<h1 class="oa-print-title">${escapeHtml(model.title)}</h1>
@@ -736,11 +729,126 @@ function buildPrintHtml(model) {
<span>打印人:${escapeHtml(model.printerName)}</span>
</div>
</div>
</div>
</div>`
}
export function buildPrintBodyHtml(model) {
return `<style>${buildPrintStylesContent()}</style>${buildPrintPageContent(model)}`
}
function buildPrintHtml(model) {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>${escapeHtml(model.title)}</title>
<style>${buildPrintStylesContent()}</style>
</head>
<body>
${buildPrintPageContent(model)}
</body>
</html>`
}
/** 用 canvas 绘制中文页码(jsPDF 内置字体不支持汉字,会乱码) */
function renderPageLabelCanvas(text) {
const font = '20px "PingFang SC", "Microsoft YaHei", "Heiti SC", sans-serif'
const measure = document.createElement('canvas')
const mctx = measure.getContext('2d')
if (!mctx) {
const empty = document.createElement('canvas')
empty.width = 1
empty.height = 1
return empty
}
mctx.font = font
const tw = Math.ceil(mctx.measureText(text).width) + 8
const th = 28
const scale = 2
const c = document.createElement('canvas')
c.width = tw * scale
c.height = th * scale
const ctx = c.getContext('2d')
if (!ctx) return c
ctx.scale(scale, scale)
ctx.clearRect(0, 0, tw, th)
ctx.font = font
ctx.fillStyle = '#646464'
ctx.textBaseline = 'middle'
ctx.fillText(text, 4, th / 2)
return c
}
async function canvasToPdfBlob(canvas) {
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
const pageWidth = pdf.internal.pageSize.getWidth()
const pageHeight = pdf.internal.pageSize.getHeight()
const imgWidth = pageWidth
const imgHeight = (canvas.height * imgWidth) / canvas.width
const EPS_MM = 1.5
const totalPages = Math.min(40, Math.max(1, Math.ceil((imgHeight - EPS_MM) / pageHeight)))
const pageData = canvas.toDataURL('image/jpeg', 0.95)
const labelMmH = 4.5
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
if (pageIndex > 0) pdf.addPage()
const position = -pageIndex * pageHeight
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
const label = `第${pageIndex + 1}页`
const labelCanvas = renderPageLabelCanvas(label)
if (labelCanvas.width > 1) {
const labelMmW = (labelCanvas.width / labelCanvas.height) * labelMmH
pdf.addImage(
labelCanvas.toDataURL('image/png'),
'PNG',
(pageWidth - labelMmW) / 2,
pageHeight - 10,
labelMmW,
labelMmH
)
}
}
return pdf.output('blob')
}
export async function generateOaApprovalPdfBlob(detail, opts = {}) {
if (!detail) throw new Error('暂无审批详情,无法导出')
const model = buildOaPrintModel(detail, {
printerName: opts.printerName || ''
})
const filename = buildOaPrintFilename(detail)
const host = document.createElement('div')
host.setAttribute('data-oa-print-host', '1')
host.style.cssText =
'position:fixed;left:-100000px;top:0;width:794px;z-index:-1;opacity:1;pointer-events:none;'
document.body.appendChild(host)
host.innerHTML = buildPrintBodyHtml(model)
try {
await new Promise((r) => requestAnimationFrame(() => r()))
await new Promise((r) => setTimeout(r, 50))
const el = host.querySelector('.oa-print-page')
if (!el) throw new Error('print sheet missing')
const canvas = await html2canvas(el, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false,
windowWidth: el.scrollWidth,
windowHeight: el.scrollHeight
})
const blob = await canvasToPdfBlob(canvas)
return { blob, filename }
} finally {
host.remove()
}
}
function isWeChatMiniWebView() {
try {
if (typeof window !== 'undefined' && window.__wxjs_environment === 'miniprogram') {