feat(文书管理): 增加版本工作流与版本历史

- 模板状态按后端原始状态展示与筛选,待审核与已驳回分开展示
- 仅已发布模板可发起签署,新增提交审核/审核通过/驳回等动作
- 新增版本历史弹窗,复用统一的模板时间格式化方法
- 演示数据补齐已签署任务的签名图,并提供获取签名 dataURL 的接口
- 修复编辑器插入占位符时未定位光标导致内容落到文首的问题
This commit is contained in:
yelan
2026-09-17 16:44:54 +08:00
parent 81922d774f
commit 97c5182ba4
13 changed files with 982 additions and 64 deletions
+9 -2
View File
@@ -44,7 +44,14 @@ const mockRecords: DocumentRecord[] = [
},
]
function formatDateTime(value: string | null | undefined) {
/**
* 后端时间戳 → 页面展示用的 `YYYY-MM-DD HH:mm`。
*
* 导出给版本历史弹窗复用:版本列表与模板列表是同一批时间字段,
* 各写一份格式化早晚会分叉出两种显示口径。解析不出来时原样返回去壳的字符串,
* 不返回 Invalid Date。
*/
export function formatTemplateDateTime(value: string | null | undefined) {
if (!value) {
return ''
}
@@ -94,7 +101,7 @@ function mapDocument(dto: TemplateResponseDto): DocumentRecord {
status: normalizeDocumentStatus(dto.status),
backendStatus: dto.status ?? undefined,
updatedBy: dto.updatedBy ?? dto.createdBy ?? '—',
updatedAt: formatDateTime(dto.updatedAt ?? dto.createdAt),
updatedAt: formatTemplateDateTime(dto.updatedAt ?? dto.createdAt),
versionId,
departmentId: dto.departmentId,
departmentName: dto.departmentId ? undefined : '全院通用',
@@ -195,3 +195,38 @@ export function downloadSigningArtifact(artifactId: string): Promise<Blob> {
responseType: 'blob',
})
}
/** Blob → dataURL */
function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
resolve(typeof reader.result === 'string' ? reader.result : '')
}
reader.onerror = () => {
reject(new Error('签名图读取失败'))
}
reader.readAsDataURL(blob)
})
}
/**
* 取回签名图并转成 dataURL,供文书渲染使用。
*
* 签名在后端是独立的 SIGNATURE_IMAGE 产物,任务接口(SignTaskResponseDto)
* **不返回任何签名字段**,所以前端必须自己把它取回来——否则文书里的签名位
* 永远是「待患者签名」,导出的 PDF 也就没有患者签名。
*
* 这里刻意转成 dataURL 而不是直接把下载地址塞给 <img>:文书导出 PDF 走
* html2canvas 截图,跨域的 <img src="http://..."> 会被画布安全策略拦掉
* (`useCORS` 只在服务端返回 CORS 响应头时才生效),表现为
* 「页面上看得到签名、导出的 PDF 里却是空的」。
*/
export async function fetchSignatureDataUrl(artifact: SigningArtifact): Promise<string> {
try {
return await blobToDataUrl(await downloadSigningArtifact(artifact.id))
} catch {
// 取签名图失败不该让整个详情页失败:正文、审计轨迹、产物列表都还能看
return ''
}
}
+21 -11
View File
@@ -5,6 +5,7 @@ import {
readCachedTemplateContent,
} from '@/api/management/documents'
import { unwrapApiResponse } from '@/utils/api-response'
import { DEMO_SIGNATURE_DATA_URL } from '@/utils/demo-signature'
import { mockFlags } from '@/utils/mock-flags'
import { readLocalSigningTemplates } from '@/utils/mock-document-library'
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from '@/utils/mock-storage'
@@ -254,30 +255,39 @@ const defaultMockRecords: SigningTaskRecord[] = [
]
/**
* 给演示任务补上文书正文快照。
* 给演示任务补齐渲染所需的数据:文书正文快照 + 已签署任务的签名图。
*
* 预置任务(以及历史上从写死模板创建的任务)只记了 documentId,没有正文快照;
* 缺正文时详情会退化成前端写死的回退纸样——内容与模板无关、没有签名位、
* 也没有固定页眉。这里按 documentId 从文书库取正文补上;
* 实在取不到的保持原样,由详情页走回退分支,不伪造正文。
* ① 正文:预置任务(以及历史上从写死模板创建的任务)只记了 documentId,没有正文快照;
* 缺正文时详情会退化成前端写死的回退纸样——内容与模板无关、没有签名位、
* 也没有固定页眉。这里按 documentId 从文书库取正文补上;
* 实在取不到的保持原样,由详情页走回退分支,不伪造正文。
* ② 签名图:种子任务里标了「已签署」却不带签名,会出现「列表说已签署、打开没有签名、
* 产物列表也是空的」这种自相矛盾的状态,下载的 PDF 也缺签名。
* 这里给缺签名的已签署任务补一张示意签名;真实签署过的任务已有签名图,不会被覆盖。
*/
function hydrateTaskDocuments(records: SigningTaskRecord[]): SigningTaskRecord[] {
const templates = new Map(readLocalSigningTemplates().map((template) => [template.id, template]))
return records.map((record) => {
if (record.documentContentHtml) {
return record
let next = record
if (next.status === 'signed' && !next.signatureDataUrl) {
next = { ...next, signatureDataUrl: DEMO_SIGNATURE_DATA_URL }
}
const template = templates.get(record.documentId)
if (next.documentContentHtml) {
return next
}
const template = templates.get(next.documentId)
if (!template?.contentHtml) {
return record
return next
}
return {
...record,
templateVersionId: record.templateVersionId || template.versionId,
...next,
templateVersionId: next.templateVersionId || template.versionId,
documentContentHtml: template.contentHtml,
}
})