@veltra/sheet 2.0.0 → 2.1.1

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 (203) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.js +3 -22
  3. package/dist/tools/builtin.js +279 -8
  4. package/dist/tools/context.d.ts +54 -11
  5. package/dist/tools/context.js +28 -5
  6. package/dist/tools/download.js +68 -0
  7. package/dist/tools/export.worker.d.ts +16 -0
  8. package/dist/tools/export.worker.js +29 -0
  9. package/dist/tools/registry.d.ts +7 -4
  10. package/dist/tools/registry.js +1 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/sheet.d.ts +6 -5
  13. package/dist/vue/formula-bar.js +393 -0
  14. package/dist/vue/formula-suggest-list.js +66 -0
  15. package/dist/vue/import-file.js +140 -0
  16. package/dist/vue/insert-count-menu-item.js +68 -0
  17. package/dist/vue/insert-image.js +79 -0
  18. package/dist/vue/popup-helpers.js +73 -0
  19. package/dist/vue/popups/border-popup.js +75 -0
  20. package/dist/vue/popups/border-preset-glyph.js +106 -0
  21. package/dist/vue/popups/export-popup.js +51 -0
  22. package/dist/vue/popups/fill-color-popup.js +37 -0
  23. package/dist/vue/popups/find-popup.js +113 -0
  24. package/dist/vue/popups/font-color-popup.js +37 -0
  25. package/dist/vue/popups/font-size-popup.js +63 -0
  26. package/dist/vue/popups/import.worker.d.ts +21 -0
  27. package/dist/vue/popups/import.worker.js +41 -0
  28. package/dist/vue/popups/insert-image-popup.js +38 -0
  29. package/dist/vue/sheet-context-menu.js +200 -0
  30. package/dist/vue/sheet-tabs.js +162 -0
  31. package/dist/vue/sheet-toolbar.js +106 -0
  32. package/dist/vue/sheet.js +144 -138
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +11 -0
  35. package/dist/vue/style2.css +564 -3
  36. package/dist/vue/use-find-replace.js +161 -0
  37. package/dist/vue/use-formula-ref-select.js +37 -0
  38. package/dist/vue/use-formula-suggest.js +126 -0
  39. package/dist/vue/use-sheet-grid.js +180 -0
  40. package/dist/vue/use-sheet-state.js +122 -0
  41. package/dist/vue/use-sheet-tabs-bar.js +90 -0
  42. package/dist/vue/use-tool-groups.js +42 -0
  43. package/dist/vue/use-tool-popup.js +123 -0
  44. package/dist/vue/use-toolbar-scroll.js +72 -0
  45. package/package.json +12 -6
  46. package/src/env.d.ts +14 -0
  47. package/src/index.ts +203 -13
  48. package/src/tools/__test__/builtin.test.ts +208 -66
  49. package/src/tools/__test__/context.test.ts +62 -2
  50. package/src/tools/builtin.ts +350 -6
  51. package/src/tools/context.ts +89 -11
  52. package/src/tools/download.ts +82 -0
  53. package/src/tools/export.worker.ts +61 -0
  54. package/src/tools/registry.ts +16 -1
  55. package/src/types/index.ts +6 -0
  56. package/src/types/sheet.ts +5 -3
  57. package/src/vue/__test__/formula-bar.test.ts +455 -0
  58. package/src/vue/__test__/import-file.test.ts +183 -0
  59. package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
  60. package/src/vue/__test__/insert-image-popup.test.ts +89 -0
  61. package/src/vue/__test__/insert-image.test.ts +100 -0
  62. package/src/vue/__test__/sheet-component.test.ts +716 -27
  63. package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
  64. package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
  65. package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
  66. package/src/vue/formula-bar.vue +473 -0
  67. package/src/vue/formula-suggest-list.vue +64 -0
  68. package/src/vue/import-file.ts +173 -0
  69. package/src/vue/insert-count-menu-item.vue +69 -0
  70. package/src/vue/insert-image.ts +90 -0
  71. package/src/vue/parsing.ts +11 -0
  72. package/src/vue/popup-helpers.ts +50 -0
  73. package/src/vue/popups/border-popup.vue +93 -0
  74. package/src/vue/popups/border-preset-glyph.vue +111 -0
  75. package/src/vue/popups/export-popup.vue +52 -0
  76. package/src/vue/popups/fill-color-popup.vue +39 -0
  77. package/src/vue/popups/find-popup.vue +111 -0
  78. package/src/vue/popups/font-color-popup.vue +39 -0
  79. package/src/vue/popups/font-size-popup.vue +52 -0
  80. package/src/vue/popups/import.worker.ts +66 -0
  81. package/src/vue/popups/insert-image-popup.vue +39 -0
  82. package/src/vue/sheet-context-menu.ts +229 -0
  83. package/src/vue/sheet-tabs.vue +162 -0
  84. package/src/vue/sheet-toolbar.vue +91 -0
  85. package/src/vue/sheet.vue +163 -148
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +12 -0
  88. package/src/vue/use-find-replace.ts +200 -0
  89. package/src/vue/use-formula-ref-select.ts +34 -0
  90. package/src/vue/use-formula-suggest.ts +150 -0
  91. package/src/vue/use-sheet-grid.ts +243 -0
  92. package/src/vue/use-sheet-state.ts +161 -0
  93. package/src/vue/use-sheet-tabs-bar.ts +115 -0
  94. package/src/vue/use-tool-groups.ts +61 -0
  95. package/src/vue/use-tool-popup.ts +150 -0
  96. package/src/vue/use-toolbar-scroll.ts +95 -0
  97. package/dist/core/address.d.ts +0 -60
  98. package/dist/core/address.js +0 -142
  99. package/dist/core/address.js.map +0 -1
  100. package/dist/core/cell-store.d.ts +0 -61
  101. package/dist/core/cell-store.js +0 -135
  102. package/dist/core/cell-store.js.map +0 -1
  103. package/dist/core/command/default-registry.d.ts +0 -10
  104. package/dist/core/command/default-registry.js +0 -18
  105. package/dist/core/command/default-registry.js.map +0 -1
  106. package/dist/core/command/history.d.ts +0 -64
  107. package/dist/core/command/history.js +0 -127
  108. package/dist/core/command/history.js.map +0 -1
  109. package/dist/core/command/merge-cells.d.ts +0 -21
  110. package/dist/core/command/merge-cells.js +0 -74
  111. package/dist/core/command/merge-cells.js.map +0 -1
  112. package/dist/core/command/registry.d.ts +0 -17
  113. package/dist/core/command/registry.js +0 -26
  114. package/dist/core/command/registry.js.map +0 -1
  115. package/dist/core/command/set-cell-formula.d.ts +0 -17
  116. package/dist/core/command/set-cell-formula.js +0 -29
  117. package/dist/core/command/set-cell-formula.js.map +0 -1
  118. package/dist/core/command/set-cell-value.d.ts +0 -22
  119. package/dist/core/command/set-cell-value.js +0 -35
  120. package/dist/core/command/set-cell-value.js.map +0 -1
  121. package/dist/core/command/types.d.ts +0 -68
  122. package/dist/core/events.js +0 -29
  123. package/dist/core/events.js.map +0 -1
  124. package/dist/core/formula/ast.d.ts +0 -58
  125. package/dist/core/formula/ast.js +0 -38
  126. package/dist/core/formula/ast.js.map +0 -1
  127. package/dist/core/formula/dependency-graph.d.ts +0 -70
  128. package/dist/core/formula/dependency-graph.js +0 -269
  129. package/dist/core/formula/dependency-graph.js.map +0 -1
  130. package/dist/core/formula/errors.d.ts +0 -25
  131. package/dist/core/formula/errors.js +0 -36
  132. package/dist/core/formula/errors.js.map +0 -1
  133. package/dist/core/formula/evaluator.d.ts +0 -41
  134. package/dist/core/formula/evaluator.js +0 -154
  135. package/dist/core/formula/evaluator.js.map +0 -1
  136. package/dist/core/formula/functions.d.ts +0 -30
  137. package/dist/core/formula/functions.js +0 -209
  138. package/dist/core/formula/functions.js.map +0 -1
  139. package/dist/core/formula/parser.d.ts +0 -7
  140. package/dist/core/formula/parser.js +0 -208
  141. package/dist/core/formula/parser.js.map +0 -1
  142. package/dist/core/formula/tokenizer.d.ts +0 -37
  143. package/dist/core/formula/tokenizer.js +0 -146
  144. package/dist/core/formula/tokenizer.js.map +0 -1
  145. package/dist/core/merge-manager.d.ts +0 -64
  146. package/dist/core/merge-manager.js +0 -111
  147. package/dist/core/merge-manager.js.map +0 -1
  148. package/dist/core/selection.d.ts +0 -31
  149. package/dist/core/selection.js +0 -62
  150. package/dist/core/selection.js.map +0 -1
  151. package/dist/core/sheet.d.ts +0 -105
  152. package/dist/core/sheet.js +0 -202
  153. package/dist/core/sheet.js.map +0 -1
  154. package/dist/core/workbook.d.ts +0 -43
  155. package/dist/core/workbook.js +0 -77
  156. package/dist/core/workbook.js.map +0 -1
  157. package/dist/grid/sheet-grid.d.ts +0 -59
  158. package/dist/grid/sheet-grid.js +0 -230
  159. package/dist/grid/sheet-grid.js.map +0 -1
  160. package/dist/tools/builtin.js.map +0 -1
  161. package/dist/tools/context.js.map +0 -1
  162. package/dist/tools/registry.js.map +0 -1
  163. package/dist/vue/index.d.ts +0 -2
  164. package/dist/vue/sheet.js.map +0 -1
  165. package/dist/vue/sheet.vue.d.ts +0 -24
  166. package/src/core/__test__/address.test.ts +0 -113
  167. package/src/core/__test__/cell-store.test.ts +0 -97
  168. package/src/core/__test__/command.test.ts +0 -377
  169. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  170. package/src/core/__test__/formula-engine.test.ts +0 -470
  171. package/src/core/__test__/formula-functions.test.ts +0 -178
  172. package/src/core/__test__/formula-parser.test.ts +0 -160
  173. package/src/core/__test__/history.test.ts +0 -225
  174. package/src/core/__test__/merge-manager.test.ts +0 -106
  175. package/src/core/__test__/sheet.test.ts +0 -142
  176. package/src/core/address.ts +0 -166
  177. package/src/core/cell-store.ts +0 -173
  178. package/src/core/command/default-registry.ts +0 -15
  179. package/src/core/command/history.ts +0 -167
  180. package/src/core/command/index.ts +0 -7
  181. package/src/core/command/merge-cells.ts +0 -74
  182. package/src/core/command/registry.ts +0 -30
  183. package/src/core/command/set-cell-formula.ts +0 -29
  184. package/src/core/command/set-cell-value.ts +0 -38
  185. package/src/core/command/types.ts +0 -73
  186. package/src/core/events.ts +0 -37
  187. package/src/core/formula/ast.ts +0 -66
  188. package/src/core/formula/dependency-graph.ts +0 -329
  189. package/src/core/formula/errors.ts +0 -41
  190. package/src/core/formula/evaluator.ts +0 -219
  191. package/src/core/formula/functions.ts +0 -266
  192. package/src/core/formula/index.ts +0 -7
  193. package/src/core/formula/parser.ts +0 -198
  194. package/src/core/formula/tokenizer.ts +0 -160
  195. package/src/core/merge-manager.ts +0 -158
  196. package/src/core/selection.ts +0 -66
  197. package/src/core/sheet.ts +0 -257
  198. package/src/core/workbook.ts +0 -99
  199. package/src/grid/__test__/canvas-mock.ts +0 -49
  200. package/src/grid/__test__/setup.ts +0 -3
  201. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  202. package/src/grid/__test__/sheet-grid.test.ts +0 -177
  203. package/src/grid/sheet-grid.ts +0 -283
