diff --git a/patient-h5/src/components/DocumentPaper.vue b/patient-h5/src/components/DocumentPaper.vue
index 5fe636d..908db6a 100644
--- a/patient-h5/src/components/DocumentPaper.vue
+++ b/patient-h5/src/components/DocumentPaper.vue
@@ -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;
}
diff --git a/patient-h5/src/composables/useSignFlow.ts b/patient-h5/src/composables/useSignFlow.ts
index 2b0218a..66bfc20 100644
--- a/patient-h5/src/composables/useSignFlow.ts
+++ b/patient-h5/src/composables/useSignFlow.ts
@@ -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 },
]
diff --git a/patient-h5/src/mock/demo-narcotic.ts b/patient-h5/src/mock/demo-narcotic.ts
new file mode 100644
index 0000000..b836018
--- /dev/null
+++ b/patient-h5/src/mock/demo-narcotic.ts
@@ -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 = [
+ '
麻醉药品、精神药品知情同意书
',
+ '(依据国卫医政发〔2026〕21 号第二十七条、第六十九条制定)
',
+
+ '一、患者基本信息
',
+ '',
+ '| 患者姓名 | 患者姓名 | ',
+ '性别 | 性别 |
',
+ '| 年龄 | 年龄 | ',
+ '病历号 | 病历号 |
',
+ '| 身份证号 | 身份证号 | ',
+ '联系电话 | 联系电话 |
',
+ '| 诊断 | 诊断 | ',
+ '就诊科室 | 就诊科室 |
',
+ '
',
+
+ '二、代办人信息(代办领取时填写)
',
+ '',
+ '| 代办人姓名 | 代办人姓名 | ',
+ '与患者关系 | 与患者关系 |
',
+ '| 代办人身份证号 | 代办人身份证号 | ',
+ '联系电话 | 联系电话 |
',
+ '
',
+
+ '三、身份核验记录
',
+ '已核对:患者本人 '
+ + '代办人 '
+ + '身份证原件
',
+ '',
+ '| 证件类型及号码 | 证件类型及号码 |
',
+ '| 医师确认 | 医师确认 | ',
+ '核对医师签名 | 核对医师签名 |
',
+ '
',
+
+ '四、用药信息
',
+ '',
+ '| 药品名称 | 药品名称 |
',
+ '| 剂型 / 规格 | 剂型规格 | ',
+ '用法用量 | 用法用量 |
',
+ '| 是否带离机构使用 | 带离机构使用 | ',
+ '使用期限 | 使用期限 |
',
+ '
',
+
+ '五、风险告知与注意事项
',
+ '',
+ '- 麻醉药品、精神药品具有成瘾性,须严格遵照医嘱使用,不得自行加量或延长使用时间。
',
+ '- 用药期间可能出现头晕、嗜睡、恶心、便秘等不良反应,如出现呼吸抑制、意识障碍请立即就医。
',
+ '- 药品须妥善保管,避免儿童接触;剩余药品应交回医疗机构统一处理。
',
+ '- 不得转借、转让或代他人开具,违者依法承担相应责任。
',
+ '
',
+
+ '六、签字确认
',
+ '',
+ '| 患者(或监护人 / 代办人)签名 | '
+ + '患者签名 | ',
+ '签字日期 | 签署日期 |
',
+ '| 开具医师签名 | '
+ + '开具医师签名 | ',
+ '科室 | 科室 |
',
+ '
',
+
+ '注:本知情同意书由医疗机构妥善保存。采用电子形式的,内容须与纸质一致、完整且不可篡改(第六十九条)。
',
+].join('')
+
+/** 演示用的「医生已填」结果。同样**不含**患者签名位 —— 那一格留给患者当场签 */
+export const DEMO_NARCOTIC_STAFF_ANSWERS: Record = {
+ 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,',
+ prescribingDoctor:
+ 'data:image/svg+xml;utf8,',
+}
diff --git a/patient-h5/src/views/signer/SignerView.vue b/patient-h5/src/views/signer/SignerView.vue
index 3e34a87..b709ec1 100644
--- a/patient-h5/src/views/signer/SignerView.vue
+++ b/patient-h5/src/views/signer/SignerView.vue
@@ -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"
/>