一个"EcologyOA工程师导航"网站Top
AI辅助制作标识 该文章由丸子引导AI辅助制作生成，如有改进意见，欢迎评论区留言。 引言作为一个泛微OA工程师，我深知在日常工作中寻找技术资料和解决方案的困难。因此，我决定创建一个专门为泛微OA工程师设计的导航网站——huahan.cc。本文旨在分享这一旅程的起点、过程以及网站的主要功能。点击访问“huahan.cc-泛微OA工程师导航”网站的创立初衷在泛微OA系统的开发和维护过程中，找到高质量、实用的资源一直是一个挑战。市面上虽有许多资源，但分散且不易找到。因此，我希望通过这个导航站，为同行们提供一个汇总这些资源的平台，让寻找资料变得更加简单和高效。网站内容与特色huahan.cc致力于为泛微OA工程师提供一个全面的资源导航站，无论是OA论坛、技术文档还是知识文档，这里都应有尽有。网站的设计旨在简化信息探索过程，加速个人和团队的成长。特色功能资源汇总：聚合多种泛微OA相关资源，包括官方文档、技术论坛和教程等。易用性设计：界面简洁，分类明确，便于快速找到所需资源。社区互动：提供反馈和建议通道，鼓励用户贡献资源，形成互助的社区氛围。未来展望我希望huahan.cc能持续成长，不仅限于现有资源的汇总，还将探索更多便利工程师工作和学习的新功能。我也期待更多同行的参与和反馈，一起打造更完善的泛微OA工程师社区。结语感谢每一位访问huahan.cc的朋友。我将继续努力，不断优化和扩展网站内容，希望能为泛微OA工程师的工作带来真正的便利。如果你有任何建议或想要申请友链，欢迎通过mail@oneszhang.com与我联系。 欢迎分享这篇文章，并在评论区留下你的看法和建议！
[EcologyEC9] 之ecodeSDK API文档Top
写在前面主要内容本文档主要对泛微 ecodeSDK 的 API 列表和详细信息进行展示，以帮助开发人员更好地了解和使用该 SDK 的功能。由于未能找到完整的 SDK API 列表文档，因此制作此文档以记录供大家及自己使用。本文档包括以下内容：API 列表：提供 SDK 中可用的所有 API 的名称和功能简介。API 详细：提供每个 API 的详细信息，包括参数、返回值、使用示例等。使用示例：提供一些使用示例，以便开发人员更好地理解如何使用 SDK 中的 API。需要注意的是，由于文档可能存在更新和变化，建议在使用前再次查阅官方文档以获取最新的 SDK API 信息。 特别注意 本文档由丸子本人收集整理制作，内容多由丸子自行分析整理，部分来源自网络，难免存在错误和不详细之处，使用时请自行甄别；欢迎评论区反馈，引用请务必注明出处！ 泛微 ecode 简介ecode 是内嵌在 E-cology9 系统中，为了解决 E-cology9 所有标准界面无侵入定制和扩展二次开发的开发工具。泛微官方 ecode 使用说明：e-cloudstore.com 展开详情 ecode 核心是解决 EC 所有界面无侵入定制和扩展二次开发，采用动态注册，不直接修改源码的方案解决开发需求。除了新开发页面的之外，它还支持所有标准页面的修改。ecode 包含了整套前端编辑器、编译器，可以让用户无需搭建开发环境直接通过浏览器开始 single page 前端开发，同时采用 ecode 开发的内容都是插件化，支持一键共享、复用、二次封装。ecode 中可能用到的技术栈（Technology Stack）：PC 端：js、css、es6、react16.x、react-router3、mobxMOBILE 端：js、css、es6、react16.x、react-router4、mobx 关于如何使用 ecode 关于如何使用 ecode，官方已给出详细的使用说明，你可以直接点击上方链接或访问官方说明文档：https://e-cloudstore.com/doc.html 进行学习，丸子不在此进行过多介绍 * SDK API 列表ecodeSDK1. 数组排序（ecode 运行顺序）对 item 结构体数组按照 order 属性进行排序，该 API 无需使用ecodeSDK.ecodeQueueSort(arr)参数说明参数参数类型必须说明arritem 结构体数组是需要排序的数组样例let arr = [ { order: 3, name: "C" }, { order: 1, name: "A" }, { order: 4, name: "D" }, { order: 2, name: "B" } ]; ecodeSDK.ecodeQueueSort(arr);2. PC 端组件参数复写对 EC 全系统 PC 端组件参数进行复写，包含 组件站 PC 组件库 ecCom、antd 大部分组件 ，其中 Radio、Radio.Button、Tree.TreeNode、Menu.ItemGruop、Select.Option、Select.OptGroup、SelectNew.Option、SelectNew.OptGroup 暂不支持复写ecodeSDK.overwritePropsFnQueueMapSet(name,option)参数说明参数参数类型必须说明nameString是组件库 ecCom、antd 组件名称optionObject是复写配置option.fnFunction是复写钩子函数 (newProps)=>{}option.orderInteger否复写排序option.descString否复写说明样例ecodeSDK.overwritePropsFnQueueMapSet('WeaTop',{ //组件名 fn:(newProps)=>{ //newProps 代表组件参数 //建议添加进行位置判断操作 console.log('WeaTop:',newProps); //在这里输出日志，如果成功输出代表组件成功定位 }, order:1, //排序字段，如果存在同一个页面复写了同一个组件，控制顺序时使用 desc:'在这里写此复写的作用，在调试的时候方便查找' });3. PC 端组件重写对 EC 全系统 PC 端组件参数进行重写，重新定义的组件只要遵循原组件的交互要求，即可进行自由定义ecodeSDK.overwriteClassFnQueueMapSet(name,option)参数说明参数参数类型必须说明nameString是组件库 ecCom、antd 组件名称optionObject是重写配置option.fnFunction是重写钩子函数 (Com,newProps)=>{}option.orderInteger否重写排序option.descString否重写说明样例ecodeSDK.overwriteClassFnQueueMapSet('Table',{ fn:(Com,newProps)=>{ //Com是当前复写的原组件 //newProps是当前复写的原组件参数 return { com:Com, props:newProps }; }, order:1, desc:'浏览按钮复写' });4. MOBILE 端组件参数复写：对 EC 全系统 Mobile 端组件参数进行复写，包含 组件站 移动组件库 WeaverMobile、WeaMobilePage 大部分组件ecodeSDK.overwriteMobilePropsFnQueueMapSet(name,option)参数说明参数参数类型必须说明nameString是组件库 WeaverMobile、WeaMobilePage 组件名称optionObject是复写配置option.fnFunction是复写钩子函数 (newProps)=>{}option.orderInteger否复写排序option.descString否复写说明样例ecodeSDK.overwriteMobilePropsFnQueueMapSet('TabPage',{ //组件名 fn:(newProps)=>{ //newProps代表组件参数 //建议添加进行位置判断操作 console.log('TabPage:',newProps); //在这里输出日志，如果成功输出代表组件成功定位 }, order:1, //排序字段，如果存在同一个页面复写了同一个组件，控制顺序时使用 desc:'在这里写此复写的作用，在调试的时候方便查找' });5. MOBILE 端组件重写对 EC 全系统 Mobile 端组件参数进行重写，重新定义的组件只要遵循原组件的交互要求，即可进行自由定义ecodeSDK.overwriteMobileClassFnQueueMapSet(name,option)参数说明参数参数类型必须说明nameString是组件库 WeaverMobile、WeaMobilePage 组件名称optionObject是重写配置option.fnFunction是重写钩子函数 (Com,newProps)=>{}option.orderInteger否重写排序option.descString否重写说明样例ecodeSDK.overwriteMobileClassFnQueueMapSet('WeaBrowser',{ fn:(Com,newProps)=>{ //Com是当前复写的原组件 //newProps是当前复写的原组件参数 return { com:Com, props:newProps }; } }, order:1, desc:'浏览按钮复写' })6. 检查 url 是否匹配用于检查当前页面的 URL 是否与给定的 URL 匹配ecodeSDK.checkLPath(url)参数说明参数参数类型必须说明urlString是需要检查的url样例function checkURL() { var url = '/wui/index.html#/main/workflow/listDoing'; var result = ecodeSDK.checkLPath(url); if (result) { console.log('URL路径有效'); } else { console.log('URL路径无效'); } } checkURL();7. 在 props 对象中查找指定的键值在复杂的属性对象中查找指定键的值。它通过递归地遍历属性对象的子对象和子数组来查找，并返回找到的第一个非空值。如果没有找到非空值，则返回空字符串。ecodeSDK.findPropData(props, key)参数说明参数参数类型必须说明propsObject是要查找的属性对象keyString是要查找的键样例var props = { children: [ { props: { key1: 'value1', key2: 'value2' } }, { props: { key3: 'value3', key4: 'value4' } } ] }; var key = 'key3'; var result = ecodeSDK.findPropData(props, key); console.log(result); // 输出：value38. 复写 API 响应数据用于修改 API 的响应数据。这种功能通常在开发过程中使用，允许开发人员对从 API 接收到的数据进行定制化的处理。在这个函数中，可以根据特定的条件来拦截接口响应数据，并响应修改后的数据。注意：本接口会监听PC端、手机端接口都会触发。ecodeSDK.rewriteApiDataQueueSet(fn)参数说明参数参数类型必须说明fnFunction是用于修改 API 响应数据的函数 (url, params, data)=>{}(url)String-API 的 URL（接口路径）(params)Object-API 的参数(data)Object-API 的响应数据样例ecodeSDK.rewriteApiDataQueueSet({ fn: function fn(url, params, data) { // 添加判断，如果当前页面路径不是'/spa/hrm/engine.html#/hrmengine/authAdjust'，则不执行复写逻辑 if (!ecodeSDK.checkLPath('/spa/hrm/engine.html#/hrmengine/authAdjust')) return; console.log("路径识别正确！"); if (url.indexOf("/api/public/browser/condition/1") === -1) return; console.log("API识别正确！"); console.log("url", url); console.log("params", params); console.log("data", data); // 开始复写，这里是复写的内容 //return data;（丸子一般不习惯return，注释掉就好） }, });9. 复写 PC 端 API 请求参数修改接口请求的参数。具体来说，这个函数会接收一个函数作为参数，这个函数会被调用来处理接口请求的参数。在这个函数中，可以根据特定的条件来修改请求参数，并返回修改后的参数。ecodeSDK.rewriteApiParamsQueueSet(fn)参数说明参数参数类型必须说明fnFunction是用于修改 API 请求参数的函数 (url, method, params)=>{}(url)String-API 的 URL（接口路径）(method)Object-API 的请求类型(params)Object-API 的请求参数样例ecodeSDK.rewriteApiParamsQueueSet({ fn: (url, method, params) => { // 不是需要拦截的接口返回默认的参数 return { url: url, // 接口路径 method: method, // 请求类型 params: params // 请求参数 } } });10. 复写 MOBILE 端 API 请求参数修改接口请求的参数。具体来说，这个函数会接收一个函数作为参数，这个函数会被调用来处理接口请求的参数。在这个函数中，可以根据特定的条件来修改请求参数，并返回修改后的参数。ecodeSDK.rewriteMobileApiParamsQueueSet(fn)参数说明参数参数类型必须说明fnFunction是用于修改 API 请求参数的函数 (url, method, params)=>{}(url)String-API 的 URL（接口路径）(method)Object-API 的请求类型(params)Object-API 的请求参数样例ecodeSDK.rewriteMobileApiParamsQueueSet({ fn: (url, method, params) => { // 不是需要拦截的接口返回默认的参数 return { url: url, // 接口路径 method: method, // 请求类型 params: params // 请求参数 } } });11. 获取本地存储中的 JSON 数据用于从 window.localStorage 中获取存储的 JSON 数据并将其解析为 JavaScript 对象，函数返回获取到的属性值，如果指定的键名不存在或数据无法解析为 JSON 格式，函数会返回一个空对象 {}。ecodeSDK.getJSONObj(key)参数说明参数参数类型必须说明keyString是要获取的本地存储数据的键名样例// 存储在本地的数据 window.localStorage['userData'] = '{"name": "Alice", "age": 25, "email": "alice@example.com"}'; // 使用示例 var userData = ecodeSDK.getJSONObj('userData'); console.log(userData.name); // 输出：Alice console.log(userData.age); // 输出：25 console.log(userData.email); // 输出：alice@example.com12. 逐级获取本地存储中嵌套的 JSON 数据用于逐级获取本地存储数据中嵌套的 JSON 对象的属性值，函数返回获取到的属性值，如果遍历过程中出现了错误（比如某个键名不存在或获取数据失败），则函数会返回空字符串 ''。ecodeSDK.getEcodeParams(keys)参数说明参数参数类型必须说明keysArray是由字符串组成的数组，表示逐级获取本地存储数据中的属性路径。样例// 存储在本地的数据 window.localStorage['userData'] = '{"name": "Alice", "age": 25, "address": {"city": "New York", "country": "USA"}}'; // 使用示例 var name = ecodeSDK.getEcodeParams(['userData', 'name']); console.log(name); // 输出：Alice var city = ecodeSDK.getEcodeParams(['userData', 'address', 'city']); console.log(city); // 输出：New York13. 动态加载资源文件为动态加载 JavaScript 和 CSS 资源提供了方便的接口，使得页面在需要时可以按需加载所需的资源文件，该函数还提供了一些静态方法，例如 t.ready、t.done、t.reset 和 t.isDefined，用于处理资源的加载完成状态、重置状态和检查资源是否已定义等;loadjs 加载第三方资源，注意加载次数，避免多次请求重复资源ecodeSDK.loadjs(resourceUrl, { callbacks, options })参数说明参数参数类型必须说明resourceUrlString是要加载的资源的 URL，可以是 JavaScript 或 CSS 文件的 URL。callbacksObject否包含回调函数的对象，用于在资源加载成功或失败后执行相应的操作。可以包含 success 和 error 回调函数。callbacks.successFunction否资源加载成功后调用的回调函数。callbacks.errorFunction否资源加载失败后调用的回调函数。optionsObject否包含加载资源的选项的对象。可以指定以下选项：async、numRetries、before和returnPromise。options.asyncBoolean否表示是否异步加载资源，默认为 true。options.numRetriesNumber否表示加载重试次数，默认为 0。options.beforeFunction否在加载资源之前执行的回调函数，默认为空函数。options.returnPromiseBoolean否如果为 true，则返回一个 Promise 对象，否则返回 undefined。retryCountNumber否内部使用的加载重试计数，默认为 0。successCbFunction否资源加载成功后调用的回调函数。errorCbFunction否资源加载失败后调用的回调函数。样例//异步加载 JavaScript 文件 ecodeSDK.loadjs('https://example.com/js/script.js', { async: true, numRetries: 2, before: function () { console.log('Before loading the script...'); }, success: function () { console.log('Script loaded successfully!'); }, error: function () { console.error('Failed to load the script!'); } }); //同步加载 JavaScript 文件并使用 Promise ecodeSDK.loadjs('https://example.com/js/script.js', { async: false, returnPromise: true }) .then(function () { console.log('Script loaded successfully!'); }) .catch(function () { console.error('Failed to load the script!'); }); //加载 CSS 文件： ecodeSDK.loadjs('https://example.com/css/styles.css', { async: true, numRetries: 1, before: function () { console.log('Before loading the CSS...'); }, success: function () { console.log('CSS loaded successfully!'); }, error: function () { console.error('Failed to load the CSS!'); } }); //loadjs 加载第三方资源，注意加载次数，避免多次请求重复资源 ecodeSDK.loadjs.isDefined('name') // 判断资源是否加载到window.weaJs1. 提取当前窗口的 URL 中查询参数从当前窗口的 URL 中提取查询参数，并将这些查询参数作为一个对象返回。window.weaJs.getFrameParams()参数说明无样例// 调用 getFrameParams 方法获取当前页面的查询参数 var params = window.weaJs.getFrameParams(); // 输出获取的查询参数对象 console.log(params);2. 构建 options 对象调用 API用于调用 API，根据传入的选项执行相应的操作；请注意，实际使用时，您需要根据具体的 API 接口和请求方法，调整选项中的 URL、method、params 等参数，以及根据需要处理返回的 API 响应。window.weaJs.callApi(options)参数说明参数参数类型必须说明optionsObject是包含调用 API 的选项的对象，可以指定以下参数。options.urlString是要调用的 API 的 URL。options.methodString否请求方法，默认为 'GET'。options.paramsObject否请求参数，作为 GET 或 POST 请求的参数。options.typeString否指定返回数据的类型，默认为 'json'。options.deepBoolean否是否深度请求，针对复杂请求类型。options.checkCodeBoolean否是否检查响应状态码，如果为 true，则会检查返回的状态码，否则不检查。样例var options = { url: 'https://api.example.com/data', method: 'POST', params: { key: 'value' }, type: 'json', deep: true, checkCode: false }; var ApiResponse = window.weaJs.callApi(options); console.log(ApiResponse);3. 显示弹窗提示用于在页面上显示弹窗提示。该方法在移动端和 PC 端使用方式略有不同，会根据环境调用相应的弹窗组件或原生弹窗。window.weaJs.alert(content)参数说明参数参数类型必须说明contentString是弹窗的提示信息内容。样例// 显示弹窗提示 var content = "这是一个提示信息。"; window.weaJs.alert(content);4. 显示对话框或浮层用于显示对话框或浮层。根据页面运行环境，在 PC 端将创建对话框并显示指定 URL 的页面，而在移动端（0601 之后的版本）将创建一个浮层显示指定 URL 的页面。请注意，对话框和浮层的显示效果可能因具体页面环境和样式设置而有所不同。在移动端，创建的浮层可以通过 frameView.destory() 方法来关闭。window.weaJs.showDialog(url, params)参数说明参数参数类型必须说明urlString是要显示的页面的 URL。paramsObject否作为 GET 请求的参数，传递给要显示的页面。params 参数说明 (移动端默认为路由全屏，不支持设置特殊参数)属性说明类型params.title标题Stringparams.moduleName模块名Stringparams.style弹框样式，一般用来设置宽高Objectparams.callback确认回调Functionparams.onCancel取消回调Function样例// 在 PC 端显示对话框 var url = 'https://example.com/dialog.html'; var params ={ title: '标题', moduleName: 'workflow', style: { width: 800, height: 600 }, callback: function() {}, onCancel: function() {}, }; var dialog = window.weaJs.showDialog(url, params); // PC端关闭对话框 dialog.destory(); // 移动端关闭浮层 frameView.destory();5. 创建自定义组件-Con是一个构造函数，用于创建自定义组件。在这个方法中，定义了一个名为 WeaCon 的类（同样继承自 React.Component），用于创建自定义组件。window.weaJs.Con()参数说明无样例// 使用 window.weaJs.Con 方法创建自定义组件 var MyCustomComponent = new window.weaJs.Con(); // 定义组件的 render 方法 MyCustomComponent.prototype.render = function() { return ( Hello, This is another Custom Component! It can also be used to render custom content. ); }; // 渲染自定义组件 ReactDOM.render(<MyCustomComponent />, document.getElementById('root')); 提示 使用 window.weaJs.Con 方法创建的自定义组件是一个类，继承自 React.Component。组件创建的方式更接近传统的 ES6 类的写法，通过 class 关键字和 extends 关键字来定义组件。适用于在 PC 端开发时，使用 React 库创建自定义组件。 6. 创建自定义组件-Custom是一个构造函数，用于创建自定义组件。在这个方法中，定义了一个名为 WeaCustom 的类（继承自 React.Component），可以通过 prototype.render 方法定义组件的显示内容。window.weaJs.Custom()参数说明无样例// 使用 window.weaJs.Custom 方法创建自定义组件 var MyCustomComponent = new window.weaJs.Custom(); // 定义组件的 render 方法 MyCustomComponent.prototype.render = function() { return ( Hello, This is a Custom Component! It can be used to render custom content. ); }; // 渲染自定义组件 ReactDOM.render(<MyCustomComponent />, document.getElementById('root')); 提示 使用 window.weaJs.Custom 方法创建的自定义组件也是一个类，同样继承自 React.Component。组件创建的方式更接近传统的 JavaScript 构造函数的写法，通过原型链和构造函数来定义组件。适用于在移动端开发时，使用 React 库创建自定义组件。
问题描述在更新到基线 10.0.2512.01 后，出现了角色权限缓存问题：应用或菜单已经给了角色或群组权限人员也已经加入到对应的角色或群组但访问资源时仍然提示："对不起，没有使用权限"原因分析这是由于权限缓存未及时更新导致的。系统虽然已经分配了权限，但缓存中的权限信息没有同步刷新。解决办法直接访问以下 API 接口来刷新权限缓存：/api/app/basicserver/data/permission/cache/sourceIdCache/reloadEmployee?tenantKey=xxxx&employeeId=xxxx参数说明tenantKey：租户 IDemployeeId：权限存在问题的人员 ID示例假设租户 ID 为 txzzkwa3op，问题人员 ID 为 1134278335853099999，则访问：/api/app/basicserver/data/permission/cache/sourceIdCache/reloadEmployee?tenantKey=txzzkwa3op&employeeId=1134278335853099999访问后，该人员的权限缓存会立即刷新，问题即可解决。更新时间：2026-02-12
Ecology E9 时代，antd.message.loading 是很多前端人的心头好——轻量、可控、非遮罩式、还能手动关闭。但到了 Ecology 10 ，UI 层全面转向 weappUi，只保留了 Spin 的全局遮罩样式。结果就是：异步任务的 Loading 不够友好，也缺乏 message 级别的可控 loading 提示。丸子用最简单的方式，实现了一个「伪 message.loading」效果：轻量、可关闭、不干扰页面交互。效果展示：方案：自己封装一个 message-like Loading核心思路：利用 weappUi.Dialog.message + weappUi.Spin 拼一个轻量提示，再返回可销毁实例。代码极简两行：window.showLoading = () => weappUi.Dialog.message({ type: 'custom', delay: 0, icon: React.createElement(weappUi.Spin, { spinning: true, size: 'small' }), content: '加载中...' }); window.hideLoading = (inst) => inst?.destroy();使用方式也与 antd 非常相似：const loadingInst = window.showLoading(); // 模拟异步任务 setTimeout(() => { window.hideLoading(loadingInst); }, 1500);Minimal. Elegant. 有手就行。为什么不用全局遮罩？原因很现实：Ecology10 的全局遮罩太“重”了遮住整个屏幕 → 用户无法操作对轻任务（接口查询、短轮询）不友好经常误导用户以为系统卡死影响复杂页面的交互体验很多时候，我们只是想来个“轻提示”，而不是把世界暂停。自定义 Loading 的优点 更轻量可控性强视觉一致代码极简 不阻断用户操作，比全屏 Spin 更自然。返回实例 → 我们想关就关，不用等待延时关闭。照样用 weappUi 的风格，不突兀。只两行代码，随处可复用。 写在最后Ecology10 虽然去掉了很多 E9 时代顺手的 Web 组件，但也给了前端更多自由度。轻量级 Loading 只是其中一个小例子。如果你也在 E10 做前端扩展，欢迎来丸子的博客看看
[泛微 Ecology10] 浏览按钮搜索按钮样式处理（真按钮）
效果对比：修改前修改后CSS代码/* 修改输入框长度 */ input[placeholder="Search Quickly"] { min-width: 100px !important; } /* 基础按钮样式 */ div.field1190285555456860160span .associative-search-icon { background-color: #5d9cec; border-radius: 4px; cursor: pointer; padding: 6px 12px; display: inline-flex; align-items: center; gap: 6px; min-width: 120px; max-height: 24px; } /* 修改图标颜色（与蓝色背景对比） */ div.field1190285555456860160span .associative-search-icon .ui-icon { color: white; } /* 添加文字（通过伪元素实现，无需改HTML） */ div.field1190285555456860160span .associative-search-icon::after { content: "Batch Select"; color: white; } /* hover效果增强 */ div.field1190285555456860160span .associative-search-icon:hover { background-color: #7bb0f2; }使用修改输入框长度代码中placeholder="Search Quickly"对应编辑时显示提示语field1190285555456860160span中的数字部分对应字段ID
[泛微Ecology10] 获取当前表单所有字段值的方法
小白丸子一开始的做法（点击展开） /** * 获取表单内所有字段值 * @returns {Object} 包含主表字段和明细表数据的对象 */ const getAllFieldValues = () => { const formSdk = window.WeFormSDK.getWeFormInstance(); const fieldInfos = formSdk.listFieldInfo(); // 复用现有格式化逻辑 const formatFields = (fieldInfos) => { const result = { mainFields: [], detailTables: {} }; fieldInfos.forEach(field => { if (field.componentKey === 'DataTable') { result.detailTables[field.subFormId] = []; } else if (field.fieldId && /^\d+$/.test(field.fieldId)) { const target = field.subFormId && result.detailTables[field.subFormId] ? result.detailTables[field.subFormId] : result.mainFields; target.push(field.fieldId); } }); return result; }; // 复用现有数据获取逻辑 const getFormData = (fields) => { const result = { mainFields: {}, detailTables: {} }; fields.mainFields.forEach(fieldId => { try { result.mainFields[fieldId] = formSdk.getFieldObj(fieldId); } catch (error) { console.warn(`获取字段 ${fieldId} 失败:`, error); result.mainFields[fieldId] = null; } }); Object.keys(fields.detailTables).forEach(tableId => { try { result.detailTables[tableId] = formSdk.getDetailData(tableId) || []; } catch (error) { console.warn(`获取明细表 ${tableId} 失败:`, error); result.detailTables[tableId] = []; } }); return result; }; const fields = formatFields(fieldInfos); return getFormData(fields); }; 现在的做法formSdk.weFormApi.formStore.getFormData().dataDetails;
检查表单在打开后数据是否存在变更:window.WeaForm.getDataStatus();无刷(静默)保存：formSdk.weFormApi.formStore.submitFormData();获取整个表单数据: formSdk.weFormApi.formStore.getFormData().dataDetails;图片上传字段单图片上传样式优化：.ui-upload-line-of-row > div:nth-child(1):nth-last-child(2) { display: none !important ; }优化前优化后ESB调用const ebSdk=window.ebuilderSDK; ebSdk.callEsbFlow("1991410702295896092",{dept:"taet"},[]).then(res=>{/*动作流回调逻辑*/});// 1. 构造请求参数 const requestData = { customParams: { mainTable: { requestId: requestId } }, esbFlowId: "1981608218199199750" }; // 2. 发送POST请求到ESB接口 fetch('/api/esb/server/event/triggerActionFlow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }) .then((response) => { // 3. 解析响应JSON（捕获HTTP错误状态） if (!response.ok) { throw new Error(`HTTP错误！状态码：${response.status}`); } return response.json(); }) .then((data) => { // 4. 请求成功回调 console.log('ESB流程触发成功:', data); // wffpSdk.showMessage('触发流程成功', 1, 3); // 成功提示（按需启用） }) .catch((error) => { // 5. 异常捕获（网络错误/HTTP错误/解析错误） console.error('ESB流程触发失败:', error); wffpSdk.showMessage('触发刷新合同失败', 2, 3); // 失败提示 });
[泛微 E9] [ 高阶精华 ] 泛微 E9 新插件：kkfileview，让文件预览 “零门槛”
本博客只涉及部分核心操作，不是保姆级教学，希望你也能有自己的思路，感悟破局的乐趣。背景在日常办公中，文件预览是一个高频且重要的需求。传统的文件预览方案往往存在格式支持有限、体验不佳等问题。泛微E9自带的文档预览功能虽然基础，但在面对复杂格式或特殊需求时往往力不从心。今天给大家介绍如何通过集成开源的kkfileview项目，为泛微E9打造一个强大、统一的文档预览解决方案。功能亮点多格式支持：支持Office、PDF、图片、视频、音频等常见格式无缝集成：与泛微E9原有文档模块完美融合零客户端：基于Web的纯前端预览，无需安装任何插件高性能：基于Spring Boot构建，响应迅速开源免费：基于Apache 2.0协议，可自由使用和修改实施步骤第一步：部署kkfileview服务丸子采用的是从官方GitHub下载源码并自行编译打包的方式。如果你不想自己打包，可以私信丸子获取编译好的版本（有时间的话会分享）。启动成功后访问演示页：访问成功界面：第二步：泛微E9文档功能改造核心工具类首先创建一个工具类，用于生成文件下载的ddcode（这是泛微文档外发的核心知识）：package com.onestools; import weaver.docs.docs.util.DesUtils; public class getFileDownloadUrl { /** * 根据文件ID生成临时下载URL * @param fileid 文件ID * @return 下载URL */ public static String getFileDownloadUrl(int fileid) { try { String encrypt = new DesUtils().encrypt(1 + "_" + fileid); return "/weaver/weaver.file.FileDownload?fileid=" + fileid + "&download=1&ddcode=" + encrypt; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据文件ID和基础URL生成临时下载URL * @param fileid 文件ID * @param baseurl 基础URL * @return 下载URL */ public static String getFileDownloadUrl(int fileid, String baseurl) { try { String encrypt = new DesUtils().encrypt(1 + "_" + fileid); return baseurl + "/weaver/weaver.file.FileDownload?fileid=" + fileid + "&download=1&ddcode=" + encrypt; } catch (Exception e) { e.printStackTrace(); return null; } } }JSP页面改造在ecology目录下/docs/e9/index2file.jsp中增加kkfileview预览逻辑：增加引用：<%@ page import="com.onestools.getFileDownloadUrl" %> <%@ page import="sun.misc.BASE64Encoder" %>添加kkfileview预览逻辑：// 现在检查kkfileview配置 boolean useKKFileView = "1".equals(rs.getPropValue("kkfileview","IsUsekkFileView")); String kkFileViewUrl = rs.getPropValue("kkfileview","kkFileViewUrl"); // kkfileview服务地址 String e9BaseUrl = rs.getPropValue("kkfileview","E9BaseUrl"); // E9基础URL if(useKKFileView && !isEdit) { // KKFileView预览逻辑 if(!"".equals(Util.null2String(kkFileViewUrl)) && !"".equals(Util.null2String(e9BaseUrl))) { try { // 使用getFileDownloadUrl工具类生成下载链接 String originUrl = getFileDownloadUrl.getFileDownloadUrl(imagefileId, e9BaseUrl); String previewUrl = originUrl + "&fullfilename=" + java.net.URLEncoder.encode(imagefilename, "UTF-8"); // Base64编码previewUrl String base64Url = new BASE64Encoder().encode(previewUrl.getBytes("UTF-8")); // 生成最终的iframe地址 docContent = " "; } catch (Exception e) { // 如果kkfileview出错，继续使用原有逻辑 e.printStackTrace(); } } }第三步：系统配置在ecology目录下/WEB-INF/prop新建kkfileview.properties增加以下内容：# 是否开启kkfileview IsUsekkFileView=1 # kkfileview的外部访问地址 kkFileViewUrl=http://192.168.31.130:8012/kkpreview # 相对于kkfileview访问E9的地址(建议内网地址) E9BaseUrl=http://127.0.0.1:8080注意事项安全性考虑：该方案的安全性和稳定性暂未经过全面评估，请在生产环境使用前进行充分测试性能优化：大量并发预览时可能需要考虑kkfileview的集群部署文件大小：超大文件预览时需要注意网络传输和服务器性能移动端适配：本示例只处理了index2file.jsp，如需移动端支持，需要对index2mobile.jsp进行类似改造总结通过集成kkfileview，我们成功为泛微E9赋予了强大的文档预览能力。这种"破局"思路不仅适用于文档预览，还可以扩展到其他系统功能的增强和改造。技术只是工具，思路才是关键。希望这个案例能够激发大家更多的创新想法，在现有系统的基础上创造出更多可能性。温馨提示：本文涉及的技术方案仅供学习参考，请根据实际环境进行评估和测试。如有技术问题，欢迎交流讨论！
为什么？ 无sysadmin限制，便于测试系统排查问题！速度！快！好看！ 怎么用？ 做一个xxxx.jsp，仍在ecology/目录下访问你的OA地址：/xxxx.jsp 注： 丸子直接用cursor写的！效果图上代码<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ page import="weaver.hrm.User" %> <%@ page import="weaver.hrm.HrmUserVarify" %> <%@ page import="java.io.File" %> <%@ page import="java.io.RandomAccessFile" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Collections" %> <%@ page import="weaver.hrm.User" %> <%@ page import="weaver.hrm.HrmUserVarify" %> <%@ page import="java.io.File" %> <%@ page import="java.io.RandomAccessFile" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Collections" %> <%-- 1. 登录验证：未登录跳转到登录页 --%> <% User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; } // 方案A：页面与请求保持UTF-8 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); // 源日志编码（默认GBK，可传GB2312/GB18030） String srcCharset = request.getParameter("srcCharset"); if (srcCharset == null || srcCharset.trim().isEmpty()) { srcCharset = "GBK"; } // 2. 核心参数：当前ecology日志路径（固定路径，无需日期） String logDirPath = request.getRealPath("/") + "log" + File.separator; File logFile = new File(logDirPath + "ecology"); // 3. 显示行数设置：默认100行，限制10-1000行 String lineCountStr = request.getParameter("lineCount"); int lineCount = 100; if (lineCountStr != null && !lineCountStr.trim().isEmpty()) { try { lineCount = Integer.parseInt(lineCountStr.trim()); lineCount = Math.max(10, lineCount); lineCount = Math.min(1000, lineCount); } catch (NumberFormatException e) { lineCount = 100; } } List<String> logContent = new ArrayList<>(); String errorMsg = ""; // 日志级别统计 int infoCount = 0, errorCount = 0, warnCount = 0, debugCount = 0; if ("POST".equalsIgnoreCase(request.getMethod())) { try { if (!logFile.exists()) { errorMsg = "日志文件不存在：" + logFile.getAbsolutePath(); } else if (!logFile.canRead()) { errorMsg = "无权限读取日志文件：" + logFile.getAbsolutePath(); } else { // 使用源编码读取（GB2312/GBK/GB18030等） List<String> rawLogContent = readLastNLines(logFile, lineCount, srcCharset); // 合并多行日志并统计日志级别 StringBuilder currentLog = new StringBuilder(); String currentLevel = "info"; for (String line : rawLogContent) { if (line == null || line.replaceAll("[ \t\u3000]", "").trim().isEmpty()) { continue; } // 检查是否是新的日志条目（以时间戳开头） if (isNewLogEntry(line)) { // 保存之前的日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); // 统计日志级别 switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } // 开始新的日志条目 currentLog = new StringBuilder(line); currentLevel = parseLogLevel(line); } else { // 继续当前日志条目 currentLog.append("\n").append(line); } } // 处理最后一个日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } } } catch (IOException e) { errorMsg = "读取日志失败：" + e.getMessage(); e.printStackTrace(); } } %> <%-- 6. 自定义方法：必须放在 <%! %> 声明块中（JSP语法要求） --%> <%! /** * 读取文件的最后N行，按指定源编码解码成Unicode */ private List<String> readLastNLines(File file, int n, String charsetName) throws IOException { List<String> result = new ArrayList<>(); if (n <= 0) return result; try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { long fileLength = raf.length(); long pos = fileLength - 1; int lines = 0; java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(256); while (pos >= 0 && lines < n) { raf.seek(pos); int b = raf.readByte() & 0xFF; if (b == 0x0A && pos != fileLength - 1) { // \n byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); baos.reset(); lines++; } else if (b != 0x0D && b != 0x0A) { baos.write(b); } pos--; } if (baos.size() > 0) { byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); } Collections.reverse(result); } return result; } private void reverseBytes(byte[] arr) { int i = 0, j = arr.length - 1; while (i < j) { byte tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } /** * 解析日志级别 */ private String parseLogLevel(String logLine) { if (logLine == null) return "info"; String upperLine = logLine.toUpperCase(); if (upperLine.contains(" ERROR ")) return "error"; if (upperLine.contains(" WARN ")) return "warn"; if (upperLine.contains(" DEBUG ")) return "debug"; if (upperLine.contains(" INFO ")) return "info"; return "info"; } /** * 解析日志时间戳 */ private String parseLogTimestamp(String logLine) { if (logLine == null) return ""; // 匹配格式：2025-10-17 14:01:34,407 java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(1); } return ""; } /** * 解析线程信息 */ private String parseLogThread(String logLine) { if (logLine == null) return ""; // 匹配格式：[null] Thread-119-239[weaver.email.timer.MailClearTimer:92] java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\s+([^\\[]+)\\[([^\\]]+)\\]"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(2) + "[" + matcher.group(3) + "]"; } return ""; } /** * 解析日志消息内容 */ private String parseLogMessage(String logLine) { if (logLine == null) return ""; // 找到最后一个 " - " 后的内容 int lastDash = logLine.lastIndexOf(" - "); if (lastDash != -1 && lastDash + 3 < logLine.length()) { return logLine.substring(lastDash + 3); } return logLine; } /** * 判断是否是新的日志条目（以时间戳开头） */ private boolean isNewLogEntry(String line) { if (line == null || line.trim().isEmpty()) return false; // 匹配格式：2025-10-17 14:01:34,407 INFO return line.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}.*"); } %> 当前Ecology日志查看 当前Ecology日志查看 登录用户：<%= user.getLoginid() %> 显示最新行数： <input type="number" id="lineCount" name="lineCount" value="<%= lineCount %>" min="10" max="1000" step="10" required> （10-1000行） 源编码： <option value="UTF-8" <%= "UTF-8".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>UTF-8 <option value="GBK" <%= "GBK".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GBK <option value="GB2312" <%= "GB2312".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GB2312 <option value="GB18030" <%= "GB18030".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GB18030 加载日志 日志文件路径：<%= logFile.getAbsolutePath() %> <% if (!logContent.isEmpty()) { %>| 实际显示行数：<%= logContent.size() %><% } %> | 源编码：<%= srcCharset %> <% if (!logContent.isEmpty()) { %> INFO: <%= infoCount %> ERROR: <%= errorCount %> WARN: <%= warnCount %> DEBUG: <%= debugCount %> <% } %> <%-- 错误提示 --%> <% if (!errorMsg.isEmpty()) { %> <%= errorMsg %> <%-- 空状态（未点击加载） --%> <% } else if (logContent.isEmpty()) { %> 请设置行数并点击"加载日志"按钮查看内容 <%-- 日志内容 --%> <% } else { %> <% for (int i = logContent.size() - 1; i >= 0; i--) { String logEntry = logContent.get(i); if (logEntry == null || logEntry.trim().isEmpty()) continue; // 解析日志各部分（从第一行获取） String[] lines = logEntry.split("\n"); String firstLine = lines[0]; String logLevel = parseLogLevel(firstLine); String timestamp = parseLogTimestamp(firstLine); String thread = parseLogThread(firstLine); // 清理日志内容，正确处理多行日志 String cleanedLogEntry = logEntry.replaceAll("\\r\\n|\\r|\\n", " ") // 将换行符替换为空格 .replaceAll("\\s+", " ") // 将多个连续空白字符替换为单个空格 .trim(); // 去除首尾空白 // HTML转义整个日志内容 String escapedLogEntry = cleanedLogEntry.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); %> <div class="log-entry <%= logLevel %>"> <%= escapedLogEntry %> <% } %> <% } %>
[泛微 EC9] 开发踩坑：weaver.conn.RecordSet-报错"字符串文字太长"
AI辅助制作标识 该文章由丸子引导AI辅助制作生成，如有改进意见，欢迎评论区留言。 记录一次为泛微OA集成AI流程审查功能时遇到的技术难题与解决历程问题背景在最近为泛微OA系统集成AI流程智能审查功能的开发过程中，遇到了一个令人困扰的问题：当尝试将AI返回的审查结果写入数据库时，表单赋值操作始终无法成功完成。经过仔细排查系统日志，发现了关键的错误信息：2025-04-29 19:01:51,512 ERROR A2 - [null] resin-port-8081-363-363[weaver.conn.RecordSet:1908] - weaver.conn.RecordSet java.sql.SQLSyntaxErrorException: ORA-01704: 字符串文字太长这个错误表明，AI返回的字符串内容长度超过了4000字符，导致数据库写入失败。问题排查过程第一阶段：数据库字段类型检查首先检查了数据库表结构，发现目标字段确实是CLOB类型，理论上应该能够存储大文本数据：-- 检查字段类型 SELECT column_name, data_type FROM user_tab_columns WHERE table_name = 'FORMTABLE_MAIN_314' AND column_name = 'AISHJG';结果显示字段类型为CLOB，这让我们一度感到困惑——既然字段类型正确，为什么还会出现长度限制的错误？第二阶段：技术原理探究经过深入研究和测试，终于找到了问题的根源：RecordSet在更新CLOB字段时的局限性在Oracle数据库环境下，SQL语句在执行之前会将所有字符类型的数据隐式转换为VARCHAR2类型，而VARCHAR2类型的最大长度限制为4000字符。当字符串长度超过这个限制时，转换就会失败，抛出ORA-01704: 字符串文字太长异常。即使目标字段是CLOB类型，使用RecordSet的更新方法仍然会受到这个限制的影响。解决方案错误做法 ❌// 以下是会导致错误的代码示例 RecordSet rs = new RecordSet(); String sql = "update formtable_main_314 set aishjg= ? where requestid=12345"; rs.executeUpdate(sql, longContent);正确做法 ✅使用ConnStatement来更新大字段：ConnStatement st = null; try { st = new ConnStatement(); st.setStatementSql("update formtable_main_314 set aishjg=? where requestid=?"); // 设置参数值 st.setString(1, aiResultContent); // 支持CLOB大字段 st.setInt(2, 12345); // 请求ID st.executeUpdate(); } catch(Exception e) { // 异常处理 e.printStackTrace(); } finally { // 资源释放 if (st != null) { st.close(); } }重要技术说明泛微OA字段类型与数据库存储关系在Oracle数据库环境下：多行文本字段（未勾选HTML编辑）：存储为VARCHAR2(4000)多行文本字段（勾选HTML编辑）：存储为CLOB类型最佳实践建议预判内容长度：在处理可能包含大文本的场景（如AI返回结果、富文本内容、日志信息等）时，优先考虑使用ConnStatement。统一的数据库操作封装：public class DatabaseUtil { /** * 更新CLOB字段的通用方法 */ public static boolean updateClobField(String tableName, String clobField, String value, String whereCondition) { ConnStatement st = null; try { st = new ConnStatement(); String sql = "update " + tableName + " set " + clobField + "=? where " + whereCondition; st.setStatementSql(sql); st.setString(1, value); return st.executeUpdate() > 0; } catch(Exception e) { e.printStackTrace(); return false; } finally { if (st != null) st.close(); } } }内容长度检查：// 在实际更新前进行长度检查 if (aiResultContent.length() > 4000) { // 使用ConnStatement更新 updateWithConnStatement(aiResultContent); } else { // 可以使用RecordSet（更简洁） updateWithRecordSet(aiResultContent); }经验总结这次踩坑经历让我们深刻认识到：技术选型要谨慎：不同的数据库操作方式有各自的适用场景，不能一概而论。理解底层原理：了解数据库驱动和框架的底层实现原理，有助于快速定位和解决问题。日志分析的重要性：详细的错误日志是解决问题的关键线索。社区力量宝贵：技术社区的交流和经验分享能够大大缩短问题解决时间。致谢特别感谢微信技术交流群中luowe老师的及时提醒，以及泛微朱老师的技术指导，正是他们的帮助让我们快速找到了问题的解决方案。希望这篇经验分享能够帮助到遇到类似问题的开发者，避免重复踩坑！如果您有更好的解决方案或其他泛微开发经验，欢迎在评论区交流分享！
[泛微 E10] 明细表处理示例JS 1：字段联动后判断明细表数据隐藏选项
在表单开发中，明细表（动态表格）的字段联动和数据控制是常见需求。比如当某个字段的值满足特定条件时，需要动态隐藏另一个字段的部分选项。本文通过一个简化的 Demo，演示如何实现这一功能。场景说明假设我们有一个“学生成绩表”明细表，包含两个字段：成绩类型（type）：下拉选择框，选项为 ["优秀(0)", "良好(1)", "及格(2)", "不及格(3)"]分数（score）：数字输入框需求：当“分数”字段的值为空或 0 时，自动隐藏“成绩类型”中的“优秀”和“良好”选项（即移除选项值为 0 和 1 的选项）。实现思路获取明细表及字段的标识（ID）监听数据联动事件（如分数变化后触发）遍历明细表的每一行，判断分数值是否符合条件若符合条件，隐藏当前行“成绩类型”的指定选项Demo 代码实现// 明细表标识（实际开发中通常由表单 SDK 自动生成） const detailTableId = "student_score_table"; // 获取明细表中字段的 ID（参数：字段名，明细表 ID） const typeFieldId = formSdk.convertFieldNameToId("type", detailTableId); const scoreFieldId = formSdk.convertFieldNameToId("score", detailTableId); // 监听数据联动事件（当字段值变化时触发） formSdk.registerAction(window.WeFormSDK.ACTION_DATA_LINKAGE, (data) => { // 获取明细表所有行的 ID 字符串（格式："row1,row2,row3"） const allRowIdsStr = formSdk.getDetailAllRowIndexStr(detailTableId); if (!allRowIdsStr) return; // 无数据行时直接返回 // 解析行 ID 为数组 const rowIds = allRowIdsStr.split(','); // 遍历每一行 rowIds.forEach(rowId => { // 拼接当前行的分数字段 ID（格式：字段 ID_行 ID） const currentScoreField = `${scoreFieldId}_${rowId}`; // 获取当前行的分数值 const scoreValue = formSdk.getFieldValue(currentScoreField); // 判断分数是否为空或 0 if (scoreValue === '' || scoreValue === '0' || scoreValue === 0) { // 拼接当前行的成绩类型字段 ID const currentTypeField = `${typeFieldId}_${rowId}`; // 移除选项值为 0 和 1 的选项（即"优秀"和"良好"） formSdk.removeSelectOption(currentTypeField, "0,1"); } }); });代码解析字段标识获取通过 formSdk.convertFieldNameToId 方法，将字段名转换为表单内部使用的 ID，需传入字段名和明细表 ID（确保在明细表中唯一）。事件监听使用 formSdk.registerAction 监听 ACTION_DATA_LINKAGE 事件，该事件在数据联动（如字段值变化、表单加载完成等场景）时触发，确保逻辑在数据更新后执行。行遍历处理通过 formSdk.getDetailAllRowIndexStr 获取所有行的 ID 字符串，拆分后得到每一行的唯一标识（rowId）。对每一行，通过 formSdk.getFieldValue 获取分数值，判断是否为空或 0。选项隐藏当条件满足时，调用 formSdk.removeSelectOption 方法，传入当前行的“成绩类型”字段 ID 和需要移除的选项值（"0,1"），实现选项的动态隐藏。关键 API 说明方法作用convertFieldNameToId(fieldName, detailTableId)将字段名转换为表单内部 ID（明细表字段需传入明细表 ID）registerAction(actionName, callback)注册事件监听，这里监听数据联动事件getDetailAllRowIndexStr(detailTableId)获取明细表所有行的 ID 字符串（格式："row1,row2"）getFieldValue(fieldId)获取指定字段的值removeSelectOption(fieldId, optionValues)移除下拉框的指定选项（optionValues 为逗号分隔的选项值）总结通过上述方法，我们实现了明细表中字段联动后的动态选项控制。实际开发中，可根据业务需求调整判断条件（如分数大于 90 时显示特定选项）或扩展字段逻辑（如多字段联合判断）。表单 SDK 的事件机制和行 ID 拼接规则是实现明细表动态控制的核心，掌握这些可灵活应对各类明细表处理场景。
[泛微 E10] 批量下载当前表单内所有附件
功能背景泛微 E10 支持单附件字段的批量下载能力。但默认情况下，用户只能逐个字段进行批量下载附件，当表单包含大量附件字段时，操作效率较低。通过开发批量下载功能，提升用户体验。技术实现方案下面是实现批量下载功能的核心代码，主要分为三个步骤：收集附件 ID、创建异步下载任务、轮询检查任务状态并下载：// 初始化 SDK 实例 const weFormSdk = window.WeFormSDK.getWeFormInstance(); // 定义需要下载附件的字段名称，请修改为对应的主表字段名称 const fieldNames = [ "sigvendatf", "poa", "regcer", "taxcer", "pscq", "pscend", "bancon", "conpo", "emaappandc" ]; // 收集所有附件 ID const values = []; fieldNames.forEach(fieldName => { try { const fieldId = weFormSdk.convertFieldNameToId(fieldName); const fieldValue = weFormSdk.getFieldValue(fieldId); if (fieldValue) { values.push(fieldValue); } } catch (error) { console.error(` 获取字段 ${fieldName} 失败:`, error); } }); // 创建异步下载任务 async function createDownloadTask() { if (values.length === 0) { window.WeFormSDK.showMessage("没有可下载的文件", 1, 3); return; } const today = new Date().toISOString().split('T')[0]; // 获取当前日期 try { const response = await fetch('/api/file/batch/asynchronous/downloadByFileIds', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "module": "ebuildercard", "fileIds": values.join(','), "authModule": "ebuildercard", "source": "form", "zipName": `附件名称在这里-请修改或赋值变量-${today}` }) }); const data = await response.json(); if (!data.data.taskId) { throw new Error('获取任务 ID 失败'); } window.WeFormSDK.showMessage("开始创建下载任务", 3, 2); await checkDownloadStatus(data.data.taskId); } catch (error) { console.error('创建下载任务失败:', error); window.WeFormSDK.showMessage("创建下载任务失败", 2, 3); } } // 轮询检查下载任务状态 async function checkDownloadStatus(taskId) { let attempt = 0; const maxAttempts = 20; // 最多尝试 20 次 while (attempt < maxAttempts) { try { const response = await fetch(`/api/file/batchDownloadPercent?uuid=${taskId}`); const data = await response.json(); if (data.data.fileId) { window.WeFormSDK.showMessage("下载任务已完成", 3, 2); downloadFile(data.data.fileId); return; } attempt++; if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); // 等待 500ms } } catch (error) { console.error('查询下载状态失败:', error); throw error; } } throw new Error('下载任务超时'); } // 执行文件下载 function downloadFile(fileId) { const link = document.createElement('a'); link.href = `/api/file/remotedownload/${fileId}/upload/true`; link.setAttribute('download', ''); document.body.appendChild(link); link.click(); document.body.removeChild(link); } // 启动下载流程 createDownloadTask().catch(error => { console.error('下载流程出错:', error); window.WeFormSDK.showMessage("下载过程中出现错误", 2, 3); });代码功能解析附件收集模块通过 weFormSdk.convertFieldNameToId 方法将字段名称转换为系统 ID使用 weFormSdk.getFieldValue 获取附件 ID 值支持从多个字段收集附件，适应复杂表单结构异步下载任务创建调用平台提供的 /api/file/batch/asynchronous/downloadByFileIds 接口将所有附件 ID 合并后作为参数传递支持自定义 ZIP 文件名，自动添加日期戳任务状态监控机制通过轮询 /api/file/batchDownloadPercent 接口检查进度设置最大尝试次数和间隔时间，防止无限循环任务完成后自动触发下载用户体验优化使用平台 SDK 提供的消息提示功能完整的错误处理机制异步操作避免界面卡顿