feat(签署文书): 通用文本项按出现位置独立成项并插入文本框占位符
This commit is contained in:
@@ -129,11 +129,14 @@ ClinicalLayout
|
||||
|
||||
科室在文档库的 Word 编辑器中维护文书,签署时需要交互化的内容按下面的写法即可,系统在签署和「签署预览」时自动转换:
|
||||
|
||||
- 占位符:`{患者姓名}`、`{性别}`、`{年龄}`、`{就诊号}`、`{签署日期}`、`{患者签名}`、`{医师签名}` 等,由签署任务的患者与就诊快照填充;
|
||||
- 占位符分两类,写法都是 `{…}`:
|
||||
- **系统预填项**:`{患者姓名}`、`{性别}`、`{年龄}`、`{就诊号}`、`{签署日期}`、`{患者签名}`、`{医师签名}` 等,字段键与取值来源固定,由签署任务的患者与就诊快照填充,目录见 `utils/signing-fields.ts` 的 `SIGNING_FIELD_PRESETS`;
|
||||
- **通用文本项**:现场手填的输入框,默认写 `{文本框}`,签署时渲染成一个空文本框。**每个占位符各自是一个填写项**,字段键按出现位置编号(`custom:名称`、第 2 个起是 `custom:名称#2`……),所以同一份文书里放几个文本框就是几份取值,不需要为了区分而改名字;名称只决定文书里显示的文字,直接把正文里的「文本框」改成「与患者关系」「受托人姓名」即可(仅为可读性,不影响取值归属)。
|
||||
- 只有上面那类系统预填项按字段键去重、同名共用一份取值——页眉的患者姓名、每页的签名位反复出现,取值本来就该是同一个。
|
||||
- 勾选表:做成表格,第一行表头写「勾选」,另设一列写「项目」作为勾选条目名,签署时「勾选」列每个单元格变成复选框;
|
||||
- 行内选项:正文里直接写方框符号,如 `□否 □是`、`□同意 □不同意`。同一段落或同一单元格内出现两个及以上方框时按二选一(互斥单选)处理,只有一个方框时保持独立复选框语义。
|
||||
|
||||
编辑器弹窗顶部提供「插入勾选框」「二选一」按钮,光标停在表格单元格内时插入内容会落进该单元格,不需要科室自己找符号面板。导入(.docx / 粘贴)阶段只做格式转换,不做勾选识别;识别统一在签署渲染时进行。
|
||||
编辑器弹窗顶部提供「插入勾选框」「二选一」按钮,以及插入通用文本项的「文本框」按钮(虚线圈出的那一个,点一下插入 `{文本框}`,把「文本框」三个字改成实际名称即可)。光标停在表格单元格内时插入内容会落进该单元格,不需要科室自己找符号面板。导入(.docx / 粘贴)阶段只做格式转换,不做勾选识别;识别统一在签署渲染时进行。
|
||||
|
||||
识别规则集中在 `utils/signing-document.ts`,调整后签署端与「签署预览」会同步生效。
|
||||
|
||||
|
||||
@@ -18,11 +18,10 @@
|
||||
import type { SigningAnswers, SigningFieldDef, SigningFieldKind } from '@/api/workbench/types'
|
||||
|
||||
import {
|
||||
createFieldTokenPattern,
|
||||
FIELD_TOKEN_OPEN,
|
||||
PRIMARY_SIGNATURE_FIELD_ID,
|
||||
resolveAnswersFromSystem,
|
||||
resolveFieldByLabel,
|
||||
scanFieldTokens,
|
||||
SIGNING_DOCUMENT_HEADER,
|
||||
SIGNING_OPTION_MARKERS,
|
||||
type SigningSystemSnapshot,
|
||||
@@ -260,6 +259,11 @@ function buildFieldControl(
|
||||
* 占位符渲染:把 {患者姓名} 就地替换成填写项控件,保留同一文本节点内的前后文字。
|
||||
* 模板未收录的占位符按自定义文本项处理,仍可在文书上填写,
|
||||
* 避免出现"写了占位符但签署时既填不上也看不见"的情况。
|
||||
*
|
||||
* 解析统一走 scanFieldTokens,且计数器跨文本节点共用一个:
|
||||
* 自定义文本项靠"同名占位符第几次出现"编号,这里数出的序号必须和
|
||||
* 填写项统计(collectFieldDefs)数出来的完全一致,否则同一处内容
|
||||
* 在面板里是一个字段键、落到 DOM 上是另一个,取值就悄悄对不上了。
|
||||
*/
|
||||
function renderTokenFields(
|
||||
root: Element,
|
||||
@@ -270,6 +274,7 @@ function renderTokenFields(
|
||||
const doc = root.ownerDocument
|
||||
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
||||
const targets: Text[] = []
|
||||
const counters = new Map<string, number>()
|
||||
let node = walker.nextNode()
|
||||
|
||||
while (node) {
|
||||
@@ -282,28 +287,18 @@ function renderTokenFields(
|
||||
|
||||
targets.forEach((textNode) => {
|
||||
const text = textNode.textContent ?? ''
|
||||
const pattern = createFieldTokenPattern()
|
||||
const fragment = doc.createDocumentFragment()
|
||||
let cursor = 0
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
if (match.index > cursor) {
|
||||
fragment.appendChild(doc.createTextNode(text.slice(cursor, match.index)))
|
||||
scanFieldTokens(text, counters).forEach(({ start, end, field }) => {
|
||||
if (start > cursor) {
|
||||
fragment.appendChild(doc.createTextNode(text.slice(cursor, start)))
|
||||
}
|
||||
|
||||
fragment.appendChild(
|
||||
buildFieldWidget(
|
||||
doc,
|
||||
resolveFieldByLabel(match[1] ?? ''),
|
||||
answers,
|
||||
interactive,
|
||||
signatureActionable,
|
||||
),
|
||||
)
|
||||
fragment.appendChild(buildFieldWidget(doc, field, answers, interactive, signatureActionable))
|
||||
|
||||
cursor = pattern.lastIndex
|
||||
}
|
||||
cursor = end
|
||||
})
|
||||
|
||||
if (cursor < text.length) {
|
||||
fragment.appendChild(doc.createTextNode(text.slice(cursor)))
|
||||
|
||||
@@ -30,6 +30,7 @@ import type { IEditorConfig } from '@wangeditor/editor'
|
||||
|
||||
import {
|
||||
createFieldTokenPattern,
|
||||
isCustomFieldId,
|
||||
resolveFieldByLabel,
|
||||
SIGNING_FIELD_KIND_LABELS,
|
||||
SIGNING_OPTION_MARKERS,
|
||||
@@ -57,6 +58,8 @@ interface SigningLeafMark {
|
||||
/** 控件类型的中文名,渲染在胶囊右侧 */
|
||||
signTag?: string
|
||||
signRequired?: boolean
|
||||
/** 是否目录外的自定义文本项(现场手填),决定悬浮提示里说"自动填充"还是"现场填写" */
|
||||
signCustom?: boolean
|
||||
/** 原文,如 {患者姓名},用于悬浮提示 */
|
||||
signRaw?: string
|
||||
}
|
||||
@@ -97,6 +100,7 @@ export function decorateSigningMarkers([node, path]: DecoratedEntry): DecoratedR
|
||||
signKind: field.kind,
|
||||
signTag: SIGNING_FIELD_KIND_LABELS[field.kind] ?? '文本',
|
||||
signRequired: field.required,
|
||||
signCustom: isCustomFieldId(field.id),
|
||||
signRaw: text.slice(start, end),
|
||||
}
|
||||
|
||||
@@ -177,8 +181,11 @@ function renderSigningMarker(node: StyledNode, vnode: StyledVNode): StyledVNode
|
||||
|
||||
if (mark.signPart === 'label') {
|
||||
const kind = mark.signTag ?? '文本'
|
||||
// 提示必须分开说:目录里的字段签署时自动填充,自定义文本项是留给现场手填的空框。
|
||||
// 一律写成"自动填充",科室会以为写了占位符就有人替他填。
|
||||
const source = mark.signCustom ? '签署时由患者/家属现场填写' : '签署时按患者与就诊信息自动填充'
|
||||
props.className = 'ms-sign-chip'
|
||||
props.title = `待填写项:${mark.signRaw ?? ''}(${kind}${mark.signRequired ? ',必填' : ''}),签署时按患者与就诊信息自动填充`
|
||||
props.title = `待填写项:${mark.signRaw ?? ''}(${kind}${mark.signRequired ? ',必填' : ''}),${source}`
|
||||
|
||||
if (mark.signTag) {
|
||||
dataset.signTag = mark.signTag
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* 文书填写项(占位符)模型。
|
||||
*
|
||||
* 相比「把占位符当普通文字做字符串替换」,这里给每个占位符补上语义:
|
||||
* ① 稳定字段键(id):与 HIS / 患者快照的取值一一对应,不再依赖中文标签匹配;
|
||||
* ① 稳定字段键(id):目录里的字段与 HIS / 患者快照的取值一一对应,不依赖中文标签匹配;
|
||||
* 目录外的自定义文本项按出现位置编号(custom:名称#第几次),两处文本框就是两份取值;
|
||||
* ② 控件类型(kind):签名是与文本/日期/单选/勾选并列的一种填写项,
|
||||
* 签完把取值写回同一字段即可在文书原位置回显;
|
||||
* ③ 必填与选项:模板保存时可做去重校验,签署时可校验必填项是否填完;
|
||||
@@ -64,6 +65,12 @@ export const PRIMARY_SIGNATURE_FIELD_ID = 'signature'
|
||||
/**
|
||||
* 填写项目录:key 为字段键,label 是文书里显示的文字,kind 决定签署时的控件。
|
||||
* id 与 label 的对应关系由这里统一维护,渲染端与编辑器共用同一份,避免两处各自演化。
|
||||
*
|
||||
* 收录标准只有一条:**取值有固定来源**。这里的每一项都能按 id 从 HIS / 患者就诊快照
|
||||
* 或签署动作里取到值(见 resolveAnswersFromSystem),所以字段键必须稳定、不能随叫法变。
|
||||
* 现场手填的文本框没有系统取值来源,不需要固定字段键,一律走自定义文本项
|
||||
* (createCustomField + buildGenericTextToken),不再为「与患者关系」这类写法逐条登记——
|
||||
* 逐条登记既列不全科室的叫法,也会让同一个输入框因写法不同而落到两个字段键上。
|
||||
*/
|
||||
export const SIGNING_FIELD_PRESETS: SigningFieldDef[] = [
|
||||
{ id: 'patient', label: '患者姓名', kind: 'text', required: true, options: [] },
|
||||
@@ -73,7 +80,6 @@ export const SIGNING_FIELD_PRESETS: SigningFieldDef[] = [
|
||||
{ id: 'phone', label: '联系电话', kind: 'text', required: false, options: [] },
|
||||
{ id: 'appointment', label: '预约日期', kind: 'date', required: false, options: [] },
|
||||
{ id: 'signedDate', label: '签署日期', kind: 'date', required: true, options: [] },
|
||||
{ id: 'relationship', label: '与患者关系', kind: 'text', required: false, options: [] },
|
||||
{ id: 'signature', label: '患者签名', kind: 'signature', required: true, options: [] },
|
||||
{ id: 'doctor', label: '医师签名', kind: 'signature', required: false, options: [] },
|
||||
]
|
||||
@@ -136,7 +142,10 @@ const FIELD_LABEL_ALIASES: Record<string, string> = {
|
||||
const PRESET_BY_ID = new Map(SIGNING_FIELD_PRESETS.map((field) => [field.id, field]))
|
||||
const PRESET_BY_LABEL = new Map(SIGNING_FIELD_PRESETS.map((field) => [field.label, field]))
|
||||
|
||||
/** 自定义占位符的字段键前缀:label 不在目录里时按文本填写项处理 */
|
||||
/**
|
||||
* 自定义占位符的字段键前缀:label 不在目录里时按文本填写项处理。
|
||||
* 键 = `custom:名称`,同名占位符第 2 个起再接 `#出现次序`——见 resolveFieldByLabel。
|
||||
*/
|
||||
const CUSTOM_FIELD_PREFIX = 'custom:'
|
||||
|
||||
export function getPresetField(id: string): SigningFieldDef | null {
|
||||
@@ -148,9 +157,11 @@ export function createCustomField(
|
||||
label: string,
|
||||
kind: SigningFieldKind = 'text',
|
||||
options: string[] = [],
|
||||
/** 同名占位符在文书里的出现次序,从 1 开始;见 resolveFieldByLabel */
|
||||
sequence = 1,
|
||||
): SigningFieldDef {
|
||||
return {
|
||||
id: `${CUSTOM_FIELD_PREFIX}${label}`,
|
||||
id: `${CUSTOM_FIELD_PREFIX}${label}${sequence > 1 ? `#${sequence}` : ''}`,
|
||||
label,
|
||||
kind,
|
||||
required: false,
|
||||
@@ -158,11 +169,54 @@ export function createCustomField(
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断字段键是否来自目录之外的自定义文本项(现场手填的输入框) */
|
||||
export function isCustomFieldId(id: string): boolean {
|
||||
return id.startsWith(CUSTOM_FIELD_PREFIX)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用文本项的默认名称(占位符 {文本框})。名称只决定文书里显示的文字,不决定它是哪个字段。
|
||||
*
|
||||
* 为什么留一个通用入口,而不是逐个登记「与患者关系」「受托人姓名」:
|
||||
* 这类内容是现场手填的输入框,没有系统取值来源,叫法又千变万化(代理文书里
|
||||
* 还有「受托人姓名」「代理人身份证号」……),在目录里逐条登记既列不全,
|
||||
* 也会让同一个输入框因叫法不同落到两个字段键上。
|
||||
*
|
||||
* 名称也不负责区分"是不是同一个填写项":文本框按出现位置各自成项(见 resolveFieldByLabel),
|
||||
* 所以一份文书里的两个文本框天然就是两份取值,不需要为了区分而改名字——
|
||||
* 改名只是让文书写得像文书(「文本框」改成「与患者关系」)。
|
||||
*
|
||||
* 默认名不叫「文本」是为了在编辑器里能认出来:胶囊的角标已经写着「文本」,
|
||||
* 名称再叫「文本」会显示成「文本 文本」,分不清哪个是名字哪个是类型。
|
||||
*/
|
||||
export const GENERIC_TEXT_FIELD_LABEL = '文本框'
|
||||
|
||||
/**
|
||||
* 生成通用文本项的占位符,默认 {文本框}。
|
||||
*
|
||||
* 走的是目录外的自定义文本项:resolveFieldByLabel 查不到目录就按文本控件处理,
|
||||
* 签署时渲染成一个输入框,取值落在 fieldAnswers 里。
|
||||
*/
|
||||
export function buildGenericTextToken(label: string = GENERIC_TEXT_FIELD_LABEL): string {
|
||||
return buildFieldToken(createCustomField(label.trim()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 按占位符文字定位字段定义:先查目录,再查别名,都不是就当自定义文本项。
|
||||
* 这样存量文书里写的是「患者(或家属)签名」也能命中签名控件。
|
||||
*
|
||||
* sequence 是**同名占位符在本文书里的第几次出现**(从 1 开始),只有自定义文本项用得上:
|
||||
*
|
||||
* - 目录里的字段按 id 去重,同名共用一份取值是刻意的——页眉的患者姓名、每页的签名位
|
||||
* 反复出现,取值只能是同一个;
|
||||
* - 自定义文本项没有取值来源,两个 {文本框} 是两处要填的内容,共用一个值才是错的。
|
||||
* 而占位符是纯文本,正文里只有文字可依据,所以用"第几次出现"给它们各自编号:
|
||||
* 第 1 个 custom:文本框,第 2 个 custom:文本框#2……位置就是它们的身份。
|
||||
*
|
||||
* 由此得到一条对科室更省心的规则:**改名字不影响取值归属**,名字纯粹是给人看的。
|
||||
* 前提是模板内容定稿后就冻结(发起签署时会快照正文),位置编号才不会漂移。
|
||||
*/
|
||||
export function resolveFieldByLabel(label: string): SigningFieldDef {
|
||||
export function resolveFieldByLabel(label: string, sequence = 1): SigningFieldDef {
|
||||
const trimmed = label.trim()
|
||||
const preset = PRESET_BY_LABEL.get(trimmed)
|
||||
if (preset) {
|
||||
@@ -177,7 +231,50 @@ export function resolveFieldByLabel(label: string): SigningFieldDef {
|
||||
}
|
||||
}
|
||||
|
||||
return createCustomField(trimmed)
|
||||
return createCustomField(trimmed, 'text', [], sequence)
|
||||
}
|
||||
|
||||
/** 扫描结果:占位符在文本里的位置 + 它对应的字段定义 */
|
||||
export interface ScannedFieldToken {
|
||||
start: number
|
||||
end: number
|
||||
field: SigningFieldDef
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描一段文本里的占位符并解析成字段定义。
|
||||
*
|
||||
* counters 必须**跨段传递**(同一份文书的每个文本节点/整段 HTML 共用一个):
|
||||
* 自定义文本项靠"同名占位符第几次出现"编号,分段扫描时如果各自从 1 数起,
|
||||
* 编辑器面板统计出的字段键就会和签署渲染时写进 DOM 的键对不上,
|
||||
* 表现是填写项统计里有这一项、签署时却收集不到值。
|
||||
*
|
||||
* 之所以要有个统一的扫描函数,就是因为这条编号规则被三处用到
|
||||
* (填写项统计 collectFieldDefs、重复检查 findDuplicateFields、签署渲染 renderTokenFields),
|
||||
* 三处各写一份计时器迟早会漂移。
|
||||
*/
|
||||
export function scanFieldTokens(text: string, counters: Map<string, number>): ScannedFieldToken[] {
|
||||
const tokens: ScannedFieldToken[] = []
|
||||
const pattern = createFieldTokenPattern()
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const label = (match[1] ?? '').trim()
|
||||
|
||||
if (!label) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sequence = (counters.get(label) ?? 0) + 1
|
||||
counters.set(label, sequence)
|
||||
tokens.push({
|
||||
start: match.index,
|
||||
end: pattern.lastIndex,
|
||||
field: resolveFieldByLabel(label, sequence),
|
||||
})
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +291,10 @@ export function buildFieldToken(field: SigningFieldDef): string {
|
||||
|
||||
/**
|
||||
* 解析文书 HTML 里的全部占位符,按出现顺序去重。
|
||||
* 同一个字段在文书里出现多次(如签名与日期在每页都出现)只算一个填写项。
|
||||
*
|
||||
* 「去重」只对目录里的字段生效:同一个字段在文书里出现多次(如签名与日期在每页都出现)
|
||||
* 只算一个填写项,它们共用一份取值。自定义文本项按出现位置各自成项(见 resolveFieldByLabel),
|
||||
* 所以两份 {文本框} 会得到两个填写项,不会被合成一个。
|
||||
*/
|
||||
export function collectFieldDefs(contentHtml: string): SigningFieldDef[] {
|
||||
if (!contentHtml) {
|
||||
@@ -203,37 +303,37 @@ export function collectFieldDefs(contentHtml: string): SigningFieldDef[] {
|
||||
|
||||
const seen = new Set<string>()
|
||||
const fields: SigningFieldDef[] = []
|
||||
const pattern = createFieldTokenPattern()
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = pattern.exec(contentHtml)) !== null) {
|
||||
const field = resolveFieldByLabel(match[1] ?? '')
|
||||
scanFieldTokens(contentHtml, new Map()).forEach(({ field }) => {
|
||||
if (!field.label || seen.has(field.id)) {
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
seen.add(field.id)
|
||||
fields.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
/** 同一占位符在文书里写了多次的字段(保存模板时提示,通常是误重复) */
|
||||
/**
|
||||
* 同一占位符在文书里写了多次的字段(保存模板时提示,通常是误重复)。
|
||||
*
|
||||
* 这里**只会命中目录里的字段**:自定义文本项按出现位置编号,每次出现的字段键都不同,
|
||||
* 计数永远到不了 2。这是对的——两处 {文本框} 是两处要填的内容,
|
||||
* 不是"同一个字段写重复了",不该提示科室去合并。
|
||||
*/
|
||||
export function findDuplicateFields(contentHtml: string): SigningFieldDef[] {
|
||||
if (!contentHtml) {
|
||||
return []
|
||||
}
|
||||
|
||||
const counts = new Map<string, { field: SigningFieldDef; count: number }>()
|
||||
const pattern = createFieldTokenPattern()
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = pattern.exec(contentHtml)) !== null) {
|
||||
const field = resolveFieldByLabel(match[1] ?? '')
|
||||
scanFieldTokens(contentHtml, new Map()).forEach(({ field }) => {
|
||||
const entry = counts.get(field.id)
|
||||
counts.set(field.id, { field, count: (entry?.count ?? 0) + 1 })
|
||||
}
|
||||
})
|
||||
|
||||
return [...counts.values()].filter((item) => item.count > 1).map((item) => item.field)
|
||||
}
|
||||
@@ -267,9 +367,10 @@ export function resolveAnswersFromSystem(snapshot: SigningSystemSnapshot): Signi
|
||||
// 真正确认的日期以签署完成时固化的取值(signedAt)为准
|
||||
signedDate: (snapshot.signedAt ?? new Date().toISOString()).slice(0, 10),
|
||||
signature: snapshot.signatureDataUrl ?? '',
|
||||
// 医师签名与代理人关系必须由现场填写,不能由系统代填;
|
||||
// 上一版把「与患者关系」恒填「本人」,等于把代理人签署这条路堵死了。
|
||||
relationship: '',
|
||||
// 医师签名必须由现场采集,不能由系统代填。
|
||||
// 同理,与患者关系、受托人姓名这类现场手填的文本框是自定义文本项
|
||||
// (见 createCustomField),没有系统初值——把「与患者关系」恒填「本人」
|
||||
// 等于把代理人签署这条路堵死了,所以这一项连空值都不在这里预置。
|
||||
doctor: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import { ElMessage } from 'element-plus'
|
||||
import type { CampusRecord, DepartmentRecord } from '@/api/management/types'
|
||||
import {
|
||||
buildFieldToken,
|
||||
buildGenericTextToken,
|
||||
collectFieldDefs,
|
||||
findDuplicateFields,
|
||||
GENERIC_TEXT_FIELD_LABEL,
|
||||
SIGNING_FIELD_KIND_LABELS,
|
||||
SIGNING_FIELD_PRESETS,
|
||||
} from '@/utils/signing-fields'
|
||||
@@ -43,9 +45,12 @@ const importing = ref(false)
|
||||
const importMessage = ref('')
|
||||
const paneRef = ref<EditorPaneHandle | null>(null)
|
||||
|
||||
// 占位符在签署时按字段键取 HIS 与患者就诊信息自动填充,其中签名是独立控件类型:
|
||||
// 签字后签名图作为该字段取值落回文书原本的签名位,不需要再替换文字。
|
||||
// 目录见 utils/signing-fields 的 SIGNING_FIELD_PRESETS,与签署端渲染共用同一份。
|
||||
// 占位符分两类:
|
||||
// ① 目录里的字段(utils/signing-fields 的 SIGNING_FIELD_PRESETS):取值有固定来源,
|
||||
// 签署时按字段键取 HIS / 患者就诊快照自动填充,其中签名是独立控件类型——
|
||||
// 签字后签名图作为该字段取值落回文书原本的签名位,不需要再替换文字;
|
||||
// ② 通用文本项(现场手填的输入框,如与患者关系、受托人姓名):没有系统取值来源,
|
||||
// 默认写 {文本框},签署时渲染成一个空文本框,名称在正文里改即可,见 insertGenericTextField。
|
||||
|
||||
const form = reactive<TemplateEditorForm>({
|
||||
templateCode: '',
|
||||
@@ -113,6 +118,17 @@ function insertField(field: (typeof SIGNING_FIELD_PRESETS)[number]) {
|
||||
insertContent(buildFieldToken(field))
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入通用文本项:现场手填的输入框,默认 {文本框}。
|
||||
*
|
||||
* 每插一个就是一个独立填写项(字段键按出现位置编号,见 signing-fields 的 resolveFieldByLabel),
|
||||
* 所以两处文本框天然是两份取值,不需要为了区分而改名字。
|
||||
* 名称只决定文书里显示的文字:改成「与患者关系」是为了读起来像文书,不影响取值归属。
|
||||
*/
|
||||
function insertGenericTextField() {
|
||||
insertContent(buildGenericTextToken())
|
||||
}
|
||||
|
||||
/** 已插入的填写项:让科室在保存前确认签名位、日期位有没有漏 */
|
||||
const detectedFields = computed(() => collectFieldDefs(valueHtml.value))
|
||||
|
||||
@@ -284,6 +300,14 @@ function save() {
|
||||
>
|
||||
{{ field.label }}{{ field.required ? ' *' : '' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="placeholder-chip is-generic"
|
||||
:title="`插入一个文本输入框(占位符 ${GENERIC_TEXT_FIELD_LABEL}),签署时由患者/家属现场填写。插几个就是几个独立填写项,各填各的;名称只决定显示的文字,可在正文里直接改(如改成「与患者关系」)`"
|
||||
@click="insertGenericTextField()"
|
||||
>
|
||||
{{ GENERIC_TEXT_FIELD_LABEL }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-field">
|
||||
@@ -344,7 +368,8 @@ function save() {
|
||||
<span v-if="importMessage" class="docx-message">{{ importMessage }}</span>
|
||||
<span v-else class="docx-hint">
|
||||
建议从 Word/WPS
|
||||
中全选复制文书后直接粘贴到下方编辑区;需要患者勾选的内容可点上方「插入勾选框」按钮插入(光标停在表格单元格里就插到该单元格),或用表头含「勾选」列的表格承载。
|
||||
中全选复制文书后直接粘贴到下方编辑区;需要患者勾选的内容可点上方「插入勾选框」按钮插入(光标停在表格单元格里就插到该单元格),或用表头含「勾选」列的表格承载;
|
||||
需要患者现场填写的内容(与患者关系、受托人姓名等)点「文本框」插入输入框占位符,插几个就是几处独立填写、各填各的;把名称改成实际叫法只是为了读起来像文书。
|
||||
占位符与勾选框在编辑区会显示成带类型标签的胶囊,便于和正文区分。
|
||||
</span>
|
||||
</div>
|
||||
@@ -514,6 +539,12 @@ function save() {
|
||||
background: #fff4e2;
|
||||
}
|
||||
|
||||
/* 通用文本项:虚线圈住,和左边一排系统预填字段区分开——
|
||||
它不是某个固定字段,插到哪就是一个现场手填的输入框,名称可以在正文里随便改 */
|
||||
.placeholder-chip.is-generic {
|
||||
border: 1px dashed var(--brand);
|
||||
}
|
||||
|
||||
.placeholder-check {
|
||||
padding: 10px 18px 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user