@evansun/excel-designer

基于 Canvas + Vue 3 的在线 Excel 表格组件(单元格合并、行高列宽拖拽、公式引擎、撤销重做)
1,691
Install
npm install @evansun/excel-designer
Latest Version:1.0.13
Vue:3
License:MIT
Last Updated:Oct 22, 2026
Links: npm
Maintainer: evansun

@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

@gridsheet/vue-core

Spreadsheet component for Vue 3

4,566 198
vue-grid-canvas

A vue component, it can generate a similar excel form for you

2,337 21
@sheetwrite/vue

Vue adapter for Sheetwrite.

1,060 3
vue-canvas-sheet

A high-performance, modular Canvas-based spreadsheet engine with WASM-powered ca...

5
@excel-report/vue

ExcelReport 前端预览组件的 Vue 3 适配层

0

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