Files
xh-medical-sign-web/clinical-web/src/views/workbench/signing/components/SigningTaskDetail.vue
T

338 lines
8.9 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">
import { ref } from 'vue'
import type {
SigningAnswers,
SigningArtifact,
SigningTaskEvent,
SigningTaskRecord,
} from '@/api/workbench/types'
import type { SigningFieldProgress, SigningSignatureRequest } from '@/utils/signing-document'
import SigningDocumentInteractive from './SigningDocumentInteractive.vue'
import SigningDocumentPreview from './SigningDocumentPreview.vue'
import SigningArtifactList from './SigningArtifactList.vue'
import TaskAuditTimeline from './TaskAuditTimeline.vue'
import type { SigningTaskAction } from '../types'
defineProps<{
task: SigningTaskRecord | null
loading: boolean
auditEvents: SigningTaskEvent[]
auditLoading: boolean
auditError: boolean
allowLocalSigning: boolean
artifacts: SigningArtifact[]
artifactsLoading: boolean
artifactsError: boolean
padSessionStatus: string | null
padSessionLoading: boolean
canDeliveryWrite: boolean
canReopen: boolean
canVoid: boolean
canViewAudit: boolean
canViewArtifacts: boolean
canDownloadArtifact: boolean
/** 本次会话在文书里点击采集到的签名图,由签署页持有并回灌 */
signatureInjections: SigningAnswers
}>()
const emit = defineEmits<{
action: [action: SigningTaskAction]
downloadArtifact: [artifact: SigningArtifact]
selectionChange: [items: string[]]
fieldChange: [answers: SigningAnswers, progress: SigningFieldProgress]
signatureRequest: [request: SigningSignatureRequest]
}>()
const documentRef = ref<InstanceType<typeof SigningDocumentInteractive> | null>(null)
/** 供父级「导出 PDF」使用:转发到实际渲染文书的那一层 */
function getDocumentElement() {
return documentRef.value?.getDocumentElement() ?? null
}
defineExpose({ getDocumentElement })
</script>
<template>
<section class="detail-panel">
<div v-if="loading" class="detail-loading" aria-label="正在加载任务详情">
<span class="detail-loading__actions" />
<span class="detail-loading__paper" />
</div>
<template v-else-if="task">
<div class="task-actions">
<span class="task-action-meta">任务 {{ task.id }} · {{ task.patientName }}</span>
<template v-if="task.status === 'pending' && task.method === 'pad'">
<template v-if="allowLocalSigning">
<button type="button" class="action-button" @click="emit('action', 'pad-sign')">
✍ 手写板签署
</button>
<button
type="button"
class="action-button action-button--ghost"
@click="emit('action', 'switch-method')"
>
转为短信发送
</button>
</template>
<template v-else-if="canDeliveryWrite">
<button
type="button"
class="action-button"
:disabled="padSessionLoading"
@click="emit('action', 'create-pad-session')"
>
{{ padSessionLoading ? '创建会话中…' : '创建手写板会话' }}
</button>
<span v-if="padSessionStatus" class="action-hint">
会话状态:{{ padSessionStatus }};等待设备适配器读取 challenge
</span>
<span v-else class="action-hint">创建会话后仍需接入厂商设备适配器完成签名采集</span>
</template>
</template>
<template v-else-if="task.status === 'pending' && task.method === 'sms'">
<button
v-if="allowLocalSigning"
type="button"
class="action-button"
@click="emit('action', 'online-sign')"
>
📱 查看线上签署
</button>
<span v-else class="action-hint">线上签署页面接口待接入</span>
<button
v-if="allowLocalSigning"
type="button"
class="action-button action-button--ghost"
@click="emit('action', 'switch-method')"
>
转手写板签署
</button>
<button
v-if="canDeliveryWrite"
type="button"
class="action-button action-button--ghost"
@click="emit('action', 'resend-sms')"
>
重发短信
</button>
</template>
<template v-else-if="task.status === 'expired' && canReopen">
<button type="button" class="action-button" @click="emit('action', 'reopen')">
↻ 重新发起
</button>
</template>
<template v-else-if="task.status === 'signed' && 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"
@click="emit('action', 'download-signature')"
>
签名原图
</button>
<button
type="button"
class="action-button action-button--ghost"
@click="emit('action', 'print')"
>
打印
</button>
</template>
<button
v-if="
canVoid &&
(task.status === 'pending' ||
task.status === 'expired' ||
task.status === 'signing' ||
task.status === 'failed')
"
type="button"
class="action-button action-button--danger"
@click="emit('action', 'void')"
>
作废
</button>
</div>
<!-- 任务接口只返回模板版本哈希,正文由 API 层按哈希补回;
补不到时如实提示并给出占位版式,不退化成"看起来像真文书"的假纸样 -->
<SigningDocumentInteractive
v-if="task.documentContentHtml"
ref="documentRef"
:task="task"
:interactive="allowLocalSigning && task.status !== 'signed' && task.status !== 'void'"
:signature-injections="signatureInjections"
@selection-change="emit('selectionChange', $event)"
@field-change="(answers, progress) => emit('fieldChange', answers, progress)"
@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"
:loading="artifactsLoading"
:error="artifactsError"
:can-download="canDownloadArtifact"
@download="emit('downloadArtifact', $event)"
/>
</template>
<div v-else class="detail-empty">
<span aria-hidden="true">📋</span>
<p>请从左侧列表选择一个签署任务</p>
</div>
</section>
</template>
<style scoped>
.detail-panel {
flex: 1;
min-width: 0;
}
.task-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 14px;
}
.task-action-meta {
align-self: center;
margin-right: auto;
color: var(--mut);
font-size: 12px;
}
.action-hint {
padding: 8px 12px;
color: var(--mut);
font-size: 12px;
background: #f7fafb;
border: 1px dashed var(--line);
border-radius: 7px;
}
.action-button {
flex-shrink: 0;
padding: 8px 16px;
color: #fff;
font-size: 13px;
font-weight: 700;
background: var(--brand);
border-radius: 7px;
transition: background 0.15s ease;
}
.action-button:hover {
background: var(--brand-d);
}
.action-button--ghost {
color: var(--brand);
background: #fff;
border: 1px solid var(--brand);
}
.action-button--ghost:hover {
background: var(--brand-l);
}
.action-button--danger {
color: var(--err);
background: #fff;
border: 1px solid var(--err);
}
.action-button--danger:hover {
background: var(--err-l);
}
.detail-empty {
display: grid;
min-height: 560px;
padding: 40px 24px;
color: var(--mut);
text-align: center;
background: var(--card);
border-radius: var(--r);
box-shadow: var(--sh);
place-items: center;
align-content: center;
}
.detail-empty span {
margin-bottom: 8px;
font-size: 34px;
opacity: 0.5;
}
.detail-empty p {
margin: 0;
font-size: 13px;
}
.detail-loading {
display: flex;
min-height: 680px;
flex-direction: column;
gap: 14px;
}
.detail-loading span {
display: block;
background: linear-gradient(90deg, #edf3f5 25%, #f7fafb 37%, #edf3f5 63%);
background-size: 400% 100%;
border-radius: var(--r);
box-shadow: var(--sh);
animation: skeleton-shimmer 1.4s ease infinite;
}
.detail-loading__actions {
height: 38px;
}
.detail-loading__paper {
flex: 1;
min-height: 560px;
}
@keyframes skeleton-shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
@media (max-width: 700px) {
.task-action-meta {
width: 100%;
}
}
</style>