你的泛微 E9 流程节点 HTML 模板同步小助手!
在日常 OA 系统管理中,随着流程节点数量的增多,管理和同步这些节点的 HTML 模板常常成为一项耗时又繁琐的任务。作为一名 OA 工程师,我经常遇到以下问题:
- 不知道哪些节点模板完全一致,导致反复比对浪费时间,但没有比对又不敢直接复制模板。
- 难以区分需要单独处理的模板,管理起来不够直观。
为了高效解决这些问题,我开发了「流程节点 HTML 模板一致性比对功能」,能够快速分析流程节点的模板一致性,并用可视化的方式帮助用户理解和管理模板。今天,我将分享这一工具的实现与优化过程,希望能对大家有所帮助!
2024年12月19日 10:19:39代码BUG修复
修复Table组件污染BUG2024年12月19日 10:19:39代码优化
感谢wintsa佬友的修正和建议功能背景与目标
在泛微 E9 的工作流引擎中,每个流程节点都有自己的 HTML 模板和脚本,由于节点数量众多且分布复杂,手动检查模板一致性常常需要耗费大量时间。基于这一痛点,我设计并实现了以下功能:
- 自动分析节点模板:通过 API 接口提取每个节点的 HTML 模板和脚本信息。
- 生成唯一一致性标签:自动比对模板和脚本内容,为每个节点生成唯一的标签。
- 颜色标识可视化:通过颜色标识,直观展示节点模板和脚本的一致性分组。
- 一键操作简单高效:用户只需点击一个按钮,即可完成所有节点的模板一致性检查。
代码实现
完整代码
此处内容需要评论回复后方可阅读
核心功能实现
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 流程节点模板管理的复杂度。如果大家有任何建议或问题,欢迎随时交流!
[泛微 ecode] 流程节点Html模板检查一致性比对 by https://oneszhang.com/archives/119.html
666
丸子!开开门开开门
丸子666
开门开门
丸子牛啊,666
丸子!开开门开开门
强!
来拿代码了
牛哇牛哇
丸子上班了!
666
大佬6666
牛子丸啊
666
芝麻开门
render@
6666
js的生成好像不太对,我没仔细检查代码,但是我尝试 1 2 3 4节点同一个js和html模块,这时候修改2的js和模块,出现的问题是1 3 4 判定的脚本居然不属于同一个
昨天我检查了这个,但是不计划修复这个问题,比如js的换行空格等,都属于一个新的js对于html模板来说,泛微在保存模板时保存了用户最后选中的单元格,如果模板一致,但是最后选中的单元格不一致,也是会判断为属于一个新的html模板
再提个建议,这个’比对已完成,请勿重复操作‘的判断条件,comparisonCompleted变量可以通过监听接口保存之类的更新@(哈哈)
哇哈哈哈哈哈,有道理~
const processNodeData = async (dataSource) => {
const templateMap = new Map(); const jsMap = new Map(); const colorMap = new Map(); try { await Promise.all( await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2; // 获取 modeInfo 和 Designer 数据 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 = layoutInfo.scripts || '无脚本'; 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 }; }};
我见到改了一下这个方法,if (comparisonCompleted) {
antd.message.warn('比对已完成,请勿重复操作!'); return; }这个条件可以去掉了,但是js生成还是会乱的问题我没仔细看
let tableProps = {};
let workflowId;
let comparisonCompleted = false;
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( await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2; // 获取 modeInfo 和 Designer 数据 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 (comparisonCompleted) { // antd.message.warn('比对已完成,请勿重复操作!'); // return; // } 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('模板一致比对完成!'); comparisonCompleted = true; } else { antd.message.error('比对过程中发生错误,请稍后重试!'); } tableProps.loading = false;};
ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', {
fn: (newProps) => { if (!customCheck()) { return; } 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; comparisonCompleted = false; } if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { // 确保 newProps.dataSource 已存在,如果没有则初始化 if (!newProps.dataSource) { newProps.dataSource = []; } // 遍历 tableProps.dataSource 并将 _node_2span 赋值到 newProps.dataSource 中 tableProps.dataSource.forEach((item, index) => { // 如果 newProps.dataSource 中没有相应的对象,则初始化 if (newProps.dataSource[index]) { newProps.dataSource[index]._node_2span = item._node_2span; } }); } tableProps = { ...newProps }; return newProps; },});
发现了三个bug,顺便修复了@(吐舌)
十分感谢~~,BUG我已复现,马上更新博客代码为最新~
突然发现这段代码在添加节点的时候,旧的tag还是会错位。最后那段_node_2span还可以再加个判断条件优化下。不过现在已经足够满足使用了~@(吐舌)
咦,我的不会错位捏,但是标签不会自动跟着刷新,比如排序变化那些,但是考虑到不是用户使用,还好,暂时就酱紫用,哈哈哈哈哈哈
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; comparisonCompleted = false; } if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { // 确保 newProps.dataSource 已存在,如果没有则初始化 if (!newProps.dataSource) { newProps.dataSource = []; } // 遍历 tableProps.dataSource 并将 _node_2span 赋值到 newProps.dataSource 中 tableProps.dataSource.forEach((item) => { // 找到 newProps.dataSource 中与当前 item 匹配的对象 const matchedItem = newProps.dataSource.find( (newItem) => newItem.randomFieldId === item.randomFieldId ); // 如果找到匹配项,进行比较和更新 if (matchedItem) { // 提取 item 和 matchedItem 的 <a> 标签 innerText const itemContainer = document.createElement('div'); itemContainer.innerHTML = item._node_2span; const matchedContainer = document.createElement('div'); matchedContainer.innerHTML = matchedItem._node_2span; const itemInnerText = itemContainer.querySelector('a').innerText; const matchedInnerText = matchedContainer.querySelector('a').innerText; // 如果 innerText 相同,则更新 _node_2span if (itemInnerText === matchedInnerText) { matchedItem._node_2span = item._node_2span; } } }); } tableProps = { ...newProps }; return newProps; },ecodeSDK.overwritePropsFnQueueMapSet('Table', {
});
最后那一段我修改了一下,这就很完美了
return; } // 处理模板标签 let tmp=JSON.parse(layoutInfo.datajson) delete tmp.eformdesign.eattr const templateKey = JSON.stringify(tmp); if (!templateMap.has(templateKey)) { templateMap.set(templateKey, `模板${templateMap.size + 1}`); colorMap.set(`模板${templateMap.size}`, generatePastelColor()); } const templateTag = templateMap.get(templateKey); if (!layoutInfo || !layoutInfo.datajson) {
我再修改了一下,模板那里应该对比datajson才对。
还是有点问题,我用同步功能后,明明字段属性的可编辑不一样,但是他还是显示同模板
厉害
6666
666
丸子大佬牛啊
let tableProps = {};
let workflowId;
let comparisonCompleted = false;
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; }};
try { const response = await fetch('/api/workflow/wfexceldesign/doLoadFormInfo', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ formid,isbill:1,nodeid }).toString(), }); return response.json(); } catch { return null; }const fetchFormInfo = async (formid,nodeid) => {
};
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( await Promise.all( dataSource.map(async (node) => { const nodeid = node._node_2; // 获取 modeInfo 和 Designer 数据 console.log(nodeid,'1') const modeInfo = await fetchModeInfo(nodeid); if (!modeInfo || !modeInfo.html || modeInfo.html.modeid === 0) { return; } console.log(modeInfo.html.modeid,'2') const Designer = await fetchDesigner(modeInfo.html.modeid); const { layoutInfo } = Designer; if (!layoutInfo || !layoutInfo.datajson) { return; } const {formInfo} = await fetchFormInfo(JSON.parse(node.moreSetspan).formid,nodeid); if (!formInfo ) { return; } // 处理模板标签 let tmp=JSON.parse(layoutInfo.datajson) delete tmp.eformdesign.eattr.nodeid const templateKey = JSON.stringify(tmp)+JSON.stringify(formInfo) // 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 processNodeData = async (dataSource) => {
};
const handleComparison = async () => {
// if (comparisonCompleted) { // antd.message.warn('比对已完成,请勿重复操作!'); // return; // } 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('模板一致比对完成!'); comparisonCompleted = true; } else { antd.message.error('比对过程中发生错误,请稍后重试!'); } tableProps.loading = false;};
ecodeSDK.overwritePropsFnQueueMapSet('WeaTop', {
fn: (newProps) => { if (!customCheck()) { return; } 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; comparisonCompleted = false; } if (tableProps && tableProps.dataSource && tableProps.dataSource.length > 0) { // 确保 newProps.dataSource 已存在,如果没有则初始化 if (!newProps.dataSource) { newProps.dataSource = []; } // 遍历 tableProps.dataSource 并将 _node_2span 赋值到 newProps.dataSource 中 tableProps.dataSource.forEach((item) => { // 找到 newProps.dataSource 中与当前 item 匹配的对象 const matchedItem = newProps.dataSource.find( (newItem) => newItem.randomFieldId === item.randomFieldId ); // 如果找到匹配项,进行比较和更新 if (matchedItem) { // 提取 item 和 matchedItem 的 <a> 标签 innerText const itemContainer = document.createElement('div'); itemContainer.innerHTML = item._node_2span; const matchedContainer = document.createElement('div'); matchedContainer.innerHTML = matchedItem._node_2span; const itemInnerText = itemContainer.querySelector('a').innerText; const matchedInnerText = matchedContainer.querySelector('a').innerText; // 如果 innerText 相同,则更新 _node_2span if (itemInnerText === matchedInnerText) { matchedItem._node_2span = item._node_2span; } } }); } tableProps = { ...newProps }; return newProps; },});
重新改了一下,之前的版本,对比模板没有对比字段属性。添加多了一个接口获取字段属性
学习!
666
大佬厉害
膜拜大佬
6666
6666
@丸子,这个完整得Ecode代码怎么写呢?
完整就是文章中的代码
666666
厉害
22222
丸子,貌似不对哈,同步模板之后,但是这时我在一个模板添加公式、禁止手动编辑、隐藏行、字段属性,然后对比的时候是不一致,但是这时我再去把这个配置给删了,再次对比,还是显示模板不一致耶 。。
保存时选中单元格不一致也会被判定为不一致,只要改动过没有完完全全的还原就会不对,他是自动比对的后台报告的json。
1
正痛苦如何检查节点的一致性,来学习了