Files
xh-medical-sign-web/clinical-web/src/views/workbench/signing/components/SignedDocumentDialog.vue
T
yelan 82acc03442 feat(签署工作台): 补代签标签、关联处方卡、线上等待态与只读副本
按原型 V10.2 详情页补齐四块:

- 任务列表「代签」标签(原型 .vtag.chk 橙底):SigningTaskRecord 增加
  signerType / signerRelation,仅签署人非「本人」时显示。
  有意偏离:关系为空时不输出空括号(原型会渲染成「家属代签()」)。
- 「关联处方」可折叠卡(原型 rxCard):新建 api/workbench/prescriptions.ts
  统一处方来源,把弹窗里那份搬过来消除重复,并新增详情用的 7 列明细。
  原型两处处方口径本就不同(弹窗按就诊号哈希、详情按任务号派生),分开维护不合并。
  真实模式返回空 → 整卡不渲染,不显示空表(会被读成「系统查过了,确实没处方」)。
  顺带按原型调了详情卡片顺序:文书 → 签署文件 → 关联处方 → 操作留痕。
- 短信 / 公众号待签署的等待态(原型 .tg-wait):号码取后端 smsMobileMasked,
  没有值时不拼号码,不拿患者档案里的号码冒充发送目标。
- 已签署的「在线预览(PDF)」与「只读副本」:
  预览弹窗复用 SigningDocumentInteractive(只读态),保证预览 / 工作台 / 导出 PDF
  三处版式一致;只读副本走 signing-pdf 新增的 watermark 选项,
  在 html2canvas 克隆体上平铺斜排水印,审计编号直接用任务号。
  水印行数按克隆体 scrollHeight 算,写死行数会让长文书下半部分没水印。
2026-09-26 19:45:40 +08:00

222 lines
5.1 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">
/**
* 已签署文书的在线预览(原型 openPdfView 的 mdPdfView)。
*
* 用的是**同一个**文书渲染组件(SigningDocumentInteractive,只读态),
* 而不是另写一套纸样:预览看到的版式必须与工作台里、与导出 PDF 的完全一致,
* 三处各写一份必然漂移。
*
* 底部两个出口对应原型结果卡上的「下载」「只读副本」:
* - 下载 PDF 原件:优先取服务端产物,没有产物时(演示环境)由前端现场生成;
* - 只读副本:前端加带审计编号的水印后导出,对外提供时用这份。
*/
import type { SigningTaskRecord } from '@/api/workbench/types'
import SigningDocumentInteractive from '@/components/signing/SigningDocumentInteractive.vue'
import SigningDocumentPreview from '@/components/signing/SigningDocumentPreview.vue'
defineProps<{
visible: boolean
task: SigningTaskRecord | null
/** 导出中:两个下载按钮一起禁用,避免重复点出多份 PDF */
exporting: boolean
}>()
const emit = defineEmits<{
close: []
download: []
'download-readonly-copy': []
}>()
</script>
<template>
<Teleport to="body">
<div v-if="visible && task" class="preview-mask" @click.self="emit('close')">
<section
class="preview-card"
role="dialog"
aria-modal="true"
aria-labelledby="signed-preview-title"
>
<header class="preview-header">
<strong id="signed-preview-title">在线预览(PDF)</strong>
<span class="preview-subtitle">
{{ task.documentName }} · {{ task.id }} · {{ task.patientName }}
</span>
<button type="button" class="preview-close" aria-label="关闭" @click="emit('close')">
×
</button>
</header>
<div class="preview-body">
<SigningDocumentInteractive
v-if="task.documentContentHtml"
:task="task"
:interactive="false"
/>
<SigningDocumentPreview v-else :task="task" />
</div>
<footer class="preview-footer">
<span class="preview-note">
正式文书以服务端生成的 PDF 原件为准;此处预览按任务固化的文书版本渲染。
</span>
<span class="preview-spacer" />
<button
type="button"
class="preview-button preview-button--ghost"
:disabled="exporting"
title="生成带水印与审计编号的副本,对外提供时使用"
@click="emit('download-readonly-copy')"
>
只读副本
</button>
<button
type="button"
class="preview-button"
:disabled="exporting"
@click="emit('download')"
>
{{ exporting ? '生成中…' : '下载 PDF 原件' }}
</button>
</footer>
</section>
</div>
</Teleport>
</template>
<style scoped>
.preview-mask {
position: fixed;
z-index: 60;
inset: 0;
display: flex;
padding: 18px;
background: rgb(9 40 52 / 45%);
}
.preview-card {
display: flex;
width: 900px;
max-width: 96vw;
height: calc(100vh - 36px);
min-height: 0;
flex-direction: column;
margin: auto;
background: #fff;
border-radius: 14px;
box-shadow: var(--sh-modal);
animation: preview-pop 0.18s ease;
}
.preview-header {
display: flex;
flex-shrink: 0;
gap: 10px;
align-items: baseline;
padding: 12px 18px;
border-bottom: 1px solid var(--line);
}
.preview-header strong {
font-size: 15px;
}
.preview-subtitle {
overflow: hidden;
color: var(--mut);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-close {
flex-shrink: 0;
padding: 0;
margin-left: auto;
color: var(--mut);
font-size: 20px;
line-height: 1;
background: transparent;
}
/* 唯一滚动区:文书在自己的框里滚,头尾常驻 */
.preview-body {
flex: 1;
min-height: 0;
padding: 14px;
background: #f2f6f8;
}
/* 让复用的文书外壳吃满弹窗高度,由它内部的 .sign-doc-body 滚动 */
.preview-body :deep(.sign-doc) {
height: 100%;
min-height: 0;
}
.preview-footer {
display: flex;
flex-shrink: 0;
gap: 10px;
align-items: center;
padding: 10px 18px;
color: var(--mut);
font-size: 12px;
background: #f8fbfc;
border-top: 1px solid var(--line);
border-radius: 0 0 14px 14px;
}
.preview-note {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-spacer {
flex: 1;
}
.preview-button {
flex-shrink: 0;
padding: 8px 16px;
color: #fff;
font-size: 13px;
font-weight: 700;
background: var(--brand);
border-radius: 7px;
}
.preview-button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.preview-button--ghost {
color: var(--brand);
background: #fff;
border: 1px solid var(--brand);
}
.preview-button--ghost:hover:not(:disabled) {
background: var(--brand-l);
}
@keyframes preview-pop {
from {
opacity: 0;
transform: scale(0.97) translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
@media (max-width: 680px) {
.preview-note {
display: none;
}
}
</style>