@veltra/sheet 2.2.0 → 2.3.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 (84) hide show
  1. package/dist/components/report/binding-badge.js +45 -21
  2. package/dist/components/report/designer/col-widths.js +18 -4
  3. package/dist/components/report/designer/conditional-rules/helpers.js +3 -3
  4. package/dist/components/report/designer/float-panel.js +73 -36
  5. package/dist/components/report/designer/rule-row.js +111 -127
  6. package/dist/components/report/designer/rules-dialog.js +5 -22
  7. package/dist/components/report/preview-grid-size.js +32 -0
  8. package/dist/components/report/report-designer.js +75 -25
  9. package/dist/components/report/report-viewer.js +38 -25
  10. package/dist/components/report/style2.css +44 -39
  11. package/dist/components/report/use-report-designer.js +34 -12
  12. package/dist/components/report/use-report-viewer.js +6 -1
  13. package/dist/components/sheet/popups/border-popup.js +30 -2
  14. package/dist/components/sheet/popups/fill-color-popup.js +2 -2
  15. package/dist/components/sheet/popups/font-color-popup.js +2 -2
  16. package/dist/components/sheet/popups/font-size-popup.js +2 -2
  17. package/dist/components/sheet/sheet-context-menu.js +6 -3
  18. package/dist/components/sheet/sheet.js +8 -0
  19. package/dist/components/sheet/use-sheet-grid.js +14 -4
  20. package/dist/components/sheet/use-sheet-state.js +3 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +2 -2
  23. package/dist/report/binding.d.ts +49 -11
  24. package/dist/report/binding.js +186 -29
  25. package/dist/report/connector.js +1 -1
  26. package/dist/report/export-xlsx.d.ts +3 -5
  27. package/dist/report/export-xlsx.js +7 -10
  28. package/dist/report/index.d.ts +2 -2
  29. package/dist/report/render/aggregate.js +20 -8
  30. package/dist/report/template.d.ts +8 -4
  31. package/dist/report/template.js +5 -2
  32. package/dist/tools/apply-style.js +29 -0
  33. package/dist/tools/builtin.js +17 -17
  34. package/dist/tools/context.d.ts +38 -5
  35. package/dist/tools/context.js +38 -1
  36. package/dist/types/report-designer.d.ts +2 -0
  37. package/dist/types/report-viewer.d.ts +2 -2
  38. package/dist/types/sheet.d.ts +4 -0
  39. package/package.json +6 -6
  40. package/src/components/report/__test__/conditional-rules-helpers.test.ts +1 -0
  41. package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
  42. package/src/components/report/__test__/report-designer.test.ts +76 -0
  43. package/src/components/report/__test__/report-viewer.test.ts +24 -1
  44. package/src/components/report/__test__/use-report-designer.test.ts +142 -10
  45. package/src/components/report/binding-badge.ts +39 -20
  46. package/src/components/report/designer/col-widths.ts +34 -11
  47. package/src/components/report/designer/conditional-rules/helpers.ts +6 -3
  48. package/src/components/report/designer/float-panel.vue +71 -27
  49. package/src/components/report/designer/rule-row.vue +67 -67
  50. package/src/components/report/designer/rules-dialog.vue +3 -19
  51. package/src/components/report/preview-grid-size.ts +33 -0
  52. package/src/components/report/report-designer.vue +79 -15
  53. package/src/components/report/report-viewer.vue +37 -29
  54. package/src/components/report/style-designer.scss +45 -41
  55. package/src/components/report/use-report-designer.ts +63 -14
  56. package/src/components/report/use-report-viewer.ts +11 -2
  57. package/src/components/sheet/__test__/sheet-component.test.ts +37 -0
  58. package/src/components/sheet/__test__/sheet-context-menu.test.ts +3 -2
  59. package/src/components/sheet/popups/border-popup.vue +31 -3
  60. package/src/components/sheet/popups/fill-color-popup.vue +2 -2
  61. package/src/components/sheet/popups/font-color-popup.vue +2 -2
  62. package/src/components/sheet/popups/font-size-popup.vue +2 -2
  63. package/src/components/sheet/sheet-context-menu.ts +8 -2
  64. package/src/components/sheet/sheet.vue +2 -0
  65. package/src/components/sheet/use-sheet-grid.ts +17 -3
  66. package/src/components/sheet/use-sheet-state.ts +8 -3
  67. package/src/report/__test__/binding.test.ts +103 -1
  68. package/src/report/__test__/connector.test.ts +21 -0
  69. package/src/report/__test__/export-xlsx.test.ts +4 -4
  70. package/src/report/__test__/render.test.ts +102 -0
  71. package/src/report/__test__/template.test.ts +11 -0
  72. package/src/report/binding.ts +278 -47
  73. package/src/report/connector.ts +3 -2
  74. package/src/report/export-xlsx.ts +9 -14
  75. package/src/report/render/aggregate.ts +29 -8
  76. package/src/report/template.ts +8 -4
  77. package/src/tools/__test__/apply-style.test.ts +129 -0
  78. package/src/tools/apply-style.ts +41 -0
  79. package/src/tools/builtin.ts +18 -18
  80. package/src/tools/context.ts +67 -4
  81. package/src/tools/index.ts +1 -0
  82. package/src/types/report-designer.ts +2 -0
  83. package/src/types/report-viewer.ts +2 -2
  84. package/src/types/sheet.ts +4 -0
