ooxml-excel-editor 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/dist/chunks/plugin-overlay-Cfnn9EOi.js +7144 -0
- package/dist/chunks/worker-client.stub-BQVZfaLd.js +7 -0
- package/dist/components/ActionToolbar.vue.d.ts +9 -0
- package/dist/components/ExcelViewer.vue.d.ts +283 -0
- package/dist/components/ExportDialog.vue.d.ts +14 -0
- package/dist/components/FilterPopup.vue.d.ts +21 -0
- package/dist/components/FindBar.vue.d.ts +27 -0
- package/dist/components/SheetTabs.vue.d.ts +11 -0
- package/dist/components/ToolbarMenu.vue.d.ts +10 -0
- package/dist/components/ViewerToolbar.vue.d.ts +21 -0
- package/dist/components/export-types.d.ts +14 -0
- package/dist/components/toolbar-icons.d.ts +7 -0
- package/dist/components/toolbar-types.d.ts +18 -0
- package/dist/composables/useExcelDocument.d.ts +17 -0
- package/dist/composables/worker-client.d.ts +3 -0
- package/dist/composables/worker-client.stub.d.ts +3 -0
- package/dist/core/edit/commands.d.ts +94 -0
- package/dist/core/edit/default-editor.d.ts +2 -0
- package/dist/core/edit/edit-controller.d.ts +137 -0
- package/dist/core/edit/editor-context.d.ts +40 -0
- package/dist/core/edit/editor-host.d.ts +22 -0
- package/dist/core/edit/permissions.d.ts +3 -0
- package/dist/core/edit/types.d.ts +23 -0
- package/dist/core/export/composite.d.ts +18 -0
- package/dist/core/export/data-export.d.ts +9 -0
- package/dist/core/export/exporter.d.ts +64 -0
- package/dist/core/export/index.d.ts +9 -0
- package/dist/core/export/paginate.d.ts +36 -0
- package/dist/core/export/pdf.d.ts +28 -0
- package/dist/core/export/print.d.ts +3 -0
- package/dist/core/export/raster.d.ts +13 -0
- package/dist/core/export/types.d.ts +92 -0
- package/dist/core/export/vector-pdf.d.ts +42 -0
- package/dist/core/export/xlsx-writer.d.ts +15 -0
- package/dist/core/finalize.d.ts +10 -0
- package/dist/core/format/builtin-formats.d.ts +7 -0
- package/dist/core/format/color.d.ts +11 -0
- package/dist/core/format/date-serial.d.ts +16 -0
- package/dist/core/format/number-format.d.ts +5 -0
- package/dist/core/formula/engine.d.ts +27 -0
- package/dist/core/formula/hyperformula-adapter.d.ts +3 -0
- package/dist/core/formula/recalc.d.ts +9 -0
- package/dist/core/formula/refs.d.ts +21 -0
- package/dist/core/index.d.ts +52 -0
- package/dist/core/layout/autofit.d.ts +2 -0
- package/dist/core/layout/freeze.d.ts +11 -0
- package/dist/core/layout/grid-metrics.d.ts +37 -0
- package/dist/core/layout/merges.d.ts +14 -0
- package/dist/core/layout/units.d.ts +20 -0
- package/dist/core/layout/viewport.d.ts +29 -0
- package/dist/core/loader.d.ts +5 -0
- package/dist/core/model/clone.d.ts +9 -0
- package/dist/core/model/data-access.d.ts +30 -0
- package/dist/core/model/mutations.d.ts +46 -0
- package/dist/core/model/snapshot.d.ts +14 -0
- package/dist/core/model/structure.d.ts +26 -0
- package/dist/core/model/types.d.ts +289 -0
- package/dist/core/overlay/anchor.d.ts +9 -0
- package/dist/core/overlay/chart-mapper.d.ts +3 -0
- package/dist/core/overlay/echarts-loader.d.ts +3 -0
- package/dist/core/parse.worker.d.ts +14 -0
- package/dist/core/parser/chart-parser.d.ts +3 -0
- package/dist/core/parser/drawing-parser.d.ts +3 -0
- package/dist/core/parser/exceljs-adapter.d.ts +8 -0
- package/dist/core/parser/index.d.ts +3 -0
- package/dist/core/parser/page-break-parser.d.ts +3 -0
- package/dist/core/parser/raw-xml.d.ts +15 -0
- package/dist/core/parser/sparkline-parser.d.ts +3 -0
- package/dist/core/parser/theme.d.ts +3 -0
- package/dist/core/plugin.d.ts +180 -0
- package/dist/core/progress.d.ts +7 -0
- package/dist/core/render/autofilter.d.ts +14 -0
- package/dist/core/render/borders.d.ts +13 -0
- package/dist/core/render/canvas-renderer.d.ts +225 -0
- package/dist/core/render/conditional.d.ts +25 -0
- package/dist/core/render/fills.d.ts +2 -0
- package/dist/core/render/text.d.ts +20 -0
- package/dist/core/render/theme.d.ts +19 -0
- package/dist/core/viewer/controller.d.ts +339 -0
- package/dist/core/viewer/overlay-manager.d.ts +27 -0
- package/dist/core/viewer/plugin-overlay.d.ts +8 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +74 -0
- package/dist/demo-shared/demo-editor.d.ts +2 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1493 -0
- package/dist/react/ExcelViewer.d.ts +153 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/use-excel-document.d.ts +17 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +838 -0
- package/dist/style.css +1 -0
- package/package.json +117 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
|
|
5
|
+
## [1.1.0] - 2026-06-05
|
|
6
|
+
|
|
7
|
+
把 1.0.0 编辑能力的三处已知 v1 限制做成增强(向后兼容)。
|
|
8
|
+
|
|
9
|
+
### 公式引用自动重写(增删行列)
|
|
10
|
+
- 增删行/列后,自动重写全簿公式里指向该表的 A1 引用(`=A5` 插一行→`=A6`;删被引用行→`#REF!`),
|
|
11
|
+
含绝对/相对 `$`、跨表 `Sheet1!A5`/`'My Sheet'!A5`、区域(删除收缩、全删 `#REF!`);跳过字符串字面量
|
|
12
|
+
与函数名。结构命令改为整簿快照(`cloneWorkbook`)→ 跨表重写也可撤销;开 `recalc` 时引擎按新公式重建。
|
|
13
|
+
- 新 `formula/refs.ts`:`shiftFormulaRefs` / `rewriteWorkbookFormulas`。
|
|
14
|
+
|
|
15
|
+
### 图片导出保真
|
|
16
|
+
- 区分锚型:有 `to` 的双格锚 → ExcelJS `br`(随单元格缩放);单格锚 → `tl`(含子格 EMU 偏移转分数列/行)
|
|
17
|
+
+ 像素 `ext`;`editAs` 跟随模型。不再一律导成 oneCell+ext、不再丢子格偏移。
|
|
18
|
+
|
|
19
|
+
### .xlsx 高保真 overlay 导出
|
|
20
|
+
- 新 `exportXlsx({ fidelity: 'overlay' })`:重载原始 .xlsx,只把编辑后的 值/样式/合并/行高列宽/冻结
|
|
21
|
+
叠加上去,**保留** ExcelJS 能往返的其余部分(条件格式/数据验证/打印设置/定义名/图表 等)——默认 `rebuild`
|
|
22
|
+
会丢这些。组件加载时留存原件字节供其使用,缺原件自动回退 `rebuild`。overlay 不反映 增删行列/图片 编辑。
|
|
23
|
+
|
|
24
|
+
### 测试
|
|
25
|
+
- 188 单测 + 40 e2e + build 全绿;core 仍零 vue/react/hyperformula/exceljs 静态 import。
|
|
26
|
+
|
|
27
|
+
## [1.0.0] - 2026-06-05
|
|
28
|
+
|
|
29
|
+
**只读 → 可编辑** —— 在 0.2.0(只读双壳)基础上,把组件升级成**可选编辑器**(默认仍只读、零回归)。
|
|
30
|
+
所有编辑能力建在**一份可变内存模型**上,读 / 写 / 事件 / 导出共用同一层;core 始终框架无关、两壳同构。
|
|
31
|
+
|
|
32
|
+
### 编辑能力(props `editable` 开启)
|
|
33
|
+
- **单元格编辑**:双击 / F2 / 打字进编辑,内置文本编辑器 + `editor` 钩子自定义(下拉/日期/图片选择器,返回任意 DOM,框架无关)。
|
|
34
|
+
- **命令栈**:撤销/重做(Ctrl+Z/Y),所有写操作(值/样式/宽高/图片/结构)统一进栈。
|
|
35
|
+
- **样式编辑**:`setStyle(range, patch)`(font/fill/borders/对齐/数字格式)。
|
|
36
|
+
- **列宽行高**:拖拽 + `setColumnWidth/setRowHeight`,入命令栈。
|
|
37
|
+
- **图片**:浮动图拖拽移动 + `addImage/removeImage/moveImage/resizeImage`。
|
|
38
|
+
- **行列结构**:`insertRows/deleteRows/insertCols/deleteCols`(重键 cells、移合并/宽高/图片,快照逆撤销)。
|
|
39
|
+
- **公式重算**:`recalc` 开启 → 依赖格自动级联;默认 HyperFormula(可选 peer,GPL/商业双授权),`formulaEngine` 可换自研/持牌引擎(`FormulaEngine` 接口)。
|
|
40
|
+
- **脏状态**:`isDirty()` + `dirty-change` 事件 + `resetToOriginal()`(放弃修改还原原件)。
|
|
41
|
+
|
|
42
|
+
### 事件 + 查询(底层机制)
|
|
43
|
+
- **`cell-change` 携前后完整快照**(`CellSnapshot`:底层 CellModel + 解析 style + raw/computed/text),与 `getCellSnapshot` 查询 API 同一份结构。
|
|
44
|
+
- `dim-change` / `image-change` / `struct-change` / `dirty-change` / `edit-start` / `edit-commit`。
|
|
45
|
+
|
|
46
|
+
### 导出(一份数据层,所见即所得)
|
|
47
|
+
- **`.xlsx`**(`exportXlsx/downloadXlsx`):从编辑后模型重建(需 `exceljs`),覆盖值/公式/样式/合并/宽高/冻结/图片。
|
|
48
|
+
- **`.json`**(raw 类型值)/ **`.csv`**(格式化显示值,UTF-8 BOM):复用 `getSheetData/getWorkbookJSON`,三格式天然一致。
|
|
49
|
+
|
|
50
|
+
### 限制
|
|
51
|
+
- 增删行列**不自动重写公式引用文本**(缓存值随格移动);写回 .xlsx 丢 VBA/工作表保护/复杂 DrawingML;图片导出忽略部分子格偏移。
|
|
52
|
+
|
|
53
|
+
### 测试
|
|
54
|
+
- 175 单测 + 38 真浏览器 e2e(Vue + React 双覆盖)+ build 全绿;`dist/core.js` 零 vue/react/hyperformula/exceljs 静态 import(重依赖全动态懒加载)。
|
|
55
|
+
|
|
56
|
+
## [0.2.0] - 2026-06-05
|
|
57
|
+
|
|
58
|
+
**只读** —— 在 0.1.0(Vue-only v1)基础上加:**Vue + React 双壳共享框架无关 core**、列排序、
|
|
59
|
+
跨框架插件、边框还原、多入口分包(core/vue/react)、完整文档。后续「编辑」能力按 semver 推进(0.3.0+)。
|
|
60
|
+
|
|
61
|
+
### 交互 / 插件
|
|
62
|
+
- **列排序**:自动筛选下拉加「升序/降序」,按列重排数据区(整行移动),合并区相交则拒绝。
|
|
63
|
+
- **插件跨框架**:`overlay` 钩子从返回 Vue VNode 改为返回 **DOM 节点**,`core/plugin` 不再 import vue(core 彻底框架无关)。**同一份 `definePlugin` 在 Vue 与 React 壳通用**;React 壳新增 `plugins` prop,支持 theme/transformModel/cellStyle/events/overlay/toolbar/setup 全套。
|
|
64
|
+
|
|
65
|
+
### 边框还原(对齐 Excel/WPS)
|
|
66
|
+
- **合并单元格内部不再画网格线**(之前无填充的合并格会透出内部浅灰网格线)。
|
|
67
|
+
- **斜线边框(对角线 ↘/↗)**:parser 解析 `diagonal{up,down,style,color}`,canvas 与矢量 PDF 都绘制。
|
|
68
|
+
- **相邻共享边按权重取较重者**(hair<…<medium<thick<double):普通格的边框绘制顺序无关、与 Excel/WPS 一致(合并区仍画自身四周)。
|
|
69
|
+
|
|
70
|
+
### 新增
|
|
71
|
+
- **React 壳**:`ooxml-excel-editor/react` 导出 `<ExcelViewer>`(`forwardRef` + 命令式 `ExcelViewerHandle`)与 `useExcelDocument`,与 Vue 壳**共用 ~100% core 引擎**。
|
|
72
|
+
- **框架无关 core 入口**:`ooxml-excel-editor/core` 暴露引擎(`ViewerController` / `CanvasRenderer` / `WorkbookExporter` / `OverlayManager` / `PluginOverlayHost`)+ 解析 + 读数据 + 类型,零框架依赖。
|
|
73
|
+
- **多入口构建**:产物拆为 `dist/core.js`(引擎)+ `dist/index.js`(Vue 壳)+ `dist/react.js`(React 壳),后两者共享同一份 core 引擎 chunk;各自 `.d.ts`。
|
|
74
|
+
- React demo(`/react.html`)+ React 真浏览器 e2e(渲染 / 选区 / 查找 / 数据 API / 导出 / 插件)。
|
|
75
|
+
|
|
76
|
+
### 变更(重构,行为零回归)
|
|
77
|
+
- 把 `ExcelViewer.vue` 的非框架编排逐步下沉到框架无关 `src/core/viewer/`:
|
|
78
|
+
- `OverlayManager`(图片/图表/形状叠加层)+ `PluginOverlayHost`(插件 overlay DOM)
|
|
79
|
+
- `ViewerController`:渲染引擎、选区 + 鼠标/键盘交互、查找、自动筛选、排序、导出编排桥接
|
|
80
|
+
- `WorkbookExporter`(`src/core/export/exporter.ts`):导出/打印编排,靠 `ExporterHost` 与壳解耦
|
|
81
|
+
- `ExcelViewer.vue` 收薄为薄壳:props/插件桥接 + chrome + 经 hooks 桥接控制器响应式。
|
|
82
|
+
- `package.json`:`exports` 增 `./react`、`./core`;`vue`/`react`/`react-dom` 改为可选 peer(按框架二选一)。
|
|
83
|
+
|
|
84
|
+
### 修复
|
|
85
|
+
- React 壳:控制器创建与 rebuild 改用 `useLayoutEffect`,避免晚到的 passive rebuild 清掉刚设置的交互态。
|
|
86
|
+
- 库构建:`worker-client` stub 别名兼容 React 壳的 `@/composables/worker-client` 引入,避免误把 1.4MB exceljs 打进产物。
|
|
87
|
+
|
|
88
|
+
### 基线
|
|
89
|
+
- 测试:**111 单测 + 16 e2e(Vue + React)**全绿;`dist/core.js` 无 vue/react import;exceljs 仅运行时 `import()` 不打包。
|
|
90
|
+
|
|
91
|
+
## [0.1.0] - 2026-06-03
|
|
92
|
+
|
|
93
|
+
高保真 .xlsx 预览组件 v1:从零实现解析 + Canvas 渲染(Vue,只读)。冻结窗格 / 合并单元格 /
|
|
94
|
+
条件格式 / 图表 / 图片形状 / 数字日期格式 / 超链接批注 / 查找 / 自动筛选 / 可配置可插件工具栏 /
|
|
95
|
+
导出(图片·位图PDF·矢量PDF·打印) / 读数据 API / 主题·钩子·插件扩展点。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ooxml-excel-preview contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# ooxml-excel-editor
|
|
2
|
+
|
|
3
|
+
> Vue 3 + React 高保真 **.xlsx 预览 / 编辑组件** —— Canvas 渲染,**默认只读预览,可选开启编辑**。从零实现解析与渲染,尽量还原微软 Excel 打开工作簿的观感。
|
|
4
|
+
|
|
5
|
+
[English](#english) · 中文
|
|
6
|
+
|
|
7
|
+
## ⚡ 快速开始
|
|
8
|
+
|
|
9
|
+
**装**(按框架二选一):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i ooxml-excel-editor vue exceljs # Vue
|
|
13
|
+
npm i ooxml-excel-editor react react-dom exceljs # React
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**用**(Vue,容器要给高度;`src` 可传 `File` / `Blob` / `ArrayBuffer` / `Uint8Array` / URL 字符串):
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { ref } from 'vue'
|
|
21
|
+
import { ExcelViewer } from 'ooxml-excel-editor'
|
|
22
|
+
import 'ooxml-excel-editor/style.css'
|
|
23
|
+
const src = ref<File>() // 绑个 <input type="file" @change> 给它即可
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<ExcelViewer :src="src" style="height: 100vh" />
|
|
28
|
+
</template>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
默认**只读预览**;想编辑加 `:editable="true"`(React 同名 `editable`)。React 写法、props/事件表、编辑 / 导出 API 见下文对应章节。
|
|
32
|
+
|
|
33
|
+
> 纯使用者只需读 **安装 / 使用 / API / 编辑 / 导出** 几节即可接入,无需看源码;类型随包发 `.d.ts`(IDE 自动补全)。「扩展 API / 插件 / 开发」是进阶,可跳过。
|
|
34
|
+
|
|
35
|
+
## 特性
|
|
36
|
+
|
|
37
|
+
- 📊 **Canvas 高保真渲染**:DPR 高清、虚拟滚动(万行流畅)、冻结窗格四象限
|
|
38
|
+
- 🔢 **自写数字格式引擎**:千分位/货币/百分比/科学计数/分数、四段格式(正;负;零;文本)、`[Red]` 颜色、`[>=100]` 条件段、中文日期 `yyyy"年"`、`[h]:mm` 经过时间
|
|
39
|
+
- 🗓 **日期序列号**:含 Excel 1900 闰年 bug、1904 系统
|
|
40
|
+
- 🎨 **主题色 + tint**、indexed 调色板、合并单元格、边框(细/粗/虚/双线)、填充(纯色/图案/渐变)
|
|
41
|
+
- 🌈 **条件格式**:色阶 / 数据条 / 图标集 / cellIs / top10
|
|
42
|
+
- 🖼 **图片 + 图表**(DrawingML → ECharts 近似还原)、**形状/文本框**、**迷你图**(sparklines)、**批注**、**数据验证**下拉、**自动筛选**样式
|
|
43
|
+
- 📝 **文本溢出**到相邻空格、**自动行高**
|
|
44
|
+
- 🖱 **交互**:单元格选区(合并感知)、拖选、公式栏、状态栏(计数/求和/均值/最值)、超链接可点、裁切文本悬停看全文、Ctrl+C 复制为 TSV、**Ctrl+F 查找**(高亮 + 上/下定位 + 计数 + 区分大小写/全字匹配)、**自动筛选**(点下拉真能筛:去重值多选 + 搜值 + 清除)
|
|
45
|
+
- 🖨 **导出 / 打印**:整表/选区/多表导出 **PNG/JPEG**、**PDF**(位图 + **矢量·文字可选可搜**两种)、**系统打印**(可另存 PDF);默认还原原生 `pageSetup`(纸张/方向/页边距/缩放/打印区域/**打印标题行列每页重复**);宽表**横向跨页**(页矩阵);`beforeRenderPage` 注入页眉/页脚/水印、`configureDoc` 注册字体;内置「导出设置」对话框
|
|
46
|
+
- ⚡ **按需加载**(无图表文件不下载 echarts、不导出 PDF 不下载 jspdf)、**友好错误兜底**(损坏/加密/旧 .xls)、解析失败自动给出可读提示
|
|
47
|
+
|
|
48
|
+
- 📤 **数据读取 API**:不必自己再解析 —— `getCellText`/`getSheetData`/`sheetToJSON`/`getRangeData`(独立函数 + 组件 ref 方法),值/显示文本可选,合并/日期/数字格式都处理好
|
|
49
|
+
- ✏️ **编辑(可选,默认只读)**:开 `editable` 即可编辑 —— 单元格值 / 样式(粗体/对齐/填充)/ 列宽行高 / 浮动图片(拖拽移改)/ 增删行列;**撤销重做**(Ctrl+Z/Y)、**前后完整快照事件**、**脏状态 + 一键还原原件**;可换**公式引擎**自动重算依赖格;可注入**自定义编辑器**(下拉/日期/图片选择器);**导出回 .xlsx / JSON / CSV**(所见即所得)。见 [编辑](#编辑可选默认只读)
|
|
50
|
+
|
|
51
|
+
> 纯预览不需要公式引擎 —— .xlsx 缓存了公式结果,直接显示;仅开启**编辑 + 重算**时才用(可选 `hyperformula`)。详见 [EXCEL还原难点.md](./EXCEL还原难点.md)。
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
一个包,三个子入口 —— **框架无关的 core 引擎被 Vue / React 两个薄壳共享**(`dist/core.js` 只打一份)。按你的框架装对应 peer:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Vue 项目
|
|
59
|
+
npm i ooxml-excel-editor vue exceljs
|
|
60
|
+
|
|
61
|
+
# React 项目
|
|
62
|
+
npm i ooxml-excel-editor react react-dom exceljs
|
|
63
|
+
|
|
64
|
+
# 只解析 / 读数据 / 导出(不渲染 UI)
|
|
65
|
+
npm i ooxml-excel-editor exceljs
|
|
66
|
+
|
|
67
|
+
# echarts 可选:仅渲染图表时需要;jspdf 可选:仅导出 PDF 时需要
|
|
68
|
+
npm i echarts jspdf
|
|
69
|
+
# hyperformula 可选:仅开启编辑 + 公式重算(recalc)时需要
|
|
70
|
+
npm i hyperformula
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
三个入口:
|
|
74
|
+
|
|
75
|
+
| import | 内容 | 需要的 peer |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `ooxml-excel-editor` | Vue 3 组件 `<ExcelViewer>` | `vue` + `exceljs` |
|
|
78
|
+
| `ooxml-excel-editor/react` | React 组件 `<ExcelViewer>` | `react` + `react-dom` + `exceljs` |
|
|
79
|
+
| `ooxml-excel-editor/core` | 框架无关引擎(解析/渲染/控制器/导出/读数据) | `exceljs` |
|
|
80
|
+
|
|
81
|
+
`exceljs` 必需;`vue` / `react` / `react-dom` 按框架二选一(均为可选 peer);`echarts` / `jspdf` / `hyperformula` 为**可选** peer —— 未装分别只影响"图表渲染""PDF 导出""公式重算",其余正常,且**绝不打包进你的产物**(运行时才动态加载)。
|
|
82
|
+
|
|
83
|
+
> ⚠️ **公式重算的许可证**:默认公式引擎是 [HyperFormula](https://hyperformula.handsontable.com/),**GPL-3.0 / 商业 双授权**。本组件以 `licenseKey: 'gpl-v3'` 调用(适合开源/GPL 场景)。**商业闭源项目**请改用 `formulaEngine` prop 注入你自己持有商业 license 的引擎(或自研引擎),只需实现 `FormulaEngine` 接口即可。不开启 `recalc` 时完全不加载 hyperformula,无许可证负担。
|
|
84
|
+
|
|
85
|
+
## 使用
|
|
86
|
+
|
|
87
|
+
### Vue
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
import { ref } from 'vue'
|
|
92
|
+
import { ExcelViewer } from 'ooxml-excel-editor'
|
|
93
|
+
import 'ooxml-excel-editor/style.css'
|
|
94
|
+
|
|
95
|
+
const file = ref<File>()
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<template>
|
|
99
|
+
<ExcelViewer
|
|
100
|
+
:src="file"
|
|
101
|
+
:file-name="file?.name"
|
|
102
|
+
style="height: 100vh"
|
|
103
|
+
@rendered="(wb) => console.log('已渲染', wb.sheets.length, '个工作表')"
|
|
104
|
+
@error="(msg) => console.error(msg)"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### React
|
|
110
|
+
|
|
111
|
+
同一套 core 引擎,React 薄壳。命令式 API 走 `ref`(`getSheetData` / `setSelection` / `downloadPdf` …,与 Vue 组件 ref 对齐):
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { useRef, useState } from 'react'
|
|
115
|
+
import { ExcelViewer, type ExcelViewerHandle } from 'ooxml-excel-editor/react'
|
|
116
|
+
|
|
117
|
+
export function Preview() {
|
|
118
|
+
const [file, setFile] = useState<File>()
|
|
119
|
+
const viewer = useRef<ExcelViewerHandle>(null)
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<input type="file" accept=".xlsx" onChange={(e) => setFile(e.target.files?.[0])} />
|
|
123
|
+
<ExcelViewer
|
|
124
|
+
ref={viewer}
|
|
125
|
+
src={file}
|
|
126
|
+
fileName={file?.name}
|
|
127
|
+
style={{ height: '100vh' }}
|
|
128
|
+
onRendered={(wb) => console.log('已渲染', wb.sheets.length, '个工作表')}
|
|
129
|
+
onSelectionChange={({ range, active }) => console.log(range, active)}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 仅引擎(不渲染 UI)
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// 解析 + 读数据 + 导出,全程不依赖任何框架
|
|
140
|
+
import { parseWorkbook, loadArrayBuffer, getSheetData, WorkbookExporter } from 'ooxml-excel-editor/core'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 全局注册(Vue 插件)
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import OoxmlExcelPreview from 'ooxml-excel-editor'
|
|
147
|
+
import 'ooxml-excel-editor/style.css'
|
|
148
|
+
|
|
149
|
+
app.use(OoxmlExcelPreview) // 注册全局组件 <ExcelViewer />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 读取数据(好用的数据访问 API)
|
|
153
|
+
|
|
154
|
+
不必自己再解析。两条路径,都拿同一份数据:
|
|
155
|
+
|
|
156
|
+
**A. 独立函数**(配 `parseWorkbook`,不渲染也能用):
|
|
157
|
+
```ts
|
|
158
|
+
import { parseWorkbook, loadArrayBuffer, getSheetData, sheetToJSON, getCellText, getWorkbookJSON } from 'ooxml-excel-editor'
|
|
159
|
+
|
|
160
|
+
const wb = await parseWorkbook(await loadArrayBuffer(file))
|
|
161
|
+
const sheet = wb.sheets[0]
|
|
162
|
+
|
|
163
|
+
getCellText(sheet, 1, 0, wb.date1904) // 单格显示文本,如 '产品' / '¥1,234.50'
|
|
164
|
+
getSheetData(sheet, { date1904: wb.date1904 }) // 二维数组(显示文本)
|
|
165
|
+
getSheetData(sheet, { format: false, date1904: wb.date1904 }) // 二维数组(原始 number/Date)
|
|
166
|
+
sheetToJSON(sheet, { headerRow: 0, date1904: wb.date1904 }) // 首行作表头 → [{ 产品:'鼠标', 单价:89 }, ...]
|
|
167
|
+
getWorkbookJSON(wb) // 全簿 → { 表名: 对象数组 }(自动带 date1904)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**B. 组件 ref**(自动带 `date1904` + 默认当前表;插件 `ctx.viewer` 同样可用):
|
|
171
|
+
```ts
|
|
172
|
+
const viewer = ref() // <ExcelViewer ref="viewer" />
|
|
173
|
+
viewer.value.getCellText(1, 0) // 显示文本
|
|
174
|
+
viewer.value.getCellValue(1, 0) // 原始值
|
|
175
|
+
viewer.value.getSheetData() // 当前表 2D(默认显示文本)
|
|
176
|
+
viewer.value.getSheetJSON({ headerRow: 1 }) // 对象数组
|
|
177
|
+
viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区域
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| 函数 / 方法 | 返回 |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `getCellValue` / `getCellText` / `getCellStyle` / `getCell` | 单格 原始值 / 显示文本 / 解析样式 / 模型 |
|
|
183
|
+
| `getSheetData` | 二维数组(稠密 `rows×cols`) |
|
|
184
|
+
| `getRangeData(range)` | 区域二维数组 |
|
|
185
|
+
| `sheetToJSON` | 对象数组(首行作 key,空表头回退列字母,全空行跳过) |
|
|
186
|
+
| `getWorkbookJSON` | `{ 表名: 对象数组 }` |
|
|
187
|
+
|
|
188
|
+
- **值 vs 文本**:`format` 默认 `true` → 套了数字/日期格式的**显示文本**(所见即所得);`{ format: false }` → 原始 `number/Date/boolean`。
|
|
189
|
+
- **合并单元格**:2D/JSON 里**锚点(左上)持值,其余为空**(单格 `getCellValue` 仍返回模型里的字面值)。
|
|
190
|
+
- **公式**:不重算,沿用 Excel 缓存结果(公式串见 `cell.formula`)。
|
|
191
|
+
- 也 re-export 了底层 `formatValue` / `cellKey`。
|
|
192
|
+
|
|
193
|
+
> 想要更底层的渲染模型,仍可直接用 `parseWorkbook` 的返回值 / `getWorkbook()` / `@rendered`(`WorkbookModel`:`sheets[].cells: Map<"row:col">`、`styles[styleId]`)。
|
|
194
|
+
|
|
195
|
+
## API
|
|
196
|
+
|
|
197
|
+
### `<ExcelViewer>`
|
|
198
|
+
|
|
199
|
+
| Prop | 类型 | 说明 |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `src` | `File \| Blob \| ArrayBuffer \| Uint8Array \| string(URL)` | 要预览的 .xlsx 数据源 |
|
|
202
|
+
| `fileName` | `string` | 标题栏显示的文件名(可选) |
|
|
203
|
+
| `editable` | `boolean` | 开启编辑(默认 `false` = 只读,行为与历史一致) |
|
|
204
|
+
| `cellReadOnly` | `(cell, pos) => boolean` | 按格只读判定(编辑时) |
|
|
205
|
+
| `readOnlyRanges` | `MergeRange[]` | 只读区域(命中即只读) |
|
|
206
|
+
| `editor` | `EditorResolver` | 自定义单元格编辑器工厂(返回任意 DOM) |
|
|
207
|
+
| `recalc` | `boolean` | 公式重算(默认 `false`;需 `editable`) |
|
|
208
|
+
| `formulaEngine` | `FormulaEngineFactory` | 自定义/自研公式引擎(默认 HyperFormula) |
|
|
209
|
+
|
|
210
|
+
> 编辑相关 props 详见下方 [编辑](#编辑可选默认只读) 章节。
|
|
211
|
+
|
|
212
|
+
| 事件 | 载荷 | 触发时机 |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| `rendered` | `WorkbookModel` | 解析并首次渲染完成 |
|
|
215
|
+
| `error` | `string` | 解析失败(友好文案) |
|
|
216
|
+
| `cell-change` | `{ before, after, source }` | 单元格变更(编辑/撤销/重做/公式级联);`before`/`after` 是**完整快照**(底层 cell + 解析 style + raw/computed/text) |
|
|
217
|
+
| `edit-start` / `edit-commit` | `{ cell, ... }` | 进入 / 提交编辑 |
|
|
218
|
+
| `dim-change` | `{ axis, index, before, after }` | 列宽/行高变更 |
|
|
219
|
+
| `image-change` | `{ index, before, after }` | 图片增删移改(前后 `ImageAnchor`) |
|
|
220
|
+
| `struct-change` | `{ op, at, count }` | 增删行列 |
|
|
221
|
+
| `dirty-change` | `{ dirty }` | 有/无未保存修改 |
|
|
222
|
+
|
|
223
|
+
容器需要有明确高度(组件填满父容器),例如 `style="height: 100vh"`。
|
|
224
|
+
|
|
225
|
+
### 具名导出
|
|
226
|
+
|
|
227
|
+
| 导出 | 说明 |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `ExcelViewer` | 预览/编辑组件 |
|
|
230
|
+
| `parseWorkbook(buffer)` | `ArrayBuffer → Promise<WorkbookModel>`(优先 Web Worker) |
|
|
231
|
+
| `loadArrayBuffer(src)` | 多种输入归一化为 `ArrayBuffer` |
|
|
232
|
+
| `default` | Vue 插件(`app.use`) |
|
|
233
|
+
| 类型 | `WorkbookModel` / `SheetModel` / `CellModel` / `CellStyle` / `MergeRange` / `ConditionalRule` / `ChartSpec` / `ImageAnchor` / `CssColor` / `ExcelSource` |
|
|
234
|
+
|
|
235
|
+
## 编辑(可选,默认只读)
|
|
236
|
+
|
|
237
|
+
组件默认是**只读预览**,行为与历史完全一致、零额外成本。传 `:editable` 才进入编辑模式,所有编辑能力建在**一份可变内存模型**(`WorkbookModel`)上,读 / 写 / 事件 / 导出共用同一层。
|
|
238
|
+
|
|
239
|
+
### 开启 + 只读控制
|
|
240
|
+
|
|
241
|
+
```vue
|
|
242
|
+
<ExcelViewer
|
|
243
|
+
:src="src"
|
|
244
|
+
:editable="true"
|
|
245
|
+
:read-only-ranges="[{ top: 0, left: 0, bottom: 0, right: 4 }]" <!-- 表头整行只读 -->
|
|
246
|
+
:cell-read-only="(cell, pos) => pos.col === 0" <!-- 第 0 列只读 -->
|
|
247
|
+
@cell-change="onCellChange"
|
|
248
|
+
/>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
开编辑后:**双击 / F2 / 直接打字**进格编辑(Enter 提交下移、Tab 右移、Esc 取消),**Ctrl+Z / Ctrl+Y** 撤销重做,拖列头/行头边界改宽高,拖浮动图片移动。只读格 / 区域不进编辑。
|
|
252
|
+
|
|
253
|
+
### 命令式编辑 API(模板 ref / 插件 `viewer`)
|
|
254
|
+
|
|
255
|
+
| 类别 | 方法 |
|
|
256
|
+
|---|---|
|
|
257
|
+
| 值 | `editCell(row,col,value)` · `editRange(range,values[][])` · `clearRange(range)` |
|
|
258
|
+
| 样式 | `setStyle(range, patch)`(`patch` = `CellStyleOverride`:font/fill/borders/对齐/numFmt) |
|
|
259
|
+
| 列宽行高 | `setColumnWidth(col,px)` · `setRowHeight(row,px)` |
|
|
260
|
+
| 行列结构 | `insertRows(at,count?)` · `deleteRows(at,count?)` · `insertCols(at,count?)` · `deleteCols(at,count?)` |
|
|
261
|
+
| 图片 | `getImages()` · `addImage(anchor)` · `removeImage(i)` · `moveImage(i,dxPx,dyPx)` · `resizeImage(i,wPx,hPx)` |
|
|
262
|
+
| 撤销/进编辑 | `undo()` · `redo()` · `canUndo()` · `canRedo()` · `beginEdit(row,col)` · `cancelEdit()` · `isEditing()` · `getEditingCell()` |
|
|
263
|
+
| 查询/状态 | `getCellSnapshot(row,col)` · `isDirty()` · `resetToOriginal()` · `isRecalcReady()` |
|
|
264
|
+
| 导出 | `exportXlsx/downloadXlsx` · `exportJson/downloadJson` · `exportCsv/downloadCsv`(见 [导出](#导出--打印)) |
|
|
265
|
+
|
|
266
|
+
所有写操作(含拖拽改宽高/移图)**统一进撤销栈**、发对应事件、翻**脏标记**;`resetToOriginal()` 一键放弃全部修改、还原到刚加载的原件。
|
|
267
|
+
|
|
268
|
+
### 自定义编辑器 `:editor`
|
|
269
|
+
|
|
270
|
+
返回一个工厂(拿到 `ctx` 里的快照 / 矩形 / `commit`/`cancel`),挂任意 DOM 当编辑控件 —— 下拉、日期选择器、图片选择器、带按钮的面板…… 是框架无关的 DOM(Vue/React 共用)。`commit` 可只给值,也可 `{ value, style }` 同时套样式。
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const myEditor: EditorResolver = (cell, pos) => {
|
|
274
|
+
if (pos.col !== 0) return // 该列不接管 → 用内置文本编辑器
|
|
275
|
+
return (ctx) => { // 工厂:返回 DOM
|
|
276
|
+
const sel = document.createElement('select')
|
|
277
|
+
sel.innerHTML = `<option>A</option><option>B</option>`
|
|
278
|
+
sel.value = String(ctx.snapshot.raw ?? '')
|
|
279
|
+
sel.onchange = () => ctx.commit(sel.value)
|
|
280
|
+
return sel
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
插件也可经 `editor` 字段贡献(多插件数组序,组件 prop 最后覆盖),与 `cellStyle`/`overlay` 同范式。
|
|
286
|
+
|
|
287
|
+
### 公式重算(可换引擎)
|
|
288
|
+
|
|
289
|
+
开 `:recalc` 后,编辑公式格或被公式引用的格 → 依赖格**自动级联重算**,每个变动都发 `cell-change`(`source: 'api'|'ui'|'undo'|'redo'`)。默认引擎 [HyperFormula](https://hyperformula.handsontable.com/)(可选 peer,`npm i hyperformula`;**GPL-3.0/商业双授权**,商业项目用 `:formula-engine` 注入持牌/自研引擎,实现 `FormulaEngine` 接口即可)。`isRecalcReady()` 查引擎是否就绪(异步懒加载)。
|
|
290
|
+
|
|
291
|
+
### 事件 = 前后完整快照
|
|
292
|
+
|
|
293
|
+
每次编辑都以 **`cell-change`** 通知:`{ before, after, source }`,`before`/`after` 是 `CellSnapshot` —— 不只 raw,还含**计算值 computed、显示文本 text、整个底层 `CellModel` + 解析后 `style`**。事件流**和**查询 API(`getCellSnapshot`)同一份底层结构 → JSON/CSV/XLSX 导出都复用它,无需按格式各写一遍解析。另有 `dim-change`/`image-change`/`struct-change`/`dirty-change`(见上方事件表)。
|
|
294
|
+
|
|
295
|
+
### v1 已知限制
|
|
296
|
+
|
|
297
|
+
- 增删行列**会自动重写公式引用**(`=A5` 上方插一行 → `=A6`,删被引用行 → `#REF!`,含跨表 `Sheet1!A5`)。
|
|
298
|
+
- 写回 .xlsx 默认从模型重建,丢 VBA/工作表保护/复杂 DrawingML 等;需要更高保真可用 `exportXlsx({ fidelity: 'overlay' })` **重载原件叠加编辑**(见 [导出保真边界](#导出--打印))。
|
|
299
|
+
|
|
300
|
+
## 扩展 API(不改源码定制)
|
|
301
|
+
|
|
302
|
+
组件按"分层可扩展"设计 —— 用内置 props/events/slots/命令式 API 即可定制外观、行为、数据,并在网格上叠自己的 UI。
|
|
303
|
+
|
|
304
|
+
### 外观主题 `:theme`
|
|
305
|
+
```vue
|
|
306
|
+
<ExcelViewer :src="file" :theme="{ gridLine: '#e8e8e8', selBorder: '#e91e63', selFill: 'rgba(233,30,99,.1)' }" />
|
|
307
|
+
```
|
|
308
|
+
可覆盖:`headerBg / headerText / headerLine / gridLine / selBorder / selFill`(见 `ViewerTheme` / `DEFAULT_THEME` 导出)。
|
|
309
|
+
|
|
310
|
+
### 数据 / 渲染钩子
|
|
311
|
+
```vue
|
|
312
|
+
<ExcelViewer
|
|
313
|
+
:src="file"
|
|
314
|
+
:transform-model="(wb) => { wb.sheets[0].name = '改过的名字'; return wb }"
|
|
315
|
+
:cell-style="(cell) => typeof cell.raw === 'number' && cell.raw < 0 ? { font: { color: '#d00' } } : undefined"
|
|
316
|
+
/>
|
|
317
|
+
```
|
|
318
|
+
- `transformModel(wb)`:解析后、渲染前改模型(返回新模型或就地改)。
|
|
319
|
+
- `cellStyle(cell, {row,col})`:按条件覆盖单元格样式(`font/fill/borders` 浅合并)。
|
|
320
|
+
|
|
321
|
+
### 事件
|
|
322
|
+
| 事件 | 载荷 |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `cell-click` / `cell-dblclick` | `{ row, col, text }` |
|
|
325
|
+
| `selection-change` | `{ range, active }` |
|
|
326
|
+
| `sheet-change` | `{ index, name }` |
|
|
327
|
+
| `hyperlink-click` | `{ url, cell }`(配 `:open-links="false"` 接管跳转) |
|
|
328
|
+
| `rendered` / `error` / `progress` | 见上 |
|
|
329
|
+
|
|
330
|
+
### 命令式 API(模板 ref)
|
|
331
|
+
`load(src)` / `getWorkbook()` / `getActiveSheet()` / `setActiveSheet(i)` / `getSelection()` / `setSelection(range)` / `rectOf(row,col)` / `rectOfRange(range)` / `redraw()`,以及下面的导出方法;**编辑命令式 API**(`editCell`/`setStyle`/`insertRows`/`undo`/`exportXlsx`…)见 [编辑](#编辑可选默认只读)。
|
|
332
|
+
|
|
333
|
+
### 导出 / 打印
|
|
334
|
+
内置工具栏右侧有「导出 ▾」菜单(PNG / PDF / 打印 / **导出设置…**)。「导出设置…」打开对话框,可选**范围**(当前选区 / 当前表 / 全部表)、清晰度、是否含行列号/网格线、纸张方向。也可命令式调用(模板 ref / 插件 `viewer`):
|
|
335
|
+
|
|
336
|
+
| 方法 | 说明 |
|
|
337
|
+
|---|---|
|
|
338
|
+
| `exportImage(opts?)` | → `Promise<Blob>`,当前/指定表渲染为图片(png/jpeg/webp) |
|
|
339
|
+
| `downloadImage(opts?)` | 导出图片并触发下载 |
|
|
340
|
+
| `exportPdf(opts?)` | → `Promise<Blob>`,分页 PDF(需可选依赖 `jspdf`) |
|
|
341
|
+
| `downloadPdf(opts?)` | 导出 PDF 并触发下载 |
|
|
342
|
+
| `print(opts?)` | 打开系统打印对话框(可另存为 PDF,零依赖) |
|
|
343
|
+
| `exportXlsx(opts?)` / `downloadXlsx(opts?)` | → `Promise<Blob>` / 下载 **.xlsx**(默认从模型重建;`{fidelity:'overlay'}` 重载原件叠加,保真更高;需可选依赖 `exceljs`) |
|
|
344
|
+
| `exportJson(opts?)` / `downloadJson(opts?)` | → `string` / 下载 **.json**(各表首行作 key 的对象数组,raw 类型值) |
|
|
345
|
+
| `exportCsv(opts?)` / `downloadCsv(opts?)` | → `string` / 下载 **.csv**(格式化显示值,带 UTF-8 BOM;`opts.target` 指定表,默认当前表) |
|
|
346
|
+
|
|
347
|
+
**编辑后导出(.xlsx / JSON / CSV)** —— 三种格式都建在**同一份内存数据层**上(`WorkbookModel`:读 `data-access` + 写 `mutations`),无需为每种格式各写一遍解析,故与渲染所见、彼此之间天然一致。JSON 默认输出 raw 类型值(`{format:true}` 可改显示串);CSV 默认输出格式化显示值(WYSIWYG)。
|
|
348
|
+
|
|
349
|
+
**.xlsx 两种保真模式**:
|
|
350
|
+
|
|
351
|
+
- **`rebuild`(默认)** —— **从编辑后模型完整重建**:遍历 cells/公式/样式(字体/填充/边框/对齐/数字格式)/合并/行高列宽/冻结/图片 重组成 ExcelJS 工作簿。干净、所见即所得,但**丢失**原件里我们不建模的部分(条件格式、数据验证、VBA 宏、工作表保护、复杂 DrawingML/图表 等)。图片导出区分 oneCell/twoCell 锚点 + 子格 EMU 偏移。
|
|
352
|
+
- **`overlay`(`exportXlsx({ fidelity: 'overlay' })`)** —— **重载原始 .xlsx,只把编辑后的 值/样式/合并/行高列宽/冻结 叠加上去**,**保留** ExcelJS 能往返的其余部分(条件格式 / 数据验证 / 打印设置 / 定义名 / 图表 等)。组件加载时自动留存原件字节供其使用;缺原件时自动回退 `rebuild`。注:overlay 不反映**增删行列 / 图片**编辑(那类用 `rebuild`)。
|
|
353
|
+
|
|
354
|
+
公共选项:`target`(`'active'`(默认)/`'all'`/索引/索引数组)、`range`(限定单元格区域)、`scale`(清晰度,默认 2)、`includeHeaders`、`gridlines`、`background`;PDF/打印另有 `format`(a4/a3/letter/`[宽,高]mm`)、`orientation`、`margin`(mm)、`fitToWidth`。
|
|
355
|
+
|
|
356
|
+
**默认还原 OOXML 原生页面设置** —— PDF/打印时,未显式指定的 `format`/`orientation`/`margin`/`fitToWidth` 自动取自工作表的 `pageSetup`(纸张、方向、页边距、适应页面/缩放),并应用**打印区域**(默认导出范围)与**打印标题行/列**(每页顶部/左侧重复)。显式传入的选项始终覆盖之。
|
|
357
|
+
|
|
358
|
+
**分页** —— `fitToWidth: true`(默认)把整表缩放到页宽、只竖向跨页;`fitToWidth: false`(或工作表未设"适应页面")按自然尺寸×缩放,**宽表横向跨页 + 高表竖向跨页**(像 Excel 的页矩阵,顺序"先下后右"),此时打印标题列在每张横向页左侧重复。
|
|
359
|
+
|
|
360
|
+
**`beforeRenderPage` 扩展钩子** —— 每页贴图后调用,拿到 `jsPDF` 实例画页眉/页脚/水印/页码:
|
|
361
|
+
```ts
|
|
362
|
+
const viewer = ref() // <ExcelViewer ref="viewer" />
|
|
363
|
+
await viewer.value.downloadPdf({
|
|
364
|
+
target: 'all',
|
|
365
|
+
beforeRenderPage: ({ doc, pageIndex, pageCount, pageWidth, pageHeight, margin, sheetName }) => {
|
|
366
|
+
doc.setFontSize(9); doc.setTextColor(120)
|
|
367
|
+
doc.text(sheetName, margin.left, pageHeight - 5)
|
|
368
|
+
doc.text(`第 ${pageIndex + 1} / ${pageCount} 页`, pageWidth - margin.right, pageHeight - 5, { align: 'right' })
|
|
369
|
+
doc.setFontSize(56); doc.setTextColor(230)
|
|
370
|
+
doc.text('PREVIEW', pageWidth / 2, pageHeight / 2, { align: 'center', angle: 30 }) // 水印
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
打印另有 `title` / `headerHtml` / `footerHtml`(每页 HTML 片段)。
|
|
375
|
+
> 图片/图表/形状是 DOM 叠加层,导出时会自动合成到底图;"导出全部表"中非当前表的图表需 `echarts` 可用才能离屏渲染。
|
|
376
|
+
|
|
377
|
+
#### 矢量 PDF(文字可选可搜)
|
|
378
|
+
|
|
379
|
+
两种 PDF 并存,工具栏菜单有「位图 / 矢量」两项,API 用 `vector` 切换:
|
|
380
|
+
```ts
|
|
381
|
+
await viewer.value.downloadPdf({ vector: true })
|
|
382
|
+
```
|
|
383
|
+
- **位图 PDF**(默认):整表贴图,完整还原观感。
|
|
384
|
+
- **矢量 PDF**:逐格用真文字 + 矢量填充/边框绘制 —— 文字**可选中、可搜索、放大清晰、文件更小**。条件格式(背景色/数据条/图标)也走矢量绘制;仅**迷你图、旋转文字、富文本**这几类格会自动从底图**裁小图兜底**(内容不丢)。
|
|
385
|
+
|
|
386
|
+
**中文字体** —— jsPDF 内置字体只认拉丁/数字。用 `configureDoc(doc)` 钩子注册中文 TTF 即可全矢量;不注册时,含中文的单元格自动转为该格小图(清晰但不可选):
|
|
387
|
+
```ts
|
|
388
|
+
await viewer.value.downloadPdf({
|
|
389
|
+
vector: true,
|
|
390
|
+
configureDoc: (doc) => {
|
|
391
|
+
doc.addFileToVFS('NotoSansSC.ttf', base64Ttf) // 你的中文字体(建议子集化)
|
|
392
|
+
doc.addFont('NotoSansSC.ttf', 'NotoSC', 'normal')
|
|
393
|
+
doc.setFont('NotoSC') // 设为默认 → 中文也走矢量
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
```
|
|
397
|
+
> 提示:中文表格若不注册字体,矢量模式会产生很多小图、文件偏大且较慢 —— 注册一个子集字体即可全矢量。
|
|
398
|
+
|
|
399
|
+
### 操作工具栏(可配置 / 可插件 / 响应式)
|
|
400
|
+
顶栏(文件名/导出/缩放)下方有一行**操作工具栏**,内置 `find`/`filter` 按钮默认显示。用 `:toolbar` 配置:
|
|
401
|
+
```vue
|
|
402
|
+
<ExcelViewer :src="file" /> <!-- 默认: find + filter -->
|
|
403
|
+
<ExcelViewer :toolbar="['find','filter','separator','zoom','export']" /> <!-- 控制项/顺序/分隔 -->
|
|
404
|
+
<ExcelViewer :toolbar="false" /> <!-- 隐藏整条 -->
|
|
405
|
+
```
|
|
406
|
+
- **内置 id**:`find`(查找)、`filter`(切换自动筛选 —— 文件没设也能点出下拉)、`clear-filter`(清除筛选,无筛选时禁用)、`copy`(复制选区)、`freeze`(冻结/取消)、`zoom`(缩放下拉)、`export`(导出/打印下拉)、`'separator'`/`'|'`(分隔线);`sort` 规划中。
|
|
407
|
+
- **富项类型**(`ToolbarItem`):`type:'separator'` 分隔线;`items: ToolbarItem[]` 变下拉子菜单;`disabled?(viewer)` 禁用态;`iconSvg`(内联 SVG,优先于 `icon` emoji)/ `icon` / `label` / `title` / `onClick(viewer)` / `active?(viewer)`。
|
|
408
|
+
- **响应式溢出**:宽度不足时,放不下的项自动折叠进「⋯ 更多」下拉。
|
|
409
|
+
- **插件贡献**:`ExcelPlugin.toolbar: ToolbarItem[]`,插件加载即追加(opt-in)。
|
|
410
|
+
- 内置图标用极简线性 **SVG**(跨平台一致);`filter` 按钮让筛选**看得见**,不必依赖文件自带 autofilter。
|
|
411
|
+
|
|
412
|
+
### 分层 UI(slots)
|
|
413
|
+
具名 slot:`header`(顶栏)/ `toolbar`(作用域 `{ items }`,替换整条操作栏)/ `statusbar` / `loading` / `error` / `empty`(缺省用内置)。
|
|
414
|
+
**作用域 `overlay` slot** —— 在格子上叠自己的 Vue 组件,随滚动/缩放跟随:
|
|
415
|
+
```vue
|
|
416
|
+
<ExcelViewer :src="file">
|
|
417
|
+
<template #overlay="{ rectOf, tick }">
|
|
418
|
+
<!-- tick 变化触发重算;rectOf(row,col) 给当前屏幕矩形 -->
|
|
419
|
+
<button v-if="rectOf(2,1)" :style="posStyle(rectOf(2,1), tick)" @click="...">★</button>
|
|
420
|
+
</template>
|
|
421
|
+
</ExcelViewer>
|
|
422
|
+
```
|
|
423
|
+
覆盖层容器 `pointer-events:none`(滚动穿透),子元素自动 `pointer-events:auto`(可点)。
|
|
424
|
+
|
|
425
|
+
### 插件 `definePlugin`
|
|
426
|
+
把上面所有扩展点(主题/数据钩子/渲染钩子/事件/overlay/命令式 API)打包成一个插件,`:plugins` 分发;多个插件按数组顺序合并,组件自身 props 最后覆盖。
|
|
427
|
+
```ts
|
|
428
|
+
import { definePlugin } from 'ooxml-excel-editor'
|
|
429
|
+
|
|
430
|
+
const highlightNegatives = definePlugin({
|
|
431
|
+
name: 'highlight-negatives',
|
|
432
|
+
theme: { selBorder: '#e91e63' },
|
|
433
|
+
cellStyle: (c) => (typeof c.raw === 'number' && c.raw < 0 ? { font: { color: '#d00' } } : undefined),
|
|
434
|
+
events: { 'cell-click': (p) => console.log('clicked', p) },
|
|
435
|
+
overlay: ({ rectOf }) => {
|
|
436
|
+
const r = rectOf(0, 0)
|
|
437
|
+
if (!r) return null
|
|
438
|
+
const el = document.createElement('div') // 返回 DOM(框架无关,Vue/React 通用)
|
|
439
|
+
el.textContent = '⚑'
|
|
440
|
+
Object.assign(el.style, { position: 'absolute', left: r.x + 'px', top: r.y + 'px' })
|
|
441
|
+
return el
|
|
442
|
+
},
|
|
443
|
+
setup: ({ viewer, on }) => {
|
|
444
|
+
on('selection-change', (s) => console.log(s))
|
|
445
|
+
// viewer.setSelection(...) / viewer.getWorkbook() ...
|
|
446
|
+
return () => {/* 清理 */}
|
|
447
|
+
},
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
```vue
|
|
451
|
+
<ExcelViewer :src="file" :plugins="[highlightNegatives]" />
|
|
452
|
+
```
|
|
453
|
+
插件字段:`theme` / `transformModel` / `cellStyle` / `events`(事件→处理器) / `overlay`(返回 **DOM 节点**,随滚动跟随) / `toolbar`(贡献操作栏按钮 `ToolbarItem[]`) / `setup(ctx)`(拿 `viewer` 命令式 API、`on()` 订阅事件,返回可选清理函数)。
|
|
454
|
+
|
|
455
|
+
> **跨框架**:插件全字段框架无关,**同一份 `definePlugin` 在 Vue 和 React 壳通用**(`overlay` 返回 DOM 而非 VNode)。React 用法:`<ExcelViewer plugins={[myPlugin]} />`。
|
|
456
|
+
|
|
457
|
+
## 浏览器支持
|
|
458
|
+
|
|
459
|
+
现代浏览器(Chrome/Edge 80+、Safari 15+、Firefox 114+,需支持 Canvas / ResizeObserver)。
|
|
460
|
+
|
|
461
|
+
> **解析线程**:发布的组件库在**主线程**解析(`exceljs` 为 peer 依赖,不重复打包)。本仓库的 demo/dev 额外启用了 **Web Worker** 解析(大文件不卡 UI)。如果你的应用要处理很大的文件,可直接用导出的 `parseWorkbook` 包进你自己的 Worker。
|
|
462
|
+
|
|
463
|
+
## 范围边界
|
|
464
|
+
|
|
465
|
+
- **编辑**已支持(默认只读;开 `editable`,见 [编辑](#编辑可选默认只读))。下列为暂不覆盖项:
|
|
466
|
+
- 增删行列**不自动重写公式引用**;写回 .xlsx 丢 VBA/工作表保护/复杂 DrawingML
|
|
467
|
+
- 透视表:**数据按普通单元格显示**,但无字段按钮/下拉等透视专属 UI
|
|
468
|
+
- SmartArt;形状仅支持 rect/roundRect/ellipse + 文本(复杂自定义几何按矩形近似)
|
|
469
|
+
- `.xls`(旧 BIFF 二进制)/ 加密文件(给出友好提示)
|
|
470
|
+
- 图表为 ECharts 近似,非像素级一致
|
|
471
|
+
- 导出为**位图**(PNG/PDF 内嵌图片),非矢量;PDF 按页宽缩放后竖向分页
|
|
472
|
+
|
|
473
|
+
## 开发
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
npm install
|
|
477
|
+
npm run dev # 本地预览(demo)
|
|
478
|
+
node scripts/gen-sample.mjs # 生成 public/sample.xlsx 示例
|
|
479
|
+
npm run test # 单元测试(node 环境,纯逻辑)
|
|
480
|
+
npm run test:e2e # 真浏览器 e2e(Playwright):canvas 渲染 + jsPDF 导出 + 下载全链路
|
|
481
|
+
npm run typecheck # 类型检查
|
|
482
|
+
npm run build # 构建组件库(dist/)
|
|
483
|
+
npm run build:demo # 构建 demo 站点
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
> **e2e 说明**:`npm run test:e2e` 用 Playwright 起 dev 服务 + 无头 Chromium,加载示例 → 渲染 → 导出 PNG/位图PDF/矢量PDF,校验产物(PNG 魔数、`%PDF`、矢量 PDF 的文字操作符数量多于位图)。覆盖 node 单测做不到的真实 canvas/jsPDF 绘制。首次需 `npx playwright install chromium` 下载浏览器(本仓库 `@playwright/test` 固定 `1.58.0` 对应 chromium-1208)。Vue demo 在 `/`、React demo 在 `/react.html`。
|
|
487
|
+
|
|
488
|
+
## 文档 / 二开
|
|
489
|
+
|
|
490
|
+
- [ARCHITECTURE.md](./ARCHITECTURE.md) —— 包/入口、core 分层、数据流、`ViewerController` 桥接、"加功能改哪"
|
|
491
|
+
- [CONTRIBUTING.md](./CONTRIBUTING.md) —— 本地跑通、改动流程、不可破坏的硬约束
|
|
492
|
+
- [CHANGELOG.md](./CHANGELOG.md) / [RELEASING.md](./RELEASING.md) —— 变更记录 / 发布清单
|
|
493
|
+
|
|
494
|
+
> **React props/events** 与 Vue 对齐(事件用 camelCase 回调:`onRendered`/`onError`/`onCellClick`/`onSelectionChange`/`onSheetChange`/`onHyperlinkClick`),命令式句柄 `ExcelViewerHandle` 与 Vue 组件 ref 同名方法一致。上面「扩展 API」中的**插件 `definePlugin`** 目前服务 Vue 壳;React 壳已可用全部 props/命令式 API/事件,插件 overlay 跨框架化在路线图中。
|
|
495
|
+
|
|
496
|
+
## License
|
|
497
|
+
|
|
498
|
+
MIT
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
<a name="english"></a>
|
|
503
|
+
## English
|
|
504
|
+
|
|
505
|
+
A **Vue 3 + React high-fidelity `.xlsx` preview & edit component** with a from-scratch parser and canvas renderer. Renders cells, number formats, merges, conditional formatting, images, charts (via ECharts), sparklines, comments, data validation, frozen panes, and supports selection / copy / hyperlinks. **Read-only by default**; set `editable` to enable editing — cell values / styles / column-row sizes / floating images / insert-delete rows-cols, with undo-redo, before/after full-snapshot events, dirty tracking + reset-to-original, swappable formula recalc engine, custom cell editors, and **export back to .xlsx / JSON / CSV**. Parsing runs in a Web Worker (with main-thread fallback). `vue` / `react` / `exceljs` are peer dependencies; `echarts` / `jspdf` / `hyperformula` are optional peers (charts / PDF / formula recalc).
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
npm i ooxml-excel-editor vue exceljs
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
import { ExcelViewer } from 'ooxml-excel-editor'
|
|
513
|
+
import 'ooxml-excel-editor/style.css'
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
See the API table above. MIT licensed.
|