feat(患者签署): 签署自动落款日期并新增精麻同意书演示文书

This commit is contained in:
yelan
2026-09-28 11:08:06 +08:00
parent 28c45dc7b2
commit 3995c027fc
4 changed files with 156 additions and 7 deletions
+23 -4
View File
@@ -25,11 +25,17 @@ const props = withDefaults(
patientSignature?: string | null
/** 患者签名位的字段 id(与 clinical-web 的 PRIMARY_SIGNATURE_FIELD_ID 同口径) */
patientFieldId?: string
/**
* 签署日期(`YYYY-MM-DD`)。签了字就该有日期,不该再让患者填一次 ——
* 只在签名已采到时由调用方传入;没传就保持模板里的空占位。
*/
signedAt?: string | null
}>(),
{
answers: () => ({}),
patientSignature: null,
patientFieldId: 'signature',
signedAt: null,
},
)
@@ -93,7 +99,15 @@ function paint() {
const value = (props.answers?.[id] ?? '').trim()
// ② 没有值:保留模板里的标签文字,只标记成空
// ② 签字日期不劳患者填:签了字,日期就是签署当天。
// 只在签名已采到时才填 —— 还没签就把日期写上,文书会看起来像已经完成了
if (!value && kind === 'date' && props.signedAt) {
node.classList.add('is-filled')
node.textContent = props.signedAt
return
}
// ③ 没有值:保留模板里的标签文字,只标记成空
if (!value) {
node.classList.add('is-empty')
@@ -106,13 +120,13 @@ function paint() {
node.classList.add('is-filled')
// ③ 签名图(医师签名等)
// ④ 签名图(医师签名等)
if (kind === 'signature' || isImageValue(value)) {
appendSignatureImage(node, value, label || '签名')
return
}
// ④ 勾选类:单选一律显示已勾,多选/复选按值判
// ⑤ 勾选类:单选一律显示已勾,多选/复选按值判
if (kind === 'checkbox' || kind === 'choice') {
const checked = kind === 'choice' ? true : isChecked(value)
// 值是 1/0 这类占位时回落到标签文字,避免正文里出现一个孤零零的「1」
@@ -122,7 +136,7 @@ function paint() {
return
}
// ⑤ 其余(text / date)按文本显示
// ⑥ 其余(text / date)按文本显示
node.textContent = value
})
}
@@ -199,6 +213,9 @@ watch(
padding: 8px 10px;
vertical-align: top;
border: 1px solid #dbe7ea;
/* 中文默认可在任意字符处断行,窄列里会把「签字日期」拆成四个竖排的字。
keep-all 让浏览器优先保证词不被拆开;真的放不下时由 overflow-wrap 兜底。 */
word-break: keep-all;
}
.patient-paper :deep(img) {
@@ -229,8 +246,10 @@ watch(
border-bottom: 1px dashed #b9d8d2;
}
/* 签名图不设宽上限会挤掉旁边的文字列,把「签字日期」这类标签压成竖排 */
.patient-paper :deep(.field-signature) {
display: block;
max-width: 120px;
max-height: 90px;
margin: 2px 0;
}
+13 -3
View File
@@ -7,6 +7,7 @@ import type {
SigningContextResponse,
} from '@/api/types'
import { DEMO_DOCUMENT_HTML, DEMO_PATIENT_FIELD_ID, DEMO_STAFF_ANSWERS } from '@/mock/demo-document'
import { DEMO_NARCOTIC_HTML, DEMO_NARCOTIC_STAFF_ANSWERS } from '@/mock/demo-narcotic'
import type { SignerRelation, SignFlowState } from '@/types/signing'
/**
@@ -296,10 +297,19 @@ async function loadSigningContext(token: string) {
* 「演示模式下好好的、真实模式一片空白」这种最难查的偏差。
*/
function applyDemoContext() {
signingState.documentTitle = 'CT 增强扫描知情同意书'
// `?demo=narcotic` 切到精麻同意书:它是演示文库(clinical-web)里唯一真实存在的模板,
// 字段也最多(代办人 / 身份核验 / 用药信息),最能压出占位符渲染的问题
const useNarcotic =
new URLSearchParams(window.location.search).get('demo') === 'narcotic'
signingState.documentTitle = useNarcotic
? '麻醉药品、精神药品知情同意书'
: 'CT 增强扫描知情同意书'
signingState.templateVersion = 'v1.0.0'
signingState.documentContentHtml = DEMO_DOCUMENT_HTML
signingState.staffAnswers = { ...DEMO_STAFF_ANSWERS }
signingState.documentContentHtml = useNarcotic ? DEMO_NARCOTIC_HTML : DEMO_DOCUMENT_HTML
signingState.staffAnswers = {
...(useNarcotic ? DEMO_NARCOTIC_STAFF_ANSWERS : DEMO_STAFF_ANSWERS),
}
signingState.fields = [
{ id: DEMO_PATIENT_FIELD_ID, label: '患者签名', kind: 'signature', required: true },
]
+104
View File
@@ -0,0 +1,104 @@
/**
* 演示模式的第二份文书:**麻醉药品、精神药品知情同意书**。
*
* 与 `demo-document.ts` 的 CT 同意书并存,用 `?demo=narcotic` 切换。
* 为什么单独做一份:精麻这份是演示文库(clinical-web)里**唯一真实存在**的模板,
* 字段也最多(代办人、身份核验、用药信息),占位符渲染最容易在这里暴露问题 ——
* CT 那份只有 11 个占位符,压不出「多行表格 + 成对字段」的排版毛病。
*
* 占位符属性名与 clinical-web 的 `utils/signing-fields.ts` 一致:
* `data-field` / `data-kind`(text|date|choice|checkbox|signature)/ `data-label`。
*/
export const DEMO_NARCOTIC_PATIENT_FIELD_ID = 'signature'
export const DEMO_NARCOTIC_HTML = [
'<h2>麻醉药品、精神药品知情同意书</h2>',
'<p style="text-align:center;font-size:13px;color:#7c8f99;">(依据国卫医政发〔2026〕21 号第二十七条、第六十九条制定)</p>',
'<h3>一、患者基本信息</h3>',
'<table><tbody>',
'<tr><td>患者姓名</td><td><span data-field="patient" data-kind="text" data-label="患者姓名" data-required="true">患者姓名</span></td>',
'<td>性别</td><td><span data-field="sex" data-kind="text" data-label="性别">性别</span></td></tr>',
'<tr><td>年龄</td><td><span data-field="age" data-kind="text" data-label="年龄">年龄</span></td>',
'<td>病历号</td><td><span data-field="visit" data-kind="text" data-label="病历号">病历号</span></td></tr>',
'<tr><td>身份证号</td><td><span data-field="idCard" data-kind="text" data-label="身份证号">身份证号</span></td>',
'<td>联系电话</td><td><span data-field="phone" data-kind="text" data-label="联系电话">联系电话</span></td></tr>',
'<tr><td>诊断</td><td><span data-field="diagnosis" data-kind="text" data-label="诊断">诊断</span></td>',
'<td>就诊科室</td><td><span data-field="department" data-kind="text" data-label="就诊科室">就诊科室</span></td></tr>',
'</tbody></table>',
'<h3>二、代办人信息(代办领取时填写)</h3>',
'<table><tbody>',
'<tr><td>代办人姓名</td><td><span data-field="agentName" data-kind="text" data-label="代办人姓名">代办人姓名</span></td>',
'<td>与患者关系</td><td><span data-field="agentRelation" data-kind="text" data-label="与患者关系">与患者关系</span></td></tr>',
'<tr><td>代办人身份证号</td><td><span data-field="agentIdCard" data-kind="text" data-label="代办人身份证号">代办人身份证号</span></td>',
'<td>联系电话</td><td><span data-field="agentPhone" data-kind="text" data-label="联系电话">联系电话</span></td></tr>',
'</tbody></table>',
'<h3>三、身份核验记录</h3>',
'<p>已核对:<span data-field="identityPatient" data-kind="checkbox" data-label="患者本人" data-options="患者本人">患者本人</span> '
+ '<span data-field="identityAgent" data-kind="checkbox" data-label="代办人" data-options="代办人">代办人</span> '
+ '<span data-field="idOriginal" data-kind="checkbox" data-label="身份证原件" data-options="身份证原件">身份证原件</span></p>',
'<table><tbody>',
'<tr><td>证件类型及号码</td><td colspan="3"><span data-field="idTypeNo" data-kind="text" data-label="证件类型及号码">证件类型及号码</span></td></tr>',
'<tr><td>医师确认</td><td><span data-field="doctorConfirm" data-kind="text" data-label="医师确认">医师确认</span></td>',
'<td>核对医师签名</td><td><span data-field="verifyDoctor" data-kind="signature" data-label="核对医师签名" data-required="true">核对医师签名</span></td></tr>',
'</tbody></table>',
'<h3>四、用药信息</h3>',
'<table><tbody>',
'<tr><td>药品名称</td><td colspan="3"><span data-field="drugName" data-kind="text" data-label="药品名称" data-required="true">药品名称</span></td></tr>',
'<tr><td>剂型 / 规格</td><td><span data-field="drugSpec" data-kind="text" data-label="剂型规格">剂型规格</span></td>',
'<td>用法用量</td><td><span data-field="drugUsage" data-kind="text" data-label="用法用量">用法用量</span></td></tr>',
'<tr><td>是否带离机构使用</td><td><span data-field="takeAway" data-kind="checkbox" data-label="带离机构使用" data-options="带离机构使用">带离机构使用</span></td>',
'<td>使用期限</td><td><span data-field="usePeriod" data-kind="text" data-label="使用期限">使用期限</span></td></tr>',
'</tbody></table>',
'<h3>五、风险告知与注意事项</h3>',
'<ol>',
'<li>麻醉药品、精神药品具有成瘾性,须严格遵照医嘱使用,不得自行加量或延长使用时间。</li>',
'<li>用药期间可能出现头晕、嗜睡、恶心、便秘等不良反应,如出现呼吸抑制、意识障碍请立即就医。</li>',
'<li>药品须妥善保管,避免儿童接触;剩余药品应交回医疗机构统一处理。</li>',
'<li>不得转借、转让或代他人开具,违者依法承担相应责任。</li>',
'</ol>',
'<h3>六、签字确认</h3>',
'<table><tbody>',
'<tr><td>患者(或监护人 / 代办人)签名</td>'
+ '<td><span data-field="signature" data-kind="signature" data-label="患者签名" data-required="true">患者签名</span></td>',
'<td>签字日期</td><td><span data-field="signedDate" data-kind="date" data-label="签署日期" data-required="true">签署日期</span></td></tr>',
'<tr><td>开具医师签名</td>'
+ '<td><span data-field="prescribingDoctor" data-kind="signature" data-label="开具医师签名" data-required="true">开具医师签名</span></td>',
'<td>科室</td><td><span data-field="department2" data-kind="text" data-label="科室">科室</span></td></tr>',
'</tbody></table>',
'<p>注:本知情同意书由医疗机构妥善保存。采用电子形式的,内容须与纸质一致、完整且不可篡改(第六十九条)。</p>',
].join('')
/** 演示用的「医生已填」结果。同样**不含**患者签名位 —— 那一格留给患者当场签 */
export const DEMO_NARCOTIC_STAFF_ANSWERS: Record<string, string> = {
patient: '李某某',
sex: '男',
age: '46 岁',
visit: 'ZY20260827001',
idCard: '3301**********1234',
phone: '138****5678',
diagnosis: '癌性疼痛',
department: '消化内科',
identityPatient: '1',
idOriginal: '1',
doctorConfirm: '患者身份信息与登记一致',
drugName: '盐酸吗啡缓释片',
drugSpec: '30mg × 10 片 / 盒',
drugUsage: '口服,一次 1 片,每 12 小时一次',
takeAway: '1',
usePeriod: '2026-08-27 至 2026-09-03',
department2: '消化内科',
verifyDoctor:
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40">'
+ '<path d="M10 28 Q28 8 46 22 T82 16 Q100 10 114 24" stroke="%23173b56" stroke-width="2" fill="none" stroke-linecap="round"/></svg>',
prescribingDoctor:
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40">'
+ '<path d="M8 26 Q24 10 40 24 T74 18 Q94 12 112 28" stroke="%23173b56" stroke-width="2" fill="none" stroke-linecap="round"/></svg>',
}
@@ -29,6 +29,21 @@ const signerLine = computed(() => {
return `${name}(${relation})`
})
/**
* 签字日期:签了字就是签署当天。
* 没落笔时不传,文书里的日期格保持空占位 —— 否则文书会看起来像已经签完了。
*/
const signedDate = computed(() => {
if (!signatureDataUrl.value) {
return null
}
const now = new Date()
const pad = (part: number) => String(part).padStart(2, '0')
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
})
function dataUrlToBlob(dataUrl: string) {
const [header, encoded] = dataUrl.split(',')
const mimeType = header?.match(/data:(.*?);base64/)?.[1] ?? 'image/png'
@@ -105,6 +120,7 @@ async function submitSigning() {
:content-html="signingState.documentContentHtml"
:answers="signingState.staffAnswers"
:patient-signature="signatureDataUrl || null"
:signed-at="signedDate"
/>
<SignatureCanvas