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 算,写死行数会让长文书下半部分没水印。
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 关联处方(原型 V10.2 的 rxCard)。
|
||||
*
|
||||
* 处方由 HIS 带出、按就诊号自动匹配,签署工作台只做展示:
|
||||
* 默认收起,点表头展开 7 列明细(处方号 / 类别 / 药品名称 / 规格 / 数量 / 用法用量 / 状态)。
|
||||
*
|
||||
* **真实模式整卡不渲染**:后端没有处方接口,`readTaskPrescriptions` 会返回空数组。
|
||||
* 这种情况下显示一张空表或一条"暂无数据"都比不显示更糟 ——
|
||||
* 医务人员会以为系统查过了、确实没有处方,而实际是压根没查。
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { readTaskPrescriptions } from '@/api/workbench/prescriptions'
|
||||
import type { SigningTaskRecord } from '@/api/workbench/types'
|
||||
|
||||
const props = defineProps<{
|
||||
task: SigningTaskRecord
|
||||
}>()
|
||||
|
||||
const expanded = ref(false)
|
||||
|
||||
const prescriptions = computed(() => readTaskPrescriptions(props.task))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="prescriptions.length" class="rx-card" aria-labelledby="rx-title">
|
||||
<button
|
||||
type="button"
|
||||
class="rx-head"
|
||||
:class="{ 'is-open': expanded }"
|
||||
:aria-expanded="expanded"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<span class="rx-arrow" aria-hidden="true">▸</span>
|
||||
<strong id="rx-title">关联处方</strong>
|
||||
<span class="rx-count">{{ prescriptions.length }} 条处方</span>
|
||||
<span class="rx-hint">本次就诊({{ task.visitNo }})开立 · 点击展开 / 收起</span>
|
||||
<span class="rx-spacer" />
|
||||
<span class="rx-note">处方保存 3 年 · 与知情同意签署记录相互关联</span>
|
||||
</button>
|
||||
|
||||
<div v-show="expanded" class="rx-body">
|
||||
<table class="rx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>处方号</th>
|
||||
<th>类别</th>
|
||||
<th>药品名称</th>
|
||||
<th>规格</th>
|
||||
<th>数量</th>
|
||||
<th>用法用量</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in prescriptions" :key="item.no">
|
||||
<td class="rx-mono">{{ item.no }}</td>
|
||||
<td>{{ item.category }}</td>
|
||||
<td>{{ item.drug }}</td>
|
||||
<td>{{ item.spec }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td class="rx-usage">{{ item.usage }}</td>
|
||||
<td>
|
||||
<span class="rx-status">{{ item.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="rx-footnote">
|
||||
处方由 HIS 带出并按就诊号自动匹配;知情同意书签署完成后方可计费 / 取药,全过程留痕可审计。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rx-card {
|
||||
padding: 12px 16px;
|
||||
margin-top: 14px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.rx-head {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--ink);
|
||||
text-align: left;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rx-head strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 展开时三角转 90°,与原型 .rx-head.open .ar 一致 */
|
||||
.rx-arrow {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
color: var(--mut);
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.rx-head.is-open .rx-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.rx-count {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 9px;
|
||||
color: var(--brand);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--brand-l);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.rx-hint,
|
||||
.rx-note {
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.rx-note {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rx-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rx-body {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.rx-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.rx-table th {
|
||||
padding: 8px 10px;
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
background: #f5f9fa;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.rx-table td {
|
||||
padding: 8px 10px;
|
||||
color: var(--ink);
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #eef4f6;
|
||||
}
|
||||
|
||||
.rx-table tbody tr:hover td {
|
||||
background: #f7fbfc;
|
||||
}
|
||||
|
||||
.rx-mono {
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rx-usage {
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rx-status {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
color: var(--ok);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--ok-l);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.rx-footnote {
|
||||
margin: 6px 0 0;
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.rx-note,
|
||||
.rx-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 窄屏表格自己横向滚,不把整页撑宽 */
|
||||
.rx-body {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.rx-table {
|
||||
min-width: 640px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<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>
|
||||
@@ -14,6 +14,7 @@ import { formatSigningMethodTitle, readSigningMethod } from '@/utils/signing-met
|
||||
import SigningDocumentInteractive from '@/components/signing/SigningDocumentInteractive.vue'
|
||||
import SigningDocumentPreview from '@/components/signing/SigningDocumentPreview.vue'
|
||||
import TaskAuditTimeline from '@/components/signing/TaskAuditTimeline.vue'
|
||||
import LinkedPrescriptionCard from './LinkedPrescriptionCard.vue'
|
||||
import SigningArtifactList from './SigningArtifactList.vue'
|
||||
import type { SigningTaskAction } from '../types'
|
||||
|
||||
@@ -68,6 +69,25 @@ function isOnsiteMethod(method: SigningMethod): boolean {
|
||||
return method === 'pad' || method === 'ipad'
|
||||
}
|
||||
|
||||
/**
|
||||
* 线上方式(短信 / 公众号)的等待态提示(原型 renderPreview 里那条 `.tg-wait`)。
|
||||
*
|
||||
* 说的是「任务正等患者签」,这是任务状态本身能确认的事;
|
||||
* 短信再把发送目标带出来 —— 号码取后端返回的**脱敏值**,
|
||||
* 后端没给就只写「已发送」,不拿患者档案里的号码冒充发送目标。
|
||||
*/
|
||||
function waitingHint(task: SigningTaskRecord): string {
|
||||
const { icon, short } = readSigningMethod(task.method)
|
||||
|
||||
if (task.method === 'wx') {
|
||||
return `${icon} ${short}签署消息已推送 · 等待患者签署…`
|
||||
}
|
||||
|
||||
const mobile = task.smsMobileMasked?.trim()
|
||||
|
||||
return `${icon} ${short}${mobile ? `已发送至 ${mobile}` : '已发送'} · 等待患者签署…`
|
||||
}
|
||||
|
||||
/** 供父级「导出 PDF」使用:转发到实际渲染文书的那一层 */
|
||||
function getDocumentElement() {
|
||||
return documentRef.value?.getDocumentElement() ?? null
|
||||
@@ -113,6 +133,8 @@ defineExpose({ getDocumentElement })
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 线上方式先给等待态:这条任务此刻等的是患者,不是操作人 -->
|
||||
<span class="action-wait">{{ waitingHint(task) }}</span>
|
||||
<button
|
||||
v-if="allowLocalSigning"
|
||||
type="button"
|
||||
@@ -152,28 +174,48 @@ defineExpose({ getDocumentElement })
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="task.status === 'signed' && canDownloadArtifact">
|
||||
<!-- 已签署:预览是主入口(原型主按钮就是「在线预览(PDF)」),
|
||||
下载类动作在后;只读副本带水印与审计编号,对外提供时用那一份 -->
|
||||
<template v-else-if="task.status === 'signed'">
|
||||
<button
|
||||
v-if="canViewArtifacts"
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'download-pdf')"
|
||||
class="action-button"
|
||||
@click="emit('action', 'preview-signed')"
|
||||
>
|
||||
下载签署后 PDF
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'download-signature')"
|
||||
>
|
||||
签名原图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'print')"
|
||||
>
|
||||
打印
|
||||
在线预览(PDF)
|
||||
</button>
|
||||
<template v-if="canDownloadArtifact">
|
||||
<button
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'download-pdf')"
|
||||
>
|
||||
下载 PDF 原件
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
title="生成带水印与审计编号的副本,对外提供时使用"
|
||||
@click="emit('action', 'download-readonly-copy')"
|
||||
>
|
||||
只读副本
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'download-signature')"
|
||||
>
|
||||
签名原图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'print')"
|
||||
>
|
||||
打印
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<button
|
||||
@@ -208,12 +250,7 @@ defineExpose({ getDocumentElement })
|
||||
@signature-request="emit('signatureRequest', $event)"
|
||||
/>
|
||||
<SigningDocumentPreview v-else :task="task" />
|
||||
<TaskAuditTimeline
|
||||
v-if="canViewAudit"
|
||||
:events="auditEvents"
|
||||
:loading="auditLoading"
|
||||
:error="auditError"
|
||||
/>
|
||||
<!-- 顺序照原型:文书 → 签署文件(归档结果)→ 关联处方 → 操作留痕 -->
|
||||
<SigningArtifactList
|
||||
v-if="canViewArtifacts"
|
||||
:artifacts="artifacts"
|
||||
@@ -222,6 +259,13 @@ defineExpose({ getDocumentElement })
|
||||
:can-download="canDownloadArtifact"
|
||||
@download="emit('downloadArtifact', $event)"
|
||||
/>
|
||||
<LinkedPrescriptionCard :task="task" />
|
||||
<TaskAuditTimeline
|
||||
v-if="canViewAudit"
|
||||
:events="auditEvents"
|
||||
:loading="auditLoading"
|
||||
:error="auditError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -275,6 +319,17 @@ defineExpose({ getDocumentElement })
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/* 等待态:原型 .tag.tg-wait(信息蓝),与旁边的主按钮区分开 */
|
||||
.action-wait {
|
||||
align-self: center;
|
||||
padding: 5px 11px;
|
||||
color: var(--info);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
background: var(--info-l);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
|
||||
@@ -47,6 +47,24 @@ function getPendingLabel(task: SigningTaskRecord) {
|
||||
return readPendingTag(task.method).text
|
||||
}
|
||||
|
||||
/**
|
||||
* 代签标签(原型 `.vtag.chk`):只有签署人不是「本人」时才显示。
|
||||
*
|
||||
* 与原型的一处差别:关系为空时**不输出空括号**(原型会渲染成「家属代签()」),
|
||||
* 空括号看着像渲染出错,也会让人以为关系字段丢了。
|
||||
*/
|
||||
function getSignerTag(task: SigningTaskRecord) {
|
||||
const type = task.signerType?.trim()
|
||||
|
||||
if (!type || type === '本人') {
|
||||
return ''
|
||||
}
|
||||
|
||||
const relation = task.signerRelation?.trim()
|
||||
|
||||
return relation ? `${type}代签(${relation})` : `${type}代签`
|
||||
}
|
||||
|
||||
function getVisitClass(type: SigningTaskRecord['visitType']) {
|
||||
return type === '住院'
|
||||
? 'visit-tag--hospital'
|
||||
@@ -94,6 +112,7 @@ function getVisitClass(type: SigningTaskRecord['visitType']) {
|
||||
<span>{{ task.patientId }}</span>
|
||||
<span>{{ task.updatedAt }}</span>
|
||||
<span>{{ getMethodLabel(task) }}</span>
|
||||
<span v-if="getSignerTag(task)" class="signer-tag">{{ getSignerTag(task) }}</span>
|
||||
</span>
|
||||
<span class="task-row task-row--visit">
|
||||
<span class="visit-tag" :class="getVisitClass(task.visitType)">{{ task.visitType }}</span>
|
||||
@@ -289,6 +308,20 @@ function getVisitClass(type: SigningTaskRecord['visitType']) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 代签标签:原型 .titem .vtag.chk 的橙底(#fff3e0 / #d9821f)。
|
||||
它与方式标签并排出现,因此不能继承 .task-row--meta 的灰字色 */
|
||||
.signer-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
color: #d9821f;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
background: #fff3e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.visit-tag--outpatient {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
|
||||
@@ -49,6 +49,7 @@ import { downloadSigningPdf } from '@/utils/signing-pdf'
|
||||
|
||||
import OnlineSigningDialog from './components/OnlineSigningDialog.vue'
|
||||
import SignaturePadDialog from './components/SignaturePadDialog.vue'
|
||||
import SignedDocumentDialog from './components/SignedDocumentDialog.vue'
|
||||
import SigningFilterBar from './components/SigningFilterBar.vue'
|
||||
import SigningTaskDetail from './components/SigningTaskDetail.vue'
|
||||
import SigningTaskList from './components/SigningTaskList.vue'
|
||||
@@ -128,6 +129,8 @@ const newDialogVisible = ref(false)
|
||||
const signatureDialogVisible = ref(false)
|
||||
const onlineDialogVisible = ref(false)
|
||||
const voidDialogVisible = ref(false)
|
||||
/** 已签署文书的在线预览弹窗(原型 openPdfView) */
|
||||
const signedPreviewVisible = ref(false)
|
||||
const taskDetailRef = ref<InstanceType<typeof SigningTaskDetail> | null>(null)
|
||||
const pdfExporting = ref(false)
|
||||
|
||||
@@ -271,6 +274,8 @@ async function loadTaskDetail(id: string | null) {
|
||||
fieldSignatureInjections.value = {}
|
||||
pendingSignatureField.value = null
|
||||
fieldSignatureDialogVisible.value = false
|
||||
// 预览弹窗跟着任务走:换任务时留在上一份文书的预览上会让人以为看的是当前任务
|
||||
signedPreviewVisible.value = false
|
||||
|
||||
if (!id) {
|
||||
selectedTask.value = null
|
||||
@@ -531,7 +536,11 @@ async function handleTaskAction(action: SigningTaskAction, targetMethod?: Signin
|
||||
(action === 'resend-sms' && canDeliveryWrite.value) ||
|
||||
(action === 'reopen' && canReopenTask.value) ||
|
||||
(action === 'void' && canVoidTask.value) ||
|
||||
((action === 'download-pdf' || action === 'download-signature' || action === 'print') &&
|
||||
(action === 'preview-signed' && canViewArtifacts.value) ||
|
||||
((action === 'download-pdf' ||
|
||||
action === 'download-signature' ||
|
||||
action === 'download-readonly-copy' ||
|
||||
action === 'print') &&
|
||||
canDownloadArtifacts.value)
|
||||
|
||||
if (!allowed) {
|
||||
@@ -619,6 +628,16 @@ async function handleTaskAction(action: SigningTaskAction, targetMethod?: Signin
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'preview-signed') {
|
||||
signedPreviewVisible.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'download-readonly-copy') {
|
||||
await exportReadonlyCopy()
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'print') {
|
||||
ElMessage.info('原型演示:打印服务待接入')
|
||||
return
|
||||
@@ -695,6 +714,43 @@ async function exportSignedPdf() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只读副本:在按任务固化版式渲染的结果上加「水印 + 审计编号」再导出。
|
||||
*
|
||||
* 与原件的区别是用途:原件是对内留存的正式凭证,只读副本是对外提供的那一份,
|
||||
* 因此必须能一眼看出不是原件。水印直接烙进截图,改不掉文本层。
|
||||
* 审计编号直接用任务号 —— 它是全链路可追溯的真实标识,
|
||||
* 另外编一个前端生成的编号只会让追溯断在这里。
|
||||
*/
|
||||
async function exportReadonlyCopy() {
|
||||
const task = selectedTask.value
|
||||
const element = taskDetailRef.value?.getDocumentElement()
|
||||
|
||||
if (!task || !element) {
|
||||
ElMessage.info('该任务没有固化的文书正文,无法生成只读副本')
|
||||
return
|
||||
}
|
||||
|
||||
if (pdfExporting.value) {
|
||||
return
|
||||
}
|
||||
|
||||
pdfExporting.value = true
|
||||
|
||||
try {
|
||||
await downloadSigningPdf({
|
||||
element,
|
||||
fileName: `${task.documentName}-${task.id}-只读副本`,
|
||||
watermark: `只读副本 · 审计编号 ${task.id}`,
|
||||
})
|
||||
ElMessage.success('只读副本已生成(含水印与审计编号)')
|
||||
} catch {
|
||||
ElMessage.error('只读副本生成失败,请稍后重试')
|
||||
} finally {
|
||||
pdfExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadArtifact(artifact: SigningArtifact) {
|
||||
if (!canDownloadArtifacts.value) {
|
||||
ElMessage.warning('当前账号没有下载签署文件的权限')
|
||||
@@ -917,6 +973,14 @@ onMounted(() => {
|
||||
@complete="completeOnlineSignature"
|
||||
@resend="handleOnlineResend"
|
||||
/>
|
||||
<SignedDocumentDialog
|
||||
:visible="signedPreviewVisible"
|
||||
:task="selectedTask"
|
||||
:exporting="pdfExporting"
|
||||
@close="signedPreviewVisible = false"
|
||||
@download="handleTaskAction('download-pdf')"
|
||||
@download-readonly-copy="exportReadonlyCopy"
|
||||
/>
|
||||
<VoidTaskDialog
|
||||
:visible="voidDialogVisible"
|
||||
:task="selectedTask"
|
||||
|
||||
@@ -49,6 +49,10 @@ export type SigningTaskAction =
|
||||
| 'void'
|
||||
| 'download-pdf'
|
||||
| 'download-signature'
|
||||
/** 在线预览已签署文书(原型 openPdfView 的弹窗预览) */
|
||||
| 'preview-signed'
|
||||
/** 生成带水印与审计编号的只读副本(原型「只读副本」) */
|
||||
| 'download-readonly-copy'
|
||||
| 'print'
|
||||
|
||||
export type SigningPadSessionStatus = SignDeliveryStatus | null
|
||||
|
||||
Reference in New Issue
Block a user