fix(signing-editor-marks): 统一占位符样式并修复删除后 DOM 残留

自定义渲染器渲染钩子改走 data.attrs 挂类名与 title:data.class 会被
wangEditor 的规范化函数搬进 props 且到不了 class 模块(胶囊不渲染),
data.props.className 则因 props 模块只单向写入而摘不掉旧值,导致勾选框
退格删除后 DOM 仍残留 ms-sign-box 空壳。

同时移除必填标记相关的红角标、签名位专属橙色、插入按钮与「占位符检查」
面板里的 `*` / 「必填」文案,所有占位符共用一个样式。

verify-editor-marks.cjs 增加断言台账(PASS/FAIL 并反映退出码)、叶子结构
探针与退格删除回归,README 补充「踩过的坑 · 5」说明渲染通道的选择。
This commit is contained in:
yelan
2026-09-18 16:40:08 +08:00
parent 6034f4f8e2
commit b967077873
6 changed files with 348 additions and 311 deletions
+100 -11
View File
@@ -62,12 +62,13 @@ VERIFY_URL=http://127.0.0.1:5199/ node scripts/verify-pages.cjs
## verify-editor-marks.cjs — 占位符 / 勾选框胶囊验证
**为什么需要**:占位符胶囊(`utils/signing-editor-marks`)是**只改渲染、不改数据**的实现——
它靠 wangEditor 的 `decorate` 把文本切成叶子再挂样式。这种写法有两类问题编译期完全看不出来:
它靠 wangEditor 的 `decorate` 把文本切成叶子再挂样式。这种写法有三类问题编译期完全看不出来:
- 叶子切分 / 样式挂载没生效 → 胶囊根本没渲染出来(看起来"功能没做");
- 装饰破坏了 Slate 的光标偏移计算 → 点击胶囊后光标落点错位、编辑时字符插到别处。
- 装饰破坏了 Slate 的光标偏移计算 → 点击胶囊后光标落点错位、编辑时字符插到别处;
- 装饰挂上去容易、**摘下来难** → 内容删了,DOM 上的类名还赖着不走(见下方「踩过的坑 · 5」)。
所以必须真实打开编辑器,一边看渲染一边点进去打字。
所以必须真实打开编辑器,一边看渲染一边点进去打字、删字符。
**用法**(登录步骤同 `verify-pages.cjs`,注意端口要和实际 dev server 一致):
@@ -77,18 +78,25 @@ node scripts/login.cjs 8944 # 登录
VERIFY_URL=http://127.0.0.1:5174/ node scripts/verify-editor-marks.cjs
```
脚本会:打开文档库 → 新增文档(弹出 Word 编辑器)→ 依次插入 3 个占位符与 2 个勾选框 →
输出编辑区探针(胶囊数量/文案/配色/类型角标)与「占位符检查」面板文案 →
点进胶囊中部插入一个 `X`,验证字符确实落在占位符内部。
脚本会:打开文档库 → 新增文档(弹出 Word 编辑器)→ 依次插入 5 个占位符与勾选框 →
输出编辑区探针与「占位符检查」面板文案 → 点进胶囊中部插入一个 `X` → 按退格删掉最后插入的
勾选框 → 再 dump 一遍叶子结构 → 打印 PASS/FAIL 台账并以退出码反映结果。
**两个关键断言**:
**断言清单**(全通过才算过,失败会列出未通过项并 `exit 1`):
- `panelText` 显示「N 个填写项」——面板读的就是 `editor.getHtml()` 的序列化结果,
能数出填写项即证明**底层仍是 `{…}` 纯文本**,保存 / 签署链路的口径没被改动;
- 点进胶囊后插入 `X` 得到 `{患者X签名}`——证明装饰没有破坏光标偏移计算。
| 断言 | 它防的是什么 |
| ---------------------------------------------------- | ----------------------------------------------- |
| 占位符 / 勾选框都渲染成胶囊 | 装饰完全没生效,功能像没做 |
| 每个胶囊都有类型角标(`::after` 有文案) | 角标挂错通道(`content: attr(...)` 拿不到值) |
| 所有胶囊配色 / 边框 / 角标色**完全一致** | 又冒出「必填红」「签名位橙」这类分色分支 |
| 胶囊与插入按钮上都没有 `*`、面板里没有「必填」 | 必填标记从别的地方漏回来 |
| 面板能数出「N 个填写项」(N≠0) | 装饰把序列化结果改脏了,保存 / 签署链路口径被改 |
| 点进胶囊后插入 `X` 得到 `{…X…}` | 装饰破坏了 Slate 的光标偏移计算 |
| 退格后无 `.ms-sign-box`、无叶子残留 `ms-sign-*` 类名 | **本轮回归重点**:内容删了类名还挂着 |
| 退格后正文不再有 `□` | 数据层确实删掉了(区别于「只有 DOM 脏」) |
截图:`04-editor-marks.png`(整窗)、`04b-editor-marks-zoom.png`(编辑区 4 倍放大,用来核对胶囊细节)、
`05-editor-marks-caret.png`(插入字符后)。
`05-editor-marks-caret.png`(插入字符后)、`05b-editor-marks-delete.png`(退格后)。
## verify-signature-click.cjs — 点击文中签名位采集签名
@@ -167,6 +175,87 @@ document.getElementById('app').__vue_app__.config.globalProperties.$router
`package.json` 里有 `"type": "module"`,`.js` 会被当成 ES 模块,
而这里用 `require` 更简单,所以显式声明成 CommonJS。
### 5. 自定义渲染器里改类名 / 属性,只能走 `data.attrs`
这条是排查「插入勾选框后删不干净」时挖出来的,两个坑叠在一起,值得单独记一笔。
#### 坑一:`data.class` 会被规范化函数搬走,根本到不了 class 模块
wangEditor 在渲染前会用一个内部函数(压缩后叫 `RT`)**递归重写整棵 vnode 树的 data**:
```js
var LT = ["props", "attrs", "style", "dataset", "on", "hook"] // 保留键白名单
function RT(vnode) {
var data = vnode.data || {}
Object.keys(data).forEach(function (key) {
if (key === 'key') { vnode.key = data[key]; return }
if (LT.includes(key)) return // 白名单里的原样透传
if (key.startsWith('data-')) {
Object.assign(vnode.data.dataset, …) // data-* 归入 dataset
} else {
Object.assign(vnode.data.props, …) // 其余一律塞进 props
}
delete vnode.data[key] // 原键被删掉
})
vnode.children.forEach(RT) // 递归
}
```
`class` **不在白名单里**,所以 `data.class = { 'ms-sign-chip': true }` 会被搬成
`data.props.class` 并 `delete data.class` —— snabbdom 的 class 模块读的是 `data.class`,
读到 `undefined` 就什么都不做。**结果是类名压根不出现在 DOM 上,胶囊整个不渲染。**
(`attrs` 与 `dataset` 都在白名单里,能原样透传。)
#### 坑二:`data.props` 只单向写入,旧值摘不掉
wangEditor 内嵌的 snabbdom 用 `fg([yg, Ag, Tg, wg, xg, gg])` 初始化,六个模块依次是
**class / style / dataset / eventlisteners / props / attrs**。其中 `props` 模块:
```js
function propsModule(oldVnode, vnode) {
for (key in newProps) {
old === cur || (elm[key] = cur)
} // 只遍历「新」props
}
```
它从不清理已经消失的旧 prop;而 `className` / `title` 在 DOM 元素上是原型访问器,
`delete elm.className` 是个空操作。所以类名一旦通过 `props` 挂上去,就**再也摘不掉**:
勾选框被退格删掉后,那个叶子已经空了、`decorate` 也早已不再返回它,
DOM 里却还留着 `<span class="ms-sign-box" data-slate-zero-width="n">` ——
编辑器里表现为一个擦不掉的浅蓝小方块。
注意这两条是**先后被踩**的:最早用 `props.className`(渲染出来了,但删不掉),
改成 `data.class`(删得掉,但压根渲染不出来),最后才落到 `data.attrs`。
| 通道 | 挂得上吗 | 摘得掉吗 | 结论 |
| ---------------------- | ------------------ | -------- | -------------------------- |
| `data.props.className` | ✅ | ❌ | 会留下擦不掉的残留 |
| `data.class` | ❌(被 `RT` 搬走) | — | 完全不渲染 |
| `data.dataset` | ✅ | ✅ | 可用,适合放 `data-*` 标记 |
| **`data.attrs`** | ✅ | ✅ | **类名与 title 走这里** |
`attrs` 模块两头都正常:
```js
for (n in newAttrs) oldAttrs[n] !== v ? setAttribute(n, v) : …
for (n in oldAttrs) n in newAttrs || removeAttribute(n) // ← 摘得掉
```
#### 现象为什么长成「编辑器有问题、签署没问题」
`decorate` 侧其实一直是对的(数据层干净、签署侧按文本解析也一直正常),
出问题的只有渲染通道。用户的原话「在编辑中显示侧,签署的时候没问题」,
正是这个机制决定的:**问题不在数据,在渲染通道。**
排查手法:反编译 `node_modules/@wangeditor/editor/dist/index.esm.js`,
搜 `registerRenderStyle` / `fg([` / `removeAttribute` / `classList.remove`,
把 snabbdom 的六个模块和 `RT` 的白名单逐个打印出来看。
对应的回归断言在 `verify-editor-marks.cjs` 的「删掉勾选框后 DOM 不残留」一段。
## 安全提醒
`scripts/.session.json` 里是**真实 token**,已在 `.gitignore` 中忽略,**不要提交**。