本文摘要
泛微 E10(e-cology10)流程页右上角有一排操作按钮(提交/保存/删除/更多),「更多」菜单里还有十几项操作,业务用户看不懂;而按钮名称既可以被管理员自定义、也会随界面语言变化,因此任何按名称匹配的说明方案都会失效。本文给出的做法是做一个悬浮的 i 入口 + 下拉面板,数据来自平台官方口子 window.weappWorkflow.getFlowPageSDK().baseStore.operMenus,其字段 menutype 是稳定操作码(SUBMIT、SAVE_DATA、DELETE、REMIND 等),istop===1 表示渲染在右上角、否则进「更多」菜单,title 是平台当前显示名。实现上:面板每行的名称取平台 title(改名后与屏幕一致),释义按 menutype 查一张中英对照表;语言用 window.weappUtils.getLang(),zh* 走中文、其余走英文,每 800ms 重读一次,因此切换界面语言无需刷新页面;入口 top 不写死,取动作条内按钮的真实 bottom+8px,因为带「第 N 条(共 M 条)」条幅的页面会把工具条从约 30px 撑到 54px(实测写死 52px 会压住「更多」),而贴容器底边又会偏下(实测容器底部比按钮低 13px)。页面判定同样不看 URL:同一流程可以是 /sp/workflow/flowpage/view/{id},也可以是 e-builder 流程卡片页 /sp/ebdfpage/card/0/{objId}/{requestId},因此改用平台注入的流程上下文 getFlowPageSDK().baseStore.baseParam.workflowId 作为判据;实测 getFlowPageSDK() 在任何页面都返回对象(连门户都是),而动作条 .ui-title-right-container 在 e-builder 设计器页也存在,两者都不能用来判断;发起流程页 /sp/workflow/newflow、流程列表 /workflow/list/mine 与 /workflow/list/center、设计器页、门户均无 workflowId,判定为 false。降级链:拿不到 getFlowPageSDK() 时退回读 DOM(动作条 button)与 __WEAPP_AGENT_BRIDGE__.describe(menuId) 按名称匹配;表里没有的操作不渲染;页面无流程上下文或无动作条则不注入任何节点;节点被平台整块替换时下次 tick 自愈重挂。实测环境 @weapp/ecodesdk 0.1.54、@weapp/ui 0.0.389-hotfix11、agent bridge 0.2.0,面板层级 1050/1040(高于平台下拉 999、低于平台弹窗)。

🔗 配套 e-code 应用源码:https://github.com/OnesZhang/e10-ecode-workflow-action-guide

📌

该文章由丸子引导AI辅助制作生成,如有改进意见,欢迎评论区留言。

这次的关键判断只有一句话:不要按按钮名称绑定。名称是给人看的,menutype 才是给程序看的。

流程动作说明浮标:右上角 i 入口 + 中英双语说明面板

结论先行

  1. 流程页右上角按钮与「更多」菜单的稳定标识是 menutype(SUBMIT / SAVE_DATA / REMIND …),用平台官方口子 window.weappWorkflow.getFlowPageSDK().baseStore.operMenus 就能拿到,不需要展开菜单、不需要点任何按钮;
  2. istop === 1 的就是右上角按钮,其余进「更多」菜单 —— 分组不用去猜 DOM 结构;
  3. 面板里显示的名称取平台当前标题(管理员改过名就显示改后的名字,永远和屏幕一致),释义按 menutype 查表 —— 这就是「改名不失效」的关键;
  4. 判断”这是不是一个流程页”也不能看 URL:同一个流程可以是 /sp/workflow/flowpage/view/{id},也可以是 /sp/ebdfpage/card/…/{id};判据要落在平台注入的流程上下文 baseParam.workflowId 上;
  5. 语言用 weappUtils.getLang(),每 800ms 重读一次,切换界面语言不需要刷新页面;
  6. 位置千万别写死 top:带「第 N 条(共 M 条)」条幅的页面会把工具条整体下压(约 30px → 54px),写死偏移就会压住「更多」;而改贴容器底边又会偏下,因为容器底部比按钮低 13px。

