@veltra/sheet 2.5.8 → 2.7.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 (37) hide show
  1. package/dist/components/sheet/formula-bar.js +6 -1
  2. package/dist/components/sheet/sheet-context-menu.js +41 -5
  3. package/dist/components/sheet/sheet.js +19 -6
  4. package/dist/components/sheet/style2.css +1 -1
  5. package/dist/components/sheet/use-sheet-grid.js +20 -7
  6. package/dist/components/sheet/use-sheet-state.js +4 -1
  7. package/dist/components/sheet/use-tool-popup.js +1 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/tools/context.d.ts +15 -0
  10. package/dist/tools/context.js +22 -1
  11. package/dist/types/sheet.d.ts +25 -5
  12. package/package.json +5 -2
  13. package/src/components/sheet/__test__/formula-bar.test.ts +52 -42
  14. package/src/components/sheet/__test__/functions-popup.test.ts +1 -1
  15. package/src/components/sheet/__test__/import-file.test.ts +1 -1
  16. package/src/components/sheet/__test__/insert-count-menu-item.test.ts +1 -1
  17. package/src/components/sheet/__test__/insert-image-popup.test.ts +1 -1
  18. package/src/components/sheet/__test__/insert-image.test.ts +1 -1
  19. package/src/components/sheet/__test__/sheet-component.test.ts +357 -24
  20. package/src/components/sheet/__test__/sheet-context-menu.test.ts +32 -3
  21. package/src/components/sheet/__test__/use-formula-ref-select.test.ts +1 -1
  22. package/src/components/sheet/__test__/use-formula-suggest.test.ts +1 -1
  23. package/src/components/sheet/formula-bar.vue +14 -1
  24. package/src/components/sheet/sheet-context-menu.ts +61 -4
  25. package/src/components/sheet/sheet.vue +9 -4
  26. package/src/components/sheet/style.scss +2 -2
  27. package/src/components/sheet/use-sheet-grid.ts +36 -9
  28. package/src/components/sheet/use-sheet-state.ts +5 -1
  29. package/src/components/sheet/use-tool-popup.ts +5 -4
  30. package/src/index.ts +1 -1
  31. package/src/tools/__test__/apply-style.test.ts +1 -1
  32. package/src/tools/__test__/builtin.test.ts +1 -1
  33. package/src/tools/__test__/context.test.ts +66 -2
  34. package/src/tools/__test__/registry.test.ts +1 -1
  35. package/src/tools/context.ts +47 -0
  36. package/src/types/sheet.ts +24 -5
  37. package/src/components/sheet/parsing.ts +0 -11
@@ -4,6 +4,7 @@ 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 ? '✓ 冻结到当前列' : '冻结到当前列',
@@ -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')">
@@ -110,7 +111,8 @@ const props = withDefaults(defineProps<SheetProps>(), {
110
111
  showTabs: true,
111
112
  showRowHeader: true,
112
113
  showColHeader: true,
113
- readonly: false
114
+ readonly: false,
115
+ colResize: false
114
116
  })
115
117
 
116
118
  const emit = defineEmits<SheetEmits>()
@@ -129,7 +131,9 @@ const { workbook, activeIndex, sheetList, activeSheet, context, stateTick, syncF
129
131
  onBeforeSheetChange: () => closePopup(),
130
132
  rebuildGrid: () => rebuildGrid(),
131
133
  activateGrid: () => activateGrid(),
132
- pruneCache: (sheets) => pruneCache(sheets)
134
+ pruneCache: (sheets) => pruneCache(sheets),
135
+ // 行高/列宽菜单确认 → 活动 grid 引擎尺寸即时生效(getGrid 声明于下方,事件时才求值)
136
+ syncAxisSizes: (axis, indexes) => getGrid()?.applyAxisSizes(axis, indexes)
133
137
  })
134
138
 
135
139
  // ─── 弹层型工具编排(打开 / 关闭 / 面板事务)─────────────────────
@@ -205,14 +209,15 @@ const toolGroups = useToolGroups(context, stateTick)
205
209
  const gridRef = useTemplateRef<HTMLElement>('gridRef')
206
210
  const formulaBarRef = useTemplateRef<InstanceType<typeof UFormulaBar>>('formulaBarRef')