@@ -1,17 +1,83 @@
1
- import { rangesEqual } from '../core/address'
1
+ import { FontColor } from '@veltra/icons/colorful'
2
+ import {
3
+ AlignBottom,
4
+ AlignCenter,
5
+ AlignTop,
6
+ Bold,
7
+ Border,
8
+ Download,
9
+ Fill,
10
+ FontSize,
11
+ Italic,
12
+ MergeCells,
13
+ PictureRounded,
14
+ Rollback,
15
+ Rollfront,
16
+ Search,
17
+ Strikethrough,
18
+ Underline,
19
+ UnmergeCells,
20
+ Upload,
21
+ VerticalAlignCenter,
22
+ VerticalAlignLeft,
23
+ VerticalAlignRight,
24
+ Wrap
25
+ } from '@veltra/icons/normal'
26
+ import { rangesEqual } from '@veltra/sheet-core/core/address'
27
+ import type { HorizontalAlign, VerticalAlign } from '@veltra/sheet-core/core/style/types'
28
+
29
+ import type { SheetContext } from './context'
2
30
  import { registerTool } from './registry'
3
31
 
32
+ /** 取活动格所在首选区;无选区返回 null */
33
+ function selectionRange(ctx: SheetContext) {
34
+ const { activeCell, ranges } = ctx.getSelection()
35
+ const range = ranges[0]
36
+ if (!activeCell || !range) return null
37
+ return { activeCell, range }
38
+ }
39
+
40
+ /** 字体 toggle:以活动格当前值为基准,对选区统一翻转(Excel 语义) */
41
+ function toggleFontFlag(
42
+ ctx: SheetContext,
43
+ field: 'bold' | 'italic' | 'underline' | 'strikethrough'
44
+ ): void {
45
+ const sel = selectionRange(ctx)
46
+ if (!sel) return
47
+ const on = ctx.getCellStyle(sel.activeCell)?.font?.[field] === true
48
+ ctx.setCellStyle(sel.range, { font: { [field]: on ? null : true } })
49
+ }
50
+
51
+ /** 对齐 toggle:点当前档 = 清除,点其它档 = 切换 */
52
+ function toggleAlign(
53
+ ctx: SheetContext,
54
+ axis: 'horizontal' | 'vertical',
55
+ value: HorizontalAlign | VerticalAlign
56
+ ): void {
57
+ const sel = selectionRange(ctx)
58
+ if (!sel) return
59
+ const current = ctx.getCellStyle(sel.activeCell)?.align?.[axis]
60
+ if (current === value) {
61
+ ctx.setCellStyle(sel.range, { align: { [axis]: null } })
62
+ } else {
63
+ ctx.setCellStyle(sel.range, { align: { [axis]: value } })
64
+ }
65
+ }
66
+
4
67
  /**
5
- * 内置工具(dogfood 扩展机制):undo/redo + 合并/取消合并。
68
+ * 内置工具(dogfood 扩展机制)。
6
69
  *
70
+ * 分组:history | cell | text | edit | insert | file。
7
71
  * 与第三方工具走同一注册通道,可由宿主 unregisterTool 移除或同 id 覆盖。
8
- * 本模块由 tools/index.ts 引入(与 core/command 的 default-registry 同构):
9
- * 经包入口导入即完成注册,无头使用 core 深导入不涉及此全局副作用。
72
+ * 本模块由包入口引入:经包入口导入即完成注册。
10
73
  */
