ooxml-excel-editor 1.1.0 → 1.3.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +206 -18
  3. package/dist/assets/exceljs.min-DH9BABny.js +23046 -0
  4. package/dist/assets/parse.worker-DIaMHX0x.js +2646 -0
  5. package/dist/chunks/plugin-overlay-DLb6sRhU.js +9005 -0
  6. package/dist/chunks/toolbar-icons-BcnMin5s.js +223 -0
  7. package/dist/components/ExcelViewer.vue.d.ts +172 -19
  8. package/dist/components/ExportProgressOverlay.vue.d.ts +11 -0
  9. package/dist/components/FilterPopup.vue.d.ts +4 -4
  10. package/dist/components/ViewerToolbar.vue.d.ts +2 -0
  11. package/dist/composables/useExcelDocument.d.ts +1 -0
  12. package/dist/core/edit/clipboard-html.d.ts +24 -0
  13. package/dist/core/edit/commands.d.ts +45 -1
  14. package/dist/core/edit/context-menu.d.ts +19 -0
  15. package/dist/core/edit/edit-controller.d.ts +70 -2
  16. package/dist/core/edit/editor-context.d.ts +18 -3
  17. package/dist/core/edit/editor-host.d.ts +6 -1
  18. package/dist/core/edit/permissions.d.ts +41 -2
  19. package/dist/core/edit/types.d.ts +62 -0
  20. package/dist/core/export/abort.d.ts +21 -0
  21. package/dist/core/export/exporter.d.ts +2 -1
  22. package/dist/core/export/types.d.ts +8 -0
  23. package/dist/core/export/wps-cellimages.d.ts +6 -0
  24. package/dist/core/export/xlsx-writer.d.ts +9 -0
  25. package/dist/core/format/color.d.ts +5 -0
  26. package/dist/core/format/number-format.d.ts +3 -0
  27. package/dist/core/layout/autofit.d.ts +3 -0
  28. package/dist/core/layout/grid-metrics.d.ts +14 -2
  29. package/dist/core/loader-json.d.ts +23 -0
  30. package/dist/core/model/clone.d.ts +3 -4
  31. package/dist/core/model/inspect.d.ts +43 -0
  32. package/dist/core/model/mutations.d.ts +16 -1
  33. package/dist/core/model/types.d.ts +44 -2
  34. package/dist/core/parser/cell-image-parser.d.ts +9 -0
  35. package/dist/core/parser/row-meta-parser.d.ts +3 -0
  36. package/dist/core/plugin.d.ts +144 -6
  37. package/dist/core/progress.d.ts +23 -0
  38. package/dist/core/render/canvas-renderer.d.ts +56 -2
  39. package/dist/core/render/conditional.d.ts +7 -0
  40. package/dist/core/template/style-overlay.d.ts +9 -0
  41. package/dist/core/viewer/controller.d.ts +209 -6
  42. package/dist/core/viewer/lightbox-host.d.ts +16 -0
  43. package/dist/core.js +1 -1
  44. package/dist/index.js +1169 -841
  45. package/dist/react/ExcelViewer.d.ts +143 -3
  46. package/dist/react/ExportProgressOverlay.d.ts +6 -0
  47. package/dist/react/use-excel-document.d.ts +2 -0
  48. package/dist/react.js +938 -335
  49. package/dist/style.css +1 -1
  50. package/dist/vue2.css +1 -0
  51. package/dist/vue2.js +10433 -0
  52. package/package.json +18 -6
  53. package/dist/chunks/plugin-overlay-Cfnn9EOi.js +0 -7144
  54. package/dist/chunks/worker-client.stub-BQVZfaLd.js +0 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,160 @@
2
2
 
