NetSuite Suitlet自定义GL匹配页面分页及选中项处理技术咨询
NetSuite Suitlet 类SPA式GL匹配页面实现方案
一、分页功能实现(无刷新类SPA模式)
完全可以实现每次加载500条结果的分页,且规避传统页码参数跳转的局限性,采用前端异步请求+后端数据接口的类SPA模式即可:
- 将Suitlet设计为两种运行模式:
- 模式1:初始请求时返回完整页面框架,包含筛选栏、分页按钮、结果表格容器及客户端脚本。
- 模式2:接收前端传入的
page(页码)、filters(筛选条件)参数,返回当前页的交易记录JSON数据,每次最多返回500条。
- 客户端脚本核心逻辑:
- 点击「上一页/下一页」按钮时,拦截默认跳转行为,用
fetch或N/https客户端模块异步调用Suitlet的模式2接口,传入当前页码与筛选条件。 - 拿到返回的JSON数据后,动态更新页面结果表格内容,全程不刷新页面,符合类SPA的交互要求。
- 计算总页数:首次请求时获取搜索结果的总记录数,用总记录数除以500得到总页数,以此控制分页按钮的禁用状态,例如第一页时禁用「上一页」按钮。
- 点击「上一页/下一页」按钮时,拦截默认跳转行为,用
二、选中项存储方案(基于NetSuite缓存模块)
用N/cache模块存储选中项完全可行,且能适配后续计划脚本/映射归约脚本的处理需求:
- 缓存配置:创建用户作用域的缓存(
CacheScope.PRIVATE),确保每个用户的选中项独立存储,互不干扰。 - 客户端与缓存的交互逻辑:
- 当用户勾选/取消勾选交易记录时,客户端脚本将选中的交易ID列表(或单个ID的状态变化)发送到Suitlet的专用接口,由Suitlet调用
N/cache更新缓存数据,例如用put方法存储JSON格式的ID数组。 - 切换分页加载新数据时,客户端先从缓存中读取已选中的ID列表,渲染新页表格时自动匹配ID,恢复对应记录的勾选状态。
- 当用户勾选/取消勾选交易记录时,客户端脚本将选中的交易ID列表(或单个ID的状态变化)发送到Suitlet的专用接口,由Suitlet调用
- 与处理脚本的对接:
- 用户触发批量操作(如GL匹配)时,客户端调用Suitlet接口获取缓存中的所有选中ID。
- 若选中项数量较多,直接传递ID列表可能超出参数长度限制,可生成一个唯一临时键(如用户ID+时间戳),将选中ID列表存入该键下,再将临时键作为参数传给计划脚本/映射归约脚本。
- 处理脚本通过临时键从缓存中读取选中ID列表,执行后续业务逻辑。
三、关键代码片段示例
1. Suitlet核心逻辑(页面渲染+数据接口)
/** * @NApiVersion 2.x * @NScriptType Suitelet */ define(['N/ui/serverWidget', 'N/search', 'N/cache', 'N/format', 'N/task'], (serverWidget, search, cache, format, task) => { const onRequest = (context) => { const request = context.request; const response = context.response; // 处理数据请求(模式2) if (request.parameters.data) { const page = parseInt(request.parameters.page) || 1; const filters = JSON.parse(request.parameters.filters); const pageSize = 500; // 构建交易搜索(包含发票、账单、日记账) const transactionSearch = search.create({ type: search.Type.TRANSACTION, filters: [...filters, ['type', 'anyof', 'CustInvc', 'Bill', 'Journal']], columns: ['tranid', 'type', 'trandate', 'amount', 'status'] }); // 获取当前页数据与总页数 const pagedData = transactionSearch.runPaged({pageSize: pageSize}); const currentPageData = pagedData.fetch({index: page - 1}).data; const totalPages = pagedData.count; // 格式化数据为JSON const formattedResults = currentPageData.map(result => ({ id: result.id, tranid: result.getValue('tranid'), type: result.getText('type'), trandate: format.format({value: result.getValue('trandate'), type: format.Type.DATE}), amount: result.getValue('amount'), status: result.getText('status') })); response.write(JSON.stringify({data: formattedResults, totalPages: totalPages})); return; } // 触发计划脚本处理选中项 if (request.parameters.trigger_process) { const tempKey = JSON.parse(request.body).tempKey; const cacheObj = cache.getCache({name: 'gl_match_selected', scope: cache.Scope.PRIVATE}); const selectedIds = JSON.parse(cacheObj.get({key: tempKey})); // 创建并提交计划脚本任务 const scheduledTask = task.create({taskType: task.TaskType.SCHEDULED_SCRIPT}); scheduledTask.scriptId = 'customscript_gl_match_processor'; scheduledTask.deploymentId = 'customdeploy_gl_match_processor'; scheduledTask.params = {custscript_selected_ids_key: tempKey}; scheduledTask.submit(); response.write(JSON.stringify({status: 'success'})); return; } // 初始页面渲染(模式1) const form = serverWidget.createForm({title: '自定义GL匹配页面'}); // 添加筛选字段 const filterGroup = form.addFieldGroup({id: 'filters', label: '筛选条件'}); form.addField({id: 'trandate_from', type: serverWidget.FieldType.DATE, label: '交易日期从', container: 'filters'}); form.addField({id: 'trandate_to', type: serverWidget.FieldType.DATE, label: '交易日期到', container: 'filters'}); form.addField({id: 'status', type: serverWidget.FieldType.SELECT, label: '交易状态', container: 'filters', source: 'transactionstatus'}); // 添加结果容器与分页控件 form.addField({id: 'results_container', type: serverWidget.FieldType.INLINEHTML, label: '交易记录'}).defaultValue = ` <div id="results_table" style="margin: 10px 0;"></div> <div id="pagination" style="margin-top: 15px;"> <button id="prev_btn" disabled>上一页</button> <span id="page_text">第1页 / 共0页</span> <button id="next_btn" disabled>下一页</button> <button id="process_btn" style="margin-left: 20px;">批量处理选中项</button> </div> `; // 绑定客户端脚本 form.clientScriptModulePath = './gl_match_client.js'; response.writePage(form); }; return {onRequest}; });
2. 客户端脚本(分页交互+选中项同步)
/** * @NApiVersion 2.x * @NScriptType ClientScript */ define(['N/https', 'N/cache'], (https, cache) => { let currentPage = 1; let totalPages = 0; let selectedIds = []; const cacheObj = cache.getCache({name: 'gl_match_selected', scope: cache.Scope.PRIVATE}); const pageInit = () => { // 从缓存加载已选中的ID const cachedData = cacheObj.get({key: 'selected_ids'}); if (cachedData) selectedIds = JSON.parse(cachedData); // 绑定按钮事件 document.getElementById('prev_btn').addEventListener('click', () => loadPage(--currentPage)); document.getElementById('next_btn').addEventListener('click', () => loadPage(++currentPage)); document.getElementById('process_btn').addEventListener('click', triggerProcess); // 加载第一页数据 loadPage(currentPage); }; const loadPage = (page) => { // 收集筛选条件 const filters = [ ['trandate', 'within', document.getElementById('trandate_from').value, document.getElementById('trandate_to').value], ['status', 'anyof', document.getElementById('status').value] ].filter(item => item[2] && item[3]); // 异步请求分页数据 const response = https.post({ url: window.location.href + '&data=1', body: JSON.stringify({page: page, filters: filters}) }); const result = JSON.parse(response.body); totalPages = result.totalPages; // 更新分页控件状态 document.getElementById('page_text').textContent = `第${page}页 / 共${totalPages}页`; document.getElementById('prev_btn').disabled = page === 1; document.getElementById('next_btn').disabled = page === totalPages; // 渲染结果表格 let tableHtml = ` <table border="1" cellpadding="5" cellspacing="0"> <thead><tr><th>勾选</th><th>交易编号</th><th>类型</th><th>日期</th><th>金额</th><th>状态</th></tr></thead> <tbody> `; result.data.forEach(item => { const isChecked = selectedIds.includes(item.id) ? 'checked' : ''; tableHtml += ` <tr> <td><input type="checkbox" class="tran_check" data-id="${item.id}" ${isChecked}></td> <td>${item.tranid}</td> <td>${item.type}</td> <td>${item.trandate}</td> <td>${item.amount}</td> <td>${item.status}</td> </tr> `; }); tableHtml += '</tbody></table>'; document.getElementById('results_table').innerHTML = tableHtml; // 绑定勾选事件 document.querySelectorAll('.tran_check').forEach(checkbox => { checkbox.addEventListener('change', handleCheckboxChange); }); }; const handleCheckboxChange = (e) => { const tranId = e.target.dataset.id; if (e.target.checked) { if (!selectedIds.includes(tranId)) selectedIds.push(tranId); } else { selectedIds = selectedIds.filter(id => id !== tranId); } // 同步到缓存,设置1小时过期 cacheObj.put({ key: 'selected_ids', value: JSON.stringify(selectedIds), ttl: 3600 }); }; const triggerProcess = () => { if (selectedIds.length === 0) { alert('请选择需要处理的交易记录'); return; } // 生成临时键存储选中项 const tempKey = `selected_${Date.now()}`; cacheObj.put({key: tempKey, value: JSON.stringify(selectedIds), ttl: 1800}); // 请求Suitlet触发计划脚本 https.post({ url: window.location.href + '&trigger_process=1', body: JSON.stringify({tempKey: tempKey}) }); alert('处理任务已提交,请稍后查看结果'); }; return {pageInit}; });
四、注意事项
- 缓存TTL设置:根据业务场景设置合理的过期时间,既要避免缓存占用过多资源,也要确保用户操作过程中数据不会提前失效。
- 搜索性能优化:对于超大量数据的查询,建议使用已保存搜索(
search.load)而非动态创建搜索,提升查询效率。 - 参数长度限制:当选中项数量极大时,禁止直接在请求中传递ID列表,必须通过缓存临时键间接传递,避免触发参数长度限制报错。
内容的提问来源于stack exchange,提问作者AnthoVdo
相关产品推荐
相关产品推荐

