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

如何将SpreadJS与服务端Bean绑定?Spring WebMVC双向同步实现

我来帮你搞定Spring WebMVC + Primefaces + SpreadJS的双向数据绑定和交互问题,这几个组件搭在一起,核心就是要打通后端Bean、前端SpreadJS表格和Primefaces节点组件这三者的数据流,下面分步骤给你拆解:

整体逻辑梳理

简单来说,我们要实现两个核心链路:

  1. 后端Bean ↔ SpreadJS表格:服务端数据能渲染到SpreadJS,表格修改后能同步回后端更新Bean
  2. Primefaces节点组件 ↔ SpreadJS单元格:拖拽节点到表格后,组件值能同步到单元格;表格单元格修改时,对应节点组件的值也要跟着更新

一、SpreadJS与服务端Bean的数据绑定

这部分要处理前后端数据的双向传递,关键是把Bean数据转换成SpreadJS能识别的JSON格式,再把SpreadJS的JSON转成后端能接收的Bean结构。

1. 后端Bean数据传递到前端SpreadJS

在Spring Controller里,把你的业务Bean列表转换成SpreadJS的JSON配置(包含单元格数据、类型、样式等),然后通过接口返回给前端:

@GetMapping("/load-spread-data")
@ResponseBody
public String loadSpreadData() {
    // 从业务服务获取节点Bean列表
    List<NodeBean> nodeList = nodeService.getAllNodes();
    
    // 构建SpreadJS所需的JSON结构
    JSONObject spreadConfig = new JSONObject();
    JSONArray sheets = new JSONArray();
    JSONObject dataSheet = new JSONObject();
    dataSheet.put("name", "节点数据表格");
    
    JSONArray rows = new JSONArray();
    for (NodeBean bean : nodeList) {
        JSONObject row = new JSONObject();
        JSONArray cells = new JSONArray();
        
        // 第一列:节点名称(只读)
        JSONObject nameCell = new JSONObject();
        nameCell.put("value", bean.getName());
        nameCell.put("readOnly", true);
        cells.add(nameCell);
        
        // 第二列:节点值(根据输入类型配置单元格)
        JSONObject valueCell = new JSONObject();
        valueCell.put("value", bean.getValue());
        // 给单元格绑定节点ID,后续同步用
        valueCell.put("tag", new JSONObject().put("nodeId", bean.getId()));
        
        switch (bean.getInputType()) {
            case "SELECT":
                // 配置下拉框校验器(对应SpreadJS的下拉列表)
                JSONObject selectValidator = new JSONObject();
                selectValidator.put("type", "list");
                selectValidator.put("source", bean.getOptions());
                valueCell.put("validators", new JSONArray().put(selectValidator));
                break;
            case "DATE":
                // 配置日期格式和编辑器
                valueCell.put("formatter", "yyyy-MM-dd");
                JSONObject dateEditor = new JSONObject();
                dateEditor.put("type", "date");
                valueCell.put("editor", dateEditor);
                break;
            case "TEXT":
                // 普通文本输入默认配置即可
                break;
        }
        cells.add(valueCell);
        row.put("cells", cells);
        rows.add(row);
    }
    
    dataSheet.put("rows", rows);
    sheets.add(dataSheet);
    spreadConfig.put("sheets", sheets);
    
    return spreadConfig.toString();
}

前端页面加载时,通过AJAX获取这个JSON并初始化SpreadJS:

$(document).ready(function() {
    // 初始化SpreadJS实例
    const spread = new GC.Spread.Sheets.Workbook($("#spread-container")[0]);
    // 存全局变量方便后续操作
    window.spreadInstance = spread;
    
    // 请求后端数据加载表格
    $.get("/load-spread-data", function(res) {
        const spreadData = JSON.parse(res);
        spread.fromJSON(spreadData);
        // 缓存初始数据,用于后续同步校验
        window.initialSpreadData = spreadData;
    });
});

2. SpreadJS数据回传到后端更新Bean

可以在用户点击保存按钮时,把SpreadJS的表格数据转换成后端Bean的格式,通过AJAX提交:

<!-- Primefaces保存按钮 -->
<p:commandButton value="保存数据" onclick="saveSpreadData(); return false;" />

对应的JS逻辑:

function saveSpreadData() {
    const spread = window.spreadInstance;
    const activeSheet = spread.getActiveSheet();
    const rowCount = activeSheet.getRowCount();
    
    // 构建后端需要的NodeBean列表
    const nodeList = [];
    for (let i = 0; i < rowCount; i++) {
        const nodeName = activeSheet.getValue(i, 0);
        const nodeValue = activeSheet.getValue(i, 1);
        // 从单元格tag获取节点ID
        const nodeId = activeSheet.getCell(i, 1).tag().nodeId;
        // 从初始数据匹配输入类型
        const inputType = window.initialSpreadData.sheets[0].rows[i].cells[1].editor 
            ? "DATE" 
            : (window.initialSpreadData.sheets[0].rows[i].cells[1].validators ? "SELECT" : "TEXT");
        
        nodeList.push({
            id: nodeId,
            name: nodeName,
            value: nodeValue,
            inputType: inputType
        });
    }
    
    // 用Primefaces的AJAX提交数据
    PF('saveDataAjax').send({
        params: {
            'nodeListJson': JSON.stringify(nodeList)
        }
    });
}

