@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
@@ -35,6 +35,7 @@ const PG: DataConnection = {
35
35
 
36
36
  const ORDER_FIELDS: DatasetField[] = [
37
37
  { name: 'customer', label: '客户', type: 'string' },
38
+ { name: 'orderNo', label: '订单号', type: 'string' },
38
39
  { name: 'amount', label: '金额', type: 'number' }
39
40
  ]
40
41
 
@@ -94,6 +95,18 @@ function createDesigner(
94
95
  return { scope, designer, connections, workbook, calls }
95
96
  }
96
97
 
98
+ function bindGroupHeader(
99
+ designer: UseReportDesignerReturn,
100
+ workbook: Workbook,
101
+ datasetId: string,
102
+ addr: { row: number; col: number },
103
+ field = 'customer'
104
+ ): void {
105
+ designer.bindField(datasetId, field, addr)
106
+ workbook.activeSheet.selectCell(addr)
107
+ designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
108
+ }
109
+
97
110
  describe('useReportDesigner:连接 / 数据集 CRUD 与连接器调用', () => {
98
111
  it('连接 CRUD 全走 v-model 代理;删除连接级联删除其数据集', () => {
99
112
  const { designer, connections } = createDesigner()
@@ -132,7 +145,7 @@ describe('useReportDesigner:连接 / 数据集 CRUD 与连接器调用', () =>
132
145
 
133
146
  const catalog = designer.catalog.value
134
147
  expect(catalog).toHaveLength(1)
135
- expect(catalog[0]!.fields.map((field) => field.label)).toEqual(['客户名称', '金额'])
148
+ expect(catalog[0]!.fields.map((field) => field.label)).toEqual(['客户名称', '订单号', '金额'])
136
149
  })
137
150
 
138
151
  it('describeDataset 业务错误整体透传且不写字段缓存', async () => {
@@ -220,7 +233,7 @@ describe('useReportDesigner:拖拽落格写 Cell Meta', () => {
220
233
  const inventory = await seedDataset(designer, 'c2')
221
234
 
222
235
  designer.bindField(orders.id, 'customer', { row: 1, col: 0 })
223
- designer.bindField(inventory.id, 'amount', { row: 2, col: 0 })
236
+ designer.bindField(inventory.id, 'orderNo', { row: 2, col: 0 })
224
237
 
225
238
  const detail = workbook.activeSheet.getCellMeta<ReportBinding>(
226
239
  { row: 2, col: 0 },
@@ -228,7 +241,7 @@ describe('useReportDesigner:拖拽落格写 Cell Meta', () => {
228
241
  )
229
242
  expect(detail).toMatchObject({
230
243
  dataset: inventory.id,
231
- field: 'amount',
244
+ field: 'orderNo',
232
245
  rowParent: { row: 1, col: 0 }
233
246
  })
234
247
  })
@@ -237,7 +250,7 @@ describe('useReportDesigner:拖拽落格写 Cell Meta', () => {
237
250
  const { designer, workbook } = createDesigner()
238
251
  const dataset = await seedDataset(designer)
239
252
 
240
- designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
253
+ bindGroupHeader(designer, workbook, dataset.id, { row: 1, col: 0 })
241
254
  designer.bindField(dataset.id, 'amount', { row: 2, col: 0 })
242
255
 
243
256
  const subtotal = workbook.activeSheet.getCellMeta<ReportBinding>(
@@ -252,6 +265,66 @@ describe('useReportDesigner:拖拽落格写 Cell Meta', () => {
252
265
  })
253
266
  })
254
267
 
268
+ it('同行明细落格把 rowParent 推到左侧分组头', async () => {
269
+ const { designer, workbook } = createDesigner()
270
+ const dataset = await seedDataset(designer)
271
+
272
+ bindGroupHeader(designer, workbook, dataset.id, { row: 1, col: 0 })
273
+ designer.bindField(dataset.id, 'orderNo', { row: 1, col: 2 })
274
+
275
+ const detail = workbook.activeSheet.getCellMeta<ReportBinding>(
276
+ { row: 1, col: 2 },
277
+ REPORT_META_NAMESPACE
278
+ )
279
+ expect(detail).toMatchObject({
280
+ field: 'orderNo',
281
+ preset: 'detail',
282
+ rowParent: { row: 1, col: 0 }
283
+ })
284
+ })
285
+
286
+ it('下方跨列小计落格推到行分组头而不是同列 list', async () => {
287
+ const { designer, workbook } = createDesigner()
288
+ const dataset = await seedDataset(designer)
289
+
290
+ bindGroupHeader(designer, workbook, dataset.id, { row: 1, col: 0 })
291
+ designer.bindField(dataset.id, 'orderNo', { row: 1, col: 2 })
292
+ designer.bindField(dataset.id, 'amount', { row: 2, col: 2 })
293
+
294
+ const subtotal = workbook.activeSheet.getCellMeta<ReportBinding>(
295
+ { row: 2, col: 2 },
296
+ REPORT_META_NAMESPACE
297
+ )
298
+ expect(subtotal).toMatchObject({
299
+ preset: 'subtotal',
300
+ aggregate: 'sum',
301
+ expand: 'none',
302
+ rowParent: { row: 1, col: 0 }
303
+ })
304
+ expect(subtotal?.colParent).toBeUndefined()
305
+ })
306
+
307
+ it('右侧列小计落格推到列分组头', async () => {
308
+ const { designer, workbook } = createDesigner()
309
+ const dataset = await seedDataset(designer)
310
+
311
+ designer.bindField(dataset.id, 'customer', { row: 0, col: 1 })
312
+ workbook.activeSheet.selectCell({ row: 0, col: 1 })
313
+ designer.patchActiveBinding({ preset: 'groupHeader', aggregate: 'group', expand: 'right' })
314
+ designer.bindField(dataset.id, 'amount', { row: 0, col: 2 })
315
+
316
+ const subtotal = workbook.activeSheet.getCellMeta<ReportBinding>(
317
+ { row: 0, col: 2 },
318
+ REPORT_META_NAMESPACE
319
+ )
320
+ expect(subtotal).toMatchObject({
321
+ preset: 'subtotal',
322
+ aggregate: 'sum',
323
+ expand: 'none',
324
+ colParent: { row: 0, col: 1 }
325
+ })
326
+ })
327
+
255
328
  it('未给落点地址时回退到当前选区', async () => {
256
329
  const { designer, workbook } = createDesigner()
257
330
  const dataset = await seedDataset(designer)
@@ -282,7 +355,7 @@ describe('useReportDesigner:角色徽章渲染 hook(ADR-0004 首个消费者
282
355
  expect(layout!.rootContainer.attribute.fill).toBeUndefined()
283
356
 
284
357
  const style = designer.resolveCellStyle({ row: 0, col: 0 }, undefined)
285
- expect(style?.fill).toEqual({ color: '#d1fae5' })
358
+ expect(style?.fill).toEqual({ color: '#effaf3' })
286
359
 
287
360
  // 模型与快照无 renderer 残留
288
361
  expect(workbook.activeSheet.getCellData({ row: 0, col: 0 })).toBeUndefined()
@@ -347,7 +420,7 @@ describe('useReportDesigner:Action Pill 就地编辑(角色 / 聚合 / 条
347
420
  return dataset
348
421
  }
349
422
 
350
- it('预设切换写入 presetBindingDefaults 组合值;聚合切换同步 expand 缺省', async () => {
423
+ it('预设切换写入 presetBindingDefaults 组合值;同族聚合切换保留预设', async () => {
351
424
  const { designer, workbook } = createDesigner()
352
425
  await seedAndBind(designer, { row: 0, col: 0 })
353
426
  workbook.activeSheet.selectCell({ row: 0, col: 0 })
@@ -358,8 +431,17 @@ describe('useReportDesigner:Action Pill 就地编辑(角色 / 聚合 / 条
358
431
 
359
432
  designer.patchActiveBinding({ aggregate: 'avg', expand: 'none' })
360
433
  binding = designer.activeBinding.value
361
- expect(binding).toMatchObject({ aggregate: 'avg', expand: 'none' })
362
- expect(binding?.preset).toBeUndefined()
434
+ expect(binding).toMatchObject({ preset: 'subtotal', aggregate: 'avg', expand: 'none' })
435
+ })
436
+
437
+ it('聚合/展开偏离预设族时清除 preset', async () => {
438
+ const { designer, workbook } = createDesigner()
439
+ await seedAndBind(designer, { row: 0, col: 0 })
440
+ workbook.activeSheet.selectCell({ row: 0, col: 0 })
441
+
442
+ designer.patchActiveBinding(roleBindingDefaults('subtotal'))
443
+ designer.patchActiveBinding({ aggregate: 'list', expand: 'down' })
444
+ expect(designer.activeBinding.value?.preset).toBeUndefined()
363
445
  })
364
446
 
365
447
  it('条件规则与排序经 patch 写入绑定;拓扑条目跟随 meta 变更', async () => {
@@ -393,6 +475,8 @@ describe('useReportDesigner:Action Pill 就地编辑(角色 / 聚合 / 条
393
475
  designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
394
476
  designer.bindField(dataset.id, 'amount', { row: 2, col: 1 })
395
477
 
478
+ workbook.activeSheet.selectCell({ row: 1, col: 0 })
479
+ designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
396
480
  workbook.activeSheet.selectCell({ row: 2, col: 1 })
397
481
  designer.startParentPick('row')
398
482
  designer.pickParentAt({ row: 1, col: 0 })
@@ -405,14 +489,62 @@ describe('useReportDesigner:Action Pill 就地编辑(角色 / 聚合 / 条
405
489
  expect(designer.parentPick.value).toBeNull()
406
490
  })
407
491
 
492
+ it('切换小计行时纠正指向 list 的父格', async () => {
493
+ const { designer, workbook } = createDesigner()
494
+ const dataset = designer.addDataset('c1')
495
+ designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
496
+ await designer.describeDataset(dataset.id)
497
+
498
+ designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
499
+ workbook.activeSheet.selectCell({ row: 1, col: 0 })
500
+ designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
501
+ designer.bindField(dataset.id, 'orderNo', { row: 1, col: 2 })
502
+ designer.bindField(dataset.id, 'amount', { row: 2, col: 2 })
503
+
504
+ workbook.activeSheet.selectCell({ row: 2, col: 2 })
505
+ designer.patchActiveBinding({ rowParent: { row: 1, col: 2 } })
506
+ expect(designer.activeBinding.value?.rowParent).toEqual({ row: 1, col: 2 })
507
+
508
+ designer.patchActiveBinding(roleBindingDefaults('subtotal'))
509
+ expect(designer.activeBinding.value).toMatchObject({
510
+ preset: 'subtotal',
511
+ rowParent: { row: 1, col: 0 }
512
+ })
513
+ })
514
+
515
+ it('非法点选父格时选区弹回源格且保持拾取态', async () => {
516
+ const { designer, workbook } = createDesigner()
517
+ const dataset = designer.addDataset('c1')
518
+ designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
519
+ await designer.describeDataset(dataset.id)
520
+
521
+ designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
522
+ workbook.activeSheet.selectCell({ row: 1, col: 0 })
523
+ designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
524
+ designer.bindField(dataset.id, 'amount', { row: 2, col: 2 })
525
+
526
+ workbook.activeSheet.selectCell({ row: 2, col: 2 })
527
+ designer.startParentPick('row')
528
+ designer.pickParentAt({ row: 0, col: 3 })
529
+
530
+ expect(designer.parentPick.value).toEqual({ mode: 'row', source: { row: 2, col: 2 } })
531
+ expect(workbook.activeSheet.getSelection().activeCell).toEqual({ row: 2, col: 2 })
532
+ expect(
533
+ workbook.activeSheet.getCellMeta<ReportBinding>({ row: 2, col: 2 }, REPORT_META_NAMESPACE)
534
+ ?.rowParent
535
+ ).toEqual({ row: 1, col: 0 })
536
+ })
537
+
408
538
  it('清除绑定移除 Cell Meta;行 / 列方向父格标签读取绑定存储值', async () => {
409
539
  const { designer, workbook } = createDesigner()
410
540
  const dataset = designer.addDataset('c1')
411
541
  designer.updateDataset(dataset.id, { sql: 'SELECT customer, amount FROM orders' })
412
542
  await designer.describeDataset(dataset.id)
413
543
 
414
- // 同列上方纵向扩展绑定 + 下方明细(rowParent 推断到上方格)
544
+ // 同列上方分组头 + 下方小计(rowParent 推断到分组头)
415
545
  designer.bindField(dataset.id, 'customer', { row: 1, col: 0 })
546
+ workbook.activeSheet.selectCell({ row: 1, col: 0 })
547
+ designer.patchActiveBinding(roleBindingDefaults('groupHeader'))
416
548
  designer.bindField(dataset.id, 'amount', { row: 2, col: 0 })
417
549
 
418
550
  workbook.activeSheet.selectCell({ row: 2, col: 0 })
@@ -542,7 +674,7 @@ describe('useReportDesigner:template prop 载入既有模板', () => {
542
674
 
543
675
  // catalog 应用 fieldOverrides(字段面板与徽章 label 数据源)
544
676
  const catalog = designer.catalog.value
545
- expect(catalog[0]!.fields.map((field) => field.label)).toEqual(['客户名称', '金额'])
677
+ expect(catalog[0]!.fields.map((field) => field.label)).toEqual(['客户名称', '订单号', '金额'])
546
678
  })
547
679
 
548
680
  it('载入后 getTemplate 往返保持绑定与数据集定义;宿主已有同 id 连接时不覆盖', async () => {
@@ -5,24 +5,35 @@ import {
5
5
  type ResolveCellStyleHook
6
6
  } from '@veltra/sheet-core'
7
7
 
8
- import { formatBindingPlaceholder, inferReportPreset } from '../../report/binding'
8
+ import { formatBindingPlaceholderParts, inferReportPreset } from '../../report/binding'
9
9
  import type { ReportBinding, ReportPreset } from '../../report/types'
10
10
 
11
- /** 绑定徽章预设配色(设计态富渲染徽章;canvas 绘制需具体色值,无法消费 CSS 变量) */
12
- export const REPORT_PRESET_BADGE_COLORS: Record<ReportPreset, { bg: string; fg: string }> = {
13
- groupHeader: { bg: '#dbeafe', fg: '#1d4ed8' },
14
- detail: { bg: '#d1fae5', fg: '#047857' },
15
- subtotal: { bg: '#fef3c7', fg: '#b45309' },
16
- grandTotal: { bg: '#ffe4e6', fg: '#be123c' },
17
- cross: { bg: '#ede9fe', fg: '#6d28d9' }
11
+ /**
12
+ * 绑定徽章预设配色(设计态富渲染徽章;canvas 绘制需具体色值,无法消费 CSS 变量)。
13
+ * `cell` 为整格极浅底色,`fg` 为强调色(聚合标签徽章底色)。
14
+ */
15
+ export interface ReportBadgeColors {
16
+ cell: string
17
+ fg: string
18
+ }
19
+
20
+ export const REPORT_PRESET_BADGE_COLORS: Record<ReportPreset, ReportBadgeColors> = {
21
+ groupHeader: { cell: '#eef4ff', fg: '#2f54eb' },
22
+ detail: { cell: '#effaf3', fg: '#1f9254' },
23
+ subtotal: { cell: '#fff7e6', fg: '#d46b08' },
24
+ grandTotal: { cell: '#fff1f0', fg: '#cf1322' },
25
+ cross: { cell: '#f8f5ff', fg: '#5f3dc4' }
18
26
  }
19
27
 
20
- const CUSTOM_PRESET_BADGE_COLOR = { bg: '#f3f4f6', fg: '#374151' }
28
+ const CUSTOM_PRESET_BADGE_COLOR: ReportBadgeColors = { cell: '#f7f8fa', fg: '#495057' }
29
+
30
+ /** 徽章字段标签文字色(中性深灰,与强调色形成层次) */
31
+ const BADGE_LABEL_COLOR = '#343a40'
21
32
 
22
33
  /** @deprecated 05 将更名 */
23
34
  export const REPORT_ROLE_BADGE_COLORS = REPORT_PRESET_BADGE_COLORS
24
35
 
25
- function resolveBadgeColor(binding: ReportBinding): { bg: string; fg: string } {
36
+ function resolveBadgeColor(binding: ReportBinding): ReportBadgeColors {
26
37
  const preset = inferReportPreset(binding)
27
38
  return preset ? REPORT_PRESET_BADGE_COLORS[preset] : CUSTOM_PRESET_BADGE_COLOR
28
39
  }
@@ -35,14 +46,14 @@ export function createBindingBadgeStyleResolver(
35
46
  const binding = getBindingAt(addr)
36
47
  if (!binding) return base
37
48
 
38
- const { bg } = resolveBadgeColor(binding)
39
- return { ...base, fill: { color: bg } }
49
+ const { cell } = resolveBadgeColor(binding)
50
+ return { ...base, fill: { color: cell } }
40
51
  }
41
52
  }
42
53
 
43
54
  /**
44
55
  * 绑定格富渲染徽章(ADR-0004 `resolveCellRenderer` 首个消费者):
45
- * 绑定单元格由纯文本占位符升级为带预设色彩的徽章(底色 + 占位文案),
56
+ * 绑定单元格渲染为左对齐徽章(强调色聚合标签 + 中性色字段标签),
46
57
  * 未绑定格返回 `undefined` 回落默认渲染。
47
58
  */
48
59
  export function createBindingBadgeRenderer(
@@ -54,19 +65,27 @@ export function createBindingBadgeRenderer(
54
65
  if (!binding) return undefined
55
66
 
56
67
  const { fg } = resolveBadgeColor(binding)
68
+ const { tag, label } = formatBindingPlaceholderParts(binding, resolveLabel)
69
+
57
70
  const rootContainer = new CustomLayout.Container({
58
71
  display: 'flex',
59
72
  flexDirection: 'row',
60
- justifyContent: 'center',
73
+ justifyContent: 'flex-start',
61
74
  alignItems: 'center'
62
75
  })
76
+ // VTable 布局层 Tag 组件(vrender-components)与其 INode 声明存在结构性类型偏差,
77
+ // 运行时完全兼容(VTable 官方 customLayout 徽章用法即如此),此处仅收敛类型
78
+ const tagEl = new CustomLayout.Tag({
79
+ text: tag,
80
+ textStyle: { fontSize: 10, fontWeight: 600, fill: '#ffffff' },
81
+ panel: { visible: true, fill: fg, cornerRadius: 3 },
82
+ padding: [1, 4, 1, 4],
83
+ marginLeft: 6,
84
+ marginRight: 5
85
+ })
86
+ rootContainer.add(tagEl as unknown as Parameters<typeof rootContainer.add>[0])
63
87
  rootContainer.add(
64
- new CustomLayout.Text({
65
- text: formatBindingPlaceholder(binding, resolveLabel),
66
- fontSize: 11,
67
- fontWeight: 600,
68
- fill: fg
69
- })
88
+ new CustomLayout.Text({ text: label, fontSize: 11, fontWeight: 500, fill: BADGE_LABEL_COLOR })
70
89
  )
71
90
  return { rootContainer, renderDefault: false }
72
91
  }
@@ -1,27 +1,49 @@
1
+ import type { Sheet } from '@veltra/sheet-core'
2
+
1
3
  import type { ReportColWidthEntry } from '../../../report/export-xlsx'
2
- import { TABLE_ADDR_OFFSET } from './cell-coords'
3
4
 
4
- /** 列宽读写目标:SheetGrid.getTable() 的最小接口(headless 测试可桩)。
5
- * sheet-core 列宽未进 SheetSnapshot,只能经 VTable 运行时 set/getColWidth。 */
6
- export type ColWidthTarget = {
7
- getTable: () => {
8
- setColWidth: (col: number, width: number) => void
9
- getColWidth: (col: number) => number
10
- }
5
+ /** VTable 列宽读写所需的最小表接口(含行号列探测,隐藏行头时偏移为 0) */
6
+ type VTableColWidthApi = {
7
+ setColWidth: (col: number, width: number) => void
8
+ getColWidth: (col: number) => number
9
+ readonly colCount: number
10
+ readonly columnHeaderLevelCount: number
11
+ isSeriesNumber: (col: number, row: number) => boolean
12
+ }
13
+
14
+ /** 列宽读写目标:Sheet 模型或 SheetGrid.getTable() 的最小接口(headless 测试可桩) */
15
+ export type ColWidthTarget = { getTable: () => VTableColWidthApi }
16
+
17
+ /** 行号列数(隐藏行头时为 0;与 GridCoords 探测一致) */
18
+ function seriesNumberColCount(table: VTableColWidthApi): number {
19
+ const rowOffset = table.columnHeaderLevelCount
20
+ let col = 0
21
+ while (col < table.colCount && table.isSeriesNumber(col, rowOffset)) col++
22
+ return col
11
23
  }
12
24
 
13
- /** 经 SheetGrid → VTable 写入指定列宽 */
25
+ /** 将模型列宽写入 VTable(模型 → grid 同步辅助) */
14
26
  export function applyGridColWidths(
15
27
  grid: ColWidthTarget | undefined | null,
16
28
  widths: ReadonlyArray<ReportColWidthEntry>
17
29
  ): void {
18
30
  if (!grid) return
19
31
  const table = grid.getTable()
32
+ const colOffset = seriesNumberColCount(table)
20
33
  for (const [sheetCol, width] of widths) {
21
- table.setColWidth(sheetCol + TABLE_ADDR_OFFSET, width)
34
+ table.setColWidth(sheetCol + colOffset, width)
22
35
  }
23
36
  }
24
37
 
38
+ /** 把列宽条目写入 Sheet 模型(不进 undo,对称 setColWidth) */
39
+ export function applySheetColWidths(
40
+ sheet: Sheet | undefined | null,
41
+ widths: ReadonlyArray<ReportColWidthEntry>
42
+ ): void {
43
+ if (!sheet) return
44
+ for (const [col, width] of widths) sheet.setColWidth(col, width)
45
+ }
46
+
25
47
  /** 从当前网格读取指定模型列宽度;网格未就绪时返回 null */
26
48
  export function readGridColWidths(
27
49
  grid: ColWidthTarget | undefined | null,
@@ -29,5 +51,6 @@ export function readGridColWidths(
29
51
  ): Array<[number, number]> | null {
30
52
  if (!grid) return null
31
53
  const table = grid.getTable()
32
- return cols.map((sheetCol) => [sheetCol, table.getColWidth(sheetCol + TABLE_ADDR_OFFSET)])
54
+ const colOffset = seriesNumberColCount(table)
55
+ return cols.map((sheetCol) => [sheetCol, table.getColWidth(sheetCol + colOffset)])
33
56
  }
@@ -20,13 +20,16 @@ export const RULE_SCOPE_OPTIONS = [
20
20
  { value: 'row' as const, label: '整行' }
21
21
  ]
22
22
 
23
+ /** 求值字段下拉的「本格字段」哨兵值(USelect 无法匹配 undefined) */
24
+ export const SELF_FIELD_VALUE = ''
25
+
23
26
  /** 求值字段下拉:缺省项表示本格绑定字段 */
24
27
  export function ruleEvalFieldOptions(
25
28
  bindingField: string,
26
29
  datasetFields: readonly DatasetField[]
27
- ): Array<{ value: string | undefined; label: string }> {
30
+ ): Array<{ value: string; label: string }> {
28
31
  return [
29
- { value: undefined, label: '本格字段' },
32
+ { value: SELF_FIELD_VALUE, label: '本格字段' },
30
33
  ...datasetFields
31
34
  .filter((field) => field.name !== bindingField)
32
35
  .map((field) => ({ value: field.name, label: field.label || field.name }))
@@ -79,7 +82,7 @@ export function defaultRuleValue(
79
82
  }
80
83
 
81
84
  export function defaultRuleStyle(): ConditionalRule['style'] {
82
- return { fill: { color: '#FEE2E2' }, font: { color: '#DC2626' } }
85
+ return { fill: { color: '#FEF3C7' }, font: { color: '#B45309' } }
83
86
  }
84
87
 
85
88
  export function createDraftItem(
@@ -21,6 +21,7 @@
21
21
  @update:model-value="onPreset"
22
22
  />
23
23
  <u-select
24
+ v-if="showAggregateSelect"
24
25
  size="small"
25
26
  :class="[cls.e('select'), cls.em('select', 'aggregate')]"
26
27
  :model-value="binding.aggregate"
@@ -28,6 +29,7 @@
28
29
  @update:model-value="onAggregate"
29
30
  />
30
31
  <u-select
32
+ v-if="showExpandSelect"
31
33
  size="small"
32
34
  :class="[cls.e('select'), cls.em('select', 'expand')]"
33
35
  :model-value="binding.expand"
@@ -35,6 +37,7 @@
35
37
  @update:model-value="onExpand"
36
38
  />
37
39
  <u-button size="small" plain @click="emit('open-rules')">条件样式</u-button>
40
+ <u-button size="small" text type="danger" @click="emit('remove')">删除绑定</u-button>
38
41
  <u-button
39
42
  size="small"
40
43
  text
@@ -138,11 +141,8 @@
138
141
  </div>
139
142
  </section>
140
143
 
141
- <footer :class="cls.e('footer')">
142
- <u-button v-if="parentPickMode" size="small" plain @click="emit('cancel-parent-pick')">
143
- 取消点选
144
- </u-button>
145
- <u-button size="small" type="danger" plain @click="emit('remove')">删除绑定</u-button>
144
+ <footer v-if="parentPickMode" :class="cls.e('footer')">
145
+ <u-button size="small" plain @click="emit('cancel-parent-pick')">取消点选</u-button>
146
146
  </footer>
147
147
  </div>
148
148
  </div>
@@ -157,7 +157,9 @@ import { computed, nextTick, ref, toRef, watch } from 'vue'
157
157
  import {
158
158
  formatBindingPlaceholder,
159
159
  formatCellAddress,
160
- inferReportPreset
160
+ formatParentCandidateLabel,
161
+ inferReportPreset,
162
+ isReductionAggregate
161
163
  } from '../../../report/binding'
162
164
  import type {
163
165
  ReportAggregate,
@@ -188,6 +190,7 @@ const props = defineProps<{
188
190
  rowParentCandidates: CellAddress[]
189
191
  colParentCandidates: CellAddress[]
190
192
  resolveFieldLabel?: (datasetId: string, fieldName: string) => string
193
+ getBindingAt?: (addr: CellAddress) => ReportBinding | undefined
191
194
  hostEl: HTMLElement | null
192
195
  getGrid: () => SheetGrid | undefined
193
196
  }>()
@@ -216,7 +219,7 @@ const presetOptions = computed(() => {
216
219
  return options
217
220
  })
218
221
 
219
- const aggregateOptions = [
222
+ const ALL_AGGREGATE_OPTIONS = [
220
223
  { value: 'list' as const, label: '明细' },
221
224
  { value: 'group' as const, label: '分组' },
222
225
  { value: 'sum' as const, label: '求和' },
@@ -226,6 +229,10 @@ const aggregateOptions = [
226
229
  { value: 'min' as const, label: '最小' }
227
230
  ]
228
231
 
232
+ const REDUCTION_AGGREGATE_OPTIONS = ALL_AGGREGATE_OPTIONS.filter((item) =>
233
+ isReductionAggregate(item.value)
234
+ )
235
+
229
236
  const expandOptions = [
230
237
  { value: 'down' as const, label: '向下' },
231
238
  { value: 'right' as const, label: '向右' },
@@ -247,24 +254,59 @@ const currentPresetValue = computed(() => {
247
254
  return inferReportPreset(props.binding) ?? CUSTOM_PRESET_VALUE
248
255
  })
249
256
 
257
+ const showAggregateSelect = computed(() => {
258
+ const preset = currentPresetValue.value
259
+ return (
260
+ preset === CUSTOM_PRESET_VALUE ||
261
+ preset === 'subtotal' ||
262
+ preset === 'grandTotal' ||
263
+ preset === 'cross'
264
+ )
265
+ })
266
+
267
+ const showExpandSelect = computed(() => {
268
+ const preset = currentPresetValue.value
269
+ return preset === CUSTOM_PRESET_VALUE || preset === 'groupHeader' || preset === 'detail'
270
+ })
271
+
272
+ const aggregateOptions = computed(() =>
273
+ currentPresetValue.value === CUSTOM_PRESET_VALUE
274
+ ? ALL_AGGREGATE_OPTIONS
275
+ : REDUCTION_AGGREGATE_OPTIONS
276
+ )
277
+
250
278
  const summaryText = computed(() => {
251
279
  if (!props.binding) return ''
252
- return formatBindingPlaceholder(props.binding, props.resolveFieldLabel)
280
+ const parts = [formatBindingPlaceholder(props.binding, props.resolveFieldLabel)]
281
+ if (props.binding.rowParent && props.resolvedRowParentLabel !== '—') {
282
+ const parent = props.getBindingAt?.(props.binding.rowParent)
283
+ const name = parent
284
+ ? (props.resolveFieldLabel?.(parent.dataset, parent.field) ?? parent.field)
285
+ : props.resolvedRowParentLabel
286
+ parts.push(`行父格 ${name} ${props.resolvedRowParentLabel}`)
287
+ }
288
+ if (props.binding.colParent && props.resolvedColParentLabel !== '—') {
289
+ const parent = props.getBindingAt?.(props.binding.colParent)
290
+ const name = parent
291
+ ? (props.resolveFieldLabel?.(parent.dataset, parent.field) ?? parent.field)
292
+ : props.resolvedColParentLabel
293
+ parts.push(`列父格 ${name} ${props.resolvedColParentLabel}`)
294
+ }
295
+ return parts.join(' · ')
253
296
  })
254
297
 
255
- const rowParentOptions = computed(() =>
256
- props.rowParentCandidates.map((addr) => ({
257
- value: formatCellAddress(addr),
258
- label: formatCellAddress(addr)
259
- }))
260
- )
298
+ function candidateOption(addr: CellAddress): { value: string; label: string } {
299
+ const value = formatCellAddress(addr)
300
+ const binding = props.getBindingAt?.(addr)
301
+ return {
302
+ value,
303
+ label: binding ? formatParentCandidateLabel(addr, binding, props.resolveFieldLabel) : value
304
+ }
305
+ }
261
306
 
262
- const colParentOptions = computed(() =>
263
- props.colParentCandidates.map((addr) => ({
264
- value: formatCellAddress(addr),
265
- label: formatCellAddress(addr)
266
- }))
267
- )
307
+ const rowParentOptions = computed(() => props.rowParentCandidates.map(candidateOption))
308
+
309
+ const colParentOptions = computed(() => props.colParentCandidates.map(candidateOption))
268
310
 
269
311
  const rowParentSelectValue = computed(() =>
270
312
  props.binding?.rowParent ? formatCellAddress(props.binding.rowParent) : undefined
@@ -281,10 +323,18 @@ const panelStyle = computed(() => ({ left: `${panelLeft.value}px`, top: `${panel
281
323
  watch(
282
324
  () => [props.cell?.row, props.cell?.col] as const,
283
325
  () => {
326
+ if (props.parentPickMode) return
284
327
  expanded.value = false
285
328
  }
286
329
  )
287
330
 
331
+ watch(
332
+ () => props.parentPickMode,
333
+ (mode) => {
334
+ if (mode) expanded.value = true
335
+ }
336
+ )
337
+
288
338
  watch(expanded, () => {
289
339
  nextTick(() => updatePosition())
290
340
  })
@@ -300,13 +350,7 @@ function onPreset(preset: ReportPreset | typeof CUSTOM_PRESET_VALUE): void {
300
350
  }
301
351
 
302
352
  function aggregateDefaultExpand(aggregate: ReportAggregate): ReportExpand {
303
- return aggregate === 'sum' ||
304
- aggregate === 'avg' ||
305
- aggregate === 'count' ||
306
- aggregate === 'max' ||
307
- aggregate === 'min'
308
- ? 'none'
309
- : 'down'
353
+ return isReductionAggregate(aggregate) ? 'none' : 'down'
310
354
  }
311
355
 
312
356
  function onAggregate(aggregate: ReportAggregate): void {