3
3
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [1.3.0] - 2026-06-08
6
+
7
+ **Vue 2 兼容子入口 + 三壳 UI 1:1 复刻 + 独立 dev scripts** — Vue 2.7+ / Vue 3 / React 三个壳视觉与交互完全一致 (Vue 3 SFC 是参考实现 Standard, Vue 2 / React 1:1 复刻).
8
+
9
+ ### 新增
10
+
11
+ - **`ooxml-excel-editor/vue2` 子入口** (`src/vue2/ExcelViewer.ts`, ~1000 行 render function 版): Vue 2.7+ 兼容壳, 跟 Vue 3 壳 1:1 功能对齐
12
+ - 全部 28 项 props (src / workbook / jsonOptions / templateFile / fileName / theme / cellStyle / cellImageFit / imageLightbox / openLinks / plugins / toolbar / editable / cellReadOnly / readOnlyRanges / editableTargets / strictDimensions / readOnlyCellStyle / editor / recalc / formulaEngine / contextMenu / exportProgress / transformModel / templateName)
13
+ - 全部 15+ events 跟 Vue 3 同名 (rendered / error / progress / cell-click / cell-dblclick / selection-change / sheet-change / hyperlink-click / cell-change / edit-start / edit-commit / dim-change / dirty-change / image-change / struct-change / permission-denied / before-context-menu / context-menu)
14
+ - 完整命令式 API (80+ 方法) 跟 Vue 3 `viewerApi` / React `ExcelViewerHandle` 对齐
15
+ - 插件系统完整 (events / cellStyle / theme / overlay / toolbar / setup / contextMenu / editor)
16
+ - **独立 dev scripts**: `npm run dev:vue3` (port 5300, 默认) / `npm run dev:react` (port 5301) / `npm run dev:vue2` (port 5302), 三个 demo 进程隔离 + 独立端口
17
+
18
+ ### UI 1:1 复刻 (★ 新中心原则)
19
+
20
+ **Vue 3 SFC 是参考实现 (Standard)**, 任何 UI 变动先 Vue 3 落地再复刻到 Vue 2 / React. 三壳 + 三 demo 视觉/交互对齐:
21
+
22
+ - **顶部 ViewerToolbar**: 文件名 + " · 模板: <name>" + 灰色 "N 个工作表" + "导出 ▾" 下拉 (PNG / PDF 位图 / PDF 矢量 / 打印 / 导出设置…) + 缩放 [− / select / +]
23
+ - **Action ToolBar**: 完整 9 项内置工具 (find / filter / clear-filter / copy / wrap-text / template / image-tools / freeze / export / zoom) + SVG 图标 (`src/components/toolbar-icons.ts`) + 下拉子菜单 + separator 分组
24
+ - **状态栏**: 选区范围 (A1:E5) + 计数 / 求和 / 平均 / 最大 / 最小 (调 `renderer.selectionStats`)
25
+ - **cell tooltip**: `onTooltip` hook → ref<TooltipState>, 支持 default / comment 黄底批注样式
26
+ - **ExportProgressOverlay**: 居中模态 + stage 标签 + 进度条 + 取消按钮 + `chain()` 包装所有 export API + `:export-progress` prop
27
+ - **ExportDialog**: 高级导出配置 (范围 / 清晰度 / 内容 / PDF 类型 / 纸张) — 工具栏 "导出设置…" 弹出
28
+ - **overlay scoped slot**: `<template v-slot:overlay="{ rectOf, rectOfRange, tick }">` 跨壳同 API
29
+ - **loading / error / empty 三态浮层**: progress bar + 错误提示 + 空状态
30
+ - **三 demo 入口** (App.vue / vue2-demo / react-demo) **绿色头共享** `src/demo-shared/demo-bar.css`:
31
+ - 共同按钮: 选择 .xlsx / 加载示例 / JSON 示例 / 编辑模式 / PDF(页码+水印) / 数据→JSON / 贴合 select / ↓XLSX / ↓CSV / ↓JSON
32
+ - 编辑模式按钮组: 设置可编辑 (EditTargets 白名单 dialog) / 高亮只读 / B 加粗选区 / 合并 / 拆分 / 背景 / 字体 / 清除填充 / 整表嵌入 / 格→图 / +行 / -行
33
+ - "⋯ 更多" 溢出折叠 (ResizeObserver + 测量行)
34
+
35
+ ### 修复
36
+
37
+ - **Vue 2 patch 复用 DOM 致 controller stale 引用 (致命空白渲染 bug)**: Vue 2 patch children 算法没 key 时按 tag 匹配, 把 `.ov-render-area` div 复用成 `.ov-toolbar` (改 className + 替换内容), controller.els.renderArea 变 stale (指向 toolbar 元素), 内部 canvas/overlays 都被 Vue 一并清掉. **修复**: 所有 chrome 子节点加唯一 `key` + canvas/overlays/scroller/editor-slot 改 imperative DOM (onMounted createElement + appendChild, Vue 完全不碰)
38
+ - **chrome computed 读 renderTick 导致滚动卡顿**: `fbCanEdit` / `formulaBarEditString` / `renderActionToolbar` 读了 `renderTick.value`, 每帧 scroll → root render function 重跑 → 整个 chrome DOM patch. **修复**: 只依赖低频 selVersion/findVersion/filterVersion
39
+ - **`updated()` 钩子内改 reactive → 死循环**: `demoRemeasure()` 改 `demoItemWidths` 触发 re-render → 又 `updated()` → CPU 100% 卡死. **修复**: 改用 `watch` 监听具体根本依赖
40
+ - **rAF 等浏览器 layout**: chrome 刚加上时 `renderArea.clientHeight` 是中间态 36px (不是最终 537px), `await nextTick()` 后加 `await rAF` 保证 layout 完成再 measure
41
+
42
+ ### 已知限制
43
+
44
+ - `dist/vue2.js` 424 KB (含内嵌 core) — Vue 2 build pass 独立, 不共享 `chunks/`。后续探索 rollup 多入口共享 chunk
45
+ - `dist/vue2.d.ts` 类型声明暂未生成 (vue-tsc 不认 Vue 2 SFC; 本入口是 .ts 后续可补)
46
+ - Vue 2 e2e 覆盖待补 (Playwright 多 entry 配置)
47
+
48
+ ### 文档
49
+
50
+ - 新 [docs/Vue2.md](docs/Vue2.md) — Vue 2 子入口完整使用文档 + 跟 Vue 3 差异表 + Vue 2 壳特殊坑(函数 ref / ResizeObserver / updated 死循环 / template v-for key)
51
+ - [README.md](README.md) 加 Vue 2 入口章节 + 四个子入口对照表 + 三壳 UI 1:1 说明
52
+ - [ARCHITECTURE.md](ARCHITECTURE.md) 加 demo-shared / 三壳对齐 / 第 7 中心原则段
53
+ - [CLAUDE.md](CLAUDE.md) 加第 7 中心原则 (UI 1:1 复刻) + UI 1:1 工作流 + Vue 2 壳特殊坑 + 路线图更新
54
+
55
+ ### Commits (累计 9 个)
56
+
57
+ `ba6470c` 修空白渲染 + dev scripts → `0f9718a` statusbar/tooltip/ExportProgressOverlay → `d74c9f1` ExportDialog/overlay slot → `e735e8f` Vue 2 1:1 工具栏 → `b075e05` React 1:1 工具栏 → `5462f5e` demo 绿色头 + 演示按钮 → `ee199c3` EditTargets dialog 迁移 → `411e328` Vue 2 ⋯ 更多溢出折叠 → `a312222` 修死循环
58
+
59
+ ---
60
+
61
+ ## [1.2.1] - 2026-06-08
62
+
63
+ **WPS 风格长文本编辑** — 默认编辑器从 `<input>` 升级 `<textarea>`, 输入长文本自动换行 + 向下撑高, 跟 WPS / Excel 用户习惯一致。
64
+
65
+ ### 修复 + 改进
66
+ - **★ 默认编辑器换 textarea + 动态撑高(WPS 风格, 2026-06-08)** — 用户反馈 / 截图: 编辑长文本时, 旧 `<input>` 单行水平溢出被裁切, 看不到全文; WPS 同场景**编辑框向下浮起 + 自动换行**显示完整内容。修复:
67
+ - [default-editor.ts](src/core/edit/default-editor.ts) `<input>` → `<textarea>`, `white-space:pre-wrap` + 行高同步渲染层 `LINE_HEIGHT_FACTOR=1.18`. 短文本 (`rows=1`) 视觉跟之前 `<input>` 一致, 不破坏短输入体验. **Shift+Enter** 插入换行, 普通 **Enter** 提交 (跟 Excel/WPS 一致).
68
+ - 新钩子 `CellEditorReturn.getDesiredHeight(widthPx) → number` + `CellEditorContext.reposition()` — 编辑器 input 事件触发 reposition, host 重测撑高. 用 [text.ts 的 `wrapLines`](src/core/render/text.ts) 复用渲染层换行算法, 保证视觉一致.
69
+ - [editor-host.ts](src/core/edit/editor-host.ts) `position()` 新逻辑: 宽度仍 = 列宽 (跟 WPS 一致, **仅向下溢出**); 高度 = `max(单元格原高, getDesiredHeight(列宽))`, 上限 viewport 一半 (防一格输入 10000 字撑爆屏).
70
+ - [`.editor-slot`](src/components/ExcelViewer.vue) + [`.rxl-editor-slot`](src/react/excel-viewer.css) CSS `overflow: hidden → visible`, 让编辑器可溢出原格. z-index:6 仍最上层, 不影响下方网格 / 冻结窗格 / 滚动交互.
71
+ - **提交后行高保持原样** (跟 WPS 一致, 不持久化撑高): 短期视觉浮起仅在编辑期, 提交后单元格还原. 如需永久撑高, 用户单元格设 `wrapText=true` 走已有 autofit (历史行为不变).
72
+ - **★ 公式栏 (fx 内容条) 长文本自动撑高 (2026-06-08)** — 用户反馈: 公式栏内容过长被 `text-overflow:ellipsis` 截断, 看不全公式 / 长文本. 修: Vue + React 壳公式栏 `<input>` → `<textarea>` + `auto-resize` (内容变化时 height = scrollHeight). 跟单元格编辑器一样, 普通 Enter 提交、Shift+Enter 插换行、上限 ~6 行 (max-height: 108px) 超过内部滚动. CSS 同步 — formula-bar 高度自动撑, addr / fx 区垂直居中.
73
+ - **★ vAlign 溢出 fallback 顶对齐 (跟 WPS 一致, 2026-06-08)** — 用户反馈: 输入长文本提交后, 单元格"显示文末而不是开头". 根因: [canvas-renderer.ts:1090](src/core/render/canvas-renderer.ts#L1090) 默认 vAlign='bottom' 时, 文本总高超过单元格高 → `startY = y + h - pad - totalH` 变负, 首行画到格外, 用户看到的是最后几行. 修: 当 `totalH > availH` 时, 强制走 `'top'` 分支 (显示文头, 末尾裁切). 跟 WPS / Excel 行为一致.
74
+ - **`CellEditorFactory` 返回类型扩展** — 旧 `HTMLElement | { el, destroy? }` → 新增可选 `{ el, destroy?, getDesiredHeight?(w) }`. 旧自定义编辑器 100% 向后兼容 (不返 `getDesiredHeight` = 高度锁单元格高, 老行为).
75
+ - **`CellEditorContext.reposition()`** — 给自定义编辑器: 主动通知 host 重测撑高. 内容变化后调一次. 不调 = 不撑 (老行为).
76
+
77
+ ### 测试
78
+ - 新单测 [default-editor.test.ts](src/core/edit/__tests__/default-editor.test.ts) 10 项: textarea 类型 / commit-cancel 行为 / Enter 移动 / Shift+Enter 换行 / 失焦提交 / 样式贴合 / `getDesiredHeight` 钩子存在 / 兜底 (依赖 canvas measureText 的具体撑高数测试走 e2e 覆盖)
79
+ - 新 e2e [edit-long-text.e2e.ts](e2e/edit-long-text.e2e.ts) Vue + React 双覆盖 12 项: 编辑器 textarea / 短文本不撑大 / 长文本撑高 / 动态变化 / Esc 取消 / Shift+Enter 换行 + Enter 提交
80
+ - devDependency 加 `jsdom` (单元格编辑器单测依赖 DOM 环境;**仅 dev**, 不影响 dist)
81
+
82
+ ---
83
+
84
+ ## [1.2.0] - 2026-06-08
85
+
86
+ **主线**: 只读边界三件套(白名单 / 尺寸多形态 / 视觉钩子 + permission-denied 事件)+
87
+ 模板语义重设计(样式捐赠者)+ WPS 单元格内嵌图(展示 / 互转 / 导出往返)+ 富粘贴 +
88
+ 图片放大下载 + 虚拟空行 + 公式栏可编辑 + 背景/字体色 + 编辑 UX 补齐(合并/粘贴/右键菜单)+
89
+ 性能 + 导出错误可见性 + 1900/1904 日期单测。**全部向后兼容、默认只读零回归**。
90
+
91
+ 测试基线: 306 单测 / 107 e2e。
92
+
93
+ ### 修复 + 新增
94
+ - **★ 只读边界三件套(2026-06-08;三次提交)** —— 用户三连问"可编辑是一切改变的基础", 怀疑有路径绕过 isEditable. 三阶段解决:
95
+ - **Phase A 闸门补漏 + `permission-denied` 事件**(commit 57ebdbc):审计找出 4 个绕过点 — `pasteRich` 合并粘贴 / `pasteRich` 图片粘贴 / `mergeCells/unmergeCells` 没逐格检查 / 图片转换族 没检查目标格.全部修复.行为 = 默认 skip(跟 `editRange` 一致)+ 一次操作结束 emit 一次 `permission-denied: { reason, cells[], dims?, message? }`(8 种 reason).Vue `@permission-denied` / React `onPermissionDenied`.右键菜单 disabled 完善:粘贴/清除/换行 = 选区有至少 1 格可编辑;合并/拆分/删除行列 = 全部可编辑.新 helper `partitionByEditable` / `rangeAllEditable` / `collectDeniedInRange`.
96
+ - **Phase B 尺寸 API 多形态 + `strictDimensions`**(commit fac634d):用户问"现 API 是否能设置不相邻一批列宽行高".新类型 `DimTarget = number | number[] | { from, to }` 自动识别.升级 `setColumnWidth(target, width)` / `setRowHeight(target, height)` 接 union,**返回值 boolean → number 成功条数(BREAKING)**;新增 `autoFitColumns/Rows(target?)` / `resetColumnWidth/RowHeight(target)` 公开 API(target 不传 = 整表).多 index 时聚合成单次 `restore-wb` undo.新 prop `:strictDimensions` 默认 `false`:`true` 时该列/行至少 1 格在白名单内才能改尺寸,跟"白名单未覆盖 = 完全只读"严格语义一致.新 helper `canEditDimension` / `normalizeDimTarget`.
97
+ - **Phase C 只读视觉钩子 + cursor**(本次):`CellStyleFn` 签名加可选第 3 入参 `ctx: CellStyleCtx { editable: boolean }`,旧 `(cell, pos) => ...` 完全兼容.新 prop `:readOnlyCellStyle: boolean | CellStyleOverride | CellStyleFn` — `true` 套内置浅灰 `#f5f7fa`,对象 = 固定样式,函数 = 按格自定义,默认 `false` 无视觉差异.canvas-renderer 渲染顺序:base style → cellStyle(传 ctx)→ 只读格再叠 readOnlyCellStyle.控制器注入 `isEditable` 回调给 renderer;鼠标悬停只读格(且编辑模式开)自动 `cursor: not-allowed`.Demo 加「高亮只读」toggle 跟「设置可编辑」配合使用.
98
+ - **测试**:permissions.test.ts 共 27 项(11 项白名单 + 4 项 Phase A helpers + 7 项 Phase B canEditDim/normalizeDim);新单测 `readonly-style.test.ts` 7 项;3 个新 e2e spec — `permission-denied.e2e.ts` 4 项 / `dimension-targets.e2e.ts` 6 项 / `readonly-visual.e2e.ts` 3 项.总 281 单测 + 101 e2e 全绿.
99
+ - **★ 可编辑白名单 `editableTargets`(2026-06-08)** —— 现有的 `cellReadOnly` / `readOnlyRanges` 是**黑名单**(默认全可编辑,标只读),新加一个 **白名单**(默认全只读,标可编辑)。用户原话:"设置不相邻的一批单元格可编辑,暴露的 API 应该支持多种参数,可以传区域,也可以传单个单元格,多个单元格,行,列;没有设置的就是只读"。
100
+ - **新类型** `EditableTarget`(`core/edit/types.ts`)联合 4 种形状,**自动识别**带哪些字段:`{row,col}` 单格 / `{row}` 整行 / `{col}` 整列 / `MergeRange` 矩形。单值或数组都接,**允许不相邻**多 target
101
+ - **新 prop** `:editableTargets` —— `undefined` (不传) = 不启用白名单 = 老行为;`[]` (显式空) = 全只读;非空 = 白名单生效。优先级:`editable=false` ► 不在白名单 ► `readOnlyRanges` ► `cellReadOnly` ► 否则可编辑(白名单内仍能被黑名单二次"黑"掉)
102
+ - **新命令式 API** `viewer.setEditableTargets(targets)` / `viewer.getEditableTargets()` —— 运行时改不动 prop,直接覆盖 `editCfg.editableTargets`,立即重绘
103
+ - **demo 加「设置可编辑」按钮 + 弹窗**(编辑模式下出现):12×8 网格化点选,可单击格 / 列标题 / 行号切换;footer 显示已选数;「应用」生效 / 「关闭白名单」恢复默认 / 「取消」放弃
104
+ - **测试**:`permissions.test.ts` 加 11 项白名单分支(单格 / 多格不相邻 / 整行 / 整列 / 矩形 / 空数组 / 混合 4 种 / 与黑名单叠加 / editable=false 时白名单也无效 / 形状识别);新 `e2e/editable-targets.e2e.ts` 3 用例(命令式 setEditableTargets 4 种 target 形状 / Demo 弹窗点选→应用 / 空数组全只读)
105
+ - 不破坏现有 API,完全 additive
106
+ - **★ 模板语义彻底重设计(2026-06-08)—— 从"占位符 + 锚点表"改为"样式捐赠者"** —— 之前几轮"占位符 + 锚点表"语义反复改: `discardUnmatched` → `trimUnused` → JSON 自然位置… 都没切中用户实际需求。用户原话:**"模板这个功能,只在 json/csv 这些本身不附带格式的数据源才生效,xlsx 数据源根本不需要"**。新语义:
107
+ - **`:templateFile` = 样式捐赠者** —— 模板贡献 styling(`styles` 池 / `merges` / 列宽 / 行高 / `freeze` / `theme`),JSON / CSV 数据**在 A1 自然位置渲染**,模板的 raw 文字 / 占位符 / 图 / 图表 / 条件格式 / 数据验证 **全部丢弃**(避免幽灵规则)
108
+ - **仅在 `:workbook` (JSON / 模型) 数据源下生效** —— xlsx 数据源(`:src`)自带格式,给 `:templateFile` 会被**忽略并 console.warn**;工具栏「模板 ▾」在 xlsx 模式下**禁用**
109
+ - **API 改动(BREAKING)**:删除 `props.template` (`TemplateFillSpec`)、删除 `viewer.applyTemplate(spec)` 命令式 API、删除 `core/template/fill.ts` (`fillTemplate` / `replacePlaceholders` / `TemplateAnchor` / `TemplateFillSpec`) 整个模块。新增 `core/template/style-overlay.ts` 的 `applyStyleTemplate(dataWb, templateWb)` 纯函数(同步,无 onProgress / signal)
110
+ - **核心契约 9 项单测**(`style-overlay.test.ts`):数据 raw 全部保留在自然位置 / 模板 raw 全部丢弃 / styleId 从模板同位置取 / merges + 列宽 + 行高 + freeze 全部从模板拷贝 / 模板的 images + charts + conditional + dataValidations 不带过来 / 数据 sheet 名 + date1904 + cellImages 透传 / 入参不被原地修改 / 空数据 + 模板 → 干净样式骨架 / 数据 dimension 跟模板列宽行高声明取大
111
+ - **demo 简化**:`App.vue` 删 `templateSpec` 计算属性(placeholders + anchors 整套不要),`loadJsonSample` 只传 `:workbook` 数据数组;`:fileName="'订单数据"`。点工具栏「模板 ▾ → 导入 .xlsx 模板」即套样式,模板的标题/客户/合计/{{占位符}} 全部不见, JSON 5 条数据仍在 A1 起的自然位置
112
+ - **e2e 全重写**(`template-switch.e2e.ts`):3 个用例 —— ① JSON 无模板时数据在 A1 自然位置 ② JSON + 模板时 模板装饰文字全部不见 + JSON 仍在 A1 + 切换/清除模板正常 ③ xlsx 数据源加载后工具栏「模板 ▾」禁用
113
+ - **过渡 / 后续迁移**:用户用旧 `applyTemplate(spec)` 自填数据的场景,改为"前端构造好完整 JSON 后再传 `:workbook`"。后续如需"模板填值"那套(不入命令栈的占位符 / 锚点表写入),应该走"应用层"而非组件 prop,组件只管渲染样式
114
+ - **右键菜单全面开放(Plan C)** —— 1.2.0 前右键菜单是内置 hardcoded、无任何对外接口。此版三层开放:
115
+ - **prop `:contextMenu`** —— `false` 关闭内置弹层(事件仍触发,供自渲染);`(ctx, items) => MenuItem[] | undefined` transform 回调,在内置 items 上加 / 减 / 重排
116
+ - **事件 `@before-context-menu` / `@context-menu`**(Vue) / `onBeforeContextMenu` / `onContextMenuShow`(React)—— `payload.preventDefault()` 接管渲染(用 Element Plus / Radix 等自渲染);`@context-menu` 拿到 `{x, y, ctx, items}` 总会触发
117
+ - **命令式 API** `openContextMenu(x, y, items?)` / `closeContextMenu()` —— 键盘 Shift+F10、工具栏触发等
118
+ - **插件贡献** `definePlugin({ contextMenu: (ctx, items) => ... })` —— 多插件按数组顺序串行,组件 prop 最后覆盖
119
+ - 顺序固定:**内置 → 插件 → prop → 事件**;`MenuItem` / `ContextMenuCtx` / `ContextMenuTransform` 全部从 `/core` 导出
120
+ - 顺手修了 Vue 4-pkt 接 prop 的"boolean prop 缺省被 Vue 判成 false"小坑(withDefaults 显式 undefined)
121
+ - **内置导出进度遮罩 + 三层覆盖机制(P1.5)** —— P1 已建好 `onProgress` + `AbortSignal` 协议,但壳没接 UI,用户调 `viewer.downloadPdf()` 看不到任何反馈 → 这次补 **Shell 默认 UI** + 用户**逐层覆盖**:
122
+ - ① 新组件 `ExportProgressOverlay`(Vue SFC + React tsx)居中模态:stage 标签 + 进度条 + 取消按钮;同视觉 / 同协议
123
+ - ② 壳自动 wrap 长任务(`downloadPdf`/`exportPdf`/`downloadImage`/`exportImage`/`downloadXlsx`/`exportXlsx`/`print`/`convertImagesInRangeToCell`/`convertCellImagesInRangeToFloat`):建内置 `AbortController` + 接 `onProgress` → 用户调时**默认看见遮罩**,无需任何 prop
124
+ - ③ 用户传 `{ onProgress, signal }` 仍正常**链回调**(并存)
125
+ - ④ 覆盖:`:export-progress="false"` 关闭内置遮罩(纯回调);Vue `#export-progress` 插槽 / React `renderExportProgress` 自渲染(拿到 `{state, busy, cancel}`)
126
+ - ⑤ **修单表 PDF 卡 0% bug**:核心导出对"单表"和"jsPDF/canvasToBlob 黑盒"阶段改 emit `ratio: undefined`,overlay 走 indeterminate 扫动条动画(看着在动);多表仍按 `i/total` 走离散进度
127
+ - ⑥ 1.2.0 起 `convertImagesInRangeToCell` / `convertCellImagesInRangeToFloat` 在壳侧返 `Promise<number>`(为接遮罩),core 内核仍同步
128
+ - **JSON 直渲(P3)** —— 新 prop `:workbook` 接 `WorkbookModel | JsonInput`(优先于 `:src`),绕过 parser 直接构造模型渲染。三种 JsonInput shape 自动识别:① 二维数组(首格 A1) ② 对象数组(首行表头 = keys) ③ `{sheets:[{name,rows,...}]}`。类型自动推断:数字字符串 → number、`TRUE`/`FALSE` → boolean、ISO 日期串 → Date(可关 `:jsonOptions="{ autoInfer: false }"`)。新公开导出 `jsonToWorkbook` / `isWorkbookModel` 给"仅引擎"用户。
129
+ - ~~**模板填值(P3)** — `props.template` (`TemplateFillSpec`) + `applyTemplate(spec)`~~ — **已废弃**, 改为本版 1.2.0 顶部的"模板样式 overlay"语义. 见上文 ★ 重设计条目.
130
+ - **图片浮动 ⇄ 嵌入 选区批量 + 工具栏入口** —— 新 API `convertImagesInRangeToCell(range)` / `convertCellImagesInRangeToFloat(range, size?)` 把"选区内"批量互转,聚合成单次撤销(复用 `convertImagesToCells` 范式 + 新增 `convert-to-floats` exec kind)。Vue 工具栏新内置 `image-tools` 下拉(选区/整表/整列 浮动 ⇄ 嵌入)发现性翻倍;右键菜单多格选区时直接出现「选区浮动图全部嵌入(N 张) / 选区内嵌图全部浮动化(N 张)」。
131
+ - **Cell Inspector(单元格全息体检)** —— 新 API `inspectCell(row,col)`,在 `getCellSnapshot` 之上聚合 **合并区**(`merge`/`isMergeAnchor`)、**覆盖到该格的浮动图**(`floatingImages[]`,从 `sheet.images[].from..to` 反推)、**WPS 内嵌图 DISPIMG**(`cellImage`)、**数据验证范围命中**(`dataValidation`)、**条件格式命中**(`conditional[]`,含规则索引 + 等效样式;`ConditionalEngine.inspectHits` 共享 evaluator,跟渲染层同一份)、超链接、批注。框架无关 `src/core/model/inspect.ts`,headless 流程也可调。
132
+ - **长任务进度回调 + AbortSignal 取消** —— 所有耗时操作(`exportImage` / `exportPdf` / `print` / `exportXlsx`)统一接 `onProgress?: (p: ExportProgress) => void` + `signal?: AbortSignal`。串行多表导出 + 每页/每表 emit `{stage,sheetIndex,ratio,label}` + `await yieldToEvent()` 让出 UI(防假死)。任意时刻 `ctrl.abort()` → 下一个调度点抛 `DOMException('Aborted','AbortError')`(标准语义),上层用 `e.name === 'AbortError'` 区分取消与真错。新 helper `src/core/export/abort.ts`(`yieldToEvent` / `checkAborted` / `isAbortError`)。
133
+ - **自动换行(WPS 风格 toggle)**:工具栏内置 `wrap-text` + 右键菜单「自动换行」+ 命令式 API `toggleWrapTextOnSelection()` / `getSelectionWrapState()`(`'all'/'none'/'mixed'`)。行为对齐 WPS:选区**全已换行 → 全部关掉**;否则**全部打开**;mixed → all。行高自动按内容重撑(失效 autofit 缓存触发),延续"只扩不缩"语义。入命令栈单次撤销 style。两壳同构(Vue 工具栏 `wrap-text` builtin + 右键;React 通过命令式 API + 右键)。
134
+ - **HiDPI/系统缩放 canvas 对齐**:Windows 125%/150% 缩放、浏览器 Ctrl+缩放(`devicePixelRatio≠1`)下,canvas 作为"替换元素"默认会按 `width*dpr` 显示,导致整个网格被放大、与 DOM 叠加层(浮动图/图表/HTML 文本框)及鼠标命中错位(越往右下偏得越多)。修复:`render()` 显式把 canvas 的 CSS `width/height` 钉成 view 逻辑尺寸,缓冲仍是 dpr 倍(高清),被浏览器降采样显示 → 像素与逻辑坐标 1:1 对齐。
135
+ - **富粘贴(从 Excel/WPS 复制整块)**:`Ctrl+V` 现在优先读剪贴板 **text/html**,完美解析**字体/颜色/填充/边框/对齐/合并单元格**(与 WPS 一致),整块**单次撤销**(整簿快照逆)。值优先取 Excel 的 `x:num`/`x:fmla`(原始值),否则取文本交类型推断。无 html 时回退原 TSV(`pasteText`,不变)。新 `clipboard-html.ts parseClipboardHtml`;`EditController.pasteRich`;API `pasteRichHtml(html,at?)`。
136
+ - **图片(多通道,硬需求)**:① HTML 里的 data-uri `<img>` → 落格转内嵌图;② 剪贴板单张图片(`image/png` 等)→ 落活动格(`pasteImageBlob`);③ 拖图片文件进网格(消费方接 `pasteImageBlob`)。**已知边界**:Excel/WPS **区域复制的内嵌图一般进不了浏览器剪贴板**(只给 text/html+text/plain),所以"复制一整块带图、粘贴时图一起来"做不到 —— 这是浏览器固有限制,需用上述替代通道(单独复制一张图 / 拖文件)。
137
+ - **图片点击放大 + 下载原图**:网格里的图(WPS 内嵌图 DISPIMG / 浮动图)可点开看大图、下载原始字节。框架无关 `LightboxHost`(body 级暗背景 + 居中大图 + 「下载原图」+ 点背景/Esc/关闭按钮)。触发:**只读模式单击图**放大;**编辑模式右键**「查看大图 / 下载原图」(不抢选区/编辑)。新 prop `imageLightbox`(默认 true);新 API `openImageLightbox(src,fileName?)` / `getCellImageAt(row,col)`。顺带给点击加 3px 拖拽死区(微抖不再被当拖动,单击语义更稳)。修了 Vue 布尔 prop 缺省被判 false 的坑(`imageLightbox` 加进 withDefaults)。
138
+ - **虚拟空行/空列(滚动自动延伸,不动 dimension)**:滚到数据末尾下方仍有空行/空列可滚动、选中、编辑,像 Excel/WPS 的"无限网格";但**不写进 dimension/文件**(避免体积变大)——只有真去编辑某空格,它才靠 `growDimension` 变实。`GridMetrics` 加虚拟范围(`vRows/vCols/virtualWidth/Height`,封顶 Excel 上限 1048576×16384);`totalWidth/Height` 仍按 dimension(**导出/data-access 不含虚拟空行**);spacer 尺寸 / 可视区 / 命中夹取改用虚拟范围;控制器 `recomputeVirtualExtent()`(滚动/缩放/resize 时只增不减、按需延伸)。纯 core,双壳自动继承;新 API `getVirtualExtent()`。
139
+ - **背景色 / 字体色(回显 + 修改)**:新 API `getActiveFillColor()` / `getActiveFontColor()`(回显活动格当前色,#RRGGBB)+ `setSelectionFill(color|null)` / `setSelectionFontColor(color)`(改选区,入命令栈)。两壳 demo 加 WPS 风格的背景/字体取色器 + 清除填充。
140
+ - **修复内嵌图"灰底"**:DISPIMG 图加载中 / 缺登记项时,之前画 `#f2f4f7` 灰底盖住了单元格自身填充色(白),看着像默认灰。改为加载中不画底色(露出单元格白填充),仅缺图时画个淡图标、不盖色。
141
+ - **公式栏(Fx 内容条)可编辑 + 联动**:顶部公式栏从只读改为可编辑 `<input>`(editable + 该格非只读时)。在栏里输入提交(回车下移、Esc 取消、失焦提交)→ 改活动格;切选区 / 格内编辑 → 栏即时反映。栏显示**可编辑字符串**:公式 `=...`、数值原始数字串(非格式化,避免编辑货币/千分位被当文本)、布尔 TRUE/FALSE。新 API:`getCellEditString()` / `canEditActiveCell()` / `commitActiveCellValue(value, move?)`。仅值真变化才入命令栈。两壳同构(React 顺带补:`cell-change` 触发 chrome 重渲)。
142
+ - **合并/拆分单元格**:`mergeCells(range)`(吸收相交旧合并、清空被覆盖格只留左上锚点)/ `unmergeCells(range)`,入命令栈可撤销。
143
+ - **粘贴**:`Ctrl+V` / `pasteText(text, at?)` —— TSV(制表符+换行)→ 区域写入,类型自动推断、跳过只读、整块一次撤销。
144
+ - **右键上下文菜单**:框架无关 body 级 DOM 菜单 —— 插入/删除行列、合并/拆分、清除内容、复制/粘贴;点外部/Esc/滚动关闭、贴边翻转。只读仍用浏览器默认菜单。
145
+ - **WPS 单元格内嵌图(DISPIMG)**:
146
+ - **展示**:解析 WPS 私有件 `xl/cellimages.xml` + 单元格 `=DISPIMG("id",1)` 公式,把图按行高列宽画进单元格内(随网格滚动/裁剪/冻结/缩放),普通工具打不开的 WPS 内嵌图现在能正常显示。新 `parser/cell-image-parser.ts`;模型加 `WorkbookModel.cellImages` + `CellModel.dispImgId`;canvas 渲染带图片解码缓存 + onload 重绘。
147
+ - **贴合方式可配置** `cellImageFit`:`contain`(默认,等比缩放,与 WPS 渲染一致——WPS 打开导出文件时 DISPIMG 固定按 contain 显示)/ `fill`(拉伸铺满随格变形)/ `cover`(等比裁剪铺满),两壳 prop + `setCellImageFit()` 运行时切换、即时重绘。
148
+ - **行 customHeight 保真**:解析 `<row customHeight="1">` 标记(ExcelJS 不暴露),自动行高跳过手动设高的行——避免长文本把"作者设矮放图"的行撑大,渲染/导出行高都与 WPS 一致。顺带放宽 fast-xml-parser 实体展开上限(大表几百个 `&quot;` 会撞默认 1000 上限致 drawing/row-meta 解析静默失败)。
149
+ - **就近 / 批量嵌入**:`convertImageToCellAuto(imgIdx)`(图压在哪格就嵌哪格,几何反推)/ `convertAllImagesToCells(col?)`(整表或整列批量,一次进撤销栈)/ `convertCellImageToFloat(row,col,size?)`(嵌入→浮动);右键单格菜单为「将此处浮动图嵌入 / 整列嵌入(N 张)/ 整表嵌入(N 张)/ 内嵌图转浮动图」。两向入命令栈(整簿快照逆)、发 `cell-change`/`image-change`、翻脏标记。`getCellImages()` 读登记表。`convertImageToCell(imgIdx,row,col)` 保留(显式目标格)。
150
+ - **导出往返**:ExcelJS 写出后在 zip 层回注 `cellimages.xml` + rels + media + `[Content_Types].xml`/`workbook.xml.rels` 补丁(新 `export/wps-cellimages.ts`,从模型重建),rebuild / overlay 两模式均覆盖。原有的 + App 内新转的内嵌图导出后用 WPS 打开都显示。验证:解析→导出→再解析 往返存活(单测)。
151
+ - **逐字节对齐真·WPS(修正 #REF!)**:首版用了 `cellimages+xml`(复数)内容类型 + `2017/etCustomData` 关系类型 + 空 `<xdr:spPr/>`,导致 WPS 加载不了登记表、DISPIMG 显示 `#REF!`。据真·WPS 文件实测修正为 **单数** `cellimage+xml`、关系类型 **`2020/cellImage`**、`<xdr:spPr>` 补全 `xfrm`+`prstGeom rect`、`cNvPr` 加 `descr`、DISPIMG 格缓存值 `<v>` 写为 `=DISPIMG("id",1)`。单测锁定这些字段。
152
+
153
+ ### 性能
154
+ - **undo 快照轻量化**:增删行列的整簿快照从 `structuredClone`(深拷图片字节/图表)改为手写轻量克隆 —— 只克隆编辑会动的部分,**图片字节/图表/形状/条件格式按引用共享**(编辑期间不可变),大文件 + undo 栈内存大幅下降。惠及结构编辑 undo、脏状态 baseline、还原原件。
155
+
156
+ ### 修复
157
+ - **导出失败不再静默**:React 壳「导出 PDF」之前只把错误转给 `onError` prop,未接就被吞(无反应、控制台无报错)。两壳统一为 `console.error` + 上报(emit/onError)+ alert。**提醒**:PDF 导出需可选 peer `jspdf`(`npm i jspdf`);未装时现在会明确报错。
158
+
5
159
  ## [1.1.0] - 2026-06-05
6
160
 
7
161
  把 1.0.0 编辑能力的三处已知 v1 限制做成增强(向后兼容)。
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ooxml-excel-editor
2
2
 
3
- > Vue 3 + React 高保真 **.xlsx 预览 / 编辑组件** —— Canvas 渲染,**默认只读预览,可选开启编辑**。从零实现解析与渲染,尽量还原微软 Excel 打开工作簿的观感。
3
+ > Vue 3 + **Vue 2** + React 高保真 **.xlsx 预览 / 编辑组件** —— Canvas 渲染,**默认只读预览,可选开启编辑**。从零实现解析与渲染,尽量还原微软 Excel 打开工作簿的观感。**三个壳 UI 1:1 对齐**(Vue 3 SFC 是标准,Vue 2 / React 复刻)。
4
4
 
5
5
  [English](#english) · 中文
6
6
 
@@ -9,8 +9,9 @@
9
9
  **装**(按框架二选一):
10
10
 
11
11
  ```bash
12
- npm i ooxml-excel-editor vue exceljs # Vue
13
- npm i ooxml-excel-editor react react-dom exceljs # React
12
+ npm i ooxml-excel-editor vue exceljs # Vue 3 (默认入口)
13
+ npm i ooxml-excel-editor react react-dom exceljs # React 壳 (/react 子入口)
14
+ npm i ooxml-excel-editor vue@2.7 exceljs # Vue 2.7+ (/vue2 子入口, 1.3.0+)
14
15
  ```
15
16
 
16
17
  **用**(Vue,容器要给高度;`src` 可传 `File` / `Blob` / `ArrayBuffer` / `Uint8Array` / URL 字符串):
@@ -28,18 +29,21 @@ const src = ref<File>() // 绑个 <input type="file" @change> 给它即可
28
29
  </template>
29
30
  ```
30
31
 
31
- 默认**只读预览**;想编辑加 `:editable="true"`(React 同名 `editable`)。React 写法、props/事件表、编辑 / 导出 API 见下文对应章节。
32
+ 默认**只读预览**;想编辑加 `:editable="true"`(React / Vue 2 同名 `editable`)。React / Vue 2 写法、props/事件表、编辑 / 导出 API 见下文对应章节。Vue 2 用法详见 [docs/Vue2.md](./docs/Vue2.md)。
32
33
 
33
34
  > 纯使用者只需读 **安装 / 使用 / API / 编辑 / 导出** 几节即可接入,无需看源码;类型随包发 `.d.ts`(IDE 自动补全)。「扩展 API / 插件 / 开发」是进阶,可跳过。
34
35
 
35
36
  ## 特性
36
37
 
37
- - 📊 **Canvas 高保真渲染**:DPR 高清、虚拟滚动(万行流畅)、冻结窗格四象限
38
+ - 📊 **Canvas 高保真渲染**:DPR 高清、虚拟滚动(万行流畅)、冻结窗格四象限、**滚动自动延伸空行/列**(像 Excel 无限网格,但不写进 dimension/文件)
38
39
  - 🔢 **自写数字格式引擎**:千分位/货币/百分比/科学计数/分数、四段格式(正;负;零;文本)、`[Red]` 颜色、`[>=100]` 条件段、中文日期 `yyyy"年"`、`[h]:mm` 经过时间
39
40
  - 🗓 **日期序列号**:含 Excel 1900 闰年 bug、1904 系统
40
41
  - 🎨 **主题色 + tint**、indexed 调色板、合并单元格、边框(细/粗/虚/双线)、填充(纯色/图案/渐变)
41
42
  - 🌈 **条件格式**:色阶 / 数据条 / 图标集 / cellIs / top10
42
43
  - 🖼 **图片 + 图表**(DrawingML → ECharts 近似还原)、**形状/文本框**、**迷你图**(sparklines)、**批注**、**数据验证**下拉、**自动筛选**样式
44
+ - 📌 **WPS 单元格内嵌图(DISPIMG)**:识别并展示 WPS 私有的"嵌在格里的图"(普通工具会缺图);编辑模式下支持**一键浮动 ⇄ 嵌入互转**。见 [WPS 单元格内嵌图](#wps-单元格内嵌图dispimg)
45
+ - 🔍 **图片点击放大 + 下载原图**:网格里的图(内嵌图/浮动图)点开看大图、下载原件。只读模式单击图放大、编辑模式右键「查看大图」。`imageLightbox` prop 控制(默认开),`openImageLightbox(src)` 命令式打开。
46
+ - 📋 **从 Excel/WPS 富粘贴**:`Ctrl+V` 解析剪贴板 HTML → 完美还原字体/颜色/填充/边框/对齐/合并单元格,整块单次撤销。图片走多通道(data-uri / 单图 / 拖文件);**注**:Excel 区域复制的内嵌图进不了浏览器剪贴板(浏览器限制)。
43
47
  - 📝 **文本溢出**到相邻空格、**自动行高**
44
48
  - 🖱 **交互**:单元格选区(合并感知)、拖选、公式栏、状态栏(计数/求和/均值/最值)、超链接可点、裁切文本悬停看全文、Ctrl+C 复制为 TSV、**Ctrl+F 查找**(高亮 + 上/下定位 + 计数 + 区分大小写/全字匹配)、**自动筛选**(点下拉真能筛:去重值多选 + 搜值 + 清除)
45
49
  - 🖨 **导出 / 打印**:整表/选区/多表导出 **PNG/JPEG**、**PDF**(位图 + **矢量·文字可选可搜**两种)、**系统打印**(可另存 PDF);默认还原原生 `pageSetup`(纸张/方向/页边距/缩放/打印区域/**打印标题行列每页重复**);宽表**横向跨页**(页矩阵);`beforeRenderPage` 注入页眉/页脚/水印、`configureDoc` 注册字体;内置「导出设置」对话框
@@ -52,15 +56,18 @@ const src = ref<File>() // 绑个 <input type="file" @change> 给它即可
52
56
 
53
57
  ## 安装
54
58
 
55
- 一个包,三个子入口 —— **框架无关的 core 引擎被 Vue / React 两个薄壳共享**(`dist/core.js` 只打一份)。按你的框架装对应 peer:
59
+ 一个包,**四个子入口** —— 框架无关的 core 引擎被 Vue 3 / React 两个壳共享(`dist/core.js` 只打一份),Vue 2 因 SFC 编译器跟 Vue 3 冲突独立打包(内嵌 core)。按你的框架装对应 peer:
56
60
 
57
61
  ```bash
58
- # Vue 项目
62
+ # Vue 3 项目
59
63
  npm i ooxml-excel-editor vue exceljs
60
64
 
61
65
  # React 项目
62
66
  npm i ooxml-excel-editor react react-dom exceljs
63
67
 
68
+ # Vue 2.7+ 项目 (1.3.0+)
69
+ npm i ooxml-excel-editor vue@2.7 exceljs
70
+
64
71
  # 只解析 / 读数据 / 导出(不渲染 UI)
65
72
  npm i ooxml-excel-editor exceljs
66
73
 
@@ -70,15 +77,18 @@ npm i echarts jspdf
70
77
  npm i hyperformula
71
78
  ```
72
79
 
73
- 三个入口:
80
+ 四个入口:
74
81
 
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` |
82
+ | import | 内容 | 需要的 peer | 体积 (gzip) |
83
+ |---|---|---|---|
84
+ | `ooxml-excel-editor` | **Vue 3** 组件 `<ExcelViewer>` (参考实现 Standard) | `vue@3` + `exceljs` | ~19 KB + 共享 chunks |
85
+ | `ooxml-excel-editor/react` | **React** 组件 `<ExcelViewer>` (1:1 复刻 Vue 3) | `react` + `react-dom` + `exceljs` | ~11 KB + 共享 chunks |
86
+ | `ooxml-excel-editor/vue2` | **Vue 2.7+** 组件 `<ExcelViewer>` (1:1 复刻 Vue 3) | `vue@2.7` + `exceljs` | ~124 KB (内嵌 core) |
87
+ | `ooxml-excel-editor/core` | 框架无关引擎(解析/渲染/控制器/导出/读数据) | `exceljs` | ~1 KB + 共享 chunks |
80
88
 
81
- `exceljs` 必需;`vue` / `react` / `react-dom` 按框架二选一(均为可选 peer);`echarts` / `jspdf` / `hyperformula` 为**可选** peer —— 未装分别只影响"图表渲染""PDF 导出""公式重算",其余正常,且**绝不打包进你的产物**(运行时才动态加载)。
89
+ `exceljs` 必需;`vue` / `react` / `vue@2` 按框架三选一(均为可选 peer);`echarts` / `jspdf` / `hyperformula` 为**可选** peer —— 未装分别只影响"图表渲染""PDF 导出""公式重算",其余正常,且**绝不打包进你的产物**(运行时才动态加载)。
90
+
91
+ > **三壳 UI 1:1**: Vue 3 SFC 是参考实现 (Standard), Vue 2 / React 1:1 复刻视觉与交互 (工具栏 SVG 图标 / 下拉子菜单 / 公式栏 / 状态栏 / dialog / 浮层 / 演示 demo 全部对齐). 详见 [docs/Vue2.md](./docs/Vue2.md) 跟 Vue 3 的差异速查 + [CLAUDE.md](./CLAUDE.md) 第 7 中心原则。
82
92
 
83
93
  > ⚠️ **公式重算的许可证**:默认公式引擎是 [HyperFormula](https://hyperformula.handsontable.com/),**GPL-3.0 / 商业 双授权**。本组件以 `licenseKey: 'gpl-v3'` 调用(适合开源/GPL 场景)。**商业闭源项目**请改用 `formulaEngine` prop 注入你自己持有商业 license 的引擎(或自研引擎),只需实现 `FormulaEngine` 接口即可。不开启 `recalc` 时完全不加载 hyperformula,无许可证负担。
84
94
 
@@ -133,6 +143,40 @@ export function Preview() {
133
143
  }
134
144
  ```
135
145
 
146
+ ### Vue 2 (1.3.0+)
147
+
148
+ 跟 Vue 3 1:1 复刻 (工具栏 / 公式栏 / dialog / 浮层 / events / API 全对齐). Vue 2 用 Options API + Composition API 都行:
149
+
150
+ ```html
151
+ <template>
152
+ <ExcelViewer
153
+ ref="viewer"
154
+ :src="src"
155
+ :file-name="fileName"
156
+ :editable="editMode"
157
+ style="height: 100vh"
158
+ @rendered="(wb) => console.log('已渲染', wb.sheets.length, '个工作表')"
159
+ @cell-change="(p) => console.log(p.before.text, '→', p.after.text)"
160
+ />
161
+ </template>
162
+
163
+ <script>
164
+ import ExcelViewer from 'ooxml-excel-editor/vue2'
165
+ import 'ooxml-excel-editor/style.css'
166
+ import 'ooxml-excel-editor/vue2.css'
167
+
168
+ export default {
169
+ components: { ExcelViewer },
170
+ data: () => ({ src: null, fileName: '', editMode: false }),
171
+ methods: {
172
+ download() { this.$refs.viewer.downloadXlsx() },
173
+ },
174
+ }
175
+ </script>
176
+ ```
177
+
178
+ 完整 Vue 2 用法 + 跟 Vue 3 的差异表 见 [docs/Vue2.md](./docs/Vue2.md)。
179
+
136
180
  ### 仅引擎(不渲染 UI)
137
181
 
138
182
  ```ts
@@ -192,6 +236,44 @@ viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区
192
236
 
193
237
  > 想要更底层的渲染模型,仍可直接用 `parseWorkbook` 的返回值 / `getWorkbook()` / `@rendered`(`WorkbookModel`:`sheets[].cells: Map<"row:col">`、`styles[styleId]`)。
194
238
 
239
+ ## UI 区域速查(给调用方 & 二开者)
240
+
241
+ `<ExcelViewer>` 渲染出的 chrome 自上而下分这几块,每块都有**名字 / DOM 选择器 / 文件 / 替换方式**,改样式或替换某块时按这张表找位置。
242
+
243
+ ```
244
+ ┌─ <ExcelViewer> .excel-viewer (Vue) / .rxl (React) ─────────────────────────┐
245
+ │ ① Header (顶栏) <ViewerToolbar> ← #header slot 整条替换│
246
+ │ 文件名 · 表数 · 缩放 · 导出 ▾ │
247
+ ├────────────────────────────────────────────────────────────────────────────┤
248
+ │ ② ActionToolbar (操作栏) .action-toolbar ← :toolbar 配置/插件 │
249
+ │ [查找][筛选][复制][自动换行][冻结][缩放][导出] … 自动「⋯ 更多」 │
250
+ ├────────────────────────────────────────────────────────────────────────────┤
251
+ │ ③ FormulaBar (公式栏 Fx) .formula-bar ← (无 slot,可全局 CSS)│
252
+ ├────────────────────────────────────────────────────────────────────────────┤
253
+ │ ④ RenderArea (网格区) .render-area + canvas.grid-canvas │
254
+ │ ├ OverlayManager 图片/图表/形状/插件 overlay(DOM 叠加,随滚动) │
255
+ │ ├ ContextMenu .ooxml-context-menu(body 级,框架无关) │
256
+ │ ├ LightboxHost .ooxml-lightbox(body 级,图片放大+下载) │
257
+ │ └ CellEditorHost .editor-slot(自定义/内置 cell 编辑器) │
258
+ ├────────────────────────────────────────────────────────────────────────────┤
259
+ │ ⑤ SheetTabs (表标签) .sheet-tabs │
260
+ └────────────────────────────────────────────────────────────────────────────┘
261
+ ```
262
+
263
+ | 区域 | DOM / class | 文件(壳) | 替换 / 自定义方式 |
264
+ |---|---|---|---|
265
+ | ① Header 顶栏 | `<ViewerToolbar>` (Vue) | [components/ViewerToolbar.vue](src/components/ViewerToolbar.vue) | `<template #header>` slot 整条替换;插槽签名见 README「分层 UI」 |
266
+ | ② 操作栏 | `.action-toolbar` | [components/ActionToolbar.vue](src/components/ActionToolbar.vue) / [react/RxlActionToolbar.tsx — 暂无,React 用内置按钮行]<br>builtin 渲染在 [components/ExcelViewer.vue](src/components/ExcelViewer.vue) `builtinTool()` | `:toolbar="[...]"` 数组 prop(内置 id + 自定义 `ToolbarItem`);插件 `definePlugin({ toolbar })` 追加项 |
267
+ | ③ 公式栏 | `.formula-bar` / `.rxl-formula-bar` | [components/ExcelViewer.vue](src/components/ExcelViewer.vue) / [react/ExcelViewer.tsx](src/react/ExcelViewer.tsx) | 暂无 slot;CSS 直接覆写(`.excel-viewer .formula-bar`)。命令式:`getCellEditString` / `commitActiveCellValue` |
268
+ | ④ 网格区 canvas | `.render-area > canvas.grid-canvas` | core: [render/canvas-renderer.ts](src/core/render/canvas-renderer.ts) | 渲染钩子 `cellStyle` / `transformModel` / 插件 `overlay` 返回 DOM;`:theme` 改配色 |
269
+ | ④a Overlay 层 | `.overlay-host` 四象限(主/冻结行/冻结列/冻结角) | core: [viewer/overlay-manager.ts](src/core/viewer/overlay-manager.ts) | `<template #overlay>` slot (Vue) / `overlay` prop 返回 DOM (React)/ 插件 `overlay` |
270
+ | ④b 右键菜单 | `.ooxml-context-menu`(body 级) | core: [edit/context-menu.ts](src/core/edit/context-menu.ts) | **Plan C 三层开放**:`:contextMenu` prop(`false` 关闭 / `(ctx,items)=>MenuItem[]` transform) · `@before-context-menu`/`@context-menu` 事件(`preventDefault()` 接管渲染) · 命令式 `openContextMenu(x,y,items?)` / `closeContextMenu()`;插件 `definePlugin({ contextMenu })` 链式贡献 |
271
+ | ④c 图片灯箱 | `.ooxml-lightbox`(body 级) | core: [viewer/lightbox-host.ts](src/core/viewer/lightbox-host.ts) | `imageLightbox={false}` 关闭;命令式 `openImageLightbox(src,fileName?)` |
272
+ | ④d 单元格编辑器 | `.editor-slot` | core: [edit/editor-host.ts](src/core/edit/editor-host.ts) + 内置默认 [edit/default-editor.ts](src/core/edit/default-editor.ts) | `:editor` prop = `EditorResolver`(按格返回工厂);插件 `editor`;详见 README「自定义编辑器」 |
273
+ | ⑤ 表标签 | `.sheet-tabs` | [components/SheetTabs.vue](src/components/SheetTabs.vue) / 内置于 React 壳 | 暂无 slot;`.excel-viewer .sheet-tabs` CSS 覆写 |
274
+
275
+ **重要:demo 顶栏不是组件的**。仓库里 `npm run dev` 看到的绿色顶栏(`.app-bar`)是 [src/App.vue](src/App.vue) 的 demo 框架栏(选 xlsx / 加载示例 / 编辑模式开关 / 演示按钮),React demo 同形见 [src/react-demo/main.tsx](src/react-demo/main.tsx)。它独立于 `<ExcelViewer>`,**用户 import 组件时不会出现**。
276
+
195
277
  ## API
196
278
 
197
279
  ### `<ExcelViewer>`
@@ -199,13 +281,24 @@ viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区
199
281
  | Prop | 类型 | 说明 |
200
282
  |---|---|---|
201
283
  | `src` | `File \| Blob \| ArrayBuffer \| Uint8Array \| string(URL)` | 要预览的 .xlsx 数据源 |
284
+ | `workbook` | `WorkbookModel \| JsonInput` | **JSON 直渲(P3)** 优先于 `src`。WorkbookModel 直用;JsonInput 走 `jsonToWorkbook` 自动构造:① `unknown[][]` 二维数组 ② `Record<string,unknown>[]` 对象数组(首行表头) ③ `{ sheets:[...] }` 多表 |
285
+ | `jsonOptions` | `JsonLoadOptions` | JSON 直渲选项(`workbook = JsonInput` 时生效):`headerRow` / `sheetName` / `autoInfer`(数字串→数字、ISO 日期串→Date,默认 on) |
286
+ | `templateFile` | `ExcelSource` | **模板样式 overlay(P3 重设计 2026-06-08)** 一份 .xlsx 当**样式捐赠者** —— 模板贡献 styling(styles / merges / 列宽 / 行高 / freeze / theme),JSON / CSV 数据**在 A1 自然位置渲染**,模板的 raw 文字 / 占位符 / 图 / 图表 / 条件格式 **全部丢弃**。**仅在 `:workbook` (JSON / 模型) 数据源下生效**;`:src` (xlsx) 数据源自带格式,给 `:templateFile` 会被忽略并 console.warn。工具栏内置 `template` 项可在运行时切换 / 导入 / 清除 |
287
+ | `templateName` | `string` | 模板显示名(标题栏 `· 模板: xxx` 后缀);不给则取运行时 File.name。`:fileName` 同步:JSON 源未给名时默认 "JSON 数据" |
288
+ | `exportProgress` | `boolean`(默认 `true`)| **内置导出进度遮罩(P1.5)** 调 `viewer.downloadPdf` / `downloadImage` / `downloadXlsx` / `print` / 选区图片批量转换 时,壳自动建 `AbortController` + 接 `onProgress` → 显示居中模态(stage 标签 + 进度条 + 取消)。**关闭**用 `false`(纯回调走 `opts.onProgress`/`signal`);**自渲染**用 `#export-progress` 插槽(Vue)/ `renderExportProgress` (React) |
289
+ | `contextMenu` | `false \| ContextMenuTransform` | **右键菜单(Plan C)** — 三层开放:① 默认 = 内置菜单(editable 时弹) ② `false` = 不弹内置(`@before-context-menu` / `@context-menu` 事件仍触发,自渲染) ③ `(ctx, items) => MenuItem[] \| undefined` = transform 回调,在内置 items 上加 / 减 / 重排 |
202
290
  | `fileName` | `string` | 标题栏显示的文件名(可选) |
203
291
  | `editable` | `boolean` | 开启编辑(默认 `false` = 只读,行为与历史一致) |
204
292
  | `cellReadOnly` | `(cell, pos) => boolean` | 按格只读判定(编辑时) |
205
- | `readOnlyRanges` | `MergeRange[]` | 只读区域(命中即只读) |
293
+ | `readOnlyRanges` | `MergeRange[]` | 只读区域(命中即只读,黑名单) |
294
+ | `editableTargets` | `EditableTarget \| EditableTarget[]` | **可编辑白名单**(2026-06-08)— 设了就是白名单语义:默认只读,**只**命中**任一** target 的格可编辑。4 种 target 形状自动识别:`{row,col}` 单格 / `{row}` 整行 / `{col}` 整列 / `MergeRange` 矩形。单值或数组都行,允许**不相邻**多 target。`undefined` (不传) = 不启用白名单 = 老行为(默认全可编辑);`[]` (显式空) = 全只读。与 `readOnlyRanges` / `cellReadOnly` 叠加 — 白名单命中后仍可被二次"黑"掉。运行时改:命令式 `viewer.setEditableTargets(targets)` |
295
+ | `strictDimensions` | `boolean` | **严格尺寸闸门**(Phase B, 2026-06-08)— 默认 `false`:`setColumnWidth` / `setRowHeight` / `autoFit` 仅受全局 `editable` 控制。设 `true` + 启用了 `editableTargets` → 该列/行至少有 1 格在白名单内才能改尺寸,否则 skip + emit `permission-denied` (`reason='dimension'`)。 |
296
+ | `readOnlyCellStyle` | `boolean \| CellStyleOverride \| CellStyleFn` | **只读视觉钩子**(Phase C, 2026-06-08)— 默认 `false` 无视觉差异(老行为);`true` 套内置浅灰底 `#f5f7fa`;对象 = 固定样式给所有只读格;函数 = 按格自定义。仅在该格 `editable=false` 时套用,跟 `editableTargets` 配合一眼看出哪些格可编辑。**鼠标光标**: 编辑模式下悬停只读格自动变 `not-allowed`(内置,不可关)。 |
206
297
  | `editor` | `EditorResolver` | 自定义单元格编辑器工厂(返回任意 DOM) |
207
298
  | `recalc` | `boolean` | 公式重算(默认 `false`;需 `editable`) |
208
299
  | `formulaEngine` | `FormulaEngineFactory` | 自定义/自研公式引擎(默认 HyperFormula) |
300
+ | `cellImageFit` | `'fill' \| 'contain' \| 'cover'` | WPS 单元格内嵌图贴合方式(默认 `contain` 等比,与 WPS 渲染一致) |
301
+ | `imageLightbox` | `boolean` | 图片点击放大灯箱(默认 `true`;只读单击图放大、编辑右键「查看大图」) |
209
302
 
210
303
  > 编辑相关 props 详见下方 [编辑](#编辑可选默认只读) 章节。
211
304
 
@@ -248,19 +341,27 @@ viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区
248
341
  />
249
342
  ```
250
343
 
251
- 开编辑后:**双击 / F2 / 直接打字**进格编辑(Enter 提交下移、Tab 右移、Esc 取消),**Ctrl+Z / Ctrl+Y** 撤销重做,拖列头/行头边界改宽高,拖浮动图片移动。只读格 / 区域不进编辑。
344
+ 开编辑后:**双击 / F2 / 直接打字**进格编辑(Enter 提交下移、Tab 右移、Esc 取消、Shift+Enter 插换行),**Ctrl+Z / Ctrl+Y** 撤销重做,拖列头/行头边界改宽高,拖浮动图片移动。只读格 / 区域不进编辑。
345
+
346
+ **长文本编辑(WPS 风格, 1.2.1)**:默认编辑器是 `<textarea>`, 输入长文本自动**换行 + 向下浮起撑高**, 编辑期显示完整内容;提交后单元格行高保持原样(跟 WPS 一致, 不持久化撑高;如需永久保持多行, 单元格设 `wrapText=true`)。自定义编辑器可通过 `CellEditorReturn.getDesiredHeight(width)` 复用此撑高机制 + `ctx.reposition()` 主动触发重撑。
252
347
 
253
348
  ### 命令式编辑 API(模板 ref / 插件 `viewer`)
254
349
 
255
350
  | 类别 | 方法 |
256
351
  |---|---|
257
352
  | 值 | `editCell(row,col,value)` · `editRange(range,values[][])` · `clearRange(range)` |
353
+ | 粘贴 | `pasteText(tsv,at?)` · `pasteRichHtml(html,at?)`(Excel/WPS 复制 → 字体/颜色/填充/边框/对齐/合并 + 单次撤销) · `pasteImageBlob(blob,at?)`(单图/拖文件落格) |
258
354
  | 样式 | `setStyle(range, patch)`(`patch` = `CellStyleOverride`:font/fill/borders/对齐/numFmt) |
355
+ | 背景/字体色 | `getActiveFillColor()` · `getActiveFontColor()`(回显活动格当前色 #RRGGBB) · `setSelectionFill(color\|null)`(null=清除填充) · `setSelectionFontColor(color)` |
356
+ | 自动换行 | `getSelectionWrapState()`→`'all'\|'none'\|'mixed'`(工具栏 active 用) · `toggleWrapTextOnSelection()`(WPS 风格 toggle:全开/全关;行高按内容重撑,只扩不缩) |
259
357
  | 列宽行高 | `setColumnWidth(col,px)` · `setRowHeight(row,px)` |
260
358
  | 行列结构 | `insertRows(at,count?)` · `deleteRows(at,count?)` · `insertCols(at,count?)` · `deleteCols(at,count?)` |
261
359
  | 图片 | `getImages()` · `addImage(anchor)` · `removeImage(i)` · `moveImage(i,dxPx,dyPx)` · `resizeImage(i,wPx,hPx)` |
360
+ | WPS 内嵌图 | `getCellImages()` · `setCellImageFit('fill'\|'contain'\|'cover')` · `convertImageToCellAuto(imgIdx)`(就近) · `convertAllImagesToCells(col?)`(整表/整列) · **`convertImagesInRangeToCell(range)`**(选区批量,单次撤销) · `convertCellImageToFloat(row,col,size?)`(嵌入→浮动) · **`convertCellImagesInRangeToFloat(range, size?)`**(选区批量浮动化) · `convertImageToCell(imgIdx,row,col)`(显式目标) |
361
+ | 图片放大 | `openImageLightbox(src,fileName?)`(命令式弹大图+下载) · `getCellImageAt(row,col)`(某格是否内嵌图→`{id,src,mime}`) |
262
362
  | 撤销/进编辑 | `undo()` · `redo()` · `canUndo()` · `canRedo()` · `beginEdit(row,col)` · `cancelEdit()` · `isEditing()` · `getEditingCell()` |
263
- | 查询/状态 | `getCellSnapshot(row,col)` · `isDirty()` · `resetToOriginal()` · `isRecalcReady()` |
363
+ | 公式栏 | `getCellEditString()`(活动格可编辑字符串:公式→`=…`,数值→原始数字串) · `canEditActiveCell()` · `commitActiveCellValue(value, move?)`(顶部 Fx 公式栏可编辑并与单元格联动,底层即用这套) |
364
+ | 查询/状态 | `getCellSnapshot(row,col)` · **`inspectCell(row,col)`**(全息体检:snapshot + 合并区 + 浮动图覆盖 + WPS 内嵌图 + 数据验证 + 条件格式命中 + 链接/批注) · `isDirty()` · `resetToOriginal()` · `isRecalcReady()` · `getVirtualExtent()`(当前虚拟行列范围) |
264
365
  | 导出 | `exportXlsx/downloadXlsx` · `exportJson/downloadJson` · `exportCsv/downloadCsv`(见 [导出](#导出--打印)) |
265
366
 
266
367
  所有写操作(含拖拽改宽高/移图)**统一进撤销栈**、发对应事件、翻**脏标记**;`resetToOriginal()` 一键放弃全部修改、还原到刚加载的原件。
@@ -284,6 +385,19 @@ const myEditor: EditorResolver = (cell, pos) => {
284
385
 
285
386
  插件也可经 `editor` 字段贡献(多插件数组序,组件 prop 最后覆盖),与 `cellStyle`/`overlay` 同范式。
286
387
 
388
+ ### WPS 单元格内嵌图(DISPIMG)
389
+
390
+ 不少 .xlsx 由 **WPS** 导出,其"嵌在单元格里的图"用了 WPS 私有存法(`xl/cellimages.xml` + 单元格 `=DISPIMG("id",1)` 公式),标准解析读不出来 → 普通工具打开会**缺图**。本组件:
391
+
392
+ - **自动识别并展示**:解析 `cellimages.xml` 登记表,把图**画进单元格内**(随行高列宽 / 滚动 / 裁剪 / 冻结 / 缩放),非浮动叠加。**贴合方式可配置** `cellImageFit`:`contain`(默认,等比缩放,**与 WPS 渲染一致**——WPS 打开导出文件时 DISPIMG 固定按 contain 显示)/ `fill`(拉伸铺满随格变形)/ `cover`(等比裁剪铺满)。`getCellImages()` 读登记表;`getCellSnapshot(row,col).cell.dispImgId` 看某格是否内嵌图。
393
+ - **一键浮动 ⇄ 嵌入互转**(编辑模式):
394
+ - `convertImageToCellAuto(imgIdx)` —— **就近嵌入**:图压在哪个单元格上就嵌进哪格(几何反推,无需手指定目标格)。
395
+ - `convertAllImagesToCells(col?)` —— **整表/整列批量**就近嵌入(`col` 给定只嵌该列),一次进撤销栈(单次 Ctrl+Z 全撤),返回嵌入张数。
396
+ - `convertCellImageToFloat(row,col,size?)` —— 内嵌图拎回浮动图。
397
+ - 右键单格菜单:「将此处浮动图嵌入单元格 / 整列浮动图嵌入(N 张)/ 整表浮动图嵌入(N 张)/ 内嵌图转为浮动图」。
398
+ - 全部入撤销栈、发 `cell-change`/`image-change`、翻脏标记。(`convertImageToCell(imgIdx,row,col)` 仍保留,用于显式指定目标格。)
399
+ - **导出往返**:`downloadXlsx()` / `exportXlsx()` 导出时,在 ExcelJS 写出后**于 zip 层回注** WPS 私有件(`cellimages.xml` + rels + media + `[Content_Types].xml`/`workbook.xml.rels` 补丁,从模型重建),原有的 + App 内新转的内嵌图导出后用 WPS 打开都正常显示。rebuild / overlay 两种保真模式均覆盖;无字节的 blob-only 图除外。
400
+
287
401
  ### 公式重算(可换引擎)
288
402
 
289
403
  开 `:recalc` 后,编辑公式格或被公式引用的格 → 依赖格**自动级联重算**,每个变动都发 `cell-change`(`source: 'api'|'ui'|'undo'|'redo'`)。默认引擎 [HyperFormula](https://hyperformula.handsontable.com/)(可选 peer,`npm i hyperformula`;**GPL-3.0/商业双授权**,商业项目用 `:formula-engine` 注入持牌/自研引擎,实现 `FormulaEngine` 接口即可)。`isRecalcReady()` 查引擎是否就绪(异步懒加载)。
@@ -353,6 +467,38 @@ const myEditor: EditorResolver = (cell, pos) => {
353
467
 
354
468
  公共选项:`target`(`'active'`(默认)/`'all'`/索引/索引数组)、`range`(限定单元格区域)、`scale`(清晰度,默认 2)、`includeHeaders`、`gridlines`、`background`;PDF/打印另有 `format`(a4/a3/letter/`[宽,高]mm`)、`orientation`、`margin`(mm)、`fitToWidth`。
355
469
 
470
+ **长任务进度 + 取消 + 内置遮罩(P1 + P1.5)** —— **两层**叠加,默认开箱即用,可逐层覆盖:
471
+
472
+ #### ① Core 层(协议):`onProgress` + `signal`
473
+ 所有导出方法(PNG / PDF / XLSX / Print)+ 选区图片批量互转 统一接:
474
+ ```ts
475
+ const ctrl = new AbortController()
476
+ try {
477
+ await viewer.value.downloadPdf({
478
+ target: 'all',
479
+ onProgress: (p) => console.log(p.stage, p.ratio, p.label), // 'render'/'compose'/'paginate'/'write'/'zip'/'convert'
480
+ signal: ctrl.signal,
481
+ })
482
+ } catch (e) {
483
+ if ((e as Error).name === 'AbortError') console.log('用户取消')
484
+ else throw e
485
+ }
486
+ ctrl.abort() // 任意时刻取消
487
+ ```
488
+ 导出全链路在调度点 `await yieldToEvent()` 让出 UI(防假死)+ 调度前 `checkAborted(signal)`(立刻中断)。`ExcelJS.writeBuffer` / `jsPDF` 内部仍是黑盒(`zip`/`write` 阶段),那两段无法细分,但全程都有可视进度。
489
+
490
+ #### ② Shell 层(UI):**内置居中模态**(默认开)
491
+ **不传任何参数**调 `viewer.downloadPdf()` 等异步方法,壳自动建 `AbortController` + 接 `onProgress` → 显示**居中模态**(stage 标签 + 进度条 + 取消按钮)。用户传入的 `onProgress`/`signal` **仍正常链回调**(并存,不冲突)。
492
+
493
+ #### ③ 关闭 / 覆盖
494
+ | 需求 | 做法 |
495
+ |---|---|
496
+ | 完全关掉内置遮罩(纯回调) | `<ExcelViewer :export-progress="false">`(Vue)/ `exportProgress={false}` (React) |
497
+ | 自渲染(用 Element Plus / Ant Design 等自家组件) | **Vue**:`<template #export-progress="{ state, busy, cancel }">…</template>` 插槽;**React**:`renderExportProgress={({state,busy,cancel}) => <YourModal …/>}` |
498
+ | 既要内置又自动注入跟踪 | 默认行为已是 —— 用户传 `{ onProgress, signal }` 仍被链回调,内置 UI 也照常显示 |
499
+
500
+ 覆盖矩阵:**导出**(PDF / PNG / XLSX / Print)、**选区图片批量互转**(P2,壳侧 1.2.0 起返 `Promise<number>` 以接遮罩)。**不包含**:文件解析(parsing 有独立的顶栏进度条,与本遮罩分开)、`copySelection` / `setStyle` 等瞬时操作、模板样式 overlay(P3 重设计后是同步纯函数,耗时可忽略)。
501
+
356
502
  **默认还原 OOXML 原生页面设置** —— PDF/打印时,未显式指定的 `format`/`orientation`/`margin`/`fitToWidth` 自动取自工作表的 `pageSetup`(纸张、方向、页边距、适应页面/缩放),并应用**打印区域**(默认导出范围)与**打印标题行/列**(每页顶部/左侧重复)。显式传入的选项始终覆盖之。
357
503
 
358
504
  **分页** —— `fitToWidth: true`(默认)把整表缩放到页宽、只竖向跨页;`fitToWidth: false`(或工作表未设"适应页面")按自然尺寸×缩放,**宽表横向跨页 + 高表竖向跨页**(像 Excel 的页矩阵,顺序"先下后右"),此时打印标题列在每张横向页左侧重复。
@@ -396,6 +542,46 @@ await viewer.value.downloadPdf({
396
542
  ```
397
543
  > 提示:中文表格若不注册字体,矢量模式会产生很多小图、文件偏大且较慢 —— 注册一个子集字体即可全矢量。
398
544
 
545
+ ### 右键菜单(Plan C:三层开放)
546
+
547
+ 默认 `editable` 时显示内置菜单(复制/粘贴/插入/删除/合并/拆分/自动换行/清除内容 + WPS 图片互转)。三种覆盖方式可同时使用:
548
+
549
+ **① 加 / 减 / 重排内置项** —— 用 `:contextMenu` 传 transform:
550
+ ```vue
551
+ <ExcelViewer
552
+ :context-menu="(ctx, items) => [
553
+ ...items,
554
+ { separator: true },
555
+ { label: `导出此格 PDF (${ctx.activeCell.row + 1},${ctx.activeCell.col + 1})`, action: () => viewer.downloadPdf() },
556
+ ]"
557
+ />
558
+ ```
559
+ - `ctx`: `{ range, single, activeCell, sheet, workbook, editable }` — 当前选区 + 活动格 + 模型句柄
560
+ - `items`: 内置 `MenuItem[]`(`{label, action, disabled, separator}`)—— 加、过滤、重排,返回新数组生效;返 `undefined` / `void` 用原样
561
+
562
+ **② 接管渲染**(用自家 UI 框架的菜单,如 Element Plus / Radix / Headless UI):
563
+ ```vue
564
+ <ExcelViewer
565
+ :context-menu="false" <!-- 关闭内置弹层(事件仍触发) -->
566
+ @before-context-menu="(p) => p.preventDefault()"
567
+ @context-menu="(p) => myMenu.show(p.x, p.y, p.items)"
568
+ />
569
+ ```
570
+ - `@before-context-menu` 在内置弹出前触发;调 `payload.preventDefault()` 取消内置;`:contextMenu="false"` 等价于自动 preventDefault
571
+ - `@context-menu` 在内置弹出后(或被 prevent 后)触发,拿到 `{ x, y, ctx, items }` —— **总会触发**,自渲染只需监听这个事件
572
+ - React:`onBeforeContextMenu` / `onContextMenuShow` 接同形 payload
573
+
574
+ **③ 命令式打开 / 关闭**(键盘 Shift+F10、工具栏触发、跨层调用):
575
+ ```ts
576
+ viewer.openContextMenu(clickX, clickY) // 按当前选区算内置 items
577
+ viewer.openContextMenu(clickX, clickY, customItems) // 直接喂自定义 items
578
+ viewer.closeContextMenu()
579
+ ```
580
+
581
+ **插件贡献**:`definePlugin({ contextMenu: (ctx, items) => [...] })` —— 多插件按数组顺序串行(后者拿前者输出),组件 `:contextMenu` prop 最后覆盖,顺序固定 `内置 → 插件 → prop`。
582
+
583
+ `MenuItem` / `ContextMenuCtx` / `ContextMenuTransform` 全部从 `ooxml-excel-editor/core` 导出(TS 类型完整)。
584
+
399
585
  ### 操作工具栏(可配置 / 可插件 / 响应式)
400
586
  顶栏(文件名/导出/缩放)下方有一行**操作工具栏**,内置 `find`/`filter` 按钮默认显示。用 `:toolbar` 配置:
401
587
  ```vue
@@ -403,7 +589,7 @@ await viewer.value.downloadPdf({
403
589
  <ExcelViewer :toolbar="['find','filter','separator','zoom','export']" /> <!-- 控制项/顺序/分隔 -->
404
590
  <ExcelViewer :toolbar="false" /> <!-- 隐藏整条 -->
405
591
  ```
406
- - **内置 id**:`find`(查找)、`filter`(切换自动筛选 —— 文件没设也能点出下拉)、`clear-filter`(清除筛选,无筛选时禁用)、`copy`(复制选区)、`freeze`(冻结/取消)、`zoom`(缩放下拉)、`export`(导出/打印下拉)、`'separator'`/`'|'`(分隔线);`sort` 规划中。
592
+ - **内置 id**:`find`(查找)、`filter`(切换自动筛选 —— 文件没设也能点出下拉)、`clear-filter`(清除筛选,无筛选时禁用)、`copy`(复制选区)、`wrap-text`(自动换行 toggle,WPS 风格,需 `editable`)、`image-tools`(图片工具 ▾:选区/整表/整列 浮动 ⇄ 嵌入互转,需 `editable`)、`template`(模板 ▾:仅 JSON / 模型数据源下生效;导入 .xlsx 当样式捐赠者;xlsx 数据源下禁用)、`freeze`(冻结/取消)、`zoom`(缩放下拉)、`export`(导出/打印下拉)、`'separator'`/`'|'`(分隔线);`sort` 规划中。
407
593
  - **富项类型**(`ToolbarItem`):`type:'separator'` 分隔线;`items: ToolbarItem[]` 变下拉子菜单;`disabled?(viewer)` 禁用态;`iconSvg`(内联 SVG,优先于 `icon` emoji)/ `icon` / `label` / `title` / `onClick(viewer)` / `active?(viewer)`。
408
594
  - **响应式溢出**:宽度不足时,放不下的项自动折叠进「⋯ 更多」下拉。
409
595
  - **插件贡献**:`ExcelPlugin.toolbar: ToolbarItem[]`,插件加载即追加(opt-in)。
@@ -490,6 +676,8 @@ npm run build:demo # 构建 demo 站点
490
676
  - [ARCHITECTURE.md](./ARCHITECTURE.md) —— 包/入口、core 分层、数据流、`ViewerController` 桥接、"加功能改哪"
491
677
  - [CONTRIBUTING.md](./CONTRIBUTING.md) —— 本地跑通、改动流程、不可破坏的硬约束
492
678
  - [CHANGELOG.md](./CHANGELOG.md) / [RELEASING.md](./RELEASING.md) —— 变更记录 / 发布清单
679
+ - [docs/编辑权限与只读边界.md](./docs/编辑权限与只读边界.md) —— **EditableTarget 白名单 / DimTarget 尺寸多形态 / readOnlyCellStyle 视觉钩子 / permission-denied 事件** 体系化说明(1.2.0)
680
+ - [docs/Vue2.md](./docs/Vue2.md) —— **Vue 2 兼容子入口**完整文档(1.3.0;`ooxml-excel-editor/vue2` 跟 Vue 3 / React 壳 ~100% 功能对齐)
493
681
 
494
682
  > **React props/events** 与 Vue 对齐(事件用 camelCase 回调:`onRendered`/`onError`/`onCellClick`/`onSelectionChange`/`onSheetChange`/`onHyperlinkClick`),命令式句柄 `ExcelViewerHandle` 与 Vue 组件 ref 同名方法一致。上面「扩展 API」中的**插件 `definePlugin`** 目前服务 Vue 壳;React 壳已可用全部 props/命令式 API/事件,插件 overlay 跨框架化在路线图中。
495
683