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
@@ -0,0 +1,142 @@
/**
* 关联处方(演示数据)。
*
* 原型 V10.2 里「关联处方」出现在两处,**两处口径本来就不一样,不要合并**:
*
* ① 新增签署任务弹窗 B 区(原型 `rxOfVisit(v)`):一行摘要,按**就诊号**哈希
* 从 `RX_POOL` 里取药,同一就诊号每次打开必须带出同一条;
* ② 签署工作台详情(原型 `taskRxs(t)`):一张 7 列的明细表,按**任务号**派生处方号,
* 三条固定药品(麻精类,与麻精知情同意书配套)。
*
* 后端没有处方接口(处方由 HIS 带出),因此两份都只在演示模式下生成;
* 真实模式一律返回空,**绝不凭空造一条看起来真实的处方**——
* 那会让医务人员以为系统真的从 HIS 拿到了这张处方。
*/
import type { PatientVisit, SigningTaskRecord } from './types'
import { isSigningMockEnabled } from './signing'
/**
* 演示处方池。原型 RX_POOL 逐条对齐。
* 只在弹窗摘要里用;工作台明细表用的是下面的 `TASK_PRESCRIPTION_ROWS`。
*/
const PRESCRIPTION_POOL = [
{ item: '碘佛醇注射液 100ml', usage: '静脉注射 · 检查用' },
{ item: '阿莫西林胶囊 0.25g×24', usage: '口服 每日3次' },
{ item: '布洛芬缓释胶囊 0.3g×20', usage: '口服 每日2次' },
{ item: '奥美拉唑肠溶片 20mg×14', usage: '口服 每日1次 · 晨服' },
{ item: '0.9%氯化钠注射液 250ml', usage: '静脉滴注 · 检查前水化' },
]
/** 弹窗摘要形态的关联处方(原型 rxOfVisit 的 `{no, desc}`) */
export interface LinkedPrescription {
no: string
description: string
}
/** 工作台明细表形态的处方行(原型 rxCard 表格的 7 列) */
export interface TaskPrescription {
/** 处方号 */
no: string
/** 处方类别,如「门诊处方」「麻醉处方」 */
category: string
/** 药品名称 */
drug: string
/** 规格 */
spec: string
/** 数量 */
quantity: string
/** 用法用量 */
usage: string
/** 状态,如「已开立」 */
status: string
}
/** 就诊号 → 稳定哈希(原型 rxOfVisit 的 seed 算法,改了口径两处会对不上) */
function hashVisitNo(visitNo: string): number {
let seed = 0
for (let index = 0; index < visitNo.length; index += 1) {
seed = (seed * 31 + visitNo.charCodeAt(index)) % 9973
}
return seed
}
/**
* 按就诊号确定性生成演示处方摘要(原型 rxOfVisit)。
*
* 用哈希而不是随机:同一就诊号每次打开弹窗必须带出同一条处方,
* 否则「关联处方」会随刷新变来变去,演示时没法解释。
*/
export function buildLinkedPrescription(visit: PatientVisit | null): LinkedPrescription | null {
if (!visit) {
return null
}
const seed = hashVisitNo(visit.visitNo)
const first = PRESCRIPTION_POOL[seed % PRESCRIPTION_POOL.length]
const second = PRESCRIPTION_POOL[(seed + 2) % PRESCRIPTION_POOL.length]
const extra = seed % 3 === 0 ? `;${second.item}(${second.usage})` : ''
return {
no: `Rx${visit.visitNo.replace(/\D/g, '').slice(-8)}`,
description: `${first.item}(${first.usage})${extra}`,
}
}
/**
* 演示处方明细(原型 taskRxs 的三条麻精类药品)。
*
* 处方号 = `Rx` + 任务号里的数字后 6 位,再拼 `-1/-2/-3`;
* 任务号里没有数字时拼出来会是 `Rx-1` 这种残缺编号,
* 这时退回用任务号本身做尾缀,保证每条处方的编号可读且唯一。
*/
const TASK_PRESCRIPTION_ROWS: ReadonlyArray<Omit<TaskPrescription, 'no'>> = [
{
category: '门诊处方',
drug: '盐酸羟考酮缓释片(精一)',
spec: '5mg×10片/盒',
quantity: '1盒',
usage: '口服,每12小时一次,整片吞服,遵医嘱',
status: '已开立',
},
{
category: '门诊处方',
drug: '地佐辛注射液',
spec: '5mg:1ml×5支/盒',
quantity: '2支',
usage: '肌内注射,必要时,单次不超过10mg',
status: '已开立',
},
{
category: '麻醉处方',
drug: '磷酸可待因片(精二)',
spec: '30mg×20片/瓶',
quantity: '1瓶',
usage: '口服,每次1片,每日3次(精神药品不得自开自用)',
status: '已开立',
},
]
/**
* 读取某个签署任务的关联处方。
*
* 真实模式返回空数组:处方由 HIS 带出,前端没有接口就拿不到,
* 详情页据此整卡不渲染(而不是显示一张空表)。
*/
export function readTaskPrescriptions(
task: Pick<SigningTaskRecord, 'id' | 'visitNo'> | null,
): TaskPrescription[] {
if (!task || !isSigningMockEnabled) {
return []
}
const digits = task.id.replace(/\D/g, '').slice(-6)
const suffix = digits || task.id.replace(/\W/g, '').slice(-6) || '0'
return TASK_PRESCRIPTION_ROWS.map((row, index) => ({
...row,
no: `Rx${suffix}-${index + 1}`,
}))
}