diff --git a/clinical-web/src/api/workbench/artifacts.ts b/clinical-web/src/api/workbench/artifacts.ts index 4232fb8..3a8932c 100644 --- a/clinical-web/src/api/workbench/artifacts.ts +++ b/clinical-web/src/api/workbench/artifacts.ts @@ -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 { if (useMockData) { - return [] + return buildMockArtifacts(taskId) } try { @@ -115,6 +178,16 @@ export async function getSigningArtifact( export function downloadSigningArtifact(artifactId: string): Promise { 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' })) } diff --git a/clinical-web/src/api/workbench/signing.ts b/clinical-web/src/api/workbench/signing.ts index 3b3a410..cf09b84 100644 --- a/clinical-web/src/api/workbench/signing.ts +++ b/clinical-web/src/api/workbench/signing.ts @@ -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(MOCK_STORAGE_KEYS.signingTasks) ?? defaultMockRecords +const mockRecords: SigningTaskRecord[] = hydrateTaskDocuments( + readMockState(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( diff --git a/clinical-web/src/composables/useSigningTaskForm.ts b/clinical-web/src/composables/useSigningTaskForm.ts index abd28b0..e470dd0 100644 --- a/clinical-web/src/composables/useSigningTaskForm.ts +++ b/clinical-web/src/composables/useSigningTaskForm.ts @@ -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') { diff --git a/clinical-web/src/utils/mock-document-library.ts b/clinical-web/src/utils/mock-document-library.ts index fc2511e..02fcb20 100644 --- a/clinical-web/src/utils/mock-document-library.ts +++ b/clinical-web/src/utils/mock-document-library.ts @@ -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 + ? `${spec.items + .map((item) => ``) + .join('')}
项目勾选
${item}
` + : '' + + const extraFields = (spec.extraFields ?? []).map((field) => `

${field}:{${field}}

`) + + return [ + `

${spec.name}

`, + ...paragraphs.map((paragraph) => `

${paragraph}

`), + checkboxTable, + ...extraFields, + '

患者(或家属)签名:{患者签名}

', + '

签署日期:{签署日期}

', + ].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() + + 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(MOCK_STORAGE_KEYS.documentLibrary) ?? demoDocumentLibrary +} + +/** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */ +export function writeDocumentLibrary(library: LocalDepartment[]): void { + writeMockState(MOCK_STORAGE_KEYS.documentLibrary, library) +} + /** * 把本地保存的文档库摊平成签署模板。 - * 科室刚建好的文书因此能直接在签署工作台、文档库「发起」里选到。 + * 文档库是模板的唯一来源:签署工作台能选到的模板,一定是在文档库里能维护的那几份。 */ export function readLocalSigningTemplates(): SigningTemplate[] { - const departments = readMockState(MOCK_STORAGE_KEYS.documentLibrary) ?? [] - - return departments.flatMap((department) => + return readDocumentLibrary().flatMap((department) => (department.categories ?? []).flatMap((category) => (category.documents ?? []).map((document) => ({ id: document.id, diff --git a/clinical-web/src/views/management/documents/mock.ts b/clinical-web/src/views/management/documents/mock.ts index a6b2bf1..a2dc5ea 100644 --- a/clinical-web/src/views/management/documents/mock.ts +++ b/clinical-web/src/views/management/documents/mock.ts @@ -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(MOCK_STORAGE_KEYS.documentLibrary) ?? documentLibrary + return readDocumentLibrary() } /** 保存演示文书库,刷新后仍可继续编辑、预览与发起签署 */ export function saveDocumentLibrary(library: DocumentDepartment[]): void { - writeMockState(MOCK_STORAGE_KEYS.documentLibrary, library) + writeDocumentLibrary(library) } /** diff --git a/clinical-web/src/views/workbench/signing/index.vue b/clinical-web/src/views/workbench/signing/index.vue index 29b1051..8cdecc0 100644 --- a/clinical-web/src/views/workbench/signing/index.vue +++ b/clinical-web/src/views/workbench/signing/index.vue @@ -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') }