Files
xh-medical-sign-web/clinical-web/src/views/management/documents/components/SignaturePositionPreview.vue
T
2026-09-29 15:50:21 +08:00

198 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { getSignaturePreview } from '@/api/management/signature-preview'
import type { SignaturePosition, SignaturePreview } from '@/api/management/signature-preview'
const props = defineProps<{
templateId: string
contentHtml: string
position: SignaturePosition
}>()
const emit = defineEmits<{ change: [value: SignaturePosition] }>()
const result = ref<SignaturePreview | null>(null)
const busy = ref(false)
const message = ref('')
const renderedHtml = ref('')
const generation = ref(0)
const stale = computed(() => renderedHtml.value !== props.contentHtml)
watch(
() => props.templateId,
() => {
generation.value++
result.value = null
renderedHtml.value = ''
},
)
async function render(detect: boolean) {
const html = props.contentHtml
const id = props.templateId
const current = ++generation.value
busy.value = true
message.value = ''
try {
const response = await getSignaturePreview(id, html)
if (current !== generation.value || html !== props.contentHtml || id !== props.templateId)
return
result.value = response
renderedHtml.value = html
if (detect) {
if (response.candidates.length === 1) {
emit('change', response.candidates[0]!)
message.value = '已识别患者签名占位符,请检查阴影位置并微调。'
} else {
message.value = response.candidates.length
? '发现多个患者签名占位符,请保留唯一占位符后重新识别。'
: '未找到可定位的患者签名占位符,请先在正文中插入。'
}
}
} catch {
message.value = 'PDF 预览失败,请检查正文格式后重试。'
} finally {
if (current === generation.value) busy.value = false
}
}
onMounted(() => {
if (props.position.page > 0 && props.position.width > 0 && props.position.height > 0)
void render(false)
})
function overlay(page: SignaturePreview['pages'][number]) {
const p = props.position
const y = p.coordinateSystem === 'PDF_BOTTOM_LEFT' ? page.height - p.y - p.height : p.y
return {
left: `${(p.x / page.width) * 100}%`,
top: `${(y / page.height) * 100}%`,
width: `${(p.width / page.width) * 100}%`,
height: `${(p.height / page.height) * 100}%`,
}
}
const valid = computed(() => {
const p = props.position
const page = result.value?.pages.find((item) => item.page === p.page)
return (
!!page &&
[p.x, p.y, p.width, p.height].every(Number.isFinite) &&
p.x >= 0 &&
p.y >= 0 &&
p.width > 0 &&
p.height > 0 &&
p.x + p.width <= page.width &&
p.y + p.height <= page.height
)
})
function move(event: PointerEvent, page: SignaturePreview['pages'][number]) {
if (stale.value || !valid.value) return
const element = event.currentTarget as HTMLElement
const bounds = element.parentElement!.getBoundingClientRect()
const original = { ...props.position }
const startX = event.clientX,
startY = event.clientY
element.setPointerCapture(event.pointerId)
element.onpointermove = (e) => {
const dx = ((e.clientX - startX) * page.width) / bounds.width
const dy = ((e.clientY - startY) * page.height) / bounds.height
emit('change', {
...original,
x:
Math.round(Math.max(0, Math.min(page.width - original.width, original.x + dx)) * 1000) /
1000,
y:
Math.round(
Math.max(
0,
Math.min(
page.height - original.height,
original.y + (original.coordinateSystem === 'PDF_BOTTOM_LEFT' ? -dy : dy),
),
) * 1000,
) / 1000,
})
}
const end = () => {
element.onpointermove = null
element.onpointerup = null
element.onpointercancel = null
}
element.onpointerup = end
element.onpointercancel = end
}
</script>
<template>
<section class="signature-preview">
<button type="button" :disabled="busy" @click="render(true)">
{{ busy ? '生成中…' : '自动识别患者签名' }}
</button>
<button type="button" :disabled="busy" @click="render(false)">生成 / 刷新 PDF 预览</button>
<p>使用示例患者数据生成 PDF;修改坐标实时更新阴影,可拖动阴影调整位置,宽高在上方输入。</p>
<p v-if="message" role="status">{{ message }}</p>
<p v-if="result && stale" class="warning">正文已变化,请刷新 PDF 预览并重新确认位置。</p>
<p v-if="result && !valid" class="warning">
签名区域无效、页码不存在或超出页面,请调整后再保存。
</p>
<div v-if="result && !stale" class="pages">
<div v-for="page in result.pages" :key="page.page">
<p>
第 {{ page.page }} 页({{ page.width.toFixed(1) }} × {{ page.height.toFixed(1) }} pt)
</p>
<div class="page" :style="{ aspectRatio: `${page.width} / ${page.height}` }">
<img :src="page.image" :alt="`PDF 第 ${page.page} 页`" draggable="false" />
<div
v-if="position.page === page.page && valid"
class="shadow"
:style="overlay(page)"
@pointerdown.prevent="move($event, page)"
>
患者签名
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.signature-preview {
padding: 12px;
border: 1px solid #cadce4;
width: 100%;
}
button {
padding: 6px 12px;
margin-right: 8px;
cursor: pointer;
}
p {
font-size: 12px;
margin: 8px 0;
}
.warning {
color: #b54708;
}
.pages {
max-height: 560px;
overflow: auto;
background: #edf2f5;
padding: 16px;
}
.page {
position: relative;
width: 100%;
max-width: 650px;
background: white;
}
img {
width: 100%;
height: 100%;
display: block;
}
.shadow {
position: absolute;
box-sizing: border-box;
background: #168aad40;
border: 2px dashed #168aad;
color: #075c78;
cursor: move;
touch-action: none;
font-size: 10px;
overflow: hidden;
}
</style>