feat(线上签署): 模拟器支持短信/公众号双入口并补齐身份校验与完整文书展示
This commit is contained in:
@@ -1,16 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
/**
|
||||
* 患者签署页(原型两套:短信走 `#mdPhone`,公众号走 `#mdWxPt`)。
|
||||
*
|
||||
* 为什么合成一个组件而不是两个:手机壳、状态栏、页头页脚、手写板这几层完全一样,
|
||||
* 差别只在**入口形态**与**看到的内容**,所以用 `variant` 分开表达:
|
||||
* - `sms`(短信 H5):短信气泡 → **身份校验**(就诊号 + 姓名)→ 文书摘要 + 手写签名;
|
||||
* - `wx`(公众号):微信会话(服务通知)→ **完整文书正文** + 手写签名。
|
||||
*
|
||||
* 两处差异是原型就有的,不是实现偷懒:短信是「凭链接进来、要先核对身份」,
|
||||
* 公众号是「从医院服务号点进来、会话里已经带了上下文」。
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { SigningTaskRecord } from '@/api/workbench/types'
|
||||
import DocumentPaper from '@/components/document/DocumentPaper.vue'
|
||||
import type { SigningDocumentData } from '@/utils/signing-document'
|
||||
|
||||
import SignatureCanvas from './SignatureCanvas.vue'
|
||||
|
||||
type OnlineStage = 'message' | 'signing'
|
||||
type OnlineStage = 'message' | 'verify' | 'signing'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
task: SigningTaskRecord | null
|
||||
}>()
|
||||
/** `sms` = 短信 H5 页(原型 #mdPhone);`wx` = 公众号页(原型 #mdWxPt) */
|
||||
variant?: 'sms' | 'wx'
|
||||
}>(),
|
||||
{ variant: 'sms' },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
@@ -22,6 +40,8 @@ const stage = ref<OnlineStage>('message')
|
||||
const signatureDataUrl = ref<string | null>(null)
|
||||
const signatureCanvas = ref<{ clear: () => void } | null>(null)
|
||||
|
||||
const isWx = computed(() => props.variant === 'wx')
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
@@ -32,7 +52,59 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
function openLink() {
|
||||
/** 弹窗标题:原型两个弹窗的页头文案 */
|
||||
const dialogTitle = computed(() =>
|
||||
isWx.value ? '公众号签署 · 患者微信端' : '线上签署 · 患者移动端 H5',
|
||||
)
|
||||
|
||||
/** 手机页头文案:短信三态、公众号两态(原型 phTitle / wxpTitle) */
|
||||
const phoneTitle = computed(() => {
|
||||
if (stage.value === 'verify') {
|
||||
return '身份校验'
|
||||
}
|
||||
|
||||
if (stage.value === 'signing') {
|
||||
return '患者签署'
|
||||
}
|
||||
|
||||
return isWx.value ? '微信' : '短信'
|
||||
})
|
||||
|
||||
/** 页脚主按钮(原型 phGo / wxpGo) */
|
||||
const primaryLabel = computed(() => {
|
||||
if (stage.value === 'message') {
|
||||
return isWx.value ? '打开签署页' : '打开签署链接'
|
||||
}
|
||||
|
||||
return isWx.value ? '等待患者操作…' : '等待患者校验…'
|
||||
})
|
||||
|
||||
/** 入口态页脚提示(原型两处写法不同) */
|
||||
const entryHint = computed(() => (isWx.value ? '↑ 患者收到的公众号服务通知' : '↑ 患者收到短信'))
|
||||
|
||||
/** 文书回填上下文:与工作台详情同口径,HIS 字段才填得上 */
|
||||
const documentData = computed<SigningDocumentData>(() => ({
|
||||
patientName: props.task?.patientName,
|
||||
sex: props.task?.sex,
|
||||
age: props.task?.age,
|
||||
visitNo: props.task?.visitNo,
|
||||
visitDate: props.task?.visitDate,
|
||||
signedAt: props.task?.signedAt,
|
||||
signatureDataUrl: props.task?.signatureDataUrl,
|
||||
selectedItems: props.task?.selectedItems,
|
||||
fieldAnswers: props.task?.fieldAnswers,
|
||||
}))
|
||||
|
||||
function advance() {
|
||||
if (stage.value !== 'message') {
|
||||
return
|
||||
}
|
||||
|
||||
// 短信要先过身份校验;公众号直接从会话进签署页(原型 phOpen / wxPtOpen)
|
||||
stage.value = isWx.value ? 'signing' : 'verify'
|
||||
}
|
||||
|
||||
function startSigning() {
|
||||
stage.value = 'signing'
|
||||
}
|
||||
|
||||
@@ -58,7 +130,7 @@ function clearSignature() {
|
||||
aria-labelledby="online-title"
|
||||
>
|
||||
<header class="dialog-header">
|
||||
<strong id="online-title">线上签署 · 患者手机模拟</strong>
|
||||
<strong id="online-title">{{ dialogTitle }}</strong>
|
||||
<span>{{ task.patientName }} · {{ task.documentName }}</span>
|
||||
<button type="button" class="dialog-close" aria-label="关闭" @click="emit('close')">
|
||||
×
|
||||
@@ -69,27 +141,75 @@ function clearSignature() {
|
||||
<div class="phone-frame">
|
||||
<div class="phone-screen">
|
||||
<div class="phone-status"><span>9:41</span><span>5G ▮▮▮</span></div>
|
||||
<div class="phone-header">{{ stage === 'message' ? '短信' : '患者签署' }}</div>
|
||||
<div class="phone-header" :class="{ 'is-wx': isWx && stage === 'message' }">
|
||||
{{ phoneTitle }}
|
||||
</div>
|
||||
<div class="phone-body">
|
||||
<!-- 入口态:短信气泡 / 微信会话 -->
|
||||
<template v-if="stage === 'message'">
|
||||
<div v-if="isWx" class="wx-chat-head">
|
||||
<span class="wx-avatar" aria-hidden="true">医</span>
|
||||
<div class="wx-account">
|
||||
<strong>【xx医院】</strong>
|
||||
<span>服务通知</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-bubble">
|
||||
<strong>【xx医院】</strong>
|
||||
<p>
|
||||
尊敬的{{ task.patientName }},您有一份《{{
|
||||
task.documentName
|
||||
}}》待签署,请点击下方链接完成签署,链接 48 小时内有效。
|
||||
<p v-if="isWx">
|
||||
{{ task.patientName }},您有一份《{{ task.documentName }}》待签署,
|
||||
请点击进入签署页完成知情同意书签署。
|
||||
</p>
|
||||
<span class="sign-link">https://s.hosp.cn/s/{{ task.id }}</span>
|
||||
<p v-else>
|
||||
尊敬的{{ task.patientName }},您有一份《{{ task.documentName }}》待签署,
|
||||
请点击下方链接完成签署,链接 48 小时内有效。
|
||||
</p>
|
||||
<span class="sign-link">
|
||||
{{ isWx ? `mpsign.hosp.cn/wx/${task.id}` : `https://s.hosp.cn/s/${task.id}` }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 短信独有:身份校验(原型 phRender 的 phStage===1) -->
|
||||
<template v-else-if="stage === 'verify'">
|
||||
<div class="verify-card">
|
||||
<strong>🔐 签署前身份校验</strong>
|
||||
<p class="verify-tip">为保障签署安全,请核对以下由医院短信通知带出的信息:</p>
|
||||
<div class="verify-row">
|
||||
<span>就诊号<em>(已带出)</em></span>
|
||||
<b class="is-mono">{{ task.visitNo }}</b>
|
||||
</div>
|
||||
<div class="verify-row">
|
||||
<span>患者姓名</span>
|
||||
<b>{{ task.patientName }}</b>
|
||||
</div>
|
||||
<p class="verify-ok">
|
||||
校验通过:就诊号与短信发送对象一致。
|
||||
<span class="verify-note"
|
||||
>(证件后四位校验需要后端提供证件号码,本期未接入)</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 患者签署 -->
|
||||
<template v-else>
|
||||
<div class="mobile-document">
|
||||
<strong>{{ task.documentName }}</strong>
|
||||
<span>{{ task.patientName }} · {{ task.patientId }}</span>
|
||||
<p>
|
||||
<span v-if="task.visitNo">就诊号 {{ task.visitNo }}</span>
|
||||
</div>
|
||||
<!-- 公众号页把**完整文书**摆出来(原型 #mdWxPt 里就是整篇 docBody) -->
|
||||
<div v-if="isWx" class="mobile-paper">
|
||||
<DocumentPaper
|
||||
:content-html="task.documentContentHtml ?? ''"
|
||||
:data="documentData"
|
||||
:interactive="false"
|
||||
/>
|
||||
</div>
|
||||
<p v-else class="mobile-summary">
|
||||
本人已充分理解本检查/治疗的医疗风险、替代方案及可能的不良后果,自愿接受并签署确认。签名图片与文档原件一并存档。
|
||||
</p>
|
||||
</div>
|
||||
<div class="mobile-signature">
|
||||
<span>请手写签名:</span>
|
||||
<SignatureCanvas
|
||||
@@ -101,6 +221,7 @@ function clearSignature() {
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="stage === 'signing'" class="phone-footer">
|
||||
<button
|
||||
type="button"
|
||||
@@ -109,21 +230,47 @@ function clearSignature() {
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
<button type="button" class="phone-button" @click="complete">确认签署</button>
|
||||
<button
|
||||
type="button"
|
||||
class="phone-button"
|
||||
:class="{ 'is-wx': isWx }"
|
||||
@click="complete"
|
||||
>
|
||||
确认签署
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="phone-footer phone-footer--hint">↑ 患者收到短信</div>
|
||||
<div v-else class="phone-footer phone-footer--hint">{{ entryHint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-footer">
|
||||
<span>模拟患者收到短信 → 打开链接 → 查看文档 → 手写签署的全过程</span>
|
||||
<span class="footer-note">
|
||||
{{
|
||||
isWx
|
||||
? '模拟患者收到服务通知 → 打开签署页 → 查看文书 → 手写签署'
|
||||
: '模拟患者收到短信 → 打开链接 → 身份校验 → 手写签署'
|
||||
}}
|
||||
</span>
|
||||
<span class="footer-spacer" />
|
||||
<button type="button" class="action-button action-button--ghost" @click="emit('resend')">
|
||||
重发短信
|
||||
{{ isWx ? '重发推送' : '重发短信' }}
|
||||
</button>
|
||||
<button v-if="stage === 'message'" type="button" class="action-button" @click="openLink">
|
||||
模拟打开链接
|
||||
<button
|
||||
v-if="stage === 'verify'"
|
||||
type="button"
|
||||
class="action-button"
|
||||
@click="startSigning"
|
||||
>
|
||||
身份校验通过 · 进入签署
|
||||
</button>
|
||||
<button
|
||||
v-else-if="stage === 'message'"
|
||||
type="button"
|
||||
class="action-button"
|
||||
@click="advance"
|
||||
>
|
||||
{{ primaryLabel }}
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
@@ -226,6 +373,11 @@ function clearSignature() {
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
/* 微信会话那一屏用微信绿,一眼能看出不是短信 */
|
||||
.phone-header.is-wx {
|
||||
background: #07c160;
|
||||
}
|
||||
|
||||
.phone-body {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
@@ -234,7 +386,9 @@ function clearSignature() {
|
||||
|
||||
.message-bubble,
|
||||
.mobile-document,
|
||||
.mobile-signature {
|
||||
.mobile-signature,
|
||||
.verify-card,
|
||||
.wx-chat-head {
|
||||
padding: 11px 13px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
@@ -258,6 +412,100 @@ function clearSignature() {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.wx-chat-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background: #ededed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.wx-avatar {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: #07c160;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.wx-account {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wx-account strong {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.wx-account span {
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.verify-card {
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.verify-card strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.verify-tip {
|
||||
margin: 8px 0;
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.verify-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.verify-row span {
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.verify-row em {
|
||||
font-style: normal;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.verify-row b {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.verify-ok {
|
||||
padding: 7px 10px;
|
||||
margin: 10px 0 0;
|
||||
color: var(--brand-d);
|
||||
font-size: 11.5px;
|
||||
background: var(--brand-l);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.verify-note {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.is-mono {
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.mobile-document {
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
@@ -279,8 +527,30 @@ function clearSignature() {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.mobile-document p {
|
||||
margin: 0;
|
||||
.mobile-summary {
|
||||
padding: 11px 13px;
|
||||
margin: 10px 0 0;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
line-height: 1.75;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
/* 公众号页里的整篇文书:手机壳里当然不是一页 A4,把 A4 规格的变量就地压掉 */
|
||||
.mobile-paper {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mobile-paper :deep(.doc-paper) {
|
||||
--doc-paper-min-h: 0px;
|
||||
--doc-paper-pad-x: 14px;
|
||||
--doc-paper-pad-y: 12px;
|
||||
|
||||
max-width: none;
|
||||
font-size: 12px;
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.mobile-signature {
|
||||
@@ -323,6 +593,11 @@ function clearSignature() {
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
/* 公众号页的确认按钮是微信绿(原型 wxpFoot 里写死 #07c160) */
|
||||
.phone-button.is-wx {
|
||||
background: #07c160;
|
||||
}
|
||||
|
||||
.phone-button--light {
|
||||
color: var(--mut);
|
||||
background: #eef2f4;
|
||||
@@ -340,6 +615,12 @@ function clearSignature() {
|
||||
border-radius: 0 0 14px 14px;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -984,8 +984,9 @@ async function confirmVoidTask(reason: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 患者页里的「重发」:短信重发短信、公众号重发推送(两个动作后端也不同) */
|
||||
async function handleOnlineResend() {
|
||||
await handleTaskAction('resend-sms')
|
||||
await handleTaskAction(selectedTask.value?.method === 'wx' ? 'resend-wx-push' : 'resend-sms')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1139,6 +1140,7 @@ onMounted(() => {
|
||||
<OnlineSigningDialog
|
||||
:visible="onlineDialogVisible"
|
||||
:task="selectedTask"
|
||||
:variant="selectedTask?.method === 'wx' ? 'wx' : 'sms'"
|
||||
@close="onlineDialogVisible = false"
|
||||
@complete="completeOnlineSignature"
|
||||
@resend="handleOnlineResend"
|
||||
|
||||
Reference in New Issue
Block a user