@veltra/sheet 2.5.7 → 2.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.
Files changed (65) hide show
  1. package/dist/components/sheet/formula-bar.js +7 -2
  2. package/dist/components/sheet/import-file.js +1 -1
  3. package/dist/components/sheet/popup-helpers.js +1 -1
  4. package/dist/components/sheet/popups/border-popup.js +2 -2
  5. package/dist/components/sheet/popups/functions-popup.js +1 -1
  6. package/dist/components/sheet/popups/import.worker.d.ts +1 -1
  7. package/dist/components/sheet/popups/import.worker.js +1 -1
  8. package/dist/components/sheet/sheet-context-menu.js +42 -6
  9. package/dist/components/sheet/sheet.js +12 -4
  10. package/dist/components/sheet/style2.css +1 -1
  11. package/dist/components/sheet/use-find-replace.js +2 -2
  12. package/dist/components/sheet/use-formula-suggest.js +1 -1
  13. package/dist/components/sheet/use-sheet-grid.js +17 -7
  14. package/dist/components/sheet/use-sheet-state.js +5 -2
  15. package/dist/components/sheet/use-tool-popup.js +1 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/tools/builtin.js +1 -1
  18. package/dist/tools/context.d.ts +26 -11
  19. package/dist/tools/context.js +22 -1
  20. package/dist/tools/download.js +1 -1
  21. package/dist/tools/export.worker.d.ts +1 -1
  22. package/dist/tools/export.worker.js +1 -1
  23. package/dist/tools/registry.js +1 -1
  24. package/dist/types/sheet.d.ts +17 -7
  25. package/package.json +5 -2
  26. package/src/components/sheet/__test__/formula-bar.test.ts +54 -44
  27. package/src/components/sheet/__test__/functions-popup.test.ts +2 -2
  28. package/src/components/sheet/__test__/import-file.test.ts +4 -4
  29. package/src/components/sheet/__test__/insert-count-menu-item.test.ts +1 -1
  30. package/src/components/sheet/__test__/insert-image-popup.test.ts +2 -2
  31. package/src/components/sheet/__test__/insert-image.test.ts +2 -2
  32. package/src/components/sheet/__test__/sheet-component.test.ts +312 -25
  33. package/src/components/sheet/__test__/sheet-context-menu.test.ts +34 -5
  34. package/src/components/sheet/__test__/use-formula-ref-select.test.ts +1 -1
  35. package/src/components/sheet/__test__/use-formula-suggest.test.ts +2 -2
  36. package/src/components/sheet/formula-bar.vue +17 -4
  37. package/src/components/sheet/import-file.ts +3 -3
  38. package/src/components/sheet/insert-image.ts +1 -1
  39. package/src/components/sheet/popup-helpers.ts +3 -3
  40. package/src/components/sheet/popups/border-popup.vue +3 -3
  41. package/src/components/sheet/popups/find-popup.vue +1 -1
  42. package/src/components/sheet/popups/functions-popup.vue +1 -1
  43. package/src/components/sheet/popups/import.worker.ts +2 -2
  44. package/src/components/sheet/sheet-context-menu.ts +63 -6
  45. package/src/components/sheet/sheet-tabs.vue +2 -2
  46. package/src/components/sheet/sheet.vue +5 -2
  47. package/src/components/sheet/style.scss +2 -2
  48. package/src/components/sheet/use-find-replace.ts +3 -3
  49. package/src/components/sheet/use-formula-suggest.ts +1 -1
  50. package/src/components/sheet/use-sheet-grid.ts +33 -11
  51. package/src/components/sheet/use-sheet-state.ts +7 -3
  52. package/src/components/sheet/use-tool-popup.ts +5 -4
  53. package/src/index.ts +1 -1
  54. package/src/tools/__test__/apply-style.test.ts +3 -3
  55. package/src/tools/__test__/builtin.test.ts +4 -4
  56. package/src/tools/__test__/context.test.ts +68 -4
  57. package/src/tools/__test__/registry.test.ts +1 -1
  58. package/src/tools/apply-style.ts +3 -3
  59. package/src/tools/builtin.ts +2 -2
  60. package/src/tools/context.ts +58 -11
  61. package/src/tools/download.ts +2 -2
  62. package/src/tools/export.worker.ts +3 -3
  63. package/src/tools/registry.ts +1 -1
  64. package/src/types/sheet.ts +19 -7
  65. package/src/components/sheet/parsing.ts +0 -11
@@ -1,9 +1,10 @@
1
1
  import type { ContextmenuItem } from '@veltra/desktop'
2
- import { createRange, type CellAddress, type CellRange } from '@veltra/sheet-core/core/address'
3
- import type { NumFmt } from '@veltra/sheet-core/core/style/types'
2
+ import { createRange, type CellAddress, type CellRange } from '@veltra/sheet-core/core/address.js'
3
+ import type { NumFmt } from '@veltra/sheet-core/core/style/types.js'
4
4
  import { defineComponent, h } from 'vue'
5
5
 
6
6
  import type { SheetContext } from '../../tools/context'
7
+ import { MIN_ROW_COL_SIZE } from '../../tools/context'
7
8
  import { defaultToolRegistry } from '../../tools/registry'
