322 lines
14 KiB
TypeScript
322 lines
14 KiB
TypeScript
/**
|
||
* 本地演示文书库的读取与转换(仅 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,
|
||
})),
|
||
),
|
||
)
|
||
}
|