feat(sign): 补齐医护端与患者端签署流程

This commit is contained in:
xy
2026-09-28 19:57:57 +08:00
parent a25716c861
commit 8024075439
22 changed files with 1082 additions and 163 deletions
+25 -5
View File
@@ -136,19 +136,37 @@ export function readVersionContentHtml(dto: TemplateVersionResponseDto | null):
/**
* 把正文 HTML 里的占位符编译成后端可留存的结构化字段定义。
*
* 后端版本 DTO 的 signatureFields 字段承载「这份模板有哪些填写项」,
* 正文本身仍以 contentHtml 提交。不提交这个字段的话,后端拿不到字段口径,
* 也就无法在签署侧做必填校验与证据固化。
* 正文填写项和 PDF 签名几何是两个独立契约:formFields 负责字段语义,
* signatureFields 只负责患者签名图在服务端生成 PDF 中的位置。
*/
export function buildSignatureFieldsPayload(contentHtml: string): unknown[] {
const field = collectFieldDefs(contentHtml).find((item) => item.id === 'signature')
if (!field || field.kind !== 'signature') {
throw new Error('请在正文中插入“患者签名”占位符')
}
if (!field.pdfPosition) {
throw new Error('请先为“患者签名”配置 PDF 页码和签名位置')
}
return [
{
id: field.id,
page: field.pdfPosition.page,
x: field.pdfPosition.x,
y: field.pdfPosition.y,
width: field.pdfPosition.width,
height: field.pdfPosition.height,
coordinateSystem: field.pdfPosition.coordinateSystem,
},
]
}
export function buildFormFieldsPayload(contentHtml: string): unknown[] {
return collectFieldDefs(contentHtml).map((field) => ({
id: field.id,
label: field.label,
kind: field.kind,
required: field.required,
options: field.options,
// 单/多选必须一起上报:后端靠它渲染签署控件与做取值校验,
// 缺了这个字段就只能按 kind 猜,多选组会被当成单个勾选框
...(field.multi === undefined ? {} : { multi: field.multi }),
...(field.risk ? { risk: field.risk } : {}),
}))
@@ -519,6 +537,7 @@ export async function createTemplateWithContent(
await createTemplateVersion(template.id, {
...version,
formFields: buildFormFieldsPayload(version.contentHtml),
signatureFields: buildSignatureFieldsPayload(version.contentHtml),
})
@@ -539,6 +558,7 @@ export async function updateTemplateContent(
): Promise<void> {
await createTemplateVersion(templateId, {
...version,
formFields: buildFormFieldsPayload(version.contentHtml),
signatureFields: buildSignatureFieldsPayload(version.contentHtml),
})
}
+2
View File
@@ -149,6 +149,8 @@ export interface CreateTemplateVersionRequest {
versionNo?: string
contentHtml: string
signatureFields?: unknown[]
formFields?: unknown[]
needConfirm?: boolean
fileStorageKey?: string
fileRelativePath?: string
fileSizeBytes?: number
+20 -5
View File
@@ -10,8 +10,15 @@ import { mockFlags } from '@/utils/mock-flags'
import { readLocalSigningTemplates } from '@/utils/mock-document-library'
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from '@/utils/mock-storage'
import { request } from '@/utils/request'
import { ALL_SIGNING_METHODS, formatSigningMethod, toBackendSigningMethod } from '@/utils/signing-method'
import { resendSigningSms as resendSigningSmsDelivery } from './deliveries'
import {
ALL_SIGNING_METHODS,
formatSigningMethod,
toBackendSigningMethod,
} from '@/utils/signing-method'
import {
resendSigningSms as resendSigningSmsDelivery,
sendSigningSms as sendSigningSmsDelivery,
} from './deliveries'
import type {
ApiPageResponse,
@@ -666,6 +673,7 @@ function mapAvailableTemplate(dto: AvailableTemplateVersionResponseDto): Signing
supportedMethods: [...ALL_SIGNING_METHODS],
// 需科室确认随模板带出;后端没给就是 false(签完直接归档)
needConfirm: dto.needConfirm ?? false,
formFields: dto.formFields ?? [],
// 接口不返回正文,只能带出哈希;正文由 hydrateTemplateContents 按需补回
contentSha256: dto.contentSha256,
contentHtml: cachedContent || undefined,
@@ -757,7 +765,7 @@ function mapSigningTask(
documentName: template?.name ?? `模板版本 ${dto.templateVersionNumber}`,
department: dto.visitSnapshot.departmentName || '未指定科室',
departmentId: dto.departmentId,
signerName: dto.patientSnapshot.name,
signerName: dto.signerName ?? dto.patientSnapshot.name,
// 签署人类型 / 关系:后端补齐后自动带出,取不到即 undefined,列表不显示代签标签
signerType: dto.signerType ?? undefined,
signerRelation: dto.signerRelation ?? undefined,
@@ -1183,7 +1191,11 @@ export async function createSigningTask(input: CreateSigningTaskInput): Promise<
visitId: input.visitId,
templateVersionId: input.templateVersionId,
signMethod: toBackendSigningMethod(input.method),
idempotencyKey: createIdempotencyKey(),
idempotencyKey: input.idempotencyKey || createIdempotencyKey(),
staffAnswers: input.staffAnswers ?? {},
signerType: input.signerType ?? 'SELF',
signerName: input.signerName,
signerRelation: input.signerRelation ?? (input.signerType === 'FAMILY' ? undefined : '本人'),
}
const response = await request.post<ApiResponseOf<SignTaskResponseDto>>(
'/v1/sign-tasks',
@@ -1283,7 +1295,10 @@ export function sendSigningSms(
mobile: string | null,
): Promise<SigningTaskRecord | null> {
if (!useMockData) {
throw new Error('当前后端未提供发送签署短信接口')
if (!mobile) return Promise.reject(new Error('请填写完整手机号后发送短信'))
return sendSigningSmsDelivery(id, { destination: mobile, templateCode: 'SIGN_LINK' }).then(() =>
getSigningTaskDetail(id),
)
}
const task = mockRecords.find((record) => record.id === id)
+35 -7
View File
@@ -27,13 +27,7 @@ export interface WorkbenchOverviewRequestDto {
* `confirming` 只能下载患者已签版本。
*/
export type SigningTaskStatus =
| 'pending'
| 'signing'
| 'confirming'
| 'signed'
| 'expired'
| 'void'
| 'failed'
'pending' | 'signing' | 'confirming' | 'signed' | 'expired' | 'void' | 'failed'
export type PatientSex = '男' | '女' | '未知'
@@ -87,6 +81,8 @@ export interface SigningTemplate {
* 发起签署时随文书带到任务上,决定签完落「待科室确认」还是「已签署」。
*/
needConfirm?: boolean
/** 医护端发起时必须采集的结构化正文填写项。 */
formFields?: SigningFormField[]
/** 原型:文书定稿 HTML 随模板携带,发起签署时进入任务快照。 */
contentHtml?: string
/**
@@ -97,6 +93,16 @@ export interface SigningTemplate {
contentSha256?: string
}
export interface SigningFormField {
id: string
label: string
kind: SigningFieldKind
required: boolean
options?: string[]
multi?: boolean
risk?: string
}
export interface WorkbenchSummary {
todaySigned: number
todaySignedChange: number
@@ -219,6 +225,15 @@ export type SigningDateRange = 'today' | 'yesterday' | '3d' | '7d' | '30d' | 'al
*/
export type SigningFieldKind = 'text' | 'date' | 'choice' | 'checkbox' | 'signature'
export interface SigningPdfPosition {
page: number
x: number
y: number
width: number
height: number
coordinateSystem: 'TOP_LEFT' | 'PDF_BOTTOM_LEFT'
}
/** 文书里的一个填写项定义,由模板内容中的占位符解析得到。 */
export interface SigningFieldDef {
/** 稳定字段键:与 HIS / 患者快照取值一一对应,不随占位符显示文字变化 */
@@ -242,6 +257,8 @@ export interface SigningFieldDef {
multi?: boolean
/** 取值等于该字符串时视为异常项(如风险筛查选了"是") */
risk?: string
/** 患者签名位在服务端生成 PDF 中的位置,单位为 PDF point。 */
pdfPosition?: SigningPdfPosition
}
/**
@@ -366,6 +383,11 @@ export interface CreateSigningTaskInput {
documentContentHtml?: string
/** 由文书模板带出的「需科室确认」标记;真实接口不提交(后端按 templateVersionId 取)。 */
needConfirm?: boolean
staffAnswers?: Record<string, string>
signerType?: 'SELF' | 'FAMILY'
signerName?: string
signerRelation?: string
idempotencyKey?: string
}
/** MEDISIGN 创建签署任务请求 DTO。不要向后端提交页面快照字段。 */
@@ -375,6 +397,10 @@ export interface CreateSigningTaskRequest {
templateVersionId: string
signMethod: BackendSigningMethod
idempotencyKey?: string
staffAnswers?: Record<string, string>
signerType?: 'SELF' | 'FAMILY'
signerName?: string
signerRelation?: string
}
export interface SigningApiOptions {
@@ -460,6 +486,7 @@ export interface AvailableTemplateVersionResponseDto {
effectiveAt: string
/** 患者签署后是否需要科室复核签章;后端补齐后前端无需改动即可带出。 */
needConfirm?: boolean | null
formFields?: SigningFormField[] | null
}
export interface SignTaskPatientSnapshotDto {
@@ -512,6 +539,7 @@ export interface SignTaskResponseDto {
* 后端补齐后前端无需改动即可显示;当前取不到时为 undefined。
*/
signerType?: string | null
signerName?: string | null
signerRelation?: string | null
/** 患者签署后是否需要科室复核签章(由文书模板标记带出)。 */
needConfirm?: boolean | null