Files
xh-medical-sign-web/clinical-web/src/components/signing/SigningDocumentInteractive.vue
T
yelan 3cb174ce0c refactor(文书预览): 抽出统一的文书纸与预览弹窗,消除五处重复渲染
预览原先在 5 个地方各写一遍(文书库预览、编辑器签署预览、发起签署弹窗预览、
工作台在线预览、工作台文书区),基础排版已经分叉:表格边框 #52646e 与 #c8d7dd
两套色、单元格内边距 5px 7px 与 5px 9px 两个值、只有两处带宋体。
原型本身是「一处渲染(.paper + fillDocHtml)、两种外框」,这次按那个形状收口。

新增
- utils/document-paper.ts:基础排版的**唯一一份** CSS(p / h1-h4 / 表格 / 宋体)。
  运行期幂等注入 head —— 因为同一份纸有两条渲染通道:v-html(吃全局样式)与
  iframe srcdoc(隔离模板自带 style,只能吃字符串),各写一份必然漂移。
- components/document/DocumentPaper.vue:唯一的纸渲染器。
  signing / raw 两模式、interactive、取值还原、☑☐ 就地切换、签名位请求采集、
  填写项与勾选上报、getDocumentElement()(供导出 PDF)。
- components/document/DocumentPreviewDialog.vue:唯一的预览外框。
  标题/副标题/徽标、可选页签、加载与错误态、纸舞台(浅色卡片 / 深灰 A4)、
  #footer 与 #empty 插槽。
- styles/document-preview.css:页脚按钮等**插槽内容**要用的类。
  插槽节点带的是父组件 scope id,弹窗的 scoped 样式命中不了 —— 与
  signing-document.css 同一类问题,同一套解法。

迁移
- 文书库预览、编辑器签署预览、发起签署弹窗预览、工作台在线预览全部改走统一组件;
  SigningDocumentInteractive 的纸换成 DocumentPaper,只留任务外壳
  (横幅 / 作废章 / 填写项进度)。
- 删除四处基础排版副本;SimSun 与 :deep(table) 现在各只剩一处。

行为保持
- 文书库预览仍是两个页签(文书内容走 iframe 隔离、签署预览走 v-html),
  没有正文时仍给占位版式(占位符写成 {患者姓名} 原始形态,不预填示例值)。
- 工作台预览的深灰 A4 舞台、页脚说明与两个出口按钮不变。
- 作废章原先是纸的兄弟节点、DOM 顺序在前,纸改成自带白底后会被盖住 ——
  给章补 z-index,并在探针里加命中测试锁死。

验证
- 新增 probe-doc-paper.cjs:31/31 PASS。核心是**计算样式**断言
  (边框色 / 内边距 / 宋体 / border-collapse),并断言文书库预览与编辑器预览
  两份纸的样式**逐项一致** —— 只看 DOM 存在证明不了"只有一份排版"。
- 回归:probe-new-task 104/104、probe-workbench-lib 46/46(新增作废章命中测试)、
  probe-dept-confirm 36/36、probe-method-domain 14/14、probe-report-analytics 92/92。
- vue-tsc 与 eslint 0 错。

净减 888 行(309 增 / 1197 删)。
2026-09-26 20:56:02 +08:00

