#SheetNextJS 集成指南
本文档面向在业务系统中接入 SheetNext 的开发者,包含初始化、常用 API 和保存恢复示例。
若要查看更详细的底层操作 API、完整方法签名和高级示例,请安装或查看 SheetNext Dev Skill:https://github.com/wyyazlz/sheetnext/tree/master/docs/skill/sheetnext-dev
对照源码版本:
0.2.23;更新日期:2026-09-08。源码版本不等于 npm 已发布版本,接入时请核对实际交付包及锁文件。快速指南维护于本站 Markdown;详细 API 来自源码注释和文档扫描器。使用旧版本时,请查看对应发布版本的文档。
#目录
- SheetNextJS 集成指南
#1. 快速开始
SheetNext 支持两种常见接入方式:
- 使用 CDN,通过
<link>和<script>直接在页面中引入。 - 使用 npm 安装,在 Vite、Webpack、Rollup 等工程化项目中通过
import引入。
先确定使用开源版还是商业版:两者都支持单元格编辑和 getData / setData JSON 对象保存恢复;文件导入导出、内置 AI、透视表创建与处理需要相应商业发行包和授权。完整差异见第 2 节。
#1.1 CDN 方式
下面以 0.2.23 的资源路径说明 CDN 接入。先确认该版本已发布且符合所需版本能力,再把示例中的版本替换为实际选定版本。JS、CSS 和语言包必须使用相同版本;生产环境不要省略版本号。商业交付包、离线环境使用第 1.3 节的自托管方式。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SheetNext CDN Demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sheetnext@0.2.23/dist/sheetnext.css" />
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
.sheetnext-container {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<div class="sheetnext-container"></div>
<script src="https://cdn.jsdelivr.net/npm/sheetnext@0.2.23/dist/sheetnext.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sheetnext@0.2.23/dist/sheetnext.locale.zh-CN.umd.js"></script>
<script>
const SN = new SheetNext(document.querySelector('.sheetnext-container'), {
locale: 'zh-CN'
// licenseKey: 'your-license-key'
});
</script>
</body>
</html>
#1.2 npm 安装方式
先核对可用发布版本,再固定安装版本;下面仍以 0.2.23 为例:
npm install --save-exact sheetnext@0.2.23
在业务入口文件中引入:
import SheetNext from 'sheetnext';
import 'sheetnext/dist/sheetnext.css';
import zhCN from 'sheetnext/locales/zh-CN';
SheetNext.registerLocale('zh-CN', zhCN);
const container = document.querySelector('.sheetnext-container');
const SN = new SheetNext(container, {
locale: 'zh-CN'
// licenseKey: 'your-license-key'
});
页面中需要提供容器并设置明确宽高:
<div class="sheetnext-container"></div>
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
.sheetnext-container {
width: 100%;
height: 100vh;
}
React、Vue 等框架中,使用组件自己的容器 ref,在挂载后初始化,每个实例独立保存引用。SSR 项目仅在浏览器端加载和初始化编辑器。
当前公开 API 没有 SN.destroy() / SN.dispose()。移除容器和业务监听不能清理编辑器内部的 document/window 监听与观察器;频繁挂载卸载的页面应复用实例,或使用独立 iframe 生命周期,待编辑器提供完整销毁能力后再直接按组件销毁。业务监听用 SN.Event.off(eventName, handler) 逐个解除,不要用 offAll(),它也会移除编辑器自身的保护等监听。
#1.3 本地 dist 文件方式
如果客户不能访问外部 CDN,也可以把构建产物放到业务系统静态资源目录后引入。
dist/
sheetnext.css
sheetnext.umd.js
sheetnext.es.js
sheetnext.locale.zh-CN.umd.js
sheetnext.locale.zh-CN.es.js
浏览器 <script> 方式通常需要:
sheetnext.csssheetnext.umd.jssheetnext.locale.zh-CN.umd.js,中文界面需要引入
本地文件引入示例:
<link rel="stylesheet" href="./dist/sheetnext.css" />
<script src="./dist/sheetnext.umd.js"></script>
<script src="./dist/sheetnext.locale.zh-CN.umd.js"></script>
注意事项:
- 容器必须有明确宽高,否则编辑器无法正常渲染。
- 中文界面需要先加载中文语言包,再初始化时传入
locale: 'zh-CN'。 - 多实例场景下,为每个容器分别
new SheetNext(container, options)即可。
#2. 产品定位
SheetNext 是一个纯前端 JavaScript Excel 编辑器。业务系统只需要在页面中准备一个容器元素,然后引入 SheetNext 的 JS 和 CSS,即可创建一个可编辑的工作簿实例。
典型能力包括:
- Excel 风格的单元格编辑、样式、合并、行列操作、排序、筛选。
- XLSX、CSV、JSON、HTML 导入导出。
- 多工作表、公式计算、撤销重做。
- 表格、批注、图片、图表、透视表、透视图、条件格式、数据验证、保护工作表等能力。
- 事件监听,便于业务系统接入保存、审计、联动和权限控制。
| 能力 | 开源版 | 商业发行包 |
|---|---|---|
| 编辑、公式、图片图表、表格、批注、数据验证、打印 | 支持 | 支持 |
JSON 对象 SN.IO.getData() / SN.IO.setData() | 支持 | 支持 |
文件 import / importFromUrl / export / exportAllImage | 禁用 | 需要相应授权 |
| 内置 AI | 禁用 | 需要相应授权和业务 AI relay |
| 透视表创建、处理及相关透视图流程 | 禁用 | 需要相应授权 |
开源发行包中的受限功能是禁用实现,填入授权码不会补齐缺失实现。需要上述功能时应使用商业交付包。商业包可能提供带额度限制的未激活体验,正式使用以实际授权为准。后文文件导入导出、透视表示例均以具备相应能力为前提。
#3. 初始化参数
const SN = new SheetNext(container, options);
container 是编辑器容器 DOM,options 是可选配置。
常用参数:
| 参数 | 类型 | 说明 |
|---|---|---|
licenseKey | string | 授权码 |
locale | string | 初始语言,例如 zh-CN、en-US |
locales | Object | 额外语言包 |
menuRight | function | 自定义右侧菜单 HTML |
menuList | function | 自定义顶部菜单配置 |
minimalToolbar | boolean | 初始使用精简工具栏 |
theme | Object 或 string | 主题颜色配置,字符串表示主色 |
AI 相关参数 AI_URL、AI_TOKEN 仅用于 AI relay,不建议普通客户手动集成时配置。
需要内置 AI 时,先阅读 AI relay 接入与执行边界。当前 Query/Edit 工具在页面执行模型生成的 JavaScript,接入方需要设计隔离和权限边界;模型服务密钥保存在后端,AI_TOKEN 只用于浏览器可见的业务认证令牌。
自定义菜单示例:
const SN = new SheetNext(container, {
locale: 'zh-CN',
menuList(config) {
return [
...config,
{
key: 'customSave',
text: '保存',
trigger: 'action',
color: '#1677ff',
action: 'window.saveSheetNextWorkbook && window.saveSheetNextWorkbook()'
}
];
}
});
#4. 基础对象关系
初始化后得到的 SN 是工作簿实例。
常用对象:
| 对象 | 获取方式 | 说明 |
|---|---|---|
| 工作簿 | SN | 管理工作表、导入导出、事件、公式等 |
| 当前工作表 | SN.activeSheet | 当前正在编辑的 Sheet |
| 指定工作表 | SN.getSheet('Sheet1') | 按名称获取 |
| 单元格 | sheet.getCell('A1') 或 sheet.getCell(0, 0) | 读写值和样式 |
| 事件中心 | SN.Event | 监听工作簿和工作表事件 |
| 导入导出 | SN.IO | 文件和 JSON 数据导入导出 |
| 工具方法 | SN.Utils | 单元格地址转换、颜色转换等 |
坐标规则:
- 数字坐标使用从 0 开始的行列索引,例如
{ r: 0, c: 0 }表示 A1。 - A1 字符串使用 Excel 风格,从 1 开始,例如
'A1'、'A1:C3'。
#5. 工作簿操作
// 新增工作表
const sheet = SN.addSheet('销售数据');
// 获取工作表
const reportSheet = SN.getSheet('销售数据');
// 移动工作表位置
SN.moveSheet('销售数据', 0);
// 设置当前工作表
SN.activeSheet = reportSheet;
// 工作簿名
SN.workbookName = '月度报表';
// 切换只读
SN.readOnly = true;
SN.readOnly = false;
// 切换语言
SN.setLocale('zh-CN');
// 重算公式
SN.recalculate();
不再需要某个工作表时调用 SN.delSheet('销售数据')。删除后不要继续把旧的 Sheet 对象设置为活动工作表;新增、查询也可能返回 null,业务代码应处理名称冲突、取消事件等情况。
#6. 单元格读写
#6.1 单元格基础设置
const sheet = SN.activeSheet;
// 写入值
sheet.getCell('A1').editVal = '姓名';
sheet.getCell('B1').editVal = '金额';
sheet.getCell('A2').editVal = '张三';
sheet.getCell('B2').editVal = 12800;
// 设置数字格式
sheet.getCell('B2').numFmt = '#,##0.00';
// 写入公式,公式也通过 editVal 写入
sheet.getCell('B3').editVal = '=SUM(B2:B2)';
// 读取值
const rawValue = sheet.getCell('A2').editVal;
const calculatedValue = sheet.getCell('B3').calcVal;
const displayText = sheet.getCell('B3').showVal;
常用属性:
| 属性 | 说明 |
|---|---|
editVal | 编辑值。普通值和公式都通过它写入 |
calcVal | 计算后的值,公式单元格通常读这个 |
showVal | 格式化后的显示文本 |
numFmt | 数字格式,例如金额、百分比、日期 |
font | 字体样式 |
fill | 填充样式 |
alignment | 对齐方式 |
border | 边框样式 |
hyperlink | 超链接配置 |
使用 .editVal 写入值和公式,不要使用 .value。editVal 的读取结果是编辑文本;需要数字、布尔或公式计算结果时读取 calcVal,展示时读取 showVal。
#6.2 常用样式
单个单元格样式:
const cell = sheet.getCell('A1');
cell.font = {
bold: true,
size: 14,
color: '#ffffff'
};
cell.fill = {
fgColor: '#1677ff'
};
cell.alignment = {
horizontal: 'center',
vertical: 'center'
};
cell.border = {
top: { style: 'thin', color: '#d9d9d9' },
right: { style: 'thin', color: '#d9d9d9' },
bottom: { style: 'thin', color: '#d9d9d9' },
left: { style: 'thin', color: '#d9d9d9' }
};
cell.numFmt = '#,##0.00';
批量处理区域:
sheet.eachCells('A1:C3', (rowIndex, colIndex) => {
const cell = sheet.getCell(rowIndex, colIndex);
cell.fill = { fgColor: '#fff7e6' };
cell.alignment = { horizontal: 'center', vertical: 'center' };
});
常用数字格式示例:
sheet.getCell('B2').numFmt = '#,##0.00'; // 数值,保留 2 位小数
sheet.getCell('C2').numFmt = '0.00%'; // 百分比
sheet.getCell('D2').numFmt = 'yyyy-mm-dd'; // 日期
sheet.getCell('E2').numFmt = '¥#,##0.00'; // 人民币金额
超链接示例:
const linkCell = sheet.getCell('A5');
linkCell.editVal = '打开官网';
linkCell.hyperlink = {
target: 'https://www.sheetnext.com',
tooltip: 'SheetNext'
};
#6.3 公式
公式通过 editVal 写入,必须以 = 开头。读取结果时用 calcVal 或 showVal。
const sheet = SN.activeSheet;
sheet.getCell('A1').editVal = '项目';
sheet.getCell('B1').editVal = '金额';
sheet.getCell('A2').editVal = '收入';
sheet.getCell('B2').editVal = 1200;
sheet.getCell('A3').editVal = '成本';
sheet.getCell('B3').editVal = 300;
sheet.getCell('A4').editVal = '利润';
sheet.getCell('B4').editVal = '=B2-B3';
// 自动计算模式下通常可直接读取
console.log(sheet.getCell('B4').calcVal);
// 如需主动重算整个工作簿
SN.recalculate();
计算模式:
SN.calcMode = 'auto'; // 自动计算
SN.calcMode = 'manual'; // 手动计算
// 手动模式下修改数据后,调用重算
SN.recalculate();
#7. 行列与区域操作
const sheet = SN.activeSheet;
// 获取行列对象
sheet.getRow(0).height = 32;
sheet.getCol(1).width = 120;
// 插入和删除行列
sheet.addRows(1, 2);
sheet.delRows(1, 2);
sheet.addCols(1, 2);
sheet.delCols(1, 2);
// 自动适配行高列宽
sheet.autoFitRows(0, 10);
sheet.autoFitCols(0, 5);
// 合并与取消合并
sheet.mergeCells('A1:C1', 'center');
sheet.unMergeCells('A1:C1');
// 排序
sheet.rangeSort(
[{ col: 'B', order: 'desc' }],
'A1:D100',
{ hasHeader: true }
);
// 获取当前工作表已使用区域(类似 Excel UsedRange)
const usedRange = sheet.getUsedRange();
// 返回 A1 风格字符串,例如 "A1:D100"
const usedRangeRef = sheet.getUsedRange({ asString: true });
// 配合 clearRange 清空当前数据区域
sheet.clearRange(usedRange, 'contents');
// 清空区域,默认清除内容和样式
sheet.clearRange('A1:D100');
// 也支持索引对象范围,行列索引从 0 开始
sheet.clearRange({ s: { r: 0, c: 0 }, e: { r: 99, c: 3 } });
// 只清内容、只清样式、只清边框
sheet.clearRange('A1:D100', 'contents');
sheet.clearRange('A1:D100', 'formats');
sheet.clearRange('A1:D100', 'borders');
// 清除更多对象时使用标准配置
sheet.clearRange('A1:D100', {
contents: true,
formats: true,
comments: true,
dataValidations: true,
hyperlinks: true,
controls: true
});
// 合并单元格不会默认拆除,需要显式指定
sheet.clearRange('A1:D100', { merges: true });
#8. 插入模板
insertTemplate 适合业务系统快速生成固定格式表单。
const sheet = SN.activeSheet;
const template = [
[{ v: '会议纪要', s: 16, b: true, mr: 3, h: 42, fg: '#f0f5ff' }, '', '', ''],
['时间', '', '地点', ''],
['主持人', '', '记录人', ''],
['议题', { mr: 2 }, '', ''],
[{ v: '内容', h: 180 }, { mr: 2 }, '', ''],
[{ v: '备注', h: 80 }, { mr: 2 }, '', '']
];
sheet.insertTemplate(template, 'A1', {
border: true,
align: 'center',
width: 120,
height: 30
});
模板单元格常用字段:
| 字段 | 说明 |
|---|---|
v | 单元格值 |
w | 列宽,通常放在第一行 |
h | 行高,通常放在第一列 |
b | 加粗 |
s | 字号 |
fg | 背景色 |
c | 字体颜色 |
a | 水平对齐,left、center、right |
mr | 向右合并的单元格数量 |
mb | 向下合并的单元格数量 |
使用 mr、mb 合并时,二维数组仍需保持矩形结构,被合并占位的位置可填空字符串。
#9. 导入导出
第 9.1~9.3 节的文件 API 需要商业发行包和相应授权。第 9.4 节的 JSON 对象保存恢复在开源版也可用。导入前处理未保存数据,并暂停自动保存;初始加载完成后再注册保存监听。
#9.1 导入本地文件
<input id="file-input" type="file" accept=".xlsx,.csv,.json" />
document.querySelector('#file-input').addEventListener('change', async (event) => {
const file = event.target.files && event.target.files[0];
if (!file) return;
try {
await SN.IO.import(file);
} catch (error) {
console.error('导入未完成,请保留原始文件并检查当前工作簿', error);
}
});
当前商业版 import / importFromUrl 返回 Promise<void>,部分错误仅通过编辑器提示显示并在内部捕获;await 完成不能当作可靠成功标志,外层 catch 也无法捕获所有失败。导入期间保持自动保存暂停,用户核对结果后再确认保存;需要无人值守导入时,先在编辑器补齐统一成功/失败契约。用户确认导入结果后调用第 17 节的业务 markDirty();初始加载已有数据无需标记为新修改。
#9.2 从 URL 导入
await SN.IO.importFromUrl('https://example.com/demo.xlsx');
目标文件服务器需要允许浏览器跨域访问,否则会被 CORS 拦截。
#9.3 导出文件
await SN.IO.export('XLSX');
await SN.IO.export('CSV');
await SN.IO.export('JSON');
await SN.IO.export('HTML');
#9.4 导出和恢复 JSON 数据
保存时获取完整工作簿对象,将其交给第 17 节的业务保存接口:
const data = await SN.IO.getData();
下面是业务系统初始加载示例,接口返回 { data, revision },其中 revision 是服务端生成的非空版本字符串:
const response = await fetch('/api/workbooks/1');
if (!response.ok) throw new Error(`读取失败:HTTP ${response.status}`);
const payload = await response.json();
const savedData = payload.data;
const loadedRevision = payload.revision;
if (typeof loadedRevision !== 'string' || !loadedRevision) {
throw new Error('业务接口缺少工作簿版本');
}
if (savedData?.version !== '2.0' || !Array.isArray(savedData.sheets) || !savedData.sheets.length) {
throw new Error('工作簿数据格式不正确');
}
const recoverySnapshot = await SN.IO.getData();
const applicationReadOnly = SN.readOnly;
try {
if (!SN.IO.setData(savedData)) {
const recovered = SN.IO.setData(recoverySnapshot);
throw new Error(recovered
? '加载失败,已恢复原工作簿内容;撤销历史无法恢复'
: '加载及恢复均失败,请保留恢复快照并停止自动保存');
}
} finally {
SN.readOnly = applicationReadOnly;
}
setData 替换整个工作簿,成功时清空撤销历史,也会载入 JSON 中的只读和工作表保护状态。返回 false 可能已经改变部分内容,并不保证旧工作簿完好。恢复快照也是一次可能失败的加载,建议在编辑器外保留快照。只有加载或恢复成功、重新应用业务权限后才恢复保存;恢复旧内容后不能把新文件的 revision 用于保存旧内容。
上面的结构检查不是完整 JSON 校验。对后端生成或外部上传的数据,应先限制大小、行列规模和对象数量,并检查引用结构;权限仍需在服务端执行。JSON 格式版本 2.0 和 npm 包版本是两种不同的版本号。
#10. JSON 数据格式摘要
SN.IO.getData() 导出的 JSON 可由 SN.IO.setData(data) 恢复。
当前 JSON 版本为:
{
"version": "2.0"
}
setData 要求:
data.version === "2.0"data.sheets是非空数组
最小结构示例:
{
"version": "2.0",
"workbookName": "Demo",
"activeSheet": "Sheet1",
"calcMode": "auto",
"readOnly": false,
"properties": {},
"definedNames": {},
"styleTable": {
"fonts": [],
"fills": [],
"borders": [],
"aligns": [],
"styles": []
},
"sizeTable": {
"rowHeights": [],
"colWidths": []
},
"sheets": [
{
"name": "Sheet1",
"hidden": false,
"showGridLines": true,
"showRowColHeaders": true,
"showPageBreaks": false,
"outlinePr": {},
"frozenCols": 0,
"frozenRows": 0,
"freezeStartRow": 0,
"freezeStartCol": 0,
"defaultColWidth": 72,
"defaultRowHeight": 21,
"zoom": 1,
"activeCell": { "r": 0, "c": 0 },
"viewStart": { "r": 0, "c": 0 },
"printSettings": null,
"rowCount": 10,
"colCount": 10,
"cols": [],
"rows": [
{
"rIndex": 0,
"cells": [
{ "c": 0, "v": "姓名" },
{ "c": 1, "v": "分数" }
]
},
{
"rIndex": 1,
"cells": [
{ "c": 0, "v": "张三" },
{ "c": 1, "v": 95 }
]
}
],
"merges": [],
"drawings": []
}
]
}
建议:
- 业务系统如需长期保存工作簿,优先保存
SN.IO.getData()的完整结果。 - 如果后端需要生成 JSON,建议先用 SheetNext 做出一个样例并导出 JSON,再以该 JSON 作为模板修改业务数据。
- 不建议手写复杂样式、透视表、切片器、绘图等高级结构,除非已经用导出的 JSON 验证过。
#11. 事件监听
SheetNext 通过 SN.Event 暴露事件能力。
SN.Event.on('afterSelectionChange', (event) => {
console.log('当前单元格:', event.data.newCell);
});
SN.Event.on('afterCellEdit', (event) => {
const { row, col, oldValue, newValue } = event.data;
console.log('单元格变更:', row, col, oldValue, newValue);
});
SN.Event.once('afterSheetAdd', (event) => {
console.log('新增工作表:', event.data.sheet);
});
常用方法:
| 方法 | 说明 |
|---|---|
SN.Event.on(event, handler, options) | 注册事件 |
SN.Event.once(event, handler, options) | 注册一次性事件 |
SN.Event.off(event, idOrHandler) | 移除事件 |
SN.Event.offAll() | 移除全部事件 |
SN.Event.hasListeners(event) | 判断事件是否存在监听 |
SN.Event.listenerCount(event) | 获取监听数量 |
常用事件:
| 事件 | 说明 |
|---|---|
afterCellEdit | 单元格值变更后 |
afterCellStyleChange | 单元格样式变更后 |
afterSelectionChange | 选区变化后 |
afterSheetAdd | 新增工作表后 |
afterSheetDelete | 删除工作表后 |
afterSheetRename | 工作表重命名后 |
afterActiveSheetChange | 当前工作表变化后 |
afterInsertRows | 插入行后 |
afterDeleteRows | 删除行后 |
afterInsertColumns | 插入列后 |
afterDeleteColumns | 删除列后 |
afterSort | 排序后 |
afterMerge | 合并单元格后 |
afterUnmerge | 取消合并后 |
afterUndo | 撤销后 |
afterRedo | 重做后 |
业务系统常见用法:监听编辑、导入、行列变更等事件后,触发保存按钮状态、自动保存、审计日志或外部表单联动。
#12. 数据验证、筛选和表格
#12.1 数据验证
const sheet = SN.activeSheet;
sheet.setDataValidation('B2:B100', {
type: 'list',
formula1: '进行中,已完成,已取消',
allowBlank: true,
showErrorMessage: true,
errorTitle: '输入错误',
error: '请选择下拉列表中的状态'
});
sheet.clearDataValidation('B2:B100');
#12.2 自动筛选
const sheet = SN.activeSheet;
sheet.AutoFilter.setRange('A1:D100');
sheet.AutoFilter.setColumnFilter(1, {
type: 'custom',
filters: [{ operator: 'contains', value: '华东' }],
and: true
});
sheet.AutoFilter.clearAllFilters();
#12.3 表格
const sheet = SN.activeSheet;
const table = sheet.Table.add({
rangeRef: 'A1:D20',
name: 'SalesTable',
showHeaderRow: true
});
sheet.Table.remove(table.id);
#13. 批注、图片、图表、透视表和透视图、迷你图和条件格式
#13.1 批注
const sheet = SN.activeSheet;
sheet.Comment.add({
cellRef: 'A1',
text: '这里填写客户名称',
author: '管理员'
});
const comment = sheet.Comment.get('A1');
sheet.Comment.remove('A1');
#13.2 图片
sheet.Drawing.addImage(imageBase64, {
startCell: { r: 1, c: 1 },
width: 240,
height: 120
});
#13.3 图表
图表通过 sheet.Drawing.addChart(chartOption, options) 插入,chartOption 使用 ECharts 风格配置;数据可以直接写数组,也可以引用工作表区域。
折线图示例:
const sheet = SN.activeSheet;
sheet.getCell('A1').editVal = '月份';
sheet.getCell('B1').editVal = '销售额';
sheet.getCell('A2').editVal = '1月';
sheet.getCell('A3').editVal = '2月';
sheet.getCell('A4').editVal = '3月';
sheet.getCell('B2').editVal = 120;
sheet.getCell('B3').editVal = 200;
sheet.getCell('B4').editVal = 150;
sheet.Drawing.addChart({
title: { text: '销售趋势' },
tooltip: {},
legend: { data: ['销售额'] },
xAxis: {
type: 'category',
data: `${sheet.name}!A2:A4`
},
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'line',
data: `${sheet.name}!B2:B4`
}]
}, {
startCell: { r: 1, c: 3 },
width: 480,
height: 300
});
柱状图只需要把 series.type 改成 bar:
sheet.Drawing.addChart({
title: { text: '销售额对比' },
xAxis: {
type: 'category',
data: ['1月', '2月', '3月']
},
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'bar',
data: [120, 200, 150]
}]
}, {
startCell: 'F2',
width: 480,
height: 300
});
图表查询、修改和删除示例:
const chart = sheet.Drawing.addChart({
title: { text: '销售趋势' },
xAxis: {
type: 'category',
data: ['1月', '2月', '3月']
},
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'line',
data: [120, 200, 150]
}]
}, {
startCell: 'F2',
width: 480,
height: 300
});
// 查询单个图表
const sameChart = sheet.Drawing.get(chart.id);
// 查询当前工作表全部图表
const charts = sheet.Drawing.getAll().filter(item => item.type === 'chart');
// 查询覆盖某个单元格的图表
const chartsAtF2 = sheet.Drawing.getByCell('F2').filter(item => item.type === 'chart');
if (sameChart) {
// 修改位置和尺寸;startCell 对象中的 r/c 从 0 开始
sameChart.startCell = { r: 1, c: 6 };
sameChart.width = 560;
sameChart.height = 320;
// 修改图表配置
sameChart.chartOption = {
...sameChart.chartOption,
title: { text: '新的销售趋势' },
series: sameChart.chartOption.series.map(item => ({
...item,
type: 'bar'
}))
};
}
// 删除图表
sheet.Drawing.remove(chart.id);
#13.4 透视表和透视图
透视图依赖透视表数据。典型流程是先用 sheet.PivotTable.add(config) 创建透视表,配置行字段、列字段和值字段并刷新,然后调用透视表实例的 createChart(options) 插入绑定的透视图。
const sheet = SN.activeSheet;
sheet.getCell('A1').editVal = '地区';
sheet.getCell('B1').editVal = '月份';
sheet.getCell('C1').editVal = '销售额';
sheet.getCell('D1').editVal = '数量';
sheet.getCell('A2').editVal = '华东';
sheet.getCell('B2').editVal = '1月';
sheet.getCell('C2').editVal = 12000;
sheet.getCell('D2').editVal = 8;
sheet.getCell('A3').editVal = '华东';
sheet.getCell('B3').editVal = '2月';
sheet.getCell('C3').editVal = 18000;
sheet.getCell('D3').editVal = 12;
sheet.getCell('A4').editVal = '华南';
sheet.getCell('B4').editVal = '1月';
sheet.getCell('C4').editVal = 15000;
sheet.getCell('D4').editVal = 10;
sheet.getCell('A5').editVal = '华南';
sheet.getCell('B5').editVal = '2月';
sheet.getCell('C5').editVal = 21000;
sheet.getCell('D5').editVal = 15;
const pt = sheet.PivotTable.add({
sourceSheet: sheet,
sourceRangeRef: 'A1:D5',
cellRef: 'F1',
name: 'SalesPivot'
});
// 字段索引按 sourceRangeRef 的列顺序从 0 开始:0=地区,1=月份,2=销售额。
pt.addRowField(0);
pt.addColField(1);
pt.addDataField(2, 'sum', '销售额合计');
pt.setFieldSort(0, 'asc');
pt.refresh();
const chart = pt.createChart({
type: 'bar_clustered',
includeTotals: false,
drawing: {
startCell: 'F10',
width: 560,
height: 320
}
});
if (!chart) {
console.warn('透视图数据区域不足,无法创建图表');
}
注意事项:
sourceRangeRef的首行会作为字段名,后续addRowField、addColField、addDataField使用源区域内从 0 开始的列索引。includeTotals控制透视图是否包含透视表的行/列总计;通常业务图表建议设为false。type使用图表类型 key,例如bar_clustered、line_basic、pie_basic。- 透视图本质上也是图表绘图对象,保存和恢复仍使用
SN.IO.getData()/SN.IO.setData(data)。
#13.5 迷你图
sheet.Sparkline.add({
cellRef: 'D2',
formula: 'A2:C2',
type: 'column',
colors: {
series: '#376092',
negative: '#d00000'
}
});
#13.6 条件格式
sheet.CF.add({
rangeRef: 'B2:B100',
type: 'cellIs',
operator: 'greaterThan',
formula1: 10000,
dxf: {
font: { color: '#ff0000' },
fill: { fgColor: '#ffecec' }
}
});
#14. 保护工作表
工作表保护、readOnly 和前置事件属于客户端编辑控制。服务端保存接口仍必须校验用户、工作簿归属及写权限;不能依靠客户端开关保护业务数据。
const sheet = SN.activeSheet;
sheet.protection.enable({
password: '123456',
selectLockedCells: true,
selectUnlockedCells: true,
formatCells: false,
insertRows: false,
deleteRows: false,
sort: true,
autoFilter: true
});
const result = sheet.protection.disable('123456');
if (!result.ok) {
console.error(result.message);
}
设置单元格锁定状态:
sheet.protection.setCellProtection('A1:C10', {
locked: true,
hidden: false
});
#15. 撤销和重做
if (SN.UndoRedo.canUndo) {
SN.UndoRedo.undo();
}
if (SN.UndoRedo.canRedo) {
SN.UndoRedo.redo();
}
自定义业务操作如需纳入撤销重做,可使用事务:
SN.UndoRedo.begin('custom update');
// 执行业务修改,并按需 SN.UndoRedo.add(...)
SN.UndoRedo.commit();
#16. 常用工具方法
// A1 -> { r: 0, c: 0 }
const cellNum = SN.Utils.cellStrToNum('A1');
// { r: 0, c: 0 } -> A1
const cellStr = SN.Utils.cellNumToStr({ r: 0, c: 0 });
// 区域对象 -> A1:C3
const rangeStr = SN.Utils.rangeNumToStr({
s: { r: 0, c: 0 },
e: { r: 2, c: 2 }
});
// 列号转换
SN.Utils.numToChar(0); // A
SN.Utils.charToNum('A'); // 0
#17. 保存策略建议
手动保存与自动保存应共用同一个串行保存队列。下面是业务系统代码示例,SheetNext 本身不提供这些 /api/workbooks 接口。
#17.1 业务接口约定
GET /api/workbooks/1返回{ data, revision },成功加载后才能注册保存监听。PUT /api/workbooks/1接收{ data, baseRevision }。服务端校验用户和工作簿写权限,并在同一原子操作中比较旧版本、写入数据及递增版本。- 保存成功返回 HTTP 2xx 和
{ revision: '新的非空版本字符串' };版本冲突返回 HTTP 409,不写入任何数据;认证、权限或数据校验失败返回相应非 2xx 状态。 - 多人或多标签页编辑冲突时保留本地未保存内容,提示用户重新加载或合并。不要直接取得最新版本号后重试旧数据。
#17.2 串行保存与错误状态
页面先提供保存状态显示区域:
<p data-save-status role="status" aria-live="polite">已加载</p>
以下代码在第 9.4 节加载成功后执行。它将编辑期间产生的新修改留在队列中,并在失败时保留未保存状态、停止自动重试。多实例时,每个工作簿维护独立队列、接口 URL 和状态元素引用。
const saveStatus = document.querySelector('[data-save-status]');
let serverRevision = loadedRevision;
let dirtySequence = 0;
let savedSequence = 0;
let saveTimer = null;
let inFlightSave = null;
let savingPaused = false;
let saveError = null;
function markDirty() {
dirtySequence += 1;
clearTimeout(saveTimer);
if (savingPaused) return;
saveStatus.textContent = '有未保存修改';
saveTimer = setTimeout(() => {
flushSave().catch(() => {});
}, 1000);
}
function flushSave() {
clearTimeout(saveTimer);
if (inFlightSave) return inFlightSave;
if (savingPaused) return Promise.reject(saveError || new Error('保存已暂停'));
inFlightSave = (async () => {
while (!savingPaused && savedSequence < dirtySequence) {
const snapshotSequence = dirtySequence;
saveStatus.textContent = '正在保存';
const data = await SN.IO.getData();
if (savingPaused) break;
const response = await fetch('/api/workbooks/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data, baseRevision: serverRevision }),
signal: AbortSignal.timeout(15000)
});
if (response.status === 409) throw new Error('版本冲突,请保留本地修改并处理冲突');
if (!response.ok) throw new Error(`保存失败:HTTP ${response.status}`);
const result = await response.json();
if (typeof result.revision !== 'string' || !result.revision) {
throw new Error('保存响应缺少新版本,需确认服务器状态');
}
serverRevision = result.revision;
savedSequence = snapshotSequence;
}
if (!savingPaused) saveStatus.textContent = '已保存';
})().catch(error => {
saveError = error;
savingPaused = true;
saveStatus.textContent = `${error.message};修改尚未确认保存`;
throw error;
}).finally(() => {
inFlightSave = null;
});
return inFlightSave;
}
const saveEvents = [
'afterOperation', 'afterUndo', 'afterRedo',
'afterSheetAdd', 'afterSheetDelete', 'afterSheetRename', 'afterSheetMove',
'afterWorkbookRename', 'afterDrawingAdd', 'afterDrawingRemove',
'afterTableAdd', 'afterTableRemove', 'afterTableRename', 'afterTableResize',
'afterTableStyleChange', 'afterTableTotalsRowToggle', 'afterTableAutoFilterToggle',
'afterPivotTableAdd', 'afterPivotTableChange',
'afterSparklineAdd', 'afterSparklineRemove', 'afterSlicerAdd', 'afterSlicerRemove'
];
saveEvents.forEach(eventName => SN.Event.on(eventName, markDirty));
afterOperation 覆盖操作层中的编辑、样式、行列等变更,但不是覆盖一切的工作簿变更事件。批注、保护状态、直接修改绘图配置等没有完整事件覆盖的业务操作,需要在成功后显式调用 markDirty();用户文件导入成功也要调用。不要依赖任意 after* 事件或选区事件来判断数据已修改。
保存按钮调用 markDirty() 后 await flushSave(),可以一并保存未被事件覆盖的修改。业务按钮需要捕获异常,失败时保留当前页面和数据。网络超时可能发生在服务器已经写入之后,应先核对服务端版本与内容,再决定重试或解决冲突,不能把超时当作确定未写入。
上例使用现代浏览器的 AbortSignal.timeout;旧浏览器可用 AbortController 和定时器实现同等超时取消。应用的会话认证、CSRF 机制和请求大小上限需要按实际后端补齐。
#17.3 暂停、恢复与离开页面
- 正常导航前
await flushSave(),确认成功后再离开;beforeunload阶段不能可靠完成异步保存,应提示未保存状态,必要时提供本地恢复草稿。 - 切换工作簿或替换数据前,先处理未保存内容,设置
savingPaused = true、清除saveTimer,再等待已有inFlightSave结束。加载期间不要自动保存中间状态。 - 加载成功后重新应用业务权限,使用所加载数据对应的服务端版本重新初始化保存状态;加载或回滚失败时继续暂停保存,并保留恢复快照。
- 保存错误解决后才允许主动重试:保留
dirtySequence,清除saveError,恢复savingPaused = false并调用flushSave()。版本冲突必须先完成合并或重新加载流程。 - 卸载业务绑定时,清除定时器,并执行
saveEvents.forEach(eventName => SN.Event.off(eventName, markDirty))。这只解绑业务保存监听,编辑器实例的生命周期限制见第 1.2 节。
#18. 集成检查清单
上线前建议确认:
- 页面已正确引入对应版本的
sheetnext/dist/sheetnext.css和 JS 文件。 - 容器 DOM 有明确宽高。
- 中文环境已引入中文语言包,并设置
locale: 'zh-CN'。 - 发行包、文档版本和功能范围已匹配;商业功能按交付要求配置
licenseKey。 - 导入文件大小、格式和跨域策略符合业务要求。
- 保存时使用
SN.IO.getData(),恢复时使用SN.IO.setData(data)。 - 自动保存已做防抖、串行请求、失败状态与服务端原子版本校验,覆盖撤销重做及业务主动标记的变更。
- 数据替换前暂停保存并保留恢复快照,成功后恢复业务权限;失败时不自动保存部分加载的数据。
- 多实例页面没有使用固定全局 DOM ID 做业务绑定。
- 不需要 AI 能力时,不配置
AI_URL、AI_TOKEN,也不暴露 AI 入口。
#19. 内部参考文档
如需核对完整 API,先打开索引,再按模块读取:
详细 API 按模块生成到 references/api/,入口记录源码版本和内容指纹。链接默认指向 master,使用历史发行包时请切换对应发布版本。文档维护应修改编辑器源码注释、scripts/doc-examples.js、扫描器或 docs/static-references/ 后运行 npm run docs;npm run docs:check 检查生成结果、示例导入路径和关键契约,不构建编辑器,也不代替浏览器运行验证。