239 lines
7.5 KiB
Vue
239 lines
7.5 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 签署入口页。
|
||
*
|
||
* 三件事:
|
||
* ① 从地址栏取一次性 token 并**立即从地址栏清掉**(防进入历史记录 / Referrer 外泄);
|
||
* ② 消费 token 换投递凭证,再拉一次上下文(患者信息 + 文书 + 医生已填结果);
|
||
* ③ 按渠道与任务状态决定展示什么:正常入口 / 终态(已签、过期、作废、失败)。
|
||
*
|
||
* **拿不到文书正文时不放行签署** —— 让患者在没看过正式文书的情况下签名,
|
||
* 比签不了更严重。这是原有实现就定下的保守口径,继续保留。
|
||
*
|
||
* 刷新不丢链接:token 清掉后靠 `useSignFlow` 的 sessionStorage 会话恢复,
|
||
* 不在这里重新拼 token。
|
||
*/
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
|
||
import { initializeSignFlow, signingState } from '@/composables/useSignFlow'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const loading = ref(true)
|
||
|
||
/** 渠道 → 入口态说明。短信要核对身份,微信与手写板都属于「已经知道你是谁」的现场 */
|
||
const CHANNEL_ENTRY: Record<string, { label: string; note: string }> = {
|
||
SMS: {
|
||
label: '短信链接进入',
|
||
note: '为保障签署安全,请核对以下由医院短信通知带出的信息。',
|
||
},
|
||
WX: {
|
||
label: '公众号进入',
|
||
note: '已从医院服务号进入本次签署,核对信息后即可阅读文书。',
|
||
},
|
||
PAD: {
|
||
label: '现场设备进入',
|
||
note: '本设备为现场签署设备,核对信息后即可阅读文书。',
|
||
},
|
||
}
|
||
|
||
const channelEntry = computed(() => CHANNEL_ENTRY[signingState.channel] ?? CHANNEL_ENTRY.SMS)
|
||
|
||
const patientMeta = computed(() =>
|
||
[
|
||
signingState.sex,
|
||
signingState.age,
|
||
signingState.visitNo ? `就诊号 ${signingState.visitNo}` : '',
|
||
signingState.department,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' · '),
|
||
)
|
||
|
||
const signerLine = computed(() => {
|
||
const name = signingState.signerName || signingState.patientName
|
||
const relation = signingState.relation || '本人'
|
||
|
||
return `${name}(${relation})`
|
||
})
|
||
|
||
/** 任务终态:不是「待签署」就不该让患者继续走流程 */
|
||
const terminal = computed(() => {
|
||
switch (signingState.taskStatus) {
|
||
case 'SIGNED':
|
||
return {
|
||
eyebrow: '已完成',
|
||
title: '本次签署已完成',
|
||
copy: signingState.submittedAt
|
||
? `签署时间:${signingState.submittedAt}。无需重复签署。`
|
||
: '本次文书已完成签署,无需重复签署。',
|
||
}
|
||
case 'WAITING_CONFIRM':
|
||
return {
|
||
eyebrow: '患者已签署',
|
||
title: '签署已提交,等待科室确认',
|
||
copy: signingState.submittedAt
|
||
? `提交时间:${signingState.submittedAt}。无需重复签署。`
|
||
: '患者签名已提交,等待医务人员完成后续确认。无需重复签署。',
|
||
}
|
||
case 'GENERATING':
|
||
return {
|
||
eyebrow: '签名已提交',
|
||
title: '文书正在生成',
|
||
copy: '签名已收到,系统正在生成正式文书。无需重复签署。',
|
||
}
|
||
case 'EXPIRED':
|
||
return {
|
||
eyebrow: '链接已过期',
|
||
title: '签署链接已过期',
|
||
copy: '请勿重复尝试。请联系现场医务人员重新发起签署。',
|
||
}
|
||
case 'VOIDED':
|
||
return {
|
||
eyebrow: '已作废',
|
||
title: '本次签署任务已作废',
|
||
copy: '该任务已被医护端作废,如有疑问请联系现场医务人员。',
|
||
}
|
||
case 'FAILED':
|
||
return {
|
||
eyebrow: '处理失败',
|
||
title: '签署处理失败',
|
||
copy: '系统未能完成本次签署处理,请联系现场医务人员。',
|
||
}
|
||
default:
|
||
return null
|
||
}
|
||
})
|
||
|
||
const hasError = computed(
|
||
() => signingState.status === 'error' || signingState.status === 'blocked',
|
||
)
|
||
|
||
const canStartSigning = computed(
|
||
() => signingState.status === 'ready' && !terminal.value && signingState.documentLoaded,
|
||
)
|
||
|
||
function startSigning() {
|
||
if (!canStartSigning.value) {
|
||
return
|
||
}
|
||
|
||
void router.push('/consent')
|
||
}
|
||
|
||
function getQueryValue(value: unknown) {
|
||
if (Array.isArray(value)) {
|
||
return value[0] ?? ''
|
||
}
|
||
|
||
return typeof value === 'string' ? value : ''
|
||
}
|
||
|
||
async function initialize() {
|
||
const token =
|
||
getQueryValue(route.query.token) ||
|
||
getQueryValue(route.query.signToken) ||
|
||
getQueryValue(route.query.t)
|
||
|
||
if (token) {
|
||
const cleanQuery = { ...route.query }
|
||
delete cleanQuery.token
|
||
delete cleanQuery.signToken
|
||
delete cleanQuery.t
|
||
// 在发起接口请求前清理地址栏,避免一次性凭证进入历史记录、截图或后续 Referer。
|
||
await router.replace({ path: route.path, query: cleanQuery, hash: route.hash })
|
||
}
|
||
|
||
try {
|
||
await initializeSignFlow(token)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
void initialize()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<main class="mobile-shell">
|
||
<header class="mobile-brand">
|
||
<div class="mobile-brand-mark">签</div>
|
||
<div>
|
||
<strong>医签通</strong>
|
||
<span>医疗知情同意签署</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div v-if="loading" class="mobile-state-card">
|
||
<p class="eyebrow">安全验证</p>
|
||
<h1>正在验证签署链接</h1>
|
||
<p>请稍候,系统正在确认本次签署凭证。</p>
|
||
</div>
|
||
|
||
<section v-else-if="hasError" class="mobile-state-card mobile-state-card--error">
|
||
<p class="eyebrow">链接不可用</p>
|
||
<h1>无法继续签署</h1>
|
||
<p>{{ signingState.errorMessage }}</p>
|
||
<p class="secure-note">请不要重复尝试或转发链接,如有疑问请联系现场医务人员。</p>
|
||
</section>
|
||
|
||
<section v-else-if="terminal" class="mobile-state-card">
|
||
<p class="eyebrow">{{ terminal.eyebrow }}</p>
|
||
<h1>{{ terminal.title }}</h1>
|
||
<p>{{ terminal.copy }}</p>
|
||
<p class="secure-note">如有疑问请联系现场医务人员。</p>
|
||
</section>
|
||
|
||
<template v-else>
|
||
<section class="mobile-hero">
|
||
<p class="eyebrow">待签署文书</p>
|
||
<h1>{{ signingState.documentTitle }}</h1>
|
||
<p>{{ channelEntry.note }}</p>
|
||
</section>
|
||
|
||
<section class="mobile-card patient-card">
|
||
<div class="card-label">{{ channelEntry.label }}</div>
|
||
<div class="patient-row">
|
||
<div class="patient-avatar">{{ signingState.patientName.slice(0, 1) }}</div>
|
||
<div>
|
||
<strong>{{ signingState.patientName }}</strong>
|
||
<span>{{ patientMeta }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="task-number">任务编号:{{ signingState.taskNo }}</div>
|
||
</section>
|
||
|
||
<section class="mobile-card notice-card">
|
||
<div class="notice-icon">i</div>
|
||
<div>
|
||
<strong>请使用实际签署人的信息</strong>
|
||
<p>
|
||
本次签署人:{{
|
||
signerLine
|
||
}}。文书内容已由医务人员填写完成,如与实际不符请联系现场医务人员。
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="!signingState.documentLoaded" class="mobile-card integration-note">
|
||
<strong>暂未读取到正式文书</strong>
|
||
<p>{{ signingState.documentError }}</p>
|
||
</section>
|
||
|
||
<button
|
||
class="mobile-primary-button"
|
||
type="button"
|
||
:disabled="!canStartSigning"
|
||
@click="startSigning"
|
||
>
|
||
{{ signingState.documentLoaded ? '开始阅读并签署' : '等待文书接口接入' }}
|
||
</button>
|
||
|
||
<p class="secure-note">本页面仅用于本次知情同意签署,请勿转发签署链接。</p>
|
||
</template>
|
||
</main>
|
||
</template>
|