ooxml-excel-editor 1.1.0 → 1.2.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 (48) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +148 -4
  3. package/dist/chunks/plugin-overlay-BBrNby8v.js +8965 -0
  4. package/dist/chunks/worker-client.stub-CJlmpAgJ.js +190 -0
  5. package/dist/components/ExcelViewer.vue.d.ts +170 -19
  6. package/dist/components/ExportProgressOverlay.vue.d.ts +11 -0
  7. package/dist/components/FilterPopup.vue.d.ts +4 -4
  8. package/dist/components/ViewerToolbar.vue.d.ts +2 -0
  9. package/dist/composables/useExcelDocument.d.ts +1 -0
  10. package/dist/core/edit/clipboard-html.d.ts +24 -0
  11. package/dist/core/edit/commands.d.ts +45 -1
  12. package/dist/core/edit/context-menu.d.ts +19 -0
  13. package/dist/core/edit/edit-controller.d.ts +70 -2
  14. package/dist/core/edit/permissions.d.ts +41 -2
  15. package/dist/core/edit/types.d.ts +62 -0
  16. package/dist/core/export/abort.d.ts +21 -0
  17. package/dist/core/export/exporter.d.ts +2 -1
  18. package/dist/core/export/types.d.ts +8 -0
  19. package/dist/core/export/wps-cellimages.d.ts +6 -0
  20. package/dist/core/export/xlsx-writer.d.ts +9 -0
  21. package/dist/core/format/color.d.ts +5 -0
  22. package/dist/core/format/number-format.d.ts +3 -0
  23. package/dist/core/layout/autofit.d.ts +3 -0
  24. package/dist/core/layout/grid-metrics.d.ts +14 -2
  25. package/dist/core/loader-json.d.ts +23 -0
  26. package/dist/core/model/clone.d.ts +3 -4
  27. package/dist/core/model/inspect.d.ts +43 -0
  28. package/dist/core/model/mutations.d.ts +16 -1
  29. package/dist/core/model/types.d.ts +44 -2
  30. package/dist/core/parser/cell-image-parser.d.ts +9 -0
  31. package/dist/core/parser/row-meta-parser.d.ts +3 -0
  32. package/dist/core/plugin.d.ts +144 -6
  33. package/dist/core/progress.d.ts +23 -0
  34. package/dist/core/render/canvas-renderer.d.ts +56 -2
  35. package/dist/core/render/conditional.d.ts +7 -0
  36. package/dist/core/template/style-overlay.d.ts +9 -0
  37. package/dist/core/viewer/controller.d.ts +209 -6
  38. package/dist/core/viewer/lightbox-host.d.ts +16 -0
  39. package/dist/core.js +1 -1
  40. package/dist/index.js +1169 -821
  41. package/dist/react/ExcelViewer.d.ts +134 -3
  42. package/dist/react/ExportProgressOverlay.d.ts +6 -0
  43. package/dist/react/use-excel-document.d.ts +2 -0
  44. package/dist/react.js +718 -281
  45. package/dist/style.css +1 -1
  46. package/package.json +1 -1
  47. package/dist/chunks/plugin-overlay-Cfnn9EOi.js +0 -7144
  48. package/dist/chunks/worker-client.stub-BQVZfaLd.js +0 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,81 @@
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.2.0] - 2026-06-08
6
+
7
+ **主线**: 只读边界三件套(白名单 / 尺寸多形态 / 视觉钩子 + permission-denied 事件)+
8
+ 模板语义重设计(样式捐赠者)+ WPS 单元格内嵌图(展示 / 互转 / 导出往返)+ 富粘贴 +
9
+ 图片放大下载 + 虚拟空行 + 公式栏可编辑 + 背景/字体色 + 编辑 UX 补齐(合并/粘贴/右键菜单)+
10
+ 性能 + 导出错误可见性 + 1900/1904 日期单测。**全部向后兼容、默认只读零回归**。
11
+
12
+ 测试基线: 306 单测 / 107 e2e。
13
+
14
+ ### 修复 + 新增
15
+ - **★ 只读边界三件套(2026-06-08;三次提交)** —— 用户三连问"可编辑是一切改变的基础", 怀疑有路径绕过 isEditable. 三阶段解决:
16
+ - **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`.
17
+ - **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`.
18
+ - **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 跟「设置可编辑」配合使用.
19
+ - **测试**: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 全绿.
20
+ - **★ 可编辑白名单 `editableTargets`(2026-06-08)** —— 现有的 `cellReadOnly` / `readOnlyRanges` 是**黑名单**(默认全可编辑,标只读),新加一个 **白名单**(默认全只读,标可编辑)。用户原话:"设置不相邻的一批单元格可编辑,暴露的 API 应该支持多种参数,可以传区域,也可以传单个单元格,多个单元格,行,列;没有设置的就是只读"。
21
+ - **新类型** `EditableTarget`(`core/edit/types.ts`)联合 4 种形状,**自动识别**带哪些字段:`{row,col}` 单格 / `{row}` 整行 / `{col}` 整列 / `MergeRange` 矩形。单值或数组都接,**允许不相邻**多 target
22
+ - **新 prop** `:editableTargets` —— `undefined` (不传) = 不启用白名单 = 老行为;`[]` (显式空) = 全只读;非空 = 白名单生效。优先级:`editable=false` ► 不在白名单 ► `readOnlyRanges` ► `cellReadOnly` ► 否则可编辑(白名单内仍能被黑名单二次"黑"掉)
23
+ - **新命令式 API** `viewer.setEditableTargets(targets)` / `viewer.getEditableTargets()` —— 运行时改不动 prop,直接覆盖 `editCfg.editableTargets`,立即重绘
24
+ - **demo 加「设置可编辑」按钮 + 弹窗**(编辑模式下出现):12×8 网格化点选,可单击格 / 列标题 / 行号切换;footer 显示已选数;「应用」生效 / 「关闭白名单」恢复默认 / 「取消」放弃
25
+ - **测试**:`permissions.test.ts` 加 11 项白名单分支(单格 / 多格不相邻 / 整行 / 整列 / 矩形 / 空数组 / 混合 4 种 / 与黑名单叠加 / editable=false 时白名单也无效 / 形状识别);新 `e2e/editable-targets.e2e.ts` 3 用例(命令式 setEditableTargets 4 种 target 形状 / Demo 弹窗点选→应用 / 空数组全只读)
26
+ - 不破坏现有 API,完全 additive
27
+ - **★ 模板语义彻底重设计(2026-06-08)—— 从"占位符 + 锚点表"改为"样式捐赠者"** —— 之前几轮"占位符 + 锚点表"语义反复改: `discardUnmatched` → `trimUnused` → JSON 自然位置… 都没切中用户实际需求。用户原话:**"模板这个功能,只在 json/csv 这些本身不附带格式的数据源才生效,xlsx 数据源根本不需要"**。新语义:
28
+ - **`:templateFile` = 样式捐赠者** —— 模板贡献 styling(`styles` 池 / `merges` / 列宽 / 行高 / `freeze` / `theme`),JSON / CSV 数据**在 A1 自然位置渲染**,模板的 raw 文字 / 占位符 / 图 / 图表 / 条件格式 / 数据验证 **全部丢弃**(避免幽灵规则)
29
+ - **仅在 `:workbook` (JSON / 模型) 数据源下生效** —— xlsx 数据源(`:src`)自带格式,给 `:templateFile` 会被**忽略并 console.warn**;工具栏「模板 ▾」在 xlsx 模式下**禁用**
30
+ - **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)
31
+ - **核心契约 9 项单测**(`style-overlay.test.ts`):数据 raw 全部保留在自然位置 / 模板 raw 全部丢弃 / styleId 从模板同位置取 / merges + 列宽 + 行高 + freeze 全部从模板拷贝 / 模板的 images + charts + conditional + dataValidations 不带过来 / 数据 sheet 名 + date1904 + cellImages 透传 / 入参不被原地修改 / 空数据 + 模板 → 干净样式骨架 / 数据 dimension 跟模板列宽行高声明取大
32
+ - **demo 简化**:`App.vue` 删 `templateSpec` 计算属性(placeholders + anchors 整套不要),`loadJsonSample` 只传 `:workbook` 数据数组;`:fileName="'订单数据"`。点工具栏「模板 ▾ → 导入 .xlsx 模板」即套样式,模板的标题/客户/合计/{{占位符}} 全部不见, JSON 5 条数据仍在 A1 起的自然位置
33
+ - **e2e 全重写**(`template-switch.e2e.ts`):3 个用例 —— ① JSON 无模板时数据在 A1 自然位置 ② JSON + 模板时 模板装饰文字全部不见 + JSON 仍在 A1 + 切换/清除模板正常 ③ xlsx 数据源加载后工具栏「模板 ▾」禁用
34
+ - **过渡 / 后续迁移**:用户用旧 `applyTemplate(spec)` 自填数据的场景,改为"前端构造好完整 JSON 后再传 `:workbook`"。后续如需"模板填值"那套(不入命令栈的占位符 / 锚点表写入),应该走"应用层"而非组件 prop,组件只管渲染样式
35
+ - **右键菜单全面开放(Plan C)** —— 1.2.0 前右键菜单是内置 hardcoded、无任何对外接口。此版三层开放:
36
+ - **prop `:contextMenu`** —— `false` 关闭内置弹层(事件仍触发,供自渲染);`(ctx, items) => MenuItem[] | undefined` transform 回调,在内置 items 上加 / 减 / 重排
37
+ - **事件 `@before-context-menu` / `@context-menu`**(Vue) / `onBeforeContextMenu` / `onContextMenuShow`(React)—— `payload.preventDefault()` 接管渲染(用 Element Plus / Radix 等自渲染);`@context-menu` 拿到 `{x, y, ctx, items}` 总会触发
38
+ - **命令式 API** `openContextMenu(x, y, items?)` / `closeContextMenu()` —— 键盘 Shift+F10、工具栏触发等
39
+ - **插件贡献** `definePlugin({ contextMenu: (ctx, items) => ... })` —— 多插件按数组顺序串行,组件 prop 最后覆盖
40
+ - 顺序固定:**内置 → 插件 → prop → 事件**;`MenuItem` / `ContextMenuCtx` / `ContextMenuTransform` 全部从 `/core` 导出
41
+ - 顺手修了 Vue 4-pkt 接 prop 的"boolean prop 缺省被 Vue 判成 false"小坑(withDefaults 显式 undefined)
42
+ - **内置导出进度遮罩 + 三层覆盖机制(P1.5)** —— P1 已建好 `onProgress` + `AbortSignal` 协议,但壳没接 UI,用户调 `viewer.downloadPdf()` 看不到任何反馈 → 这次补 **Shell 默认 UI** + 用户**逐层覆盖**:
43
+ - ① 新组件 `ExportProgressOverlay`(Vue SFC + React tsx)居中模态:stage 标签 + 进度条 + 取消按钮;同视觉 / 同协议
44
+ - ② 壳自动 wrap 长任务(`downloadPdf`/`exportPdf`/`downloadImage`/`exportImage`/`downloadXlsx`/`exportXlsx`/`print`/`convertImagesInRangeToCell`/`convertCellImagesInRangeToFloat`):建内置 `AbortController` + 接 `onProgress` → 用户调时**默认看见遮罩**,无需任何 prop
45
+ - ③ 用户传 `{ onProgress, signal }` 仍正常**链回调**(并存)
46
+ - ④ 覆盖:`:export-progress="false"` 关闭内置遮罩(纯回调);Vue `#export-progress` 插槽 / React `renderExportProgress` 自渲染(拿到 `{state, busy, cancel}`)
47
+ - ⑤ **修单表 PDF 卡 0% bug**:核心导出对"单表"和"jsPDF/canvasToBlob 黑盒"阶段改 emit `ratio: undefined`,overlay 走 indeterminate 扫动条动画(看着在动);多表仍按 `i/total` 走离散进度
48
+ - ⑥ 1.2.0 起 `convertImagesInRangeToCell` / `convertCellImagesInRangeToFloat` 在壳侧返 `Promise<number>`(为接遮罩),core 内核仍同步
49
+ - **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` 给"仅引擎"用户。
50
+ - ~~**模板填值(P3)** — `props.template` (`TemplateFillSpec`) + `applyTemplate(spec)`~~ — **已废弃**, 改为本版 1.2.0 顶部的"模板样式 overlay"语义. 见上文 ★ 重设计条目.
51
+ - **图片浮动 ⇄ 嵌入 选区批量 + 工具栏入口** —— 新 API `convertImagesInRangeToCell(range)` / `convertCellImagesInRangeToFloat(range, size?)` 把"选区内"批量互转,聚合成单次撤销(复用 `convertImagesToCells` 范式 + 新增 `convert-to-floats` exec kind)。Vue 工具栏新内置 `image-tools` 下拉(选区/整表/整列 浮动 ⇄ 嵌入)发现性翻倍;右键菜单多格选区时直接出现「选区浮动图全部嵌入(N 张) / 选区内嵌图全部浮动化(N 张)」。
52
+ - **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 流程也可调。
53
+ - **长任务进度回调 + 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`)。
54
+ - **自动换行(WPS 风格 toggle)**:工具栏内置 `wrap-text` + 右键菜单「自动换行」+ 命令式 API `toggleWrapTextOnSelection()` / `getSelectionWrapState()`(`'all'/'none'/'mixed'`)。行为对齐 WPS:选区**全已换行 → 全部关掉**;否则**全部打开**;mixed → all。行高自动按内容重撑(失效 autofit 缓存触发),延续"只扩不缩"语义。入命令栈单次撤销 style。两壳同构(Vue 工具栏 `wrap-text` builtin + 右键;React 通过命令式 API + 右键)。
55
+ - **HiDPI/系统缩放 canvas 对齐**:Windows 125%/150% 缩放、浏览器 Ctrl+缩放(`devicePixelRatio≠1`)下,canvas 作为"替换元素"默认会按 `width*dpr` 显示,导致整个网格被放大、与 DOM 叠加层(浮动图/图表/HTML 文本框)及鼠标命中错位(越往右下偏得越多)。修复:`render()` 显式把 canvas 的 CSS `width/height` 钉成 view 逻辑尺寸,缓冲仍是 dpr 倍(高清),被浏览器降采样显示 → 像素与逻辑坐标 1:1 对齐。
56
+ - **富粘贴(从 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?)`。
57
+ - **图片(多通道,硬需求)**:① HTML 里的 data-uri `<img>` → 落格转内嵌图;② 剪贴板单张图片(`image/png` 等)→ 落活动格(`pasteImageBlob`);③ 拖图片文件进网格(消费方接 `pasteImageBlob`)。**已知边界**:Excel/WPS **区域复制的内嵌图一般进不了浏览器剪贴板**(只给 text/html+text/plain),所以"复制一整块带图、粘贴时图一起来"做不到 —— 这是浏览器固有限制,需用上述替代通道(单独复制一张图 / 拖文件)。
58
+ - **图片点击放大 + 下载原图**:网格里的图(WPS 内嵌图 DISPIMG / 浮动图)可点开看大图、下载原始字节。框架无关 `LightboxHost`(body 级暗背景 + 居中大图 + 「下载原图」+ 点背景/Esc/关闭按钮)。触发:**只读模式单击图**放大;**编辑模式右键**「查看大图 / 下载原图」(不抢选区/编辑)。新 prop `imageLightbox`(默认 true);新 API `openImageLightbox(src,fileName?)` / `getCellImageAt(row,col)`。顺带给点击加 3px 拖拽死区(微抖不再被当拖动,单击语义更稳)。修了 Vue 布尔 prop 缺省被判 false 的坑(`imageLightbox` 加进 withDefaults)。
59
+ - **虚拟空行/空列(滚动自动延伸,不动 dimension)**:滚到数据末尾下方仍有空行/空列可滚动、选中、编辑,像 Excel/WPS 的"无限网格";但**不写进 dimension/文件**(避免体积变大)——只有真去编辑某空格,它才靠 `growDimension` 变实。`GridMetrics` 加虚拟范围(`vRows/vCols/virtualWidth/Height`,封顶 Excel 上限 1048576×16384);`totalWidth/Height` 仍按 dimension(**导出/data-access 不含虚拟空行**);spacer 尺寸 / 可视区 / 命中夹取改用虚拟范围;控制器 `recomputeVirtualExtent()`(滚动/缩放/resize 时只增不减、按需延伸)。纯 core,双壳自动继承;新 API `getVirtualExtent()`。
60
+ - **背景色 / 字体色(回显 + 修改)**:新 API `getActiveFillColor()` / `getActiveFontColor()`(回显活动格当前色,#RRGGBB)+ `setSelectionFill(color|null)` / `setSelectionFontColor(color)`(改选区,入命令栈)。两壳 demo 加 WPS 风格的背景/字体取色器 + 清除填充。
61
+ - **修复内嵌图"灰底"**:DISPIMG 图加载中 / 缺登记项时,之前画 `#f2f4f7` 灰底盖住了单元格自身填充色(白),看着像默认灰。改为加载中不画底色(露出单元格白填充),仅缺图时画个淡图标、不盖色。
62
+ - **公式栏(Fx 内容条)可编辑 + 联动**:顶部公式栏从只读改为可编辑 `<input>`(editable + 该格非只读时)。在栏里输入提交(回车下移、Esc 取消、失焦提交)→ 改活动格;切选区 / 格内编辑 → 栏即时反映。栏显示**可编辑字符串**:公式 `=...`、数值原始数字串(非格式化,避免编辑货币/千分位被当文本)、布尔 TRUE/FALSE。新 API:`getCellEditString()` / `canEditActiveCell()` / `commitActiveCellValue(value, move?)`。仅值真变化才入命令栈。两壳同构(React 顺带补:`cell-change` 触发 chrome 重渲)。
63
+ - **合并/拆分单元格**:`mergeCells(range)`(吸收相交旧合并、清空被覆盖格只留左上锚点)/ `unmergeCells(range)`,入命令栈可撤销。
64
+ - **粘贴**:`Ctrl+V` / `pasteText(text, at?)` —— TSV(制表符+换行)→ 区域写入,类型自动推断、跳过只读、整块一次撤销。
65
+ - **右键上下文菜单**:框架无关 body 级 DOM 菜单 —— 插入/删除行列、合并/拆分、清除内容、复制/粘贴;点外部/Esc/滚动关闭、贴边翻转。只读仍用浏览器默认菜单。
66
+ - **WPS 单元格内嵌图(DISPIMG)**:
67
+ - **展示**:解析 WPS 私有件 `xl/cellimages.xml` + 单元格 `=DISPIMG("id",1)` 公式,把图按行高列宽画进单元格内(随网格滚动/裁剪/冻结/缩放),普通工具打不开的 WPS 内嵌图现在能正常显示。新 `parser/cell-image-parser.ts`;模型加 `WorkbookModel.cellImages` + `CellModel.dispImgId`;canvas 渲染带图片解码缓存 + onload 重绘。
68
+ - **贴合方式可配置** `cellImageFit`:`contain`(默认,等比缩放,与 WPS 渲染一致——WPS 打开导出文件时 DISPIMG 固定按 contain 显示)/ `fill`(拉伸铺满随格变形)/ `cover`(等比裁剪铺满),两壳 prop + `setCellImageFit()` 运行时切换、即时重绘。
69
+ - **行 customHeight 保真**:解析 `<row customHeight="1">` 标记(ExcelJS 不暴露),自动行高跳过手动设高的行——避免长文本把"作者设矮放图"的行撑大,渲染/导出行高都与 WPS 一致。顺带放宽 fast-xml-parser 实体展开上限(大表几百个 `&quot;` 会撞默认 1000 上限致 drawing/row-meta 解析静默失败)。
70
+ - **就近 / 批量嵌入**:`convertImageToCellAuto(imgIdx)`(图压在哪格就嵌哪格,几何反推)/ `convertAllImagesToCells(col?)`(整表或整列批量,一次进撤销栈)/ `convertCellImageToFloat(row,col,size?)`(嵌入→浮动);右键单格菜单为「将此处浮动图嵌入 / 整列嵌入(N 张)/ 整表嵌入(N 张)/ 内嵌图转浮动图」。两向入命令栈(整簿快照逆)、发 `cell-change`/`image-change`、翻脏标记。`getCellImages()` 读登记表。`convertImageToCell(imgIdx,row,col)` 保留(显式目标格)。
71
+ - **导出往返**:ExcelJS 写出后在 zip 层回注 `cellimages.xml` + rels + media + `[Content_Types].xml`/`workbook.xml.rels` 补丁(新 `export/wps-cellimages.ts`,从模型重建),rebuild / overlay 两模式均覆盖。原有的 + App 内新转的内嵌图导出后用 WPS 打开都显示。验证:解析→导出→再解析 往返存活(单测)。
72
+ - **逐字节对齐真·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)`。单测锁定这些字段。
73
+
74
+ ### 性能
75
+ - **undo 快照轻量化**:增删行列的整簿快照从 `structuredClone`(深拷图片字节/图表)改为手写轻量克隆 —— 只克隆编辑会动的部分,**图片字节/图表/形状/条件格式按引用共享**(编辑期间不可变),大文件 + undo 栈内存大幅下降。惠及结构编辑 undo、脏状态 baseline、还原原件。
76
+
77
+ ### 修复
78
+ - **导出失败不再静默**:React 壳「导出 PDF」之前只把错误转给 `onError` prop,未接就被吞(无反应、控制台无报错)。两壳统一为 `console.error` + 上报(emit/onError)+ alert。**提醒**:PDF 导出需可选 peer `jspdf`(`npm i jspdf`);未装时现在会明确报错。
79
+
5
80
  ## [1.1.0] - 2026-06-05
6
81
 
7
82
  把 1.0.0 编辑能力的三处已知 v1 限制做成增强(向后兼容)。
package/README.md CHANGED
@@ -34,12 +34,15 @@ const src = ref<File>() // 绑个 <input type="file" @change> 给它即可
34
34
 
35
35
  ## 特性
36
36
 
37
- - 📊 **Canvas 高保真渲染**:DPR 高清、虚拟滚动(万行流畅)、冻结窗格四象限
37
+ - 📊 **Canvas 高保真渲染**:DPR 高清、虚拟滚动(万行流畅)、冻结窗格四象限、**滚动自动延伸空行/列**(像 Excel 无限网格,但不写进 dimension/文件)
38
38
  - 🔢 **自写数字格式引擎**:千分位/货币/百分比/科学计数/分数、四段格式(正;负;零;文本)、`[Red]` 颜色、`[>=100]` 条件段、中文日期 `yyyy"年"`、`[h]:mm` 经过时间
39
39
  - 🗓 **日期序列号**:含 Excel 1900 闰年 bug、1904 系统
40
40
  - 🎨 **主题色 + tint**、indexed 调色板、合并单元格、边框(细/粗/虚/双线)、填充(纯色/图案/渐变)
41
41
  - 🌈 **条件格式**:色阶 / 数据条 / 图标集 / cellIs / top10
42
42
  - 🖼 **图片 + 图表**(DrawingML → ECharts 近似还原)、**形状/文本框**、**迷你图**(sparklines)、**批注**、**数据验证**下拉、**自动筛选**样式
43
+ - 📌 **WPS 单元格内嵌图(DISPIMG)**:识别并展示 WPS 私有的"嵌在格里的图"(普通工具会缺图);编辑模式下支持**一键浮动 ⇄ 嵌入互转**。见 [WPS 单元格内嵌图](#wps-单元格内嵌图dispimg)
44
+ - 🔍 **图片点击放大 + 下载原图**:网格里的图(内嵌图/浮动图)点开看大图、下载原件。只读模式单击图放大、编辑模式右键「查看大图」。`imageLightbox` prop 控制(默认开),`openImageLightbox(src)` 命令式打开。
45
+ - 📋 **从 Excel/WPS 富粘贴**:`Ctrl+V` 解析剪贴板 HTML → 完美还原字体/颜色/填充/边框/对齐/合并单元格,整块单次撤销。图片走多通道(data-uri / 单图 / 拖文件);**注**:Excel 区域复制的内嵌图进不了浏览器剪贴板(浏览器限制)。
43
46
  - 📝 **文本溢出**到相邻空格、**自动行高**
44
47
  - 🖱 **交互**:单元格选区(合并感知)、拖选、公式栏、状态栏(计数/求和/均值/最值)、超链接可点、裁切文本悬停看全文、Ctrl+C 复制为 TSV、**Ctrl+F 查找**(高亮 + 上/下定位 + 计数 + 区分大小写/全字匹配)、**自动筛选**(点下拉真能筛:去重值多选 + 搜值 + 清除)
45
48
  - 🖨 **导出 / 打印**:整表/选区/多表导出 **PNG/JPEG**、**PDF**(位图 + **矢量·文字可选可搜**两种)、**系统打印**(可另存 PDF);默认还原原生 `pageSetup`(纸张/方向/页边距/缩放/打印区域/**打印标题行列每页重复**);宽表**横向跨页**(页矩阵);`beforeRenderPage` 注入页眉/页脚/水印、`configureDoc` 注册字体;内置「导出设置」对话框
@@ -192,6 +195,44 @@ viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区
192
195
 
193
196
  > 想要更底层的渲染模型,仍可直接用 `parseWorkbook` 的返回值 / `getWorkbook()` / `@rendered`(`WorkbookModel`:`sheets[].cells: Map<"row:col">`、`styles[styleId]`)。
194
197
 
198
+ ## UI 区域速查(给调用方 & 二开者)
199
+
200
+ `<ExcelViewer>` 渲染出的 chrome 自上而下分这几块,每块都有**名字 / DOM 选择器 / 文件 / 替换方式**,改样式或替换某块时按这张表找位置。
201
+
202
+ ```
203
+ ┌─ <ExcelViewer> .excel-viewer (Vue) / .rxl (React) ─────────────────────────┐
204
+ │ ① Header (顶栏) <ViewerToolbar> ← #header slot 整条替换│
205
+ │ 文件名 · 表数 · 缩放 · 导出 ▾ │
206
+ ├────────────────────────────────────────────────────────────────────────────┤
207
+ │ ② ActionToolbar (操作栏) .action-toolbar ← :toolbar 配置/插件 │
208
+ │ [查找][筛选][复制][自动换行][冻结][缩放][导出] … 自动「⋯ 更多」 │
209
+ ├────────────────────────────────────────────────────────────────────────────┤
210
+ │ ③ FormulaBar (公式栏 Fx) .formula-bar ← (无 slot,可全局 CSS)│
211
+ ├────────────────────────────────────────────────────────────────────────────┤
212
+ │ ④ RenderArea (网格区) .render-area + canvas.grid-canvas │
213
+ │ ├ OverlayManager 图片/图表/形状/插件 overlay(DOM 叠加,随滚动) │
214
+ │ ├ ContextMenu .ooxml-context-menu(body 级,框架无关) │
215
+ │ ├ LightboxHost .ooxml-lightbox(body 级,图片放大+下载) │
216
+ │ └ CellEditorHost .editor-slot(自定义/内置 cell 编辑器) │
217
+ ├────────────────────────────────────────────────────────────────────────────┤
218
+ │ ⑤ SheetTabs (表标签) .sheet-tabs │
219
+ └────────────────────────────────────────────────────────────────────────────┘
220
+ ```
221
+
222
+ | 区域 | DOM / class | 文件(壳) | 替换 / 自定义方式 |
223
+ |---|---|---|---|
224
+ | ① Header 顶栏 | `<ViewerToolbar>` (Vue) | [components/ViewerToolbar.vue](src/components/ViewerToolbar.vue) | `<template #header>` slot 整条替换;插槽签名见 README「分层 UI」 |
225
+ | ② 操作栏 | `.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 })` 追加项 |
226
+ | ③ 公式栏 | `.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` |
227
+ | ④ 网格区 canvas | `.render-area > canvas.grid-canvas` | core: [render/canvas-renderer.ts](src/core/render/canvas-renderer.ts) | 渲染钩子 `cellStyle` / `transformModel` / 插件 `overlay` 返回 DOM;`:theme` 改配色 |
228
+ | ④a Overlay 层 | `.overlay-host` 四象限(主/冻结行/冻结列/冻结角) | core: [viewer/overlay-manager.ts](src/core/viewer/overlay-manager.ts) | `<template #overlay>` slot (Vue) / `overlay` prop 返回 DOM (React)/ 插件 `overlay` |
229
+ | ④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 })` 链式贡献 |
230
+ | ④c 图片灯箱 | `.ooxml-lightbox`(body 级) | core: [viewer/lightbox-host.ts](src/core/viewer/lightbox-host.ts) | `imageLightbox={false}` 关闭;命令式 `openImageLightbox(src,fileName?)` |
231
+ | ④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「自定义编辑器」 |
232
+ | ⑤ 表标签 | `.sheet-tabs` | [components/SheetTabs.vue](src/components/SheetTabs.vue) / 内置于 React 壳 | 暂无 slot;`.excel-viewer .sheet-tabs` CSS 覆写 |
233
+
234
+ **重要: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 组件时不会出现**。
235
+
195
236
  ## API
196
237
 
197
238
  ### `<ExcelViewer>`
@@ -199,13 +240,24 @@ viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区
199
240
  | Prop | 类型 | 说明 |
200
241
  |---|---|---|
201
242
  | `src` | `File \| Blob \| ArrayBuffer \| Uint8Array \| string(URL)` | 要预览的 .xlsx 数据源 |
243
+ | `workbook` | `WorkbookModel \| JsonInput` | **JSON 直渲(P3)** 优先于 `src`。WorkbookModel 直用;JsonInput 走 `jsonToWorkbook` 自动构造:① `unknown[][]` 二维数组 ② `Record<string,unknown>[]` 对象数组(首行表头) ③ `{ sheets:[...] }` 多表 |
244
+ | `jsonOptions` | `JsonLoadOptions` | JSON 直渲选项(`workbook = JsonInput` 时生效):`headerRow` / `sheetName` / `autoInfer`(数字串→数字、ISO 日期串→Date,默认 on) |
245
+ | `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` 项可在运行时切换 / 导入 / 清除 |
246
+ | `templateName` | `string` | 模板显示名(标题栏 `· 模板: xxx` 后缀);不给则取运行时 File.name。`:fileName` 同步:JSON 源未给名时默认 "JSON 数据" |
247
+ | `exportProgress` | `boolean`(默认 `true`)| **内置导出进度遮罩(P1.5)** 调 `viewer.downloadPdf` / `downloadImage` / `downloadXlsx` / `print` / 选区图片批量转换 时,壳自动建 `AbortController` + 接 `onProgress` → 显示居中模态(stage 标签 + 进度条 + 取消)。**关闭**用 `false`(纯回调走 `opts.onProgress`/`signal`);**自渲染**用 `#export-progress` 插槽(Vue)/ `renderExportProgress` (React) |
248
+ | `contextMenu` | `false \| ContextMenuTransform` | **右键菜单(Plan C)** — 三层开放:① 默认 = 内置菜单(editable 时弹) ② `false` = 不弹内置(`@before-context-menu` / `@context-menu` 事件仍触发,自渲染) ③ `(ctx, items) => MenuItem[] \| undefined` = transform 回调,在内置 items 上加 / 减 / 重排 |
202
249
  | `fileName` | `string` | 标题栏显示的文件名(可选) |