一句话:名称负责显示,操作码负责语义,页面靠上下文识别,位置靠量不靠猜。

一、需求:按钮「看得见但不认识」

流程详情页右上角长这样:提交、保存、删除,再加一个 更多;点开「更多」还有十几项:复制流程、关注、转发、催办、消息接收设置、事项分享、事项群聊、打印、打印日志、复制链接、设为待办事项、流程信息、表单日志、委托、抄送、标记标签……

对天天用的人来说这是肌肉记忆;对偶尔走一次流程的业务用户,「转发」和「传阅」有什么区别、「催办」和「督办」差在哪,基本只能靠猜。需求就是:在右上角放一个入口,点开用用户自己的语言说明每个按钮是干什么的。

需求本身很朴素,但它埋了一个必须一开始就想清楚的坑。

二、关键判断:不能按按钮名称绑定

最初的原型是这么写的:读 DOM 里按钮的文案,拿文案去字典里查。

// ❌ 不要这样
const label = button.textContent.trim(); // 「提交」
const entry = DICT[label]; // 靠名称查表

这个写法有两个致命问题,而且两个都一定会发生:

  1. 按钮名称可以被管理员自定义。E10 的流程节点操作菜单里,名称是配置项,管理员完全可以把「保存」改成「暂存草稿」;
  2. 名称随界面语言变化。同一个按钮,中文界面是「提交」,英文界面是 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 …)改名、切语言都不变,这才是绑定依据
istop1 = 渲染成右上角按钮,否则在「更多」菜单里分组规则与平台 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/ 前缀判断的,于是这个页面一个节点都没注入。

于是我把相关页面全部量了一遍,想找一个站得住的判据:

页面URLgetFlowPageSDK()baseStorebaseParam.workflowId动作条
经典流程详情/sp/workflow/flowpage/view/{id}object✅✅✅
e-builder 流程卡片/sp/ebdfpage/card/0/{objId}/{requestId}object✅✅✅
发起流程/sp/workflow/newflowobject✗—✗
流程列表/workflow/list/mine、/workflow/list/centerobject✗—✗
e-builder 设计器/sp/ebdapp/detail/…/flow/nodeInfoobject✗—✅ ← 有动作条
门户/portal/view/…object——✗

两个「看起来能用」的判据都被否掉了:

  • window.weappWorkflow.getFlowPageSDK() 在任何页面都返回对象,连门户和列表页也是 —— 它只证明模块加载了,不证明这是流程页;
  • 右上角动作条 .ui-title-right-container e-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条) 上一条/下一条」小条幅的页面:

普通流程页带条幅的页面
动作条容器高度55px84px(被条幅整体下压)
按钮实际底边42px72px
写死 top: 52px 的结果刚好落在按钮下方压在「更多」按钮上

于是改成”按真实位置量”:

// 取动作条内所有 button 的真实底边,+8px
let 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
中英双语PASSzh_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-* 自有类名触发,无全局副作用;
  • 本地侧回滚:删掉应用目录即可。
  1. operMenus 是这套方案的地基:它是平台官方口子,但字段(menutype / istop / useable)建议在升级后复测一遍;
  2. 释义表要跟着业务走:表里没有的操作不会显示(不臆造含义)。新增动作只需补一行 menutype 映射,查 menutype 的方式就是打印 operMenus;
  3. 位置每次 tick 都量:这既是它能适应条幅页的原因,也意味着如果平台把工具条改成懒加载/动画,需要重新确认时序;
  4. 页面判据依赖 baseParam.workflowId:万一平台以后给某些非实例页也注入了流程上下文,那些页面同样会出现入口;到那时把它和别的信号组合起来收紧即可。

核心其实只有一句话:别拿给人看的表象当程序的键 —— 按钮名会改会翻译,URL 会因页面形态而变,而 menutype 和 workflowId 不会。