feat(signing): 将签署占位符改为原子节点并支持多选选项组
This commit is contained in:
@@ -3,14 +3,28 @@ import '@wangeditor/editor/dist/css/style.css'
|
||||
|
||||
import { onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { DomEditor } from '@wangeditor/editor'
|
||||
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
|
||||
|
||||
import type { SigningFieldDef } from '@/api/workbench/types'
|
||||
import { normalizePastedHtml } from '@/utils/rich-text'
|
||||
import { decorateSigningMarkers, registerSigningEditorMarks } from '@/utils/signing-editor-marks'
|
||||
import {
|
||||
createSignFieldNode,
|
||||
isSignFieldElement,
|
||||
readFieldFromSignFieldNode,
|
||||
registerSigningFieldNode,
|
||||
removeSignFieldNodes,
|
||||
SIGN_FIELD_CHIP_CLASS,
|
||||
SIGN_FIELD_TYPE,
|
||||
updateSignFieldNodes,
|
||||
} from '@/utils/signing-field-node'
|
||||
|
||||
// 渲染钩子是 wangEditor 的全局注册表,必须在编辑器创建前挂上,
|
||||
// 否则首帧渲染出来的占位符/勾选框还是普通文字(要等下一次重渲染才变胶囊)。
|
||||
// 这两个都是 wangEditor 的全局注册表,必须在编辑器创建前挂上:
|
||||
// - 渲染钩子决定旧式 {…} 占位符与 □ 勾选框的显示(要等下一次重渲染才生效就晚了);
|
||||
// - 元素节点决定结构化占位符能不能被还原成原子胶囊(首帧就要是胶囊,否则看起来像丢内容)。
|
||||
registerSigningEditorMarks()
|
||||
registerSigningFieldNode()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
@@ -18,6 +32,8 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
/** 点中了正文里的结构化占位符,交给上层打开「填写项配置」 */
|
||||
'select-field': [field: SigningFieldDef]
|
||||
}>()
|
||||
|
||||
const editorRef = shallowRef<IDomEditor>()
|
||||
@@ -96,6 +112,44 @@ function handleCreated(editor: IDomEditor) {
|
||||
editableRef.value = editable
|
||||
editable.addEventListener('pointerdown', markUserCaret)
|
||||
editable.addEventListener('keydown', markUserCaret)
|
||||
editable.addEventListener('click', handleFieldClick)
|
||||
}
|
||||
|
||||
/**
|
||||
* 点中正文里的占位符胶囊 → 整块选中并通知上层打开配置面板。
|
||||
*
|
||||
* 必须再判一次 toSlateNode 的结果:旧式 {…} 占位符被 decorate 渲染成了
|
||||
* 同名的 .ms-sign-chip,它只是个叶子、不是原子节点,
|
||||
* 点它时什么都不该发生(它的配置从上方「填写项」列表里改)。
|
||||
*/
|
||||
function handleFieldClick(event: Event) {
|
||||
const editor = editorRef.value
|
||||
const target = event.target
|
||||
|
||||
if (!editor || !(target instanceof Element)) {
|
||||
return
|
||||
}
|
||||
|
||||
const chip = target.closest(`.${SIGN_FIELD_CHIP_CLASS}`)
|
||||
|
||||
if (!chip) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const node = DomEditor.toSlateNode(editor, chip)
|
||||
|
||||
if (!isSignFieldElement(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
editor.focus()
|
||||
editor.select(DomEditor.findPath(editor, node))
|
||||
emit('select-field', readFieldFromSignFieldNode(node))
|
||||
} catch {
|
||||
// 解不出节点(点到装饰件、或节点已不在模型里)就静默放过:
|
||||
// 点击本身不该报错,保持浏览器默认行为即可
|
||||
}
|
||||
}
|
||||
|
||||
function getText(): string {
|
||||
@@ -116,11 +170,68 @@ function insertText(text: string) {
|
||||
editor.insertNode({ text })
|
||||
}
|
||||
|
||||
defineExpose({ getText, insertText })
|
||||
/**
|
||||
* 在光标处插入一个**结构化占位符**(Slate 元素,不是文本)。
|
||||
*
|
||||
* 与 insertText 的区别就是这个模块存在的理由:它进的是原子节点,
|
||||
* 点一下即整块选中、一次退格整块删除;字段键、类型、必填、选项、多选
|
||||
* 都挂在元素上,导出正文时还原成 `<span data-field>`,
|
||||
* 与签署渲染端(utils/signing-document)共用同一份契约。
|
||||
*/
|
||||
function insertField(field: SigningFieldDef) {
|
||||
const editor = editorRef.value
|
||||
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
|
||||
editor.focus(!userPlacedCaret.value)
|
||||
|
||||
// 当前选中的就是占位符时先右移一格:否则新节点会插进上一个占位符内部,
|
||||
// 连插两个签名位会互相嵌套(内置图片节点也是这么处理的)
|
||||
if (DomEditor.getSelectedNodeByType(editor, SIGN_FIELD_TYPE)) {
|
||||
editor.move(1)
|
||||
}
|
||||
|
||||
editor.insertNode(createSignFieldNode(field))
|
||||
}
|
||||
|
||||
/**
|
||||
* 就地改写某个字段的每一处占位符,返回命中的节点数。
|
||||
*
|
||||
* 面板改字段走这里而不是「改 HTML 字符串再塞回编辑器」:
|
||||
* 字符串往返要经过 setHtml,而 setHtml 内部会 `e.focus()`——它会把缓存的上次选区装回去,
|
||||
* 于是新正文的节点还没绑上 DOM 就先做了一次 DOM 同步,抛
|
||||
* `Cannot resolve a DOM node from Slate node`(实测必现,且与光标在哪儿无关)。
|
||||
* 改模型既保住光标,也彻底绕开这条路径。字段 id 不变,历史取值不受影响。
|
||||
*/
|
||||
function updateField(fieldId: string, patch: Partial<Omit<SigningFieldDef, 'id'>>): number {
|
||||
const editor = editorRef.value
|
||||
|
||||
if (!editor) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return updateSignFieldNodes(editor, fieldId, patch)
|
||||
}
|
||||
|
||||
/** 就地删掉某个字段的每一处占位符,返回命中的节点数 */
|
||||
function removeField(fieldId: string): number {
|
||||
const editor = editorRef.value
|
||||
|
||||
if (!editor) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return removeSignFieldNodes(editor, fieldId)
|
||||
}
|
||||
|
||||
defineExpose({ getText, insertText, insertField, updateField, removeField })
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editableRef.value?.removeEventListener('pointerdown', markUserCaret)
|
||||
editableRef.value?.removeEventListener('keydown', markUserCaret)
|
||||
editableRef.value?.removeEventListener('click', handleFieldClick)
|
||||
editorRef.value?.destroy()
|
||||
})
|
||||
</script>
|
||||
@@ -205,8 +316,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
/* ---- 占位符 / 勾选框胶囊 ----
|
||||
utils/signing-editor-marks 把 {患者姓名} 切成「{ / 患者姓名 / }」三段叶子、
|
||||
把 □ 切成一 段叶子,这里负责把它们渲染成一眼能认出是"待签署内容"的形态。 */
|
||||
现在有两种占位符胶囊,样式刻意保持一致(都代表"待签署内容",不该靠外观区分重要性):
|
||||
|
||||
① 结构化占位符(.ms-sign-node):Slate 元素节点,正文里存成 <span data-field>。
|
||||
标签文字走下面的 ::before,DOM 里没有可落光标的文字,一次退格整块删除。
|
||||
② 旧式 {患者姓名}(.ms-sign-brace + .ms-sign-chip):decorate 出来的叶子,
|
||||
只是显示成胶囊,本质还是文字,要按多次退格。存量模板打开后会被升级成 ①。 */
|
||||
|
||||
/* 大括号:只缩小调淡,不移除。这两个字符必须留在 DOM 里参与光标偏移计算,
|
||||
隐藏它们会让点击胶囊时的落点整体算错,也会让退格静默删掉看不见的字符 */
|
||||
@@ -238,6 +353,25 @@ onBeforeUnmount(() => {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 结构化占位符的标签文字:走 CSS 内容而不是 DOM 文本节点。
|
||||
元素是 void,DOM 里只有它自己与一个绝对定位的零宽占位叶子;
|
||||
若在这里放真实文字,浏览器原生光标会落进文字里,
|
||||
一次退格就只删掉一个字符 —— 正好破坏"一次退格整块删除"这个设计目标 */
|
||||
.pane-content :deep(.ms-sign-node)::before {
|
||||
content: attr(data-sign-label);
|
||||
}
|
||||
|
||||
/* 选中态:点一下即整块选中,给个明确的反色,让人知道接下来的退格会删掉整块 */
|
||||
.pane-content :deep(.ms-sign-node.is-selected) {
|
||||
color: #fff;
|
||||
background: var(--brand);
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.pane-content :deep(.ms-sign-node.is-selected)::after {
|
||||
border-left-color: rgb(255 255 255 / 35%);
|
||||
}
|
||||
|
||||
/* 类型角标:签名/日期/文本……让科室不点开也知道这一项要填什么。
|
||||
所有占位符共用一个样式,靠这个角标区分类型,不再按必填/签名位分色 */
|
||||
.pane-content :deep(.ms-sign-chip)::after {
|
||||
|
||||
Reference in New Issue
Block a user