@@ -62,6 +62,22 @@
62
62
  min-width: 0;
63
63
  }
64
64
 
65
+ @include m.e(pick-hint) {
66
+ position: absolute;
67
+ left: 50%;
68
+ bottom: fn.use-var(gap, small);
69
+ z-index: 17;
70
+ transform: translateX(-50%);
71
+ padding: 4px fn.use-var(gap, small);
72
+ border-radius: fn.use-var(radius, default);
73
+ background: color-mix(in srgb, fn.use-var(bg-color, top) 92%, transparent);
74
+ border: 1px dashed fn.use-var(color, primary);
75
+ font-size: fn.use-var(font-size-main, small);
76
+ color: fn.use-var(text-color, second);
77
+ pointer-events: none;
78
+ white-space: nowrap;
79
+ }
80
+
65
81
  /* 数据中枢 drawer 面板宽度(class 经 $attrs 落到 .u-drawer 面板;须压过 .u-drawer.is-right 的 320px) */
66
82
  @include m.e(hub-drawer) {
67
83
  &.u-drawer {
@@ -123,7 +139,7 @@
123
139
  }
124
140
  }
125
141
 
126
- /* Action Pill:选中绑定格上方的悬浮编辑卡(角色 / 聚合 / 条件样式 / 排序 / 清除绑定) */
142
+ /* Action Pill:选中绑定格上方的悬浮编辑卡(预设 + 互补的展开/聚合 / 条件样式 / 排序 / 清除绑定) */
127
143
  @include m.b(report-float-panel) {
128
144
  position: absolute;
129
145
  z-index: 18;
@@ -223,7 +239,7 @@
223
239
  }
224
240
 
225
241
  @include m.e(parent-select) {
226
- width: 88px;
242
+ width: 180px;
227
243
  flex: none;
228
244
  }
229
245
 
