如何将SpreadJS与服务端Bean绑定?Spring WebMVC双向同步实现
我来帮你搞定Spring WebMVC + Primefaces + SpreadJS的双向数据绑定和交互问题,这几个组件搭在一起,核心就是要打通后端Bean、前端SpreadJS表格和Primefaces节点组件这三者的数据流,下面分步骤给你拆解:
整体逻辑梳理
简单来说,我们要实现两个核心链路:
- 后端Bean ↔ SpreadJS表格:服务端数据能渲染到SpreadJS,表格修改后能同步回后端更新Bean
- 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
相关产品推荐
相关产品推荐

