feat: enable patient form interactions and revision-aware signing

This commit is contained in:
cnxianyi
2026-09-29 21:37:30 +08:00
parent dc94265703
commit 9173d623a1
17 changed files with 451 additions and 49 deletions
@@ -16,6 +16,7 @@ import {
uploadSigningSignature,
} from '@/api/workbench/deliveries'
import {
saveSigningForm,
completeSigningTask,
confirmDepartmentSigning,
getSigningTaskDetail,
@@ -284,6 +285,12 @@ async function loadOrganizationOptions() {
async function loadTaskDetail(id: string | null) {
const requestId = ++detailRequestId
detailLoading.value = true
await formSaveQueue
if (requestId !== detailRequestId) return
formDraft = null
formSaveError = null
formSaveMessage.value = ''
// 换任务时先清空勾选与填写项,避免把上一个任务的取值带到新任务上;
// 文书组件挂载后会用任务自身固化的取值重新上报
signingSelection.value = []
@@ -447,8 +454,67 @@ function handleSelectionChange(items: string[]) {
signingSelection.value = items
}
let formSaveQueue: Promise<void> = Promise.resolve()
const formSaving = ref(false)
const formSaveMessage = ref('')
let formSaveError: unknown = null
let formDraft: {
taskId: string
revision: number
fingerprint: string
fields: Set<string>
} | null = null
function persistFormAnswers(answers: SigningAnswers) {
const task = selectedTask.value
if (
detailLoading.value ||
signatureSubmitting.value ||
isSigningMockEnabled ||
!task?.formState?.editable ||
task.method !== 'pad' ||
!canSignTask.value
)
return
if (!formDraft || formDraft.taskId !== task.id) {
formDraft = {
taskId: task.id,
revision: task.formState.revision,
fingerprint: JSON.stringify(task.formState.answers),
fields: new Set(task.formState.fields.filter((f) => f.editable).map((f) => f.id)),
}
formSaveError = null
}
const draft = formDraft
const filtered = Object.fromEntries(
Object.entries(answers).filter(([key]) => draft.fields.has(key)),
) as SigningAnswers
const fingerprint = JSON.stringify(filtered)
if (fingerprint === draft.fingerprint) return
draft.fingerprint = fingerprint
formSaving.value = true
formSaveMessage.value = '正在保存填写内容…'
formSaveQueue = formSaveQueue.then(async () => {
if (formSaveError) return
try {
const saved = await saveSigningForm(draft.taskId, draft.revision, filtered)
draft.revision = saved.revision
formSaveMessage.value = '填写内容已保存'
} catch (error) {
formSaveError = error
formSaveMessage.value = '保存失败,请刷新核对后重试'
ElMessage.error('填写内容保存失败,请刷新核对后重试;本次签署已阻止')
}
})
const current = formSaveQueue
void current.finally(() => {
if (current === formSaveQueue) formSaving.value = false
})
}
function handleFieldChange(answers: SigningAnswers, progress: SigningFieldProgress) {
signingAnswers.value = answers
persistFormAnswers(answers)
signingProgress.value = progress
}
@@ -461,6 +527,12 @@ function handleFieldChange(answers: SigningAnswers, progress: SigningFieldProgre
* 但也不能不提醒,否则文书会带着空签名位被固化。
*/
async function assertSigningReady() {
await formSaveQueue
if (formSaveError) {
ElMessage.error('填写内容未保存,请刷新后重试')
return false
}
const missing = signingProgress.value?.missingRequired ?? []
if (missing.length) {
@@ -1085,6 +1157,10 @@ async function submitMousePadSignature(task: SigningTaskRecord, signatureDataUrl
throw new Error('手写板会话已失效,请重新打开鼠标签署')
}
await formSaveQueue
if (formSaveError) throw new Error('填写内容未保存,请刷新后重试')
const answerRevision =
formDraft?.taskId === task.id ? formDraft.revision : task.formState?.revision
const image = decodeSignaturePng(signatureDataUrl)
const consumed = await consumeSigningToken({
token: session.token,
@@ -1114,7 +1190,7 @@ async function submitMousePadSignature(task: SigningTaskRecord, signatureDataUrl
uploadToken: consumed.uploadToken,
file: image.blob,
metadata: buildSignatureMetadata(field, image, sha256),
signer: createPatientSignerAssertion(context.signer),
signer: { ...createPatientSignerAssertion(context.signer), answerRevision },
})
signatureDialogVisible.value = false
@@ -1335,6 +1411,8 @@ onMounted(() => {
:audit-loading="eventsLoading"
:audit-error="eventsError"
:allow-local-signing="isSigningMockEnabled"
:form-save-message="formSaveMessage"
:form-saving="formSaving"
:allow-mouse-signing="canSignTask && selectedTask?.method === 'pad'"
:artifacts="artifacts"
:artifacts-loading="artifactsLoading"