后端Controller接收并处理:

@PostMapping("/save-spread-data")
@ResponseBody
public String saveSpreadData(@RequestParam("nodeListJson") String nodeListJson) {
    ObjectMapper mapper = new ObjectMapper();
    try {
        List<NodeBean> nodeList = mapper.readValue(nodeListJson, new TypeReference<List<NodeBean>>() {});
        nodeService.batchUpdateNodes(nodeList);
        return "success";
    } catch (JsonProcessingException e) {
        return "error: " + e.getMessage();
    }
}

二、Primefaces节点组件与SpreadJS的双向同步

这部分要处理拖拽交互和值的实时同步,分两个场景:拖拽节点到表格、表格修改同步回节点组件。

1. 拖拽节点到SpreadJS后同步组件值

给每个节点绑定拖拽事件,拖拽结束时把组件当前值设置到目标单元格:

<!-- 节点列表渲染 -->
<p:dataList value="#{nodeBeanList}" var="node">
    <div class="node-item" id="node_#{node.id}">
        <h:outputText value="#{node.name}" />
        <!-- 根据输入类型渲染Primefaces组件 -->
        <c:choose>
            <c:when test="#{node.inputType == 'SELECT'}">
                <p:selectOneMenu value="#{node.value}" id="select_#{node.id}">
                    <f:selectItems value="#{node.options}" />
                </p:selectOneMenu>
            </c:when>
            <c:when test="#{node.inputType == 'DATE'}">
                <p:calendar value="#{node.value}" id="date_#{node.id}" pattern="yyyy-MM-dd" />
            </c:when>
            <c:otherwise>
                <p:inputText value="#{node.value}" id="text_#{node.id}" />
            </c:otherwise>
        </c:choose>
    </div>
    <!-- Primefaces拖拽组件 -->
    <p:draggable for="node_#{node.id}" helper="clone" onStop="handleNodeDrop(event, #{node.id});" />
</p:dataList>

拖拽结束的JS处理:

function handleNodeDrop(event, nodeId) {
    const spread = window.spreadInstance;
    const activeSheet = spread.getActiveSheet();
    // 获取拖拽到的目标单元格
    const selection = activeSheet.getSelections()[0];
    if (!selection) return;
    
    const targetRow = selection.row;
    const targetCol = selection.col;
    
    // 获取对应节点组件的值
    let nodeValue = "";
    const selectComp = PF(`select_${nodeId}`);
    if (selectComp) {
        nodeValue = selectComp.getValue();
    } else {
        const dateComp = PF(`date_${nodeId}`);
        if (dateComp) {
            nodeValue = dateComp.getValue()?.toISOString().split('T')[0] || "";
        } else {
            const textComp = PF(`text_${nodeId}`);
            nodeValue = textComp.getValue() || "";
        }
    }
    
    // 设置到SpreadJS单元格
    activeSheet.setValue(targetRow, targetCol, nodeValue);
    // 同时更新后端Bean(可选,也可以批量保存)
    updateNodeValue(nodeId, nodeValue);
}

// 实时更新单个节点的后端值
function updateNodeValue(nodeId, value) {
    PF('updateNodeAjax').send({
        params: {
            'nodeId': nodeId,
            'value': value
        }
    });
}

2. SpreadJS单元格修改同步回Primefaces组件

监听SpreadJS的单元格值变化事件,更新对应节点组件的值:

$(document).ready(function() {
    const spread = window.spreadInstance;
    const activeSheet = spread.getActiveSheet();
    
    // 监听单元格值变化事件
    activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, function(e, args) {
        const cellTag = activeSheet.getCell(args.row, args.col).tag();
        if (!cellTag?.nodeId) return;
        
        const nodeId = cellTag.nodeId;
        const newValue = args.newValue;
        
        // 更新对应的Primefaces组件
        updatePrimefacesNode(nodeId, newValue);
        // 同步更新后端Bean
        updateNodeValue(nodeId, newValue);
    });
});

function updatePrimefacesNode(nodeId, newValue) {
    const selectComp = PF(`select_${nodeId}`);
    if (selectComp) {
        selectComp.setValue(newValue);
        selectComp.update(); // 刷新组件显示
    } else {
        const dateComp = PF(`date_${nodeId}`);
        if (dateComp) {
            dateComp.setValue(new Date(newValue));
            dateComp.update();
        } else {
            const textComp = PF(`text_${nodeId}`);
            textComp.setValue(newValue);
            textComp.update();
        }
    }
}

关键注意事项
  • 数据格式统一:日期、下拉选项等类型要前后端保持一致,避免格式转换错误
  • 性能优化:如果数据量大,尽量减少实时AJAX请求,改用批量提交
  • 组件ID唯一性:所有Primefaces组件的ID要和节点ID绑定,确保能准确获取实例
  • SpreadJS事件时机:ValueChanged事件在单元格编辑完成后触发,能拿到最终的修改值

内容的提问来源于stack exchange,提问作者S.Fahmi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 09:19:12