本文摘要
针对泛微 E9 流程节点 HTML 模板数量多、人工比对费时的问题,介绍了一个基于 ecodeSDK 的一致性比对小工具:在节点页面注入按钮,调用接口提取模板与脚本,用 Map 去重并生成彩色标签,直观标出相同模板与脚本的节点分组。适合 OA 工程师日常维护流程时快速排查模板差异,文中还修复了 Table 组件污染问题。

你的泛微 E9 流程节点 HTML 模板同步小助手!

在日常 OA 系统管理中,随着流程节点数量的增多,管理和同步这些节点的 HTML 模板常常成为一项耗时又繁琐的任务。作为一名 OA 工程师,我经常遇到以下问题:

  • 不知道哪些节点模板完全一致,导致反复比对浪费时间,但没有比对又不敢直接复制模板。
  • 难以区分需要单独处理的模板,管理起来不够直观。

为了高效解决这些问题,我开发了「流程节点 HTML 模板一致性比对功能」,能够快速分析流程节点的模板一致性,并用可视化的方式帮助用户理解和管理模板。今天,我将分享这一工具的实现与优化过程,希望能对大家有所帮助!

流程节点HTML一致性比对

⚠️ 2024年12月19日 10:19:39代码BUG修复

修复Table组件污染BUG

感谢wintsa佬友的修正和建议


功能背景与目标

在泛微 E9 的工作流引擎中,每个流程节点都有自己的 HTML 模板和脚本,由于节点数量众多且分布复杂,手动检查模板一致性常常需要耗费大量时间。基于这一痛点,我设计并实现了以下功能:

  1. 自动分析节点模板:通过 API 接口提取每个节点的 HTML 模板和脚本信息。
  2. 生成唯一一致性标签:自动比对模板和脚本内容,为每个节点生成唯一的标签。
  3. 颜色标识可视化:通过颜色标识,直观展示节点模板和脚本的一致性分组。
  4. 一键操作简单高效:用户只需点击一个按钮,即可完成所有节点的模板一致性检查。

代码实现

完整代码

[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>
);
},
});

测试与优化建议

测试结果

  1. 模板一致性快速识别:节点表格中显示了颜色标记的模板和脚本标签,相同模板的节点被分配了相同的颜色,直观明了。
  2. 一键完成比对:无需繁琐的手动操作,点击按钮即可完成所有节点的模板检查。
  3. 高效而稳定:在节点数量较多的情况下,仍能保持较好的响应速度。

希望这篇分享能够帮助到更多的开发者,简化泛微 E9 流程节点模板管理的复杂度。如果大家有任何建议或问题,欢迎随时交流!