You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,恢复对应记录的勾选状态。
  • 与处理脚本的对接:
    • 用户触发批量操作(如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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 22:12:29