fix(document): 去掉签署态的系统页眉,患者信息改由正文占位符承载
This commit is contained in:
@@ -5,14 +5,16 @@
|
||||
* 再按 A4 分页拼进 jsPDF,让前端能先看到成品效果:
|
||||
* ① 截图用固定版心宽度:实时容器有固定高度、内边距,且宽度随工作台面板变化,
|
||||
* 直接截图既截不全、版面也会跟着变;
|
||||
* ② 固定页眉在每页重复:页眉是系统按 HIS 数据渲染的,服务端生成 PDF 时同样要按页重复;
|
||||
* ③ 页码只写 "1 / 2" 这类 ASCII:jsPDF 内置字体不含中文,
|
||||
* ② 页码只写 "1 / 2" 这类 ASCII:jsPDF 内置字体不含中文,
|
||||
* 画中文页码需要内嵌字体(体积会明显变大),这一步留给服务端用真字体实现;
|
||||
* ④ 导出前对 flex 容器做一次兼容降级:html2canvas 不实现 flex 布局,
|
||||
* 填写项胶囊里的文字、输入框取值、签名图会被整块丢掉,见 applyExportFlexFallback。
|
||||
* ⑤ 「只读副本」的水印在克隆体上平铺(injectWatermark):屏幕上那份文书不出现水印,
|
||||
* ③ 「只读副本」的水印在克隆体上平铺(injectWatermark):屏幕上那份文书不出现水印,
|
||||
* 只有导出的 PDF 带;水印烙进截图,每一页都有,且改不掉文本层。
|
||||
*
|
||||
* **每页只有正文,没有页眉**(2026-09-30 去掉):原先会在每页顶部重复文书那条
|
||||
* 系统页眉(患者姓名 / 性别 / 年龄 / 住院号),而编辑器里根本没有它 ——
|
||||
* 编辑时看不到、签出来和下载的 PDF 上却多一条,是"渲染端自己长出来的"东西。
|
||||
* 患者信息要出现在文书上,就在正文里插对应的占位符。
|
||||
*
|
||||
* 输出为位图 PDF:文本不可选中,仅供原型演示;正式文书以服务端生成的 PDF 为准。
|
||||
*/
|
||||
|
||||
@@ -43,13 +45,9 @@ const CONTENT_HEIGHT_MM = PAGE_HEIGHT_MM - MARGIN_TOP_MM - MARGIN_BOTTOM_MM
|
||||
const CAPTURE_WIDTH_PX = A4_CONTENT_WIDTH_PX
|
||||
/** 截图倍率:2 倍下正文与小字都还清晰 */
|
||||
const CAPTURE_SCALE = 2
|
||||
/** 量不到页眉时的兜底高度(px) */
|
||||
const FALLBACK_HEADER_PX = 46
|
||||
/** 页眉高到每页留不出正文时的下限(mm),低于它就不再重复页眉,避免切出上百页 */
|
||||
const MIN_BODY_HEIGHT_MM = 60
|
||||
|
||||
export interface SigningPdfInput {
|
||||
/** 已渲染的文书容器(含固定页眉与签署态取值) */
|
||||
/** 已渲染的文书容器(含签署态取值) */
|
||||
element: HTMLElement
|
||||
/** 文件名,不带扩展名 */
|
||||
fileName: string
|
||||
@@ -120,50 +118,16 @@ function injectWatermark(root: HTMLElement, text: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* html2canvas 兼容处理:flex 容器里的文字与控件取值会被整块丢掉。
|
||||
* ⚠️ html2canvas **不实现 flex 布局**:纸里若出现 flex 容器,容器内的文字、输入框取值
|
||||
* 与签名 `<img>` 会被整块丢掉(实测 html2canvas 1.4:页面上明明印着「与患者关系:配偶」、
|
||||
* 签名图也画出来了,导出的位图里却只剩胶囊的边框和底色)。
|
||||
*
|
||||
* 实测(签署工作台导出的 PDF,html2canvas 1.4):页面上明明印着「与患者关系:文本框 配偶」、
|
||||
* 签名图也画出来了,导出的位图里这些却全是空白,只剩胶囊的边框和底色——
|
||||
* 当时填写项胶囊与它的控件容器都是 `display: inline-flex`,html2canvas 算不出容器内
|
||||
* 文本的落点,于是连同取值与签名 `<img>` 一起被丢掉。
|
||||
*
|
||||
* 现在填写项已按原型口径改成 `display: inline-block`(.docfld / .docopt,
|
||||
* 见 styles/signing-document.css),这条降级只剩系统页眉那一层:
|
||||
* 它是 flex 排的一行患者信息,导出前改成 block + inline-block,
|
||||
* 并用等效的 margin 补回原来的项间距,保证页眉在 PDF 里不丢字。
|
||||
*
|
||||
* **只在导出克隆体上改**:屏幕上的版式一行不动。
|
||||
* 所以能在纸上用 block / inline-block 表达的排版就别用 flex —— 填写项胶囊(`.docfld`)
|
||||
* 正是按这个口径落成 inline-block 的(见 styles/signing-document.css)。
|
||||
* 历史上系统页眉是 flex 排的一行患者信息,导出前要在克隆体上降级成 block + inline-block、
|
||||
* 再用 margin 补回间距;页眉已在 2026-09-30 去掉,那段降级代码一并删了。
|
||||
* 若以后又要在纸上排 flex,记得把同样的降级加回 `onclone` 里。
|
||||
*/
|
||||
function applyExportFlexFallback(root: HTMLElement) {
|
||||
const header = root.querySelector<HTMLElement>('.sign-doc-header')
|
||||
|
||||
if (!header) {
|
||||
return
|
||||
}
|
||||
|
||||
header.style.display = 'block'
|
||||
|
||||
// gap: 10px 18px 在 inline-block 下失效,用 margin 补回来
|
||||
root.querySelectorAll<HTMLElement>('.sign-doc-header__item').forEach((item) => {
|
||||
item.style.display = 'inline-block'
|
||||
item.style.marginRight = '18px'
|
||||
item.style.marginBottom = '6px'
|
||||
})
|
||||
}
|
||||
|
||||
/** 量固定页眉的高度(含下外边距)——它是每页要重复的那一条 */
|
||||
function measureHeaderHeight(element: HTMLElement) {
|
||||
const header = element.querySelector<HTMLElement>('.sign-doc-header')
|
||||
|
||||
if (!header) {
|
||||
return FALLBACK_HEADER_PX
|
||||
}
|
||||
|
||||
const marginBottom = Number.parseFloat(window.getComputedStyle(header).marginBottom) || 0
|
||||
const height = header.getBoundingClientRect().height + marginBottom
|
||||
|
||||
return height > 0 ? Math.round(height) : FALLBACK_HEADER_PX
|
||||
}
|
||||
|
||||
export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
|
||||
// 按需加载:这两个库合计约 600KB,只在真正导出 PDF 时才拉取,
|
||||
@@ -174,17 +138,13 @@ export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
|
||||
])
|
||||
|
||||
const { element } = input
|
||||
// 克隆体在目标版心下重新排版,页眉高度以克隆体为准(实时容器可能更窄,页眉会换行)
|
||||
let clonedHeaderPx = 0
|
||||
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: CAPTURE_SCALE,
|
||||
backgroundColor: '#ffffff',
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
onclone: (clonedDocument, clonedElement) => {
|
||||
applyExportFlexFallback(clonedElement)
|
||||
|
||||
onclone: (_clonedDocument, clonedElement) => {
|
||||
clonedElement.style.width = `${CAPTURE_WIDTH_PX}px`
|
||||
clonedElement.style.height = 'auto'
|
||||
clonedElement.style.maxHeight = 'none'
|
||||
@@ -193,7 +153,7 @@ export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
|
||||
clonedElement.style.margin = '0'
|
||||
clonedElement.style.background = '#ffffff'
|
||||
|
||||
// 水印在量页眉之前注入:它是绝对定位层,不参与布局,不会影响页眉高度
|
||||
// 水印是绝对定位层,不参与布局,注入前后都不影响分页切片的位置
|
||||
if (input.watermark) {
|
||||
injectWatermark(clonedElement, input.watermark)
|
||||
}
|
||||
@@ -209,41 +169,17 @@ export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
|
||||
paper.style.boxShadow = 'none'
|
||||
paper.style.borderRadius = '0'
|
||||
}
|
||||
|
||||
const header = clonedElement.querySelector<HTMLElement>('.sign-doc-header')
|
||||
|
||||
if (!header) {
|
||||
return
|
||||
}
|
||||
|
||||
const marginBottom =
|
||||
Number.parseFloat(
|
||||
clonedDocument.defaultView?.getComputedStyle(header).marginBottom ?? '0',
|
||||
) || 0
|
||||
|
||||
clonedHeaderPx = Math.round(header.getBoundingClientRect().height + marginBottom)
|
||||
},
|
||||
})
|
||||
|
||||
const mmPerPx = CONTENT_WIDTH_MM / canvas.width
|
||||
const headerPx = clonedHeaderPx > 0 ? clonedHeaderPx : measureHeaderHeight(element)
|
||||
let headerCanvasPx = Math.min(Math.max(Math.round(headerPx * CAPTURE_SCALE), 0), canvas.height)
|
||||
|
||||
if (CONTENT_HEIGHT_MM - headerCanvasPx * mmPerPx < MIN_BODY_HEIGHT_MM) {
|
||||
headerCanvasPx = 0
|
||||
}
|
||||
|
||||
const bodyTopPx = headerCanvasPx
|
||||
const bodyTotalPx = Math.max(canvas.height - bodyTopPx, 1)
|
||||
const bodyPagePx = Math.max(
|
||||
Math.floor((CONTENT_HEIGHT_MM - headerCanvasPx * mmPerPx) / mmPerPx),
|
||||
1,
|
||||
)
|
||||
const pageCount = Math.max(Math.ceil(bodyTotalPx / bodyPagePx), 1)
|
||||
// 一页的版心高(px):正文按它连续切片,第 N 页从 (N-1) × 这个值开始
|
||||
const bodyPagePx = Math.max(Math.floor(CONTENT_HEIGHT_MM / mmPerPx), 1)
|
||||
const pageCount = Math.max(Math.ceil(canvas.height / bodyPagePx), 1)
|
||||
|
||||
const pageCanvas = document.createElement('canvas')
|
||||
pageCanvas.width = canvas.width
|
||||
pageCanvas.height = headerCanvasPx + bodyPagePx
|
||||
pageCanvas.height = bodyPagePx
|
||||
|
||||
const context = pageCanvas.getContext('2d')
|
||||
|
||||
@@ -258,23 +194,8 @@ export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
|
||||
context.fillStyle = '#ffffff'
|
||||
context.fillRect(0, 0, pageCanvas.width, pageCanvas.height)
|
||||
|
||||
// 页眉:每页重复文书顶部那一条
|
||||
if (headerCanvasPx > 0) {
|
||||
context.drawImage(
|
||||
canvas,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
headerCanvasPx,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
headerCanvasPx,
|
||||
)
|
||||
}
|
||||
|
||||
// 正文:按页连续切片,接在页眉下面
|
||||
const sourceY = bodyTopPx + page * bodyPagePx
|
||||
// 正文:从上一页的断点接着切,整页铺满版心(页眉已去掉,不再为它留位置)
|
||||
const sourceY = page * bodyPagePx
|
||||
const sliceHeight = Math.min(bodyPagePx, canvas.height - sourceY)
|
||||
|
||||
if (sliceHeight > 0) {
|
||||
@@ -285,7 +206,7 @@ export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
|
||||
canvas.width,
|
||||
sliceHeight,
|
||||
0,
|
||||
headerCanvasPx,
|
||||
0,
|
||||
canvas.width,
|
||||
sliceHeight,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user