feat(signing): 文书签署页添加 HIS 患者信息页眉

新增固定页眉展示患者姓名、性别、年龄、住院号/门诊号,取值自动来自 HIS
快照,仅作只读展示不参与填写项统计与签署证据。同时按字段键去重填写
项进度统计,避免正文与页眉重复字段导致分母虚高;并在文书编辑器中提
示页眉由系统自动带出、Word 自带页眉不会被转换。
This commit is contained in:
yelan
2026-09-16 16:49:20 +08:00
parent 5361839cf2
commit ecac2bc899
5 changed files with 140 additions and 2 deletions
+44 -2
View File
@@ -22,6 +22,7 @@ import {
FIELD_TOKEN_OPEN,
resolveAnswersFromSystem,
resolveFieldByLabel,
SIGNING_DOCUMENT_HEADER,
type SigningSystemSnapshot,
} from './signing-fields'
@@ -454,6 +455,36 @@ function convertInlineOptions(root: Element, interactive: boolean, selected: Set
})
}
/**
* 生成固定页眉:患者姓名 / 性别 / 年龄 / 住院号·门诊号,取值全部来自 HIS。
* 只读展示,不挂 data-sign-field —— 页眉不是"待填写项",也不进签署证据,
* 患者信息以任务快照为准,页眉只负责把快照呈现出来。
*/
function buildDocumentHeader(doc: Document, answers: SigningAnswers): HTMLElement {
const header = doc.createElement('header')
header.className = 'sign-doc-header'
SIGNING_DOCUMENT_HEADER.forEach(({ id, label }) => {
const item = doc.createElement('span')
item.className = 'sign-doc-header__item'
const labelElement = doc.createElement('span')
labelElement.className = 'sign-doc-header__label'
labelElement.textContent = `${label}:`
const value = answers[id]
const valueElement = doc.createElement('span')
valueElement.className = 'sign-doc-header__value'
valueElement.textContent = typeof value === 'string' ? value : ''
item.appendChild(labelElement)
item.appendChild(valueElement)
header.appendChild(item)
})
return header
}
/**
* 生成签署态文书 HTML。
* contentHtml 为空时返回空串;解析失败时原样返回,不会抛错。
@@ -495,6 +526,9 @@ export function buildSigningDocumentHtml(
convertCheckboxes(root, interactive, selected)
convertInlineOptions(root, interactive, selected)
// 页眉最后插入:保证 HIS 取值不会被当成文书内容参与占位符/方框识别
root.insertBefore(buildDocumentHeader(doc, answers), root.firstChild)
return root.innerHTML
}
@@ -537,7 +571,12 @@ export function readAnswersFromRoot(root: HTMLElement): SigningAnswers {
return answers
}
/** 统计填写项进度:已填数量、未填的必填项、命中的异常项 */
/**
* 统计填写项进度:已填数量、未填的必填项、命中的异常项。
*
* 按字段键去重:同一个字段在文书里可能出现多次(页眉的患者姓名/就诊号与正文重复是常态),
* 它们共用一份取值,只能算一个填写项,否则分母会被重复计数撑大。
*/
export function describeFieldProgress(
root: HTMLElement,
answers: SigningAnswers,
@@ -548,15 +587,18 @@ export function describeFieldProgress(
missingRequired: [],
abnormal: [],
}
const seen = new Set<string>()
root.querySelectorAll<HTMLElement>(`[${FIELD_ATTR}]`).forEach((host) => {
const id = host.getAttribute(FIELD_ATTR)
const label = host.getAttribute(FIELD_LABEL_ATTR) ?? id ?? ''
if (!id) {
if (!id || seen.has(id)) {
return
}
seen.add(id)
const value = answers[id]
const empty = value === undefined || value === '' || value === false
const risk = host.getAttribute(FIELD_RISK_ATTR)
+18
View File
@@ -44,6 +44,24 @@ export const SIGNING_FIELD_PRESETS: SigningFieldDef[] = [
{ id: 'doctor', label: '医师签名', kind: 'signature', required: false, options: [] },
]
/**
* 文书固定页眉。
*
* 页眉不由模板正文承载,也不给操作人员编辑:内容全部来自 HIS,
* 签署时按字段键自动填充,正式 PDF 由服务端按页重复同一份页眉。
* 字段键复用 SIGNING_FIELD_PRESETS,取值映射与正文共用一套,不会出现两处口径。
*
* 因为页眉是系统渲染的只读展示(不挂 data-sign-field),
* 它既不会出现在"填写项"统计里,也不会被写进签署证据——
* 患者信息以任务快照为准,页眉只是把快照展示出来。
*/
export const SIGNING_DOCUMENT_HEADER: Array<{ id: string; label: string }> = [
{ id: 'patient', label: '患者姓名' },
{ id: 'sex', label: '性别' },
{ id: 'age', label: '年龄' },
{ id: 'visit', label: '住院号/门诊号' },
]
/**
* 同一字段在不同文书里的常见写法。
* 各科室历史文书用词不统一(患者签名 / 患方签名 / 患者(或家属)签名),
@@ -455,6 +455,37 @@ function syncPreviewFields() {
max-width: 100%;
}
/* 固定页眉:患者基本信息由 HIS 自动带出,与正文之间用一条横线分隔 */
.signing-preview :deep(.sign-doc-header) {
display: flex;
flex-wrap: wrap;
gap: 12px 26px;
padding-bottom: 10px;
margin-bottom: 16px;
border-bottom: 1.5px solid #52646e;
}
.signing-preview :deep(.sign-doc-header__item) {
display: inline-flex;
gap: 4px;
align-items: baseline;
}
.signing-preview :deep(.sign-doc-header__label) {
flex-shrink: 0;
color: #52646e;
font-size: 12.5px;
}
.signing-preview :deep(.sign-doc-header__value) {
min-width: 6.5em;
padding: 0 6px 1px;
color: #1c2b33;
font-size: 12.5px;
text-align: center;
border-bottom: 1px solid #52646e;
}
.signing-preview :deep(table) {
width: 100%;
margin: 8px 0;
@@ -339,6 +339,11 @@ function save() {
中全选复制文书后直接粘贴到下方编辑区;需要患者勾选的内容可点上方「插入勾选框」按钮插入(光标停在表格单元格里就插到该单元格),或用表头含「勾选」列的表格承载。
</span>
</div>
<p class="docx-header-note">
患者姓名 / 性别 / 年龄 / 住院号·门诊号由系统在页眉自动带出(签署时按 HIS
数据填充),正文不必再写这几项; 导入 .docx 时 Word 自带的页眉 /
页脚不会被转换,请勿依赖它承载患者信息。
</p>
<div class="editor-area">
<EditorWangPane
ref="paneRef"
@@ -616,6 +621,17 @@ function save() {
font-size: 12px;
}
/* 患者基本信息由系统页眉承载、且 Word 页眉不会被 mammoth 转换,这条提示常驻在编辑区上方 */
.docx-header-note {
padding: 6px 9px;
margin: 0 0 8px;
color: var(--warn);
font-size: 11.5px;
line-height: 1.7;
background: var(--warn-l);
border-radius: 6px;
}
.editor-area {
display: flex;
flex-direction: column;
@@ -292,6 +292,37 @@ onMounted(() => {
max-width: 100%;
}
/* 固定页眉:患者基本信息由 HIS 自动带出,与正文之间用一条横线分隔 */
.sign-doc-body :deep(.sign-doc-header) {
display: flex;
flex-wrap: wrap;
gap: 12px 26px;
padding-bottom: 10px;
margin-bottom: 16px;
border-bottom: 1.5px solid #52646e;
}
.sign-doc-body :deep(.sign-doc-header__item) {
display: inline-flex;
gap: 4px;
align-items: baseline;
}
.sign-doc-body :deep(.sign-doc-header__label) {
flex-shrink: 0;
color: #52646e;
font-size: 12.5px;
}
.sign-doc-body :deep(.sign-doc-header__value) {
min-width: 6.5em;
padding: 0 6px 1px;
color: #1c2b33;
font-size: 12.5px;
text-align: center;
border-bottom: 1px solid #52646e;
}
.sign-doc-body :deep(.sign-doc-checkbox) {
width: 15px;
height: 15px;