249 lines
7.4 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">
/**
* 签署工作台的任务态文书外壳。
*
* 它只负责**任务专属的那一圈**:顶部横幅(按哪个版本渲染 / 当前签署状态)、
* 作废章、底部填写项进度。纸本身(排版、占位符回填、勾选交互、签名位采集)
* 在 components/document/DocumentPaper.vue —— 那是全项目唯一一处,
* 工作台看到的版式必须与预览、与导出 PDF 完全一致。
*/
import { computed, ref } from 'vue'
import type { SigningAnswers, SigningTaskRecord } from '@/api/workbench/types'
import DocumentPaper from '@/components/document/DocumentPaper.vue'
import type {
SigningDocumentData,
SigningFieldProgress,
SigningSignatureRequest,
} from '@/utils/signing-document'
const props = withDefaults(
defineProps<{
task: SigningTaskRecord
interactive?: boolean
/**
* 在文书里点击签名位采集到的签名图(key 是字段键,如 doctor)。
*
* 不走 task:task 是服务端快照,这里装的是「本次会话刚采到、还没提交」的取值。
* 单独一份也让「整篇重渲染」不会把刚采的签名冲掉。
*/
signatureInjections?: SigningAnswers
}>(),
{ interactive: false, signatureInjections: () => ({}) },
)
const emit = defineEmits<{
'selection-change': [items: string[]]
'field-change': [answers: SigningAnswers, progress: SigningFieldProgress]
/** 请求采集某个签名位,由签署页打开签字板并把结果回灌到 signatureInjections */
'signature-request': [request: SigningSignatureRequest]
}>()
const paperRef = ref<InstanceType<typeof DocumentPaper> | null>(null)
const progress = ref<SigningFieldProgress>({
total: 0,
filled: 0,
missingRequired: [],
abnormal: [],
pendingSignatures: [],
})
/**
* 只挑渲染真正需要的字段显式映射,不直接透传整个任务对象:
* 渲染数据契约与任务记录是两回事,哪天字段改名也不会静默丢取值。
*/
const documentData = computed<SigningDocumentData>(() => ({
patientName: props.task.patientName,
sex: props.task.sex,
age: props.task.age,
visitNo: props.task.visitNo,
visitDate: props.task.visitDate,
signedAt: props.task.signedAt,
signatureDataUrl: props.task.signatureDataUrl,
selectedItems: props.task.selectedItems,
fieldAnswers: props.task.fieldAnswers,
}))
/** 文书里的填写项数量(含只读展示),用于提示「这份文书有几个填写项」 */
const fieldTotal = computed(() => progress.value.total)
/** 签名/状态文案:作废要单独说清楚,不能被当成「待患者签署」 */
const signatureStateLabel = computed(() => {
if (props.task.status === 'void') {
return '已作废'
}
if (props.task.signatureDataUrl) {
return '已采集签名'
}
return props.task.status === 'signed' || props.task.status === 'confirming'
? '已签署'
: '待患者签署'
})
/** 纸把填写项与勾选结果上报上来,这里转给签署页,同时留一份给页脚用 */
function handleFieldChange(answers: SigningAnswers, next: SigningFieldProgress) {
progress.value = next
emit('field-change', answers, next)
}
/** 供「导出 PDF」使用:拿到已渲染的文书容器(含固定页眉与签署态取值) */
function getDocumentElement() {
return paperRef.value?.getDocumentElement() ?? null
}
defineExpose({ getDocumentElement })
</script>
<template>
<article class="sign-doc">
<div class="sign-doc-banner">
<span>已按任务固化的文书版本渲染(填写项已按患者信息预填)</span>
<span class="sign-doc-state" :class="{ 'is-void': task.status === 'void' }">
{{ signatureStateLabel }}
</span>
</div>
<div v-if="task.status === 'void'" class="void-stamp">已 作 废</div>
<DocumentPaper
ref="paperRef"
class="sign-doc-body"
:content-html="task.documentContentHtml ?? ''"
:data="documentData"
:interactive="interactive"
:selected-items="task.selectedItems"
:field-answers="task.fieldAnswers"
:signature-data-url="task.signatureDataUrl"
:signature-injections="signatureInjections"
@selection-change="emit('selection-change', $event)"
@field-change="handleFieldChange"
@signature-request="emit('signature-request', $event)"
/>
<footer v-if="interactive" class="sign-doc-footer">
<span>
填写项 <strong>{{ progress.filled }}</strong> / {{ fieldTotal }} 项已填
<template v-if="progress.abnormal.length">
・异常项:<strong class="is-abnormal">{{ progress.abnormal.join('、') }}</strong>
</template>
</span>
<span v-if="progress.missingRequired.length" class="is-missing">
必填未完成:{{ progress.missingRequired.join('、') }},请补充后再采集签名。
</span>
<span v-else-if="progress.pendingSignatures.length" class="is-pending">
还有签名位待采集:<strong>{{ progress.pendingSignatures.join('、') }}</strong
>,可直接点击文中签名位完成。
</span>
<span v-else
>完成填写后通过「手写板签署 / 线上签署」采集签名,填写项与勾选结果随签署证据一并提交。</span
>
</footer>
<footer v-else-if="fieldTotal" class="sign-doc-footer">
<span>
填写项 <strong>{{ progress.filled }}</strong> / {{ fieldTotal }} 项已填,取值已随签署固化。
</span>
</footer>
</article>
</template>
<style scoped>
.sign-doc {
position: relative;
display: flex;
flex-direction: column;
min-height: 480px;
background: #fff;
border-radius: var(--r);
box-shadow: var(--sh);
overflow: hidden;
}
/* 作废章压在文书上,位置与存量回退纸样(SigningDocumentPreview)保持一致 */
.void-stamp {
position: absolute;
z-index: 2;
top: 52px;
right: 32px;
padding: 4px 12px;
color: rgb(208 83 79 / 70%);
font-size: 20px;
font-weight: 700;
letter-spacing: 4px;
pointer-events: none;
border: 2.5px solid rgb(208 83 79 / 55%);
border-radius: 8px;
transform: rotate(-14deg);
}
.sign-doc-banner {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
padding: 9px 16px;
color: var(--brand);
font-size: 11.5px;
background: var(--brand-l);
}
.sign-doc-state {
padding: 2px 9px;
color: #fff;
font-weight: 700;
background: var(--brand);
border-radius: 10px;
}
.sign-doc-state.is-void {
background: #8a9aa3;
}
/* 纸的盒子:DocumentPaper 只给排版与白底(variant=plain),
内边距与滚动由这里给。类名通过 fallthrough 落到纸的根元素上。 */
.sign-doc-body {
flex: 1;
min-height: 0;
padding: 26px 32px 30px;
overflow: auto;
}
.sign-doc-footer {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 16px;
color: var(--mut);
font-size: 12px;
background: #f8fbfc;
border-top: 1px solid var(--line);
}
.sign-doc-footer strong {
color: var(--brand);
}
.sign-doc-footer .is-abnormal {
color: var(--err);
}
.sign-doc-footer .is-missing {
color: var(--err);
}
.sign-doc-footer .is-pending {
color: #a96900;
}
.sign-doc-footer .is-pending strong {
color: #a96900;
}
/* 正文字号、表格边框、宋体等基础排版在 components/document/DocumentPaper.vue 的
DOCUMENT_PAPER_CSS 里(全项目唯一一份)。填写项胶囊、选项 ☑☐、系统页眉在
styles/signing-document.css。这里只保留"这份文书外壳"自己的东西。 */
</style>