feat(患者签署): 接入签署上下文接口并补齐文书渲染与手写签名链路
This commit is contained in:
@@ -1,4 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 签署入口页。
|
||||
*
|
||||
* 三件事:
|
||||
* ① 从地址栏取一次性 token 并**立即从地址栏清掉**(防进入历史记录 / Referrer 外泄);
|
||||
* ② 消费 token 换投递凭证,再拉一次上下文(患者信息 + 文书 + 医生已填结果);
|
||||
* ③ 按渠道与任务状态决定展示什么:正常入口 / 终态(已签、过期、作废、失败)。
|
||||
*
|
||||
* **拿不到文书正文时不放行签署** —— 让患者在没看过正式文书的情况下签名,
|
||||
* 比签不了更严重。这是原有实现就定下的保守口径,继续保留。
|
||||
*
|
||||
* 刷新不丢链接:token 清掉后靠 `useSignFlow` 的 sessionStorage 会话恢复,
|
||||
* 不在这里重新拼 token。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -8,8 +22,91 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
|
||||
const isRealFlow = computed(() => signingState.mode === 'real')
|
||||
const canStartSigning = computed(() => signingState.status === 'ready')
|
||||
/** 渠道 → 入口态说明。短信要核对身份,微信与手写板都属于「已经知道你是谁」的现场 */
|
||||
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 '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)) {
|
||||
@@ -41,14 +138,6 @@ async function initialize() {
|
||||
}
|
||||
}
|
||||
|
||||
function startSigning() {
|
||||
if (!canStartSigning.value) {
|
||||
return
|
||||
}
|
||||
|
||||
void router.push('/consent')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void initialize()
|
||||
})
|
||||
@@ -70,43 +159,34 @@ onMounted(() => {
|
||||
<p>请稍候,系统正在确认本次签署凭证。</p>
|
||||
</div>
|
||||
|
||||
<template v-else-if="signingState.status === 'error' || signingState.status === 'blocked'">
|
||||
<section class="mobile-state-card mobile-state-card--error">
|
||||
<p class="eyebrow">链接不可用</p>
|
||||
<h1>无法继续签署</h1>
|
||||
<p>{{ signingState.errorMessage }}</p>
|
||||
<p class="secure-note">请不要重复尝试或转发链接,如有疑问请联系现场医务人员。</p>
|
||||
</section>
|
||||
</template>
|
||||
<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 v-if="isRealFlow">
|
||||
签署链接已验证。请先核对现场展示的正式文书内容,再继续确认签署人信息。
|
||||
</p>
|
||||
<p v-else>请确认以下任务信息,并在阅读文书后完成签署。</p>
|
||||
<p>{{ channelEntry.note }}</p>
|
||||
</section>
|
||||
|
||||
<section v-if="isRealFlow" class="mobile-card real-session-card">
|
||||
<div class="card-label">签署凭证</div>
|
||||
<div class="real-session-row">
|
||||
<span class="real-session-icon">✓</span>
|
||||
<div>
|
||||
<strong>一次性签署链接已验证</strong>
|
||||
<span>任务编号:{{ signingState.taskNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="mobile-card patient-card">
|
||||
<div class="card-label">患者信息</div>
|
||||
<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>{{ signingState.visitNo }}</span>
|
||||
<span>{{ patientMeta }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-number">任务编号:{{ signingState.taskNo }}</div>
|
||||
@@ -116,118 +196,27 @@ onMounted(() => {
|
||||
<div class="notice-icon">i</div>
|
||||
<div>
|
||||
<strong>请使用实际签署人的信息</strong>
|
||||
<p>如果由家属或监护人代签,请在下一步选择与患者的关系。</p>
|
||||
<p>
|
||||
本次签署人:{{ signerLine }}。文书内容已由医务人员填写完成,如与实际不符请联系现场医务人员。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="isRealFlow" class="mobile-card integration-note">
|
||||
<strong>正式文书内容暂未由公开接口返回</strong>
|
||||
<p>
|
||||
当前后端公开 Token
|
||||
接口只返回一次性上传凭证,没有提供患者端读取任务文书的接口。为避免在未阅读正式内容的情况下签署,本版本不会提交真实签名。
|
||||
</p>
|
||||
<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="isRealFlow"
|
||||
:disabled="!canStartSigning"
|
||||
@click="startSigning"
|
||||
>
|
||||
{{ isRealFlow ? '等待文书接口接入' : '开始阅读并签署' }}
|
||||
{{ signingState.documentLoaded ? '开始阅读并签署' : '等待文书接口接入' }}
|
||||
</button>
|
||||
|
||||
<p class="secure-note">本页面仅用于本次知情同意签署,请勿转发签署链接。</p>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-state-card,
|
||||
.integration-note,
|
||||
.real-session-card {
|
||||
padding: 20px;
|
||||
border: 1px solid #e0eceb;
|
||||
border-radius: 18px;
|
||||
background: rgb(255 255 255 / 86%);
|
||||
box-shadow: 0 12px 30px rgb(33 79 92 / 5%);
|
||||
}
|
||||
|
||||
.mobile-state-card {
|
||||
margin-top: 20vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobile-state-card h1 {
|
||||
margin: 0;
|
||||
color: #173b56;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.mobile-state-card p:not(.eyebrow) {
|
||||
margin: 12px 0 0;
|
||||
color: #788f9d;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mobile-state-card--error {
|
||||
border-color: #f0d8d8;
|
||||
}
|
||||
|
||||
.real-session-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.real-session-icon {
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: #1f776f;
|
||||
font-weight: 800;
|
||||
background: #dff3ee;
|
||||
border-radius: 12px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.real-session-row strong,
|
||||
.real-session-row span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.real-session-row strong {
|
||||
color: #31576c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.real-session-row div span {
|
||||
margin-top: 5px;
|
||||
color: #8aa0aa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.integration-note {
|
||||
margin-top: 14px;
|
||||
background: #fffaf2;
|
||||
border-color: #f0dfc1;
|
||||
}
|
||||
|
||||
.integration-note strong {
|
||||
color: #876126;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.integration-note p {
|
||||
margin: 7px 0 0;
|
||||
color: #927b58;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mobile-primary-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user