/** * 本地演示文书库的读取与转换(仅 Mock 模式)。 * * 管理端文档库的数据只存在前端:科室在 Word 编辑器里建好文书后写进 localStorage。 * 文档库页面和签署侧的模板列表都要读这份数据,共享同一份读取逻辑, * 否则会出现「文档库刚建好文书,签署工作台却选不到」。 * * 这里同时持有演示文书库的种子数据,原因有两点: * ① 文档库页面与签署工作台必须看到同一份模板,不能一边有数据一边空着; * ② 种子文书带真实正文(含签名位、签署日期、勾选/选择项), * 发起签署后能走完整的填写项/签名链路,不会退化成前端写死的回退纸样。 * * 种子只保留一份:**麻醉药品、精神药品知情同意书**(原型 V10.2 的 docSeedNar)。 * 正文里的占位符用结构化写法(,见 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 导入时留下的静态下划线 * (``),在编辑器里会因属性被清洗而丢失。 * 这里改成**文本框占位符**——签署时渲染出来同样是下划线,但内容能被采集, * 也顺便把「现场手填」这条链路演示完整。 */ 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 [ '

麻醉药品、精神药品知情同意书

', '

(依据国卫医政发〔2026〕21号第二十七条、第六十九条制定)

', '

一、患者基本信息

', '', ``, ``, ``, ``, ``, ``, ``, ``, '
患者姓名${field({ id: 'patient', label: '患者姓名', kind: 'text', required: true, options: [] })}性别${field({ id: 'sex', label: '性别', kind: 'text', required: false, options: [] })}
年龄${field({ id: 'age', label: '年龄', kind: 'text', required: false, options: [] })}病历号${field(handFilled('病历号'))}
身份证号${field(handFilled('身份证号'))}联系电话${field({ id: 'phone', label: '联系电话', kind: 'text', required: false, options: [] })}
诊断${field(handFilled('诊断'))}就诊科室${field(handFilled('就诊科室'))}
', '

二、代办人信息(代办领取时填写)

', '', ``, ``, ``, ``, '
代办人姓名${field(handFilled('代办人姓名'))}与患者关系${field(handFilled('与患者关系'))}
代办人身份证号${field(handFilled('代办人身份证号'))}联系电话${field(handFilled('代办人联系电话'))}
', '

三、身份核验记录

', '', ``, ``, '', ``, '
已核对${field(identityVerified)} 身份证明原件:证件类型及号码:${field(handFilled('证件类型及号码'))}
医师确认:患者/代办人身份信息与登记一致核对医师签名:${field({ id: 'doctor', label: '核对医师签名', kind: 'signature', required: true, options: [] })}
', '

四、用药信息

', '', ``, ``, ``, ``, ``, '
药品名称${field(handFilled('药品名称'))}
剂型/规格${field(handFilled('剂型规格'))}
用法用量${field(handFilled('用法用量'))}
是否带离机构使用${field(takeAway)}
使用期限${field(handFilled('使用期限'))}
', '

五、风险告知与注意事项

', `
    ${risks.map((risk) => `
  1. ${risk}
  2. `).join('')}
`, '

六、签字确认

', '', ``, ``, '', ``, '', '
患者(或监护人/代办人)签名:${field({ id: 'signature', label: '患者签名', kind: 'signature', required: true, options: [] })}签字日期:${field({ id: 'signedDate', label: '签署日期', kind: 'date', required: true, options: [] })}
(未成年人须由监护人书面同意并签名)
开具医师签名:${field({ id: 'prescribingDoctor', label: '开具医师签名', kind: 'signature', required: true, options: [] })}
', '

注:本知情同意书由医疗机构妥善保存。采用电子形式的,内容须与纸质一致、完整且不可篡改(第六十九条)。

', ].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(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, })), ), ) }