概述
本示例展示了如何在 SpreadJS 中使用目标求解(Goal Seek)功能,根据公式单元格的目标结果,反推出某个可变单元格应取的值。
页面左侧是工作表示例,内置了“收入测算”和“贷款还款”两个场景;右侧是参数面板,用户可以输入公式单元格、目标值、可变单元格、最大迭代次数和容差,然后执行目标求解并查看结果。
实现思路
页面加载后创建一个单工作表的 Workbook,并调用 initSpread 初始化示例数据。
工作表中预先写入两个业务场景:一个使用乘法公式计算总收入,一个使用 PMT 公式计算每月还款额。
右侧表单负责接收目标求解所需的参数,包括公式单元格、目标值、可变单元格以及求解配置项。
点击“运行目标求解”按钮后,代码会先把输入的单元格地址解析为行列索引,再调用 GC.Spread.Sheets.CalcEngine.goalSeek 执行求解。
根据求解结果,页面会在结果区域显示成功、失败或错误提示。
代码解析
示例入口非常直接:先创建工作簿,再初始化当前工作表内容。
工作表初始化时,代码先铺设“目标求解示例”的基础数据,再加入贷款场景和操作说明。这样用户打开页面后就能立即使用两个现成的例子:
第二个示例使用 PMT 公式计算每月还款额。这个区域适合演示“为了让公式结果达到指定值,需要把利率调到多少”这一类反向求解问题:
目标求解按钮点击后,会先读取右侧面板里的输入值,再调用 goalSeek。这里的关键点是:要同时传入“可变单元格”和“公式单元格”的工作表、行、列位置,以及目标值和可选参数:
为了让用户可以直接输入类似 B5、B11 这样的地址,示例额外封装了一个 parseCellReference 方法,把文本地址转换为目标求解 API 所需的工作表、行和列信息:
运行效果
页面打开后,工作表中会直接展示两个可用于测试的目标求解场景。
右侧面板默认给出一组可直接运行的参数,用户无需手动准备公式即可体验目标求解。
当公式单元格、目标值和可变单元格填写正确时,点击“运行目标求解”会在结果区域显示求解成功信息。
如果当前目标值无法求得可行解,结果区域会提示未找到可行解。
如果输入了无效的单元格地址,例如不是单个单元格或引用格式错误,页面会显示错误信息。
API 参考
GC.Spread.Sheets.CalcEngine.goalSeek(...)
根据公式单元格的目标值,自动调整指定可变单元格的值。
GC.Spread.Sheets.CalcEngine.formulaToRanges(...)
用于把用户输入的单元格地址解析为可操作的单元格范围。
sheet.setFormula(row, col, formula)
用于在示例工作表中写入收入计算公式和 PMT 还款公式。
window.onload = function () {
var spread = new GC.Spread.Sheets.Workbook(document.getElementById("ss"), { sheetCount: 1 });
initSpread(spread);
};
function initSpread(spread) {
var sheet = spread.getSheet(0);
// 设置示例数据
sheet.setValue(0, 0, '目标求解示例');
sheet.getRange(0, 0, 1, 2).font('bold 14px Arial');
sheet.setValue(2, 0, '单价:');
sheet.setValue(2, 1, 100);
sheet.setFormatter(2, 1, '$#,##0.00');
sheet.setValue(3, 0, '销售数量:');
sheet.setValue(3, 1, 50);
sheet.setValue(4, 0, '总收入:');
sheet.setFormula(4, 1, '=B3*B4');
sheet.setFormatter(4, 1, '$#,##0.00');
sheet.getCell(4, 1).backColor('#e3f2fd');
sheet.setColumnWidth(0, 120);
sheet.setColumnWidth(1, 100);
// 添加 PMT 示例
sheet.setValue(6, 0, '贷款还款示例(PMT)');
sheet.getRange(6, 0, 1, 2).font('bold 14px Arial');
sheet.setValue(8, 0, '贷款金额:');
sheet.setValue(8, 1, 10000);
sheet.setFormatter(8, 1, '$#,##0.00');
sheet.setValue(9, 0, '期限(月):');
sheet.setValue(9, 1, 18);
sheet.setValue(10, 0, '利率:');
sheet.setValue(10, 1, 0.05);
sheet.setFormatter(10, 1, '0.00%');
sheet.setValue(11, 0, '每月还款额:');
sheet.setFormula(11, 1, '=-PMT(B11/12,B10,B9)');
sheet.setFormatter(11, 1, '$#,##0.00');
sheet.getCell(11, 1).backColor('#fff3cd');
// 添加说明
sheet.setValue(13, 0, '操作说明:');
sheet.getCell(13, 0).font('bold 12px Arial');
sheet.setValue(14, 0, '1. 示例 1:将 B5 设为公式单元格,用于求出目标收入');
sheet.setValue(15, 0, '2. 示例 2:将 B12 设为公式单元格,目标值设为 600');
sheet.setValue(16, 0, ' 并将 B11 作为可变单元格,以求出所需利率');
sheet.setValue(17, 0, '3. 输入参数后,点击“运行目标求解”');
// 目标求解实现
document.getElementById("runGoalSeek").addEventListener('click', function() {
var formulaCellRef = document.getElementById("formulaCell").value;
var targetValue = parseFloat(document.getElementById("targetValue").value);
var variableCellRef = document.getElementById("variableCell").value;
var maximumIterations = parseInt(document.getElementById("maximumIterations").value) || 100;
var tolerance = parseFloat(document.getElementById("tolerance").value) || 0.001;
var resultDiv = document.getElementById("result");
try {
var formulaCell = parseCellReference(sheet, formulaCellRef);
var variableCell = parseCellReference(sheet, variableCellRef);
var seekResult = GC.Spread.Sheets.CalcEngine.goalSeek(
variableCell.sheet, variableCell.row, variableCell.col,
formulaCell.sheet, formulaCell.row, formulaCell.col, targetValue,
{
maximumIterations: maximumIterations,
tolerance: tolerance
});
if (seekResult) {
resultDiv.innerHTML = '<div class="success">目标求解成功!<br/>求得结果:' + formulaCell.sheet.getValue(formulaCell.row, formulaCell.col).toFixed(2) + '</div>';
} else {
resultDiv.innerHTML = '<div class="error">目标求解未找到可行解。<br/>请尝试其他目标值。</div>';
}
} catch (e) {
resultDiv.innerHTML = '<div class="error">错误:' + e.message + '</div>';
}
});
}
function parseCellReference(sheet, address) {
var ranges = GC.Spread.Sheets.CalcEngine.formulaToRanges(sheet, address, 0, 0);
if (ranges && ranges.length === 1 && ranges[0].ranges.length === 1 && ranges[0].ranges[0].rowCount === 1 && ranges[0].ranges[0].colCount === 1) {
return {
sheet: sheet.getParent().getSheetFromName(ranges[0].sheetName),
row: ranges[0].ranges[0].row,
col: ranges[0].ranges[0].col
};
} else {
throw new Error('无效的单元格引用:' + address);
}
}
<!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$/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 id="ss" class="sample-spreadsheets"></div>
<div class="options-container">
<div class="option-row">
<label>公式单元格:</label>
<input type="text" id="formulaCell" value="B5" placeholder="例如:B5" />
</div>
<div class="option-row">
<label>目标值:</label>
<input type="number" id="targetValue" value="10000" placeholder="例如:10000" />
</div>
<div class="option-row">
<label>可变单元格:</label>
<input type="text" id="variableCell" value="B3" placeholder="例如:B3" />
</div>
<div class="option-row">
<label>最大迭代次数:</label>
<input type="number" id="maximumIterations" value="100" placeholder="默认值:100" />
</div>
<div class="option-row">
<label>容差:</label>
<input type="number" id="tolerance" value="0.001" step="0.0001" placeholder="默认值:0.001" />
</div>
<div class="option-row">
<input type="button" value="运行目标求解" id="runGoalSeek" />
</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;
}
.sample-spreadsheets {
width: calc(100% - 280px);
height:100%;
overflow: hidden;
float: left;
}
.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;
}
input[type=text],
input[type=number] {
width: 100%;
padding: 6px;
border: 1px solid #ccc;
border-radius: 3px;
box-sizing: border-box;
}
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;
}
.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;
}
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}