feat(sign): 补齐医护端与患者端签署流程
This commit is contained in:
@@ -55,6 +55,7 @@ interface EditorPaneHandle {
|
||||
}
|
||||
|
||||
const valueHtml = ref('<p><br></p>')
|
||||
const needConfirm = ref(false)
|
||||
const docxInput = ref<HTMLInputElement | null>(null)
|
||||
const importing = ref(false)
|
||||
const paneRef = ref<EditorPaneHandle | null>(null)
|
||||
@@ -128,17 +129,19 @@ const fieldDraft = reactive({
|
||||
options: [] as string[],
|
||||
multi: false,
|
||||
risk: '',
|
||||
pdfPage: '',
|
||||
pdfX: '',
|
||||
pdfY: '',
|
||||
pdfWidth: '',
|
||||
pdfHeight: '',
|
||||
pdfCoordinateSystem: 'TOP_LEFT' as 'TOP_LEFT' | 'PDF_BOTTOM_LEFT',
|
||||
})
|
||||
|
||||
/** 类型下拉的取值(原型 fpKind 的 5 个选项) */
|
||||
const KIND_OPTIONS = Object.entries(SIGNING_FIELD_KIND_LABELS) as Array<
|
||||
[SigningFieldKind, string]
|
||||
>
|
||||
const KIND_OPTIONS = Object.entries(SIGNING_FIELD_KIND_LABELS) as Array<[SigningFieldKind, string]>
|
||||
|
||||
/** 选项行与高危值行只在选项类出现(原型 docFpKind) */
|
||||
const isOptionKind = computed(
|
||||
() => fieldDraft.kind === 'choice' || fieldDraft.kind === 'checkbox',
|
||||
)
|
||||
const isOptionKind = computed(() => fieldDraft.kind === 'choice' || fieldDraft.kind === 'checkbox')
|
||||
|
||||
function syncDraft(field: SigningFieldDef | null) {
|
||||
fieldDraft.label = field?.label ?? ''
|
||||
@@ -148,6 +151,12 @@ function syncDraft(field: SigningFieldDef | null) {
|
||||
// 缺省口径与 signing-fields 的 SigningFieldDef.multi 一致:勾选类默认可多选
|
||||
fieldDraft.multi = field?.multi ?? field?.kind === 'checkbox'
|
||||
fieldDraft.risk = field?.risk ?? ''
|
||||
fieldDraft.pdfPage = field?.pdfPosition ? String(field.pdfPosition.page) : ''
|
||||
fieldDraft.pdfX = field?.pdfPosition ? String(field.pdfPosition.x) : ''
|
||||
fieldDraft.pdfY = field?.pdfPosition ? String(field.pdfPosition.y) : ''
|
||||
fieldDraft.pdfWidth = field?.pdfPosition ? String(field.pdfPosition.width) : ''
|
||||
fieldDraft.pdfHeight = field?.pdfPosition ? String(field.pdfPosition.height) : ''
|
||||
fieldDraft.pdfCoordinateSystem = field?.pdfPosition?.coordinateSystem ?? 'TOP_LEFT'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,7 +176,11 @@ watch(
|
||||
watch(
|
||||
() => fieldDraft.kind,
|
||||
(kind) => {
|
||||
if ((kind === 'choice' || kind === 'checkbox') && !fieldDraft.options.length && kind === 'choice') {
|
||||
if (
|
||||
(kind === 'choice' || kind === 'checkbox') &&
|
||||
!fieldDraft.options.length &&
|
||||
kind === 'choice'
|
||||
) {
|
||||
fieldDraft.options = ['否', '是']
|
||||
}
|
||||
},
|
||||
@@ -186,6 +199,7 @@ watch(
|
||||
// 升级沿用同一套解析口径,字段键前后完全一致,历史 fieldAnswers 不受影响;
|
||||
// 正文里没有 { 时这一步原样返回,不会白白做一次 DOM 往返。
|
||||
valueHtml.value = upgradeFieldTokensToStructured(rawContent)
|
||||
needConfirm.value = Boolean(props.template?.needConfirm)
|
||||
message.text = ''
|
||||
message.tone = ''
|
||||
selectedFieldId.value = ''
|
||||
@@ -238,6 +252,33 @@ function applyFieldDraft() {
|
||||
|
||||
const options = fieldDraft.options.map((option) => option.trim()).filter(Boolean)
|
||||
|
||||
let pdfPosition: SigningFieldDef['pdfPosition']
|
||||
if (field.id === 'signature' && fieldDraft.kind === 'signature') {
|
||||
const page = Number(fieldDraft.pdfPage)
|
||||
const x = Number(fieldDraft.pdfX)
|
||||
const y = Number(fieldDraft.pdfY)
|
||||
const width = Number(fieldDraft.pdfWidth)
|
||||
const height = Number(fieldDraft.pdfHeight)
|
||||
if (
|
||||
!fieldDraft.pdfPage.trim() ||
|
||||
!fieldDraft.pdfX.trim() ||
|
||||
!fieldDraft.pdfY.trim() ||
|
||||
!fieldDraft.pdfWidth.trim() ||
|
||||
!fieldDraft.pdfHeight.trim() ||
|
||||
![page, x, y, width, height].every(Number.isFinite) ||
|
||||
!Number.isInteger(page) ||
|
||||
page < 1 ||
|
||||
x < 0 ||
|
||||
y < 0 ||
|
||||
width <= 0 ||
|
||||
height <= 0
|
||||
) {
|
||||
setMessage('患者签名位置须填写有效页码及 PDF point 坐标', 'warn')
|
||||
return
|
||||
}
|
||||
pdfPosition = { page, x, y, width, height, coordinateSystem: fieldDraft.pdfCoordinateSystem }
|
||||
}
|
||||
|
||||
if (isOptionKind.value && new Set(options).size !== options.length) {
|
||||
setMessage('存在重复选项,请修改后重试', 'warn')
|
||||
return
|
||||
@@ -261,6 +302,7 @@ function applyFieldDraft() {
|
||||
options,
|
||||
multi: options.length ? fieldDraft.multi : undefined,
|
||||
risk: fieldDraft.risk.trim() || undefined,
|
||||
pdfPosition,
|
||||
})
|
||||
|
||||
if (!changed) {
|
||||
@@ -307,17 +349,20 @@ const stats = computed(() => {
|
||||
* 只挡"确定是错的":选项类少于两个选项、同名占位符写重复。
|
||||
* 不挡"没有签名位"这类业务判断——那属于提示,不该拦住保存。
|
||||
*/
|
||||
function validate(): string | null {
|
||||
function validate(requireSignaturePosition = true): string | null {
|
||||
const broken = detectedFields.value.find(
|
||||
(field) =>
|
||||
(field.kind === 'choice' || field.kind === 'checkbox') &&
|
||||
field.options.length === 1,
|
||||
(field) => (field.kind === 'choice' || field.kind === 'checkbox') && field.options.length === 1,
|
||||
)
|
||||
|
||||
if (broken) {
|
||||
return `「${broken.label}」只配了一个选项,勾选类至少要两个(或删掉选项改成单个勾选框)`
|
||||
}
|
||||
|
||||
const patientSignature = detectedFields.value.find((field) => field.id === 'signature')
|
||||
if (requireSignaturePosition && !patientSignature?.pdfPosition) {
|
||||
return '请选中“患者签名”占位符,配置其 PDF 页码和签名位置后再保存'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -342,7 +387,7 @@ const previewing = ref(false)
|
||||
|
||||
function togglePreview() {
|
||||
if (!previewing.value) {
|
||||
const error = validate()
|
||||
const error = validate(false)
|
||||
|
||||
if (error) {
|
||||
setMessage(error, 'warn')
|
||||
@@ -418,7 +463,11 @@ function save() {
|
||||
return
|
||||
}
|
||||
|
||||
emit('save', { id: props.template.id, contentHtml: valueHtml.value })
|
||||
emit('save', {
|
||||
id: props.template.id,
|
||||
contentHtml: valueHtml.value,
|
||||
needConfirm: needConfirm.value,
|
||||
})
|
||||
}
|
||||
|
||||
/** 关闭前先退出预览态:预览里正文是只读渲染,直接关掉会让人以为编辑内容没了 */
|
||||
@@ -431,7 +480,12 @@ function requestClose() {
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="dialog-mask" role="presentation" @click.self="requestClose">
|
||||
<section class="word-editor" role="dialog" aria-modal="true" aria-labelledby="word-editor-title">
|
||||
<section
|
||||
class="word-editor"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="word-editor-title"
|
||||
>
|
||||
<header class="dialog-header">
|
||||
<div class="dialog-title">
|
||||
<h2 id="word-editor-title">编辑 Word 文书</h2>
|
||||
@@ -464,13 +518,23 @@ function requestClose() {
|
||||
<button type="button" class="action-button" @click="togglePreview">
|
||||
{{ previewing ? '✏️ 返回编辑' : '👁 签署预览' }}
|
||||
</button>
|
||||
<span class="editor-stats">正文 {{ stats.chars }} 字 · {{ stats.fields }} 个填写项</span>
|
||||
<span class="editor-stats"
|
||||
>正文 {{ stats.chars }} 字 · {{ stats.fields }} 个填写项</span
|
||||
>
|
||||
<label class="confirm-setting">
|
||||
<input v-model="needConfirm" type="checkbox" />
|
||||
患者签署后需科室确认
|
||||
</label>
|
||||
<span class="editor-tip">
|
||||
勾选项支持配置多个选项(可多选);导入 Word 后表格「勾选」列自动转为交互勾选项
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="message.text" class="editor-message" :class="message.tone ? `is-${message.tone}` : ''">
|
||||
<p
|
||||
v-if="message.text"
|
||||
class="editor-message"
|
||||
:class="message.tone ? `is-${message.tone}` : ''"
|
||||
>
|
||||
{{ message.text }}
|
||||
</p>
|
||||
|
||||
@@ -525,9 +589,44 @@ function requestClose() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedField.id === 'signature' && fieldDraft.kind === 'signature'"
|
||||
class="fp-row signature-position-row"
|
||||
>
|
||||
<b class="fp-title">患者签名 PDF 位置</b>
|
||||
<span class="fp-hint">PDF point;页码从 1 开始</span>
|
||||
<label class="fp-field"
|
||||
><span>页码</span><input v-model="fieldDraft.pdfPage" inputmode="numeric"
|
||||
/></label>
|
||||
<label class="fp-field"
|
||||
><span>X</span><input v-model="fieldDraft.pdfX" inputmode="decimal"
|
||||
/></label>
|
||||
<label class="fp-field"
|
||||
><span>Y</span><input v-model="fieldDraft.pdfY" inputmode="decimal"
|
||||
/></label>
|
||||
<label class="fp-field"
|
||||
><span>宽</span><input v-model="fieldDraft.pdfWidth" inputmode="decimal"
|
||||
/></label>
|
||||
<label class="fp-field"
|
||||
><span>高</span><input v-model="fieldDraft.pdfHeight" inputmode="decimal"
|
||||
/></label>
|
||||
<label class="fp-field">
|
||||
<span>坐标系</span>
|
||||
<select v-model="fieldDraft.pdfCoordinateSystem">
|
||||
<option value="TOP_LEFT">左上为原点</option>
|
||||
<option value="PDF_BOTTOM_LEFT">左下为原点</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="isOptionKind" class="fp-row">
|
||||
<span v-for="(_, index) in fieldDraft.options" :key="index" class="option-item">
|
||||
<input v-model="fieldDraft.options[index]" type="text" maxlength="40" placeholder="选项内容" />
|
||||
<input
|
||||
v-model="fieldDraft.options[index]"
|
||||
type="text"
|
||||
maxlength="40"
|
||||
placeholder="选项内容"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="option-remove"
|
||||
|
||||
@@ -183,8 +183,7 @@ const filteredTemplates = computed(() => {
|
||||
|
||||
return allTemplates.value.filter((template) => {
|
||||
const matchesDepartment = !filters.department || template.departmentKey === filters.department
|
||||
const matchesStatus =
|
||||
filters.status === 'all' || readLibraryStatus(template) === filters.status
|
||||
const matchesStatus = filters.status === 'all' || readLibraryStatus(template) === filters.status
|
||||
// 搜索范围照原型:模板名 / 编号 / 科室 / 分类,外加检查类型与关联项目编码。
|
||||
// 后两者是卡片上看得见的字段,搜不到会让人以为这份文书不存在。
|
||||
const matchesKeyword =
|
||||
@@ -710,6 +709,7 @@ function updateLocalContent(form: DocumentContentForm) {
|
||||
}
|
||||
|
||||
located.document.contentHtml = form.contentHtml
|
||||
located.document.needConfirm = form.needConfirm
|
||||
located.document.updatedAt = new Date().toISOString()
|
||||
}
|
||||
|
||||
@@ -739,7 +739,10 @@ async function handleContentSubmit(form: DocumentContentForm) {
|
||||
// 新建的文书还没有任何版本:第一个版本用 createTemplateWithContent 的版本那一步等价路径,
|
||||
// 这里统一走 createTemplateVersion(见 updateTemplateContent),
|
||||
// 后端会把它当成该模板的第一个版本
|
||||
await updateTemplateContent(form.id, { contentHtml: form.contentHtml })
|
||||
await updateTemplateContent(form.id, {
|
||||
contentHtml: form.contentHtml,
|
||||
needConfirm: form.needConfirm,
|
||||
})
|
||||
closeContentDialog()
|
||||
await loadLibrary()
|
||||
ElMessage.success('正文已保存为新版本')
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface DocumentContentForm {
|
||||
id: string
|
||||
/** 正文 HTML(含结构化占位符 <span data-field>) */
|
||||
contentHtml: string
|
||||
needConfirm: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user