🔗 配套 e-code 应用源码:https://github.com/OnesZhang/e10-ecode-workflow-action-guide
📌
该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。
这次的关键判断只有一句话:不要按按钮名称绑定。名称是给人看的,
menutype才是给程序看的。

结论先行
- 流程页右上角按钮与「更多」菜单的稳定标识是
menutype(SUBMIT/SAVE_DATA/REMIND…),用平台官方口子window.weappWorkflow.getFlowPageSDK().baseStore.operMenus就能拿到,不需要展开菜单、不需要点任何按钮; istop === 1的就是右上角按钮,其余进「更多」菜单 —— 分组不用去猜 DOM 结构;- 面板里显示的名称取平台当前标题(管理员改过名就显示改后的名字,永远和屏幕一致),释义按
menutype查表 —— 这就是「改名不失效」的关键; - 判断”这是不是一个流程页”也不能看 URL:同一个流程可以是
/sp/workflow/flowpage/view/{id},也可以是/sp/ebdfpage/card/…/{id};判据要落在平台注入的流程上下文baseParam.workflowId上; - 语言用
weappUtils.getLang(),每 800ms 重读一次,切换界面语言不需要刷新页面; - 位置千万别写死
top:带「第 N 条(共 M 条)」条幅的页面会把工具条整体下压(约 30px → 54px),写死偏移就会压住「更多」;而改贴容器底边又会偏下,因为容器底部比按钮低 13px。
一句话:名称负责显示,操作码负责语义,页面靠上下文识别,位置靠量不靠猜。
一、需求:按钮「看得见但不认识」
流程详情页右上角长这样:提交、保存、删除,再加一个 更多;点开「更多」还有十几项:复制流程、关注、转发、催办、消息接收设置、事项分享、事项群聊、打印、打印日志、复制链接、设为待办事项、流程信息、表单日志、委托、抄送、标记标签……
对天天用的人来说这是肌肉记忆;对偶尔走一次流程的业务用户,「转发」和「传阅」有什么区别、「催办」和「督办」差在哪,基本只能靠猜。需求就是:在右上角放一个入口,点开用用户自己的语言说明每个按钮是干什么的。
需求本身很朴素,但它埋了一个必须一开始就想清楚的坑。
二、关键判断:不能按按钮名称绑定
最初的原型是这么写的:读 DOM 里按钮的文案,拿文案去字典里查。
// ❌ 不要这样const label = button.textContent.trim(); // 「提交」const entry = DICT[label]; // 靠名称查表这个写法有两个致命问题,而且两个都一定会发生:
- 按钮名称可以被管理员自定义。E10 的流程节点操作菜单里,名称是配置项,管理员完全可以把「保存」改成「暂存草稿」;
- 名称随界面语言变化。同一个按钮,中文界面是「提交」,英文界面是
Submit。
任何以名称为 key 的方案,在这两种情况下都会静默失效——不报错,只是查不到、不显示。二开补丁里最怕的就是这种”不报错但没用”。
所以第一件事是:找到平台给这个操作分配的稳定标识。
三、稳定标识:operMenus 里的 menutype
平台 JS-SDK 文档里有一句不起眼但很关键的话:
可通过控制台输入
window.weappWorkflow.getFlowPageSDK().baseStore.operMenus查看当前流程下操作按钮清单
实测下来它返回的就是当前节点可用的操作数组,每一项长这样:
window.weappWorkflow.getFlowPageSDK().baseStore.operMenus// [// { menutype: 'SUBMIT', title: '提交', istop: 1, useable: 1, orderid: 0, menuKey: 2 },// { menutype: 'SAVE_DATA', title: '保存', istop: 1, useable: 1, orderid: 5, menuKey: 23 },// { menutype: 'DELETE', title: '删除', istop: 1, useable: 1, orderid: 6, menuKey: 16 },// { menutype: 'IMPORT', title: '复制流程', istop: 0, useable: 1, orderid: 0 },// { menutype: 'WATCH', title: '关注', istop: 0, useable: 1, orderid: 1 },// { menutype: 'REMIND', title: '催办', istop: 0, useable: 1, orderid: 13 },// // … 共 19 项:3 个 istop=1 + 16 个 istop=0// ]三个字段分工非常清楚:
| 字段 | 含义 | 为什么关键 |
|---|---|---|
menutype | 稳定操作码(SUBMIT / SAVE_DATA / REMIND …) | 改名、切语言都不变,这才是绑定依据 |
istop | 1 = 渲染成右上角按钮,否则在「更多」菜单里 | 分组规则与平台 UI 完全一致,不用猜 |
title | 平台当前的显示名 | 只用于显示,永远和屏幕上一致 |
拿到这个数组之后,「哪些按钮、分到哪一组」就全是数据问题了,一行 DOM 文案都不用读。
验证:改名之后还准不准?
这是整件事最值得实测的一步。把「提交」改名为「测试按钮改名」,「催办」改名为「提醒一下」之后再看 operMenus:
SUBMIT → title: '测试按钮改名' ← 名称变了REMIND → title: '提醒一下' ← 名称变了menutype 一个都没变,19 项依然 19/19 命中。面板的表现正是我们想要的:
| menutype(不变) | 屏幕/面板上显示的名字 | 面板给出的释义 |
|---|---|---|
SUBMIT | 测试按钮改名 | 节点处理人填写完表单后提交,流程流转到下一处理人或节点。 |
REMIND | 提醒一下 | 提醒当前未处理人、审批人、发起人、抄送人或指定人尽快处理。 |
名字跟着平台走,释义跟着操作码走 —— 管理员怎么改名都不会讲错功能。
拿不到
getFlowPageSDK()(比如非流程详情页)时,退回读 DOM:右上角读.ui-title-right-container button, 「更多」菜单项读容器里 Menu 组件的data-weapp-agent-id再走平台 Agent Bridge 的__WEAPP_AGENT_BRIDGE__.describe(id)(同样不用展开菜单),此时按名称匹配。两个来源都取不到 → 一个节点都不注入。
四、页面判定:同样不能看 URL
做完上面这些,我以为已经稳了,直到遇到另一个流程页:
/sp/ebdfpage/card/0/1131336631151837202/1326512499322241028它是 e-builder 的流程卡片页 —— 同一个流程的另一种呈现形式,右上角同样是一排流程操作按钮。
但原来的实现是按 /sp/workflow/ 前缀判断的,于是这个页面一个节点都没注入。
于是我把相关页面全部量了一遍,想找一个站得住的判据:
| 页面 | URL | getFlowPageSDK() | baseStore | baseParam.workflowId | 动作条 |
|---|---|---|---|---|---|
| 经典流程详情 | /sp/workflow/flowpage/view/{id} | object | ✅ | ✅ | ✅ |
| e-builder 流程卡片 | /sp/ebdfpage/card/0/{objId}/{requestId} | object | ✅ | ✅ | ✅ |
| 发起流程 | /sp/workflow/newflow | object | ✗ | — | ✗ |
| 流程列表 | /workflow/list/mine、/workflow/list/center | object | ✗ | — | ✗ |
| e-builder 设计器 | /sp/ebdapp/detail/…/flow/nodeInfo | object | ✗ | — | ✅ ← 有动作条 |
| 门户 | /portal/view/… | object | — | — | ✗ |
两个「看起来能用」的判据都被否掉了:
window.weappWorkflow.getFlowPageSDK()在任何页面都返回对象,连门户和列表页也是 —— 它只证明模块加载了,不证明这是流程页;- 右上角动作条
.ui-title-right-containere-builder 设计器页也有 —— 拿它判断会误伤。
最后站住的判据是**「平台有没有给这个页面注入流程上下文」**:
function isWorkflowPage() { const sdk = window.weappWorkflow && window.weappWorkflow.getFlowPageSDK ? window.weappWorkflow.getFlowPageSDK() : null; const param = sdk && sdk.baseStore ? sdk.baseStore.baseParam : null; return !!(param && String(param.workflowId || '').trim());}上表里只有两个流程页有这个字段,其余页面连 baseStore 都没有。它的语义也正好是我们想要的:
有流程上下文 = 是流程页,与 URL 长什么样无关 —— 以后再出新的页面形态(卡片、弹窗、自定义容器)都不用改判据。
menutype是「别拿会变的按钮名称当键」,workflowId是「别拿会变的 URL 当键」。 两次都是同一个教训:判据要落在平台已经给出的语义上,而不是人眼看到的表象上。
entry.js 用 1 秒轮询这个判据(前置脚本执行时平台上下文可能还没就绪),命中后加载业务模块并停掉轮询;
模块自己每 800ms 再判一次,所以 SPA 路由进出流程页会自动挂载 / 卸载。
五、语言:跟着平台走,而不是自己做个开关
const lang = window.weappUtils.getLang(); // 实测 'zh_CN' / 'en_US'const isZh = String(lang || '').toLowerCase().indexOf('zh') === 0;zh* 走中文、其余(含取不到)走英文。释义表里每条都带中英两份:
{ zh: '提交', en: 'Submit', zd: '节点处理人填写完表单后提交,流程流转到下一处理人或节点。', ed: 'Handler completes and submits the form; the workflow routes to the next assignee or node.' }模块每 800ms 重读一次语言,所以切换界面语言时不需要刷新页面,面板自己就变了 —— 这点是实测出来的:从 zh_CN 切到 en_US,面板标题、分组名、20 行释义在下一个 tick 全部变英文。
面板自身的文案(标题、分组名、关闭按钮)也走同一套语言表。
六、定位这个坑:写死 top 会翻两次车
第一版把入口钉死在 top: 52px,在普通流程页看着挺好。直到遇到一个带了「第 1 条(共3条) 上一条/下一条」小条幅的页面:
| 普通流程页 | 带条幅的页面 | |
|---|---|---|
| 动作条容器高度 | 55px | 84px(被条幅整体下压) |
| 按钮实际底边 | 42px | 72px |
写死 top: 52px 的结果 | 刚好落在按钮下方 | 压在「更多」按钮上 |
于是改成”按真实位置量”:
// 取动作条内所有 button 的真实底边,+8pxlet bottom = 0;bar.querySelectorAll('button').forEach(btn => { const rect = btn.getBoundingClientRect(); if (rect.height > 0 && rect.bottom > bottom) bottom = rect.bottom;});const top = bottom > 0 ? Math.round(bottom + 8) : 52; // 量不到才回落launcher.style.top = top + 'px';panel.style.top = (top + 36) + 'px';第二个坑:贴「容器」底边会偏下
改完第一版我贴的是容器的底边(bar.getBoundingClientRect().bottom + 8),结果用户反馈”没有条幅的时候按钮有点靠下”。
量一下就明白了:
容器 .ui-title-right-container 底边 55px里面按钮的真实底边 42px容器自身多出的内边距 13px ← 就是它容器自己有内边距,贴容器等于凭空多出 13px。改成贴按钮底边之后,两种页面距离按钮都是稳定的 8px(普通页 42→50,带条幅页 72→80)。
布局相关的位置,优先量目标元素(按钮)本身,而不是它的容器;容器什么时候加了 padding,你是不知情的。
七、其余几个实现细节
- 层级:入口
1050、面板1040—— 高于平台下拉浮层(实测999),低于平台弹窗。既不会被下拉盖住,也不会压住对话框。 - 自愈:平台在页面初始化/重渲染时会整块替换节点,实测遇到过”入口被瞬间抹掉”。每次 tick 先检查
document.body.contains(root),不在就重新挂。 - 幂等:
entry.js只做/sp/workflow/前缀判断 +asyncImport异步加载业务模块,加载成功即停掉自己的轮询;组件加载失败只打日志,页面保持平台原样。 - 零副作用:不点击、不提交、不改平台节点。检测结果签名不变就不动 DOM,避免无意义重排。
- 什么都不显示也是一种正确:页面没有流程上下文(比如流程列表页),或者当前页面没有任何可解释的操作 → 不注入任何节点。
八、生效范围与验证
生效范围由流程上下文决定,不由 URL 决定:
| 页面 | baseParam.workflowId | 表现 |
|---|---|---|
经典流程详情 /sp/workflow/flowpage/view/{id} | ✅ | 显示入口(实测 20 行) |
e-builder 流程卡片 /sp/ebdfpage/card/0/{objId}/{requestId} | ✅ | 显示入口(该页当前只有 2 个操作 → 面板 3 行:更多 / 打印 / 标记标签) |
/sp/workflow/newflow 发起流程 | ✗ | 零节点 |
/workflow/list/mine、/workflow/list/center 列表 | ✗ | 零节点 |
e-builder 设计器 /sp/ebdapp/detail/…/flow/nodeInfo | ✗ | 零节点(虽然它也有动作条) |
门户 /portal/view/… | ✗ | 零节点 |
其余验证项:
| 项目 | 结论 | 证据 |
|---|---|---|
| 稳定绑定 | PASS | 改 2 个按钮名后仍是 19/19 按 menutype 命中 |
| 页面判定 | PASS | 两个流程页 workflowId 有值;四个非流程页(含「有动作条」的设计器页)判定为 false |
| 中英双语 | PASS | zh_CN / en_US 两种语言下面板全部文案正确 |
| 语言热切换 | PASS | 不刷新页面,面板随 getLang() 自动重渲染 |
| 定位 | PASS | 普通页按钮底边 42 → 入口 top=50;带条幅页 72 → top=80,距按钮均 8px |
| 交互 | PASS | 打开 → × → 重开 → Esc → 重开 → 点面板外,全部按预期关闭 |
| 非目标隔离 | PASS | 无流程上下文的页面:0 个自有节点、无全局副作用 |
| 与平台菜单互不干扰 | PASS | 应用加载后点「更多」,平台菜单正常展开全部菜单项 |
实测环境:@weapp/ecodesdk 0.1.54、@weapp/ui 0.0.389-hotfix11、Agent Bridge 0.2.0。
九、回滚与注意事项
- 平台侧回滚:在 ecode 控制台取消发布 / 停用该应用即可完全恢复;脚本与样式都由
.wfg-*自有类名触发,无全局副作用; - 本地侧回滚:删掉应用目录即可。
operMenus是这套方案的地基:它是平台官方口子,但字段(menutype/istop/useable)建议在升级后复测一遍;- 释义表要跟着业务走:表里没有的操作不会显示(不臆造含义)。新增动作只需补一行
menutype映射,查menutype的方式就是打印operMenus;- 位置每次 tick 都量:这既是它能适应条幅页的原因,也意味着如果平台把工具条改成懒加载/动画,需要重新确认时序;
- 页面判据依赖
baseParam.workflowId:万一平台以后给某些非实例页也注入了流程上下文,那些页面同样会出现入口;到那时把它和别的信号组合起来收紧即可。
核心其实只有一句话:别拿给人看的表象当程序的键 —— 按钮名会改会翻译,URL 会因页面形态而变,而 menutype 和 workflowId 不会。