This commit is contained in:
yelan
2026-09-29 22:18:01 +08:00
17 changed files with 451 additions and 49 deletions
@@ -30,6 +30,8 @@ const props = defineProps<{
auditLoading: boolean
auditError: boolean
allowLocalSigning: boolean
formSaveMessage?: string
formSaving?: boolean
/** 真实模式下允许有签署权限的用户通过浏览器鼠标采集现场手写签名。 */
allowMouseSigning: boolean
artifacts: SigningArtifact[]
@@ -414,12 +416,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"