Files
xh-medical-sign-web/clinical-web/scripts

scripts

只用于本地开发与验证,不参与打包。

verify-pages.cjs — 浏览器真实渲染验证

为什么需要:lint / vue-tsc / build 通过 ≠ 页面能跑。数据加载失败导致列表空白、 解析逻辑只处理部分数据、组件渲染时抛错,这些问题在类型检查里完全看不出来, 只有真实打开浏览器渲染才发现。

用法:

# 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,不要装进项目:

cd C:/Users/<you>/.workbuddy-ai/binaries/node/workspace
<node>/npm.cmd install ws

运行脚本时指定 NODE_PATH 指向该目录:

NODE_PATH=C:/Users/<you>/.workbuddy-ai/binaries/node/workspace/node_modules \
  node scripts/verify-pages.cjs

验证真实模式(关掉演示数据)另起一个 server:

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 一致):

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 一致):

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,后续切页取

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:

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 模块:

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 模块两头都正常:

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 是临时文件,同样已忽略。