8
9
  import InsertCountMenuItem from './insert-count-menu-item.vue'
9
10
  import { pickAndInsertImage } from './insert-image'
@@ -16,6 +17,13 @@ export const INSERT_COUNT_MAX = 1000
16
17
  export const DECIMAL_PLACES_MIN = 0
17
18
  export const DECIMAL_PLACES_MAX = 10
18
19
 
20
+ /** 行高/列宽数值项输入上限(下限复用门面 MIN_ROW_COL_SIZE,对齐引擎 resize 最小值) */
21
+ export const AXIS_SIZE_MAX = 1000
22
+
23
+ /** 数值项默认值回落:未设置自定义尺寸时展示引擎默认行高/列宽(对齐 sheet-core grid-theme) */
24
+ export const FALLBACK_ROW_HEIGHT = 28
25
+ export const FALLBACK_COL_WIDTH = 80
26
+
19
27
  /** 选区覆盖行/列数 → 插入默认 N(min 1 / max 1000) */
20
28
  export function defaultInsertCount(range: CellRange | null, axis: 'row' | 'col'): number {
21
29
  if (!range) return INSERT_COUNT_MIN
@@ -94,12 +102,46 @@ function insertCountRender(options: {
94
102
  return menuNumberRender({ ...options, min: INSERT_COUNT_MIN, max: INSERT_COUNT_MAX })
95
103
  }
96
104
 
105
+ /** 行号/列头菜单选项(readonly 时尺寸项禁用) */
106
+ export interface HeaderMenuOptions {
107
+ /** 只读模式:行高/列宽项禁用(尺寸写入属编辑操作) */
108
+ readonly?: boolean
109
+ }
110
+
111
+ /**
112
+ * 行高/列宽菜单项:内嵌数值输入,确认经门面批量应用到选区覆盖行/列。
113
+ * readonly 时降级为禁用文本项——render 内嵌组件自带点击路径,disabled 拦不住。
114
+ */
115
+ function axisSizeMenuItem(options: {
116
+ label: string
117
+ disabled: boolean
118
+ defaultValue: number
119
+ onConfirm: (n: number) => void
120
+ }): ContextmenuItem {
121
+ if (options.disabled) return { label: options.label, disabled: true }
122
+ return {
123
+ label: options.label,
124
+ keepOpen: true,
125
+ render: menuNumberRender({
126
+ prefix: options.label,
127
+ suffix: '像素',
128
+ defaultValue: options.defaultValue,
129
+ min: MIN_ROW_COL_SIZE,
130
+ max: AXIS_SIZE_MAX,
131
+ onConfirm: options.onConfirm
132
+ })
133
+ }
134
+ }
135
+
97
136
  function primaryRange(ctx: SheetContext): CellRange | null {
98
137
  return ctx.getSelection().ranges[0] ?? null
99
138
  }
100
139
 
101
- /** 行号右键菜单:插入×2 / 删除 / divider / 冻结 / 取消冻结 */
102
- export function buildRowHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
140
+ /** 行号右键菜单:插入×2 / 删除 / 行高 / divider / 冻结 / 取消冻结 */
141
+ export function buildRowHeaderMenus(
142
+ ctx: SheetContext,
143
+ options?: HeaderMenuOptions
144
+ ): ContextmenuItem[] {
103
145
  const range = primaryRange(ctx)
104
146
  const startRow = range?.start.row ?? 0
105
147
  const endRow = range?.end.row ?? startRow
@@ -130,6 +172,12 @@ export function buildRowHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
130
172
  })
131
173
  },
132
174
  { label: '删除行', callback: () => ctx.deleteRows(startRow, deleteCount) },
175
+ axisSizeMenuItem({
176
+ label: '行高',
177
+ disabled: options?.readonly === true,
178
+ defaultValue: ctx.getRowHeight(startRow) ?? FALLBACK_ROW_HEIGHT,
179
+ onConfirm: (n) => ctx.setRowHeightBySelection(n)
180
+ }),
133
181
  { divider: true },
134
182
  {
135
183
  label: freezeActive ? '✓ 冻结到当前行' : '冻结到当前行',
@@ -143,8 +191,11 @@ export function buildRowHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
143
191
  ]
144
192
  }
145
193
 
146
- /** 列头右键菜单:插入×2 / 删除 / divider / 冻结 / 取消冻结 */
147
- export function buildColHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
194
+ /** 列头右键菜单:插入×2 / 删除 / 列宽 / divider / 冻结 / 取消冻结 */
195
+ export function buildColHeaderMenus(
196
+ ctx: SheetContext,
197
+ options?: HeaderMenuOptions
198
+ ): ContextmenuItem[] {
148
199
  const range = primaryRange(ctx)
149
200
  const startCol = range?.start.col ?? 0
150
201
  const endCol = range?.end.col ?? startCol
@@ -175,6 +226,12 @@ export function buildColHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
175
226
  })
176
227
  },
177
228
  { label: '删除列', callback: () => ctx.deleteCols(startCol, deleteCount) },
229
+ axisSizeMenuItem({
230
+ label: '列宽',
231
+ disabled: options?.readonly === true,
232
+ defaultValue: ctx.getColWidth(startCol) ?? FALLBACK_COL_WIDTH,
233
+ onConfirm: (n) => ctx.setColWidthBySelection(n)
234
+ }),
178
235
  { divider: true },
