From 39d27ae5e73c50532f9aafa01fdeab5ac3988335 Mon Sep 17 00:00:00 2001 From: yelan Date: Sun, 27 Sep 2026 14:48:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E6=96=87=E4=B9=A6=E7=BA=B8):=20?= =?UTF-8?q?=E6=8A=BD=E5=87=BA=20A4=5FPAPER=5FMM=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=B1=8F=E7=BA=B8=E4=B8=8E=20PDF=20=E7=9A=84=E9=A1=B5=E8=BE=B9?= =?UTF-8?q?=E8=B7=9D=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clinical-web/src/utils/document-paper.ts | 63 ++++++++++++++++++++---- clinical-web/src/utils/signing-pdf.ts | 31 +++++++++--- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/clinical-web/src/utils/document-paper.ts b/clinical-web/src/utils/document-paper.ts index 412fe3a..6aa5a04 100644 --- a/clinical-web/src/utils/document-paper.ts +++ b/clinical-web/src/utils/document-paper.ts @@ -22,27 +22,67 @@ * 它们与「签署态」语义强相关,统一在 styles/signing-document.css。 */ +/** + * A4 纸的物理规格(mm)—— **屏幕上的纸、导出的 PDF 共用这一份**。 + * + * 屏幕那侧把它换算成「占纸宽的百分比」(% padding 以包含块宽度为基准,纸宽 == 包含块宽), + * PDF 那侧(`utils/signing-pdf.ts`)直接用 mm 当页边距。 + * **改这里两边一起变** —— 曾经 PDF 自己写死 15/12/14mm,与屏幕的 31.75/14.34mm 对不上, + * 表现为「下载的 PDF 和预览的边距不一样」(2026-09-27 修)。 + */ +export const A4_PAPER_MM = { + width: 210, + height: 297, + /** 左右页边距 3.18cm(Word 中文默认的「一般文书」留白) */ + marginX: 31.75, + /** + * 上下页边距 1.4cm(原型工作台 `#previewBox .paper` 的 56px 那一档)。 + * 比 Word 默认(2.54cm)紧是刻意的:820px 宽的纸上 2.54cm 会在顶部空出一大片。 + */ + marginY: 14.34, +} as const + +/** 版心宽(mm):正文实际占的宽度。屏幕与 PDF 都按它排版,换行位置才对得上 */ +export const A4_CONTENT_WIDTH_MM = A4_PAPER_MM.width - A4_PAPER_MM.marginX * 2 + +/** 屏幕上的纸宽上限(CSS px)。A4 在 96dpi 下约 794px,取 820 让正文更舒展 */ +export const A4_PAPER_MAX_WIDTH_PX = 820 + +/** 纸宽百分比(% padding 四边都以包含块宽度为基准,所以上下也用纸宽算) */ +const pct = (mm: number) => `${((mm / A4_PAPER_MM.width) * 100).toFixed(2)}%` + +/** 纸宽封顶时的页边距 px(= clamp 的上界),也是导出截图要用的版心尺寸 */ +export const A4_PAPER_PAD_X_PX = Math.round( + A4_PAPER_MAX_WIDTH_PX * (A4_PAPER_MM.marginX / A4_PAPER_MM.width), +) +export const A4_PAPER_PAD_Y_PX = Math.round( + A4_PAPER_MAX_WIDTH_PX * (A4_PAPER_MM.marginY / A4_PAPER_MM.width), +) + +/** 屏幕上的版心宽(px):导出 PDF 时按它排,换行位置才与屏幕一致 */ +export const A4_CONTENT_WIDTH_PX = A4_PAPER_MAX_WIDTH_PX - A4_PAPER_PAD_X_PX * 2 + export const DOCUMENT_PAPER_CSS = ` /* ============ A4 纸规格(全项目唯一一份) ============ A4 = 210mm × 297mm。页边距: - - 左右 3.18cm(Word 中文默认的「一般文书」留白)→ 占纸宽 15.14% - - 上下 1.4cm(原型工作台 #previewBox .paper 的 56px 那一档)→ 占纸宽 6.83% + - 左右 3.18cm(Word 中文默认的「一般文书」留白)→ 占纸宽 ${pct(A4_PAPER_MM.marginX)} + - 上下 1.4cm(原型工作台 #previewBox .paper 的 56px 那一档)→ 占纸宽 ${pct(A4_PAPER_MM.marginY)} 上下比左右紧是刻意的:820px 宽的纸上 2.54cm(Word 默认)会在顶部空出一大片, 屏幕上很显眼;原型用的就是这一档。 - 纸宽上限 820px 时对应的 px 值(124px / 56px)就是 clamp 的上界。 + 纸宽上限 ${A4_PAPER_MAX_WIDTH_PX}px 时对应的 px 值(${A4_PAPER_PAD_X_PX}px / ${A4_PAPER_PAD_Y_PX}px)就是 clamp 的上界。 + **数值来源是文件顶部的 A4_PAPER_MM(mm)**,导出 PDF 的页边距也从那里取 —— 别在这里写死数字。 为什么用 clamp 而不是裸百分比:% padding 以**包含块宽度**为基准,不是自身宽。 纸在窄容器里 width:100% 时两者相等(裸百分比正好);纸一旦被 max-width 收窄, 百分比就会按更宽的包含块算 → 页边距偏大。clamp 让「纸 == 容器」和「纸被收窄」 两种情形都得到同一套页边距。 - ⚠️ 换纸宽时两个 --doc-paper-pad-* 要跟着改(与 --doc-paper-max-w 是一组)。 ⚠️ 本段在模板字符串里,注释内**不要写反引号**,会提前结束字符串。 */ :root { - --doc-paper-max-w: 820px; - --doc-paper-pad-x: clamp(20px, 15.14%, 124px); - --doc-paper-pad-y: clamp(16px, 6.83%, 56px); - /* A4 的**最小高度**:纸宽封顶 820 时就是 1159.71px(= 820 × 297/210); - 容器比 820 窄时纸也会跟着窄,那时按容器宽 × 297/210 算(cqw = 容器宽的 1%)。 + --doc-paper-max-w: ${A4_PAPER_MAX_WIDTH_PX}px; + --doc-paper-pad-x: clamp(20px, ${pct(A4_PAPER_MM.marginX)}, ${A4_PAPER_PAD_X_PX}px); + --doc-paper-pad-y: clamp(16px, ${pct(A4_PAPER_MM.marginY)}, ${A4_PAPER_PAD_Y_PX}px); + /* A4 的**最小高度**:纸宽封顶 ${A4_PAPER_MAX_WIDTH_PX} 时就是 ${Math.round(A4_PAPER_MAX_WIDTH_PX * (A4_PAPER_MM.height / A4_PAPER_MM.width))}px(= 纸宽 × 297/210); + 容器比它窄时纸也会跟着窄,那时按容器宽 × 297/210 算(cqw = 容器宽的 1%)。 两个分支在「容器 == 纸宽」处正好接上,所以 min() 挑哪个都对。 ⚠️ **不要用 aspect-ratio 表达这个下限**(2026-09-27 踩过): @@ -52,7 +92,10 @@ export const DOCUMENT_PAPER_CSS = ` ⚠️ cqw 相对**最近的容器查询祖先**解析 —— 纸的父级必须带 container-type: inline-size(probe-a4-paper.cjs 会逐处断言纵向不溢出)。 */ - --doc-paper-min-h: min(calc(var(--doc-paper-max-w) * 297 / 210), 141.43cqw); + --doc-paper-min-h: min( + calc(var(--doc-paper-max-w) * ${(A4_PAPER_MM.height / A4_PAPER_MM.width).toFixed(6)}), + ${((A4_PAPER_MM.height / A4_PAPER_MM.width) * 100).toFixed(2)}cqw + ); } /* 一页 A4:至少一页的高度 + 一般文书页边距。 diff --git a/clinical-web/src/utils/signing-pdf.ts b/clinical-web/src/utils/signing-pdf.ts index 8497a86..4da763e 100644 --- a/clinical-web/src/utils/signing-pdf.ts +++ b/clinical-web/src/utils/signing-pdf.ts @@ -16,16 +16,31 @@ * 输出为位图 PDF:文本不可选中,仅供原型演示;正式文书以服务端生成的 PDF 为准。 */ -const PAGE_WIDTH_MM = 210 -const PAGE_HEIGHT_MM = 297 -const MARGIN_X_MM = 15 -const MARGIN_TOP_MM = 12 -const MARGIN_BOTTOM_MM = 14 -const CONTENT_WIDTH_MM = PAGE_WIDTH_MM - MARGIN_X_MM * 2 +import { A4_CONTENT_WIDTH_MM, A4_CONTENT_WIDTH_PX, A4_PAPER_MM } from './document-paper' + +const PAGE_WIDTH_MM = A4_PAPER_MM.width +const PAGE_HEIGHT_MM = A4_PAPER_MM.height +/** + * 页边距与屏幕上的纸**同一份口径**(`utils/document-paper.ts` 的 `A4_PAPER_MM`)。 + * + * ⚠️ 这里曾经自己写死 15 / 12 / 14mm,与屏幕的 31.75 / 14.34mm 对不上 —— + * 表现为「下载的 PDF 和预览的边距不一样」,而且版心宽度也差 33mm、换行位置跟着变。 + * **不要再在这里写 mm 数字**,要调就调 `A4_PAPER_MM`。 + */ +const MARGIN_X_MM = A4_PAPER_MM.marginX +const MARGIN_TOP_MM = A4_PAPER_MM.marginY +const MARGIN_BOTTOM_MM = A4_PAPER_MM.marginY +const CONTENT_WIDTH_MM = A4_CONTENT_WIDTH_MM const CONTENT_HEIGHT_MM = PAGE_HEIGHT_MM - MARGIN_TOP_MM - MARGIN_BOTTOM_MM -/** 截图版心宽度(px):约等于 180mm @96dpi,让 PDF 版面接近实际纸张 */ -const CAPTURE_WIDTH_PX = 680 +/** + * 截图版心宽度(px)= 屏幕上纸的版心宽度(`A4_CONTENT_WIDTH_PX`)。 + * + * 为什么必须是这个值而不是「约 180mm @96dpi」:截图时把克隆体宽度钉成它, + * 正文的换行位置才与屏幕完全一致(都是同一个版心宽); + * 再按 `CONTENT_WIDTH_MM / canvas.width` 缩放铺进 PDF,物理尺寸也就对了。 + */ +const CAPTURE_WIDTH_PX = A4_CONTENT_WIDTH_PX /** 截图倍率:2 倍下正文与小字都还清晰 */ const CAPTURE_SCALE = 2 /** 量不到页眉时的兜底高度(px) */