feat(signing): 统一演示文书库并支持下载签名原图
This commit is contained in:
@@ -4,6 +4,7 @@ import { unwrapApiResponse } from '@/utils/api-response'
|
||||
import { mockFlags } from '@/utils/mock-flags'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
import { getMockSigningTask } from './signing'
|
||||
import type {
|
||||
ApiPageResponse,
|
||||
ApiResponseOf,
|
||||
@@ -70,9 +71,71 @@ function normalizeArtifact(dto: SignArtifactResponseDto): SigningArtifact {
|
||||
|
||||
export const isArtifactMockEnabled = useMockData
|
||||
|
||||
/**
|
||||
* 演示签名原图产物的 id 前缀。
|
||||
* 演示环境没有文件服务,下载时靠 id 反查任务里的签名图,
|
||||
* 保证「签名原图」在演示里也是真能下载到的文件,而不是占位提示。
|
||||
*/
|
||||
const MOCK_SIGNATURE_PREFIX = 'mock-signature-'
|
||||
|
||||
function estimateDataUrlBytes(dataUrl: string) {
|
||||
const payload = dataUrl.split(',')[1]
|
||||
|
||||
if (!payload) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// base64 每 4 个字符还原 3 字节,末尾可能有填充,这里取近似值用于展示
|
||||
return Math.floor((payload.replace(/=+$/, '').length * 3) / 4)
|
||||
}
|
||||
|
||||
/** 演示产物:已签署且有签名图时,给出可下载的签名原图 */
|
||||
function buildMockArtifacts(taskId: string): SigningArtifact[] {
|
||||
const task = getMockSigningTask(taskId)
|
||||
|
||||
if (!task || task.status !== 'signed' || !task.signatureDataUrl) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${MOCK_SIGNATURE_PREFIX}${task.id}`,
|
||||
taskId: task.id,
|
||||
pipelineId: `mock-pipeline-${task.id}`,
|
||||
artifactType: 'SIGNATURE_IMAGE',
|
||||
label: artifactLabels.SIGNATURE_IMAGE ?? '签名原图',
|
||||
fileName: '签名原图.png',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: estimateDataUrlBytes(task.signatureDataUrl),
|
||||
sha256: '',
|
||||
metadata: {},
|
||||
createdAt: formatDateTime(task.signedAt ?? task.updatedAt),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/** 把签名图 dataURL 还原成可下载的二进制 Blob */
|
||||
function dataUrlToBlob(dataUrl: string): Blob {
|
||||
const [meta, payload] = dataUrl.split(',')
|
||||
const mimeType = /data:([^;]+)/.exec(meta ?? '')?.[1] ?? 'application/octet-stream'
|
||||
|
||||
if (!payload) {
|
||||
return new Blob([], { type: mimeType })
|
||||
}
|
||||
|
||||
const binary = window.atob(payload)
|
||||
const bytes = new Uint8Array(binary.length)
|
||||
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index)
|
||||
}
|
||||
|
||||
return new Blob([bytes], { type: mimeType })
|
||||
}
|
||||
|
||||
export async function getSigningArtifacts(taskId: string): Promise<SigningArtifact[]> {
|
||||
if (useMockData) {
|
||||
return []
|
||||
return buildMockArtifacts(taskId)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -115,6 +178,16 @@ export async function getSigningArtifact(
|
||||
|
||||
export function downloadSigningArtifact(artifactId: string): Promise<Blob> {
|
||||
if (useMockData) {
|
||||
// 演示环境没有文件服务,按 id 反查任务里的签名图,返回真实的 PNG
|
||||
if (artifactId.startsWith(MOCK_SIGNATURE_PREFIX)) {
|
||||
const taskId = artifactId.slice(MOCK_SIGNATURE_PREFIX.length)
|
||||
const signatureDataUrl = getMockSigningTask(taskId)?.signatureDataUrl
|
||||
|
||||
if (signatureDataUrl) {
|
||||
return Promise.resolve(dataUrlToBlob(signatureDataUrl))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(new Blob(['Mock 签署文件,仅用于界面演示。'], { type: 'text/plain' }))
|
||||
}
|
||||
|
||||
|
||||
@@ -139,86 +139,6 @@ const mockPatients: PatientProfile[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const mockTemplates: SigningTemplate[] = [
|
||||
{
|
||||
id: 'tpl-001',
|
||||
versionId: 'tpl-001-v2',
|
||||
name: '住院患者知情同意书',
|
||||
code: 'DOC-HOS-001',
|
||||
version: 'V2.0',
|
||||
department: '全院通用',
|
||||
category: '住院告知',
|
||||
description: '住院期间诊疗事项、风险与患者权利告知。',
|
||||
supportedMethods: ['pad', 'sms'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-002',
|
||||
versionId: 'tpl-002-v1',
|
||||
name: '住院须知及告知书',
|
||||
code: 'DOC-HOS-002',
|
||||
version: 'V1.0',
|
||||
department: '全院通用',
|
||||
category: '住院告知',
|
||||
description: '住院流程、陪护要求和患者须知确认。',
|
||||
supportedMethods: ['pad', 'sms'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-003',
|
||||
versionId: 'tpl-003-v3',
|
||||
name: '内镜检查知情同意书',
|
||||
code: 'DOC-DIG-001',
|
||||
version: 'V3.0',
|
||||
department: '消化内科',
|
||||
category: '检查告知',
|
||||
description: '内镜检查适应证、风险及替代方案告知。',
|
||||
supportedMethods: ['pad', 'sms'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-004',
|
||||
versionId: 'tpl-004-v1',
|
||||
name: 'MRI 磁共振检查知情同意书',
|
||||
code: 'DOC-RAD-001',
|
||||
version: 'V1.0',
|
||||
department: '放射科',
|
||||
category: '检查告知',
|
||||
description: '磁共振检查注意事项、禁忌证及风险告知。',
|
||||
supportedMethods: ['pad', 'sms'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-005',
|
||||
versionId: 'tpl-005-v2',
|
||||
name: '心脏介入手术知情同意书',
|
||||
code: 'DOC-CARD-001',
|
||||
version: 'V2.0',
|
||||
department: '心内科',
|
||||
category: '手术告知',
|
||||
description: '介入治疗方案、围术期风险及替代方案告知。',
|
||||
supportedMethods: ['pad'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-006',
|
||||
versionId: 'tpl-006-v1',
|
||||
name: '体检报告发放及知情同意书',
|
||||
code: 'DOC-PE-001',
|
||||
version: 'V1.0',
|
||||
department: '健康管理中心',
|
||||
category: '体检告知',
|
||||
description: '体检报告领取方式、隐私保护和异常结果随访告知。',
|
||||
supportedMethods: ['pad', 'sms'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-007',
|
||||
versionId: 'tpl-007-v1',
|
||||
name: '患者授权委托书',
|
||||
code: 'DOC-GEN-003',
|
||||
version: 'V1.0',
|
||||
department: '医务处',
|
||||
category: '授权文书',
|
||||
description: '患者授权家属或代理人办理相关医疗事项。',
|
||||
supportedMethods: ['pad', 'sms'],
|
||||
},
|
||||
]
|
||||
|
||||
const defaultMockRecords: SigningTaskRecord[] = [
|
||||
{
|
||||
id: 'task-001',
|
||||
@@ -329,12 +249,43 @@ const defaultMockRecords: SigningTaskRecord[] = [
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 给演示任务补上文书正文快照。
|
||||
*
|
||||
* 预置任务(以及历史上从写死模板创建的任务)只记了 documentId,没有正文快照;
|
||||
* 缺正文时详情会退化成前端写死的回退纸样——内容与模板无关、没有签名位、
|
||||
* 也没有固定页眉。这里按 documentId 从文书库取正文补上;
|
||||
* 实在取不到的保持原样,由详情页走回退分支,不伪造正文。
|
||||
*/
|
||||
function hydrateTaskDocuments(records: SigningTaskRecord[]): SigningTaskRecord[] {
|
||||
const templates = new Map(readLocalSigningTemplates().map((template) => [template.id, template]))
|
||||
|
||||
return records.map((record) => {
|
||||
if (record.documentContentHtml) {
|
||||
return record
|
||||
}
|
||||
|
||||
const template = templates.get(record.documentId)
|
||||
|
||||
if (!template?.contentHtml) {
|
||||
return record
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
templateVersionId: record.templateVersionId || template.versionId,
|
||||
documentContentHtml: template.contentHtml,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示任务记录:优先用本地保存的,刷新后仍保留(任务固化的文书 HTML、签名图都在里面)。
|
||||
* 所有写操作都要调用 persistMockRecords 落盘,否则刷新会回到切换前的状态。
|
||||
*/
|
||||
const mockRecords: SigningTaskRecord[] =
|
||||
readMockState<SigningTaskRecord[]>(MOCK_STORAGE_KEYS.signingTasks) ?? defaultMockRecords
|
||||
const mockRecords: SigningTaskRecord[] = hydrateTaskDocuments(
|
||||
readMockState<SigningTaskRecord[]>(MOCK_STORAGE_KEYS.signingTasks) ?? defaultMockRecords,
|
||||
)
|
||||
|
||||
function persistMockRecords() {
|
||||
writeMockState(MOCK_STORAGE_KEYS.signingTasks, mockRecords)
|
||||
@@ -392,6 +343,21 @@ function addHours(value: string, hours: number) {
|
||||
|
||||
export const isSigningMockEnabled = useMockData
|
||||
|
||||
/**
|
||||
* 仅 Mock 模式可用:按 id 读取演示任务记录。
|
||||
* 供签署产物模块(api/workbench/artifacts)复用同一份内存数据——
|
||||
* 它没有正文快照的本地持久化,直接读 localStorage 会漏掉补过正文的预置任务。
|
||||
* 真实模式返回 null。
|
||||
*/
|
||||
export function getMockSigningTask(taskId: string): SigningTaskRecord | null {
|
||||
if (!useMockData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const task = mockRecords.find((record) => record.id === taskId)
|
||||
return task ? cloneTask(task) : null
|
||||
}
|
||||
|
||||
/** 演示患者清单(仅 Mock 模式):给签署弹窗做“点一下就能定位”的输入提示 */
|
||||
export function getMockPatientHints(): { id: string; name: string }[] {
|
||||
return useMockData ? mockPatients.map((item) => ({ id: item.id, name: item.name })) : []
|
||||
@@ -904,12 +870,10 @@ export async function getSigningTemplates(
|
||||
return records.map(mapAvailableTemplate)
|
||||
}
|
||||
|
||||
// 文档库本地新建的文书只存在 localStorage,必须一起返回;
|
||||
// 否则文档库刚建好的文书在签署侧选不到(内置 Mock 模板是写死的演示数据)。
|
||||
return Promise.resolve([
|
||||
...readLocalSigningTemplates(),
|
||||
...mockTemplates.map((template) => ({ ...template })),
|
||||
])
|
||||
// 文档库是模板的唯一来源,两侧读同一份数据:
|
||||
// 否则签署端会出现文档库里看不到、也无法预览/编辑/删除的模板,
|
||||
// 而那类模板没有正文,签出来只是前端写死的回退纸样。
|
||||
return Promise.resolve(readLocalSigningTemplates())
|
||||
}
|
||||
|
||||
export async function prepareSigningTask(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { sendSigningSms } from '@/api/workbench/deliveries'
|
||||
import { collectFieldDefs } from '@/utils/signing-fields'
|
||||
import {
|
||||
createSigningTask,
|
||||
getPatientProfile,
|
||||
@@ -196,9 +197,26 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
||||
errors.visit = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 文书必须带签名位才能发起签署:否则签名会随证据固化,但文书上没有落位的地方,
|
||||
* 签出来的文书看不出患者签过。
|
||||
*
|
||||
* 真实模式下后端按 templateVersionId 取版本快照、前端拿不到正文,
|
||||
* 此时不做判断(返回 true),交由后端校验。
|
||||
*/
|
||||
function hasSignatureSlot(template: SigningTemplate) {
|
||||
if (!template.contentHtml) {
|
||||
return true
|
||||
}
|
||||
|
||||
return collectFieldDefs(template.contentHtml).some((field) => field.id === 'signature')
|
||||
}
|
||||
|
||||
const signatureMissingMessage = '该文书没有签名占位符,请先在文档库中补上签名位再发起'
|
||||
|
||||
function selectTemplate(template: SigningTemplate) {
|
||||
selectedTemplateId.value = template.id
|
||||
errors.document = ''
|
||||
errors.document = hasSignatureSlot(template) ? '' : signatureMissingMessage
|
||||
|
||||
if (!template.supportedMethods.includes(method.value)) {
|
||||
method.value = template.supportedMethods[0] ?? 'pad'
|
||||
@@ -238,6 +256,9 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
||||
if (!selectedTemplate.value) {
|
||||
errors.document = '请至少选择一份文档'
|
||||
valid = false
|
||||
} else if (!hasSignatureSlot(selectedTemplate.value)) {
|
||||
errors.document = signatureMissingMessage
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (!isSigningMockEnabled && method.value === 'sms') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from '@/utils/mock-storage'
|
||||
import { readDocumentLibrary, writeDocumentLibrary } from '@/utils/mock-document-library'
|
||||
import type { SigningDocumentData } from '@/utils/signing-document'
|
||||
|
||||
import type { DocumentDepartment, DocumentStatus } from './types'
|
||||
@@ -17,17 +17,17 @@ export const documentStatusOptions = Object.entries(documentStatusLabels).map(([
|
||||
|
||||
export { documentStatusLabels }
|
||||
|
||||
// Word 自维护方案:文书库初始为空,所有文书由科室通过 Word 编辑器(粘贴 / 导入 .docx)创建维护。
|
||||
export const documentLibrary: DocumentDepartment[] = []
|
||||
|
||||
/** 读取演示文书库:优先用本地保存的数据,没有则用初始空库(调用方负责再克隆一份使用) */
|
||||
// 演示文书库的种子与读取逻辑都在 utils/mock-document-library:
|
||||
// 文档库页面和签署工作台必须读同一份数据,否则会出现「文档库里维护的模板,签署端选不到」,
|
||||
// 或者签署端混着一批文档库里看不到、也无法维护的模板。
|
||||
/** 读取演示文书库:优先用本地保存的,没有则用种子(调用方负责再克隆一份使用) */
|
||||
export function loadDocumentLibrary(): DocumentDepartment[] {
|
||||
return readMockState<DocumentDepartment[]>(MOCK_STORAGE_KEYS.documentLibrary) ?? documentLibrary
|
||||
return readDocumentLibrary()
|
||||
}
|
||||
|
||||
/** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */
|
||||
export function saveDocumentLibrary(library: DocumentDepartment[]): void {
|
||||
writeMockState(MOCK_STORAGE_KEYS.documentLibrary, library)
|
||||
writeDocumentLibrary(library)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -261,8 +261,9 @@ async function loadTaskDetail(id: string | null) {
|
||||
throw detailResult.reason
|
||||
}
|
||||
|
||||
selectedTask.value = detailResult.value
|
||||
signingAnswers.value = { ...(detailResult.value.fieldAnswers ?? {}) }
|
||||
const detail = detailResult.value
|
||||
selectedTask.value = detail
|
||||
signingAnswers.value = { ...(detail?.fieldAnswers ?? {}) }
|
||||
|
||||
if (eventsResult.status === 'fulfilled') {
|
||||
taskEvents.value = eventsResult.value
|
||||
@@ -465,6 +466,8 @@ async function handleTaskAction(action: SigningTaskAction) {
|
||||
|
||||
if (artifact) {
|
||||
await downloadArtifact(artifact)
|
||||
} else if (isSigningMockEnabled) {
|
||||
ElMessage.info('原型环境不生成 PDF,可在下方「签署文件」中下载签名原图')
|
||||
} else {
|
||||
ElMessage.info('当前任务暂无签署后 PDF')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user