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:
@@ -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}`,
|
||||
}))
|
||||
}
|
||||
@@ -188,7 +188,12 @@ const defaultMockRecords: SigningTaskRecord[] = [
|
||||
department: '放射科',
|
||||
status: 'signed',
|
||||
signerName: '王某某家属',
|
||||
// 代签样本:列表上要能看到「家属代签(配偶)」标签,
|
||||
// 否则这条原型口径在演示环境里没有任何样本可验
|
||||
signerType: '家属',
|
||||
signerRelation: '配偶',
|
||||
method: 'sms',
|
||||
smsMobileMasked: '139****3321',
|
||||
source: '工作台发起',
|
||||
expiresAt: '2026-08-29 18:00',
|
||||
updatedAt: '2026-08-27 09:12',
|
||||
@@ -210,7 +215,10 @@ const defaultMockRecords: SigningTaskRecord[] = [
|
||||
department: '消化内科',
|
||||
status: 'expired',
|
||||
signerName: '赵某某家属',
|
||||
signerType: '家属',
|
||||
signerRelation: '子女',
|
||||
method: 'sms',
|
||||
smsMobileMasked: '137****8845',
|
||||
source: '电子病历',
|
||||
expiresAt: '2026-08-27 16:00',
|
||||
updatedAt: '2026-08-27 08:45',
|
||||
@@ -321,6 +329,9 @@ const defaultMockRecords: SigningTaskRecord[] = [
|
||||
status: 'pending',
|
||||
signerName: '王某某',
|
||||
method: 'sms',
|
||||
// 待签署的短信任务:详情里要显示「已发送至 139****3321 · 等待患者签署…」,
|
||||
// 号码取后端返回的脱敏值,没有就不拼号码
|
||||
smsMobileMasked: '139****3321',
|
||||
source: '工作台发起',
|
||||
expiresAt: '2026-08-31 18:00',
|
||||
updatedAt: '2026-08-27 09:50',
|
||||
@@ -712,7 +723,11 @@ function mapSigningTask(
|
||||
department: dto.visitSnapshot.departmentName || '未指定科室',
|
||||
departmentId: dto.departmentId,
|
||||
signerName: dto.patientSnapshot.name,
|
||||
// 签署人类型 / 关系:后端补齐后自动带出,取不到即 undefined,列表不显示代签标签
|
||||
signerType: dto.signerType ?? undefined,
|
||||
signerRelation: dto.signerRelation ?? undefined,
|
||||
method: normalizeSigningMethod(dto.signMethod),
|
||||
smsMobileMasked: dto.smsMobileMasked ?? undefined,
|
||||
status: normalizeTaskStatus(dto.status),
|
||||
source: dto.visitSnapshot.sourceSystem || dto.patientSnapshot.sourceSystem || '—',
|
||||
expiresAt: formatApiDateTime(dto.expiredAt),
|
||||
@@ -1170,6 +1185,8 @@ export async function createSigningTask(input: CreateSigningTaskInput): Promise<
|
||||
documentName: input.documentName,
|
||||
department: input.department,
|
||||
signerName: patient?.name ?? '演示患者',
|
||||
// 原型新建任务固定落「本人」(发起弹窗没有代签人选择,代签是签署现场才知道的事)
|
||||
signerType: '本人',
|
||||
method: input.method,
|
||||
status: 'pending',
|
||||
source: '工作台发起',
|
||||
|
||||
@@ -246,7 +246,26 @@ export interface SigningTaskRecord extends WorkbenchTask {
|
||||
documentId: string
|
||||
departmentId?: string | null
|
||||
signerName: string
|
||||
/**
|
||||
* 签署人类型与「与患者的关系」(原型 V10.2 的 signerType / relation)。
|
||||
*
|
||||
* `'本人'` 表示患者自签;`'监护人'` / `'家属'` / `'代办人'` 等表示代签,
|
||||
* 任务列表要把它标出来——代签与自签在合规上是两件事,
|
||||
* 事后追责时第一个要看的就是「谁签的、凭什么签」。
|
||||
* 关系文案(如「母亲」)单独存,不拼进类型里。
|
||||
*
|
||||
* 当前后端任务接口不返回,取不到就是 undefined,列表不渲染代签标签。
|
||||
*/
|
||||
signerType?: string | null
|
||||
signerRelation?: string | null
|
||||
method: SigningMethod
|
||||
/**
|
||||
* 短信发送到的号码,后端只返回脱敏值(如 `138****5678`)。
|
||||
*
|
||||
* 只用于「已发送至 xxx · 等待患者签署…」这类提示;**没有值时不拼号码**,
|
||||
* 不拿患者档案里的号码冒充发送目标——那会让人以为短信确实发到了那个号。
|
||||
*/
|
||||
smsMobileMasked?: string | null
|
||||
source: string
|
||||
expiresAt: string
|
||||
createdAt?: string
|
||||
@@ -449,6 +468,12 @@ export interface SignTaskResponseDto {
|
||||
templateContentSha256: string
|
||||
templateFileSha256: string
|
||||
smsMobileMasked: string | null
|
||||
/**
|
||||
* 签署人类型与关系(原型 signerType / relation)。
|
||||
* 后端补齐后前端无需改动即可显示;当前取不到时为 undefined。
|
||||
*/
|
||||
signerType?: string | null
|
||||
signerRelation?: string | null
|
||||
signedAt: string | null
|
||||
expiredAt: string | null
|
||||
voidedAt: string | null
|
||||
|
||||
Reference in New Issue
Block a user