ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

泛微OA前端JS代码块整理:WfForm字段控制与明细联动

泛微OA前端JS代码块整理:WfForm字段控制与明细联动 泛微OA的二次开发里前端JS大概是最接地气也最容易踩坑的一块。它不像后端接口那样有完整的文档和调试日志很多时候就是在一个表单、一个明细表、一个按钮上凭经验加一段脚本把业务逻辑补齐。我在这套系统里泡了几年攒下来的常用代码块少说也有一两百个——字段显隐、明细联动、提交校验、弹窗回填、页面跳转、数据格式化翻来覆去就是那几类。真正让人头疼的不是写不出来而是写出来不生效、换个环境就报错、三个月后自己看不懂。这篇东西是我自己的一套代码块整理思路和实操记录围绕泛微OA常用JS代码块展开。核心解决四件事代码该写在哪个入口、常用代码块长什么样、为什么这么写、出问题了怎么查。适合两类人一类是刚接手OA运维和表单配置的同学照着抄能跑起来另一类是做了一段时间但代码散落各处、想系统梳理一下的老手。文中所有API写法以常见实践为准泛微不同版本E8、E9、E10的细节有差异具体请以你自己的环境实测为准。1. 先把场景摸清楚泛微OA里JS到底管什么1.1 三个写JS的地方位置写错全白干泛微OA里能写JS的地方远不止一处但每一处的运行环境和可用对象都不一样这是新手最容易翻车的地方。我把它们归成三类你写之前先判断自己属于哪一类。第一类是流程表单也就是走审批流的那张单子。这里的JS跑在流程页面上有完整的WfForm对象可用字段控制、明细操作、提交校验都靠它。这是使用频率最高的场景八成以上的需求都落在这里。第二类是建模应用有些人叫建模表单、建模模块表单和列表页面是独立的字段控制逻辑跟流程表单不完全一样明细行操作的API也有差别。很多人把流程的代码直接复制到建模里结果WfForm is not defined报错就是没分清。第三类是门户和自定义页面纯HTMLCSSJS的舞台这里没有WfForm只能用原生DOM和jQuery自己处理。适合做数据看板、入口导航、自定义列表。判断方法很简单打开浏览器控制台敲一下typeof WfForm返回object就是流程表单环境返回undefined就说明你不在流程页面上。这个小动作能省掉大量无效调试。注意建模和门户的代码别往流程表单里搬反之也一样。同名的字段ID在不同环境下的DOM结构完全不同。1.2 为什么多数人第一行代码就写错jQuery与WfForm的关系泛微OA前端默认内置了jQuery流程页面上还挂了一个全局的WfForm对象。理解这两者的分工代码逻辑就顺了。WfForm管的是业务层字段值、字段属性、明细行、提交事件。它操作的是字段这个概念不是DOM节点。你告诉它把 field110 设成只读它自己去找对应的DOM去改。这是官方推荐的方式因为它屏蔽了页面结构的差异版本升级时相对稳。jQuery 管的是表现层位置调整、样式修改、元素显隐、HTML拼接、事件绑定。当你需要做WfForm做不到的事比如把某个字段整行藏起来、在字段旁边插一个自定义按钮、改一下表格宽度就得回到jQuery。我的原则是能用 WfForm 的绝不用 jQuery。因为直接改DOM有两个副作用——一是页面上隐藏了但值还在提交时照样带上二是页面局部刷新比如明细行增删、字段联动重绘之后你改过的DOM会被还原代码就失效了。我见过太多明明写了隐藏点一下又出来了的案例基本都是直接操作DOM导致的。// 推荐走业务层值和方法都受控 WfForm.changeFieldAttr(field110, {viewAttr: 1}); // 慎用纯DOM操作刷新后容易丢 jQuery(#field110).closest(tr).hide();真要用jQuery藏行也得配合字段值一起处理或者把它放在页面渲染完成的时机里重复执行。这一点后面第5节会细说。1.3 我的代码块库是怎么攒出来的一开始我也是每次遇到需求现写写完全丢。后来发现80%的需求都在重复——某个字段选了什么另一个字段就必填、明细里选了物料自动带出规格、提交前检查明细不能为空。于是我开始按触发条件 动作类型两个维度归档。归档的好处是复用时只需要改字段ID和条件表达式主体逻辑一模一样。我给每个代码块都加了统一的注释头用途、适用环境流程/建模、依赖的字段ID、注意事项、最后修改日期。三个月后再翻一眼就知道能不能直接用。我建议你也这么干用最土的办法就行——一个记事本文件、一个内部文档页面、甚至OA里的一个知识文档只要能搜到就行。代码块的价值不在于写得多花哨而在于它被复用的时候不需要重新思考。2. 高频代码块第一梯队字段控制三件套2.1 显隐、必填、只读的三个取值与常见坑字段属性控制是使用频率最高的代码块没有之一。核心API就一个WfForm.changeFieldAttr(fieldId, {viewAttr: 值})。取值含义我整理成表这个表建议你贴在显示器边上。viewAttr 取值效果典型场景1只读可看见但不可编辑审批节点锁定已确认信息2可编辑非必填默认状态、条件解除时还原3必填可编辑条件触发后的强制填写这三个值是我实际用得最多的。要注意的是必填和提交校验是两回事。设成3之后星号会出现前端也会有基础的非空提示但如果你的条件是动态变化的比如只有金额大于10万时才必填光靠viewAttr不够稳还得在提交事件里再校验一次双保险。隐藏字段的情况稍微麻烦。如果你想彻底隐藏常见做法是// 兼容写法先尝试属性控制再用DOM兜底 var fieldId field110; jQuery(#field fieldId).closest(tr).hide();要注意#field110这个ID的拼接方式是field 字段ID不含 field 前缀的部分要确认清楚不同版本拼接规则有差异。稳妥的办法是在控制台里先console.log(WfForm.convertFieldNameToId(你的字段名))拿到真实ID再用jQuery(#field 真实ID)去试。另一个坑是隐藏字段的值不会自动清空。用户先填了内容你再把它藏起来提交时这个值照样带上去了。如果业务上要求藏起来就当没填过你必须补一句WfForm.changeFieldValue(fieldId, {value: })。提示字段ID和字段名的转换用WfForm.convertFieldNameToId(字段名)和WfForm.convertFieldIdToName(field110)互转。写代码时用ID读代码时看名字两边都不吃亏。2.2 按条件控制字段从部门经理才显示讲起单纯设属性不难难的是条件判断这件事放在哪里、什么时机执行。我把常见触发时机分成三类。第一类是页面加载时判断一次。适合那些一进页面就能确定的条件比如申请人所属部门是财务部时才显示预算科目字段。执行时机通常是页面初始化完成后但你要注意数据可能还没回填完直接取值可能拿到空字符串。稳妥做法是延迟一小会儿再执行或者监听字段变化后再跑。// 页面加载后执行一次的条件控制 jQuery(document).ready(function () { setTimeout(function () { var dept WfForm.getFieldValue(WfForm.convertFieldNameToId(申请部门)); var target WfForm.convertFieldNameToId(预算科目); if (dept dept.indexOf(财务) -1) { WfForm.changeFieldAttr(target, {viewAttr: 3}); } else { WfForm.changeFieldAttr(target, {viewAttr: 1}); } }, 300); });这里的setTimeout不是偷懒而是必要的。泛微页面字段值是异步回填的ready触发的时候数据表格可能还在渲染。300毫秒是经验值我一般在200到500之间试太短容易拿不到值太长用户会看到字段跳一下。第二类是字段值变化时联动。这需要绑定字段的 change 事件我通常用jQuery绑在DOM上或者用WfForm提供的注册方式不同版本支持程度不同先在控制台确认。判断字符串包含用indexOf或includes注意getFieldValue返回的可能是字符串也可能是值,值这种多选拼接格式用之前先console.log打印一次看真实结构。第三类是审批节点变化时判断。同一个表单不同节点看到的字段不一样这时候用节点相关的判断变量配合条件控制能做出很清晰的逐步解锁效果。2.3 字段值读写与格式化取值和赋值看着简单实际操作里问题不少。// 主表字段赋值 WfForm.changeFieldValue(field110, {value: 测试值}); // 主表字段取值 var val WfForm.getFieldValue(field110); // 明细字段赋值必须带 rowIndex WfForm.changeFieldValue(field120, {value: 100, rowIndex: 1});三个高频坑一是明细字段忘了带 rowIndex赋值直接作用到了错误的位置或者没效果二是日期字段的格式你给字符串2026-01-01可能页面显示不出来得给它的标准格式常见是yyyy-MM-dd三是选择框类字段你给显示值不等于给了存储值有些字段需要给内部ID才能正确显示这种字段我习惯先手动选一次然后在控制台打印getFieldValue看它真实存的是什么再照着格式赋值。格式化的代码块我也常备一个。比如金额千分位、日期补零、多选值拆数组// 金额加千分位 function formatMoney(num) { if (num null || num undefined || num ) return ; var n Number(num); if (isNaN(n)) return num; return n.toFixed(2).replace(/\B(?(\d{3})(?!\d))/g, ,); } // 多选字段值转数组 function splitMulti(val) { if (!val) return []; return String(val).split(,).filter(function (x) { return x ! ; }); } // 判断字符串是否包含兼容老环境 function contains(str, key) { return String(str || ).indexOf(key) -1; }这几个函数我几乎每个项目都用直接放在代码块库最前面当基础工具区。3. 明细表才是重头戏行操作与主表联动3.1 明细行的取数、加行、删行明细表是泛微OA最有价值也最容易写崩的部分。核心API我列一下都是实际在用的。用途常见写法说明取所有行序号WfForm.getDetailAllRowIndexStr(detail_1)返回类似 1,2,3 的字符串取明细行字段值WfForm.getFieldValue(field120_1)或用行序号变量拼接明细字段赋值WfForm.changeFieldValue(field120, {value: v, rowIndex: i})rowIndex 是行序号加一行WfForm.addDetailRow(detail_1)不同版本参数不同删一行WfForm.delDetailRow(detail_1, 2)第二个参数是行序号字符串先说删除。删除行的第二个参数是行序号的字符串不是数组也不是数字。如果你要删多行用逗号拼起来如果要删当前行得先拿到当前行的序号。这里有个经典难题怎么知道用户点的是哪一行常见做法是给明细行上的某个元素绑定事件通过closest往上找到行容器再从行容器的ID里正则提取出行序号。// 在明细行上绑定自定义按钮事件示例思路 jQuery(document).on(click, .my-row-btn, function () { var rowDom jQuery(this).closest(tr); var idStr rowDom.find([id^field]).first().attr(id) || ; var m idStr.match(/_(\d)$/); if (m) { var rowIndex m[1]; // 拿到行序号可做取值、删除、计算等操作 console.log(当前行序号 rowIndex); } });用on做事件委托是关键因为明细行是动态生成的直接 bind 到元素上的事件在新增行上不生效。这个坑我踩过好几次明明代码没错新加的行就是点不动。再说加行。addDetailRow执行之后DOM渲染是异步的如果你紧接着就getFieldValue去取新行的值很可能是空的。我的处理方式是在加行之后用setTimeout包一层或者用明细行操作事件如WfForm.registerAction配合加行动作来处理后续逻辑。3.2 主表→明细、明细→主表的双向联动联动是明细表最有价值的玩法。典型需求主表选了供应商明细里所有行的结算方式自动带出该供应商的默认设置反过来明细里所有行金额加起来回写主表的总金额。主表到明细的写法核心是遍历所有行再逐行赋值function syncToDetail(mainVal) { var rows WfForm.getDetailAllRowIndexStr(detail_1); if (!rows) return; rows.split(,).forEach(function (idx) { if (!idx) return; WfForm.changeFieldValue(field130, {value: mainVal, rowIndex: idx}); }); }明细到主表的合计注意数值转换这一步不能省。明细里的金额取出来是字符串直接相加会变成字符串拼接100 200得到100200而不是300。这个错误太常见了我第一次写合计就中招。function sumDetail() { var rows WfForm.getDetailAllRowIndexStr(detail_1); var total 0; if (rows) { rows.split(,).forEach(function (idx) { if (!idx) return; var v WfForm.getFieldValue(field140_ idx); var n parseFloat(v); if (!isNaN(n)) total n; }); } WfForm.changeFieldValue(field150, {value: total.toFixed(2)}); }用parseFloat而不是Number或直接是因为它遇到非数字会返回NaN配合isNaN判断更可控。金额统一toFixed(2)保留两位避免出现0.30000000000000004这种浮点误差。联动的执行时机也很讲究。主表字段变化触发时要把联动函数挂上去明细行增删之后还要重新算一次合计。我一般会写一个统一的重算入口所有可能影响结果的操作都调它逻辑集中不容易漏。3.3 合计、去重与序号重排明细表还有几个细节值得单独说。去重是常见需求比如明细里的物料不能重复。思路是把所有行的物料编码收集成数组检查有没有重复项。注意泛微的明细行序号不一定是连续的删行之后会断号所以遍历顺序要保持不能靠序号排序。function checkDuplicate() { var rows WfForm.getDetailAllRowIndexStr(detail_1); var seen {}; var dup []; if (!rows) return dup; rows.split(,).forEach(function (idx) { if (!idx) return; var code WfForm.getFieldValue(field160_ idx); if (!code) return; if (seen[code]) { dup.push(code); } else { seen[code] true; } }); return dup; }用对象做哈希表而不是双层循环行数多的时候性能差别很明显。明细几十行的时候双层循环还看不出来上百行就卡了。序号重排是个体验优化。删行之后行号会断用户可以接受但如果你在页面上显示了自定义序号列就得重新刷一遍。做法是遍历现有行按顺序赋 1、2、3……注意别跟泛微自己的行序号搞混。空行清理也很实用。用户加了几行又没填内容提交时校验会报错体验差。可以在提交前把完全空白的行删掉或者直接允许提交但后端忽略空行。我倾向于前者逻辑更清晰。4. 交互与提交弹窗、校验、提交拦截4.1 提交前校验的标准写法提交校验是最能体现代码块价值的地方。标准的注册方式大致是这样WfForm.registerCheckEvent(WfForm.OPER_SAVE , WfForm.OPER_SUBMIT, function (callback) { // 自定义校验 var msg ; var amount parseFloat(WfForm.getFieldValue(field150) || 0); if (amount 100000) { var reason WfForm.getFieldValue(field170); if (!reason) msg 金额超过10万请填写说明; } var dup checkDuplicate(); if (!msg dup.length 0) { msg 明细中存在重复物料 dup.join(、); } if (msg) { // 弹出提示不执行 callback提交被拦截 alert(msg); return; } callback(); });几个关键点。第一必须调用 callback 才会继续提交忘记调用的结果是点提交没反应这个bug排查起来很费时间因为没有任何报错。第二注册一次就够了重复注册会导致校验执行多次弹出多个提示框。第三alert 不是最佳体验但胜在稳定可靠泛微自带的提示组件在不同版本里用法不同如果你追求体验可以用自带的但一定要先把逻辑跑通再换。我还习惯把校验逻辑拆成独立函数一个函数只干一件事返回错误信息字符串没有错误就返回空。这样新增规则的时候只是往列表里加一项不动主流程。4.2 弹窗选人/选数据与返回值回填从其他页面选数据回填是OA里非常高频的交互。常见有三种做法各有取舍。第一种是系统自带的选人组件最稳但定制空间小。适合选人员、部门、岗位这类标准数据。第二种是自定义弹窗页面做一个独立页面列出可选数据用户点选后回调父页面赋值。灵活性最高但要注意跨页面通信的写法还要处理弹窗被拦截、用户直接关掉弹窗等情况。回调一般挂到window上弹窗里调用父窗口的函数完成回填。第三种是iframe嵌入把另一个系统的页面嵌进来比如从OA跳转到业务系统选单据。这种场景下会出现子页面操作完成、父页面需要刷新或回填的需求属于下一节的内容。三种方式的选择标准很简单标准数据用自带组件业务数据用自定义弹窗跨系统数据用iframe。别为了省事把标准数据也做成自定义维护成本反而高。4.3 iframe 嵌套页面的通信与父页刷新iframe 相关的代码块我也囤了不少因为集成场景太常见了。子页面回填父页面常见做法是子页面拿到父窗口对象后直接调用父页面的函数// 子页面中执行 if (window.parent window.parent.fillBackData) { window.parent.fillBackData(rowData); } // 父页面中定义 window.fillBackData function (data) { WfForm.changeFieldValue(field180, {value: data.name}); };这里的fillBackData要挂到window上不能是局部函数否则子页面找不到。同源的情况下这写法最省事跨域的话就得换消息传递的方式两边约定好消息格式。关闭弹窗并刷新父页面这个组合动作特别常见用户在弹窗里改完数据点确认弹窗关闭父页面的列表要刷新看到最新结果。// 关闭自己并刷新父页面jQuery 原生混用 function closeAndRefreshParent() { if (window.parent window.parent ! window) { try { window.parent.location.reload(); } catch (e) { // 跨域时刷新不了退化为提示用户手动刷新 console.log(无法刷新父页面 e.message); } } // 关闭当前弹窗/层取决于打开方式 if (window.close) { window.close(); } }一个实际经验先刷新父页面再关闭自己。顺序反过来有些浏览器里当前窗口已经关掉了后面的刷新代码就不会执行。另外跨域时location.reload()会被拒绝不要在没验证的情况下告诉业务方会自动刷新得留个兜底提示。注意父页面刷新会导致未保存的数据丢失。如果父页面有已填内容刷新前最好加个确认提示。5. 调试、排查与避坑实录5.1 常见故障速查表写JS最耗时的不是写是查。我把高频故障整理成一张表遇到问题先对照能省掉大量时间。现象常见原因处理方向WfForm is not defined不在流程表单环境或代码执行过早确认页面类型把代码放到正确入口赋值后页面不显示字段类型格式不匹配或rowIndex不对打印getFieldValue看真实格式隐藏的字段提交时还有值只做了DOM隐藏没清空值补 changeFieldValue 置空明细新加行的事件不生效事件绑定在具体元素上改用事件委托jQuery(document).on取值拿到空字符串数据异步回填取早了加 setTimeout 或换触发时机点提交没反应registerCheckEvent 没调 callback检查所有分支都有 callback校验弹窗弹了两次校验事件重复注册只注册一次或加重复保护明细合计变成字符串拼接没做数值转换统一 parseFloat isNaN 判断修改DOM后样式被还原页面局部重绘走 WfForm 业务层或监听重绘重执行弹窗回填后数据没显示回调函数没挂到 window挂到全局检查作用域这张表我基本是照着踩坑顺序写的前四条会消耗掉新手大量时间。5.2 我的调试三板斧第一板斧是控制台直接验证。不确定API怎么用的时候我不会去猜直接在控制台里敲。typeof WfForm、WfForm.convertFieldNameToId(字段名)、WfForm.getFieldValue(field110)敲一遍结果全出来了。这比翻文档快得多而且拿到的是你这个环境里的真实结果。第二板斧是打印日志定范围。代码不生效的时候第一件事是在关键位置打console.log看代码到底有没有执行到。如果日志没出来说明触发时机不对或者代码根本没加载如果日志出来了但结果不对说明是逻辑问题。这一步能把问题范围缩小一半。第三板斧是排除法。把代码注释到只剩一行看这行有没有效果然后逐步放开。很多人一上来就盯着整段代码找问题效率很低。我宁愿花两分钟做二分排查也不愿意盯着代码猜半小时。顺带说一句浏览器缓存是隐形杀手。改了代码刷新页面没变化先强制刷新CtrlF5试一下再看是不是缓存问题。我有一次排查了四十分钟最后发现是缓存。5.3 那些文档里不会写的坑这几个坑文档里基本不会提但实际做项目一定会遇到。坑一字段ID会变。你以为字段ID是固定的但在测试环境配的字段复制到正式环境之后ID可能不一样。所以任何跨环境迁移都必须重新确认字段ID我做迁移的时候第一件事就是在正式环境控制台把所有用到的字段ID打印一遍跟代码里的对照。坑二明细行序号不连续。删了几行之后剩余行的序号是断的比如只剩 1、3、5。所有遍历逻辑都必须基于getDetailAllRowIndexStr返回的真实序号不能自己写for (i 1; i 行数; i)。这个错误在测试时不一定暴露因为测试时很少删行上线后用户一删行就出问题。坑三多个人同时改同一段代码。OA的代码是配在表单上的没有版本控制。两个人各自改一段后保存的覆盖前面的谁都不知道。我的做法是代码统一抄一份到内部文档保留修改记录表单里只放最终版本。看着麻烦但出问题的时候能救命。坑四字段联动会互相触发。你在A字段的变化事件里改B字段的值如果B字段也绑了变化事件就会连锁触发。轻则多执行几次重则死循环把页面卡死。做法是加一个锁定标志处理期间不再响应新事件。var isSyncing false; function safeSync() { if (isSyncing) return; isSyncing true; try { // 联动赋值逻辑 } finally { isSyncing false; } }用try...finally保证标志位一定被释放即使中间报错也不会导致标志位永久锁死这是个小细节但很实用。坑五移动端和PC端表现不一致。同一段代码在电脑上跑得好好的在移动端页面上字段结构完全不同DOM选择器全部失效。如果业务上有移动端审批需求条件控制的代码要做兼容判断不能想当然。6. 让代码块变成资产命名、复用与升级6.1 命名与注释三个月后你还看得懂代码块最大的成本不在写在读。我给自己定了几条规矩执行下来效果很明显。函数名说清楚干什么checkDetailDuplicate比check1强一百倍。变量名不要用拼音缩写deptId可以bm就别用了。每个代码块头部写三行注释用途、适用环境、依赖字段。看起来是小事但下次接手的人很可能是三个月后的你自己会感谢你。/** * 用途明细物料重复校验 * 环境流程表单 * 依赖明细 detail_1物料编码字段 field160 * 注意需在提交校验事件中调用返回重复项数组 */ function checkDetailDuplicate() { // ... }我还习惯在代码里保留为什么这么写的注释比如这里必须加延迟因为字段值是异步回填的。这类注释比给变量赋值有用得多因为它记录的是踩坑经验。6.2 版本升级时的迁移清单泛微版本升级是个绕不开的话题E8升E9、E9升E10前端API会有变化。我总结了一份迁移检查清单升级前过一遍能提前发现大部分问题。逐条确认使用的 API 在新版本是否仍然支持尤其是明细行相关的方法确认WfForm的事件注册方式有没有变确认字段ID在新环境是否保持一致确认依赖的字段类型没有变化比如文本变成了选择框赋值格式就不同了在测试环境用真实数据跑一遍全流程不要只点两下就算通过检查移动端表现升级前我会把测试环境当作新版本预演所有代码先在新环境跑一遍人工记录每个代码块的状态兼容的、要改的、废弃的分开标记。这份记录就是升级时的操作手册。6.3 代码块与后端接口的配合前端代码块不是孤立的很多时候要和后端接口配合。常见两种场景。一种是数据源联动。字段选择之后要远程拉取数据回填这时候用的是异步请求要注意请求的时序用户连续切换选项时先发的请求可能后返回导致界面显示的是旧数据。处理方式是给请求加标识只接受最后一次请求的结果。另一种是提交前的服务端校验。前端校验只能防手误真正的业务规则还得后端把关。我的习惯是前端做基础校验提升体验后端做完整校验保证数据正确两边各司其职不要指望前端拦住所有非法数据。前端代码是用户可以绕过的这点心里要有数。另外涉及对接其他业务系统比如从OA发起单据同步到其他平台的场景接口调用要处理好超时和失败提示不能让用户点了提交之后页面卡在那里没反应。给用户明确的反馈比代码写得多优雅重要得多。最后分享一个我个人的习惯每完成一个需求就把这次用到的代码块回头整理一遍。新写的存进库复用的标注一次过时的删掉。这个过程每次只花几分钟但半年之后你的代码块库会变得非常好用绝大多数需求都能在十分钟内找到起点。这个习惯的价值比任何单个代码块的技巧都大。
返回列表