feat(documents): 抽取签署文书渲染管线并新增签署预览
This commit is contained in:
+23
-145
@@ -2,12 +2,11 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { SigningTaskRecord } from '@/api/workbench/types'
|
||||
import { buildSigningDocumentHtml } from '@/utils/signing-document'
|
||||
|
||||
// 签署文书交互化渲染(原型管线):
|
||||
// ① 占位符填充({患者姓名}等 → 任务患者快照数据)
|
||||
// ② 勾选表识别:表头含"勾选"列的表格,逐行转为可交互复选框
|
||||
// ③ 签名域:任务已有签名图(signatureDataUrl)时替换 {患者签名}
|
||||
// 真实实现中填充在后端完成,勾选结果随签署证据提交;本组件用于前端闭环验证。
|
||||
// 签署文书交互化渲染:变换管线(占位符填充 / 签名域 / 勾选表转复选框)见
|
||||
// utils/signing-document,与文书库的「签署预览」共用同一实现。
|
||||
// 本组件只负责挂载 HTML、收集勾选结果并渲染签署态外壳。
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
task: SigningTaskRecord
|
||||
@@ -23,144 +22,9 @@ const emit = defineEmits<{
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const selectedItems = ref<string[]>([])
|
||||
|
||||
function replaceTextPlaceholder(root: Element, placeholder: string, replacement: string) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
const targets: Text[] = []
|
||||
let node = walker.nextNode()
|
||||
|
||||
while (node) {
|
||||
if ((node.textContent ?? '').includes(placeholder)) {
|
||||
targets.push(node as Text)
|
||||
}
|
||||
node = walker.nextNode()
|
||||
}
|
||||
|
||||
targets.forEach((textNode) => {
|
||||
textNode.textContent = textNode.textContent?.split(placeholder).join(replacement) ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
function replaceSignaturePlaceholder(root: Element, dataUrl: string) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
const targets: Text[] = []
|
||||
let node = walker.nextNode()
|
||||
|
||||
while (node) {
|
||||
if ((node.textContent ?? '').includes('{患者签名}')) {
|
||||
targets.push(node as Text)
|
||||
}
|
||||
node = walker.nextNode()
|
||||
}
|
||||
|
||||
targets.forEach((textNode) => {
|
||||
const parts = (textNode.textContent ?? '').split('{患者签名}')
|
||||
const fragment = document.createDocumentFragment()
|
||||
|
||||
if (parts[0]) {
|
||||
fragment.appendChild(document.createTextNode(parts[0]))
|
||||
}
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = dataUrl
|
||||
image.alt = '患者签名'
|
||||
image.className = 'sign-doc-signature'
|
||||
fragment.appendChild(image)
|
||||
|
||||
if (parts[1]) {
|
||||
fragment.appendChild(document.createTextNode(parts[1]))
|
||||
}
|
||||
|
||||
textNode.replaceWith(fragment)
|
||||
})
|
||||
}
|
||||
|
||||
function fillPatientPlaceholders(root: Element) {
|
||||
const task = props.task
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
const targets: Text[] = []
|
||||
let node = walker.nextNode()
|
||||
|
||||
while (node) {
|
||||
if ((node.textContent ?? '').includes('{')) {
|
||||
targets.push(node as Text)
|
||||
}
|
||||
node = walker.nextNode()
|
||||
}
|
||||
|
||||
targets.forEach((textNode) => {
|
||||
textNode.textContent = (textNode.textContent ?? '')
|
||||
.replace(/{患者姓名}/g, task.patientName)
|
||||
.replace(/{性别}/g, task.sex)
|
||||
.replace(/{年龄}/g, `${task.age} 岁`)
|
||||
.replace(/{就诊号}/g, task.visitNo)
|
||||
.replace(/{联系电话}/g, '—')
|
||||
.replace(/{预约日期}/g, task.visitDate)
|
||||
.replace(/{与患者关系}/g, '本人')
|
||||
.replace(/{签署日期}/g, task.signedAt || task.visitDate || ' 年 月 日')
|
||||
})
|
||||
}
|
||||
|
||||
function convertCheckboxes(root: Element) {
|
||||
root.querySelectorAll('table').forEach((table) => {
|
||||
const headerCells = Array.from(table.querySelectorAll('thead th'))
|
||||
const checkIndex = headerCells.findIndex((cell) => cell.textContent?.includes('勾选'))
|
||||
|
||||
if (checkIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const nameIndex = headerCells.findIndex((cell) => cell.textContent?.includes('项目'))
|
||||
table.classList.add('sign-doc-table')
|
||||
|
||||
table.querySelectorAll('tbody tr').forEach((row) => {
|
||||
const cells = Array.from(row.querySelectorAll('td'))
|
||||
const checkCell = cells[checkIndex]
|
||||
|
||||
if (!checkCell) {
|
||||
return
|
||||
}
|
||||
|
||||
const nameCell = cells[nameIndex > -1 ? nameIndex : 1]
|
||||
const itemName = (nameCell?.textContent ?? '').trim()
|
||||
|
||||
const checkbox = document.createElement('input')
|
||||
checkbox.type = 'checkbox'
|
||||
checkbox.className = 'sign-doc-checkbox'
|
||||
checkbox.setAttribute('data-sign-item', itemName)
|
||||
checkbox.disabled = !props.interactive
|
||||
|
||||
checkCell.textContent = ''
|
||||
checkCell.appendChild(checkbox)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const processedHtml = computed(() => {
|
||||
const source = props.task.documentContentHtml
|
||||
|
||||
if (!source) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const doc = new DOMParser().parseFromString(`<div id="sign-doc-root">${source}</div>`, 'text/html')
|
||||
const root = doc.getElementById('sign-doc-root')
|
||||
|
||||
if (!root) {
|
||||
return source
|
||||
}
|
||||
|
||||
fillPatientPlaceholders(root)
|
||||
|
||||
if (props.task.signatureDataUrl) {
|
||||
replaceSignaturePlaceholder(root, props.task.signatureDataUrl)
|
||||
} else {
|
||||
replaceTextPlaceholder(root, '{患者签名}', '(待患者签署)')
|
||||
}
|
||||
|
||||
convertCheckboxes(root)
|
||||
|
||||
return root.innerHTML
|
||||
})
|
||||
const processedHtml = computed(() =>
|
||||
buildSigningDocumentHtml(props.task.documentContentHtml ?? '', props.task, props.interactive),
|
||||
)
|
||||
|
||||
function handleSelectionChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
@@ -184,7 +48,9 @@ function handleSelectionChange(event: Event) {
|
||||
<div class="sign-doc-banner">
|
||||
<span>已按任务固化的文书版本渲染(占位符已填充、"勾选"表已交互化)</span>
|
||||
<span class="sign-doc-state">
|
||||
{{ task.signatureDataUrl ? '已采集签名' : task.status === 'signed' ? '已签署' : '待患者签署' }}
|
||||
{{
|
||||
task.signatureDataUrl ? '已采集签名' : task.status === 'signed' ? '已签署' : '待患者签署'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -199,7 +65,9 @@ function handleSelectionChange(event: Event) {
|
||||
|
||||
<footer v-if="interactive" class="sign-doc-footer">
|
||||
<span>
|
||||
已勾选 <strong>{{ selectedItems.length }}</strong> 项{{ selectedItems.length ? `:${selectedItems.join('、')}` : '' }}
|
||||
已勾选 <strong>{{ selectedItems.length }}</strong> 项{{
|
||||
selectedItems.length ? `:${selectedItems.join('、')}` : ''
|
||||
}}
|
||||
</span>
|
||||
<span>完成勾选后通过「手写板签署 / 线上签署」采集签名,勾选结果将随签署证据一并提交。</span>
|
||||
</footer>
|
||||
@@ -306,6 +174,16 @@ function handleSelectionChange(event: Event) {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* 正文行内选项(□否 □是):用 label 包住方框与选项文字,点文字也能选中 */
|
||||
.sign-doc-body :deep(.sign-doc-option) {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
margin-right: 14px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sign-doc-body :deep(.sign-doc-signature) {
|
||||
max-width: 150px;
|
||||
max-height: 52px;
|
||||
|
||||
Reference in New Issue
Block a user