Files
xh-medical-sign-web/patient-h5/src/views/entry/EntryView.vue
T

239 lines
7.5 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">
/**
* 签署入口页。
*
* 三件事:
* ① 从地址栏取一次性 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>