1. 为什么需要自定义按钮开发在泛微E9的日常使用中我们经常会遇到标准功能无法满足特定业务需求的场景。就拿财务报销流程来说每次填写发票信息时手动逐条录入不仅效率低下还容易出错。这时候一个能够从发票夹直接选择并回填数据的功能按钮就显得尤为重要。我去年负责过一个集团企业的费用报销系统改造项目财务部门反馈最多的痛点就是发票录入效率问题。他们每天要处理上百张电子发票传统的复制粘贴方式经常导致金额或发票号码错位。通过开发自定义按钮功能最终实现了点击按钮弹出发票列表、多选后自动回填到明细表的效果录入效率提升了70%以上。这种定制开发的价值主要体现在三个方面首先是操作效率的显著提升其次是数据准确性的保障最重要的是用户体验的优化。不同于简单的二次开发泛微E9的ecode开发模式允许我们深度集成React和Mobx等现代前端技术栈实现真正意义上的原生体验。2. 开发前的准备工作2.1 环境与工具确认在开始编码前需要确保开发环境就绪。泛微E9的ecode开发需要以下基础条件最新版的E9设计器建议v9.0.5以上版本Chrome浏览器用于调试代码编辑器VSCode或WebStorm基础的React和Mobx知识我建议先在测试环境创建一个空白流程表单作为试验场。曾经有同事直接在正式流程上开发结果调试时导致生产数据混乱这个教训一定要避免。2.2 表单字段配置以发票夹场景为例我们需要在流程表单中预先配置好接收数据的明细表字段。关键步骤包括在表单设计器中添加明细表如命名为发票明细创建以下字段并记录其ID开票日期如field6871购买方field6872金额field6873发票类型field6874发票号码field6875特别注意字段ID在不同环境中会变化务必通过设计器查看实际ID。有次我直接复制示例代码中的ID结果数据始终无法回填排查了半天才发现这个问题。3. 创建ecode功能包3.1 初始化项目结构在E9设计器的ecode平台中新建功能包时应遵循标准目录结构/invoice-button ├── register.js # 组件注册文件 ├── index.js # 主组件入口 ├── store │ ├── index.js # store聚合文件 │ └── tableStore.js # 发票数据store └── style └── index.css # 样式文件register.js是入口文件需要特别注意acParams参数的配置。这里有个实用技巧name属性建议采用模块类型_业务名称的命名规范比如Button_Invoice这样在后期维护时能快速定位组件用途。3.2 关键注册逻辑实现register.js的核心是重写WeaBrowser组件的逻辑。以下代码展示了如何针对特定流程表单的字段添加自定义按钮const InvoiceButton (props) { const acParams { appId: ${appId}, // 自动注入的appId name: Button_Invoice, isPage: false, noCss: true, props } return ecodeSDK.getAsyncCom(acParams); } ecodeSDK.overwriteClassFnQueueMapSet(WeaBrowser, { fn: (Com, newProps) { const { hash } window.location; // 仅在工作流请求页面生效 if (hash.indexOf(#/main/workflow/req) ! -1) { // 仅针对特定字段生效 if (newProps.fieldName ! field6870) return; if (newProps._noOverwrite) return; newProps.Com Com; return { com: InvoiceButton, props: newProps }; } return; } });这段代码中有几个容易出错的地方首先是hash判断要准确匹配目标页面URL其次是fieldName必须与表单字段ID完全一致。建议新增console.log调试输出确保条件判断按预期执行。4. 核心组件开发4.1 组件基础框架InvoiceTable组件是整个功能的核心采用React Class组件形式开发结合Mobx实现状态管理。先看基础结构const { Button, Modal, Table, Input, message } antd; const { inject, observer } mobxReact; const confirm Modal.confirm; inject(tableStore) observer class InvoiceTable extends React.Component { constructor(props) { super(props); this.state { visible: false, columns: [...], // 表格列配置 data: [...], // 初始数据 valueInput: , selectedRowKeys: [], selectedRows: [] }; } // 后续方法实现... }值得注意的几个细节必须使用inject和observer装饰器使组件能响应store变化初始数据建议设置少量测试数据方便快速验证UI效果状态设计要区分临时状态如弹窗visible和业务状态如选中行4.2 数据回填关键逻辑数据回填到明细表是整个功能最复杂的部分主要涉及以下步骤获取用户选中的数据selectedRows清空目标明细表现有数据WfForm.delDetailRow逐条插入新数据WfForm.addDetailRowhandleOk () { const { selectedRowKeys, selectedRows } this.state; const { tableStore } this.props; if(selectedRowKeys.length 0) { if(tableStore.selectedRowKeys.length 0) { confirm({ title: 提示, content: 是否要清空明细表中的数据, onOk() { tableStore.setState({selectedRows:[], selectedRowKeys:[]}) WfForm.delDetailRow(detail_1, all); }, }); } else { message.warning(请选择数据); } } else { if(tableStore.selectedRowKeys.length 0) WfForm.delDetailRow(detail_1, all); selectedRows.map(item { WfForm.addDetailRow(detail_1, { field6871: {value: item.date}, field6872: {value: item.buyer}, field6873: {value: item.money}, field6874: {value: item.type}, field6875: {value: item.number}, }); }) tableStore.setState({ selectedRows: selectedRows, selectedRowKeys: selectedRowKeys }) } this.setState({visible: false}) }实际项目中遇到过两个典型问题一是字段ID与明细表不匹配导致数据错位二是大量数据插入时性能问题。建议添加防抖处理和加载状态提示优化用户体验。5. 状态管理与数据交互5.1 Mobx Store设计对于发票数据这种需要跨组件共享的状态使用Mobx管理是最佳选择。store文件通常包含const { observable, action } mobx; class TableStore { observable selectedRowKeys []; observable selectedRows []; observable tableData [...]; // 模拟数据 action setState (params) { Object.keys(params).forEach(key { this[key] params[key]; }); } } ecodeSDK.exp(TableStore);在真实项目中tableData通常会从API获取而非硬编码。建议添加fetchData异步action并在组件挂载时调用。注意处理好加载状态和错误处理避免界面无响应。5.2 与后端API集成虽然示例中使用的是模拟数据但实际项目需要对接发票系统的API。建议采用以下模式在store中创建异步actionaction fetchInvoices async (params) { try { this.loading true; const res await axios.get(/api/invoices, {params}); this.tableData res.data.map(item ({...item, key: item.id})); } catch (e) { console.error(e); } finally { this.loading false; } }在组件中调用componentDidMount() { this.props.tableStore.fetchInvoices({ startDate: 2024-01-01, endDate: 2024-06-30 }); }遇到过的一个坑是API返回数据格式与表格所需格式不一致建议在store中做数据转换而非在组件中处理。6. 样式优化与交互完善6.1 CSS样式定制虽然功能是核心但良好的UI体验同样重要。建议为自定义按钮和弹窗添加专属样式.invoice-table-btn { padding: 3px 10px; margin: 5px; background: #1890ff; border-color: #1890ff; } .invoice-table-modal { max-width: 80%; } .invoice-table-modal-body { padding: 16px; } .invoice-table-modal-body-search { margin-bottom: 16px; }特别注意E9的样式有作用域限制建议使用特定类名前缀避免污染全局样式。遇到过样式冲突导致其他表单异常的情况排查起来相当麻烦。6.2 交互细节优化好的交互设计能让功能更易用添加加载状态数据量大时显示loading空状态提示没有数据时显示友好提示键盘支持支持回车键触发搜索分页加载超过50条数据时分页显示// 在Table组件添加loading状态 Table loading{this.props.tableStore.loading} // ...其他props / // 空状态提示 locale{{ emptyText: 暂无发票数据 }}7. 调试与问题排查7.1 常见问题解决方案在开发过程中以下几个问题比较常见按钮不显示检查fieldName是否匹配、hash路由判断是否正确数据无法回填确认明细表字段ID是否正确、是否有JS报错样式不生效检查CSS类名是否被覆盖、是否有作用域限制推荐使用Chrome开发者工具逐步调试在ecodeSDK.overwriteClassFnQueueMapSet处打断点查看组件props是否正确传递监控Mobx store的状态变化7.2 性能优化建议当处理大量数据时需要注意性能问题虚拟滚动对长列表使用Table的虚拟滚动功能分页加载不要一次性加载所有数据防抖处理对搜索输入框添加防抖缓存策略对已加载的数据进行本地缓存// 虚拟滚动配置 Table scroll{{ y: 300 }} // ... / // 搜索防抖 handleInputChange _.debounce((e) { this.setState({ valueInput: e.target.value }); }, 300);8. 项目部署与维护8.1 打包与发布开发完成后需要通过E9设计器打包发布功能包在ecode平台点击打包按钮生成.eppkg格式的安装包在目标环境的应用市场安装建议在打包前删除调试用的console.log压缩CSS和JS文件更新版本号注释8.2 后续维护建议为了便于后续维护建议编写详细的README说明关键代码添加注释保留测试用例建立变更日志对于可能变化的字段ID可以考虑提取为配置文件// config.js export const FIELD_IDS { date: field6871, buyer: field6872, // ... };这样当字段变化时只需修改一处降低维护成本。
泛微E9流程表单自定义按钮开发实战:以发票夹数据回填为例
1. 为什么需要自定义按钮开发在泛微E9的日常使用中我们经常会遇到标准功能无法满足特定业务需求的场景。就拿财务报销流程来说每次填写发票信息时手动逐条录入不仅效率低下还容易出错。这时候一个能够从发票夹直接选择并回填数据的功能按钮就显得尤为重要。我去年负责过一个集团企业的费用报销系统改造项目财务部门反馈最多的痛点就是发票录入效率问题。他们每天要处理上百张电子发票传统的复制粘贴方式经常导致金额或发票号码错位。通过开发自定义按钮功能最终实现了点击按钮弹出发票列表、多选后自动回填到明细表的效果录入效率提升了70%以上。这种定制开发的价值主要体现在三个方面首先是操作效率的显著提升其次是数据准确性的保障最重要的是用户体验的优化。不同于简单的二次开发泛微E9的ecode开发模式允许我们深度集成React和Mobx等现代前端技术栈实现真正意义上的原生体验。2. 开发前的准备工作2.1 环境与工具确认在开始编码前需要确保开发环境就绪。泛微E9的ecode开发需要以下基础条件最新版的E9设计器建议v9.0.5以上版本Chrome浏览器用于调试代码编辑器VSCode或WebStorm基础的React和Mobx知识我建议先在测试环境创建一个空白流程表单作为试验场。曾经有同事直接在正式流程上开发结果调试时导致生产数据混乱这个教训一定要避免。2.2 表单字段配置以发票夹场景为例我们需要在流程表单中预先配置好接收数据的明细表字段。关键步骤包括在表单设计器中添加明细表如命名为发票明细创建以下字段并记录其ID开票日期如field6871购买方field6872金额field6873发票类型field6874发票号码field6875特别注意字段ID在不同环境中会变化务必通过设计器查看实际ID。有次我直接复制示例代码中的ID结果数据始终无法回填排查了半天才发现这个问题。3. 创建ecode功能包3.1 初始化项目结构在E9设计器的ecode平台中新建功能包时应遵循标准目录结构/invoice-button ├── register.js # 组件注册文件 ├── index.js # 主组件入口 ├── store │ ├── index.js # store聚合文件 │ └── tableStore.js # 发票数据store └── style └── index.css # 样式文件register.js是入口文件需要特别注意acParams参数的配置。这里有个实用技巧name属性建议采用模块类型_业务名称的命名规范比如Button_Invoice这样在后期维护时能快速定位组件用途。3.2 关键注册逻辑实现register.js的核心是重写WeaBrowser组件的逻辑。以下代码展示了如何针对特定流程表单的字段添加自定义按钮const InvoiceButton (props) { const acParams { appId: ${appId}, // 自动注入的appId name: Button_Invoice, isPage: false, noCss: true, props } return ecodeSDK.getAsyncCom(acParams); } ecodeSDK.overwriteClassFnQueueMapSet(WeaBrowser, { fn: (Com, newProps) { const { hash } window.location; // 仅在工作流请求页面生效 if (hash.indexOf(#/main/workflow/req) ! -1) { // 仅针对特定字段生效 if (newProps.fieldName ! field6870) return; if (newProps._noOverwrite) return; newProps.Com Com; return { com: InvoiceButton, props: newProps }; } return; } });这段代码中有几个容易出错的地方首先是hash判断要准确匹配目标页面URL其次是fieldName必须与表单字段ID完全一致。建议新增console.log调试输出确保条件判断按预期执行。4. 核心组件开发4.1 组件基础框架InvoiceTable组件是整个功能的核心采用React Class组件形式开发结合Mobx实现状态管理。先看基础结构const { Button, Modal, Table, Input, message } antd; const { inject, observer } mobxReact; const confirm Modal.confirm; inject(tableStore) observer class InvoiceTable extends React.Component { constructor(props) { super(props); this.state { visible: false, columns: [...], // 表格列配置 data: [...], // 初始数据 valueInput: , selectedRowKeys: [], selectedRows: [] }; } // 后续方法实现... }值得注意的几个细节必须使用inject和observer装饰器使组件能响应store变化初始数据建议设置少量测试数据方便快速验证UI效果状态设计要区分临时状态如弹窗visible和业务状态如选中行4.2 数据回填关键逻辑数据回填到明细表是整个功能最复杂的部分主要涉及以下步骤获取用户选中的数据selectedRows清空目标明细表现有数据WfForm.delDetailRow逐条插入新数据WfForm.addDetailRowhandleOk () { const { selectedRowKeys, selectedRows } this.state; const { tableStore } this.props; if(selectedRowKeys.length 0) { if(tableStore.selectedRowKeys.length 0) { confirm({ title: 提示, content: 是否要清空明细表中的数据, onOk() { tableStore.setState({selectedRows:[], selectedRowKeys:[]}) WfForm.delDetailRow(detail_1, all); }, }); } else { message.warning(请选择数据); } } else { if(tableStore.selectedRowKeys.length 0) WfForm.delDetailRow(detail_1, all); selectedRows.map(item { WfForm.addDetailRow(detail_1, { field6871: {value: item.date}, field6872: {value: item.buyer}, field6873: {value: item.money}, field6874: {value: item.type}, field6875: {value: item.number}, }); }) tableStore.setState({ selectedRows: selectedRows, selectedRowKeys: selectedRowKeys }) } this.setState({visible: false}) }实际项目中遇到过两个典型问题一是字段ID与明细表不匹配导致数据错位二是大量数据插入时性能问题。建议添加防抖处理和加载状态提示优化用户体验。5. 状态管理与数据交互5.1 Mobx Store设计对于发票数据这种需要跨组件共享的状态使用Mobx管理是最佳选择。store文件通常包含const { observable, action } mobx; class TableStore { observable selectedRowKeys []; observable selectedRows []; observable tableData [...]; // 模拟数据 action setState (params) { Object.keys(params).forEach(key { this[key] params[key]; }); } } ecodeSDK.exp(TableStore);在真实项目中tableData通常会从API获取而非硬编码。建议添加fetchData异步action并在组件挂载时调用。注意处理好加载状态和错误处理避免界面无响应。5.2 与后端API集成虽然示例中使用的是模拟数据但实际项目需要对接发票系统的API。建议采用以下模式在store中创建异步actionaction fetchInvoices async (params) { try { this.loading true; const res await axios.get(/api/invoices, {params}); this.tableData res.data.map(item ({...item, key: item.id})); } catch (e) { console.error(e); } finally { this.loading false; } }在组件中调用componentDidMount() { this.props.tableStore.fetchInvoices({ startDate: 2024-01-01, endDate: 2024-06-30 }); }遇到过的一个坑是API返回数据格式与表格所需格式不一致建议在store中做数据转换而非在组件中处理。6. 样式优化与交互完善6.1 CSS样式定制虽然功能是核心但良好的UI体验同样重要。建议为自定义按钮和弹窗添加专属样式.invoice-table-btn { padding: 3px 10px; margin: 5px; background: #1890ff; border-color: #1890ff; } .invoice-table-modal { max-width: 80%; } .invoice-table-modal-body { padding: 16px; } .invoice-table-modal-body-search { margin-bottom: 16px; }特别注意E9的样式有作用域限制建议使用特定类名前缀避免污染全局样式。遇到过样式冲突导致其他表单异常的情况排查起来相当麻烦。6.2 交互细节优化好的交互设计能让功能更易用添加加载状态数据量大时显示loading空状态提示没有数据时显示友好提示键盘支持支持回车键触发搜索分页加载超过50条数据时分页显示// 在Table组件添加loading状态 Table loading{this.props.tableStore.loading} // ...其他props / // 空状态提示 locale{{ emptyText: 暂无发票数据 }}7. 调试与问题排查7.1 常见问题解决方案在开发过程中以下几个问题比较常见按钮不显示检查fieldName是否匹配、hash路由判断是否正确数据无法回填确认明细表字段ID是否正确、是否有JS报错样式不生效检查CSS类名是否被覆盖、是否有作用域限制推荐使用Chrome开发者工具逐步调试在ecodeSDK.overwriteClassFnQueueMapSet处打断点查看组件props是否正确传递监控Mobx store的状态变化7.2 性能优化建议当处理大量数据时需要注意性能问题虚拟滚动对长列表使用Table的虚拟滚动功能分页加载不要一次性加载所有数据防抖处理对搜索输入框添加防抖缓存策略对已加载的数据进行本地缓存// 虚拟滚动配置 Table scroll{{ y: 300 }} // ... / // 搜索防抖 handleInputChange _.debounce((e) { this.setState({ valueInput: e.target.value }); }, 300);8. 项目部署与维护8.1 打包与发布开发完成后需要通过E9设计器打包发布功能包在ecode平台点击打包按钮生成.eppkg格式的安装包在目标环境的应用市场安装建议在打包前删除调试用的console.log压缩CSS和JS文件更新版本号注释8.2 后续维护建议为了便于后续维护建议编写详细的README说明关键代码添加注释保留测试用例建立变更日志对于可能变化的字段ID可以考虑提取为配置文件// config.js export const FIELD_IDS { date: field6871, buyer: field6872, // ... };这样当字段变化时只需修改一处降低维护成本。