概述
本示例展示了 SpreadJS 中假设分析数据表(Data Table)的加载、计算与文件导入导出流程。
页面左侧显示一个预置的数据表工作簿,右侧提供导入/导出面板、计算模式切换和手动计算按钮,便于观察数据表在不同计算模式下的行为,并验证数据表内容在不同文件格式之间的保存与恢复效果。
实现思路
页面加载后创建 Workbook,并绑定状态栏组件。
通过 spread.fromJSON(datatable) 直接载入预置的数据表工作簿内容。
右侧面板提供导入功能,支持 .sjs、.ssjson、.json 和 .xlsx 文件,并统一采用增量加载和重新计算配置。
导出功能允许把当前工作簿导出为 SSJSON、SJS 或 Excel (XLSX) 格式。
计算模式支持“自动 / 部分 / 手动”三种切换方式,用来观察数据表在不同计算策略下的刷新行为。
用户可以点击“立即计算”按钮手动触发一次完整计算,并在结果区域查看操作反馈。
代码解析
示例入口先创建工作簿实例,然后调用 initSpread 完成数据加载和事件绑定:
初始化时,代码并没有在 app.js 中手动拼装数据表,而是直接从外部的 datatable 对象恢复完整工作簿内容。这意味着示例的主要重点是“如何承载并操作一个已有的数据表模型”:
导入逻辑支持两条路径:如果文件是 .sjs,就调用 spread.open;否则走 spread.import。两种方式都复用同一套选项,确保加载后执行完整重算并启用增量计算:
导出逻辑则根据右侧下拉框决定输出格式。对于 sjs 使用 spread.save,对于 ssjson 和 xlsx 使用 spread.export:
计算模式切换是这个 demo 的另一个重点。示例通过单选按钮修改 spread.options.calculationMode,并分别处理三种模式:
其中“部分”模式不会直接执行完整计算,而是通过挂起并恢复计算服务来刷新除数据表之外的公式。这也是示例里专门区分三种计算方式的原因。
如果用户处于手动模式或希望强制刷新结果,还可以点击“立即计算”按钮主动触发一次 spread.calculate():
运行效果
页面打开后会自动加载一个预置的数据表工作簿,并在左侧工作表区域显示出来。
右侧“导入 / 导出”面板可以把当前数据表另存为不同格式,也可以重新导入外部文件验证恢复效果。
切换“自动 / 部分 / 手动”计算模式后,结果区域会即时提示当前状态。
点击“立即计算”后,工作簿会执行一次显式计算,并显示完成或失败信息。
导入或导出成功后,结果区域会显示短暂的成功提示;发生异常时则显示错误信息。
API 参考
spread.fromJSON(datatable)
用于从预置 JSON 数据恢复整个工作簿内容。
spread.open(file, success, error, options) / spread.import(file, success, error, options)
分别用于打开 SJS 文件和导入其他电子表格文件。
spread.save(...) / spread.export(...)
用于把当前工作簿导出为不同格式。
spread.options.calculationMode
用于切换自动、部分和手动三种计算模式。
spread.calculate()
用于主动触发一次完整计算。
var spread;
window.onload = function () {
spread = new GC.Spread.Sheets.Workbook(document.getElementById("ss"));
initSpread(spread);
};
function isSJSFile(file) {
var fileName = file.name.toLowerCase();
return fileName.endsWith('.sjs');
}
function initSpread(spread) {
var statusBar = new GC.Spread.Sheets.StatusBar.StatusBar(document.getElementById('statusBar'));
statusBar.bind(spread);
spread.fromJSON(datatable); // 从 datatable.js 导入
// 导入文件
document.getElementById("importBtn").addEventListener('click', function() {
document.getElementById("importFile").click();
});
document.getElementById("importFile").addEventListener('change', function(e) {
var resultDiv = document.getElementById("result");
var file = e.target.files[0];
if (!file) return;
var options = {
openMode: 2 /* 增量加载 */,
fullRecalc: true,
dynamicReferences: false,
incrementalCalculation: true
};
try {
if (isSJSFile(file)) {
spread.open(file, function() {
resultDiv.innerHTML = '<div class="success">文件导入成功!</div>';
setTimeout(function() {
resultDiv.innerHTML = '';
}, 2000);
}, function(error) {
resultDiv.innerHTML = '<div class="error">导入文件时出错:' + (error.errorMessage || error.message) + '</div>';
}, options);
} else {
spread.import(file, function() {
resultDiv.innerHTML = '<div class="success">文件导入成功!</div>';
setTimeout(function() {
resultDiv.innerHTML = '';
}, 2000);
}, function(error) {
resultDiv.innerHTML = '<div class="error">导入文件时出错:' + (error.errorMessage || error.message) + '</div>';
}, options);
}
} catch (error) {
resultDiv.innerHTML = '<div class="error">导入文件时出错:' + error.message + '</div>';
}
// 重置输入
e.target.value = '';
});
// 导出文件
document.getElementById("exportBtn").addEventListener('click', function() {
var resultDiv = document.getElementById("result");
var format = document.getElementById("exportFormat").value;
try {
var fileName = "export." + format;
if (format === "sjs") {
spread.save(function(blob) {
saveAs(blob, fileName);
resultDiv.innerHTML = '<div class="success">文件导出成功!</div>';
setTimeout(function() {
resultDiv.innerHTML = '';
}, 2000);
}, function(error) {
resultDiv.innerHTML = '<div class="error">导出文件时出错:' + (error.errorMessage || error.message) + '</div>';
});
} else {
var options = { fileType: format === "ssjson" ? GC.Spread.Sheets.FileType.ssjson : GC.Spread.Sheets.FileType.excel };
spread.export(function(blob) {
saveAs(blob, fileName);
resultDiv.innerHTML = '<div class="success">文件导出成功!</div>';
setTimeout(function() {
resultDiv.innerHTML = '';
}, 2000);
}, function(error) {
resultDiv.innerHTML = '<div class="error">导出文件时出错:' + (error.errorMessage || error.message) + '</div>';
}, options);
}
} catch (error) {
resultDiv.innerHTML = '<div class="error">导出文件时出错:' + error.message + '</div>';
}
});
// 计算模式切换
var calcModeRadios = document.querySelectorAll('input[name="calcMode"]');
calcModeRadios.forEach(function(radio) {
radio.addEventListener('change', function() {
var mode = parseInt(this.value);
var resultDiv = document.getElementById("result");
spread.options.calculationMode = mode;
if (mode === 0) {
spread.calculate();
resultDiv.innerHTML = '<div class="success">计算模式已切换为:自动</div>';
} else if (mode === 2 /* partial */) { // this will calc formulas except for data table
spread.suspendCalcService();
spread.resumeCalcService();
resultDiv.innerHTML = '<div class="success">计算模式已切换为:部分</div>';
} else if (mode === 1) {
resultDiv.innerHTML = '<div class="success">计算模式已切换为:手动</div>';
}
setTimeout(function() {
resultDiv.innerHTML = '';
}, 2000);
});
});
// 立即计算按钮
document.getElementById("calculateBtn").addEventListener('click', function() {
var resultDiv = document.getElementById("result");
try {
spread.calculate();
resultDiv.innerHTML = '<div class="success">计算已完成!</div>';
setTimeout(function() {
resultDiv.innerHTML = '';
}, 2000);
} catch (error) {
resultDiv.innerHTML = '<div class="error">计算时出错:' + error.message + '</div>';
}
});
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="spreadjs culture" content="zh-cn" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-resources-zh/dist/gc.spread.sheets.resources.zh.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-calc-worker/dist/gc.spread.sheets.calcworker.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-io/dist/gc.spread.sheets.io.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/data/datatable.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/FileSaver.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="sample-tutorial">
<div class="spreadsheet-container">
<div id="ss" class="sample-spreadsheets"></div>
<div id="statusBar" class="status-bar"></div>
</div>
<div class="options-container">
<div class="option-row">
<label>导入 / 导出:</label>
<input type="file" id="importFile" accept=".ssjson,.sjs,.json,.xlsx" style="display:none" />
<input type="button" value="导入文件" id="importBtn" />
<label for="exportFormat" style="margin-top: 8px;">导出格式:</label>
<select id="exportFormat">
<option value="ssjson">SSJSON</option>
<option value="sjs">SJS</option>
<option value="xlsx">Excel (XLSX)</option>
</select>
<input type="button" value="导出文件" id="exportBtn" />
</div>
<div class="option-row">
<label>计算模式:</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="calcMode" value="0" checked /> 自动
</label>
<label class="radio-label">
<input type="radio" name="calcMode" value="2" /> 部分
</label>
<label class="radio-label">
<input type="radio" name="calcMode" value="1" /> 手动
</label>
</div>
<input type="button" value="立即计算" id="calculateBtn" />
</div>
<div class="option-row">
<label>结果:</label>
<div id="result" class="result-container"></div>
</div>
</div>
</div>
</body>
</html>
.sample-tutorial {
position: relative;
height: 100%;
overflow: hidden;
}
.spreadsheet-container {
width: calc(100% - 280px);
height: 100%;
float: left;
display: flex;
flex-direction: column;
}
.sample-spreadsheets {
flex: 1;
overflow: hidden;
}
.status-bar {
height: 30px;
border-top: 1px solid #ccc;
}
.options-container {
float: right;
width: 280px;
overflow: auto;
padding: 12px;
height: 100%;
box-sizing: border-box;
background: #fbfbfb;
}
.option-row {
margin-bottom: 12px;
}
.option-row label {
display: block;
margin-bottom: 4px;
font-weight: bold;
font-size: 12px;
}
.description {
font-size: 12px;
color: #555;
line-height: 1.4;
margin: 0 0 8px 0;
}
.description-small {
font-size: 11px;
color: #666;
line-height: 1.3;
margin: 0 0 6px 0;
}
select {
width: 100%;
padding: 6px;
margin-bottom: 6px;
border: 1px solid #ccc;
border-radius: 3px;
box-sizing: border-box;
font-size: 12px;
}
input[type=button] {
width: 100%;
padding: 8px 6px;
margin-bottom: 6px;
background: #007acc;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
}
input[type=button]:hover {
background: #005a9e;
}
input[type=button].secondary-button {
background: #6c757d;
}
input[type=button].secondary-button:hover {
background: #545b62;
}
.result-container {
padding: 8px;
border-radius: 3px;
min-height: 40px;
}
.success {
background: #d4edda;
color: #155724;
padding: 8px;
border-radius: 3px;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 8px;
border-radius: 3px;
}
.radio-group {
margin-bottom: 8px;
}
.radio-label {
display: block;
font-weight: normal;
font-size: 12px;
margin-bottom: 4px;
cursor: pointer;
}
.radio-label input[type="radio"] {
margin-right: 6px;
cursor: pointer;
}
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}