本文摘要
泛微 E10 流程详情页(PC /sp/workflow/flowpage/*)提交失败时,平台会在页面顶部渲染一条内联红色横幅 div.wffp-operate-msgTip.form-msgtip,页面 URL 带 operResultType=FAIL_SHOW_ERROR_MSG,同时占用表单顶部位置。文章给出的做法是复写官方二开插槽 weappWorkflow.FPDevBlockSlot:只在 blockType === 'Prompts' 且 slotPlace === 'after' 时挂载自定义组件,其余插槽原样透传 。组件不重写平台 DOM、不复制平台文案,只给平台已渲染的提示节点挂一个类名 wffp-submit-fail-modal-card,由 CSS 把它变成居中弹窗,因此平台原有文案、出口条件明细和「点击这里进行设置」跳转逻辑全部保留;平台若改了该类名,补丁只是静默失效退回内联提示,不会报错。文章还记录了 hidden/open/closed 状态机与 MutationObserver 同步、遮罩/×/Esc 关闭与右下角重开入口,以及两个取值细节:卡片需 position: fixed(实测祖先无 transform)、z-index 1100 要高于页面右侧悬浮工具(实测 998/999)。最容易踩的坑是平台「条件」悬浮提示 .ui-trigger-popupInner 为 z-index 999,会被卡片(1100)/遮罩(1050) 盖住而「悬停或点击没反应」,需在 body.wffp-submit-fail-modal-open 作用域下把它抬到 1200。实测环境 @weapp/ecodesdk 0.1.53、@weapp/ui 0.0.387-hotfix7,最终 JS 91 行 + CSS 109 行。

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

📌

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

这是一次「克制」的改造记录:不重写平台一个 DOM 节点,只给平台已经渲染好的提示挂一个类名,剩下的交给 CSS。

改造后效果:居中弹窗 + 条件悬浮提示

结论先行

  1. 流程提交被拒后,平台渲染的是内联红色横幅 div.wffp-operate-msgTip.form-msgtip,URL 上会带 operResultType=FAIL_SHOW_ERROR_MSG;
  2. 正确姿势是复写官方二开插槽 weappWorkflow.FPDevBlockSlot,只命中 Prompts + after,其它插槽原样透传;
  3. 组件不重写平台 DOM、不复制平台文案,只加一个类名 wffp-submit-fail-modal-card,由 CSS 变成居中弹窗——平台原文案、条件明细、「点击这里进行设置」全部保留;
  4. 全程最大的坑是层级:平台「条件」悬浮提示会被弹窗盖住,必须把 .ui-trigger-popupInner 从 999 抬到 1200。

一句话:平台负责内容,我们只负责容器和层级。

一、现象:失败提示是一条内联横幅

流程提交被平台拒绝时页面会重新加载,URL 上带一个结果标记:

/sp/workflow/flowpage/view/{requestId}?operResultType=FAIL_SHOW_ERROR_MSG

随后平台在流程详情页顶部信息提示区渲染一条内联红色横幅 div.wffp-operate-msgTip.form-msgtip,内部依次是标题 msgTipTitle、正文 msgTipTop、出口条件明细 msgTipFirst、承载节点名与「条件」高亮的 msgTipThird、含「点击这里进行设置」的 msgTipBottom。

它本身没错,但占着表单顶部的位置,红底红图标还容易被业务用户读成「系统故障」。诉求很明确:改成弹窗。

二、思路:只挂类名,绝不重写平台 DOM

第一反应通常是「把这段 HTML 抠出来自己渲染一个 Modal」。能走通,但代价很大:平台文案会变、条件明细的 DOM 结构和样式要复刻、「点击这里进行设置」的跳转是平台逻辑,复制一份就等于埋了个会分叉的坑。

所以换了个方向:平台已经把内容渲染好了,我只要把它「搬」到视口中央——保留平台节点本身,只 classList.add('wffp-submit-fail-modal-card'),弹窗的一切(定位、圆角、投影、标题栏、条件块)全部由 CSS 完成。平台后续改文案、改条件明细结构,都不影响这个补丁。

降级是静默的。如果哪天平台把 .wffp-operate-msgTip 这个类名改了,querySelector 找不到节点,组件就什么都不做,页面退回平台原样的内联提示,不白屏、不报错、不阻断提交。对二开补丁来说,「改不动」远好于「改坏」。

代价也要说清楚:样式强耦合在平台类名上——类名在,弹窗在;类名没了,弹窗就没了。

三、入口:复写插槽,且只碰一个插槽

平台提供了官方二开口子(《流程详情页面-二开口子》),插槽组件是 weappWorkflow.FPDevBlockSlot;实测其 props 契约包含 weId / blockType / slotPlace / baseParam / visibleOperMenus,Prompts 插槽的 before、after 都存在。入口只做两件事:注册复写、异步加载组件。

import React from 'react';
import { regOvComponent } from '@weapp/utils';
import { asyncImport } from '@weapp/ecodesdk';
// 组件异步加载,避免入口包体积膨胀;失败返回空组件 → 保持平台原样
const WorkflowSubmitFailModal = React.lazy(() =>
asyncImport('${appId}', 'coms/WorkflowSubmitFailModal').catch((error) => {
console.error('[workflow-submit-fail-modal] 弹窗组件加载失败,保持平台原提示', error);
return { default: () => null };
})
);
// order=1,与官方示例保持一致
regOvComponent('weappWorkflow', 'FPDevBlockSlot', (Com) => {
return React.forwardRef((props, ref) => {
if (props.blockType === 'Prompts' && props.slotPlace === 'after') {
return (
<React.Fragment>
<Com {...props} ref={ref} />
<React.Suspense fallback={null}><WorkflowSubmitFailModal /></React.Suspense>
</React.Fragment>
);
}
return <Com {...props} ref={ref} />;
});
}, 1);
  1. 非目标插槽必须原样透传 <Com {...props} ref={ref} />,props 和 ref 一个都不能少,否则会破坏平台自身在该插槽上的渲染;
  2. 目标插槽也要保留 <Com {...props} ref={ref} />,不能只渲染自己的组件——同一个 Prompts/after 插槽可能还有其它 ecode 应用注册了内容,直接替换会把别人顶掉;
  3. asyncImport 必须挂 .catch 兜底:入口包保持极小、极稳,真正的组件异步加载,加载失败返回 { default: () => null },失败即无副作用。

顺带一提,${appId} 是 E10 平台的运行时占位符,平台会自动替换成实际值,不能写成字面量。

四、组件:一个三态状态机 + 一次 DOM 同步

平台的提示节点什么时候出现、什么时候被移除,我们说了不算,所以不猜、只观察:

const TIP = '.wffp-operate-msgTip';
const CARD = 'wffp-submit-fail-modal-card';
function findTip() {
// 只在 PC 流程详情页生效,其余路由直接放弃
if (window.location.pathname.indexOf('/sp/workflow/flowpage/') !== 0) return null;
return document.querySelector(TIP);
}
// 节点变化 → 挂/摘类名;检测到提示 → 自动弹;平台移除节点 → 复位
const sync = () => {
const tip = findTip();
if (tip === tipRef.current) return;
if (tipRef.current) tipRef.current.classList.remove(CARD);
tipRef.current = tip;
if (tip) tip.classList.add(CARD);
setState(tip ? 'open' : 'hidden');
};
const observer = new MutationObserver(onMutate); // onMutate 用 rAF 节流
observer.observe(document.body, { childList: true, subtree: true });

用同一个 ref 做「当前节点」的身份判断(tip === tipRef.current),类名只会被挂一次、摘一次,切换页签或重新提交都不会重复添加。

状态则合并成一个字符串 hidden / open / closed,不要拆成 isOpen + isClosed 两个布尔量——那一定会出现「两个都 true」的中间态。关闭有三条路径,全部映射到 closed:点击遮罩 .wffp-submit-fail-modal-mask、点击右上角 ×(带 aria-label="关闭")、按 Esc(仅在 open 时挂 keydown,关闭即卸载)。closed 时右下角渲染「流程提交失败,查看原因」入口,点击回到 open;hidden 时组件直接返回 null。状态变化时还要同步两个 body 类名给 CSS 用:wffp-submit-fail-modal-open(控制层级提升)与 wffp-submit-fail-modal-dismissed(隐藏原内联横幅)。

五、CSS:把内联横幅变成居中弹窗

改造平台节点要压过平台自己的样式,所以这一段带 !important;本应用自己渲染的节点则一律不加。

:root {
--wffp-fail-top: 12vh;
--wffp-fail-width: min(640px, 92vw);
}
.wffp-operate-msgTip.wffp-submit-fail-modal-card {
position: fixed !important; /* 实测祖先无 transform,fixed 相对视口有效 */
left: 50% !important;
top: var(--wffp-fail-top) !important;
transform: translateX(-50%) !important;
width: var(--wffp-fail-width) !important;
max-height: 74vh !important;
overflow: auto !important;
background: #fff !important;
border-radius: 14px !important;
z-index: 1100 !important; /* 高于页面右侧悬浮工具(实测 998/999) */
}
/* 关闭后隐藏原内联横幅 */
body.wffp-submit-fail-modal-dismissed .wffp-operate-msgTip.wffp-submit-fail-modal-card {
display: none !important;
}