179
236
  {
180
237
  label: freezeActive ? '✓ 冻结到当前列' : '冻结到当前列',
@@ -54,8 +54,8 @@
54
54
  <script lang="ts" setup>
55
55
  import { contextmenu, message, messageConfirm, UIcon } from '@veltra/desktop'
56
56
  import { ArrowLeft, ArrowRight } from '@veltra/icons/normal'
57
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
58
- import type { Workbook } from '@veltra/sheet-core/core/workbook'
57
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
58
+ import type { Workbook } from '@veltra/sheet-core/core/workbook.js'
59
59
  import { bem } from '@veltra/utils'
60
60
  import { nextTick, ref, toRef, useTemplateRef } from 'vue'
61
61
 
@@ -56,6 +56,7 @@
56
56
  ref="formulaBarRef"
57
57
  :sheet="activeSheet"
58
58
  :context="context"
59
+ :set-selection-anchor="setSelectionAnchor"
59
60
  />
60
61
 
61
62
  <div ref="gridRef" :class="cls.e('grid')">
@@ -129,7 +130,9 @@ const { workbook, activeIndex, sheetList, activeSheet, context, stateTick, syncF
129
130
  onBeforeSheetChange: () => closePopup(),
130
131
  rebuildGrid: () => rebuildGrid(),
131
132
  activateGrid: () => activateGrid(),
132
- pruneCache: (sheets) => pruneCache(sheets)
133
+ pruneCache: (sheets) => pruneCache(sheets),
134
+ // 行高/列宽菜单确认 → 活动 grid 引擎尺寸即时生效(getGrid 声明于下方,事件时才求值)
135
+ syncAxisSizes: (axis, indexes) => getGrid()?.applyAxisSizes(axis, indexes)
133
136
  })
134
137
 
135
138
  // ─── 弹层型工具编排(打开 / 关闭 / 面板事务)─────────────────────
@@ -205,7 +208,7 @@ const toolGroups = useToolGroups(context, stateTick)
205
208
  const gridRef = useTemplateRef<HTMLElement>('gridRef')
206
209
  const formulaBarRef = useTemplateRef<InstanceType<typeof UFormulaBar>>('formulaBarRef')
207
210
 
208
- const { rebuildGrid, activateGrid, pruneCache, getGrid } = useSheetGrid({
211
+ const { rebuildGrid, activateGrid, pruneCache, getGrid, setSelectionAnchor } = useSheetGrid({
209
212
  props,
210
213
  gridRef,
211
214
  getActiveSheet: () => activeSheet.value,
@@ -147,7 +147,7 @@
147
147
  gap: fn.use-var(gap, small);
148
148
  padding: fn.use-var(gap, small);
149
149
  border-bottom: 1px solid fn.use-var(border, muted-color);
150
- // 浮起的 fx 输入面板 / 候选列表需盖住下方 grid 与浮动图片叠层(image-layer z-index:1)
150
+ // 浮起的 fx 输入面板 / 候选列表需盖住下方 grid 与浮动图片叠层(引擎画布分层 z 序)
151
151
  position: relative;
152
152
  z-index: 30;
153
153
  }
@@ -717,7 +717,7 @@
717
717
  }
718
718
 
719
719
  /* LRU 缓存实例容器:absolute 堆叠于 grid 区,非激活实例 visibility:hidden
720
- (保持尺寸不触发 VTable resize,切回仅翻转可见性 + 选区回驱) */
720
+ (保持尺寸不触发引擎表格 resize,切回仅翻转可见性 + 选区回驱) */
721
721
  @include m.e(grid-instance) {
722
722
  position: absolute;
723
723
  inset: 0;
@@ -3,15 +3,15 @@ import {
3
3
  inferCellType,
4
4
  normalizeInputValue,
5
5
  type CellData
6
- } from '@veltra/sheet-core/core/cell-store'
6
+ } from '@veltra/sheet-core/core/cell-store.js'
7
7
  import {
8
8
  findAll,
9
9
  findNextFrom,
10
10
  findPrevFrom,
11
11
  type FindMatch,
12
12
  type FindOptions
13
- } from '@veltra/sheet-core/core/find'
14
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
13
+ } from '@veltra/sheet-core/core/find.js'
14
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
15
15
  import { computed, ref, shallowRef, watch } from 'vue'
16
16
 
17
17
  import type { SheetContext } from '../../tools/context'
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listFormulaFunctions,
3
3
  type FormulaFunctionMeta
4
- } from '@veltra/sheet-core/core/formula/functions'
4
+ } from '@veltra/sheet-core/core/formula/functions.js'
5
5
 
6
6
  /** 补全候选上限 */
7
7
  export const FORMULA_SUGGEST_LIMIT = 10
@@ -1,6 +1,6 @@
1
1
  import { contextmenu } from '@veltra/desktop'
2
- import type { CellAddress, CellRange } from '@veltra/sheet-core/core/address'
3
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
2
+ import type { CellAddress, CellRange } from '@veltra/sheet-core/core/address.js'
3
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
4
4
  import { SheetGrid, type SheetGridContextMenuInfo } from '@veltra/sheet-core/grid'
