feat(文书管理): 增加版本工作流与版本历史
- 模板状态按后端原始状态展示与筛选,待审核与已驳回分开展示 - 仅已发布模板可发起签署,新增提交审核/审核通过/驳回等动作 - 新增版本历史弹窗,复用统一的模板时间格式化方法 - 演示数据补齐已签署任务的签名图,并提供获取签名 dataURL 的接口 - 修复编辑器插入占位符时未定位光标导致内容落到文首的问题
This commit is contained in:
@@ -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 ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user