@@ -244,11 +260,12 @@
244
260
  @include m.b(report-rules-dialog) {
245
261
  display: flex;
246
262
  flex-direction: column;
247
- gap: 12px;
263
+ gap: 10px;
248
264
 
249
265
  @include m.e(hint) {
250
266
  margin: 0;
251
267
  font-size: 12px;
268
+ line-height: 1.5;
252
269
  color: fn.use-var(text-color, second);
253
270
  }
254
271
 
@@ -278,17 +295,18 @@
278
295
  display: flex;
279
296
  align-items: flex-start;
280
297
  gap: 8px;
281
- padding: 10px;
298
+ padding: 10px 10px 10px 6px;
282
299
  border: fn.use-var(border-muted);
283
300
  border-radius: 8px;
284
- background: fn.use-var(bg-color, top);
301
+ background: fn.use-var(bg-color, middle);
285
302
 
286
303
  @include m.e(handle) {
287
304
  flex: none;
288
- width: 20px;
289
- margin-top: 4px;
290
- color: fn.use-var(text-color, second);
291
- font-size: 16px;
305
+ width: 16px;
306
+ margin-top: 6px;
307
+ color: fn.use-var(text-color, placeholder);
308
+ font-size: 12px;
309
+ letter-spacing: -1px;
292
310
  line-height: 1;
293
311
  cursor: grab;
294
312
  user-select: none;
@@ -307,32 +325,33 @@
307
325
  gap: 8px;
308
326
  }
309
327
 
310
- @include m.e(meta) {
328
+ @include m.e(when) {
311
329
  display: flex;
312
330
  align-items: center;
313
- gap: 8px;
331
+ gap: 6px;
314
332
  flex-wrap: wrap;
315
333
  }
316
334
 
317
- @include m.e(field-select) {
318
- width: 120px;
319
- flex: none;
335
+ @include m.e(then) {
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 6px;
339
+ flex-wrap: wrap;
320
340
  }
321
341
 
322
- @include m.e(scope-select) {
323
- width: 72px;
342
+ @include m.e(lead) {
324
343
  flex: none;
344
+ font-size: 12px;
345
+ color: fn.use-var(text-color, second);
325
346
  }
326
347
 
327
- @include m.e(condition) {
328
- display: grid;
329
- grid-template-columns: 72px minmax(0, 1fr) auto;
330
- align-items: center;
331
- gap: 8px;
348
+ @include m.e(field-select) {
349
+ width: 128px;
350
+ flex: none;
332
351
  }
333
352
 
334
353
  @include m.e(operator) {
335
- width: 72px;
354
+ width: 96px;
336
355
  flex: none;
337
356
  }
338
357
 
@@ -345,12 +364,12 @@
345
364
  }
346
365
 
347
366
  @include m.e(scalar-input) {
348
- width: 112px;
367
+ width: 120px;
349
368
  flex: none;
350
369
  }
351
370
 
352
371
  @include m.e(between-input) {
353
- width: 96px;
372
+ width: 88px;
354
373
  flex: none;
355
374
  }
356
375
 
@@ -359,18 +378,6 @@
359
378
  color: fn.use-var(text-color, second);
360
379
  }
361
380
 
362
- @include m.e(style) {
363
- display: flex;
364
- align-items: center;
365
- gap: 8px;
366
- flex-wrap: wrap;
367
- }
368
-
369
- @include m.e(style-label) {
370
- font-size: 12px;
371
- color: fn.use-var(text-color, second);
372
- }
373
-
374
381
  @include m.e(toggle) {
375
382
  min-width: 28px;
376
383
  padding-inline: 6px;
@@ -378,14 +385,11 @@
378
385
 
379
386
  @include m.e(preview) {
380
387
  flex: none;
381
- margin-top: 2px;
382
388
  }
383
389
 
384
- @include m.e(actions) {
390
+ @include m.e(remove) {
385
391
  flex: none;
386
- display: flex;
387
- flex-direction: column;
388
- gap: 2px;
392
+ margin-top: 2px;
389
393
  }
390
394
  }
391
395
 
@@ -8,13 +8,16 @@ import {
8
8
  applyReportPreset,
9
9
  createReportBinding,
10
10
  formatCellAddress,
11
- inferColParentCandidate,
12
11
  inferDropPreset,
13
- inferRowParentCandidate,
12
+ inferParentsForPreset,
13
+ isColGroupBinding,
14
14
  isHorizontalExpandBinding,
15
+ isRowGroupBinding,
15
16
  isVerticalExpandBinding,
16
17
  listColParentCandidates,
17
- listRowParentCandidates
18
+ listRowParentCandidates,
19
+ prefersGroupParent,
20
+ presetStillApplies
18
21
  } from '../../report/binding'
19
22
  import type { DataConnection, QueryResult, Result } from '../../report/connector'
20
23
  import { buildParamDefs } from '../../report/params'
@@ -227,10 +230,9 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
227
230
  let binding = createReportBinding(dataset, fieldName)
228
231
  binding = applyReportPreset(binding, preset)
229
232
 
230
- const rowParent = inferRowParentCandidate(target, getBindingAt)
231
- if (rowParent) binding.rowParent = rowParent
232
- const colParent = inferColParentCandidate(target, getBindingAt)
233
- if (colParent) binding.colParent = colParent
233
+ const parents = inferParentsForPreset(target, preset, getBindingAt)
234
+ if (parents.rowParent) binding.rowParent = parents.rowParent
235
+ if (parents.colParent) binding.colParent = parents.colParent
234
236
 
235
237
  workbook.value.activeSheet.setCellMeta(target, REPORT_META_NAMESPACE, binding)
236
238
  }
@@ -276,14 +278,18 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
276
278
  metaTick.value
277
279
  const binding = activeBinding.value
278
280
  if (!binding) return []
279
- return listRowParentCandidates(binding, bindingEntries.value)
281
+ return listRowParentCandidates(binding, bindingEntries.value, {
282
+ preferGroup: prefersGroupParent(binding)
283
+ })
280
284
  })
281
285
 
282
286
  const colParentCandidates = computed((): CellAddress[] => {
283
287
  metaTick.value
284
288
  const binding = activeBinding.value
285
289
  if (!binding) return []
286
- return listColParentCandidates(binding, bindingEntries.value)
290
+ return listColParentCandidates(binding, bindingEntries.value, {
291
+ preferGroup: prefersGroupParent(binding)
292
+ })
287
293
  })
288
294
 
289
295
  const bindingEntries = computed((): TopologyBindingEntry[] => {
@@ -296,6 +302,12 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
296
302
  return entries
297
303
  })
298
304
 
305
+ function parentNeedsCorrection(addr: CellAddress | undefined): boolean {
306
+ if (!addr) return true
307
+ const parent = getBindingAt(addr)
308
+ return !parent || parent.aggregate === 'list'
309
+ }
310
+
299
311
  function patchActiveBinding(patch: Partial<ReportBinding>): void {
300
312
  const cell = activeCell.value
301
313
  const binding = activeBinding.value
@@ -304,7 +316,25 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
304
316
  const next: ReportBinding = { ...binding, ...patch }
305
317
  if ('rowParent' in patch && patch.rowParent === undefined) delete next.rowParent
306
318
  if ('colParent' in patch && patch.colParent === undefined) delete next.colParent
307
- if (('aggregate' in patch || 'expand' in patch) && !('preset' in patch)) delete next.preset
319
+ if (
320
+ ('aggregate' in patch || 'expand' in patch) &&
321
+ !('preset' in patch) &&
322
+ !presetStillApplies(next)
323
+ ) {
324
+ delete next.preset
325
+ }
326
+
327
+ if (patch.preset === 'subtotal') {
328
+ const inferred = inferParentsForPreset(cell, 'subtotal', getBindingAt)
329
+ if (parentNeedsCorrection(next.rowParent)) {
330
+ if (inferred.rowParent) next.rowParent = inferred.rowParent
331
+ else delete next.rowParent
332
+ }
333
+ if (parentNeedsCorrection(next.colParent)) {
334
+ if (inferred.colParent) next.colParent = inferred.colParent
335
+ else delete next.colParent
336
+ }
337
+ }
308
338
 
309
339
  workbook.value.activeSheet.setCellMeta(cell, REPORT_META_NAMESPACE, next)
310
340
  }
@@ -319,17 +349,36 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
319
349
  parentPick.value = null
320
350
  }