视觉基调从「红色报错」改成「白色对话框 + 琥珀提醒」:

元素原平台改造后
卡片内联条、底色 #FEEDE7纯白 #fff、圆角 14px、柔和投影
标题图标错误红琥珀 #FAAD14
出口条件明细无底色#FFFBF0 + 3px #FFE58F 左细条
「条件」高亮蓝柔和琥珀 #D48806
「点击这里」/ 遮罩品牌蓝 / —保持 #1677FF / rgba(0,0,0,.35)

平台自带的「收缩全部」按钮在弹窗里没有意义,用 display: none !important 隐掉即可——只是隐藏,不删节点。

六、最大的坑:条件悬浮提示被弹窗盖住了

弹窗上线后业务反馈:悬停/点击条件明细里的「条件」,没反应。

排查下来:样式和文案都正常、节点没被裁掉;用 elementFromPoint 打在「条件」上,命中的不是条件元素,而是弹窗卡片自己——说明不是事件问题,而是有一个浮层被盖住了。

原因:平台「条件」的悬浮提示是独立浮层 .ui-trigger-popupInner,z-index 为 999;而弹窗卡片是 1100、遮罩是 1050——浮层正好夹在中间,一辈子都出不来。修复只加一行,但必须限定作用域:

/* 弹窗打开时,把平台条件浮层抬到弹窗之上 */
body.wffp-submit-fail-modal-open .ui-trigger-popupInner {
z-index: 1200 !important;
}

