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