feat(签署文书): 统一签署态文书样式并精简演示种子为单份麻精同意书

This commit is contained in:
yelan
2026-09-24 17:08:12 +08:00
parent 9a6ac93e21
commit 5ea79178cb
30 changed files with 3787 additions and 2878 deletions
+160 -207
View File
@@ -7,14 +7,19 @@
*
* 这里同时持有演示文书库的种子数据,原因有两点:
* ① 文档库页面与签署工作台必须看到同一份模板,不能一边有数据一边空着;
* ② 种子文书都带真实正文(含签名位、签署日期、勾选表),
* ② 种子文书带真实正文(含签名位、签署日期、勾选/选择项),
* 发起签署后能走完整的填写项/签名链路,不会退化成前端写死的回退纸样。
*
* 种子只保留一份:**麻醉药品、精神药品知情同意书**(原型 V10.2 的 docSeedNar)。
* 正文里的占位符用结构化写法(<span data-field>,见 utils/signing-fields),
* 与编辑器插入的占位符、签署端渲染共用同一份属性契约。
*
* 只声明实际用到的字段,不绑定文档库页面的类型,避免 utils 反向依赖 views。
*/
import type { SigningMethod, SigningTemplate } from '@/api/workbench/types'
import type { SigningFieldDef, SigningMethod, SigningTemplate } from '@/api/workbench/types'
import { buildFieldSpanHtml } from './signing-fields'
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from './mock-storage'
/**
@@ -39,6 +44,11 @@ export interface LocalDocument {
versionId?: string
departmentId?: string | null
departmentName?: string
/**
* 除归属科室外还共享给哪些科室(原型 sharedDepts)。
* 后端 CreateTemplateRequest 只接受单个 departmentId,因此这里只在演示文库里有值。
*/
sharedDepartments?: string[]
campusId?: string | null
campusName?: string
/**
@@ -63,162 +73,6 @@ export interface LocalDepartment {
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>',
'<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 = ['全院通用', '放射科', '消化内科', '心内科', '健康管理中心', '医务处']
/**
* 演示院区字典。
*
@@ -239,19 +93,17 @@ export const demoCampuses = [
/**
* 演示科室字典。
* 文档库的科室下拉在演示模式下用它,真实模式下改由 getDepartments(campusId) 提供。
* 科室名与演示文书库的分组名刻意保持一致(放射科、消化内科……),
* 否则会出现"下拉里选不到文书实际所属的科室"。
* 文档库「文档归属」的科室勾选列表在演示模式下用它,真实模式下改由
* getAllDepartments() 提供。科室名与演示文书库的分组名刻意保持一致,
* 否则会出现「勾选列表里选不到文书实际所属的科室」。
*/
export const demoDepartments = [
'医学影像科',
'医学检验科',
'临床遗传中心',
'放射科',
'消化内科',
'心内科',
'健康管理中心',
'麻醉科·精神科',
'药学部',
'医务处',
'医学影像科',
'消化内科',
'健康管理中心',
].map((name, index) => ({
id: `mock-department-${index + 1}`,
campusId: '',
@@ -262,9 +114,10 @@ export const demoDepartments = [
sortNo: index + 1,
}))
function groupKeyOf(department: string) {
return `demo-${DEMO_DEPARTMENT_ORDER.indexOf(department)}`
}
/** 演示文书库里唯一的科室分组 */
const DEMO_DEPARTMENT = '麻醉科·精神科'
/** 演示文书库里的分类(原型 library 里该文书落在「麻精药品」分类下) */
const DEMO_CATEGORY = '麻精药品'
/**
* 演示文书库种子。
@@ -275,50 +128,150 @@ function groupKeyOf(department: string) {
*/
const DEMO_UPDATED_AT = '2026-08-20T09:30:00+08:00'
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: 'published',
backendStatus: 'PUBLISHED',
description: spec.description,
versionId: spec.versionId,
departmentId: null,
departmentName: department,
campusId: null,
campusName: '全院通用',
updatedAt: DEMO_UPDATED_AT,
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)
/** 现场手填的文本框:字段键统一走 custom: 前缀,编辑器据此提示「签署时由患者/家属现场填写」 */
function handFilled(label: string): SigningFieldDef {
return { id: `custom:${label}`, label, kind: 'text', required: false, options: [] }
}
/** 演示文书库种子:文档库页面与签署工作台读同一份,保证两个页面看到的模板一致 */
export const demoDocumentLibrary: LocalDepartment[] = buildDemoLibrary()
/**
* 麻醉药品、精神药品知情同意书(原型 V10.2 的 docSeedNar)。
*
* 正文结构与原型逐条对齐:一、患者基本信息 / 二、代办人信息 / 三、身份核验记录 /
* 四、用药信息 / 五、风险告知与注意事项 / 六、签字确认。
*
* 与原型的一处差别:原型里「代办人信息」等处的填空是 Word 导入时留下的静态下划线
* (`<span style="border-bottom:…">`),在编辑器里会因属性被清洗而丢失。
* 这里改成**文本框占位符**——签署时渲染出来同样是下划线,但内容能被采集,
* 也顺便把「现场手填」这条链路演示完整。
*/
function buildNarcoticConsentHtml(): string {
const field = (def: SigningFieldDef) => buildFieldSpanHtml(def)
const identityVerified: SigningFieldDef = {
id: 'identityVerified',
label: '已核对',
kind: 'checkbox',
required: true,
options: ['患者本人', '代办人'],
multi: true,
}
const takeAway: SigningFieldDef = {
id: 'takeAway',
label: '是否带离机构使用',
kind: 'choice',
required: true,
options: ['否(本机构内使用)', '是(带离机构外使用,按单次门诊处方量管理)'],
multi: false,
}
const risks = [
'麻精药品具有成瘾性(依赖性)风险,须严格按照医嘱使用,不得擅自调整用药剂量或改变用法。',
'不得套开麻精药品,不得将药品转借、转售他人。',
'转售麻精药品涉嫌毒品犯罪,需承担严重法律后果(第二十七条“特殊属性告知”)。',
'使用麻精药品需按医嘱定期复诊(长期使用者至少每 3 个月复诊或电话/视频随诊)。',
'过期、未使用的麻精药品及其废贴、空安瓿应当交回医疗机构,由医疗机构按规定销毁,不得自行处理或丢弃。',
'注射剂再次领取时须交回此前使用的空安瓿(第二十七条、第四十二条)。',
'可能出现的不良反应:恶心、呕吐、便秘、头晕、嗜睡、呼吸抑制等,如有不适及时就医。',
'本药品仅供本患者使用,请妥善保管,防止丢失、被盗或被他人误用。',
]
return [
'<h2>麻醉药品、精神药品知情同意书</h2>',
'<p>(依据国卫医政发〔2026〕21号第二十七条、第六十九条制定)</p>',
'<h3>一、患者基本信息</h3>',
'<table><tbody>',
`<tr><td>患者姓名</td><td>${field({ id: 'patient', label: '患者姓名', kind: 'text', required: true, options: [] })}</td>`,
`<td>性别</td><td>${field({ id: 'sex', label: '性别', kind: 'text', required: false, options: [] })}</td></tr>`,
`<tr><td>年龄</td><td>${field({ id: 'age', label: '年龄', kind: 'text', required: false, options: [] })}</td>`,
`<td>病历号</td><td>${field(handFilled('病历号'))}</td></tr>`,
`<tr><td>身份证号</td><td>${field(handFilled('身份证号'))}</td>`,
`<td>联系电话</td><td>${field({ id: 'phone', label: '联系电话', kind: 'text', required: false, options: [] })}</td></tr>`,
`<tr><td>诊断</td><td>${field(handFilled('诊断'))}</td>`,
`<td>就诊科室</td><td>${field(handFilled('就诊科室'))}</td></tr>`,
'</tbody></table>',
'<h3>二、代办人信息(代办领取时填写)</h3>',
'<table><tbody>',
`<tr><td>代办人姓名</td><td>${field(handFilled('代办人姓名'))}</td>`,
`<td>与患者关系</td><td>${field(handFilled('与患者关系'))}</td></tr>`,
`<tr><td>代办人身份证号</td><td>${field(handFilled('代办人身份证号'))}</td>`,
`<td>联系电话</td><td>${field(handFilled('代办人联系电话'))}</td></tr>`,
'</tbody></table>',
'<h3>三、身份核验记录</h3>',
'<table><tbody>',
`<tr><td>已核对${field(identityVerified)} 身份证明原件:</td>`,
`<td>证件类型及号码:${field(handFilled('证件类型及号码'))}</td></tr>`,
'<tr><td>医师确认:患者/代办人身份信息与登记一致</td>',
`<td>核对医师签名:${field({ id: 'doctor', label: '核对医师签名', kind: 'signature', required: true, options: [] })}</td></tr>`,
'</tbody></table>',
'<h3>四、用药信息</h3>',
'<table><tbody>',
`<tr><td>药品名称</td><td>${field(handFilled('药品名称'))}</td></tr>`,
`<tr><td>剂型/规格</td><td>${field(handFilled('剂型规格'))}</td></tr>`,
`<tr><td>用法用量</td><td>${field(handFilled('用法用量'))}</td></tr>`,
`<tr><td>是否带离机构使用</td><td>${field(takeAway)}</td></tr>`,
`<tr><td>使用期限</td><td>${field(handFilled('使用期限'))}</td></tr>`,
'</tbody></table>',
'<h3>五、风险告知与注意事项</h3>',
`<ol>${risks.map((risk) => `<li>${risk}</li>`).join('')}</ol>`,
'<h3>六、签字确认</h3>',
'<table><tbody>',
`<tr><td>患者(或监护人/代办人)签名:${field({ id: 'signature', label: '患者签名', kind: 'signature', required: true, options: [] })}</td>`,
`<td>签字日期:${field({ id: 'signedDate', label: '签署日期', kind: 'date', required: true, options: [] })}</td></tr>`,
'<tr><td>(未成年人须由监护人书面同意并签名)</td><td></td></tr>',
`<tr><td>开具医师签名:${field({ id: 'prescribingDoctor', label: '开具医师签名', kind: 'signature', required: true, options: [] })}</td>`,
'<td></td></tr>',
'</tbody></table>',
'<p>注:本知情同意书由医疗机构妥善保存。采用电子形式的,内容须与纸质一致、完整且不可篡改(第六十九条)。</p>',
].join('')
}
function buildDemoLibrary(): LocalDepartment[] {
const document: LocalDocument = {
id: 'tpl-pha-nar-001',
name: '麻醉药品、精神药品知情同意书',
code: 'TPL-PHA-NAR-001',
version: 'v1.0.0',
status: 'published',
backendStatus: 'PUBLISHED',
description: '麻精药品成瘾性风险、特殊属性告知、身份核验、代办人信息与用药信息登记。',
versionId: 'tpl-pha-nar-001-v1',
departmentId: null,
departmentName: DEMO_DEPARTMENT,
campusId: null,
campusName: '全院通用',
updatedAt: DEMO_UPDATED_AT,
contentHtml: buildNarcoticConsentHtml(),
}
return [
{
key: 'demo-0',
name: DEMO_DEPARTMENT,
campus: 'all' as const,
categories: [{ name: DEMO_CATEGORY, documents: [document] }],
},
]
}
/**
* 演示文书库种子(惰性构建)。
*
* 正文里的占位符由 buildFieldSpanHtml 产出,而它要读 document,
* 因此在首次取用时才构建:模块被 Node 侧脚本 import 时不会因为缺 DOM 直接抛错。
*/
let demoLibraryCache: LocalDepartment[] | null = null
export function getDemoDocumentLibrary(): LocalDepartment[] {
demoLibraryCache ??= buildDemoLibrary()
return demoLibraryCache
}
/**
* 读取演示文书库:优先用本地保存的(科室改过、建过的),没有则用种子。
* 调用方负责再克隆一份使用,避免直接改到种子数据。
*/
export function readDocumentLibrary(): LocalDepartment[] {
return readMockState<LocalDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? demoDocumentLibrary
return readMockState<LocalDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? getDemoDocumentLibrary()
}
/** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */