257 lines
7.9 KiB
Vue
257 lines
7.9 KiB
Vue
<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>
|
||
|
||
<!-- 纸桌:灰底 + 内滚,纸(一页 A4)居中摆在上面。
|
||
纸不能直接当 flex 子项 —— `flex: 1` 会把高度交给布局,
|
||
`aspect-ratio` 在主轴上不生效,A4 比例就没了 -->
|
||
<div class="sign-doc-body">
|
||
<DocumentPaper
|
||
ref="paperRef"
|
||
: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)"
|
||
/>
|
||
</div>
|
||
|
||
<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;
|
||
}
|
||
|
||
/* 纸桌:灰底 + 内滚,纸(一页 A4)居中摆在上面。
|
||
纸自己的 A4 最小高度与页边距走统一 A4 规格(.doc-paper--a4,见 utils/document-paper.ts),
|
||
这里只留「桌子」—— 四周留一圈灰边,纸才不会顶到卡片边缘。
|
||
`container-type` 是纸的 cqw 下限需要的容器查询上下文。 */
|
||
.sign-doc-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 16px 20px 20px;
|
||
overflow: auto;
|
||
background: var(--bg);
|
||
container-type: inline-size;
|
||
}
|
||
|
||
.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>
|