203
250
  | `editable` | `boolean` | 开启编辑(默认 `false` = 只读,行为与历史一致) |
204
251
  | `cellReadOnly` | `(cell, pos) => boolean` | 按格只读判定(编辑时) |
205
- | `readOnlyRanges` | `MergeRange[]` | 只读区域(命中即只读) |
252
+ | `readOnlyRanges` | `MergeRange[]` | 只读区域(命中即只读,黑名单) |
253
+ | `editableTargets` | `EditableTarget \| EditableTarget[]` | **可编辑白名单**(2026-06-08)— 设了就是白名单语义:默认只读,**只**命中**任一** target 的格可编辑。4 种 target 形状自动识别:`{row,col}` 单格 / `{row}` 整行 / `{col}` 整列 / `MergeRange` 矩形。单值或数组都行,允许**不相邻**多 target。`undefined` (不传) = 不启用白名单 = 老行为(默认全可编辑);`[]` (显式空) = 全只读。与 `readOnlyRanges` / `cellReadOnly` 叠加 — 白名单命中后仍可被二次"黑"掉。运行时改:命令式 `viewer.setEditableTargets(targets)` |
254
+ | `strictDimensions` | `boolean` | **严格尺寸闸门**(Phase B, 2026-06-08)— 默认 `false`:`setColumnWidth` / `setRowHeight` / `autoFit` 仅受全局 `editable` 控制。设 `true` + 启用了 `editableTargets` → 该列/行至少有 1 格在白名单内才能改尺寸,否则 skip + emit `permission-denied` (`reason='dimension'`)。 |
255
+ | `readOnlyCellStyle` | `boolean \| CellStyleOverride \| CellStyleFn` | **只读视觉钩子**(Phase C, 2026-06-08)— 默认 `false` 无视觉差异(老行为);`true` 套内置浅灰底 `#f5f7fa`;对象 = 固定样式给所有只读格;函数 = 按格自定义。仅在该格 `editable=false` 时套用,跟 `editableTargets` 配合一眼看出哪些格可编辑。**鼠标光标**: 编辑模式下悬停只读格自动变 `not-allowed`(内置,不可关)。 |
206
256
  | `editor` | `EditorResolver` | 自定义单元格编辑器工厂(返回任意 DOM) |