11
74
 
75
+ // ─── history ───────────────────────────────────────────────
76
+
12
77
  registerTool({
13
78
  id: 'undo',
14
79
  title: '撤销',
80
+ icon: Rollback,
15
81
  tooltip: '撤销(Ctrl/Cmd+Z)',
16
82
  group: 'history',
17
83
  order: 0,
@@ -24,6 +90,7 @@ registerTool({
24
90
  registerTool({
25
91
  id: 'redo',
26
92
  title: '重做',
93
+ icon: Rollfront,
27
94
  tooltip: '重做(Ctrl/Cmd+Shift+Z 或 Ctrl+Y)',
28
95
  group: 'history',
29
96
  order: 1,
@@ -33,12 +100,39 @@ registerTool({
33
100
  }
34
101
  })
35
102
 
103
+ // ─── cell(边框 / 填充颜色 / 合并 / 取消合并)────────────────
104
+
105
+ registerTool({
106
+ id: 'border',
107
+ title: '边框',
108
+ icon: Border,
109
+ tooltip: '设置单元格边框',
110
+ group: 'cell',
111
+ order: 0,
112
+ popup: 'border',
113
+ disabled: (ctx) => !ctx.getSelection().activeCell,
114
+ onClick: () => {}
115
+ })
116
+
117
+ registerTool({
118
+ id: 'fill-color',
119
+ title: '填充颜色',
120
+ icon: Fill,
121
+ tooltip: '设置单元格背景填充',
122
+ group: 'cell',
123
+ order: 1,
124
+ popup: 'fill-color',
125
+ disabled: (ctx) => !ctx.getSelection().activeCell,
126
+ onClick: () => {}
127
+ })
128
+
36
129
  registerTool({
37
130
  id: 'merge',
38
131
  title: '合并',
132
+ icon: MergeCells,
39
133
  tooltip: '合并选中区域',
40
134
  group: 'cell',
41
- order: 0,
135
+ order: 2,
42
136
  disabled: (ctx) => {
43
137
  const { activeCell, ranges } = ctx.getSelection()
44
138
  const range = ranges[0]
@@ -59,9 +153,10 @@ registerTool({
59
153
  registerTool({
60
154
  id: 'unmerge',
61
155
  title: '取消合并',
156
+ icon: UnmergeCells,
62
157
  tooltip: '取消活动格所在合并',
63
158
  group: 'cell',
64
- order: 1,
159
+ order: 3,
65
160
  disabled: (ctx) => {
66
161
  const { activeCell } = ctx.getSelection()
67
162
  if (!activeCell) return true
@@ -73,3 +168,252 @@ registerTool({
73
168
  ctx.unmergeCells(ranges[0])
74
169
  }
75
170
  })
171
+
172
+ // ─── text(B/I/U/S / 字体颜色 / 字号 / 对齐×6 / 自动换行)────
173
+
174
+ registerTool({
175
+ id: 'bold',
176
+ title: '加粗',
177
+ icon: Bold,
178
+ tooltip: '加粗',
179
+ group: 'text',
180
+ order: 0,
181
+ disabled: (ctx) => !ctx.getSelection().activeCell,
182
+ active: (ctx) => {
183
+ const active = ctx.getSelection().activeCell
184
+ return !!active && ctx.getCellStyle(active)?.font?.bold === true
185
+ },
186
+ onClick: (ctx) => toggleFontFlag(ctx, 'bold')
187
+ })
188
+
189
+ registerTool({
190
+ id: 'italic',
191
+ title: '斜体',
192
+ icon: Italic,
193
+ tooltip: '斜体',
194
+ group: 'text',
195
+ order: 1,
196
+ disabled: (ctx) => !ctx.getSelection().activeCell,
197
+ active: (ctx) => {
198
+ const active = ctx.getSelection().activeCell
199
+ return !!active && ctx.getCellStyle(active)?.font?.italic === true
200
+ },
201
+ onClick: (ctx) => toggleFontFlag(ctx, 'italic')
202
+ })
203
+
204
+ registerTool({
205
+ id: 'underline',
206
+ title: '下划线',
207
+ icon: Underline,
208
+ tooltip: '下划线',
209
+ group: 'text',
210
+ order: 2,
211
+ disabled: (ctx) => !ctx.getSelection().activeCell,
212
+ active: (ctx) => {
213
+ const active = ctx.getSelection().activeCell
214
+ return !!active && ctx.getCellStyle(active)?.font?.underline === true
215
+ },
216
+ onClick: (ctx) => toggleFontFlag(ctx, 'underline')
217
+ })
218
+
219
+ registerTool({
220
+ id: 'strikethrough',
221
+ title: '删除线',
222
+ icon: Strikethrough,
223
+ tooltip: '删除线',
224
+ group: 'text',
225
+ order: 3,
226
+ disabled: (ctx) => !ctx.getSelection().activeCell,
227
+ active: (ctx) => {
228
+ const active = ctx.getSelection().activeCell
229
+ return !!active && ctx.getCellStyle(active)?.font?.strikethrough === true
230
+ },
231
+ onClick: (ctx) => toggleFontFlag(ctx, 'strikethrough')
232
+ })
233
+
234
+ registerTool({
235
+ id: 'font-color',
236
+ title: '字体颜色',
237
+ icon: FontColor,
238
+ tooltip: '设置字体颜色',
239
+ group: 'text',
240
+ order: 4,
241
+ popup: 'font-color',
242
+ disabled: (ctx) => !ctx.getSelection().activeCell,
243
+ onClick: () => {}
244
+ })
245
+
246
+ registerTool({
247
+ id: 'font-size',
248
+ title: '字号',
249
+ icon: FontSize,
250
+ tooltip: '设置字体大小',
251
+ group: 'text',
252
+ order: 5,
253
+ popup: 'font-size',
254
+ disabled: (ctx) => !ctx.getSelection().activeCell,
255
+ onClick: () => {}
256
+ })
257
+
258
+ registerTool({
259
+ id: 'align-left',
260
+ title: '左对齐',
261
+ icon: VerticalAlignLeft,
262
+ tooltip: '水平左对齐',
263
+ group: 'text',
264
+ order: 6,
265
+ disabled: (ctx) => !ctx.getSelection().activeCell,
266
+ active: (ctx) => {
267
+ const active = ctx.getSelection().activeCell
268
+ return !!active && ctx.getCellStyle(active)?.align?.horizontal === 'left'
269
+ },
270
+ onClick: (ctx) => toggleAlign(ctx, 'horizontal', 'left')
271
+ })
272
+
273
+ registerTool({
274
+ id: 'align-center',
275
+ title: '居中',
276
+ icon: VerticalAlignCenter,
277
+ tooltip: '水平居中',
278
+ group: 'text',
279
+ order: 7,
280
+ disabled: (ctx) => !ctx.getSelection().activeCell,
281
+ active: (ctx) => {
282
+ const active = ctx.getSelection().activeCell
283
+ return !!active && ctx.getCellStyle(active)?.align?.horizontal === 'center'
284
+ },
285
+ onClick: (ctx) => toggleAlign(ctx, 'horizontal', 'center')
286
+ })
287
+
288
+ registerTool({
289
+ id: 'align-right',
290
+ title: '右对齐',
291
+ icon: VerticalAlignRight,
292
+ tooltip: '水平右对齐',
293
+ group: 'text',
294
+ order: 8,
295
+ disabled: (ctx) => !ctx.getSelection().activeCell,
296
+ active: (ctx) => {
297
+ const active = ctx.getSelection().activeCell
298
+ return !!active && ctx.getCellStyle(active)?.align?.horizontal === 'right'
299
+ },
300
+ onClick: (ctx) => toggleAlign(ctx, 'horizontal', 'right')
301
+ })
302
+
303
+ registerTool({
304
+ id: 'valign-top',
305
+ title: '顶端对齐',
306
+ icon: AlignTop,
307
+ tooltip: '垂直顶端对齐',
308
+ group: 'text',
309
+ order: 9,
310
+ disabled: (ctx) => !ctx.getSelection().activeCell,
311
+ active: (ctx) => {
312
+ const active = ctx.getSelection().activeCell
313
+ return !!active && ctx.getCellStyle(active)?.align?.vertical === 'top'
314
+ },
315
+ onClick: (ctx) => toggleAlign(ctx, 'vertical', 'top')
316
+ })
317
+
318
+ registerTool({
319
+ id: 'valign-middle',
320
+ title: '垂直居中',
321
+ icon: AlignCenter,
322
+ tooltip: '垂直居中对齐',
323
+ group: 'text',
324
+ order: 10,
325
+ disabled: (ctx) => !ctx.getSelection().activeCell,
326
+ active: (ctx) => {
327
+ const active = ctx.getSelection().activeCell
328
+ return !!active && ctx.getCellStyle(active)?.align?.vertical === 'middle'
329
+ },
330
+ onClick: (ctx) => toggleAlign(ctx, 'vertical', 'middle')
331
+ })
332
+
333
+ registerTool({
334
+ id: 'valign-bottom',
335
+ title: '底端对齐',
336
+ icon: AlignBottom,
337
+ tooltip: '垂直底端对齐',
338
+ group: 'text',
339
+ order: 11,
340
+ disabled: (ctx) => !ctx.getSelection().activeCell,
341
+ active: (ctx) => {
342
+ const active = ctx.getSelection().activeCell
343
+ return !!active && ctx.getCellStyle(active)?.align?.vertical === 'bottom'
344
+ },
345
+ onClick: (ctx) => toggleAlign(ctx, 'vertical', 'bottom')
346
+ })
347
+
348
+ registerTool({
349
+ id: 'wrap-text',
350
+ title: '自动换行',
351
+ icon: Wrap,
352
+ tooltip: '自动换行',
353
+ group: 'text',
354
+ order: 12,
355
+ disabled: (ctx) => !ctx.getSelection().activeCell,
356
+ active: (ctx) => {
357
+ const active = ctx.getSelection().activeCell
358
+ return !!active && ctx.getCellStyle(active)?.align?.wrap === true
359
+ },
360
+ onClick: (ctx) => {
361
+ const sel = selectionRange(ctx)
362
+ if (!sel) return
363
+ const on = ctx.getCellStyle(sel.activeCell)?.align?.wrap === true
364
+ ctx.setCellStyle(sel.range, { align: { wrap: on ? null : true } })
365
+ }
366
+ })
367
+
368
+ // ─── edit(查找)───────────────────────────────────────────
369
+
370
+ registerTool({
371
+ id: 'find',
372
+ title: '查找',
373
+ icon: Search,
374
+ tooltip: '查找与替换(Ctrl/Cmd+F)',
375
+ group: 'edit',
376
+ order: 0,
377
+ popup: 'find',
378
+ onClick: () => {}
379
+ })
380
+
381
+ // ─── insert(插入图片)─────────────────────────────────────
382
+
383
+ registerTool({
384
+ id: 'insert-image',
385
+ title: '插入图片',
386
+ icon: PictureRounded,
387
+ tooltip: '插入浮动图片',
388
+ group: 'insert',
389
+ order: 0,
390
+ popup: 'insert-image',
391
+ disabled: (ctx) => !ctx.getSelection().activeCell,
392
+ onClick: () => {}
393
+ })
394
+
395
+ // ─── file(导入 / 导出)────────────────────────────────────
396
+ // 导出 = 弹层选 xlsx / csv;导入 = 点击直接系统文件选择(vue/import-file.ts,
397
+ // sheet.vue 覆盖 onClick;tools 层不 import vue)。
398
+ // 下载逻辑见 tools/download.ts(core/io 保持纯 TS 无头可测)。
399
+
400
+ registerTool({
401
+ id: 'import',
402
+ title: '导入',
403
+ icon: Download,
404
+ tooltip: '从 .xlsx / .csv 文件导入',
405
+ group: 'file',
406
+ order: 0,
407
+ onClick: () => {}
408
+ })
409
+
410
+ registerTool({
411
+ id: 'export',
412
+ title: '导出',
413
+ icon: Upload,
414
+ tooltip: '导出为 Excel 或 CSV',
415
+ group: 'file',
416
+ order: 1,
417
+ popup: 'export',
418
+ onClick: () => {}
419
+ })
@@ -1,10 +1,14 @@
1
- import type { CellAddress, CellRange } from '../core/address'
2
- import type { CellData, CellValue } from '../core/cell-store'
3
- import type { HistoryState } from '../core/command/history'
4
- import type { SetCellValueItem } from '../core/command/set-cell-value'
5
- import type { CellInfo } from '../core/merge-manager'
6
- import type { SelectionState } from '../core/selection'
7
- import type { Sheet } from '../core/sheet'
1
+ import type { CellAddress, CellRange } from '@veltra/sheet-core/core/address'
2
+ import type { CellData, CellValue } from '@veltra/sheet-core/core/cell-store'
3
+ import type { HistoryState } from '@veltra/sheet-core/core/command/history'
4
+ import type { ImageUpdateFields } from '@veltra/sheet-core/core/command/image'
5
+ import type { SetCellValueItem } from '@veltra/sheet-core/core/command/set-cell-value'
6
+ import type { ImageInput, SheetImage } from '@veltra/sheet-core/core/image'
7
+ import type { CellInfo } from '@veltra/sheet-core/core/merge-manager'
8
+ import type { SelectionState } from '@veltra/sheet-core/core/selection'
9
+ import type { FrozenState, Sheet } from '@veltra/sheet-core/core/sheet'
10
+ import type { CellStyle, CellStylePatch } from '@veltra/sheet-core/core/style/types'
11
+ import type { Workbook } from '@veltra/sheet-core/core/workbook'
8
12
 
9
13
  /**
10
14
  * SheetContext:工具的唯一操作门面。
@@ -18,12 +22,17 @@ import type { Sheet } from '../core/sheet'
18
22
  export interface SheetContext {
19
23
  /** 当前活动 sheet 名 */
20
24
  readonly sheetName: string
25
+ /**
26
+ * 当前工作簿(导入导出等需要工作簿级操作的场景;无头/单 sheet 场景可能为 undefined)。
27
+ * 只读引用:sheet 增删改名等结构操作仍应经宿主直接操作 Workbook(Phase 3 门面边界)。
28
+ */
29
+ readonly workbook?: Workbook
21
30
 
22
31
  // ─── 选区 ────────────────────────────────────────────────
23
32
  getSelection(): SelectionState
24
33
  /** 选中单格(被覆盖格自动定位锚点) */
25
34
  selectCell(addr: CellAddress): void
26
- selectRange(range: CellRange): void
35
+ selectRange(range: CellRange, active?: CellAddress): void
27
36
 
28
37
  // ─── 读取 ────────────────────────────────────────────────
29
38
  /** 原始存储语义读取(被合并覆盖格 → undefined) */
@@ -33,6 +42,14 @@ export interface SheetContext {
33
42
  /** 合并语义下的单元格信息(普通格 / 合并锚点 / 被覆盖格) */
34
43
  getCellInfo(addr: CellAddress): CellInfo
35
44
 
45
+ // ─── 样式(写入经命令系统,可 undo) ───────────────────────
46
+ /** 设置区域样式(部分合并语义,见 CellStylePatch);空样式 = 删除 s 字段 */
47
+ setCellStyle(range: CellRange, partial: CellStylePatch): void
48
+ /** 清除区域样式(保留值 / 公式) */
49
+ clearCellStyle(range: CellRange): void
50
+ /** 读取单元格样式(原始存储语义:被覆盖格 → undefined) */
51
+ getCellStyle(addr: CellAddress): CellStyle | undefined
52
+
36
53
  // ─── 写入(全部经命令系统,可 undo) ───────────────────────
37
54
  setCellValue(addr: CellAddress, value: CellValue): void
38
55
  setCellFormula(addr: CellAddress, formula: string): void
@@ -54,26 +71,67 @@ export interface SheetContext {
54
71
  readonly canUndo: boolean
55
72
  readonly canRedo: boolean
56
73
 
74
+ // ─── 冻结(模型状态,不进 undo;随快照序列化) ──────────────
75
+ /** 当前冻结状态(rows = 冻结顶部行数,cols = 冻结左侧列数) */
76
+ readonly frozen: FrozenState
77
+ /** 设置冻结行列数(不进 undo,同 rowHeights 先例) */
78
+ setFrozen(rows: number, cols: number): void
79
+
80
+ // ─── 行列插入/删除(结构变更,可 undo) ──────────────────
81
+ /** 插入 count 行到 at 行之前 */
82
+ insertRows(at: number, count?: number): void
83
+ /** 插入 count 列到 at 列之前 */
84
+ insertCols(at: number, count?: number): void
85
+ /** 删除 [at, at+count) 行 */
86
+ deleteRows(at: number, count?: number): void
87
+ /** 删除 [at, at+count) 列 */
88
+ deleteCols(at: number, count?: number): void
89
+
90
+ // ─── 浮动图片(写入经命令系统,可 undo) ──────────────────
91
+ /** 插入浮动图片;返回生成的 id */
92
+ insertImage(input: ImageInput): string
93
+ /** 删除浮动图片;不存在则无操作 */
94
+ removeImage(id: string): void
95
+ /** 更新浮动图片锚点/尺寸/文案;不存在或无变更则无操作 */
96
+ updateImage(id: string, patch: ImageUpdateFields): void
97
+ /** 只读图片列表(快照副本) */
98
+ getImages(): readonly SheetImage[]
99
+
57
100
  // ─── 事件订阅(订阅时绑定到当前活动 sheet;tab 切换后需重新订阅) ──
58
101
  onSelectionChange(handler: (state: SelectionState) => void): () => void
59
102
  onHistoryChange(handler: (state: HistoryState) => void): () => void
103
+ onFrozenChange(handler: (state: FrozenState) => void): () => void
104
+ /** 图片集合变化(单图带 id;整表替换时 id 缺省) */
105
+ onImageChange(handler: (payload: { id?: string }) => void): () => void
60
106
  }
61
107
 
62
108
  /**
63
109
  * 创建工具上下文。
64
110
  * @param resolveSheet 活动 sheet 解析器(USheet 传 `() => activeSheet`,
65
111
  * tab 切换后上下文自动指向当前 sheet);也可直接传 Sheet 实例(无头 / 测试场景)
112
+ * @param workbook 当前工作簿(导入导出等工具需要;无头 / 单 sheet 场景可省略)。
113
+ * 支持传解析函数 `() => Workbook`——USheet 在 props.workbook 动态切换后,
114
+ * 上下文始终解析到**当前**工作簿(#2:旧实现取构造时值快照,宿主切换
115
+ * workbook prop 后导出工具仍导出旧工作簿)
66
116
  */
67
- export function createSheetContext(resolveSheet: Sheet | (() => Sheet)): SheetContext {
117
+ export function createSheetContext(
118
+ resolveSheet: Sheet | (() => Sheet),
119
+ workbook?: Workbook | (() => Workbook)
120
+ ): SheetContext {
68
121
  const sheet = typeof resolveSheet === 'function' ? resolveSheet : () => resolveSheet
122
+ const resolveWorkbook =
123
+ typeof workbook === 'function' ? workbook : workbook != null ? () => workbook : undefined
69
124
  return {
125
+ get workbook() {
126
+ return resolveWorkbook?.()
127
+ },
70
128
  get sheetName() {
71
129
  return sheet().name
72
130
  },
73
131
 
74
132
  getSelection: () => sheet().getSelection(),
75
133
  selectCell: (addr) => sheet().selectCell(addr),
76
- selectRange: (range) => sheet().selectRange(range),
134
+ selectRange: (range, active) => sheet().selectRange(range, active),
77
135
 
78
136
  getCellData: (addr) => sheet().getCellData(addr),
79
137
  getDisplayValue: (addr) => sheet().getDisplayValue(addr),
@@ -84,9 +142,27 @@ export function createSheetContext(resolveSheet: Sheet | (() => Sheet)): SheetCo
84
142
  setCells: (items) => sheet().setCells(items),
85
143
  mergeCells: (range) => sheet().mergeCells(range),
86
144
  unmergeCells: (range) => sheet().unmergeCells(range),
145
+
146
+ setCellStyle: (range, partial) => sheet().setCellStyle(range, partial),
147
+ clearCellStyle: (range) => sheet().clearCellStyle(range),
148
+ getCellStyle: (addr) => sheet().getCellStyle(addr),
87
149
  executeCommand: <R = void>(commandId: string, params: unknown): R | undefined =>
88
150
  sheet().executeCommand<R>(commandId, params),
89
151
 
152
+ get frozen() {
153
+ return sheet().frozen
154
+ },
155
+ setFrozen: (rows, cols) => sheet().setFrozen(rows, cols),
156
+ insertRows: (at, count) => sheet().insertRows(at, count),
157
+ insertCols: (at, count) => sheet().insertCols(at, count),
158
+ deleteRows: (at, count) => sheet().deleteRows(at, count),
159
+ deleteCols: (at, count) => sheet().deleteCols(at, count),
160
+
161
+ insertImage: (input) => sheet().insertImage(input),
162
+ removeImage: (id) => sheet().removeImage(id),
163
+ updateImage: (id, patch) => sheet().updateImage(id, patch),
164
+ getImages: () => sheet().getImages(),
165
+
90
166
  beginTransaction: () => sheet().beginTransaction(),
91
167
  commit: () => sheet().commit(),
92
168
  rollback: () => sheet().rollback(),
@@ -101,6 +177,8 @@ export function createSheetContext(resolveSheet: Sheet | (() => Sheet)): SheetCo
101
177
  },
102
178
 
103
179
  onSelectionChange: (handler) => sheet().on('selection-change', handler),
104
- onHistoryChange: (handler) => sheet().on('history-change', handler)
180
+ onHistoryChange: (handler) => sheet().on('history-change', handler),
181
+ onFrozenChange: (handler) => sheet().on('frozen-change', handler),
182
+ onImageChange: (handler) => sheet().on('image-change', handler)
105
183
  }
106
184
  }
@@ -0,0 +1,82 @@
1
+ import { saveBlob } from '@cat-kit/fe'
2
+ import { exportSheetCsv, exportWorkbookXlsx } from '@veltra/sheet-core/core/io/export'
3
+ import type { Workbook } from '@veltra/sheet-core/core/workbook'
4
+
5
+ import type { SheetContext } from './context'
6
+ import type { ExportWorkerPayload, ExportWorkerResponse } from './export.worker'
7
+
8
+ /**
9
+ * 大工作簿的 XLSX 导出(hucre 序列化 + ZIP 压缩为秒级同步重活)移入 Web Worker:
10
+ * 主线程只采集快照(结构化克隆友好的纯数据,行高随 SheetSnapshot.rowHeights
11
+ * 携带),序列化与压缩在 worker 线程进行。worker 不可用(极端环境 / 打包产物
12
+ * 缺文件)回退主线程导出——与导入侧 parseXlsxAsync 同一模式。
13
+ */
14
+ async function exportWorkbookXlsxAsync(workbook: Workbook): Promise<Uint8Array> {
15
+ let worker: Worker | undefined
16
+ try {
17
+ // new URL 模式:dev(vite)与 build(rolldown)都支持把 worker 提为独立
18
+ // chunk(与 import.worker 一致);onerror 再兜底降级主线程
19
+ const url = import.meta.env?.DEV
20
+ ? new URL('./export.worker.ts', import.meta.url)
21
+ : new URL('./export.worker.js', import.meta.url)
22
+ worker = new Worker(url, { type: 'module' })
23
+ } catch {
24
+ return exportWorkbookXlsx(workbook)
25
+ }
26
+ const payload: ExportWorkerPayload = {
27
+ sheets: workbook.getSheets().map((sheet) => ({ name: sheet.name, snapshot: sheet.snapshot() })),
28
+ activeIndex: workbook.activeSheetIndex
29
+ }
30
+ return await new Promise<Uint8Array>((resolve, reject) => {
31
+ let settled = false
32
+ // worker 加载/启动失败(如打包产物缺文件)→ 降级主线程导出
33
+ const fallback = (): void => {
34
+ if (settled) return
35
+ settled = true
36
+ worker!.terminate()
37
+ resolve(exportWorkbookXlsx(workbook))
38
+ }
39
+ worker!.onmessage = (e: MessageEvent<ExportWorkerResponse>) => {
40
+ if (settled) return
41
+ settled = true
42
+ worker!.terminate()
43
+ const data = e.data
44
+ if (!data.ok || !data.buffer) {
45
+ reject(new Error(data.error ?? 'worker 导出失败'))
46
+ return
47
+ }
48
+ resolve(data.buffer)
49
+ }
50
+ worker!.onerror = () => fallback()
51
+ worker!.postMessage(payload)
52
+ })
53
+ }
54
+
55
+ /**
56
+ * 导出当前工作簿为 .xlsx(无 workbook 时空操作)。
57
+ * 下载动作复用 @cat-kit/fe 的 saveBlob(#28:不再自研 Object URL + a.click)。
58
+ * hucre 1.0 起 writeXlsx 会校验 sheet 名(Excel 非法字符 / 超长 / 保留名)并抛
59
+ * InvalidArgumentError——失败经返回的 Promise 传播,调用方(导出面板)负责提示。
60
+ */
61
+ export async function exportWorkbookFile(ctx: SheetContext): Promise<void> {
62
+ const workbook = ctx.workbook
63
+ if (!workbook) return
64
+ const buffer = await exportWorkbookXlsxAsync(workbook)
65
+ saveBlob(
66
+ new Blob([buffer as unknown as BlobPart], {
67
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
68
+ }),
69
+ `${workbook.activeSheet.name || 'workbook'}.xlsx`
70
+ )
71
+ }
72
+
73
+ /** 导出当前工作表为 .csv(无 workbook 时空操作;同步,异常直接抛给调用方) */
74
+ export function exportSheetCsvFile(ctx: SheetContext): void {
75
+ const workbook = ctx.workbook
76
+ if (!workbook) return
77
+ const csv = exportSheetCsv(workbook.activeSheet)
78
+ saveBlob(
79
+ new Blob([csv], { type: 'text/csv;charset=utf-8' }),
80
+ `${workbook.activeSheet.name || 'sheet'}.csv`
81
+ )
82
+ }