#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 来自源码注释和文档扫描器。使用旧版本时,请查看对应发布版本的文档。

#目录

#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.css
  • sheetnext.umd.js
  • sheetnext.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 是可选配置。

常用参数:

参数类型说明
licenseKeystring授权码
localestring初始语言,例如 zh-CNen-US
localesObject额外语言包
menuRightfunction自定义右侧菜单 HTML
menuListfunction自定义顶部菜单配置
minimalToolbarboolean初始使用精简工具栏
themeObjectstring主题颜色配置,字符串表示主色

AI 相关参数 AI_URLAI_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 写入值和公式,不要使用 .valueeditVal 的读取结果是编辑文本;需要数字、布尔或公式计算结果时读取 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 写入,必须以 = 开头。读取结果时用 calcValshowVal

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水平对齐,leftcenterright
mr向右合并的单元格数量
mb向下合并的单元格数量

使用 mrmb 合并时,二维数组仍需保持矩形结构,被合并占位的位置可填空字符串。

#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 的首行会作为字段名,后续 addRowFieldaddColFieldaddDataField 使用源区域内从 0 开始的列索引。
  • includeTotals 控制透视图是否包含透视表的行/列总计;通常业务图表建议设为 false
  • type 使用图表类型 key,例如 bar_clusteredline_basicpie_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_URLAI_TOKEN,也不暴露 AI 入口。

#19. 内部参考文档

如需核对完整 API,先打开索引,再按模块读取:

详细 API 按模块生成到 references/api/,入口记录源码版本和内容指纹。链接默认指向 master,使用历史发行包时请切换对应发布版本。文档维护应修改编辑器源码注释、scripts/doc-examples.js、扫描器或 docs/static-references/ 后运行 npm run docsnpm run docs:check 检查生成结果、示例导入路径和关键契约,不构建编辑器,也不代替浏览器运行验证。