207
257
  | `recalc` | `boolean` | 公式重算(默认 `false`;需 `editable`) |
208
258
  | `formulaEngine` | `FormulaEngineFactory` | 自定义/自研公式引擎(默认 HyperFormula) |
259
+ | `cellImageFit` | `'fill' \| 'contain' \| 'cover'` | WPS 单元格内嵌图贴合方式(默认 `contain` 等比,与 WPS 渲染一致) |
260
+ | `imageLightbox` | `boolean` | 图片点击放大灯箱(默认 `true`;只读单击图放大、编辑右键「查看大图」) |
209
261
 
210
262
  > 编辑相关 props 详见下方 [编辑](#编辑可选默认只读) 章节。
211
263
 
@@ -255,12 +307,18 @@ viewer.value.getRangeData(viewer.value.getSelection()) // 取"我选中的"区
255
307
  | 类别 | 方法 |
256
308
  |---|---|
257
309
  | 值 | `editCell(row,col,value)` · `editRange(range,values[][])` · `clearRange(range)` |
310
+ | 粘贴 | `pasteText(tsv,at?)` · `pasteRichHtml(html,at?)`(Excel/WPS 复制 → 字体/颜色/填充/边框/对齐/合并 + 单次撤销) · `pasteImageBlob(blob,at?)`(单图/拖文件落格) |
258
311
  | 样式 | `setStyle(range, patch)`(`patch` = `CellStyleOverride`:font/fill/borders/对齐/numFmt) |
312
+ | 背景/字体色 | `getActiveFillColor()` · `getActiveFontColor()`(回显活动格当前色 #RRGGBB) · `setSelectionFill(color\|null)`(null=清除填充) · `setSelectionFontColor(color)` |
313
+ | 自动换行 | `getSelectionWrapState()`→`'all'\|'none'\|'mixed'`(工具栏 active 用) · `toggleWrapTextOnSelection()`(WPS 风格 toggle:全开/全关;行高按内容重撑,只扩不缩) |
259
314
  | 列宽行高 | `setColumnWidth(col,px)` · `setRowHeight(row,px)` |
260
315
  | 行列结构 | `insertRows(at,count?)` · `deleteRows(at,count?)` · `insertCols(at,count?)` · `deleteCols(at,count?)` |
261
316
  | 图片 | `getImages()` · `addImage(anchor)` · `removeImage(i)` · `moveImage(i,dxPx,dyPx)` · `resizeImage(i,wPx,hPx)` |
317
+ | WPS 内嵌图 | `getCellImages()` · `setCellImageFit('fill'\|'contain'\|'cover')` · `convertImageToCellAuto(imgIdx)`(就近) · `convertAllImagesToCells(col?)`(整表/整列) · **`convertImagesInRangeToCell(range)`**(选区批量,单次撤销) · `convertCellImageToFloat(row,col,size?)`(嵌入→浮动) · **`convertCellImagesInRangeToFloat(range, size?)`**(选区批量浮动化) · `convertImageToCell(imgIdx,row,col)`(显式目标) |
318
+ | 图片放大 | `openImageLightbox(src,fileName?)`(命令式弹大图+下载) · `getCellImageAt(row,col)`(某格是否内嵌图→`{id,src,mime}`) |
262
319
  | 撤销/进编辑 | `undo()` · `redo()` · `canUndo()` · `canRedo()` · `beginEdit(row,col)` · `cancelEdit()` · `isEditing()` · `getEditingCell()` |
263
- | 查询/状态 | `getCellSnapshot(row,col)` · `isDirty()` · `resetToOriginal()` · `isRecalcReady()` |
320
+ | 公式栏 | `getCellEditString()`(活动格可编辑字符串:公式→`=…`,数值→原始数字串) · `canEditActiveCell()` · `commitActiveCellValue(value, move?)`(顶部 Fx 公式栏可编辑并与单元格联动,底层即用这套) |
321
+ | 查询/状态 | `getCellSnapshot(row,col)` · **`inspectCell(row,col)`**(全息体检:snapshot + 合并区 + 浮动图覆盖 + WPS 内嵌图 + 数据验证 + 条件格式命中 + 链接/批注) · `isDirty()` · `resetToOriginal()` · `isRecalcReady()` · `getVirtualExtent()`(当前虚拟行列范围) |
264
322
  | 导出 | `exportXlsx/downloadXlsx` · `exportJson/downloadJson` · `exportCsv/downloadCsv`(见 [导出](#导出--打印)) |
265
323
 
266
324
  所有写操作(含拖拽改宽高/移图)**统一进撤销栈**、发对应事件、翻**脏标记**;`resetToOriginal()` 一键放弃全部修改、还原到刚加载的原件。
@@ -284,6 +342,19 @@ const myEditor: EditorResolver = (cell, pos) => {
284
342
 
285
343
  插件也可经 `editor` 字段贡献(多插件数组序,组件 prop 最后覆盖),与 `cellStyle`/`overlay` 同范式。
286
344
 
345
+ ### WPS 单元格内嵌图(DISPIMG)
346
+
347
+ 不少 .xlsx 由 **WPS** 导出,其"嵌在单元格里的图"用了 WPS 私有存法(`xl/cellimages.xml` + 单元格 `=DISPIMG("id",1)` 公式),标准解析读不出来 → 普通工具打开会**缺图**。本组件:
348
+
349
+ - **自动识别并展示**:解析 `cellimages.xml` 登记表,把图**画进单元格内**(随行高列宽 / 滚动 / 裁剪 / 冻结 / 缩放),非浮动叠加。**贴合方式可配置** `cellImageFit`:`contain`(默认,等比缩放,**与 WPS 渲染一致**——WPS 打开导出文件时 DISPIMG 固定按 contain 显示)/ `fill`(拉伸铺满随格变形)/ `cover`(等比裁剪铺满)。`getCellImages()` 读登记表;`getCellSnapshot(row,col).cell.dispImgId` 看某格是否内嵌图。
350
+ - **一键浮动 ⇄ 嵌入互转**(编辑模式):
351
+ - `convertImageToCellAuto(imgIdx)` —— **就近嵌入**:图压在哪个单元格上就嵌进哪格(几何反推,无需手指定目标格)。
352
+ - `convertAllImagesToCells(col?)` —— **整表/整列批量**就近嵌入(`col` 给定只嵌该列),一次进撤销栈(单次 Ctrl+Z 全撤),返回嵌入张数。
353
+ - `convertCellImageToFloat(row,col,size?)` —— 内嵌图拎回浮动图。
354
+ - 右键单格菜单:「将此处浮动图嵌入单元格 / 整列浮动图嵌入(N 张)/ 整表浮动图嵌入(N 张)/ 内嵌图转为浮动图」。
355
+ - 全部入撤销栈、发 `cell-change`/`image-change`、翻脏标记。(`convertImageToCell(imgIdx,row,col)` 仍保留,用于显式指定目标格。)
356
+ - **导出往返**:`downloadXlsx()` / `exportXlsx()` 导出时,在 ExcelJS 写出后**于 zip 层回注** WPS 私有件(`cellimages.xml` + rels + media + `[Content_Types].xml`/`workbook.xml.rels` 补丁,从模型重建),原有的 + App 内新转的内嵌图导出后用 WPS 打开都正常显示。rebuild / overlay 两种保真模式均覆盖;无字节的 blob-only 图除外。
357
+
287
358
  ### 公式重算(可换引擎)
288
359
 
289
360
  开 `: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 +424,38 @@ const myEditor: EditorResolver = (cell, pos) => {
353
424
 
354
425
  公共选项:`target`(`'active'`(默认)/`'all'`/索引/索引数组)、`range`(限定单元格区域)、`scale`(清晰度,默认 2)、`includeHeaders`、`gridlines`、`background`;PDF/打印另有 `format`(a4/a3/letter/`[宽,高]mm`)、`orientation`、`margin`(mm)、`fitToWidth`。
355
426
 
427
+ **长任务进度 + 取消 + 内置遮罩(P1 + P1.5)** —— **两层**叠加,默认开箱即用,可逐层覆盖:
428
+
429
+ #### ① Core 层(协议):`onProgress` + `signal`
430
+ 所有导出方法(PNG / PDF / XLSX / Print)+ 选区图片批量互转 统一接:
431
+ ```ts
432
+ const ctrl = new AbortController()
433
+ try {
434
+ await viewer.value.downloadPdf({
435
+ target: 'all',
436
+ onProgress: (p) => console.log(p.stage, p.ratio, p.label), // 'render'/'compose'/'paginate'/'write'/'zip'/'convert'
437
+ signal: ctrl.signal,
438
+ })
439
+ } catch (e) {
440
+ if ((e as Error).name === 'AbortError') console.log('用户取消')
441
+ else throw e
442
+ }
443
+ ctrl.abort() // 任意时刻取消
444
+ ```
445
+ 导出全链路在调度点 `await yieldToEvent()` 让出 UI(防假死)+ 调度前 `checkAborted(signal)`(立刻中断)。`ExcelJS.writeBuffer` / `jsPDF` 内部仍是黑盒(`zip`/`write` 阶段),那两段无法细分,但全程都有可视进度。
446
+
447
+ #### ② Shell 层(UI):**内置居中模态**(默认开)
448
+ **不传任何参数**调 `viewer.downloadPdf()` 等异步方法,壳自动建 `AbortController` + 接 `onProgress` → 显示**居中模态**(stage 标签 + 进度条 + 取消按钮)。用户传入的 `onProgress`/`signal` **仍正常链回调**(并存,不冲突)。
449
+
450
+ #### ③ 关闭 / 覆盖
451
+ | 需求 | 做法 |
452
+ |---|---|
453
+ | 完全关掉内置遮罩(纯回调) | `<ExcelViewer :export-progress="false">`(Vue)/ `exportProgress={false}` (React) |
454
+ | 自渲染(用 Element Plus / Ant Design 等自家组件) | **Vue**:`<template #export-progress="{ state, busy, cancel }">…</template>` 插槽;**React**:`renderExportProgress={({state,busy,cancel}) => <YourModal …/>}` |
455
+ | 既要内置又自动注入跟踪 | 默认行为已是 —— 用户传 `{ onProgress, signal }` 仍被链回调,内置 UI 也照常显示 |
456
+
457
+ 覆盖矩阵:**导出**(PDF / PNG / XLSX / Print)、**选区图片批量互转**(P2,壳侧 1.2.0 起返 `Promise<number>` 以接遮罩)。**不包含**:文件解析(parsing 有独立的顶栏进度条,与本遮罩分开)、`copySelection` / `setStyle` 等瞬时操作、模板样式 overlay(P3 重设计后是同步纯函数,耗时可忽略)。
458
+
356
459
  **默认还原 OOXML 原生页面设置** —— PDF/打印时,未显式指定的 `format`/`orientation`/`margin`/`fitToWidth` 自动取自工作表的 `pageSetup`(纸张、方向、页边距、适应页面/缩放),并应用**打印区域**(默认导出范围)与**打印标题行/列**(每页顶部/左侧重复)。显式传入的选项始终覆盖之。
357
460
 
358
461
  **分页** —— `fitToWidth: true`(默认)把整表缩放到页宽、只竖向跨页;`fitToWidth: false`(或工作表未设"适应页面")按自然尺寸×缩放,**宽表横向跨页 + 高表竖向跨页**(像 Excel 的页矩阵,顺序"先下后右"),此时打印标题列在每张横向页左侧重复。
@@ -396,6 +499,46 @@ await viewer.value.downloadPdf({
396
499
  ```
397
500
  > 提示:中文表格若不注册字体,矢量模式会产生很多小图、文件偏大且较慢 —— 注册一个子集字体即可全矢量。
398
501
 
502
+ ### 右键菜单(Plan C:三层开放)
503
+
504
+ 默认 `editable` 时显示内置菜单(复制/粘贴/插入/删除/合并/拆分/自动换行/清除内容 + WPS 图片互转)。三种覆盖方式可同时使用:
505
+
506
+ **① 加 / 减 / 重排内置项** —— 用 `:contextMenu` 传 transform:
507
+ ```vue
508
+ <ExcelViewer
509
+ :context-menu="(ctx, items) => [
510
+ ...items,
511
+ { separator: true },
512
+ { label: `导出此格 PDF (${ctx.activeCell.row + 1},${ctx.activeCell.col + 1})`, action: () => viewer.downloadPdf() },
513
+ ]"
514
+ />
515
+ ```
516
+ - `ctx`: `{ range, single, activeCell, sheet, workbook, editable }` — 当前选区 + 活动格 + 模型句柄
517
+ - `items`: 内置 `MenuItem[]`(`{label, action, disabled, separator}`)—— 加、过滤、重排,返回新数组生效;返 `undefined` / `void` 用原样
518
+
519
+ **② 接管渲染**(用自家 UI 框架的菜单,如 Element Plus / Radix / Headless UI):
520
+ ```vue
521
+ <ExcelViewer
522
+ :context-menu="false" <!-- 关闭内置弹层(事件仍触发) -->
523
+ @before-context-menu="(p) => p.preventDefault()"
524
+ @context-menu="(p) => myMenu.show(p.x, p.y, p.items)"
525
+ />
526
+ ```
527
+ - `@before-context-menu` 在内置弹出前触发;调 `payload.preventDefault()` 取消内置;`:contextMenu="false"` 等价于自动 preventDefault
528
+ - `@context-menu` 在内置弹出后(或被 prevent 后)触发,拿到 `{ x, y, ctx, items }` —— **总会触发**,自渲染只需监听这个事件
529
+ - React:`onBeforeContextMenu` / `onContextMenuShow` 接同形 payload
530
+
531
+ **③ 命令式打开 / 关闭**(键盘 Shift+F10、工具栏触发、跨层调用):
532
+ ```ts
533
+ viewer.openContextMenu(clickX, clickY) // 按当前选区算内置 items
534
+ viewer.openContextMenu(clickX, clickY, customItems) // 直接喂自定义 items
535
+ viewer.closeContextMenu()
536
+ ```
537
+
538
+ **插件贡献**:`definePlugin({ contextMenu: (ctx, items) => [...] })` —— 多插件按数组顺序串行(后者拿前者输出),组件 `:contextMenu` prop 最后覆盖,顺序固定 `内置 → 插件 → prop`。
539
+
540
+ `MenuItem` / `ContextMenuCtx` / `ContextMenuTransform` 全部从 `ooxml-excel-editor/core` 导出(TS 类型完整)。
541
+
399
542
  ### 操作工具栏(可配置 / 可插件 / 响应式)
400
543
  顶栏(文件名/导出/缩放)下方有一行**操作工具栏**,内置 `find`/`filter` 按钮默认显示。用 `:toolbar` 配置:
401
544
  ```vue
@@ -403,7 +546,7 @@ await viewer.value.downloadPdf({
403
546
  <ExcelViewer :toolbar="['find','filter','separator','zoom','export']" /> <!-- 控制项/顺序/分隔 -->
404
547
  <ExcelViewer :toolbar="false" /> <!-- 隐藏整条 -->
405
548
  ```
406
- - **内置 id**:`find`(查找)、`filter`(切换自动筛选 —— 文件没设也能点出下拉)、`clear-filter`(清除筛选,无筛选时禁用)、`copy`(复制选区)、`freeze`(冻结/取消)、`zoom`(缩放下拉)、`export`(导出/打印下拉)、`'separator'`/`'|'`(分隔线);`sort` 规划中。
549
+ - **内置 id**:`find`(查找)、`filter`(切换自动筛选 —— 文件没设也能点出下拉)、`clear-filter`(清除筛选,无筛选时禁用)、`copy`(复制选区)、`wrap-text`(自动换行 toggle,WPS 风格,需 `editable`)、`image-tools`(图片工具 ▾:选区/整表/整列 浮动 ⇄ 嵌入互转,需 `editable`)、`template`(模板 ▾:仅 JSON / 模型数据源下生效;导入 .xlsx 当样式捐赠者;xlsx 数据源下禁用)、`freeze`(冻结/取消)、`zoom`(缩放下拉)、`export`(导出/打印下拉)、`'separator'`/`'|'`(分隔线);`sort` 规划中。
407
550
  - **富项类型**(`ToolbarItem`):`type:'separator'` 分隔线;`items: ToolbarItem[]` 变下拉子菜单;`disabled?(viewer)` 禁用态;`iconSvg`(内联 SVG,优先于 `icon` emoji)/ `icon` / `label` / `title` / `onClick(viewer)` / `active?(viewer)`。
408
551
  - **响应式溢出**:宽度不足时,放不下的项自动折叠进「⋯ 更多」下拉。
409
552
  - **插件贡献**:`ExcelPlugin.toolbar: ToolbarItem[]`,插件加载即追加(opt-in)。
@@ -490,6 +633,7 @@ npm run build:demo # 构建 demo 站点
490
633
  - [ARCHITECTURE.md](./ARCHITECTURE.md) —— 包/入口、core 分层、数据流、`ViewerController` 桥接、"加功能改哪"
491
634
  - [CONTRIBUTING.md](./CONTRIBUTING.md) —— 本地跑通、改动流程、不可破坏的硬约束
492
635
  - [CHANGELOG.md](./CHANGELOG.md) / [RELEASING.md](./RELEASING.md) —— 变更记录 / 发布清单
636
+ - [docs/编辑权限与只读边界.md](./docs/编辑权限与只读边界.md) —— **EditableTarget 白名单 / DimTarget 尺寸多形态 / readOnlyCellStyle 视觉钩子 / permission-denied 事件** 体系化说明(1.2.0)
493
637
 
494
638
  > **React props/events** 与 Vue 对齐(事件用 camelCase 回调:`onRendered`/`onError`/`onCellClick`/`onSelectionChange`/`onSheetChange`/`onHyperlinkClick`),命令式句柄 `ExcelViewerHandle` 与 Vue 组件 ref 同名方法一致。上面「扩展 API」中的**插件 `definePlugin`** 目前服务 Vue 壳;React 壳已可用全部 props/命令式 API/事件,插件 overlay 跨框架化在路线图中。
495
639