feat(sign): 补齐医护端与患者端签署流程
This commit is contained in:
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ export interface CreateTemplateVersionRequest {
|
||||
versionNo?: string
|
||||
contentHtml: string
|
||||
signatureFields?: unknown[]
|
||||
formFields?: unknown[]
|
||||
needConfirm?: boolean
|
||||
fileStorageKey?: string
|
||||
fileRelativePath?: string
|
||||
fileSizeBytes?: number
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user