feat(签署工作台): 补代签标签、关联处方卡、线上等待态与只读副本

按原型 V10.2 详情页补齐四块:

- 任务列表「代签」标签(原型 .vtag.chk 橙底):SigningTaskRecord 增加
  signerType / signerRelation,仅签署人非「本人」时显示。
  有意偏离:关系为空时不输出空括号(原型会渲染成「家属代签()」)。
- 「关联处方」可折叠卡(原型 rxCard):新建 api/workbench/prescriptions.ts
  统一处方来源,把弹窗里那份搬过来消除重复,并新增详情用的 7 列明细。
  原型两处处方口径本就不同(弹窗按就诊号哈希、详情按任务号派生),分开维护不合并。
  真实模式返回空 → 整卡不渲染,不显示空表(会被读成「系统查过了,确实没处方」)。
  顺带按原型调了详情卡片顺序:文书 → 签署文件 → 关联处方 → 操作留痕。
- 短信 / 公众号待签署的等待态(原型 .tg-wait):号码取后端 smsMobileMasked,
  没有值时不拼号码,不拿患者档案里的号码冒充发送目标。
- 已签署的「在线预览(PDF)」与「只读副本」:
  预览弹窗复用 SigningDocumentInteractive(只读态),保证预览 / 工作台 / 导出 PDF
  三处版式一致;只读副本走 signing-pdf 新增的 watermark 选项,
  在 html2canvas 克隆体上平铺斜排水印,审计编号直接用任务号。
  水印行数按克隆体 scrollHeight 算,写死行数会让长文书下半部分没水印。
This commit is contained in:
yelan
2026-09-26 19:45:40 +08:00
parent f871613abe
commit 82acc03442
11 changed files with 882 additions and 67 deletions
+71
View File
@@ -10,6 +10,8 @@
* 画中文页码需要内嵌字体(体积会明显变大),这一步留给服务端用真字体实现;
* ④ 导出前对 flex 容器做一次兼容降级:html2canvas 不实现 flex 布局,
* 填写项胶囊里的文字、输入框取值、签名图会被整块丢掉,见 applyExportFlexFallback。
* ⑤ 「只读副本」的水印在克隆体上平铺(injectWatermark):屏幕上那份文书不出现水印,
* 只有导出的 PDF 带;水印烙进截图,每一页都有,且改不掉文本层。
*
* 输出为位图 PDF:文本不可选中,仅供原型演示;正式文书以服务端生成的 PDF 为准。
*/
@@ -36,6 +38,70 @@ export interface SigningPdfInput {
element: HTMLElement
/** 文件名,不带扩展名 */
fileName: string
/**
* 只读副本水印文案。
*
* 只有「只读副本」这类对外提供、不可作为正式凭证的副本才传;
* 正式文书(PDF 原件)不加水印,传空即原样导出。
* 水印直接烙进截图,因此每一页都会带上,且无法通过编辑 PDF 文本层去掉。
*/
watermark?: string
}
/** 水印平铺的列数:5 列按 20% 步进,再左移 10% 让斜排文字盖住左右两侧 */
const WATERMARK_COLUMNS = 5
/** 水印行距(px):比文字高度大,斜排后不重叠 */
const WATERMARK_ROW_GAP_PX = 90
/**
* 往克隆体里平铺斜向水印。
*
* 必须在 `onclone` 里做:屏幕上那份文书不能出现水印,
* 而 html2canvas 只截克隆体,所以改克隆体既不影响界面也能进 PDF。
*
* 行数按克隆体的实际高度算(`scrollHeight` 会强制一次重排,这里正是需要的时机),
* 写死行数会让长文书的下半部分没有水印。
*/
function injectWatermark(root: HTMLElement, text: string) {
const height = Math.max(root.scrollHeight, root.clientHeight, 1)
const rows = Math.ceil(height / WATERMARK_ROW_GAP_PX) + 2
// 绝对定位的水印层要有定位祖先;容器自身多半是 static,
// 这里只改克隆体,屏幕上的版式不受影响
root.style.position = 'relative'
const overlay = document.createElement('div')
overlay.style.cssText = [
'position:absolute',
'inset:0',
'z-index:9',
'overflow:hidden',
'pointer-events:none',
].join(';')
for (let row = 0; row < rows; row += 1) {
for (let column = 0; column < WATERMARK_COLUMNS; column += 1) {
const item = document.createElement('span')
item.textContent = text
item.style.cssText = [
'position:absolute',
'display:block',
'width:280px',
'color:rgba(14,110,140,0.13)',
'font-size:14px',
'font-weight:700',
'letter-spacing:2px',
'white-space:nowrap',
`left:${column * 20 - 10}%`,
`top:${row * WATERMARK_ROW_GAP_PX + 30}px`,
'transform:rotate(-28deg)',
'transform-origin:left top',
].join(';')
overlay.appendChild(item)
}
}
root.appendChild(overlay)
}
/**
@@ -112,6 +178,11 @@ export async function createSigningPdf(input: SigningPdfInput): Promise<Blob> {
clonedElement.style.margin = '0'
clonedElement.style.background = '#ffffff'
// 水印在量页眉之前注入:它是绝对定位层,不参与布局,不会影响页眉高度
if (input.watermark) {
injectWatermark(clonedElement, input.watermark)
}
// 文书外壳(.sign-doc)在页面里是定高滚动卡片:overflow: hidden 会把超出一屏的内容裁掉,
// 纸张阴影与圆角也不该出现在 PDF 页面上,这里一并抹平
const paper = clonedElement.closest<HTMLElement>('.sign-doc')