feat(签署): 支持文书填写项与签名回显

This commit is contained in:
yelan
2026-09-16 15:54:40 +08:00
parent b15df82be2
commit 5361839cf2
10 changed files with 1178 additions and 164 deletions
@@ -1,7 +1,12 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import { buildSigningDocumentHtml } from '@/utils/signing-document'
import {
buildSigningDocumentHtml,
describeFieldProgress,
readAnswersFromRoot,
type SigningFieldProgress,
} from '@/utils/signing-document'
import { documentStatusLabels, signingPreviewPatient } from '../mock'
import type { DocumentTemplate } from '../types'
@@ -22,14 +27,30 @@ const emit = defineEmits<{
const activeTab = ref<'content' | 'signing'>('content')
/** 签署预览里科室试勾选的结果,仅用于预览反馈,不作为签署证据 */
const signingSelection = ref<string[]>([])
/** 签署预览里填写项的完成情况,同样只用于预览反馈 */
const signingProgress = ref<SigningFieldProgress>({
total: 0,
filled: 0,
missingRequired: [],
abnormal: [],
})
const signingBodyRef = ref<HTMLElement | null>(null)
// 切到签署预览时才挂载文书 DOM,挂载后再统计填写项,否则进页面会显示「0 / 0 项」
watch(activeTab, async (tab) => {
if (tab === 'signing') {
await nextTick()
syncPreviewFields()
}
})
watch(
() => props.visible,
(visible) => {
if (visible) {
activeTab.value = 'content'
signingSelection.value = []
signingProgress.value = { total: 0, filled: 0, missingRequired: [], abnormal: [] }
}
},
)
@@ -98,20 +119,19 @@ const signingPreviewHtml = computed(() =>
buildSigningDocumentHtml(props.template?.contentHtml ?? '', signingPreviewPatient, true),
)
// 勾选汇总只在预览内展示(切页签、重开弹窗即重置),不作为签署证据。
function handleSigningSelectionChange(event: Event) {
const target = event.target as HTMLInputElement
// 填写项与勾选汇总只在预览内展示(切页签、重开弹窗即重置),不作为签署证据。
function syncPreviewFields() {
const body = signingBodyRef.value
if (!target.classList.contains('sign-doc-checkbox')) {
if (!body) {
return
}
const body = signingBodyRef.value
const checked = body
? Array.from(body.querySelectorAll<HTMLInputElement>('input.sign-doc-checkbox:checked'))
: []
signingSelection.value = checked.map((input) => input.getAttribute('data-sign-item') ?? '')
const answers = readAnswersFromRoot(body)
signingProgress.value = describeFieldProgress(body, answers)
signingSelection.value = Array.from(
body.querySelectorAll<HTMLInputElement>('input.sign-doc-checkbox:checked'),
).map((input) => input.getAttribute('data-sign-item') ?? '')
}
</script>
@@ -179,14 +199,22 @@ function handleSigningSelectionChange(event: Event) {
<div
ref="signingBodyRef"
class="signing-preview"
@change="handleSigningSelectionChange"
@change="syncPreviewFields"
@input="syncPreviewFields"
v-html="signingPreviewHtml"
/>
<!-- eslint-enable vue/no-v-html -->
<p class="signing-selection">
已勾选 <strong>{{ signingSelection.length }}</strong> 项{{
signingSelection.length ? `:${signingSelection.join('、')}` : ''
}}
填写项 <strong>{{ signingProgress.filled }}</strong> /
{{ signingProgress.total }} 项已填<template v-if="signingSelection.length">
・已勾选 <strong>{{ signingSelection.length }}</strong> 项:{{
signingSelection.join('、')
}}</template
><template v-if="signingProgress.abnormal.length">
・异常项:<strong class="is-abnormal">{{
signingProgress.abnormal.join('、')
}}</strong></template
>
</p>
</template>
<iframe
@@ -473,6 +501,101 @@ function handleSigningSelectionChange(event: Event) {
vertical-align: middle;
}
/* 填写项:签署预览里按签署时的控件外观渲染,样式与签署端保持一致 */
.signing-preview :deep(.sign-field) {
display: inline-flex;
gap: 4px;
align-items: center;
padding: 2px 7px;
margin: 0 2px;
vertical-align: middle;
background: #f7fbfd;
border: 1px solid #d4e3e8;
border-radius: 5px;
}
.signing-preview :deep(.sign-field__label) {
flex-shrink: 0;
color: #587786;
font-size: 11.5px;
}
.signing-preview :deep(.sign-field__required) {
margin-left: 2px;
color: var(--err);
}
.signing-preview :deep(.sign-field__control) {
display: inline-flex;
gap: 8px;
align-items: center;
}
.signing-preview :deep(.sign-field__input) {
width: 9em;
min-width: 0;
padding: 1px 5px;
color: #1c2b33;
font: inherit;
font-size: 12.5px;
background: #fff;
border: 1px solid #cfe0e6;
border-radius: 4px;
}
.signing-preview :deep(.sign-field__input:focus) {
border-color: var(--brand);
outline: none;
}
.signing-preview :deep(.sign-field__choices) {
display: inline-flex;
flex-wrap: wrap;
gap: 10px;
}
.signing-preview :deep(.sign-field__radio),
.signing-preview :deep(.sign-field__checkbox) {
width: 15px;
height: 15px;
accent-color: var(--brand);
}
.signing-preview :deep(.sign-field__signature-empty) {
padding: 12px 18px;
color: #a96900;
font-size: 11px;
background: #fff9ec;
border: 1px dashed #f0a21a;
border-radius: 4px;
}
.signing-preview :deep(.sign-field--signature) {
padding: 3px 8px;
}
.signing-preview :deep(.sign-field--signature .sign-field__control) {
min-width: 150px;
justify-content: center;
}
.signing-preview :deep(.sign-field--abnormal) {
background: #fdf0f0;
border-color: #efb9bb;
}
.signing-preview :deep(.sign-field--abnormal .sign-field__label) {
color: #a42e34;
}
.signing-preview :deep(.sign-field__badge) {
padding: 1px 6px;
color: #fff;
font-size: 10px;
background: var(--err);
border-radius: 8px;
}
.signing-selection {
padding: 8px 12px;
margin: 10px 0 0;
@@ -487,6 +610,10 @@ function handleSigningSelectionChange(event: Event) {
color: var(--brand);
}
.signing-selection .is-abnormal {
color: var(--err);
}
.preview-paper {
min-height: 420px;
padding: 26px 34px;