feat(signing): 统一演示文书库并支持下载签名原图

This commit is contained in:
yelan
2026-09-16 17:15:10 +08:00
parent 4556515c17
commit 2279f7a624
6 changed files with 381 additions and 106 deletions
+221 -7
View File
@@ -5,18 +5,30 @@
* 文档库页面和签署侧的模板列表都要读这份数据,共享同一份读取逻辑,
* 否则会出现「文档库刚建好文书,签署工作台却选不到」。
*
* 这里只声明实际用到的字段,不绑定文档库页面的类型,避免 api 层反向依赖 views。
* 这里同时持有演示文书库的种子数据,原因有两点:
* ① 文档库页面与签署工作台必须看到同一份模板,不能一边有数据一边空着;
* ② 种子文书都带真实正文(含签名位、签署日期、勾选表),
* 发起签署后能走完整的填写项/签名链路,不会退化成前端写死的回退纸样。
*
* 只声明实际用到的字段,不绑定文档库页面的类型,避免 utils 反向依赖 views。
*/
import type { SigningMethod, SigningTemplate } from '@/api/workbench/types'
import { MOCK_STORAGE_KEYS, readMockState } from './mock-storage'
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from './mock-storage'
/** 与 views/management/documents/types.ts 的 DocumentStatus 保持一致 */
type LocalDocumentStatus = 'enabled' | 'draft' | 'archived' | 'review'
/** 与 views/management/documents/types.ts 的 DocumentCampus 保持一致 */
type LocalCampus = 'all' | '本部院区' | '东院区' | '西院区'
interface LocalDocument {
id: string
name: string
code: string
version: string
description?: string
status: LocalDocumentStatus
description: string
versionId?: string
departmentId?: string | null
campusId?: string | null
@@ -31,17 +43,219 @@ interface LocalCategory {
interface LocalDepartment {
key: string
name: string
campus: LocalCampus
categories: LocalCategory[]
}
interface DemoDocumentSpec {
id: string
versionId: string
name: string
code: string
version: string
/** 所属科室(文档库分组名) */
department: string
category: string
description: string
/** 主体说明句:各类文书的措辞不同,逐份写清楚,不做模板化拼接 */
statement: string
/** 需要患者逐项确认的条目,渲染成表头含「勾选」的表格(签署时转为可勾选) */
items: string[]
/** 补充说明段落 */
paragraphs?: string[]
/** 需要签署现场填写的额外字段,如代理文书里的「与患者关系」 */
extraFields?: string[]
}
/**
* 生成演示文书正文。
* 标题、说明段、勾选表、签名位、签署日期都用真实文书的写法,
* 其中 {患者签名}{签署日期} 是签署端识别签名位与日期的依据。
*/
function buildDemoDocumentHtml(spec: DemoDocumentSpec): string {
const paragraphs = [
spec.statement,
...(spec.paragraphs ?? []),
'本文档采用电子方式签署,患者手写签名图片将合成至本文档签名位,文书原件与签名原图双留存,可随时查验。',
]
const checkboxTable = spec.items.length
? `<table><thead><tr><th>项目</th><th>勾选</th></tr></thead><tbody>${spec.items
.map((item) => `<tr><td>${item}</td><td></td></tr>`)
.join('')}</tbody></table>`
: ''
const extraFields = (spec.extraFields ?? []).map((field) => `<p>${field}:{${field}}</p>`)
return [
`<h1 style="margin:0 0 14px;font-size:18px;letter-spacing:2px;text-align:center">${spec.name}</h1>`,
...paragraphs.map((paragraph) => `<p>${paragraph}</p>`),
checkboxTable,
...extraFields,
'<p>患者(或家属)签名:{患者签名}</p>',
'<p>签署日期:{签署日期}</p>',
].join('')
}
const DEMO_DOCUMENT_SPECS: DemoDocumentSpec[] = [
{
id: 'tpl-001',
versionId: 'tpl-001-v2',
name: '住院患者知情同意书',
code: 'DOC-HOS-001',
version: 'V2.0',
department: '全院通用',
category: '住院告知',
description: '住院期间诊疗事项、风险与患者权利告知。',
statement:
'医务人员已就住院期间的诊疗方案、检查与用药安排、可能出现的医疗风险、替代方案及不良后果向本人(或授权委托人)充分说明,本人已完全理解上述内容。',
items: ['诊疗方案与风险知情', '自费项目知情确认', '个人信息使用授权', '住院期间安全告知'],
},
{
id: 'tpl-002',
versionId: 'tpl-002-v1',
name: '住院须知及告知书',
code: 'DOC-HOS-002',
version: 'V1.0',
department: '全院通用',
category: '住院告知',
description: '住院流程、陪护要求和患者须知确认。',
statement: '现将住院期间的相关制度、注意事项与患者权利义务告知如下,请仔细阅读并确认。',
items: ['探视与陪护制度', '请假离院风险告知', '费用结算方式确认'],
},
{
id: 'tpl-003',
versionId: 'tpl-003-v3',
name: '内镜检查知情同意书',
code: 'DOC-DIG-001',
version: 'V3.0',
department: '消化内科',
category: '检查告知',
description: '内镜检查适应证、风险及替代方案告知。',
statement:
'医务人员已就内镜检查的目的、操作过程、可能的风险、替代方案及不良后果向本人(或授权委托人)充分说明,本人已完全理解上述内容。',
paragraphs: ['本次检查可能采用普通内镜或无痛内镜,若采用无痛内镜需另行完成麻醉评估。'],
items: ['肠道准备工作知情', '麻醉方式选择', '病理活检同意', '息肉切除同意'],
},
{
id: 'tpl-004',
versionId: 'tpl-004-v1',
name: 'MRI 磁共振检查知情同意书',
code: 'DOC-RAD-001',
version: 'V1.0',
department: '放射科',
category: '检查告知',
description: '磁共振检查注意事项、禁忌证及风险告知。',
statement:
'医务人员已就磁共振检查的目的、注意事项、可能的风险、替代方案及不良后果向本人(或授权委托人)充分说明,本人已完全理解上述内容。',
paragraphs: [
'磁共振检查需进入强磁场环境,体外及体内金属植入物可能造成伤害,请务必如实告知。',
'如使用钆对比剂,存在过敏反应及肾功能相关风险,检查前后需配合完成评估。',
],
items: ['体内金属植入物筛查', '幽闭恐惧症告知', '造影剂过敏史确认', '妊娠情况确认'],
},
{
id: 'tpl-005',
versionId: 'tpl-005-v2',
name: '心脏介入手术知情同意书',
code: 'DOC-CARD-001',
version: 'V2.0',
department: '心内科',
category: '手术告知',
description: '介入治疗方案、围术期风险及替代方案告知。',
statement:
'医务人员已就介入手术的方案、操作过程、围术期风险、替代方案及可能的不良后果向本人(或授权委托人)充分说明,本人已完全理解上述内容。',
paragraphs: ['介入操作过程中可能根据实际情况调整术式,需家属在院等候。'],
items: ['冠状动脉造影同意', '支架植入同意', '术后抗凝方案知情', '穿刺部位并发症告知'],
},
{
id: 'tpl-006',
versionId: 'tpl-006-v1',
name: '体检报告发放及知情同意书',
code: 'DOC-PE-001',
version: 'V1.0',
department: '健康管理中心',
category: '体检告知',
description: '体检报告领取方式、隐私保护和异常结果随访告知。',
statement: '现将体检报告发放方式、个人隐私保护与异常结果随访安排告知如下,请确认。',
items: ['报告领取方式确认', '异常结果随访告知', '个人隐私保护知情'],
},
{
id: 'tpl-007',
versionId: 'tpl-007-v1',
name: '患者授权委托书',
code: 'DOC-GEN-003',
version: 'V1.0',
department: '医务处',
category: '授权文书',
description: '患者授权家属或代理人办理相关医疗事项。',
statement: '本人因病情或客观原因无法亲自办理下列事项,特授权受托人代为办理。',
paragraphs: ['受托人在授权范围内签署的医疗文书,与本人签署具有同等效力。'],
items: ['代为签署医疗文书', '代为了解病情与诊疗方案', '代为领取检查报告'],
extraFields: ['与患者关系', '受托人姓名'],
},
]
/** 数组顺序即文档库里的科室分组顺序,全院通用排在最前 */
const DEMO_DEPARTMENT_ORDER = ['全院通用', '放射科', '消化内科', '心内科', '健康管理中心', '医务处']
function groupKeyOf(department: string) {
return `demo-${DEMO_DEPARTMENT_ORDER.indexOf(department)}`
}
function buildDemoLibrary(): LocalDepartment[] {
return DEMO_DEPARTMENT_ORDER.map((department) => {
const specs = DEMO_DOCUMENT_SPECS.filter((spec) => spec.department === department)
const categories = new Map<string, LocalDocument[]>()
specs.forEach((spec) => {
const documents = categories.get(spec.category) ?? []
documents.push({
id: spec.id,
name: spec.name,
code: spec.code,
version: spec.version,
status: 'enabled',
description: spec.description,
versionId: spec.versionId,
departmentId: null,
campusId: null,
contentHtml: buildDemoDocumentHtml(spec),
})
categories.set(spec.category, documents)
})
return {
key: groupKeyOf(department),
name: department,
campus: 'all' as const,
categories: [...categories.entries()].map(([name, documents]) => ({ name, documents })),
}
}).filter((department) => department.categories.length > 0)
}
/** 演示文书库种子:文档库页面与签署工作台读同一份,保证两个页面看到的模板一致 */
export const demoDocumentLibrary: LocalDepartment[] = buildDemoLibrary()
/**
* 读取演示文书库:优先用本地保存的(科室改过、建过的),没有则用种子。
* 调用方负责再克隆一份使用,避免直接改到种子数据。
*/
export function readDocumentLibrary(): LocalDepartment[] {
return readMockState<LocalDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? demoDocumentLibrary
}
/** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */
export function writeDocumentLibrary(library: LocalDepartment[]): void {
writeMockState(MOCK_STORAGE_KEYS.documentLibrary, library)
}
/**
* 把本地保存的文档库摊平成签署模板。
* 科室刚建好的文书因此能直接在签署工作台、文档库「发起」里选到。
* 文档库是模板的唯一来源:签署工作台能选到的模板,一定是在文档库里能维护的那几份。
*/
export function readLocalSigningTemplates(): SigningTemplate[] {
const departments = readMockState<LocalDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? []
return departments.flatMap((department) =>
return readDocumentLibrary().flatMap((department) =>
(department.categories ?? []).flatMap((category) =>
(category.documents ?? []).map((document) => ({
id: document.id,