Files
xh-medical-sign-web/clinical-web/src/utils/mock-document-library.ts
T

322 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 本地演示文书库的读取与转换(仅 Mock 模式)。
*
* 管理端文档库的数据只存在前端:科室在 Word 编辑器里建好文书后写进 localStorage。
* 文档库页面和签署侧的模板列表都要读这份数据,共享同一份读取逻辑,
* 否则会出现「文档库刚建好文书,签署工作台却选不到」。
*
* 这里同时持有演示文书库的种子数据,原因有两点:
* ① 文档库页面与签署工作台必须看到同一份模板,不能一边有数据一边空着;
* ② 种子文书带真实正文(含签名位、签署日期、勾选/选择项),
* 发起签署后能走完整的填写项/签名链路,不会退化成前端写死的回退纸样。
*
* 种子只保留一份:**麻醉药品、精神药品知情同意书**(原型 V10.2 的 docSeedNar)。
* 正文里的占位符用结构化写法(<span data-field>,见 utils/signing-fields),
* 与编辑器插入的占位符、签署端渲染共用同一份属性契约。
*
* 只声明实际用到的字段,不绑定文档库页面的类型,避免 utils 反向依赖 views。
*/
import type { SigningFieldDef, SigningTemplate } from '@/api/workbench/types'
import { ALL_SIGNING_METHODS } from './signing-method'
import { buildFieldSpanHtml } from './signing-fields'
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from './mock-storage'
/**
* 演示文书的状态。
* 取值与 api/management/types.ts 的 DocumentStatus 一致(对应后端 DRAFT / PENDING_REVIEW /
* APPROVED / PUBLISHED / DISABLED / ARCHIVED 的归一结果),不再使用后端没有的 'enabled'。
*/
type LocalDocumentStatus = 'draft' | 'review' | 'published' | 'archived'
/** 与 views/management/documents/types.ts 的 DocumentCampus 保持一致 */
type LocalCampus = string
export interface LocalDocument {
id: string
name: string
/** 文书编码。与模板接口的 DocumentRecord.code 一样允许缺省,页面统一兜底显示 */
code?: string
version: string
status: LocalDocumentStatus
backendStatus?: string
/** 文书说明。来源同 code,允许缺省 */
description?: string
versionId?: string
departmentId?: string | null
departmentName?: string
/**
* 除归属科室外还共享给哪些科室(原型 sharedDepts)。
* 后端 CreateTemplateRequest 只接受单个 departmentId,因此这里只在演示文库里有值。
*/
sharedDepartments?: string[]
campusId?: string | null
campusName?: string
/**
* 演示数据的更新时间。
* 演示文库的种子不带时间,页面在这一层会显示成"—"而不是伪造一个时间戳;
* 页面模型要求这个字段必填,所以在演示数据创建时写一次真实时间。
*/
updatedAt?: string
contentHtml?: string
contentSha256?: string
}
export interface LocalCategory {
name: string
documents: LocalDocument[]
}
export interface LocalDepartment {
key: string
name: string
campus: LocalCampus
categories: LocalCategory[]
}
/**
* 演示院区字典。
*
* 文档库的「所属院区」下拉在真实模式下取 api/management/organization 的 getCampuses,
* 演示模式下没有接口可调,这里给一份同构的演示院区,键名与 CampusRecord 一致。
* 演示 id 带 mock- 前缀,一眼能看出不是真实 UUID,不会误当成可提交给后端的取值。
*/
export const demoCampuses = [
{
id: 'mock-campus-main',
code: 'MAIN',
name: '本部院区',
institutionName: '新华院区',
status: 'ENABLED' as const,
sortNo: 1,
},
{
id: 'mock-campus-east',
code: 'EAST',
name: '东院区',
institutionName: '新华院区',
status: 'ENABLED' as const,
sortNo: 2,
},
]
/**
* 演示科室字典。
* 文档库「文档归属」的科室勾选列表在演示模式下用它,真实模式下改由
* getAllDepartments() 提供。科室名与演示文书库的分组名刻意保持一致,
* 否则会出现「勾选列表里选不到文书实际所属的科室」。
*/
export const demoDepartments = [
'麻醉科·精神科',
'药学部',
'医务处',
'医学影像科',
'消化内科',
'健康管理中心',
].map((name, index) => ({
id: `mock-department-${index + 1}`,
campusId: '',
code: `MOCK-DEPT-${index + 1}`,
name,
parentId: null,
status: 'ENABLED' as const,
sortNo: index + 1,
}))
/** 演示文书库里唯一的科室分组 */
const DEMO_DEPARTMENT = '麻醉科·精神科'
/** 演示文书库里的分类(原型 library 里该文书落在「麻精药品」分类下) */
const DEMO_CATEGORY = '麻精药品'
/**
* 演示文书库种子。
*
* updatedAt 用固定的历史时间而不是"当前时间":
* 每次构建都取 now 的话,同一份种子文书在每次刷新后更新时间都会变,
* 看起来像有人在不停改动文书,反而失真。
*/
const DEMO_UPDATED_AT = '2026-08-20T09:30:00+08:00'
/** 现场手填的文本框:字段键统一走 custom: 前缀,编辑器据此提示「签署时由患者/家属现场填写」 */
function handFilled(label: string): SigningFieldDef {
return { id: `custom:${label}`, label, kind: 'text', required: false, options: [] }
}
/**
* 麻醉药品、精神药品知情同意书(原型 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) ?? getDemoDocumentLibrary()
}
/** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */
export function writeDocumentLibrary(library: LocalDepartment[]): void {
writeMockState(MOCK_STORAGE_KEYS.documentLibrary, library)
}
/**
* 把本地保存的文档库摊平成签署模板。
* 文档库是模板的唯一来源:签署工作台能选到的模板,一定是在文档库里能维护的那几份。
*
* contentSha256 一并带出:真实模式下可用模板接口只给哈希不给正文,
* 签署端要靠这个哈希去换回正文快照;演示模式虽然不缺正文,但两边字段保持一致,
* 页面就不用按模式分叉。
*/
export function readLocalSigningTemplates(): SigningTemplate[] {
return readDocumentLibrary().flatMap((department) =>
(department.categories ?? []).flatMap((category) =>
(category.documents ?? []).map((document) => ({
id: document.id,
versionId: document.versionId ?? document.id,
name: document.name,
// SigningTemplate.code 必填而演示文书可能没有编码,这里如实兜底为空串,
// 不编一个看起来像真实编码的假值
code: document.code ?? '',
version: document.version,
department: department.name,
departmentId: document.departmentId ?? null,
campusId: document.campusId ?? undefined,
category: category.name,
description: document.description ?? '',
supportedMethods: [...ALL_SIGNING_METHODS],
contentHtml: document.contentHtml,
contentSha256: document.contentSha256,
})),
),
)
}