Files
xh-medical-sign-web/clinical-web/scripts/README.md
T
yelan b967077873 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」说明渲染通道的选择。
2026-09-18 16:40:08 +08:00

263 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# scripts
只用于本地开发与验证,不参与打包。
## verify-pages.cjs — 浏览器真实渲染验证
**为什么需要**:`lint` / `vue-tsc` / `build` 通过 ≠ 页面能跑。数据加载失败导致列表空白、
解析逻辑只处理部分数据、组件渲染时抛错,这些问题在类型检查里完全看不出来,
只有真实打开浏览器渲染才发现。
**用法**:
```bash
# 1. 起 dev server(另开一个终端)
npm run dev
# 2. 抓验证码图片,然后自己看图认出数字
node scripts/login.cjs
# 图片在 scripts/.captcha.png,例如显示 8944
# 3. 带验证码登录,会话写入 scripts/.session.json
node scripts/login.cjs 8944
# 4. 跑渲染验证:打开页面、截图、断言关键内容、收集运行时错误
node scripts/verify-pages.cjs
```
截图输出到仓库根目录的 `screenshots/`。
**环境变量**:
| 变量 | 默认值 | 说明 |
| ------------------------------------- | ------------------------------------------ | ---------------------- |
| `VERIFY_URL` | `http://127.0.0.1:5173/` | 目标地址 |
| `VERIFY_ROUTES` | `/management/documents,/workbench/signing` | 要验证的路由,逗号分隔 |
| `VERIFY_OUT` | `<repo>/screenshots` | 截图输出目录 |
| `CHROME_PATH` | Chrome 默认安装路径 | Chrome 可执行文件 |
| `VERIFY_USERNAME` / `VERIFY_PASSWORD` | `admin` / `admin` | 登录账号 |
| `VERIFY_BASE_URL` | `http://127.0.0.1:5173` | 登录接口所在地址 |
**依赖**:需要 `ws`(CDP 通信)。装到隔离 workspace,不要装进项目:
```bash
cd C:/Users/<you>/.workbuddy-ai/binaries/node/workspace
<node>/npm.cmd install ws
```
运行脚本时指定 `NODE_PATH` 指向该目录:
```bash
NODE_PATH=C:/Users/<you>/.workbuddy-ai/binaries/node/workspace/node_modules \
node scripts/verify-pages.cjs
```
**验证真实模式**(关掉演示数据)另起一个 server:
```bash
VITE_MOCK_DOCUMENTS=false VITE_MOCK_SIGNING=false npx vite --port 5199
VERIFY_URL=http://127.0.0.1:5199/ node scripts/verify-pages.cjs
```
## verify-editor-marks.cjs — 占位符 / 勾选框胶囊验证
**为什么需要**:占位符胶囊(`utils/signing-editor-marks`)是**只改渲染、不改数据**的实现——
它靠 wangEditor 的 `decorate` 把文本切成叶子再挂样式。这种写法有三类问题编译期完全看不出来:
- 叶子切分 / 样式挂载没生效 → 胶囊根本没渲染出来(看起来"功能没做");
- 装饰破坏了 Slate 的光标偏移计算 → 点击胶囊后光标落点错位、编辑时字符插到别处;
- 装饰挂上去容易、**摘下来难** → 内容删了,DOM 上的类名还赖着不走(见下方「踩过的坑 · 5」)。
所以必须真实打开编辑器,一边看渲染一边点进去打字、删字符。
**用法**(登录步骤同 `verify-pages.cjs`,注意端口要和实际 dev server 一致):
```bash
node scripts/login.cjs # 抓验证码,看图认数字
node scripts/login.cjs 8944 # 登录
VERIFY_URL=http://127.0.0.1:5174/ node scripts/verify-editor-marks.cjs
```
脚本会:打开文档库 → 新增文档(弹出 Word 编辑器)→ 依次插入 5 个占位符与勾选框 →
输出编辑区探针与「占位符检查」面板文案 → 点进胶囊中部插入一个 `X` → 按退格删掉最后插入的
勾选框 → 再 dump 一遍叶子结构 → 打印 PASS/FAIL 台账并以退出码反映结果。
**断言清单**(全通过才算过,失败会列出未通过项并 `exit 1`):
| 断言 | 它防的是什么 |
| ---------------------------------------------------- | ----------------------------------------------- |
| 占位符 / 勾选框都渲染成胶囊 | 装饰完全没生效,功能像没做 |
| 每个胶囊都有类型角标(`::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`(插入字符后)、`05b-editor-marks-delete.png`(退格后)。
## verify-signature-click.cjs — 点击文中签名位采集签名
**为什么需要**:签名位的点击采集横跨三层(`utils/signing-document` 渲染 → `SigningDocumentInteractive`
事件委托 → 签署页弹窗与回灌),每一层都可能在编译期看不出问题:
- 渲染层挂了 `data-sign-action` 但没人监听 → 看着像按钮,点了没反应;
- 事件委托的 `closest()` 没做容器校验 → 容器外的事件也会误触发;
- 回灌走了「整篇重渲染」而不是就地改写 → 正在输入的填写项连同光标一起丢。
所以必须真的用鼠标点、真的在签字板上画一笔、再看签名图有没有回到那一个签名位上。
**用法**(登录步骤同上,端口要和实际 dev server 一致):
```bash
node scripts/login.cjs # 抓验证码,看图认数字
node scripts/login.cjs 8944 # 登录
VERIFY_URL=http://127.0.0.1:5173/ node scripts/verify-signature-click.cjs
```
脚本会:打开签署工作台 → 选一个「待签署」任务 → 输出签名位探针 → 真实点击「医师签名」→
在签字板上画一笔并确认 → 复检回显与页脚提示 → 用键盘 `Enter` 触发「患者签名」的采集入口。
**关键断言**:
- 空签名位必须同时有 `data-sign-action=signature`、`role=button`、`tabindex=0`、
`cursor: pointer` 与 `✍ 点击签名` 文案——少一样就是「看着能点其实不能点」;
- 采集后该签名位出现 `img.sign-doc-signature` 且 `src` 是 `data:image/png`,
同时采集入口被摘掉(`data-sign-action` 与 `sign-field--actionable` 都消失);
- **患者签名位的采集入口不能被误伤**——医师签名采完,患者签名还得能点;
- 页脚不再把已采集的签名位列进「还有签名位待采集」。
截图:`06-signature-actions.png`(初始:两个签名位都是可点入口)、
`07-signature-dialog.png`(弹窗标题指向被点的签名位)、`08-signature-applied.png`(回显 + 页脚更新)、
`09-signature-keyboard.png`(键盘触发)。
## 踩过的坑
改脚本前先读这段,都是实际踩出来的。
### 1. 登录不要 stub,走真实登录
MEDISIGN 后端对无效 token 返回 **401**,而 `utils/request.ts` 的响应拦截器收到 401 会
`clearAuthStorage()` + 跳登录页。伪造会话很容易被踢出去。
如果确实要 stub XHR(比如只想验证某个组件),**必须实现完整响应**:
`getAllResponseHeaders` / `getResponseHeader` / `onloadend` 一个都不能少。
只设 `responseText` 会让 axios 在解析响应头时抛错、**Promise 永不 settle**,
路由守卫的 `await` 一直挂着 → `#app` 只剩 `<!---->`、**整页空白**。
这个现象极易被误判成「页面渲染挂了」,实际是验证脚本自己的 bug。
### 2. 验证码是强制的
不传 `captchaId` / `captchaCode` 会报「验证码错误或已过期」。
验证码有时效,抓图和登录要连着做。
### 3. CDP 连续导航在 SPA 里不可靠
`Page.navigate` 连续调用时:
- `pushState` 跳转**不触发** `Page.loadEventFired`
- 轮询 `document.readyState` 会立刻通过,但读到的是**旧文档**
结果就是断言拿到上一个页面的内容,看起来像「导航了但没渲染」。
正确做法(脚本已采用):只做**一次**整页 `Page.navigate`,后续切页取
```js
document.getElementById('app').__vue_app__.config.globalProperties.$router
```
调 `router.push()`。注意 `history.pushState` + 手动派发 `popstate` 对 Vue Router **无效**。
### 4. 文件用 .cjs 后缀
`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` 中忽略,**不要提交**。
`.captcha.json` / `.captcha.png` 是临时文件,同样已忽略。