@4399ywkf/editor 0.4.4 → 0.6.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/README.md +169 -10
- package/dist/PptxView-ZH4FZKNO.js +3 -0
- package/dist/PptxView-ZH4FZKNO.js.map +1 -0
- package/dist/SheetView-DSADIZNS.js +3 -0
- package/dist/{SheetView-2EID6B2I.js.map → SheetView-DSADIZNS.js.map} +1 -1
- package/dist/chunk-32WQX3NR.js +37 -0
- package/dist/chunk-32WQX3NR.js.map +1 -0
- package/dist/chunk-5XG4YMAB.js +149 -0
- package/dist/chunk-5XG4YMAB.js.map +1 -0
- package/dist/chunk-L6ZR5HHW.js +84 -0
- package/dist/chunk-L6ZR5HHW.js.map +1 -0
- package/dist/csv.d.ts +83 -0
- package/dist/csv.js +153 -0
- package/dist/csv.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts +1 -1
- package/dist/pptx.d.ts +36 -0
- package/dist/pptx.js +3 -0
- package/dist/pptx.js.map +1 -0
- package/dist/registry-BBL8yeIT.d.ts +148 -0
- package/dist/sheet.d.ts +8 -4
- package/dist/sheet.js +1 -1
- package/dist/types-C37MAt8o.d.ts +178 -0
- package/dist/xlsx.d.ts +100 -0
- package/dist/xlsx.js +588 -0
- package/dist/xlsx.js.map +1 -0
- package/package.json +18 -2
- package/dist/SheetView-2EID6B2I.js +0 -3
- package/dist/chunk-RJUDOUEZ.js +0 -56
- package/dist/chunk-RJUDOUEZ.js.map +0 -1
- package/dist/registry-CvGg65s9.d.ts +0 -68
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @4399ywkf/editor
|
|
2
2
|
|
|
3
|
-
4399 多格式文档编辑器。一个包覆盖 **doc / sheet / pdf**,引擎按需下载。
|
|
3
|
+
4399 多格式文档编辑器。一个包覆盖 **doc / sheet / pdf / pptx**,引擎按需下载。
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm add @4399ywkf/editor
|
|
@@ -20,13 +20,13 @@ import "@4399ywkf/editor/styles"
|
|
|
20
20
|
| **doc**(含云文档) | tiptap | ✅ | ✅ | ✅ | 内置 |
|
|
21
21
|
| **sheet** | Univer OSS 核心 | ✅ | ✅ | ✕ | 可选 peer `@univerjs/presets` |
|
|
22
22
|
| **pdf** | pdf.js | ✕ 只读 | ✕ | ✕ | 可选 peer `pdfjs-dist` |
|
|
23
|
-
| **pptx** |
|
|
23
|
+
| **pptx** | pptx-preview | ✕ 只读 | ✕ | ✕ | 可选 peer `pptx-preview` |
|
|
24
24
|
|
|
25
25
|
云文档与本地 doc 是**同一套 tiptap**,不做区分。
|
|
26
26
|
|
|
27
27
|
## 为什么引擎是懒加载的
|
|
28
28
|
|
|
29
|
-
主入口只有 **3KB**(格式路由),tiptap 的 698KB 在独立 chunk 里,Univer / pdf.js
|
|
29
|
+
主入口只有 **3KB**(格式路由),tiptap 的 698KB 在独立 chunk 里,Univer / pdf.js / pptx-preview 都是可选 peer。
|
|
30
30
|
`import "@4399ywkf/editor"` 不会把任何引擎拉进你的主包 —— 只有真的渲染到某种格式时才下载。
|
|
31
31
|
|
|
32
32
|
Univer sheets 核心 gzip 后 871KB;无条件打进主包会直接毁掉阅读侧首屏。
|
|
@@ -36,11 +36,167 @@ Univer sheets 核心 gzip 后 871KB;无条件打进主包会直接毁掉阅读
|
|
|
36
36
|
```ts
|
|
37
37
|
import { NotionEditor, ArticleViewer } from "@4399ywkf/editor/doc"
|
|
38
38
|
import { ImportDocx, ExportDocx } from "@4399ywkf/editor/docx"
|
|
39
|
+
import { downloadXlsx } from "@4399ywkf/editor/xlsx"
|
|
40
|
+
import { downloadCsv } from "@4399ywkf/editor/csv"
|
|
39
41
|
import { SheetView } from "@4399ywkf/editor/sheet"
|
|
40
42
|
import { PdfView } from "@4399ywkf/editor/pdf"
|
|
43
|
+
import { PptxView, repairPptx } from "@4399ywkf/editor/pptx"
|
|
41
44
|
import "@4399ywkf/editor/styles"
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
## pptx 只读预览
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add pptx-preview # 可选 peer,不装就落到「暂不支持」兜底
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<DocumentView format="pptx" source={file} onError={(e) => toast(e.message)} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**定位是「看清这页讲什么」,不是所见即所得。** 申报也照此写死:
|
|
58
|
+
`editable: false`、`aiOperable: false`。已知差距,接之前请先认下:
|
|
59
|
+
|
|
60
|
+
- **图表渲染不出数据** —— 轴画得出来,柱子/折线是空的,系列名和标题退化成
|
|
61
|
+
「系列1 / 图表标题」。该库打包了 ECharts,却没把 `c:cat` / `c:val` 接上去
|
|
62
|
+
- 缺字体时替换成系统字体,观感和原稿不一致
|
|
63
|
+
- 内部偶有 SVG 路径算出 NaN,个别形状画不出来
|
|
64
|
+
|
|
65
|
+
只认 OOXML:`.pptx` `.ppsx` `.potx` `.pptm` `.ppsm` `.potm`。
|
|
66
|
+
`.ppt` / `.pps` / `.pot` 是二进制复合文档、不是 zip,该库解不了,
|
|
67
|
+
所以**不申报** —— 让它落到「暂不支持」比装作能开要好。
|
|
68
|
+
|
|
69
|
+
### 为什么要先修一道
|
|
70
|
+
|
|
71
|
+
真实文件里大半份的 `[Content_Types].xml` 声明了 zip 中并不存在的 slideMaster。
|
|
72
|
+
PowerPoint 自己不介意,`pptx-preview` 会**整份静默失败**:不抛错、不报警,
|
|
73
|
+
`slideCount` 就是 0,面板一片空白。
|
|
74
|
+
|
|
75
|
+
17 份真实文件实测:**裸接只有 5 份能打开,内置 `repairPptx` 后 17 份全开**。
|
|
76
|
+
`PptxView` 已经自动做了这一道;直接用 `pptx-preview` 的宿主请自己先过:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { repairPptx } from "@4399ywkf/editor/pptx"
|
|
80
|
+
const { bytes, removed } = repairPptx(new Uint8Array(await file.arrayBuffer()))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
只在真有幽灵声明时才解压重打包(用已有的 fflate,不引 JSZip),
|
|
84
|
+
文件没问题就原样返回入参。17 份文件逐条目做过 sha256 比对:
|
|
85
|
+
除 `[Content_Types].xml` 外**字节全等**。
|
|
86
|
+
|
|
87
|
+
引擎选型与这道修复都对齐腾讯 WorkBuddy 内置的 pptx 预览。要更高保真,
|
|
88
|
+
用同名 `registerFormat` 覆盖本通道即可(见下)。
|
|
89
|
+
|
|
90
|
+
## 表格通道配置
|
|
91
|
+
|
|
92
|
+
Univer 的 UI 是 `univer-` 前缀的原子类,没有稳定选择器 —— **别用 CSS 硬藏工具栏**。
|
|
93
|
+
开关走 `sheet` 这个 prop,直接透传到 Univer preset:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<DocumentView
|
|
97
|
+
format="sheet"
|
|
98
|
+
content={snapshot}
|
|
99
|
+
readOnly
|
|
100
|
+
sheet={{
|
|
101
|
+
findReplace: true, // Ctrl+F 查找替换
|
|
102
|
+
ui: { footer: true, formulaBar: false }, // 底栏保留(多表切换靠它),公式栏收掉
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`ui` 可用:`header` / `toolbar` / `ribbonType` / `footer` / `formulaBar` / `contextMenu` /
|
|
108
|
+
`statusBarStatistic` / `menu`。
|
|
109
|
+
|
|
110
|
+
- **`readOnly` 时 `toolbar` 默认为 `false`** —— 整条 ribbon 在只读预览里全是死按钮,
|
|
111
|
+
窄面板里还挤。要它回来就显式传 `ui: { toolbar: true }`。
|
|
112
|
+
- `footer` 别随手关:多工作表时它是唯一的切表入口,关掉用户就只看得见第一张表。
|
|
113
|
+
- `findReplace` 不需要额外装包:`preset-sheets-find-replace` 是 `@univerjs/presets`
|
|
114
|
+
自己的 dependency,走子路径动态 import,没开就完全不下载。
|
|
115
|
+
|
|
116
|
+
其它能力(筛选、排序、条件格式、图片……)自己 import preset 传进来。
|
|
117
|
+
`createUniver` **不会**自动收集 preset 自带的 locales,得配套传 `locales`,
|
|
118
|
+
否则菜单会显示成原始 i18n key:
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { UniverSheetsFilterPreset } from "@univerjs/presets/preset-sheets-filter"
|
|
122
|
+
import zhCN from "@univerjs/presets/preset-sheets-filter/locales/zh-CN"
|
|
123
|
+
|
|
124
|
+
<DocumentView format="sheet" content={snapshot}
|
|
125
|
+
sheet={{ presets: [UniverSheetsFilterPreset()], locales: [zhCN] }} />
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`sheet` 里的配置**只在引擎创建时读一次**,挂载后改不生效也不会重建
|
|
129
|
+
(重建 = 重下引擎 + 丢掉选区和滚动位置,而宿主几乎总是传内联字面量)。要换配置请换 `key`。
|
|
130
|
+
|
|
131
|
+
## xlsx 导出
|
|
132
|
+
|
|
133
|
+
和 docx 一样**在本地生成,文档不出内网**;不引 SheetJS,只多用已有的 fflate。
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { downloadXlsx, exportXlsx } from "@4399ywkf/editor/xlsx"
|
|
137
|
+
|
|
138
|
+
const api = useRef<unknown>(null)
|
|
139
|
+
<DocumentView format="sheet" content={snapshot} onReady={(a) => (api.current = a)} />
|
|
140
|
+
|
|
141
|
+
// univerAPI 或快照都收
|
|
142
|
+
const { warnings } = downloadXlsx(api.current, "年度报表.xlsx")
|
|
143
|
+
if (warnings.length) toast(warnings.join("\n")) // ← 别吞,见下
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
服务端 / 定时任务只有 JSON 也能导 —— 整条链路不碰 DOM,不用起 Univer:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { xlsxFromSnapshot } from "@4399ywkf/editor/xlsx"
|
|
150
|
+
const { bytes, warnings, stats } = xlsxFromSnapshot(snapshot)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
保真范围:
|
|
154
|
+
|
|
155
|
+
| | 状态 |
|
|
156
|
+
|---|---|
|
|
157
|
+
| 值与类型、共享字符串、强制文本(`quotePrefix`) | ✅ |
|
|
158
|
+
| 公式 + 缓存值,填充公式还原成 xlsx 的共享公式 | ✅ |
|
|
159
|
+
| 数字格式、字体、颜色、填充、边框、对齐、换行、旋转 | ✅ |
|
|
160
|
+
| 合并单元格、行高列宽、隐藏行列、冻结窗格 | ✅ |
|
|
161
|
+
| 多工作表、工作表隐藏、标签颜色、网格线开关 | ✅ |
|
|
162
|
+
| 富文本单元格 | ~ 压平成纯文本 |
|
|
163
|
+
| 图片、图表、透视表、条件格式、数据校验、筛选、批注、超链接 | ✕ |
|
|
164
|
+
|
|
165
|
+
最后一行是 Univer 社区版的结构性限制:这些数据不在 `cellData` 里,而是各插件自己
|
|
166
|
+
序列化进 `resources` 的私有结构。**所以 `warnings` 必须展示给用户** —— 导出时它会
|
|
167
|
+
逐条点名丢了什么(「条件格式未随 xlsx 导出」),打开文件才发现要糟得多。
|
|
168
|
+
|
|
169
|
+
`cachedFormulaValues`(默认 true)决定公式格写不写 Univer 算好的值。关掉则只留公式、
|
|
170
|
+
由 Excel 打开时重算 —— 这时会自动加上 `fullCalcOnLoad`,否则不重算的消费方
|
|
171
|
+
(网页预览、pandas)会看到一片空白。
|
|
172
|
+
|
|
173
|
+
## csv 导出
|
|
174
|
+
|
|
175
|
+
单独子路径,**一个依赖都不需要**(xlsx 那边要 fflate 打 zip),产物 6.9KB:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { downloadCsv, exportCsv } from "@4399ywkf/editor/csv"
|
|
179
|
+
|
|
180
|
+
const { warnings } = downloadCsv(univerAPI, "明细.csv")
|
|
181
|
+
if (warnings.length) toast(warnings.join("\n"))
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**csv 是单表格式,工作簿是多表的** —— 只导一张,其余的在 `warnings` 里逐个点名:
|
|
185
|
+
|
|
186
|
+
> csv 是单表格式,只导出了「明细」。未导出的 1 张表:「汇总」
|
|
187
|
+
|
|
188
|
+
传 `univerAPI` 时导**真正的活动表**;只传快照时导第一张 —— 快照里根本没有
|
|
189
|
+
「活动表」这个字段,只有运行时实例问得出来。要指定就用 `options.sheet`(表 id 或表名)。
|
|
190
|
+
|
|
191
|
+
默认加 **UTF-8 BOM**:不加的话 Excel 会按本地编码(简中系统是 GBK)解析,中文全是乱码。
|
|
192
|
+
转义按 RFC 4180,含分隔符/引号/换行的字段加引号、内部引号翻倍。
|
|
193
|
+
`delimiter: "\t"` 就是 tsv。
|
|
194
|
+
|
|
195
|
+
其余降级同样会点名:合并单元格被拆开、富文本压平、公式写成算好的值
|
|
196
|
+
(`formulas: "formula"` 可改成写公式原文)。**日期不做转换** —— 它在 Univer 里是
|
|
197
|
+
「序列号 + 数字格式」,csv 里会写成 `45678`。格式码有转义、区域前缀、还有和 `mm`
|
|
198
|
+
同形的分钟,猜错日期比原样给出序列号更糟,所以只警告不转换。
|
|
199
|
+
|
|
44
200
|
## docx 导入导出
|
|
45
201
|
|
|
46
202
|
API 形态对齐 `@tiptap-pro/extension-import-docx` / `-export-docx`,但**在本地解析**,
|
|
@@ -69,20 +225,23 @@ editor.chain().exportDocx({ onCompleteExport: (blob) => save(blob) }).run()
|
|
|
69
225
|
|
|
70
226
|
## 注册自有格式
|
|
71
227
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
228
|
+
内置通道覆盖不了、或保真度不够时,宿主可以用同名 `registerFormat` 直接覆盖。
|
|
229
|
+
|
|
230
|
+
pptx **编辑**就属于这种:Univer Slides 官方标注「请勿用于生产环境」,开源世界没有
|
|
231
|
+
第二个可用的前端 pptx 编辑引擎;真要做只有 OnlyOffice / Collabora 那一档
|
|
232
|
+
(AGPL,需法务口径 + 独立部署)。所以内置的只有只读预览,要更高保真自己接:
|
|
75
233
|
|
|
76
234
|
```ts
|
|
77
235
|
import { registerFormat } from "@4399ywkf/editor"
|
|
78
236
|
|
|
237
|
+
// 同名注册会覆盖内置通道
|
|
79
238
|
registerFormat({
|
|
80
239
|
format: "pptx",
|
|
81
240
|
label: "演示文稿",
|
|
82
|
-
extensions: [".ppt", ".pptx"],
|
|
241
|
+
extensions: [".ppt", ".pptx"], // 自家引擎能开二进制 .ppt 就一并申报
|
|
83
242
|
capabilities: {
|
|
84
|
-
editable:
|
|
85
|
-
importExtensions: [], exportFormats: [],
|
|
243
|
+
editable: true, aiOperable: false, collaborative: false,
|
|
244
|
+
importExtensions: [".ppt", ".pptx"], exportFormats: ["pptx"],
|
|
86
245
|
},
|
|
87
246
|
load: () => import("./MyOnlyOfficeSlideView"),
|
|
88
247
|
})
|
|
@@ -109,6 +268,6 @@ schema 不一致时一端产生的节点到另一端会被 y-prosemirror 当非
|
|
|
109
268
|
```bash
|
|
110
269
|
pnpm install
|
|
111
270
|
pnpm build # tsup + 样式编译
|
|
112
|
-
pnpm test #
|
|
271
|
+
pnpm test # 203 个用例
|
|
113
272
|
pnpm typecheck
|
|
114
273
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"PptxView-ZH4FZKNO.js"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"SheetView-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"SheetView-DSADIZNS.js"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/xlsx/snapshot.ts
|
|
2
|
+
function snapshotFromUniver(univerAPI) {
|
|
3
|
+
const api = univerAPI;
|
|
4
|
+
const book = api?.getActiveWorkbook?.();
|
|
5
|
+
if (!book) throw new Error("拿不到当前工作簿:univerAPI 无效,或表格尚未就绪");
|
|
6
|
+
const snapshot = book.getSnapshot?.() ?? book.save?.();
|
|
7
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
8
|
+
throw new Error("当前 Univer 版本没有 getSnapshot() / save(),无法取快照");
|
|
9
|
+
}
|
|
10
|
+
return snapshot;
|
|
11
|
+
}
|
|
12
|
+
function toSnapshot(source) {
|
|
13
|
+
const maybe = source;
|
|
14
|
+
if (maybe && typeof maybe === "object" && ("sheets" in maybe || "sheetOrder" in maybe))
|
|
15
|
+
return maybe;
|
|
16
|
+
return snapshotFromUniver(source);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/xlsx/cell.ts
|
|
20
|
+
function numericKeys(o) {
|
|
21
|
+
if (!o) return [];
|
|
22
|
+
return Object.keys(o).map(Number).filter((n) => Number.isInteger(n) && n >= 0).sort((a, b) => a - b);
|
|
23
|
+
}
|
|
24
|
+
function richTextToPlain(cell) {
|
|
25
|
+
const stream = cell.p?.body?.dataStream;
|
|
26
|
+
if (typeof stream !== "string") return void 0;
|
|
27
|
+
return stream.replace(/\r\n$/, "").replace(/\r/g, "\n");
|
|
28
|
+
}
|
|
29
|
+
function looksLikeDateFormat(pattern) {
|
|
30
|
+
if (!pattern) return false;
|
|
31
|
+
const bare = pattern.replace(/"[^"]*"/g, "").replace(/\\./g, "");
|
|
32
|
+
return /[yYdD]/.test(bare) || /h/i.test(bare) || /\[\$-/.test(pattern);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { looksLikeDateFormat, numericKeys, richTextToPlain, snapshotFromUniver, toSnapshot };
|
|
36
|
+
//# sourceMappingURL=chunk-32WQX3NR.js.map
|
|
37
|
+
//# sourceMappingURL=chunk-32WQX3NR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/xlsx/snapshot.ts","../src/xlsx/cell.ts"],"names":[],"mappings":";AASO,SAAS,mBAAmB,SAAA,EAAkC;AACnE,EAAA,MAAM,GAAA,GAAM,SAAA;AAGZ,EAAA,MAAM,IAAA,GAAO,KAAK,iBAAA,IAAoB;AACtC,EAAA,IAAI,CAAC,IAAA,EAAM,MAAM,IAAI,MAAM,+BAA+B,CAAA;AAC1D,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,WAAA,IAAc,IAAK,KAAK,IAAA,IAAO;AACrD,EAAA,IAAI,CAAC,QAAA,IAAY,OAAO,QAAA,KAAa,QAAA,EAAU;AAC7C,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,EAC/D;AACA,EAAA,OAAO,QAAA;AACT;AAGO,SAAS,WAAW,MAAA,EAA8C;AACvE,EAAA,MAAM,KAAA,GAAQ,MAAA;AACd,EAAA,IAAI,SAAS,OAAO,KAAA,KAAU,QAAA,KAAa,QAAA,IAAY,SAAS,YAAA,IAAgB,KAAA,CAAA;AAC9E,IAAA,OAAO,KAAA;AACT,EAAA,OAAO,mBAAmB,MAAM,CAAA;AAClC;;;ACjBO,SAAS,YAAY,CAAA,EAAkD;AAC5E,EAAA,IAAI,CAAC,CAAA,EAAG,OAAO,EAAC;AAChB,EAAA,OAAO,MAAA,CAAO,KAAK,CAAC,CAAA,CACjB,IAAI,MAAM,CAAA,CACV,MAAA,CAAO,CAAC,CAAA,KAAM,MAAA,CAAO,UAAU,CAAC,CAAA,IAAK,KAAK,CAAC,CAAA,CAC3C,KAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AACzB;AAGO,SAAS,gBAAgB,IAAA,EAAoC;AAClE,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,CAAA,EAAG,IAAA,EAAM,UAAA;AAC7B,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,EAAU,OAAO,MAAA;AACvC,EAAA,OAAO,OAAO,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAA,CAAE,OAAA,CAAQ,OAAO,IAAI,CAAA;AACxD;AASO,SAAS,oBAAoB,OAAA,EAA6C;AAC/E,EAAA,IAAI,CAAC,SAAS,OAAO,KAAA;AAErB,EAAA,MAAM,IAAA,GAAO,QAAQ,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC/D,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,IAAK,IAAA,CAAK,KAAK,IAAI,CAAA,IAAK,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACvE","file":"chunk-32WQX3NR.js","sourcesContent":["/** 从 Univer 实例取快照。xlsx 与 csv 两条导出路径共用。 */\n\nimport type { WorkbookData } from \"./types.ts\"\n\n/**\n * 参数类型是 `unknown` 而不是 `FUniver`:`@univerjs/presets` 是可选 peer,\n * 写成具体类型会让没装它的项目 typecheck 直接红。运行时按 facade 的两个\n * 方法名依次试探(`getSnapshot` 是新名字,`save` 是旧的)。\n */\nexport function snapshotFromUniver(univerAPI: unknown): WorkbookData {\n const api = univerAPI as {\n getActiveWorkbook?: () => { getSnapshot?: () => unknown; save?: () => unknown } | null\n } | null\n const book = api?.getActiveWorkbook?.()\n if (!book) throw new Error(\"拿不到当前工作簿:univerAPI 无效,或表格尚未就绪\")\n const snapshot = book.getSnapshot?.() ?? book.save?.()\n if (!snapshot || typeof snapshot !== \"object\") {\n throw new Error(\"当前 Univer 版本没有 getSnapshot() / save(),无法取快照\")\n }\n return snapshot as WorkbookData\n}\n\n/** 快照或 Univer 实例都收 —— 调用方不必自己判断手里是什么 */\nexport function toSnapshot(source: WorkbookData | unknown): WorkbookData {\n const maybe = source as WorkbookData\n if (maybe && typeof maybe === \"object\" && (\"sheets\" in maybe || \"sheetOrder\" in maybe))\n return maybe\n return snapshotFromUniver(source)\n}\n","/** 读 Univer 快照时 xlsx 与 csv 两条导出路径共用的小工具。 */\n\nimport type { CellData } from \"./types.ts\"\n\n/**\n * 稀疏矩阵的行号/列号。\n *\n * Univer 的 `cellData` 是 `{ [row]: { [col]: cell } }` 的普通对象,键是字符串。\n * 直接 `Object.keys()` 会得到 `[\"10\", \"2\"]` 这种字典序,必须转数字再排 ——\n * 否则导出的行序就是乱的。\n */\nexport function numericKeys(o: Record<number, unknown> | undefined): number[] {\n if (!o) return []\n return Object.keys(o)\n .map(Number)\n .filter((n) => Number.isInteger(n) && n >= 0)\n .sort((a, b) => a - b)\n}\n\n/** 富文本只取纯文本。Univer 的 dataStream 用 \\r 断段、结尾还带一个 \\r\\n */\nexport function richTextToPlain(cell: CellData): string | undefined {\n const stream = cell.p?.body?.dataStream\n if (typeof stream !== \"string\") return undefined\n return stream.replace(/\\r\\n$/, \"\").replace(/\\r/g, \"\\n\")\n}\n\n/**\n * 这个单元格的数字格式是不是日期/时间。\n *\n * 只做**粗判**,用来给 csv 提示「这里会导出成序列号」,不用来做转换 ——\n * Excel 的格式码有转义(`\\d`、`\"年\"`)、有区域前缀(`[$-409]`)、\n * 还有和 `mm` 同形的分钟,真解析是个坑。猜错日期比原样给出序列号更糟。\n */\nexport function looksLikeDateFormat(pattern: string | undefined | null): boolean {\n if (!pattern) return false\n // 去掉引号里的字面量和反斜杠转义,剩下的才是真正的格式符\n const bare = pattern.replace(/\"[^\"]*\"/g, \"\").replace(/\\\\./g, \"\")\n return /[yYdD]/.test(bare) || /h/i.test(bare) || /\\[\\$-/.test(pattern)\n}\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { unzipSync, zipSync } from 'fflate';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/views/PptxView.tsx
|
|
6
|
+
var CONTENT_TYPES = "[Content_Types].xml";
|
|
7
|
+
var MASTER_ENTRY = /^ppt\/slideMasters\/slideMaster(\d+)\.xml$/;
|
|
8
|
+
var MASTER_DECL = /PartName="\/ppt\/slideMasters\/slideMaster(\d+)\.xml"/g;
|
|
9
|
+
function repairPptx(input) {
|
|
10
|
+
let files;
|
|
11
|
+
try {
|
|
12
|
+
files = unzipSync(input);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return { bytes: input, removed: 0, skipped: `不是有效的 zip:${e.message}` };
|
|
15
|
+
}
|
|
16
|
+
const contentTypes = files[CONTENT_TYPES];
|
|
17
|
+
if (!contentTypes) return { bytes: input, removed: 0, skipped: `缺少 ${CONTENT_TYPES}` };
|
|
18
|
+
const actual = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const name of Object.keys(files)) {
|
|
20
|
+
const m = MASTER_ENTRY.exec(name);
|
|
21
|
+
if (m) actual.add(m[1]);
|
|
22
|
+
}
|
|
23
|
+
const xml = new TextDecoder().decode(contentTypes);
|
|
24
|
+
const ghosts = [...xml.matchAll(MASTER_DECL)].map((m) => m[1]).filter((n) => !actual.has(n));
|
|
25
|
+
if (!ghosts.length) return { bytes: input, removed: 0 };
|
|
26
|
+
let patched = xml;
|
|
27
|
+
for (const n of new Set(ghosts)) {
|
|
28
|
+
patched = patched.replace(
|
|
29
|
+
new RegExp(`<Override[^>]+PartName="/ppt/slideMasters/slideMaster${n}\\.xml"[^>]*>\\s*`, "g"),
|
|
30
|
+
""
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (!patched.trim()) return { bytes: input, removed: 0, skipped: "替换后内容为空" };
|
|
34
|
+
files[CONTENT_TYPES] = new TextEncoder().encode(patched);
|
|
35
|
+
try {
|
|
36
|
+
return { bytes: zipSync(files, { level: 6 }), removed: new Set(ghosts).size };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return { bytes: input, removed: 0, skipped: `重新打包失败:${e.message}` };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var BASE_WIDTH = 960;
|
|
42
|
+
function PptxView({
|
|
43
|
+
source,
|
|
44
|
+
content,
|
|
45
|
+
className,
|
|
46
|
+
onReady,
|
|
47
|
+
onError
|
|
48
|
+
}) {
|
|
49
|
+
const hostRef = useRef(null);
|
|
50
|
+
const wrapperRef = useRef(null);
|
|
51
|
+
const [state, setState] = useState("loading");
|
|
52
|
+
const [slides, setSlides] = useState(0);
|
|
53
|
+
const latest = useRef({ onReady, onError });
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
latest.current = { onReady, onError };
|
|
56
|
+
});
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
const host = hostRef.current;
|
|
60
|
+
if (!host || !source && !content) return;
|
|
61
|
+
let previewer = null;
|
|
62
|
+
setState("loading");
|
|
63
|
+
setSlides(0);
|
|
64
|
+
(async () => {
|
|
65
|
+
const { onReady: ready, onError: fail } = latest.current;
|
|
66
|
+
try {
|
|
67
|
+
const [{ init }, bytes] = await Promise.all([
|
|
68
|
+
import('pptx-preview'),
|
|
69
|
+
toBytes(source, content)
|
|
70
|
+
]);
|
|
71
|
+
if (cancelled) return;
|
|
72
|
+
const { bytes: fixed, removed, skipped } = repairPptx(bytes);
|
|
73
|
+
if (skipped) console.warn(`[PptxView] 跳过修复:${skipped}`);
|
|
74
|
+
else if (removed) console.info(`[PptxView] 已移除 ${removed} 条无效的 slideMaster 声明`);
|
|
75
|
+
host.innerHTML = "";
|
|
76
|
+
previewer = init(host, { width: BASE_WIDTH });
|
|
77
|
+
await previewer.preview(
|
|
78
|
+
toArrayBuffer(fixed)
|
|
79
|
+
);
|
|
80
|
+
if (cancelled) return;
|
|
81
|
+
const count = previewer.slideCount ?? 0;
|
|
82
|
+
setSlides(count);
|
|
83
|
+
if (!count) {
|
|
84
|
+
setState("error");
|
|
85
|
+
fail?.(new Error("演示文稿解析失败:没有可渲染的幻灯片"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setState("ready");
|
|
89
|
+
ready?.(previewer);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (cancelled) return;
|
|
92
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
93
|
+
setState("error");
|
|
94
|
+
fail?.(
|
|
95
|
+
new Error(
|
|
96
|
+
`演示文稿预览失败:${err.message}。请确认已安装可选依赖 pptx-preview(pnpm add pptx-preview)`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
return () => {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
previewer?.destroy?.();
|
|
104
|
+
if (host) host.innerHTML = "";
|
|
105
|
+
};
|
|
106
|
+
}, [source, content]);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const wrapper = wrapperRef.current;
|
|
109
|
+
const host = hostRef.current;
|
|
110
|
+
if (!wrapper || !host) return;
|
|
111
|
+
const update = () => {
|
|
112
|
+
const width = wrapper.clientWidth;
|
|
113
|
+
host.style.zoom = width && width < BASE_WIDTH ? String(width / BASE_WIDTH) : "";
|
|
114
|
+
};
|
|
115
|
+
update();
|
|
116
|
+
const observer = new ResizeObserver(update);
|
|
117
|
+
observer.observe(wrapper);
|
|
118
|
+
return () => observer.disconnect();
|
|
119
|
+
}, []);
|
|
120
|
+
return /* @__PURE__ */ jsx(
|
|
121
|
+
"div",
|
|
122
|
+
{
|
|
123
|
+
ref: wrapperRef,
|
|
124
|
+
className,
|
|
125
|
+
"data-pptx-state": state,
|
|
126
|
+
"data-pptx-slides": slides,
|
|
127
|
+
style: { width: "100%", height: "100%", overflow: "auto" },
|
|
128
|
+
children: /* @__PURE__ */ jsx("div", { ref: hostRef, style: { width: BASE_WIDTH } })
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
async function toBytes(source, content) {
|
|
133
|
+
if (content instanceof Uint8Array) return content;
|
|
134
|
+
if (content instanceof ArrayBuffer) return new Uint8Array(content);
|
|
135
|
+
if (source instanceof Blob) return new Uint8Array(await source.arrayBuffer());
|
|
136
|
+
if (typeof source === "string") {
|
|
137
|
+
const res = await fetch(source);
|
|
138
|
+
if (!res.ok) throw new Error(`拉取失败 HTTP ${res.status}`);
|
|
139
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
140
|
+
}
|
|
141
|
+
throw new Error("需要 source(URL / File / Blob)或 content(ArrayBuffer)");
|
|
142
|
+
}
|
|
143
|
+
function toArrayBuffer(bytes) {
|
|
144
|
+
return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes.buffer : bytes.slice().buffer;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { PptxView, repairPptx };
|
|
148
|
+
//# sourceMappingURL=chunk-5XG4YMAB.js.map
|
|
149
|
+
//# sourceMappingURL=chunk-5XG4YMAB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pptx-repair.ts","../src/views/PptxView.tsx"],"names":[],"mappings":"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","file":"chunk-5XG4YMAB.js","sourcesContent":["/**\n * pptx 的「幽灵 slideMaster」修复。\n *\n * 很多 pptx 的 `[Content_Types].xml` 里声明了 zip 中**并不存在**的\n * `ppt/slideMasters/slideMasterN.xml`。PowerPoint 自己不介意,但 `pptx-preview`\n * 会因为解析不到这些部件而整份失败 —— 表现是 `slideCount === 0`、面板一片空白,\n * 不抛错、不报警。\n *\n * 这不是边缘情况:在 17 份真实文件上实测,**12 份中招**(AI 生成的和\n * PowerPoint 16 存出来的都有)。裸接这个库只有 5/17 能打开,加上本修复后 17/17。\n *\n * 做法与腾讯 WorkBuddy 内置 pptx 预览里的 `checkPPTXFile` 一致:删掉指向缺失\n * 部件的 `<Override>` 再重新打包。只处理 slideMaster —— 其它部件缺失时该库\n * 能自己降级,动它反而可能改坏。\n */\n\nimport { unzipSync, zipSync } from \"fflate\"\n\nconst CONTENT_TYPES = \"[Content_Types].xml\"\nconst MASTER_ENTRY = /^ppt\\/slideMasters\\/slideMaster(\\d+)\\.xml$/\nconst MASTER_DECL = /PartName=\"\\/ppt\\/slideMasters\\/slideMaster(\\d+)\\.xml\"/g\n\nexport interface RepairPptxResult {\n /** 修好的字节;没问题时**原样返回入参**,不做无谓的解压重打包 */\n bytes: Uint8Array\n /** 删掉了几条幽灵声明。0 表示这份文件本来就是好的 */\n removed: number\n /** 修复过程中放弃的原因(文件不是 zip、没有 [Content_Types].xml 等) */\n skipped?: string\n}\n\n/**\n * @param input pptx 原始字节\n * @returns 可交给 `pptx-preview` 的字节。**任何异常都退回原字节** ——\n * 修不了也不该让预览失败,让引擎自己去报它的错。\n */\nexport function repairPptx(input: Uint8Array): RepairPptxResult {\n let files: Record<string, Uint8Array>\n try {\n files = unzipSync(input)\n } catch (e) {\n return { bytes: input, removed: 0, skipped: `不是有效的 zip:${(e as Error).message}` }\n }\n\n const contentTypes = files[CONTENT_TYPES]\n if (!contentTypes) return { bytes: input, removed: 0, skipped: `缺少 ${CONTENT_TYPES}` }\n\n const actual = new Set<string>()\n for (const name of Object.keys(files)) {\n const m = MASTER_ENTRY.exec(name)\n if (m) actual.add(m[1] as string)\n }\n\n const xml = new TextDecoder().decode(contentTypes)\n const ghosts = [...xml.matchAll(MASTER_DECL)]\n .map((m) => m[1] as string)\n .filter((n) => !actual.has(n))\n if (!ghosts.length) return { bytes: input, removed: 0 }\n\n let patched = xml\n for (const n of new Set(ghosts)) {\n patched = patched.replace(\n new RegExp(`<Override[^>]+PartName=\"/ppt/slideMasters/slideMaster${n}\\\\.xml\"[^>]*>\\\\s*`, \"g\"),\n \"\",\n )\n }\n // 替换后为空说明正则吃掉了不该吃的,宁可不修\n if (!patched.trim()) return { bytes: input, removed: 0, skipped: \"替换后内容为空\" }\n\n files[CONTENT_TYPES] = new TextEncoder().encode(patched)\n try {\n return { bytes: zipSync(files, { level: 6 }), removed: new Set(ghosts).size }\n } catch (e) {\n return { bytes: input, removed: 0, skipped: `重新打包失败:${(e as Error).message}` }\n }\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport { repairPptx } from \"../pptx-repair\"\nimport type { DocumentViewProps } from \"../registry\"\n\n/**\n * pptx 通道:`pptx-preview` 只读预览。\n *\n * **只做「看清这页讲什么」,不是所见即所得。** 已知差距:\n * - **图表渲染不出数据** —— 轴画得出来,柱子/折线是空的,系列名与标题退化成默认值。\n * 该库打包了 ECharts 却没把 `c:cat` / `c:val` 接上去\n * - 原字体缺失时替换成系统字体,观感和原稿不一致\n * - 内部偶有 SVG 路径算出 NaN 的报错,个别形状会画不出来\n *\n * 编辑不做:开源世界没有可用的前端 pptx 编辑引擎(Univer Slides 官方标注\n * 「请勿用于生产环境」),真要做只有 OnlyOffice / Collabora 那一档\n * (AGPL,需法务口径 + 独立部署)。宿主可以用 `registerFormat` 覆盖本通道。\n *\n * 引擎选型与「幽灵 slideMaster」修复都对齐腾讯 WorkBuddy 的内置 pptx 预览。\n */\n\n/** 该库以此宽度布局,缩放靠 CSS zoom。和 WorkBuddy 取值一致 */\nconst BASE_WIDTH = 960\n\nexport default function PptxView({\n source,\n content,\n className,\n onReady,\n onError,\n}: DocumentViewProps) {\n const hostRef = useRef<HTMLDivElement>(null)\n const wrapperRef = useRef<HTMLDivElement>(null)\n const [state, setState] = useState<\"loading\" | \"ready\" | \"error\">(\"loading\")\n const [slides, setSlides] = useState(0)\n\n // 同 SheetView:回调多是内联箭头函数,进依赖数组会让父组件每渲染一次就重解析一遍\n const latest = useRef({ onReady, onError })\n useEffect(() => {\n latest.current = { onReady, onError }\n })\n\n useEffect(() => {\n let cancelled = false\n const host = hostRef.current\n if (!host || (!source && !content)) return\n let previewer: { destroy?: () => void } | null = null\n\n setState(\"loading\")\n setSlides(0)\n ;(async () => {\n const { onReady: ready, onError: fail } = latest.current\n try {\n // 动态 import:这个库连着 ECharts 一起 gzip 后 400KB+,\n // 不该因为「包里支持 pptx」就让所有人付这笔钱\n const [{ init }, bytes] = await Promise.all([\n import(\"pptx-preview\"),\n toBytes(source, content),\n ])\n if (cancelled) return\n\n // 先修再喂。不修的话真实文件里大半会静默变成 0 页空白\n const { bytes: fixed, removed, skipped } = repairPptx(bytes)\n if (skipped) console.warn(`[PptxView] 跳过修复:${skipped}`)\n else if (removed) console.info(`[PptxView] 已移除 ${removed} 条无效的 slideMaster 声明`)\n\n host.innerHTML = \"\"\n // 只传 width:不传 mode 时该库按整列堆叠渲染,正是预览面板要的\n previewer = (init as (el: HTMLElement, o: object) => never)(host, { width: BASE_WIDTH })\n await (previewer as unknown as { preview: (b: ArrayBuffer) => Promise<unknown> }).preview(\n toArrayBuffer(fixed),\n )\n if (cancelled) return\n\n const count = (previewer as unknown as { slideCount?: number }).slideCount ?? 0\n setSlides(count)\n if (!count) {\n // 该库解析失败时不抛错,只是什么都不画 —— 不显式判空就会是一片白板\n setState(\"error\")\n fail?.(new Error(\"演示文稿解析失败:没有可渲染的幻灯片\"))\n return\n }\n setState(\"ready\")\n ready?.(previewer)\n } catch (e) {\n if (cancelled) return\n const err = e instanceof Error ? e : new Error(String(e))\n setState(\"error\")\n fail?.(\n new Error(\n `演示文稿预览失败:${err.message}。` +\n `请确认已安装可选依赖 pptx-preview(pnpm add pptx-preview)`,\n ),\n )\n }\n })()\n\n return () => {\n cancelled = true\n previewer?.destroy?.()\n if (host) host.innerHTML = \"\"\n }\n }, [source, content])\n\n // 容器比 960 窄时整体缩放。用 zoom 而不是 transform:\n // transform 不改变布局盒,缩完外层还会留着原高度的空白\n useEffect(() => {\n const wrapper = wrapperRef.current\n const host = hostRef.current\n if (!wrapper || !host) return\n const update = () => {\n const width = wrapper.clientWidth\n host.style.zoom = width && width < BASE_WIDTH ? String(width / BASE_WIDTH) : \"\"\n }\n update()\n const observer = new ResizeObserver(update)\n observer.observe(wrapper)\n return () => observer.disconnect()\n }, [])\n\n return (\n <div\n ref={wrapperRef}\n className={className}\n data-pptx-state={state}\n data-pptx-slides={slides}\n style={{ width: \"100%\", height: \"100%\", overflow: \"auto\" }}\n >\n <div ref={hostRef} style={{ width: BASE_WIDTH }} />\n </div>\n )\n}\n\nasync function toBytes(\n source: DocumentViewProps[\"source\"],\n content: DocumentViewProps[\"content\"],\n): Promise<Uint8Array> {\n if (content instanceof Uint8Array) return content\n if (content instanceof ArrayBuffer) return new Uint8Array(content)\n if (source instanceof Blob) return new Uint8Array(await source.arrayBuffer())\n if (typeof source === \"string\") {\n const res = await fetch(source)\n if (!res.ok) throw new Error(`拉取失败 HTTP ${res.status}`)\n return new Uint8Array(await res.arrayBuffer())\n }\n throw new Error(\"需要 source(URL / File / Blob)或 content(ArrayBuffer)\")\n}\n\n/** fflate 可能返回带 offset 的视图,直接取 .buffer 会把整块底层内存交出去 */\nfunction toArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength\n ? (bytes.buffer as ArrayBuffer)\n : (bytes.slice().buffer as ArrayBuffer)\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/views/SheetView.tsx
|
|
5
|
+
function SheetView({
|
|
6
|
+
content,
|
|
7
|
+
readOnly,
|
|
8
|
+
className,
|
|
9
|
+
sheet,
|
|
10
|
+
onReady,
|
|
11
|
+
onError
|
|
12
|
+
}) {
|
|
13
|
+
const hostRef = useRef(null);
|
|
14
|
+
const disposeRef = useRef(null);
|
|
15
|
+
const latest = useRef({ sheet, onReady, onError });
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
latest.current = { sheet, onReady, onError };
|
|
18
|
+
});
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
const host = hostRef.current;
|
|
22
|
+
if (!host) return;
|
|
23
|
+
(async () => {
|
|
24
|
+
const { sheet: opts, onReady: ready, onError: fail } = latest.current;
|
|
25
|
+
try {
|
|
26
|
+
const [{ createUniver, LocaleType, merge }, sheetsCore, zhCN] = await Promise.all([
|
|
27
|
+
import('@univerjs/presets'),
|
|
28
|
+
import('@univerjs/presets/preset-sheets-core'),
|
|
29
|
+
import('@univerjs/presets/preset-sheets-core/locales/zh-CN')
|
|
30
|
+
]);
|
|
31
|
+
if (cancelled) return;
|
|
32
|
+
const ui = { ...readOnly ? { toolbar: false } : null, ...opts?.ui };
|
|
33
|
+
const presets = [
|
|
34
|
+
sheetsCore.UniverSheetsCorePreset({ container: host, ...ui })
|
|
35
|
+
];
|
|
36
|
+
const locales = [zhCN.default];
|
|
37
|
+
if (opts?.findReplace) {
|
|
38
|
+
const [findReplace, findReplaceZhCN] = await Promise.all([
|
|
39
|
+
import('@univerjs/presets/preset-sheets-find-replace'),
|
|
40
|
+
import('@univerjs/presets/preset-sheets-find-replace/locales/zh-CN')
|
|
41
|
+
]);
|
|
42
|
+
if (cancelled) return;
|
|
43
|
+
presets.push(
|
|
44
|
+
findReplace.UniverSheetsFindReplacePreset()
|
|
45
|
+
);
|
|
46
|
+
locales.push(findReplaceZhCN.default);
|
|
47
|
+
}
|
|
48
|
+
if (opts?.presets?.length) presets.push(...opts.presets);
|
|
49
|
+
if (opts?.locales?.length) locales.push(...opts.locales);
|
|
50
|
+
const { univerAPI, univer } = createUniver({
|
|
51
|
+
locale: LocaleType.ZH_CN,
|
|
52
|
+
// createUniver 不会自动收集 preset 自带的 locales,得在这里一起合进来,
|
|
53
|
+
// 否则新加的 preset 菜单会显示成原始 i18n key
|
|
54
|
+
locales: { [LocaleType.ZH_CN]: merge({}, ...locales) },
|
|
55
|
+
presets
|
|
56
|
+
});
|
|
57
|
+
const dispose = () => univer.dispose();
|
|
58
|
+
if (cancelled) return dispose();
|
|
59
|
+
if (content) univerAPI.createWorkbook(content);
|
|
60
|
+
else univerAPI.createWorkbook({});
|
|
61
|
+
if (readOnly) univerAPI.getActiveWorkbook?.()?.setEditable?.(false);
|
|
62
|
+
disposeRef.current = dispose;
|
|
63
|
+
ready?.(univerAPI);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
66
|
+
fail?.(
|
|
67
|
+
new Error(
|
|
68
|
+
`表格引擎加载失败:${err.message}。请确认已安装可选依赖 @univerjs/presets(pnpm add @univerjs/presets)`
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
return () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
disposeRef.current?.();
|
|
76
|
+
disposeRef.current = null;
|
|
77
|
+
};
|
|
78
|
+
}, [content, readOnly]);
|
|
79
|
+
return /* @__PURE__ */ jsx("div", { ref: hostRef, className, style: { width: "100%", height: "100%" } });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { SheetView };
|
|
83
|
+
//# sourceMappingURL=chunk-L6ZR5HHW.js.map
|
|
84
|
+
//# sourceMappingURL=chunk-L6ZR5HHW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/views/SheetView.tsx"],"names":[],"mappings":"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","file":"chunk-L6ZR5HHW.js","sourcesContent":["import { useEffect, useRef } from \"react\"\nimport type { DocumentViewProps } from \"../registry\"\nimport type { SheetUiOptions } from \"../sheet-options\"\n\n/**\n * sheet 通道:Univer 适配。\n *\n * 只用 Apache-2.0 的 OSS 核心。**不碰** `@univerjs-pro/*` 与 `univer-cli`:\n * 前者要自建 Univer Server 且需商业授权,后者 63MB、license 字段为 None、\n * 90/91 个 JS 被混淆、还会向 univer.ai 上报遥测 —— 不能进生产链路。\n *\n * xlsx **导出**用本包的 `./xlsx`:直接读 Univer 快照拼 OOXML,不引 SheetJS。\n * **导入**仍在宿主侧自理(SheetJS 或后端解析),本包不做 xlsx → 快照那一向。\n *\n * 配置见 `SheetOptions`:UI 开关直接透传给 preset(Univer 的 class 是\n * `univer-` 前缀的原子类,没有稳定选择器,CSS 硬藏靠不住),额外能力靠追加 preset。\n */\nexport default function SheetView({\n content,\n readOnly,\n className,\n sheet,\n onReady,\n onError,\n}: DocumentViewProps) {\n const hostRef = useRef<HTMLDivElement>(null)\n const disposeRef = useRef<(() => void) | null>(null)\n\n // 这几项都是「创建时读一次」的,且宿主多半传内联字面量/箭头函数。\n // 放进依赖数组 = 父组件每渲染一次就重建一次引擎(重下 871KB + 丢选区滚动位置),\n // 所以走 ref:取最新值,但不参与依赖比较。\n const latest = useRef({ sheet, onReady, onError })\n useEffect(() => {\n latest.current = { sheet, onReady, onError }\n })\n\n useEffect(() => {\n let cancelled = false\n const host = hostRef.current\n if (!host) return\n\n ;(async () => {\n const { sheet: opts, onReady: ready, onError: fail } = latest.current\n try {\n // 动态 import:871KB gzip 的引擎只在真的打开表格时才下载\n const [{ createUniver, LocaleType, merge }, sheetsCore, zhCN] = await Promise.all([\n import(\"@univerjs/presets\"),\n import(\"@univerjs/presets/preset-sheets-core\"),\n import(\"@univerjs/presets/preset-sheets-core/locales/zh-CN\"),\n ])\n if (cancelled) return\n\n // 只读时默认摘掉工具栏:整条 ribbon 全是死按钮,窄面板里还挤。\n // 宿主显式传了 toolbar 就以宿主为准。\n const ui: SheetUiOptions = { ...(readOnly ? { toolbar: false } : null), ...opts?.ui }\n\n // 可选 peer:@univerjs/presets 未安装时这里的类型解析不到,\n // 统一用 never 过桥,运行时由外层 try/catch 兜底并给出可读提示\n const presets: unknown[] = [\n (\n sheetsCore as unknown as { UniverSheetsCorePreset: (o: object) => never }\n ).UniverSheetsCorePreset({ container: host, ...ui }),\n ]\n const locales: unknown[] = [(zhCN as unknown as { default: object }).default]\n\n if (opts?.findReplace) {\n // preset-sheets-find-replace 是 @univerjs/presets 自己的 dependency,\n // 走子路径不需要宿主再装包;没开就完全不下载。\n const [findReplace, findReplaceZhCN] = await Promise.all([\n import(\"@univerjs/presets/preset-sheets-find-replace\"),\n import(\"@univerjs/presets/preset-sheets-find-replace/locales/zh-CN\"),\n ])\n if (cancelled) return\n presets.push(\n (\n findReplace as unknown as { UniverSheetsFindReplacePreset: () => never }\n ).UniverSheetsFindReplacePreset(),\n )\n locales.push((findReplaceZhCN as unknown as { default: object }).default)\n }\n\n if (opts?.presets?.length) presets.push(...opts.presets)\n if (opts?.locales?.length) locales.push(...opts.locales)\n\n const { univerAPI, univer } = createUniver({\n locale: LocaleType.ZH_CN,\n // createUniver 不会自动收集 preset 自带的 locales,得在这里一起合进来,\n // 否则新加的 preset 菜单会显示成原始 i18n key\n locales: { [LocaleType.ZH_CN]: merge({}, ...(locales as object[])) },\n presets: presets as never,\n })\n\n const dispose = () => (univer as unknown as { dispose: () => void }).dispose()\n // 卸载可能发生在上面任一个 await 期间:那时 cleanup 拿到的 disposeRef 还是 null,\n // 不在这儿自己收掉,引擎就挂在一个已脱离文档的 host 上泄漏了\n if (cancelled) return dispose()\n\n if (content) univerAPI.createWorkbook(content as never)\n else univerAPI.createWorkbook({})\n if (readOnly) univerAPI.getActiveWorkbook?.()?.setEditable?.(false)\n\n disposeRef.current = dispose\n ready?.(univerAPI)\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e))\n fail?.(\n new Error(\n `表格引擎加载失败:${err.message}。` +\n `请确认已安装可选依赖 @univerjs/presets(pnpm add @univerjs/presets)`,\n ),\n )\n }\n })()\n\n return () => {\n cancelled = true\n disposeRef.current?.()\n disposeRef.current = null\n }\n }, [content, readOnly])\n\n return <div ref={hostRef} className={className} style={{ width: \"100%\", height: \"100%\" }} />\n}\n"]}
|