diff --git a/clinical-web/src/utils/signing-document.ts b/clinical-web/src/utils/signing-document.ts index 182c625..026ad3a 100644 --- a/clinical-web/src/utils/signing-document.ts +++ b/clinical-web/src/utils/signing-document.ts @@ -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() root.querySelectorAll(`[${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) diff --git a/clinical-web/src/utils/signing-fields.ts b/clinical-web/src/utils/signing-fields.ts index 3fde3e0..1bcc222 100644 --- a/clinical-web/src/utils/signing-fields.ts +++ b/clinical-web/src/utils/signing-fields.ts @@ -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: '住院号/门诊号' }, +] + /** * 同一字段在不同文书里的常见写法。 * 各科室历史文书用词不统一(患者签名 / 患方签名 / 患者(或家属)签名), diff --git a/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue b/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue index fc02183..ccbc6bf 100644 --- a/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue +++ b/clinical-web/src/views/management/documents/components/DocumentPreviewDialog.vue @@ -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; diff --git a/clinical-web/src/views/management/documents/components/WordEditorDialog.vue b/clinical-web/src/views/management/documents/components/WordEditorDialog.vue index 67eb42e..cbb69ee 100644 --- a/clinical-web/src/views/management/documents/components/WordEditorDialog.vue +++ b/clinical-web/src/views/management/documents/components/WordEditorDialog.vue @@ -339,6 +339,11 @@ function save() { 中全选复制文书后直接粘贴到下方编辑区;需要患者勾选的内容可点上方「插入勾选框」按钮插入(光标停在表格单元格里就插到该单元格),或用表头含「勾选」列的表格承载。 +

+ 患者姓名 / 性别 / 年龄 / 住院号·门诊号由系统在页眉自动带出(签署时按 HIS + 数据填充),正文不必再写这几项; 导入 .docx 时 Word 自带的页眉 / + 页脚不会被转换,请勿依赖它承载患者信息。 +

{ 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;