feat: enable patient form interactions and revision-aware signing
This commit is contained in:
@@ -30,6 +30,8 @@ const props = defineProps<{
|
||||
auditLoading: boolean
|
||||
auditError: boolean
|
||||
allowLocalSigning: boolean
|
||||
formSaveMessage?: string
|
||||
formSaving?: boolean
|
||||
/** 真实模式下允许有签署权限的用户通过浏览器鼠标采集现场手写签名。 */
|
||||
allowMouseSigning: boolean
|
||||
artifacts: SigningArtifact[]
|
||||
@@ -400,12 +402,15 @@ defineExpose({ getDocumentElement, locateSignatureSlot })
|
||||
<div class="detail-body">
|
||||
<!-- 任务接口只返回模板版本哈希,正文由 API 层按哈希补回;
|
||||
补不到时如实提示并给出占位版式,不退化成"看起来像真文书"的假纸样 -->
|
||||
<p v-if="formSaveMessage" role="status" class="action-hint">
|
||||
{{ formSaving ? '正在保存填写内容…' : formSaveMessage }}
|
||||
</p>
|
||||
<SigningDocumentInteractive
|
||||
v-if="task.documentContentHtml"
|
||||
ref="documentRef"
|
||||
:task="task"
|
||||
:interactive="
|
||||
allowLocalSigning &&
|
||||
(allowLocalSigning || (allowMouseSigning && task.formState?.editable)) &&
|
||||
task.status !== 'signed' &&
|
||||
task.status !== 'void' &&
|
||||
task.status !== 'confirming'
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user