321
351
 
352
+ function isValidParentTarget(
353
+ mode: ParentPickMode,
354
+ source: ReportBinding,
355
+ target: ReportBinding | undefined
356
+ ): boolean {
357
+ if (!target) return false
358
+ if (target.dataset !== source.dataset) return false
359
+ const groupOnly = prefersGroupParent(source)
360
+ if (mode === 'row') {
361
+ return groupOnly ? isRowGroupBinding(target) : isVerticalExpandBinding(target)
362
+ }
363
+ return groupOnly ? isColGroupBinding(target) : isHorizontalExpandBinding(target)
364
+ }
365
+
322
366
  function pickParentAt(target: CellAddress): void {
323
367
  const pick = parentPick.value
324
368
  if (!pick) return
325
369
  if (target.row === pick.source.row && target.col === pick.source.col) return
326
370
 
327
371
  const sourceBinding = getBindingAt(pick.source)
372
+ if (!sourceBinding) {
373
+ parentPick.value = null
374
+ return
375
+ }
376
+
328
377
  const targetBinding = getBindingAt(target)
329
- if (!sourceBinding || !targetBinding) return
330
- if (targetBinding.dataset !== sourceBinding.dataset) return
331
- if (pick.mode === 'row' && !isVerticalExpandBinding(targetBinding)) return
332
- if (pick.mode === 'col' && !isHorizontalExpandBinding(targetBinding)) return
378
+ if (!isValidParentTarget(pick.mode, sourceBinding, targetBinding)) {
379
+ workbook.value.activeSheet.selectCell(pick.source)
380
+ return
381
+ }
333
382
 
334
383
  const patch = pick.mode === 'row' ? { rowParent: { ...target } } : { colParent: { ...target } }
335
384
  const next: ReportBinding = { ...sourceBinding, ...patch }
@@ -2,7 +2,8 @@ import type { SheetSnapshot } from '@veltra/sheet-core'
2
2
  import { computed, ref, shallowRef, watch, type ComputedRef, type Ref, type ShallowRef } from 'vue'
3
3
 
4
4
  import type { ConnectorError } from '../../report/connector'
5
- import { renderReport } from '../../report/render'
5
+ import type { ReportColWidthEntry } from '../../report/export-xlsx'
6
+ import { renderReport, resolveFilledColWidths } from '../../report/render'
6
7
  import {
7
8
  fetchTemplateRecords,
8
9
  resolveParamDefaults,
@@ -22,6 +23,8 @@ export interface UseReportViewerReturn {
22
23
  error: ShallowRef<ConnectorError | null>
23
24
  /** 最近一次成功展开渲染的 Filled Report 快照(组件壳据此替换网格内容) */
24
25
  filledSnapshot: ShallowRef<SheetSnapshot | null>
26
+ /** 展开后物理列宽(由模板 colWidths 映射;无模板列宽时为 undefined) */
27
+ filledColWidths: ShallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>
25
28
  /** 重新取数并展开渲染 */
26
29
  refresh: () => Promise<void>
27
30
  /** Filter Bar 改值:写入新参数值并重新取数 */
@@ -36,6 +39,7 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
36
39
  const loading = ref(false)
37
40
  const error = shallowRef<ConnectorError | null>(null)
38
41
  const filledSnapshot = shallowRef<SheetSnapshot | null>(null)
42
+ const filledColWidths = shallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>(undefined)
39
43
 
40
44
  const params = computed(() => resolveTemplateParams(props.template))
41
45
  // 初始值由下方 immediate watch 播种(参数默认值)
@@ -54,9 +58,13 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
54
58
  loading.value = false
55
59
  if (!result.ok) {
56
60
  error.value = result.error
61
+ filledColWidths.value = undefined
57
62
  return
58
63
  }
59
64
  filledSnapshot.value = renderReport(template, result.data)
65
+ filledColWidths.value = template.colWidths?.length
66
+ ? resolveFilledColWidths(template, result.data, template.colWidths)
67
+ : undefined
60
68
  }
61
69
 
62
70
  function setValues(next: ParamValues): void {
@@ -69,11 +77,12 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
69
77
  () => props.template,
70
78
  () => {
71
79
  filledSnapshot.value = null
80
+ filledColWidths.value = undefined
72
81
  values.value = resolveParamDefaults(params.value)
73
82
  void refresh()
74
83
  },
75
84
  { immediate: true }
76
85
  )
77
86
 
78
- return { params, values, loading, error, filledSnapshot, refresh, setValues }
87
+ return { params, values, loading, error, filledSnapshot, filledColWidths, refresh, setValues }
79
88
  }
@@ -258,6 +258,43 @@ describe('USheet 组件', () => {
258
258
  expect(gridAfter).not.toBe(gridBefore)
259
259
  })
260
260
 
261
+ it('showRowHeader/showColHeader 为 false 时隐藏行号与列头', async () => {
262
+ const workbook = createWorkbook()
263
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
264
+ mount(
265
+ () => ({ workbook, rows: 8, cols: 4, showRowHeader: false, showColHeader: false }),
266
+ exposed
267
+ )
268
+ await nextTick()
269
+
270
+ workbook.activeSheet.setCellValue({ row: 0, col: 0 }, 'A1')
271
+ await nextTick()
272
+ const table = exposed.value!.getGrid()!.getTable()
273
+ expect(table.options.showHeader).toBe(false)
274
+ expect(table.options.rowSeriesNumber).toBeUndefined()
275
+ expect(table.columnHeaderLevelCount).toBe(0)
276
+ expect(table.rowCount).toBe(8)
277
+ expect(table.getCellValue(0, 0)).toBe('A1')
278
+ })
279
+
280
+ it('删除行后渲染行数跟随模型收缩(不被 props.rows 撑回)', async () => {
281
+ const workbook = createWorkbook()
282
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
283
+ mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
284
+ await nextTick()
285
+
286
+ const sheet = exposed.value!.getActiveSheet()
287
+ expect(sheet.rows).toBe(10)
288
+ expect(exposed.value!.getGrid()!.getTable().rowCount).toBe(11) // 列头 + 10
289
+
290
+ sheet.deleteRows(0, 2)
291
+ await nextTick()
292
+ await nextTick()
293
+
294
+ expect(sheet.rows).toBe(8)
295
+ expect(exposed.value!.getGrid()!.getTable().rowCount).toBe(9) // 列头 + 8
296
+ })
297
+
261
298
  it('resolveCellStyle prop 传入 SheetGrid 视口渲染', async () => {
262
299
  const workbook = createWorkbook()
263
300
  const sheet = workbook.activeSheet
@@ -36,8 +36,9 @@ describe('sheet-context-menu helpers', () => {
36
36
  expect(isColInSelection(ranges, 6)).toBe(false)
37
37
  })
38
38
 
39
- it('resolveRenderSize = max(props, sheet)', () => {
40
- expect(resolveRenderSize(100, 26, 120, 10)).toEqual({ rows: 120, cols: 26 })
39
+ it('resolveRenderSize:模型已声明时以模型为准(可低于 props)', () => {
40
+ expect(resolveRenderSize(100, 26, 120, 10)).toEqual({ rows: 120, cols: 10 })
41
+ expect(resolveRenderSize(100, 26, 29, 26)).toEqual({ rows: 29, cols: 26 })
41
42
  expect(resolveRenderSize(undefined, undefined, 0, 0)).toEqual({ rows: 100, cols: 26 })
42
43
  })
43
44
  })
@@ -40,7 +40,8 @@ import { buildBorderPresetItems } from '@veltra/sheet-core/core/style/border-pre
40
40
  import {
41
41
  BORDER_STYLE_WIDTH,
42
42
  type BorderEdge,
43
- type BorderLineStyle
43
+ type BorderLineStyle,
44
+ type CellStylePatch
44
45
  } from '@veltra/sheet-core/core/style/types'
45
46
  import { bem } from '@veltra/utils'
46
47
  import { ref } from 'vue'
@@ -63,6 +64,7 @@ defineOptions({ name: 'USheetBorderPopup' })
63
64
  * 预设补丁由 core/style/border-presets 生成(含邻居格共享边同步,对齐
64
65
  * Excel「写入时同步、最近设置生效」);面板打开期间的写入由
65
66
  * use-tool-popup 事务包裹为一个 undo 单元。
67
+ * 整行/整列选区下,均匀预设(all/none/单侧)走 applyStyle → 行列默认样式。
66
68
  */
67
69
  const props = defineProps<{
68
70
  /** 工具上下文(写入走命令系统,天然可撤销) */
@@ -75,7 +77,27 @@ const cls = bem('sheet')
75
77
  const borderLineStyle = ref<BorderLineStyle>('thin')
76
78
  const borderColor = ref('#000000')
77
79
 
78
- /** 应用边框预设:一次 executeCommand(items 批量,含邻居同步补丁)= 一个 undo 单元 */
80
+ /** 均匀边框补丁(可写进行/列默认样式);outer/inner 几何相关 → null */
81
+ function uniformBorderPatch(preset: BorderPresetId, edge: BorderEdge): CellStylePatch | null {
82
+ switch (preset) {
83
+ case 'all':
84
+ return { border: { top: edge, right: edge, bottom: edge, left: edge } }
85
+ case 'none':
86
+ return { border: { top: null, right: null, bottom: null, left: null } }
87
+ case 'top':
88
+ return { border: { top: edge } }
89
+ case 'bottom':
90
+ return { border: { bottom: edge } }
91
+ case 'left':
92
+ return { border: { left: edge } }
93
+ case 'right':
94
+ return { border: { right: edge } }
95
+ default:
96
+ return null
97
+ }
98
+ }
99
+
100
+ /** 应用边框预设:整行/整列均匀预设走 applyStyle;其余逐格批量(含邻居同步) */
79
101
  function applyBorderPreset(preset: BorderPresetId): void {
80
102
  const range = currentRange(props.context)
81
103
  if (!range) return
@@ -84,8 +106,14 @@ function applyBorderPreset(preset: BorderPresetId): void {
84
106
  width: BORDER_STYLE_WIDTH[borderLineStyle.value],
85
107
  color: borderColor.value
86
108
  }
109
+ const target = props.context.resolveStyleTarget(range)
110
+ const uniform = target !== 'cell' ? uniformBorderPatch(preset, edge) : null
111
+ if (uniform) {
112
+ props.context.applyStyle(range, uniform)
113
+ return
114
+ }
87
115
  const items: SetCellStyleItem[] = buildBorderPresetItems(range, preset, edge, (addr) =>
88
- props.context.getCellStyle(addr)
116
+ props.context.getEffectiveStyle(addr)
89
117
  ).map(({ addr, patch }) => ({ addr, partial: patch }))
90
118
  if (items.length === 0) return
91
119
  props.context.executeCommand('sheet.command.set-cell-style', { items })
@@ -26,7 +26,7 @@ const fillColor = ref('')
26
26
  // 打开时初始化为活动格当前填充色
27
27
  onMounted(() => {
28
28
  const active = props.context.getSelection().activeCell
29
- const style = active ? props.context.getCellStyle(active) : undefined
29
+ const style = active ? props.context.getEffectiveStyle(active) : undefined
30
30
  fillColor.value = style?.fill?.color ?? ''
31
31
  })
32
32
 
@@ -34,6 +34,6 @@ onMounted(() => {
34
34
  function applyFillColor(color: string): void {
35
35
  const range = currentRange(props.context)
36
36
  if (!range) return
37
- props.context.setCellStyle(range, color ? { fill: { color } } : { fill: {} })
37
+ props.context.applyStyle(range, color ? { fill: { color } } : { fill: {} })
38
38
  }
39
39
  </script>
@@ -26,7 +26,7 @@ const fontColor = ref('')
26
26
  // 打开时初始化为活动格当前字体色
27
27
  onMounted(() => {
28
28
  const active = props.context.getSelection().activeCell
29
- const style = active ? props.context.getCellStyle(active) : undefined
29
+ const style = active ? props.context.getEffectiveStyle(active) : undefined
30
30
  fontColor.value = style?.font?.color ?? ''
31
31
  })
32
32
 
@@ -34,6 +34,6 @@ onMounted(() => {
34
34
  function applyFontColor(color: string): void {
35
35
  const range = currentRange(props.context)
36
36
  if (!range) return
37
- props.context.setCellStyle(range, color ? { font: { color } } : { font: { color: null } })
37
+ props.context.applyStyle(range, color ? { font: { color } } : { font: { color: null } })
38
38
  }
39
39
  </script>
@@ -39,7 +39,7 @@ const currentSize = ref<number | undefined>(undefined)
39
39
 
40
40
  onMounted(() => {
41
41
  const active = props.context.getSelection().activeCell
42
- const style = active ? props.context.getCellStyle(active) : undefined
42
+ const style = active ? props.context.getEffectiveStyle(active) : undefined
43
43
  currentSize.value = style?.font?.size
44
44
  })
45
45
 
@@ -47,6 +47,6 @@ function applyFontSize(size: number): void {
47
47
  const range = currentRange(props.context)
48
48
  if (!range) return
49
49
  currentSize.value = size
50
- props.context.setCellStyle(range, { font: { size } })
50
+ props.context.applyStyle(range, { font: { size } })
51
51
  }
52
52
  </script>
@@ -39,14 +39,20 @@ export function wholeColRange(col: number, rowCount: number): CellRange {
39
39
  return createRange({ row: 0, col }, { row: Math.max(rowCount - 1, 0), col })
40
40
  }
41
41
 
42
- /** 渲染尺寸 = max(props, sheet.rows/cols),与 SheetGrid 一致 */
42
+ /**
43
+ * 渲染尺寸:模型已声明时以模型为准(删行可低于 props);未声明时回落 props。
44
+ * 与 SheetGrid / useSheetGrid 一致——避免 ensureTableSize(props) 把删行结果撑回。
45
+ */
43
46
  export function resolveRenderSize(
44
47
  propsRows: number | undefined,
45
48
  propsCols: number | undefined,
46
49
  sheetRows: number,
47
50
  sheetCols: number
48
51
  ): { rows: number; cols: number } {
49
- return { rows: Math.max(propsRows ?? 100, sheetRows), cols: Math.max(propsCols ?? 26, sheetCols) }
52
+ return {
53
+ rows: sheetRows > 0 ? sheetRows : (propsRows ?? 100),
54
+ cols: sheetCols > 0 ? sheetCols : (propsCols ?? 26)
55
+ }
50
56
  }
51
57
 
52
58
  function insertCountRender(options: {
@@ -103,6 +103,8 @@ const props = withDefaults(defineProps<SheetProps>(), {
103
103
  showToolbar: true,
104
104
  showFormulaBar: true,
105
105
  showTabs: true,
106
+ showRowHeader: true,
107
+ showColHeader: true,
106
108
  readonly: false
107
109
  })
108
110
 
@@ -109,12 +109,16 @@ export function useSheetGrid(options: UseSheetGridOptions) {
109
109
  const el = document.createElement('div')
110
110
  el.className = 'u-sheet__grid-instance'
111
111
  host.appendChild(el)
112
+ // 模型已声明尺寸时传入模型值,避免删行后被 props 经 ensureTableSize 撑回
113
+ const { rows, cols } = resolveRenderSize(props.rows, props.cols, sheet.rows, sheet.cols)
112
114
  const grid = new SheetGrid({
113
115
  container: el,
114
116
  sheet,
115
- rows: props.rows,
116
- cols: props.cols,
117
+ rows,
118
+ cols,
117
119
  readonly: props.readonly,
120
+ showRowHeader: props.showRowHeader,
121
+ showColHeader: props.showColHeader,
118
122
  resolveDisplayValue: props.resolveDisplayValue,
119
123
  resolveCellStyle: props.resolveCellStyle,
120
124
  resolveCellRenderer: props.resolveCellRenderer,
@@ -237,7 +241,17 @@ export function useSheetGrid(options: UseSheetGridOptions) {
237
241
 
238
242
  onMounted(activateGrid)
239
243
 
240
- watch(() => [props.rows, props.cols, props.readonly], rebuildGrid)
244
+ // props 尺寸显式增大时先扩张模型,再重建(删行缩小不走这条路径)
245
+ watch(
246
+ () =>
247
+ [props.rows, props.cols, props.readonly, props.showRowHeader, props.showColHeader] as const,
248
+ ([nextRows, nextCols]) => {
249
+ const sheet = getActiveSheet()
250
+ if (nextRows != null) sheet.ensureTableSize(nextRows, 0)
251
+ if (nextCols != null) sheet.ensureTableSize(0, nextCols)
252
+ rebuildGrid()
253
+ }
254
+ )
241
255
 
242
256
  onBeforeUnmount(() => {
243
257
  invalidateAll()
@@ -5,6 +5,7 @@ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vu
5
5
  import { createSheetContext } from '../../tools/context'
6
6
  import { defaultToolRegistry } from '../../tools/registry'
7
7
  import type { SheetProps } from '../../types'
8
+ import { resolveRenderSize } from './sheet-context-menu'
8
9
 
9
10
  /**
10
11
  * useSheetState 与宿主组件的协作钩子。
@@ -44,10 +45,14 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
44
45
  () => sheetList.value[activeIndex.value] ?? workbook.value.activeSheet
45
46
  )
46
47
 
47
- /** 工具上下文:动态解析活动 sheet 与当前工作簿,tab 切换 / workbook prop 切换后自动指向最新值 */
48
+ /** 工具上下文:动态解析活动 sheet / 工作簿 / 渲染尺寸,tab 切换后自动指向最新值 */
48
49
  const context = createSheetContext(
49
50
  () => activeSheet.value,
50
- () => workbook.value
51
+ () => workbook.value,
52
+ {
53
+ resolveGridSize: () =>
54
+ resolveRenderSize(props.rows, props.cols, activeSheet.value.rows, activeSheet.value.cols)
55
+ }
51
56
  )
52
57
 
53
58
  /** 工具栏状态版本号:visible/disabled/active 是 (ctx) => boolean 纯函数,状态源变化时 bump 触发重算 */
@@ -80,7 +85,7 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
80
85
  // 整表内容替换(导入 / undo/redo 回放):restore 静默不发 cell-change,
81
86
  // 状态源需 bump(grid 层自行订阅 content-reset 全量刷新,此处不重建)
82
87
  sheet.on('content-reset', scheduleBump),
83
- // 行列插入/删除 → 重建网格(渲染行列数 = max(props, sheet.rows/cols),
88
+ // 行列插入/删除 → 重建网格(渲染行列数跟随模型 sheet.rows/cols,
84
89
  // 数据/选区/冻结/行高随重建恢复;低频操作直接重建)
85
90
  sheet.on('structure-change', hooks.rebuildGrid)
86
91
  ]