@evansun/excel-designer
@evansun/excel-designer
基于 Canvas + Vue 3 的在线 Excel 表格组件库。支持单元格合并、行高列宽拖拽、公式引擎、撤销重做、序列化等完整功能。
特性
- 纯 Canvas 渲染 — 不依赖 DOM 表格,性能稳定,支持大数据量
- 单元格合并 — 合并/取消合并(Ctrl+M),支持跨行跨列合并
- 行高列宽调整 — 表头边线拖拽、双击自适应、右键数值精确设置
- 空白区可用 — 数据区外的空白网格可直接点击选中、合并、输入文字,按需自动扩展行列
- 公式引擎 — 内置公式解析器,支持
SUM、AVERAGE等常用函数与单元格引用 - 撤销/重做 — 完整的历史栈,所有编辑操作可撤销
- 序列化 —
exportJSON/loadJSON完整往返,支持合并、尺寸、样式持久化 - Vue 3 组件 — 提供
SpreadsheetCanvas和Toolbar开箱即用组件
安装
npm install @evansun/excel-designer
# 或
pnpm add @evansun/excel-designer
Peer Dependencies
{ "vue": "^3.3.0" }
快速开始
Vue 3 项目中使用
<template>
<div class="app">
<Toolbar
:cell-style="activeStyle"
:can-undo="canUndo"
:can-redo="canRedo"
:merged="isMerged"
@undo="handleUndo"
@redo="handleRedo"
@apply-style="handleApplyStyle"
@toggle-merge="handleToggleMerge"
@insert-row="handleInsertRow"
@delete-row="handleDeleteRow"
@insert-col="handleInsertCol"
@delete-col="handleDeleteCol"
/>
<SpreadsheetCanvas
ref="canvasRef"
:sheet-data="initialData"
:show-toolbar="true"
:show-save="true"
:extra-data="{ zones: zoneConfig }"
@ready="onReady"
@selection-changed="onSelectionChanged"
@cell-change="onCellChange"
@history-change="onHistoryChange"
@save="handleSave"
@export="handleExport"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { SpreadsheetCanvas, Toolbar } from '@evansun/excel-designer'
import '@evansun/excel-designer/style.css'
const canvasRef = ref()
const initialData = ref(null) // 从后端加载的表格数据
const zoneConfig = ref({}) // 额外配置(带区等)
let engine = null
function onReady(api) {
engine = api
}
function handleSave(payload) {
// payload = { version, savedAt, workbook, ...extraData }
console.log('保存数据', payload)
// 发送到后端
}
function handleExport(data) {
console.log('导出 JSON', data)
}
</script>
仅使用核心引擎(不依赖 Vue)
import { Spreadsheet } from '@evansun/excel-designer'
const canvas = document.getElementById('my-canvas')
// 可传入外部数据,不传则使用默认示例数据
const engine = new Spreadsheet(canvas, {
sheetData: serverData // 可选,传入则作为初始数据加载
})
engine.on('ready', () => {
console.log('表格就绪')
})
engine.on('selection-changed', (data) => {
console.log('选区变化', data)
})
engine.mount()
// 导出数据
const json = engine.exportJSON()
// 加载数据
engine.loadJSON(newData)
核心模型导出
除组件外,库还导出以下核心类和工具函数,可用于二次开发:
import {
Sheet, Row, Cell, // 数据模型
DEFAULT_STYLE, // 默认样式常量
MIN_ROW_HEIGHT, // 最小行高(18)
MIN_COL_WIDTH, // 最小列宽(36)
colIndexToLetter, // 列索引→字母(0→A, 26→AA)
colLetterToIndex, // 列字母→索引(A→0)
LayoutCache, // 布局缓存(prefixSum 前缀和)
History, // 撤销/重做历史栈
Selection, // 选区管理
Editor, // 内联编辑器
FormulaEngine, // 公式引擎
Parser, parseFormula, tokenize, // 公式解析
DependencyGraph, // 公式依赖图
builtinFunctions, // 内置函数(SUM/AVERAGE 等)
FormulaError // 公式错误类型
} from '@evansun/excel-designer'
API 文档
SpreadsheetCanvas 组件
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sheetData |
Object |
null |
外部传入的表格数据(rows/cols/merges),传入则作为初始数据加载 |
showToolbar |
Boolean |
false |
是否显示组件内置顶部操作栏 |
showSave |
Boolean |
true |
是否显示保存按钮(需 showToolbar=true) |
showExport |
Boolean |
false |
是否显示导出按钮 |
showTreeLines |
Boolean |
false |
是否显示子行左侧层级竖线(父子级线),默认隐藏,仅保留子行浅蓝底色 |
tableProps |
Object |
null |
业务传入的表格属性 { rowType, dataSourceType, precision, field },缺省用系统默认 |
bizVars |
Array |
null |
业务传入的业务变量 [{ name, explain, dataOriginName }],缺省用系统默认 |
extraData |
Object |
null |
保存时附加的额外数据(如 zones 配置),原样合并到 save 事件输出 |
Emits
| 事件名 | 参数 | 说明 |
|---|---|---|
ready |
(api: Spreadsheet) |
表格初始化完成,返回引擎实例 |
selection-changed |
(data) |
选区变化时触发 |
cell-change |
(data) |
单元格内容变化时触发 |
row-toggle |
(data) |
行折叠/展开时触发 |
history-change |
(data) |
撤销/重做栈变化时触发 |
save |
(payload) |
点击保存按钮时触发,payload 含 { version, savedAt, workbook, tableProps, bizVars, ...extraData } |
export |
(data) |
点击导出按钮时触发,data 为表格 JSON |
暴露方法(defineExpose)
| 方法 | 说明 |
|---|---|
getInstance() |
获取底层 Spreadsheet 引擎实例 |
setCell(rowId, colId, value) |
设置单元格值 |
insertFormulaAt(rowId, colId, expr) |
插入公式 |
setActiveCell(rowId, colId) |
设置活动单元格 |
scrollToRow(rowId) |
滚动到指定行 |
exportJSON() |
导出序列化数据 |
loadJSON(json) |
加载序列化数据 |
getTableProps() |
获取解析后的表格属性(外部传参优先,否则系统默认) |
getBizVars() |
获取解析后的业务变量列表(外部传参优先,否则系统默认) |
save() |
手动触发保存事件(等价于点击保存按钮) |
Toolbar 组件
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
cellStyle |
Object |
见下 | 当前活动单元格样式 |
canUndo |
Boolean |
false |
是否可撤销 |
canRedo |
Boolean |
false |
是否可重做 |
merged |
Boolean |
false |
当前选区是否已合并 |
cellStyle 默认值:
{
fontFamily: 'Arial',
fontSize: 11,
bold: false,
italic: false,
underline: false,
strikethrough: false,
align: 'left', // 'left' | 'center' | 'right'
valign: 'middle', // 'top' | 'middle' | 'bottom'
fg: '#333333',
bg: '#ffffff'
}
Emits
| 事件名 | 参数 | 说明 |
|---|---|---|
undo |
() |
撤销 |
redo |
() |
重做 |
apply-style |
(patch: Object) |
应用样式变更 |
apply-border |
({ command, edge }) |
应用边框,command: 't'/'b'/'l'/'r'/'all'/'outer'/'inner'/'none',edge: { color, style, disabled? }。disabled: true 只禁用 command 指定的边,none 禁用全部四边 |
toggle-merge |
() |
合并/取消合并 |
insert-row |
() |
插入行 |
delete-row |
() |
删除行 |
insert-col |
() |
插入列 |
delete-col |
() |
删除列 |
Spreadsheet 引擎 API
| 方法 | 参数 | 说明 |
|---|---|---|
mount() |
— | 初始化默认 Sheet 并启动渲染 |
destroy() |
— | 销毁引擎,移除所有事件监听 |
on(event, cb) |
(string, Function) |
注册事件监听 |
render() |
— | 手动触发重绘(通常自动调用) |
setCell(rowId, colId, value) |
(string, string, string|number) |
设置单元格值 |
insertFormulaAt(rowId, colId, expr) |
(string, string, string) |
插入公式(如 =SUM(A1:A3)) |
setActiveCell(rowId, colId) |
(string, string) |
设置活动单元格 |
getActiveStyle() |
— | 返回活动单元格样式对象 |
applyStyle(patch) |
(Object) |
批量应用样式(如 { bold: true, fg: '#f00' }) |
applyBorderToSelection(command, edge) |
(string, Object) |
对选区应用边框(合并区域感知);单边命令同时写入对应边和宽度字段,其他三边归零;edge.disabled: true 只将对应宽度设为 0,none 将四边全部设为 0 |
mergeSelection() |
— | 合并当前选区 |
unmergeSelection() |
— | 取消当前选区合并 |
toggleMerge() |
— | 切换合并/取消合并 |
isActiveMerged() |
— | 返回活动单元格是否处于合并区域 |
setRowHeight(rowId, height) |
(string, number) |
设置行高(最小 18px) |
setColWidth(colId, width) |
(string, number) |
设置列宽(最小 36px) |
autoFitCol(colId) |
(string) |
按内容自动适配列宽 |
insertRow(parentId, afterRowId, height) |
(string?, string?, number?) |
插入行(支持父子级) |
insertCol(refColId, position, width) |
(string, 'before'|'after', number?) |
插入列(左侧/右侧) |
removeCol(colId) |
(string) |
删除列(自动清理单元格和合并) |
setRowParent(rowId, parentId) |
(string, string?) |
设置行父子关系 |
setRowZoneId(rowId, zoneId) |
(string, string) |
绑定行到分区 |
getRowIdByIndex(rowIndex) |
(number) |
按索引获取行 ID |
getRowsByZoneId(zoneId) |
(string) |
按分区获取行列表 |
getSelectionCellPositions() |
— | 返回当前选区内所有单元格坐标 |
exportJSON() |
— | 导出完整序列化数据 |
loadJSON(json) |
(Object) |
加载序列化数据 |
undo() |
— | 撤销 |
redo() |
— | 重做 |
序列化数据格式
exportJSON() 返回 / loadJSON(json) 接收的完整数据结构:
{
"name": "Sheet1",
"rowOrder": ["r1", "r2", "r3"],
"colOrder": ["c1", "c2", "c3"],
"rows": [
{
"id": "r1",
"parentId": null,
"height": 24,
"collapsed": false,
"zoneId": null,
"cells": [
{
"colId": "c1",
"value": "单项工程费汇总表",
"rawInput": "单项工程费汇总表",
"formula": null,
"style": {
"fontFamily": "Arial",
"fontSize": 14,
"bold": true,
"italic": false,
"underline": false,
"strikethrough": false,
"align": "center",
"valign": "middle",
"fg": "#333333",
"bg": "#ffffff"
}
}
]
},
{
"id": "r2",
"parentId": "r1",
"height": 24,
"collapsed": false,
"zoneId": null,
"cells": []
}
],
"cols": [
{ "id": "c1", "width": 100 },
{ "id": "c2", "width": 100 },
{ "id": "c3", "width": 100 }
],
"merges": [
{ "startRowId": "r1", "startColId": "c1", "endRowId": "r1", "endColId": "c3" }
]
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string |
工作表名称 |
rowOrder |
string[] |
行 ID 顺序数组(决定显示顺序) |
colOrder |
string[] |
列 ID 顺序数组 |
rows[].id |
string |
行唯一 ID |
rows[].parentId |
string|null |
父行 ID(null=根行,用于树形层级) |
rows[].height |
number |
行高(px),最小 18 |
rows[].collapsed |
boolean |
是否折叠子行 |
rows[].zoneId |
string|null |
分区 ID(用于分组管理) |
rows[].cells |
Array |
该行的单元格列表 |
cells[].colId |
string |
列 ID |
cells[].value |
string|number |
显示值 |
cells[].rawInput |
string |
原始输入(用户输入的文本) |
cells[].formula |
string|null |
公式表达式(如 =SUM(A1:A3)) |
cells[].style |
Object |
样式对象(见下表) |
cols[].id |
string |
列唯一 ID |
cols[].width |
number |
列宽(px),最小 36 |
merges[] |
Object |
合并区域,左上角为主格 |
样式字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fontFamily |
string |
'Arial' |
字体 |
fontSize |
number |
11 |
字号 |
bold |
boolean |
false |
加粗 |
italic |
boolean |
false |
斜体 |
underline |
boolean |
false |
下划线 |
strikethrough |
boolean |
false |
删除线 |
align |
string |
'left' |
水平对齐:left/center/right |
valign |
string |
'middle' |
垂直对齐:top/middle/bottom |
fg |
string |
'#333333' |
字体颜色 |
bg |
string |
'#ffffff' |
背景颜色 |
键盘快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+Z |
撤销 |
Ctrl+Y / Ctrl+Shift+Z |
重做 |
Ctrl+M |
合并/取消合并单元格 |
Ctrl+B |
加粗 |
Ctrl+I |
斜体 |
Ctrl+U |
下划线 |
Ctrl+C / Ctrl+X / Ctrl+V |
复制/剪切/粘贴 |
Tab / Shift+Tab |
右移/左移活动格 |
Enter |
下移活动格(编辑态则提交) |
Arrow Keys |
方向键移动活动格(跨越合并区域) |
Delete / Backspace |
清除单元格内容 |
| 字符键 | 直接进入编辑态 |
鼠标交互
| 操作 | 功能 |
|---|---|
| 点击单元格 | 选中 |
| 拖拽 | 框选区域 |
| Shift+点击 | 扩展选区 |
| 双击单元格 | 进入编辑态 |
| 双击列标边线 | 列宽自适应内容 |
| 拖拽行号底边 | 调整行高 |
| 拖拽列标右边 | 调整列宽 |
| 右键 | 上下文菜单(合并/取消合并/行高/列宽/插入行/删除行/清除内容) |
| 点击空白区 | 自动扩展行列并选中(Excel 语义) |
开发
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建应用(演示页)
npm run build
# 构建库产物(发布用)
npm run build:lib
构建产物
dist/
excel-designer.es.js # ES Module
excel-designer.umd.js # UMD
style.css # 组件样式
发布
# 1. 确保已构建
npm run build:lib
# 2. 登录 npm
npm login
# 3. 发布(scoped 包需指定公开访问)
npm publish --access public
常见问题
如何从后端加载数据?
// 1. 从后端获取数据
const res = await fetch('/api/sheet/123')
const sheetData = await res.json()
// 2. 传入组件
// 方式一:通过 prop
<SpreadsheetCanvas :sheet-data="sheetData" />
// 方式二:通过引擎实例
const engine = canvasRef.value.getInstance()
engine.loadJSON(sheetData)
如何保存到后端?
<SpreadsheetCanvas
:show-save="true"
:extra-data="{ projectId: 'p001', sheetId: 's001' }"
@save="handleSave"
/>
async function handleSave(payload) {
// payload = { version, savedAt, workbook, projectId, sheetId }
await fetch('/api/sheet/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
}
如何自定义工具栏?
不使用内置 Toolbar,自行构建:
<template>
<div>
<!-- 你的自定义工具栏 -->
<MyToolbar @bold="onBold" @merge="onMerge" />
<!-- 只保留画布,不显示内置操作栏 -->
<SpreadsheetCanvas ref="canvasRef" :show-toolbar="false" @ready="onReady" />
</div>
</template>
<script setup>
const canvasRef = ref()
let engine = null
function onReady(api) { engine = api }
function onBold() {
engine.applyStyle({ bold: !engine.getActiveStyle().bold })
}
function onMerge() {
engine.toggleMerge()
}
</script>
如何动态修改单元格?
const engine = canvasRef.value.getInstance()
// 设置值
engine.setCell('r1', 'c1', 'Hello World')
// 设置公式
engine.insertFormulaAt('r7', 'c3', '=SUM(C4:C6)')
// 设置样式
engine.applyStyle({ bold: true, align: 'center', fg: '#ff0000' })
// 设置行高/列宽
engine.setRowHeight('r1', 40)
engine.setColWidth('c1', 150)
// 插入行/列
engine.insertRow(null, 'r1', 24) // 在 r1 后插入行
engine.insertCol('c1', 'after', 100) // 在 c1 右侧插入列
如何监听单元格变化?
engine.on('cell-change', (data) => {
// data = { rowId, colId, value, oldValue }
console.log(`单元格 ${data.rowId}|${data.colId} 改为 ${data.value}`)
})
License
MIT
Related Packages
vue-canvas-sheet
A high-performance, modular Canvas-based spreadsheet engine with WASM-powered ca...
5
Version History
| Version | Released | License |
|---|---|---|
| 1.0.13 | MIT | |
| 1.0.12 | MIT | |
| 1.0.11 | MIT | |
| 1.0.10 | MIT | |
| 1.0.9 | MIT | |
| 1.0.8 | MIT | |
| 1.0.7 | MIT | |
| 1.0.6 | MIT | |
| 1.0.5 | MIT | |
| 1.0.4 | MIT | |
| 1.0.3 | MIT | |
| 1.0.0 | MIT |