本文摘要
针对泛微 E9 流程节点 HTML 模板数量多、人工比对费时的问题,介绍了一个基于 ecodeSDK 的一致性比对小工具:在节点页面注入按钮,调用接口提取模板与脚本,用 Map 去重并生成彩色标签,直观标出相同模板与脚本的节点分组。适合 OA 工程师日常维护流程时快速排查模板差异,文中还修复了 Table 组件污染问题。
你的泛微 E9 流程节点 HTML 模板同步小助手!
在日常 OA 系统管理中,随着流程节点数量的增多,管理和同步这些节点的 HTML 模板常常成为一项耗时又繁琐的任务。作为一名 OA 工程师,我经常遇到以下问题:
- 不知道哪些节点模板完全一致,导致反复比对浪费时间,但没有比对又不敢直接复制模板。
- 难以区分需要单独处理的模板,管理起来不够直观。
为了高效解决这些问题,我开发了「流程节点 HTML 模板一致性比对功能」,能够快速分析流程节点的模板一致性,并用可视化的方式帮助用户理解和管理模板。今天,我将分享这一工具的实现与优化过程,希望能对大家有所帮助!

⚠️ 2024年12月19日 10:19:39代码BUG修复
修复Table组件污染BUG
感谢wintsa佬友的修正和建议
功能背景与目标
在泛微 E9 的工作流引擎中,每个流程节点都有自己的 HTML 模板和脚本,由于节点数量众多且分布复杂,手动检查模板一致性常常需要耗费大量时间。基于这一痛点,我设计并实现了以下功能:
- 自动分析节点模板:通过 API 接口提取每个节点的 HTML 模板和脚本信息。
- 生成唯一一致性标签:自动比对模板和脚本内容,为每个节点生成唯一的标签。
- 颜色标识可视化:通过颜色标识,直观展示节点模板和脚本的一致性分组。
- 一键操作简单高效:用户只需点击一个按钮,即可完成所有节点的模板一致性检查。
代码实现
完整代码
[hide]
index.css(前置加载)
.customflow-tag-container { display: flex; gap: 4px; margin-top: 1px;}
.customflow-tag { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; color: #fff; white-space: nowrap; text-align: center;}register.js(前置加载)
let tableProps = {};let workflowId;
const generatePastelColor = () => { const hue = Math.floor(Math.random() * 360); const lightness = Math.random() * (60 - 30) + 30; return `hsl(${hue}, 70%, ${lightness}%)`;};
const customCheck = () => { return ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo');};
const fetchModeInfo = async (nodeid) => { try { const response = await fetch(`/api/workflow/nodeformedit/getModeInfo?workflowid=${workflowId}&nodeid=${nodeid}`); return response.json(); } catch { return null; }};
const fetchDesigner = async (modeid) => { try { const response = await fetch('/api/workflow/wfexceldesign/doLoadDesigner', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ layoutid: modeid }).toString(), }); return response.json(); } catch { return null; }};
const processNodeData = async (dataSource) => { const templateMap = new Map(); const jsMap = new Map(); const colorMap = new Map();
function removeSuffix(str, suffixLength) { if (str.length <= suffixLength) { return ''; } return str.slice(0, str.length - suffixLength); }
try { await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2;
const modeInfo = await fetchModeInfo(nodeid); if (!modeInfo || !modeInfo.html || modeInfo.html.modeid === 0) { return; }
const Designer = await fetchDesigner(modeInfo.html.modeid); const { layoutInfo } = Designer; if (!layoutInfo || !layoutInfo.pluginjson) { return; }
const templateKey = layoutInfo.pluginjson; if (!templateMap.has(templateKey)) { templateMap.set(templateKey, `模板${templateMap.size + 1}`); colorMap.set(`模板${templateMap.size}`, generatePastelColor()); } const templateTag = templateMap.get(templateKey);
const jsKey = removeSuffix(layoutInfo.scripts, 33) || '无脚本'; if (!jsMap.has(jsKey)) { jsMap.set(jsKey, `JS${jsMap.size + 1}`); colorMap.set(`JS${jsMap.size}`, generatePastelColor()); } const jsTag = jsMap.get(jsKey);
const container = document.createElement('div'); container.innerHTML = node._node_2span || ''; const existingTags = container.querySelector('.customflow-tag-container');
if (existingTags) { existingTags.remove(); }
const newTags = `<div class="customflow-tag-container"><div class="customflow-tag" style="background-color: ${colorMap.get(templateTag)}">${templateTag}</div><div class="customflow-tag" style="background-color: ${colorMap.get(jsTag)}">${jsTag}</div></div>`; container.innerHTML += newTags;
// 更新节点内容 node._node_2span = container.innerHTML; }) );
return { success: true }; } catch { return { success: false }; }};
const handleComparison = async () => { if (!tableProps) { antd.message.warn('获取节点信息数据失败,请刷新页面后重试!'); return; }
const dataSourceCopy = JSON.parse(JSON.stringify(tableProps.dataSource)); tableProps.loading = true;
const result = await processNodeData(dataSourceCopy);
if (result.success) { tableProps.dataSource = [...dataSourceCopy]; tableProps.onChange && tableProps.onChange([...dataSourceCopy]); antd.message.success('模板一致比对完成!'); } else { antd.message.error('比对过程中发生错误,请稍后重试!'); }
tableProps.loading = false;};
ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', { fn: (newProps) => { if (!customCheck()) { return newProps; }
if (!workflowId) { workflowId = window.weaJs.getFrameParams().workflowId; }
let { buttons } = newProps; buttons = buttons || []; const { Button } = antd;
buttons.push( <Button key="handleComparison" onClick={handleComparison}>模板一致比对</Button> ); return newProps; },});
ecodeSDK.overwritePropsFnQueueMapSet('Table', { fn: (newProps) => { if (!customCheck()) return; if (!newProps.rootMap || newProps.rootMap.tabletype !== 'none' || newProps.rootMap.pageId !== 'wf:nodeinfolist') return;
const currentWorkflowId = window.weaJs.getFrameParams().workflowId; if (workflowId !== currentWorkflowId) { workflowId = currentWorkflowId; tableProps = null; }
if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { if (!newProps.dataSource) { newProps.dataSource = []; }
tableProps.dataSource.forEach((item, index) => { if (newProps.dataSource[index]) { newProps.dataSource[index]._node_2span = item._node_2span; } }); } tableProps = { ...newProps }; return newProps; },});[/hide]
核心功能实现
1. 自定义路径校验,限定功能适用范围
为了确保该功能仅在特定页面生效,我设计了路径校验逻辑 customCheck,仅当用户在特定的工作流节点页面中时才启用功能。
const customCheck = () => { return ecodeSDK.checkLPath('/wui/engine.html#/workflowengine/path/pathSet/pathDetail/flowSet/nodeInfo');};2. 数据获取与处理
通过两个 API 接口分别获取节点的 HTML 模板信息和脚本设计信息:
- 获取 HTML 模板信息:调用
/api/workflow/nodeformedit/getModeInfo接口,提取模板的基本信息(例如模板 ID)。 - 获取脚本设计信息:调用
/api/workflow/wfexceldesign/doLoadDesigner接口,提取模板的布局和脚本内容。
代码示例如下:
const fetchModeInfo = async (nodeid) => { try { const response = await fetch(`/api/workflow/nodeformedit/getModeInfo?workflowid=${workflowId}&nodeid=${nodeid}`); return response.json(); } catch { return null; }};
const fetchDesigner = async (modeid) => { try { const response = await fetch('/api/workflow/wfexceldesign/doLoadDesigner', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ layoutid: modeid }).toString(), }); return response.json(); } catch { return null; }};3. 模板与脚本一致性比对
在获取到节点的模板和脚本信息后,我使用 Map 数据结构存储唯一的模板和脚本内容,并生成对应的标签和颜色标识。通过遍历所有节点,比对内容,给每个节点添加唯一的可视化标签。
- 模板与脚本的去重逻辑:模板和脚本的内容作为
Map的 key,确保重复的内容不会生成重复标签。 - 颜色标识分配:通过
generatePastelColor方法生成随机柔和颜色,帮助用户快速区分模板和脚本分组。
具体代码实现如下:
const processNodeData = async (dataSource) => { const templateMap = new Map(); const jsMap = new Map(); const colorMap = new Map();
function removeSuffix(str, suffixLength) { if (str.length <= suffixLength) { return ''; } return str.slice(0, str.length - suffixLength); }
await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2;
const modeInfo = await fetchModeInfo(nodeid); if (!modeInfo || !modeInfo.html || modeInfo.html.modeid === 0) { return; }
const Designer = await fetchDesigner(modeInfo.html.modeid); const { layoutInfo } = Designer; if (!layoutInfo || !layoutInfo.pluginjson) { return; }
const templateKey = layoutInfo.pluginjson; if (!templateMap.has(templateKey)) { templateMap.set(templateKey, `模板${templateMap.size + 1}`); colorMap.set(`模板${templateMap.size}`, generatePastelColor()); } const templateTag = templateMap.get(templateKey);
const jsKey = removeSuffix(layoutInfo.scripts, 33) || '无脚本'; if (!jsMap.has(jsKey)) { jsMap.set(jsKey, `JS${jsMap.size + 1}`); colorMap.set(`JS${jsMap.size}`, generatePastelColor()); } const jsTag = jsMap.get(jsKey);
const container = document.createElement('div'); container.innerHTML = node._node_2span || ''; const existingTags = container.querySelector('.customflow-tag-container'); if (existingTags) { existingTags.remove(); }
const newTags = `<div class="customflow-tag-container"><div class="customflow-tag" style="background-color: ${colorMap.get(templateTag)}">${templateTag}</div><div class="customflow-tag" style="background-color: ${colorMap.get(jsTag)}">${jsTag}</div></div>`; container.innerHTML += newTags;
node._node_2span = container.innerHTML; }) );
return { success: true };};4. 一键触发比对功能
在页面顶部按钮区域,添加一个「模板一致比对」按钮,通过点击按钮即可触发模板一致性检查流程。此按钮的注入逻辑如下:
const handleComparison = async () => { if (!tableProps) { antd.message.warn('获取节点信息数据失败,请刷新页面后重试!'); return; } const dataSourceCopy = JSON.parse(JSON.stringify(tableProps.dataSource)); tableProps.loading = true;
const result = await processNodeData(dataSourceCopy);
if (result.success) { tableProps.dataSource = [...dataSourceCopy]; tableProps.onChange && tableProps.onChange([...dataSourceCopy]); antd.message.success('模板一致比对完成!'); } else { antd.message.error('比对过程中发生错误,请稍后重试!'); }
tableProps.loading = false;};
ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', { fn: (newProps) => { if (!customCheck()) { return; }
let { buttons } = newProps; buttons = buttons || []; const { Button } = antd;
buttons.push( <Button key="handleComparison" onClick={handleComparison}>模板一致比对</Button> ); },});测试与优化建议
测试结果
- 模板一致性快速识别:节点表格中显示了颜色标记的模板和脚本标签,相同模板的节点被分配了相同的颜色,直观明了。
- 一键完成比对:无需繁琐的手动操作,点击按钮即可完成所有节点的模板检查。
- 高效而稳定:在节点数量较多的情况下,仍能保持较好的响应速度。
希望这篇分享能够帮助到更多的开发者,简化泛微 E9 流程节点模板管理的复杂度。如果大家有任何建议或问题,欢迎随时交流!