5
5
  import { onBeforeUnmount, onMounted, watch } from 'vue'
6
6
 
@@ -55,9 +55,9 @@ interface CachedGrid {
55
55
 
56
56
  /**
57
57
  * 缓存容量:最近 N 个 sheet 的 SheetGrid 实例常驻(tab 来回切换零重建)。
58
- * 大文件多 sheet 场景下切换卡顿的主因是每次重建 VTable(实例创建 + scenegraph
58
+ * 大文件多 sheet 场景下切换卡顿的主因是每次重建引擎表格(实例创建 + 画布分层
59
59
  * 首屏构建 ~300ms);缓存命中时仅切换容器可见性 + 选区回驱(≈0)。
60
- * 每实例持有一个 canvas + scenegraph,容量即内存上限(含隐藏实例的渲染开销)。
60
+ * 每实例持有一组 [data-layer-kind] 分层画布,容量即内存上限(含隐藏实例的渲染开销)。
61
61
  */
62
62
  const GRID_CACHE_CAPACITY = 3
63
63
 
@@ -84,9 +84,9 @@ export function useSheetGrid(options: UseSheetGridOptions) {
84
84
 
85
85
  const menus =
86
86
  info.kind === 'row-header'
87
- ? buildRowHeaderMenus(context)
87
+ ? buildRowHeaderMenus(context, { readonly: props.readonly })
88
88
  : info.kind === 'col-header'
89
- ? buildColHeaderMenus(context)
89
+ ? buildColHeaderMenus(context, { readonly: props.readonly })
90
90
  : buildBodyMenus(context)
91
91
 
92
92
  // 最小宽度兜底;实际宽度由最长菜单项撑开(勿固定 240 导致空白过大)
@@ -126,6 +126,8 @@ export function useSheetGrid(options: UseSheetGridOptions) {
126
126
  resolveDisplayValue: props.resolveDisplayValue,
127
127
  resolveCellStyle: props.resolveCellStyle,
128
128
  resolveCellRenderer: props.resolveCellRenderer,
129
+ header: props.header,
130
+ editors: props.editors,
129
131
  onContextMenu: handleContextMenu,
130
132
  onEditStart: (addr) => formulaBarRef.value?.mirrorGridEdit(addr),
131
133
  onEditEnd: (addr) => formulaBarRef.value?.exitMirror(addr),
@@ -198,13 +200,18 @@ export function useSheetGrid(options: UseSheetGridOptions) {
198
200
  * 命中时校验实例是否过期(隐藏期间程序化 structure-change——插入/删除/undo
199
201
  * 行列——vue 层只绑定激活 sheet 的 structure-change,隐藏实例靠自持订阅标记
200
202
  * dirty);过期则释放重建。命中且未过期只翻可见性:隐藏实例的 cell/merge/
201
- * content-reset 已在 GridSyncManager 置脏,setVisible(true) 时一次性 flush,
202
- * 不必每次切 tab 都 syncFromModel(会重放全部列宽)。
203
+ * content-reset 已在 SheetGrid 内部置脏,setVisible(true) 时一次性全量同步,
204
+ * 无需宿主补推送。
203
205
  */
204
206
  function activateGrid(): void {
205
207
  const sheet = getActiveSheet()
206
208
  const host = gridRef.value
207
209
  if (!host) return
210
+ // 换实例前清旧实例的选区锚点:fx 编辑会话随切 sheet 结束(公式栏同步复位
211
+ // 编辑态),缓存实例复用时不得残留上次会话的高亮
212
+ if (active && active.sheet !== sheet) {
213
+ active.grid.setSelectionAnchor(null)
214
+ }
208
215
  const cached = cache.get(sheet)
209
216
  if (cached && !cached.dirty) {
210
217
  cached.lastUsed = ++seq
@@ -249,10 +256,20 @@ export function useSheetGrid(options: UseSheetGridOptions) {
249
256
 
250
257
  onMounted(activateGrid)
251
258
 
252
- // props 尺寸显式增大时先扩张模型,再重建(删行缩小不走这条路径)
259
+ // props 尺寸显式增大时先扩张模型,再重建(删行缩小不走这条路径);
260
+ // header / editors 按引用更替判定(机制对象构造期装配,变化即重建——宿主用
261
+ // computed 持稳定引用,勿在模板内联对象字面量)
253
262
  watch(
254
263
  () =>
255
- [props.rows, props.cols, props.readonly, props.showRowHeader, props.showColHeader] as const,
264
+ [
265
+ props.rows,
266
+ props.cols,
267
+ props.readonly,
268
+ props.showRowHeader,
269
+ props.showColHeader,
270
+ props.header,
271
+ props.editors
272
+ ] as const,
256
273
  ([nextRows, nextCols]) => {
257
274
  const sheet = getActiveSheet()
258
275
  if (nextRows != null) sheet.ensureTableSize(nextRows, 0)
@@ -265,5 +282,10 @@ export function useSheetGrid(options: UseSheetGridOptions) {
265
282
  invalidateAll()
266
283
  })
267
284
 
268
- return { rebuildGrid, activateGrid, pruneCache, getGrid: () => active?.grid }
285
+ /** fx 引用拾取会话置/清选区锚点(公式栏 → 活动实例透传;无活动实例时静默) */
286
+ function setSelectionAnchor(addr: CellAddress | null): void {
287
+ active?.grid.setSelectionAnchor(addr)
288
+ }
289
+
290
+ return { rebuildGrid, activateGrid, pruneCache, getGrid: () => active?.grid, setSelectionAnchor }
269
291
  }
@@ -1,5 +1,5 @@
1
- import type { Sheet } from '@veltra/sheet-core/core/sheet'
2
- import { Workbook } from '@veltra/sheet-core/core/workbook'
1
+ import type { Sheet } from '@veltra/sheet-core/core/sheet.js'
2
+ import { Workbook } from '@veltra/sheet-core/core/workbook.js'
3
3
  import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
4
4
 
5
5
  import { createSheetContext } from '../../tools/context'
@@ -21,6 +21,8 @@ export interface SheetStateHooks {
21
21
  activateGrid: () => void
22
22
  /** sheet 列表变化(增删)后清理已删除 sheet 的缓存实例 */
23
23
  pruneCache?: (sheets: ReadonlyArray<Sheet>) => void
24
+ /** 行列尺寸经门面写模型后同步活动 grid 引擎 table(菜单数值项画面即时生效) */
25
+ syncAxisSizes: (axis: 'row' | 'col', indexes: number[]) => void
24
26
  }
25
27
 
26
28
  type Emit = (event: 'active-sheet-change', payload: { sheet: Sheet; index: number }) => void
@@ -51,7 +53,9 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
51
53
  () => workbook.value,
52
54
  {
53
55
  resolveGridSize: () =>
54
- resolveRenderSize(props.rows, props.cols, activeSheet.value.rows, activeSheet.value.cols)
56
+ resolveRenderSize(props.rows, props.cols, activeSheet.value.rows, activeSheet.value.cols),
57
+ // 行高/列宽菜单写入模型后同步活动 grid 引擎尺寸(Sheet 尺寸 API 不发事件)
58
+ syncAxisSizes: (axis, indexes) => hooks.syncAxisSizes(axis, indexes)
55
59
  }
56
60
  )
57
61
 
@@ -107,15 +107,16 @@ export function useToolPopup(context: SheetContext, rootEl: ElRef) {
107
107
  /** Ctrl/Cmd+F 打开 / 关闭查找条(与工具按钮同一 toggle 逻辑) */
108
108
  function onGlobalKeydown(event: KeyboardEvent): void {
109
109
  if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'f') return
110
- // 仅当焦点/事件源落在本实例容器内才响应(#6):容器外不得 preventDefault,
110
+ // 仅当事件源落在本实例容器内才响应(#6):容器外不得 preventDefault,
111
111
  // 否则劫持浏览器原生查找(页面任意位置按 Ctrl+F 都被屏蔽);同页多实例
112
112
  // 也各只响应自己容器内的按键,不再每个实例都弹出查找条
113
113
  const root = rootEl.value
114
114
  if (root) {
115
+ // 只认事件源:真实键盘事件 target 恒为焦点元素;activeElement 兜底会把
116
+ // 「容器程序化聚焦后(SheetGrid 挂载即聚焦)的异源合成按键」误判为本实例
117
+ // 按键,重新劫持容器外的浏览器查找
115
118
  const target = event.target instanceof Node ? event.target : null
116
- const active = document.activeElement instanceof Node ? document.activeElement : null
117
- const inside =
118
- (target !== null && root.contains(target)) || (active !== null && root.contains(active))
119
+ const inside = target !== null && root.contains(target)
119
120
  // 例外:弹层 Teleport 到 body 级 #pop-container(不在 root 内),本实例弹层
120
121
  // 打开期间焦点在弹层输入框里(如查找条内再按 Ctrl+F)——同样视为本实例的
121
122
  // 按键,否则查找条无法 toggle 关闭,且放行浏览器原生查找盖在上面。
package/src/index.ts CHANGED
@@ -11,6 +11,6 @@ export {
11
11
  type SheetToolGroup
12
12
  } from './tools/registry'
13
13
 
14
- export { type SheetProps, type SheetEmits, type _SheetExposed, type SheetExposed } from './types'
14
+ export { type SheetProps, type SheetEmits, type SheetExposed } from './types'
15
15
 
16
16
  export { USheet } from './components/sheet'
@@ -1,6 +1,6 @@
1
- import { createRange } from '@veltra/sheet-core/core/address'
2
- import { Sheet } from '@veltra/sheet-core/core/sheet'
3
- import { describe, expect, it } from 'vitest'
1
+ import { createRange } from '@veltra/sheet-core/core/address.js'
2
+ import { Sheet } from '@veltra/sheet-core/core/sheet.js'
3
+ import { describe, expect, it } from 'vite-plus/test'
4
4
 
5
5
  import { classifySelectionStyleTarget, type SelectionStyleTarget } from '../apply-style'
6
6
  import { createSheetContext } from '../context'
@@ -1,9 +1,9 @@
1
- import { createRange } from '@veltra/sheet-core/core/address'
1
+ import { createRange } from '@veltra/sheet-core/core/address.js'
2
2
 
3
3
  import '../builtin'
4
- import { Sheet } from '@veltra/sheet-core/core/sheet'
5
- import { Workbook } from '@veltra/sheet-core/core/workbook'
6
- import { describe, expect, it, vi } from 'vitest'
4
+ import { Sheet } from '@veltra/sheet-core/core/sheet.js'
5
+ import { Workbook } from '@veltra/sheet-core/core/workbook.js'
6
+ import { describe, expect, it, vi } from 'vite-plus/test'
7
7
 
8
8
  import { createSheetContext } from '../context'
9
9
  import { exportSheetCsvFile, exportWorkbookFile } from '../download'
@@ -1,8 +1,8 @@
1
- import { createRange } from '@veltra/sheet-core/core/address'
2
- import { Sheet } from '@veltra/sheet-core/core/sheet'
3
- import { describe, expect, it, vi } from 'vitest'
1
+ import { createRange } from '@veltra/sheet-core/core/address.js'
2
+ import { Sheet } from '@veltra/sheet-core/core/sheet.js'
3
+ import { describe, expect, it, vi } from 'vite-plus/test'
4
4
 
5
- import { createSheetContext } from '../context'
5
+ import { createSheetContext, MIN_ROW_COL_SIZE } from '../context'
6
6
  import type { SheetTool } from '../registry'
7
7
 
8
8
  describe('SheetContext', () => {
@@ -180,6 +180,70 @@ describe('SheetContext', () => {
180
180
  expect(sheet.getCellData({ row: 0, col: 0 })).toEqual({ v: 'keep', t: 's' })
181
181
  })
182
182
 
183
+ it('行列尺寸门面:读取 + 按选区覆盖行/列批量应用,不进 undo', () => {
184
+ const sheet = new Sheet()
185
+ const ctx = createSheetContext(sheet)
186
+
187
+ // 未设置自定义尺寸时读取为 undefined(视图层用默认行高/列宽)
188
+ expect(ctx.getRowHeight(1)).toBeUndefined()
189
+ expect(ctx.getColWidth(2)).toBeUndefined()
190
+
191
+ ctx.selectRange(createRange({ row: 1, col: 2 }, { row: 3, col: 4 }))
192
+ ctx.setRowHeightBySelection(40)
193
+ ctx.setColWidthBySelection(120)
194
+ // 选区覆盖行 1..3 全部生效,行 0 不受影响
195
+ expect(ctx.getRowHeight(1)).toBe(40)
196
+ expect(ctx.getRowHeight(2)).toBe(40)
197
+ expect(ctx.getRowHeight(3)).toBe(40)
198
+ expect(ctx.getRowHeight(0)).toBeUndefined()
199
+ // 选区覆盖列 2..4 全部生效,列 5 不受影响
200
+ expect(ctx.getColWidth(2)).toBe(120)
201
+ expect(ctx.getColWidth(3)).toBe(120)
202
+ expect(ctx.getColWidth(4)).toBe(120)
203
+ expect(ctx.getColWidth(5)).toBeUndefined()
204
+
205
+ // 同 rowHeights/冻结先例:尺寸写入不进 undo 历史
206
+ expect(ctx.canUndo).toBe(false)
207
+ })
208
+
209
+ it('行列尺寸门面:下限钳制到引擎最小值 20;空选区无操作', () => {
210
+ const sheet = new Sheet()
211
+ const ctx = createSheetContext(sheet)
212
+
213
+ ctx.selectRange(createRange({ row: 2, col: 3 }, { row: 2, col: 4 }))
214
+ ctx.setRowHeightBySelection(10)
215
+ ctx.setColWidthBySelection(5)
216
+ expect(ctx.getRowHeight(2)).toBe(MIN_ROW_COL_SIZE)
217
+ expect(ctx.getColWidth(3)).toBe(MIN_ROW_COL_SIZE)
218
+ expect(ctx.getColWidth(4)).toBe(MIN_ROW_COL_SIZE)
219
+
220
+ // 空选区:无写入目标,不产生任何尺寸
221
+ sheet.selection.clear()
222
+ ctx.setRowHeightBySelection(60)
223
+ ctx.setColWidthBySelection(200)
224
+ expect(ctx.getRowHeight(0)).toBeUndefined()
225
+ expect(ctx.getColWidth(0)).toBeUndefined()
226
+ })
227
+
228
+ it('行列尺寸门面:写入后经 syncAxisSizes 同步钩子(携带选区覆盖行/列;空选区不触发)', () => {
229
+ const sheet = new Sheet()
230
+ const syncAxisSizes = vi.fn()
231
+ const ctx = createSheetContext(sheet, undefined, { syncAxisSizes })
232
+
233
+ ctx.selectRange(createRange({ row: 1, col: 2 }, { row: 2, col: 3 }))
234
+ ctx.setRowHeightBySelection(40)
235
+ expect(syncAxisSizes).toHaveBeenCalledWith('row', [1, 2])
236
+ ctx.setColWidthBySelection(120)
237
+ expect(syncAxisSizes).toHaveBeenCalledWith('col', [2, 3])
238
+
239
+ // 空选区:模型无写入,同步钩子不触发
240
+ syncAxisSizes.mockClear()
241
+ sheet.selection.clear()
242
+ ctx.setRowHeightBySelection(60)
243
+ ctx.setColWidthBySelection(60)
244
+ expect(syncAxisSizes).not.toHaveBeenCalled()
245
+ })
246
+
183
247
  it('图片门面:insertImage / removeImage / updateImage / getImages / onImageChange,经命令可 undo', () => {
184
248
  const sheet = new Sheet()
185
249
  const ctx = createSheetContext(sheet)
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, vi } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vite-plus/test'
2
2
 
3
3
  import type { SheetTool } from '../registry'
4
4
  import { DEFAULT_TOOL_GROUP, ToolRegistry } from '../registry'
@@ -1,6 +1,6 @@
1
- import type { CellRange } from '@veltra/sheet-core/core/address'
2
- import type { SetAxisStyleItem } from '@veltra/sheet-core/core/command/set-axis-style'
3
- import type { CellStylePatch } from '@veltra/sheet-core/core/style/types'
1
+ import type { CellRange } from '@veltra/sheet-core/core/address.js'
2
+ import type { SetAxisStyleItem } from '@veltra/sheet-core/core/command/set-axis-style.js'
3
+ import type { CellStylePatch } from '@veltra/sheet-core/core/style/types.js'
4
4
 
5
5
  /** 样式写入目标:整行 / 整列默认样式,或逐格 */
6
6
  export type SelectionStyleTarget = 'row' | 'col' | 'cell'
@@ -24,8 +24,8 @@ import {
24
24
  VerticalAlignRight,
25
25
  Wrap
26
26
  } from '@veltra/icons/normal'
27
- import { rangesEqual } from '@veltra/sheet-core/core/address'
28
- import type { HorizontalAlign, VerticalAlign } from '@veltra/sheet-core/core/style/types'
27
+ import { rangesEqual } from '@veltra/sheet-core/core/address.js'
28
+ import type { HorizontalAlign, VerticalAlign } from '@veltra/sheet-core/core/style/types.js'
29
29
 
30
30
  import type { SheetContext } from './context'
31
31
  import { registerTool } from './registry'
@@ -1,17 +1,20 @@
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'
1
+ import type { CellAddress, CellRange } from '@veltra/sheet-core/core/address.js'
2
+ import type { CellData, CellValue } from '@veltra/sheet-core/core/cell-store.js'
3
+ import type { HistoryState } from '@veltra/sheet-core/core/command/history.js'
4
+ import type { ImageUpdateFields } from '@veltra/sheet-core/core/command/image.js'
5
+ import type { SetCellValueItem } from '@veltra/sheet-core/core/command/set-cell-value.js'
6
+ import type { ImageInput, SheetImage } from '@veltra/sheet-core/core/image.js'
7
+ import type { CellInfo } from '@veltra/sheet-core/core/merge-manager.js'
8
+ import type { SelectionState } from '@veltra/sheet-core/core/selection.js'
9
+ import type { FrozenState, Sheet } from '@veltra/sheet-core/core/sheet.js'
10
+ import type { CellStyle, CellStylePatch } from '@veltra/sheet-core/core/style/types.js'
11
+ import type { Workbook } from '@veltra/sheet-core/core/workbook.js'
12
12
 
13
13
  import { axisStyleItemsForRange, classifySelectionStyleTarget } from './apply-style'
14
14
 
15
+ /** 行列尺寸下限:对齐引擎 resize 最小值(`MIN_ROW_HEIGHT` / `MIN_COL_WIDTH` = 20) */
16
+ export const MIN_ROW_COL_SIZE = 20
17
+
15
18
  /**
16
19
  * SheetContext:工具的唯一操作门面。
17
20
  *
@@ -100,6 +103,16 @@ export interface SheetContext {
100
103
  /** 设置冻结行列数(不进 undo,同 rowHeights 先例) */
101
104
  setFrozen(rows: number, cols: number): void
102
105
 
106
+ // ─── 行列尺寸(模型状态,不进 undo;随快照序列化) ─────────
107
+ /** 读取自定义行高;未设置返回 undefined(视图层用默认行高) */
108
+ getRowHeight(row: number): number | undefined
109
+ /** 读取自定义列宽;未设置返回 undefined(视图层用默认列宽) */
110
+ getColWidth(col: number): number | undefined
111
+ /** 按选区覆盖行批量设置行高(下限钳到 MIN_ROW_COL_SIZE;不进 undo,同 rowHeights 先例) */
112
+ setRowHeightBySelection(height: number): void
113
+ /** 按选区覆盖列批量设置列宽(下限钳到 MIN_ROW_COL_SIZE;不进 undo,同 colWidths 先例) */
114
+ setColWidthBySelection(width: number): void
115
+
103
116
  // ─── 行列插入/删除(结构变更,可 undo) ──────────────────
104
117
  /** 插入 count 行到 at 行之前 */
105
118
  insertRows(at: number, count?: number): void
@@ -135,6 +148,12 @@ export interface SheetContextOptions {
135
148
  * applyStyle / clearStyle 用其判定整行/整列;缺省时不走行列默认样式。
136
149
  */
137
150
  resolveGridSize?: () => { rows: number; cols: number }
151
+ /**
152
+ * 行列尺寸写模型后的即时同步钩子:`Sheet.setRowHeight`/`setColWidth` 不发事件,
153
+ * 宿主经此把尺寸落到活动 SheetGrid 引擎 table(菜单数值项画面即时生效);
154
+ * 缺省(无头 / 测试直挂 Sheet)时仅写模型。
155
+ */
156
+ syncAxisSizes?: (axis: 'row' | 'col', indexes: number[]) => void
138
157
  }
139
158
 
140
159
  /**
@@ -146,6 +165,7 @@ export interface SheetContextOptions {
146
165
  * 上下文始终解析到**当前**工作簿(#2:旧实现取构造时值快照,宿主切换
147
166
  * workbook prop 后导出工具仍导出旧工作簿)
148
167
  * @param options.resolveGridSize 渲染尺寸解析器(整行/整列样式路由)
168
+ * @param options.syncAxisSizes 行列尺寸写模型后的即时同步钩子(USheet 接活动 grid)
149
169
  */
150
170
  export function createSheetContext(
151
171
  resolveSheet: Sheet | (() => Sheet),
@@ -156,6 +176,7 @@ export function createSheetContext(
156
176
  const resolveWorkbook =
157
177
  typeof workbook === 'function' ? workbook : workbook != null ? () => workbook : undefined
158
178
  const resolveGridSize = options?.resolveGridSize
179
+ const syncAxisSizes = options?.syncAxisSizes
159
180
  const resolveTarget = (range: CellRange) => {
160
181
  const size = resolveGridSize?.()
161
182
  return size ? classifySelectionStyleTarget(range, size.rows, size.cols) : 'cell'
@@ -216,6 +237,32 @@ export function createSheetContext(
216
237
  return sheet().frozen
217
238
  },
218
239
  setFrozen: (rows, cols) => sheet().setFrozen(rows, cols),
240
+
241
+ getRowHeight: (row) => sheet().getRowHeight(row),
242
+ getColWidth: (col) => sheet().getColWidth(col),
243
+ setRowHeightBySelection: (height) => {
244
+ const size = Math.max(MIN_ROW_COL_SIZE, height)
245
+ const rows: number[] = []
246
+ for (const range of sheet().getSelection().ranges) {
247
+ for (let row = range.start.row; row <= range.end.row; row++) {
248
+ sheet().setRowHeight(row, size)
249
+ rows.push(row)
250
+ }
251
+ }
252
+ // 写模型后即时同步活动 grid(Sheet.setRowHeight 不发事件)
253
+ if (rows.length > 0) syncAxisSizes?.('row', rows)
254
+ },
255
+ setColWidthBySelection: (width) => {
256
+ const size = Math.max(MIN_ROW_COL_SIZE, width)
257
+ const cols: number[] = []
258
+ for (const range of sheet().getSelection().ranges) {
259
+ for (let col = range.start.col; col <= range.end.col; col++) {
260
+ sheet().setColWidth(col, size)
261
+ cols.push(col)
262
+ }
263
+ }
264
+ if (cols.length > 0) syncAxisSizes?.('col', cols)
265
+ },
219
266
  insertRows: (at, count) => sheet().insertRows(at, count),
220
267
  insertCols: (at, count) => sheet().insertCols(at, count),
221
268
  deleteRows: (at, count) => sheet().deleteRows(at, count),
@@ -1,6 +1,6 @@
1
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'
2
+ import { exportSheetCsv, exportWorkbookXlsx } from '@veltra/sheet-core/core/io/export.js'
3
+ import type { Workbook } from '@veltra/sheet-core/core/workbook.js'
4
4
 
5
5
  import type { SheetContext } from './context'
6
6
  import type { ExportWorkerPayload, ExportWorkerResponse } from './export.worker'
@@ -12,7 +12,7 @@
12
12
  * 静态 import 在 vite dev 的 worker 上下文中会因模块图加载顺序导致
13
13
  * `Workbook is not defined`(实测)。
14
14
  */
15
- import type { SheetSnapshot } from '@veltra/sheet-core/core/sheet'
15
+ import type { SheetSnapshot } from '@veltra/sheet-core/core/sheet.js'
16
16
 
17
17
  export interface ExportWorkerPayload {
18
18
  sheets: { name: string; snapshot: SheetSnapshot }[]
@@ -27,8 +27,8 @@ export interface ExportWorkerResponse {
27
27
 
28
28
  self.onmessage = (e: MessageEvent<ExportWorkerPayload>): void => {
29
29
  void Promise.all([
30
- import('@veltra/sheet-core/core/io/export'),
31
- import('@veltra/sheet-core/core/workbook')
30
+ import('@veltra/sheet-core/core/io/export.js'),
31
+ import('@veltra/sheet-core/core/workbook.js')
32
32
  ])
33
33
  .then(([{ exportWorkbookXlsx }, { Workbook }]) => {
34
34
  const wb = new Workbook()
@@ -1,4 +1,4 @@
1
- import { TypedEventEmitter } from '@veltra/sheet-core/core/events'
1
+ import { TypedEventEmitter } from '@veltra/sheet-core/core/events.js'
2
2
 
3
3
  import type { SheetContext } from './context'
4
4