207
211
 
208
- const { rebuildGrid, activateGrid, pruneCache, getGrid } = useSheetGrid({
212
+ const { rebuildGrid, activateGrid, pruneCache, getGrid, setSelectionAnchor } = useSheetGrid({
209
213
  props,
210
214
  gridRef,
211
215
  getActiveSheet: () => activeSheet.value,
212
216
  context,
213
217
  formulaBarRef,
214
218
  // 工具栏「函数」弹框打开期间与 fx 入口一致:画布点击不清除目标格高亮
215
- isFunctionPopupOpen: () => popupTool.value?.popup === 'functions'
219
+ isFunctionPopupOpen: () => popupTool.value?.popup === 'functions',
220
+ onColResizeEnd: (event) => emit('col-resize-end', event)
216
221
  })
217
222
 
218
223
  const exposed: _SheetExposed = {
@@ -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;
@@ -39,6 +39,8 @@ interface UseSheetGridOptions {
39
39
  formulaBarRef: ElRef<FormulaBarMirror>
40
40
  /** 函数弹框打开中(宿主侧入口,如工具栏「函数」按钮) */
41
41
  isFunctionPopupOpen?: () => boolean
42
+ /** 列宽拖拽落定(SheetGrid 透传 → 宿主 emit col-resize-end) */
43
+ onColResizeEnd?: (event: { col: number; width: number }) => void
42
44
  }
43
45
 
44
46
  /** 缓存实例(LRU 淘汰):每个 sheet 一个独立容器 div,非激活容器 visibility:hidden 堆叠 */
@@ -55,9 +57,9 @@ interface CachedGrid {
55
57
 
56
58
  /**
57
59
  * 缓存容量:最近 N 个 sheet 的 SheetGrid 实例常驻(tab 来回切换零重建)。
58
- * 大文件多 sheet 场景下切换卡顿的主因是每次重建 VTable(实例创建 + scenegraph
60
+ * 大文件多 sheet 场景下切换卡顿的主因是每次重建引擎表格(实例创建 + 画布分层
59
61
  * 首屏构建 ~300ms);缓存命中时仅切换容器可见性 + 选区回驱(≈0)。
60
- * 每实例持有一个 canvas + scenegraph,容量即内存上限(含隐藏实例的渲染开销)。
62
+ * 每实例持有一组 [data-layer-kind] 分层画布,容量即内存上限(含隐藏实例的渲染开销)。
61
63
  */
62
64
  const GRID_CACHE_CAPACITY = 3
63
65
 
@@ -84,9 +86,9 @@ export function useSheetGrid(options: UseSheetGridOptions) {
84
86
 
85
87
  const menus =
86
88
  info.kind === 'row-header'
87
- ? buildRowHeaderMenus(context)
89
+ ? buildRowHeaderMenus(context, { readonly: props.readonly })
88
90
  : info.kind === 'col-header'
89
- ? buildColHeaderMenus(context)
91
+ ? buildColHeaderMenus(context, { readonly: props.readonly })
90
92
  : buildBodyMenus(context)
91
93
 
92
94
  // 最小宽度兜底;实际宽度由最长菜单项撑开(勿固定 240 导致空白过大)
@@ -126,9 +128,13 @@ export function useSheetGrid(options: UseSheetGridOptions) {
126
128
  resolveDisplayValue: props.resolveDisplayValue,
127
129
  resolveCellStyle: props.resolveCellStyle,
128
130
  resolveCellRenderer: props.resolveCellRenderer,
131
+ header: props.header,
132
+ editors: props.editors,
133
+ colResize: props.colResize,
129
134
  onContextMenu: handleContextMenu,
130
135
  onEditStart: (addr) => formulaBarRef.value?.mirrorGridEdit(addr),
131
136
  onEditEnd: (addr) => formulaBarRef.value?.exitMirror(addr),
137
+ onColResizeEnd: options.onColResizeEnd,
132
138
  // 引用选择:不回写模型选区,序列化为 A1 / A1:B2 交给公式栏;
133
139
  // 函数弹框打开期间(fx / 工具栏两入口)同样拦截,画布点击不离开目标格
134
140
  interceptSelection: () =>
@@ -198,13 +204,18 @@ export function useSheetGrid(options: UseSheetGridOptions) {
198
204
  * 命中时校验实例是否过期(隐藏期间程序化 structure-change——插入/删除/undo
199
205
  * 行列——vue 层只绑定激活 sheet 的 structure-change,隐藏实例靠自持订阅标记
200
206
  * dirty);过期则释放重建。命中且未过期只翻可见性:隐藏实例的 cell/merge/
201
- * content-reset 已在 GridSyncManager 置脏,setVisible(true) 时一次性 flush,
202
- * 不必每次切 tab 都 syncFromModel(会重放全部列宽)。
207
+ * content-reset 已在 SheetGrid 内部置脏,setVisible(true) 时一次性全量同步,
208
+ * 无需宿主补推送。
203
209
  */
204
210
  function activateGrid(): void {
205
211
  const sheet = getActiveSheet()
206
212
  const host = gridRef.value
207
213
  if (!host) return
214
+ // 换实例前清旧实例的选区锚点:fx 编辑会话随切 sheet 结束(公式栏同步复位
215
+ // 编辑态),缓存实例复用时不得残留上次会话的高亮
216
+ if (active && active.sheet !== sheet) {
217
+ active.grid.setSelectionAnchor(null)
218
+ }
208
219
  const cached = cache.get(sheet)
209
220
  if (cached && !cached.dirty) {
210
221
  cached.lastUsed = ++seq
@@ -249,10 +260,21 @@ export function useSheetGrid(options: UseSheetGridOptions) {
249
260
 
250
261
  onMounted(activateGrid)
251
262
 
252
- // props 尺寸显式增大时先扩张模型,再重建(删行缩小不走这条路径)
263
+ // props 尺寸显式增大时先扩张模型,再重建(删行缩小不走这条路径);
264
+ // header / editors 按引用更替判定(机制对象构造期装配,变化即重建——宿主用
265
+ // computed 持稳定引用,勿在模板内联对象字面量)
253
266
  watch(
254
267
  () =>
255
- [props.rows, props.cols, props.readonly, props.showRowHeader, props.showColHeader] as const,
268
+ [
269
+ props.rows,
270
+ props.cols,
271
+ props.readonly,
272
+ props.showRowHeader,
273
+ props.showColHeader,
274
+ props.header,
275
+ props.editors,
276
+ props.colResize
277
+ ] as const,
256
278
  ([nextRows, nextCols]) => {
257
279
  const sheet = getActiveSheet()
258
280
  if (nextRows != null) sheet.ensureTableSize(nextRows, 0)
@@ -265,5 +287,10 @@ export function useSheetGrid(options: UseSheetGridOptions) {
265
287
  invalidateAll()
266
288
  })
267
289
 
268
- return { rebuildGrid, activateGrid, pruneCache, getGrid: () => active?.grid }
290
+ /** fx 引用拾取会话置/清选区锚点(公式栏 → 活动实例透传;无活动实例时静默) */
291
+ function setSelectionAnchor(addr: CellAddress | null): void {
292
+ active?.grid.setSelectionAnchor(addr)
293
+ }
294
+
295
+ return { rebuildGrid, activateGrid, pruneCache, getGrid: () => active?.grid, setSelectionAnchor }
269
296
  }
@@ -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
1
  import { createRange } from '@veltra/sheet-core/core/address.js'
2
2
  import { Sheet } from '@veltra/sheet-core/core/sheet.js'
3
- import { describe, expect, it } from 'vitest'
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'
@@ -3,7 +3,7 @@ import { createRange } from '@veltra/sheet-core/core/address.js'
3
3
  import '../builtin'
4
4
  import { Sheet } from '@veltra/sheet-core/core/sheet.js'
5
5
  import { Workbook } from '@veltra/sheet-core/core/workbook.js'
6
- import { describe, expect, it, vi } from 'vitest'
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
1
  import { createRange } from '@veltra/sheet-core/core/address.js'
2
2
  import { Sheet } from '@veltra/sheet-core/core/sheet.js'
3
- import { describe, expect, it, vi } from 'vitest'
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'
@@ -12,6 +12,9 @@ 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),
@@ -4,7 +4,9 @@ import type {
4
4
  ResolveCellRenderer,
5
5
  ResolveCellStyleHook,
6
6
  ResolveDisplayValue,
7
- SheetGrid
7
+ SheetGrid,
8
+ SheetGridEditorsOptions,
9
+ SheetGridHeaderOptions
8
10
  } from '@veltra/sheet-core/grid'
9
11
  import type { DeconstructValue } from '@veltra/utils'
10
12
  import type { ComputedRef } from 'vue'
@@ -20,7 +22,7 @@ export interface SheetProps {
20
22
  /** 渲染列数,默认 26(A..Z) */
21
23
  cols?: number
22
24
  /**
23
- * 显示值覆盖(设计态 Binding Placeholder 等):覆盖 VTable record,不写 CellData.v
25
+ * 显示值覆盖(设计态 Binding Placeholder 等):覆盖引擎视口显示文本,不写 CellData.v
24
26
  */
25
27
  resolveDisplayValue?: ResolveDisplayValue
26
28
  /**
@@ -28,11 +30,21 @@ export interface SheetProps {
28
30
  */
29
31
  resolveCellStyle?: ResolveCellStyleHook
30
32
  /**
31
- * 动态单元格渲染(ADR-0004):视口布局时按格自定义渲染形态(VTable
32
- * customLayout,布局构建用 sheet-core 导出的 CustomLayout),返回 undefined
33
- * 回落默认渲染;不写模型、不进快照
33
+ * 动态单元格渲染(ADR-0004):视口布局时按格自定义渲染形态(返回引擎
34
+ * `CellRenderer`,见 `@veltra/sheet-core/grid`),返回 undefined 回落默认渲染;
35
+ * 不写模型、不进快照
34
36
  */
35
37
  resolveCellRenderer?: ResolveCellRenderer
38
+ /**
39
+ * 列头机制(透传 SheetGrid):按列列头标题与表头自定义 DOM 渲染,不传保持
40
+ * 缺省字母表头;传值变化(引用更替)触发网格重建
41
+ */
42
+ header?: SheetGridHeaderOptions
43
+ /**
44
+ * 类型化编辑器机制(透传 SheetGrid):多编辑器注册与按格路由,不传保持统一
45
+ * 文本编辑器;readonly 下忽略;传值变化(引用更替)触发网格重建
46
+ */
47
+ editors?: SheetGridEditorsOptions
36
48
  /** 是否显示工具栏,默认 true */
37
49
  showToolbar?: boolean
38
50
  /** 是否显示顶部公式栏(名称框 + fx 输入栏),默认 true */
@@ -45,11 +57,18 @@ export interface SheetProps {
45
57
  showColHeader?: boolean
46
58
  /** 只读预览(关闭编辑回写、填充柄等写入口) */
47
59
  readonly?: boolean
60
+ /**
61
+ * 列宽拖拽(透传 SheetGrid):readonly 下置 true 仅放开列头 resize 手柄,编辑仍
62
+ * 关闭;缺省 false 行为不变。变化触发网格重建(构造期选项)
63
+ */
64
+ colResize?: boolean
48
65
  }
49
66
 
50
67
  export interface SheetEmits {
51
68
  /** 激活 sheet 切换(点击 tab 或宿主调用 workbook.activateSheet) */
52
69
  (name: 'active-sheet-change', payload: { sheet: Sheet; index: number }): void
70
+ /** 列宽拖拽落定(写模型之后):载荷含列索引与最终宽度 */
71
+ (name: 'col-resize-end', payload: { col: number; width: number }): void
53
72
  }
54
73
 
55
74
  /** 在组件内部引用 */
@@ -1,11 +0,0 @@
1
- import type { InjectionKey, Ref } from 'vue'
2
-
3
- /**
4
- * xlsx 解析中状态(历史 inject key,现由 import-file 直接写 sheet.vue 持有的 ref)。
5
- * 保留导出以便测试或扩展仍可 provide/inject。
6
- */
7
- export const SHEET_PARSING_KEY: InjectionKey<Ref<boolean>> = Symbol('sheet-parsing')
8
-
9
- /** 解析进度(worker 分片构建期间 done/total;readXlsx 同步解析段无进度) */
10
- export const SHEET_PARSE_PROGRESS_KEY: InjectionKey<Ref<{ done: number; total: number }>> =
11
- Symbol('sheet-parse-progress')