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
@@ -2,6 +2,10 @@ import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { ElMessage } from 'element-plus'
import { sendSigningSms } from '@/api/workbench/deliveries'
import {
buildLinkedPrescription,
type LinkedPrescription,
} from '@/api/workbench/prescriptions'
import { collectFieldDefs } from '@/utils/signing-fields'
import {
createSigningTask,
@@ -40,50 +44,14 @@ export interface DocumentListRow {
breadcrumb: string
}
/** 关联处方(原型 V6.4:按就诊号确定性生成,仅演示数据) */
export interface LinkedPrescription {
no: string
description: string
}
/**
* 演示处方池。原型 RX_POOL 逐条对齐。
* 真实模式没有处方接口,因此只在 Mock 模式下生成,绝不凭空造一条看起来真实的处方。
*/
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: '静脉滴注 · 检查前水化' },
]
/**
* 按就诊号确定性生成演示处方。
* 关联处方摘要(原型 V6.4 的 rxOfVisit)。
*
* 用哈希而不是随机:同一就诊号每次打开弹窗必须带出同一条处方,
* 否则「关联处方」会随刷新变来变去,演示时没法解释。
* 演示数据与生成口径已挪到 api/workbench/prescriptions,那里同时供
* 签署工作台详情的「关联处方」明细表使用——两处口径不同但同一份药品池,
* 各写一份必然漂移。这里只做转出,保持既有导入路径不变。
*/
function buildPrescription(visit: PatientVisit | null): LinkedPrescription | null {
if (!visit) {
return null
}
let seed = 0
for (let index = 0; index < visit.visitNo.length; index += 1) {
seed = (seed * 31 + visit.visitNo.charCodeAt(index)) % 9973
}
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}`,
}
}
export type { LinkedPrescription }
export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
const patientId = ref('')
@@ -128,7 +96,7 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
/** 关联处方:随选中就诊带出(仅演示数据) */
const prescription = computed<LinkedPrescription | null>(() =>
isSigningMockEnabled ? buildPrescription(selectedVisit.value) : null,
isSigningMockEnabled ? buildLinkedPrescription(selectedVisit.value) : null,
)
/**