feat(sign): 支持多位置签名并优化坐标识别与模板版本校验

This commit is contained in:
xy
2026-09-30 18:14:22 +08:00
parent 69825dfa0a
commit e5f0a5ec97
17 changed files with 622 additions and 229 deletions
@@ -76,9 +76,12 @@ const message = reactive({ text: '', tone: '' as '' | 'ok' | 'warn' })
* 那种位置必须有占位符,否则签署时不会回填(系统不会另外在文书上插患者信息)。
*/
const AUTO_FILL_LABELS = ['患者姓名', '性别', '年龄', '就诊号', '与患者关系', '签署日期']
const MANUAL_LABELS = ['患者签名', '勾选项']
const MANUAL_LABELS = ['签名', '勾选项']
function findPreset(label: string): SigningFieldDef | null {
if (label === '签名') {
return { id: 'signature', label, kind: 'signature', required: false, options: [] }
}
return SIGNING_FIELD_PRESETS.find((field) => field.label === label) ?? null
}
@@ -139,6 +142,29 @@ const fieldDraft = reactive({
pdfCoordinateSystem: 'TOP_LEFT' as 'TOP_LEFT' | 'PDF_BOTTOM_LEFT',
})
const signaturePreviewHtml = computed(() => {
const document = new DOMParser().parseFromString(valueHtml.value, 'text/html')
// A field converted to “signature” must be locatable before its first coordinates exist.
for (const node of document.querySelectorAll('[data-field]')) {
if (
node.getAttribute('data-field') === selectedFieldId.value &&
fieldDraft.kind === 'signature'
)
node.setAttribute('data-kind', 'signature')
}
return document.documentElement.outerHTML
})
const signaturePositionEmpty = computed(() =>
[
fieldDraft.pdfPage,
fieldDraft.pdfX,
fieldDraft.pdfY,
fieldDraft.pdfWidth,
fieldDraft.pdfHeight,
].every((value) => !value.trim()),
)
const previewPosition = computed<SignaturePosition>(() => ({
page: Number(fieldDraft.pdfPage),
x: Number(fieldDraft.pdfX),
@@ -154,6 +180,12 @@ function changePosition(position: SignaturePosition) {
fieldDraft.pdfWidth = String(position.width)
fieldDraft.pdfHeight = String(position.height)
fieldDraft.pdfCoordinateSystem = position.coordinateSystem
// Keep this position when switching to another field; other draft settings still require “更新该项”.
if (selectedField.value)
paneRef.value?.updateField(selectedField.value.id, {
kind: fieldDraft.kind,
pdfPosition: position,
})
}
/** 类型下拉的取值(原型 fpKind 的 5 个选项) */
@@ -240,9 +272,16 @@ function selectFieldFromEditor(field: SigningFieldDef) {
function insertField(field: SigningFieldDef) {
// 自定义文本项每插一个就是独立填写项,字段键必须当场取一个没被占用的
// (见 allocateCustomFieldId)——两个文本框是两处要填的内容,共用一个键才是错的
const target = field.id.startsWith('custom:')
? { ...field, id: allocateCustomFieldId(valueHtml.value, field.label) }
: field
const target =
field.kind === 'signature' && detectedFields.value.some((item) => item.id === field.id)
? {
...field,
id: `sign_${globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`}`,
required: false,
}
: field.id.startsWith('custom:')
? { ...field, id: allocateCustomFieldId(valueHtml.value, field.label) }
: field
paneRef.value?.insertField(target)
}
@@ -272,7 +311,7 @@ function applyFieldDraft() {
const options = fieldDraft.options.map((option) => option.trim()).filter(Boolean)
let pdfPosition: SigningFieldDef['pdfPosition']
if (field.id === 'signature' && fieldDraft.kind === 'signature') {
if (fieldDraft.kind === 'signature') {
const page = Number(fieldDraft.pdfPage)
const x = Number(fieldDraft.pdfX)
const y = Number(fieldDraft.pdfY)
@@ -292,7 +331,7 @@ function applyFieldDraft() {
width <= 0 ||
height <= 0
) {
setMessage('患者签名位置须填写有效页码及 PDF point 坐标', 'warn')
setMessage('签名位置须填写有效页码及坐标', 'warn')
return false
}
pdfPosition = { page, x, y, width, height, coordinateSystem: fieldDraft.pdfCoordinateSystem }
@@ -378,9 +417,12 @@ function validate(requireSignaturePosition = true): string | null {
return `「${broken.label}」只配了一个选项,勾选类至少要两个(或删掉选项改成单个勾选框)`
}
const patientSignature = detectedFields.value.find((field) => field.id === 'signature')
if (requireSignaturePosition && !patientSignature?.pdfPosition) {
return '请选中“患者签名”占位符,配置其 PDF 页码和签名位置后再保存'
const signatures = detectedFields.value.filter((field) => field.kind === 'signature')
if (
requireSignaturePosition &&
(!signatures.length || signatures.some((field) => !field.pdfPosition))
) {
return '请为每个签名占位符识别并保存 PDF 签名位置'
}
return null
@@ -391,8 +433,8 @@ const saveHints = computed(() => {
const ids = new Set(detectedFields.value.map((field) => field.id))
const hints: string[] = []
if (!ids.has('signature')) {
hints.push('文书中没有签名占位符,签署后签名无处落位,建议插入「患者签名」')
if (!detectedFields.value.some((field) => field.kind === 'signature')) {
hints.push('文书中没有签名占位符,签署后签名无处落位,建议插入「签名」')
}
if (!ids.has('signedDate')) {
@@ -476,7 +518,7 @@ async function save() {
return
}
if (selectedField.value?.id === 'signature') {
if (selectedField.value?.kind === 'signature') {
if (!applyFieldDraft()) return
await nextTick()
valueHtml.value = paneRef.value?.getHtml() ?? valueHtml.value
@@ -601,15 +643,6 @@ function requestClose() {
</button>
</div>
<SignaturePositionPreview
v-if="template && selectedField.id === 'signature' && fieldDraft.kind === 'signature'"
:key="template.id"
:template-id="template.id"
:content-html="valueHtml"
:position="previewPosition"
@change="changePosition"
/>
<div v-if="isOptionKind" class="fp-row">
<b class="fp-title">选项设置(支持多个)</b>
<span class="fp-hint">选项逐项展示为可勾选框;勾选类默认可多选,选择类默认单选</span>
@@ -623,11 +656,8 @@ function requestClose() {
</label>
</div>
<div
v-if="selectedField.id === 'signature' && fieldDraft.kind === 'signature'"
class="fp-row signature-position-row"
>
<b class="fp-title">患者签名 PDF 位置</b>
<div v-if="fieldDraft.kind === 'signature'" class="fp-row signature-position-row">
<b class="fp-title">签名位置</b>
<span class="fp-hint">PDF point;页码从 1 开始</span>
<label class="fp-field"
><span>页码</span><input v-model="fieldDraft.pdfPage" inputmode="numeric"
@@ -654,11 +684,13 @@ function requestClose() {
</div>
<SignaturePositionPreview
v-if="template && selectedField.id === 'signature' && fieldDraft.kind === 'signature'"
:key="template.id"
v-if="template && fieldDraft.kind === 'signature'"
:key="`${template.id}:${selectedField.id}`"
:field-id="selectedField.id"
:template-id="template.id"
:content-html="valueHtml"
:content-html="signaturePreviewHtml"
:position="previewPosition"
:position-empty="signaturePositionEmpty"
@change="changePosition"
/>
@@ -724,16 +756,14 @@ function requestClose() {
:class="`is-${button.field.kind}`"
:title="
button.field.kind === 'signature'
? '签署时由患者手写采集,签名图落在文书原位置'
? '插入独立签名位置,可改名为患者签名、家属签名或医生签名'
: '签署时由患者逐项勾选;点正文里的胶囊可配置多个选项'
"
@click="insertField(button.field)"
>
[{{ button.label }}]
</button>
<span class="pb-note">
签名 / 勾选由患者在签署时填写;其余签署时按患者信息自动回填
</span>
<span class="pb-note"> 签名可重复插入并自定义名称;勾选项在签署时填写 </span>
</div>
</template>
</EditorWangPane>