222 lines
5.5 KiB
Vue
222 lines
5.5 KiB
Vue
<script setup lang="ts">
|
||
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 isRealFlow = computed(() => signingState.mode === 'real')
|
||
const canStartSigning = computed(() => signingState.status === 'ready')
|
||
|
||
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)
|
||
|
||
await initializeSignFlow(token)
|
||
loading.value = false
|
||
}
|
||
|
||
function startSigning() {
|
||
if (!canStartSigning.value) {
|
||
return
|
||
}
|
||
|
||
void router.push('/consent')
|
||
}
|
||
|
||
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>
|
||
|
||
<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>
|
||
|
||
<template v-else>
|
||
<section class="mobile-hero">
|
||
<p class="eyebrow">待签署文书</p>
|
||
<h1>{{ signingState.documentTitle }}</h1>
|
||
<p v-if="isRealFlow">
|
||
签署链接已验证。请先核对现场展示的正式文书内容,再继续确认签署人信息。
|
||
</p>
|
||
<p v-else>请确认以下任务信息,并在阅读文书后完成签署。</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>
|
||
<div class="patient-row">
|
||
<div class="patient-avatar">{{ signingState.patientName.slice(0, 1) }}</div>
|
||
<div>
|
||
<strong>{{ signingState.patientName }}</strong>
|
||
<span>{{ signingState.visitNo }}</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>如果由家属或监护人代签,请在下一步选择与患者的关系。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="isRealFlow" class="mobile-card integration-note">
|
||
<strong>正式文书内容暂未由公开接口返回</strong>
|
||
<p>
|
||
当前后端公开 Token
|
||
接口只返回一次性上传凭证,没有提供患者端读取任务文书的接口。为避免在未阅读正式内容的情况下签署,本版本不会提交真实签名。
|
||
</p>
|
||
</section>
|
||
|
||
<button
|
||
class="mobile-primary-button"
|
||
type="button"
|
||
:disabled="isRealFlow"
|
||
@click="startSigning"
|
||
>
|
||
{{ isRealFlow ? '等待文书接口接入' : '开始阅读并签署' }}
|
||
</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>
|