.ui-trigger-popupInner 是平台全局浮层类名,页面上很多组件都在用。无条件抬到 1200 会影响整个流程页乃至其它弹层的层级关系。用 body.wffp-submit-fail-modal-open 包一层,等价于声明「只在失败弹窗出现的这段时间里,条件浮层排在最上面」,弹窗一关类名移除,平台恢复原状。

最终这一套的层级是:卡片 1100 / 遮罩 1050 / 关闭按钮 1150 / 条件浮层 1200。

七、其它两个取值细节

卡片为什么可以直接 position: fixed? 因为实测从卡片到 body 的祖先链上没有 transform。只要链上任何一层有 transform,fixed 就会相对那一层定位——所以这条不是「永远成立」,改动平台结构时要复测。

关闭按钮为什么不需要定位容器? 让按钮和卡片共用同一组 CSS 变量,再用 calc() 放到卡片右上角,省掉一层 DOM,也不用担心那层容器的 overflow 把按钮裁掉。实测按钮与卡片右上角偏差 dx=0 / dy=0:

.wffp-submit-fail-modal-close {
position: fixed;
left: calc(50% + var(--wffp-fail-width) / 2);
top: var(--wffp-fail-top);
transform: translate(-50%, -50%);
z-index: 1150;
}

八、生效范围与验证

项目结论
环境E10,@weapp/ecodesdk 0.1.53、@weapp/ui 0.0.387-hotfix7
生效范围仅 PC 流程详情页 /sp/workflow/flowpage/*
移动端 / 列表页弹窗打开的详情不改,仍按平台原样内联展示
关闭后再失败平台重建提示节点 → 自动重新弹出
非流程详情页 / 无失败提示组件返回 null,无任何副作用

非目标隔离单独验证过:在流程列表页 /workflow/newflow 上,应用虽已被页面加载,但零残留元素、无 body 类名、页面内容无变化——findTip() 第一行就按路由前缀挡掉了非目标页面。三条关闭路径(遮罩 / × / Esc)+ 一条关闭后的重开入口,与「点击这里进行设置」跳转均逐条实测通过。

九、先跑通,再精简

第一版能跑但很啰嗦:首版用 present / open / dismissed 三个布尔量、关闭按钮外面套了定位容器、MutationObserver 还额外监听 characterData……总计 JS 152 行、CSS 186 行。效果确认稳定后再精简:三个布尔量合并为单状态、去掉定位容器改用共用变量 + calc()、去掉多余的 characterData 观察、自有节点不再写 !important。最终 JS 91 行 + CSS 109 行。

二开补丁优先保证「先跑通、先验证」,精简放到功能确认之后。反过来做,很容易在还没验证效果时就为了少写几行而引入难查的定位/时序问题。

十、回滚与注意事项

  • 平台侧回滚:在 ecode 控制台取消发布 / 停用该应用即可完全恢复内联提示;脚本全走类名触发,没有全局副作用;
  • 本地侧回滚:删掉该应用目录即可。
  1. 强依赖平台类名:.wffp-operate-msgTip 是这套方案唯一的耦合点。平台改类名 → 补丁静默失效退回内联提示(不报错,但也不再生效),升级后建议复测;
  2. 层级要跟着平台走:1100 / 1050 / 1150 / 1200 是当前实测结论,平台调整浮层 z-index 时需要重新核对。

核心其实只有一句话:不要和平台抢 DOM,只接管容器与层级。如果你在其它 E10 版本或其它提示插槽上试过类似做法,欢迎评论区交流。