@veltra/sheet 2.3.4 → 2.5.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 (129) hide show
  1. package/dist/index.d.ts +2 -17
  2. package/dist/index.js +2 -13
  3. package/dist/types/index.d.ts +1 -3
  4. package/package.json +5 -6
  5. package/src/index.ts +1 -27
  6. package/src/types/index.ts +0 -2
  7. package/dist/components/report/binding-badge.js +0 -112
  8. package/dist/components/report/designer/cell-coords.js +0 -46
  9. package/dist/components/report/designer/col-widths.js +0 -29
  10. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  11. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  12. package/dist/components/report/designer/float-panel.js +0 -422
  13. package/dist/components/report/designer/role.js +0 -17
  14. package/dist/components/report/designer/rule-preview.js +0 -33
  15. package/dist/components/report/designer/rule-row.js +0 -253
  16. package/dist/components/report/designer/rules-dialog.js +0 -118
  17. package/dist/components/report/designer/topology-overlay.js +0 -122
  18. package/dist/components/report/designer/topology.js +0 -64
  19. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  20. package/dist/components/report/designer-hub.js +0 -177
  21. package/dist/components/report/field-panel-helpers.js +0 -33
  22. package/dist/components/report/field-panel.js +0 -132
  23. package/dist/components/report/filter-bar.js +0 -74
  24. package/dist/components/report/hub-connection-form.js +0 -145
  25. package/dist/components/report/hub-dataset-editor.js +0 -409
  26. package/dist/components/report/preview-grid-size.js +0 -32
  27. package/dist/components/report/report-designer.js +0 -393
  28. package/dist/components/report/report-viewer.js +0 -124
  29. package/dist/components/report/style.d.ts +0 -17
  30. package/dist/components/report/style.js +0 -18
  31. package/dist/components/report/style2.css +0 -874
  32. package/dist/components/report/use-report-designer.js +0 -386
  33. package/dist/components/report/use-report-viewer.js +0 -55
  34. package/dist/report/binding.d.ts +0 -83
  35. package/dist/report/binding.js +0 -334
  36. package/dist/report/connector.d.ts +0 -66
  37. package/dist/report/connector.js +0 -159
  38. package/dist/report/export-xlsx.d.ts +0 -13
  39. package/dist/report/export-xlsx.js +0 -13
  40. package/dist/report/filter-bar.d.ts +0 -10
  41. package/dist/report/filter-bar.js +0 -31
  42. package/dist/report/index.d.ts +0 -12
  43. package/dist/report/params.d.ts +0 -13
  44. package/dist/report/params.js +0 -61
  45. package/dist/report/render/aggregate.js +0 -169
  46. package/dist/report/render/builder.d.ts +0 -12
  47. package/dist/report/render/builder.js +0 -139
  48. package/dist/report/render/coordinate.d.ts +0 -25
  49. package/dist/report/render/coordinate.js +0 -733
  50. package/dist/report/render/index.d.ts +0 -14
  51. package/dist/report/render/index.js +0 -21
  52. package/dist/report/render/style-resolver.js +0 -50
  53. package/dist/report/render/template-index.d.ts +0 -24
  54. package/dist/report/render/template-index.js +0 -137
  55. package/dist/report/rules/evaluate.d.ts +0 -25
  56. package/dist/report/rules/evaluate.js +0 -82
  57. package/dist/report/rules/index.d.ts +0 -2
  58. package/dist/report/template.d.ts +0 -63
  59. package/dist/report/template.js +0 -83
  60. package/dist/report/types.d.ts +0 -85
  61. package/dist/types/report-designer.d.ts +0 -38
  62. package/dist/types/report-viewer.d.ts +0 -38
  63. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  64. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  65. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  66. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  67. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  68. package/src/components/report/__test__/report-designer.test.ts +0 -464
  69. package/src/components/report/__test__/report-viewer.test.ts +0 -357
  70. package/src/components/report/__test__/topology.test.ts +0 -143
  71. package/src/components/report/__test__/use-report-designer.test.ts +0 -697
  72. package/src/components/report/binding-badge.ts +0 -117
  73. package/src/components/report/designer/cell-coords.ts +0 -95
  74. package/src/components/report/designer/col-widths.ts +0 -56
  75. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  76. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  77. package/src/components/report/designer/float-panel.vue +0 -432
  78. package/src/components/report/designer/role.ts +0 -32
  79. package/src/components/report/designer/rule-preview.vue +0 -40
  80. package/src/components/report/designer/rule-row.vue +0 -252
  81. package/src/components/report/designer/rules-dialog.vue +0 -110
  82. package/src/components/report/designer/topology-overlay.vue +0 -128
  83. package/src/components/report/designer/topology.ts +0 -124
  84. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  85. package/src/components/report/designer-hub.vue +0 -198
  86. package/src/components/report/field-panel-helpers.ts +0 -44
  87. package/src/components/report/field-panel.vue +0 -136
  88. package/src/components/report/filter-bar.vue +0 -77
  89. package/src/components/report/hub-connection-form.vue +0 -124
  90. package/src/components/report/hub-dataset-editor.vue +0 -387
  91. package/src/components/report/index.ts +0 -2
  92. package/src/components/report/preview-grid-size.ts +0 -33
  93. package/src/components/report/report-designer.vue +0 -421
  94. package/src/components/report/report-viewer.vue +0 -136
  95. package/src/components/report/style-designer.scss +0 -587
  96. package/src/components/report/style-hub.scss +0 -292
  97. package/src/components/report/style-viewer.scss +0 -104
  98. package/src/components/report/style.scss +0 -3
  99. package/src/components/report/style.ts +0 -20
  100. package/src/components/report/use-report-designer.ts +0 -602
  101. package/src/components/report/use-report-viewer.ts +0 -88
  102. package/src/report/__test__/binding.test.ts +0 -320
  103. package/src/report/__test__/connector.test.ts +0 -239
  104. package/src/report/__test__/export-xlsx.test.ts +0 -96
  105. package/src/report/__test__/filter-bar.test.ts +0 -58
  106. package/src/report/__test__/params.test.ts +0 -156
  107. package/src/report/__test__/render.test.ts +0 -856
  108. package/src/report/__test__/rules.test.ts +0 -131
  109. package/src/report/__test__/template.test.ts +0 -366
  110. package/src/report/binding.ts +0 -474
  111. package/src/report/connector.ts +0 -206
  112. package/src/report/export-xlsx.ts +0 -15
  113. package/src/report/filter-bar.ts +0 -36
  114. package/src/report/index.ts +0 -13
  115. package/src/report/params.ts +0 -96
  116. package/src/report/render/__test__/aggregate.test.ts +0 -16
  117. package/src/report/render/__test__/coordinate.test.ts +0 -405
  118. package/src/report/render/aggregate.ts +0 -261
  119. package/src/report/render/builder.ts +0 -211
  120. package/src/report/render/coordinate.ts +0 -1239
  121. package/src/report/render/index.ts +0 -30
  122. package/src/report/render/style-resolver.ts +0 -68
  123. package/src/report/render/template-index.ts +0 -200
  124. package/src/report/rules/evaluate.ts +0 -113
  125. package/src/report/rules/index.ts +0 -7
  126. package/src/report/template.ts +0 -145
  127. package/src/report/types.ts +0 -96
  128. package/src/types/report-designer.ts +0 -38
  129. package/src/types/report-viewer.ts +0 -37
@@ -1,36 +0,0 @@
1
- import type { ParamValues } from './types'
2
-
3
- // Filter Bar 控件值规范化(自 playground filter-bar-helpers 平移,行为不变)
4
-
5
- function toParamString(value: unknown): string {
6
- if (value == null || value === '') return ''
7
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
8
- return String(value)
9
- }
10
- return ''
11
- }
12
-
13
- /** 将运行时 date-range 参数值规范为 [from, to] 字符串元组 */
14
- export function parseDateRangeValue(raw: unknown): [string, string] {
15
- if (Array.isArray(raw)) {
16
- return [toParamString(raw[0]), toParamString(raw[1])]
17
- }
18
- if (raw && typeof raw === 'object') {
19
- const obj = raw as Record<string, unknown>
20
- return [toParamString(obj.from ?? obj.start), toParamString(obj.to ?? obj.end)]
21
- }
22
- return ['', '']
23
- }
24
-
25
- /** 将运行时 number 参数值规范为 UNumberInput 可接受的 number | undefined */
26
- export function resolveNumberParamValue(raw: unknown): number | undefined {
27
- if (raw === '' || raw == null) return undefined
28
- if (typeof raw === 'number' && Number.isFinite(raw)) return raw
29
- const parsed = Number(raw)
30
- return Number.isFinite(parsed) ? parsed : undefined
31
- }
32
-
33
- /** 合并单参数变更,保持其余参数值不变 */
34
- export function patchParamValues(values: ParamValues, id: string, value: unknown): ParamValues {
35
- return { ...values, [id]: value }
36
- }
@@ -1,13 +0,0 @@
1
- // 报表纯 TS 内核(ADR-0003 决策 1):渲染引擎 / 绑定 / 条件规则 / 查询参数 / 数据连接器 /
2
- // Report Template(自包含模板与取数)/ Filter Bar 值规范化。
3
- // 框架无关、无 DOM 依赖;组件壳(UReportDesigner / UReportViewer)落 components/report/。
4
-
5
- export * from './types'
6
- export * from './binding'
7
- export * from './rules'
8
- export * from './params'
9
- export * from './render'
10
- export * from './connector'
11
- export * from './template'
12
- export * from './filter-bar'
13
- export * from './export-xlsx'
@@ -1,96 +0,0 @@
1
- import type { QueryParamDef, QueryParamType } from './types'
2
-
3
- /** 从 SQL 提取 `${param}` 占位符 id(ADR-0002 决策 2:前端纯函数) */
4
- export function extractParamIds(sql: string): string[] {
5
- const ids: string[] = []
6
- const re = /\$\{([^}]+)\}/g
7
- let match: RegExpExecArray | null
8
- while ((match = re.exec(sql)) !== null) {
9
- const id = match[1]!.trim()
10
- if (id && !ids.includes(id)) ids.push(id)
11
- }
12
- return ids
13
- }
14
-
15
- function escapeRegExp(value: string): string {
16
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
17
- }
18
-
19
- /** 从 SQL 上下文推断参数控件类型(BETWEEN/LIKE/数值比较) */
20
- function inferParamTypeFromSql(sql: string, paramId: string): QueryParamType {
21
- const betweenSingle = new RegExp(
22
- `\\bBETWEEN\\s+\\$\\{${escapeRegExp(paramId)}\\}(?!\\s+AND)`,
23
- 'i'
24
- )
25
- if (betweenSingle.test(sql)) return 'date-range'
26
-
27
- const betweenPair = new RegExp(
28
- `\\bBETWEEN\\s+\\$\\{${escapeRegExp(paramId)}\\}\\s+AND\\s+\\$\\{([^}]+)\\}`,
29
- 'i'
30
- )
31
- const pairMatch = betweenPair.exec(sql)
32
- if (pairMatch && pairMatch[1]!.trim() !== paramId) return 'date'
33
-
34
- const like = new RegExp(`\\bLIKE\\s+\\$\\{${escapeRegExp(paramId)}\\}`, 'i')
35
- if (like.test(sql)) return 'text'
36
-
37
- const numericCompare = new RegExp(
38
- `[<>=]+\\s+\\$\\{${escapeRegExp(paramId)}\\}|\\$\\{${escapeRegExp(paramId)}\\}\\s*[<>=]+`,
39
- 'i'
40
- )
41
- if (numericCompare.test(sql)) return 'number'
42
-
43
- return 'text'
44
- }
45
-
46
- function defaultValueForType(type: QueryParamType): unknown {
47
- switch (type) {
48
- case 'number':
49
- return 0
50
- case 'date-range':
51
- return ['', '']
52
- case 'select':
53
- return ''
54
- default:
55
- return ''
56
- }
57
- }
58
-
59
- /**
60
- * 从 SQL 构建查询参数定义:提取 `${param}` 占位符 + 上下文类型推断,
61
- * 再叠加数据集编辑器的元数据覆盖(label / 类型 / 默认值 / 选项)。
62
- */
63
- export function buildParamDefs(
64
- sql: string,
65
- overrides?: Record<string, Partial<Omit<QueryParamDef, 'id'>>>
66
- ): QueryParamDef[] {
67
- const ids = extractParamIds(sql)
68
- return ids.map((id) => {
69
- const inferred = inferParamTypeFromSql(sql, id)
70
- const override = overrides?.[id]
71
- return {
72
- id,
73
- label: override?.label ?? id,
74
- type: override?.type ?? inferred,
75
- defaultValue: override?.defaultValue ?? defaultValueForType(override?.type ?? inferred),
76
- options: override?.options
77
- }
78
- })
79
- }
80
-
81
- /** 按绑定的数据集 id 合并查询参数(同名参数先见为准) */
82
- export function resolveBoundDatasetParams(
83
- queryParams: QueryParamDef[],
84
- boundDatasetIds: readonly string[],
85
- getDatasetParams: (datasetId: string) => QueryParamDef[]
86
- ): QueryParamDef[] {
87
- const map = new Map<string, QueryParamDef>()
88
- for (const datasetId of boundDatasetIds) {
89
- const params = getDatasetParams(datasetId)
90
- for (const param of params) {
91
- if (!map.has(param.id)) map.set(param.id, param)
92
- }
93
- }
94
- if (map.size > 0) return Array.from(map.values())
95
- return queryParams
96
- }
@@ -1,16 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { aggregateField } from '../aggregate'
4
-
5
- describe('aggregateField', () => {
6
- it('avg 空集返回 undefined,sum 空集仍为 0', () => {
7
- expect(aggregateField([], 'amount', 'avg')).toBeUndefined()
8
- expect(aggregateField([], 'amount', 'sum')).toBe(0)
9
- })
10
-
11
- it('max / min 对数值集求值', () => {
12
- const rows = [{ amount: 10 }, { amount: 30 }, { amount: 20 }]
13
- expect(aggregateField(rows, 'amount', 'max')).toBe(30)
14
- expect(aggregateField(rows, 'amount', 'min')).toBe(10)
15
- })
16
- })
@@ -1,405 +0,0 @@
1
- import { Sheet } from '@veltra/sheet-core'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { REPORT_META_NAMESPACE, createReportBinding } from '../../binding'
5
- import type { DatasetRecords, ReportBinding } from '../../types'
6
- import {
7
- computeExpansionLayout,
8
- enumerateExpansionInstances,
9
- physicalColSpan,
10
- physicalRowSpan,
11
- placementsAt
12
- } from '../coordinate'
13
- import { TemplateStructureError, buildTemplateIndex } from '../template-index'
14
-
15
- // ---- fixtures ----
16
-
17
- const ORDERS: DatasetRecords = {
18
- orders: [
19
- { customer: '甲', region: '华东', orderNo: 'O-1', amount: 100 },
20
- { customer: '甲', region: '华东', orderNo: 'O-2', amount: 200 },
21
- { customer: '乙', region: '华南', orderNo: 'O-3', amount: 300 },
22
- { customer: '丙', region: '华北', orderNo: 'O-4', amount: 400 }
23
- ]
24
- }
25
-
26
- const MATRIX: DatasetRecords = {
27
- sales: [
28
- { region: '华东', category: 'A', amount: 10 },
29
- { region: '华东', category: 'B', amount: 20 },
30
- { region: '华南', category: 'A', amount: 30 },
31
- { region: '华南', category: 'B', amount: 40 }
32
- ]
33
- }
34
-
35
- const MULTI_DS: DatasetRecords = { orders: ORDERS.orders!, customers: [{ id: 'C1' }, { id: 'C2' }] }
36
-
37
- function bind(sheet: Sheet, addr: { row: number; col: number }, binding: ReportBinding): void {
38
- sheet.setCellMeta(addr, REPORT_META_NAMESPACE, binding)
39
- }
40
-
41
- function snapshot(sheet: Sheet) {
42
- return sheet.snapshot()
43
- }
44
-
45
- function layoutOf(sheet: Sheet, data: DatasetRecords = ORDERS) {
46
- const index = buildTemplateIndex(snapshot(sheet))
47
- return computeExpansionLayout(index, data)
48
- }
49
-
50
- function placementRange(
51
- layout: ReturnType<typeof layoutOf>,
52
- row: number,
53
- col: number,
54
- listIndex = 0
55
- ) {
56
- const items = placementsAt(layout, { row, col })
57
- const hit = items[listIndex] ?? items[0]
58
- expect(hit, `逻辑格 R${row}C${col} 无映射`).toBeDefined()
59
- return hit!.physical
60
- }
61
-
62
- // ---- template-index ----
63
-
64
- describe('buildTemplateIndex', () => {
65
- it('父格不存在时抛可读错误', () => {
66
- const sheet = new Sheet()
67
- const binding = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
68
- binding.rowParent = { row: 0, col: 9 }
69
- bind(sheet, { row: 1, col: 0 }, binding)
70
-
71
- expect(() => buildTemplateIndex(snapshot(sheet))).toThrow(TemplateStructureError)
72
- expect(() => buildTemplateIndex(snapshot(sheet))).toThrow(/J1/)
73
- })
74
-
75
- it('父链成环时抛可读错误', () => {
76
- const sheet = new Sheet()
77
- const a = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
78
- const b = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'region')
79
- a.rowParent = { row: 2, col: 0 }
80
- b.rowParent = { row: 1, col: 0 }
81
- bind(sheet, { row: 1, col: 0 }, a)
82
- bind(sheet, { row: 2, col: 0 }, b)
83
-
84
- expect(() => buildTemplateIndex(snapshot(sheet))).toThrow(/父链存在环/)
85
- })
86
- })
87
-
88
- // ---- enumerateExpansionInstances ----
89
-
90
- describe('enumerateExpansionInstances', () => {
91
- const groupBinding: ReportBinding = {
92
- dataset: 'orders',
93
- field: 'customer',
94
- expand: 'down',
95
- aggregate: 'group'
96
- }
97
-
98
- it('group 取去重值数', () => {
99
- const instances = enumerateExpansionInstances(groupBinding, ORDERS, {})
100
- expect(instances).toHaveLength(3)
101
- expect(instances.map((i) => i.value)).toEqual(['甲', '乙', '丙'])
102
- })
103
-
104
- it('list 取记录数', () => {
105
- const listBinding: ReportBinding = {
106
- dataset: 'orders',
107
- field: 'orderNo',
108
- expand: 'down',
109
- aggregate: 'list'
110
- }
111
- expect(enumerateExpansionInstances(listBinding, ORDERS, {})).toHaveLength(4)
112
- })
113
-
114
- it('expand none 恒为 1 个实例', () => {
115
- const noneBinding: ReportBinding = {
116
- dataset: 'orders',
117
- field: 'amount',
118
- expand: 'none',
119
- aggregate: 'sum'
120
- }
121
- expect(enumerateExpansionInstances(noneBinding, ORDERS, {})).toHaveLength(1)
122
- })
123
-
124
- it('空数据集 group 产出 0 实例', () => {
125
- expect(enumerateExpansionInstances(groupBinding, { orders: [] }, {})).toHaveLength(0)
126
- })
127
- })
128
-
129
- // ---- coordinate geometry ----
130
-
131
- describe('computeExpansionLayout', () => {
132
- it('单级纵向:分组头 merge 跨度 = 明细行数,静态表头不被推移', () => {
133
- const sheet = new Sheet()
134
- sheet.setCells([
135
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
136
- { addr: { row: 0, col: 1 }, data: { v: '订单号' } }
137
- ])
138
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
139
- group.aggregate = 'group'
140
- group.expand = 'down'
141
- bind(sheet, { row: 1, col: 0 }, group)
142
-
143
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
144
- detail.aggregate = 'list'
145
- detail.expand = 'down'
146
- detail.rowParent = { row: 1, col: 0 }
147
- bind(sheet, { row: 1, col: 1 }, detail)
148
-
149
- const layout = layoutOf(sheet)
150
- // 表头 1 + 甲2 + 乙1 + 丙1 = 5 行
151
- expect(layout.rowCount).toBe(5)
152
-
153
- const header = placementRange(layout, 0, 0)
154
- expect(header.start).toEqual({ row: 0, col: 0 })
155
-
156
- const groupA = placementsAt(layout, { row: 1, col: 0 }).find((p) => p.rowPath[0] === 0)!
157
- expect(physicalRowSpan(groupA)).toBe(2)
158
- expect(groupA.physical.start.row).toBe(1)
159
-
160
- const groupB = placementsAt(layout, { row: 1, col: 0 }).find((p) => p.rowPath[0] === 1)!
161
- expect(physicalRowSpan(groupB)).toBe(1)
162
- expect(groupB.physical.start.row).toBe(3)
163
- })
164
-
165
- it('多级纵向嵌套:子分组在父实例内展开', () => {
166
- const sheet = new Sheet()
167
- const data: DatasetRecords = {
168
- orders: [
169
- { region: '华东', customer: '甲', orderNo: '1' },
170
- { region: '华东', customer: '甲', orderNo: '2' },
171
- { region: '华东', customer: '乙', orderNo: '3' },
172
- { region: '华南', customer: '丙', orderNo: '4' }
173
- ]
174
- }
175
-
176
- const region = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'region')
177
- region.aggregate = 'group'
178
- region.expand = 'down'
179
- bind(sheet, { row: 1, col: 0 }, region)
180
-
181
- const customer = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
182
- customer.aggregate = 'group'
183
- customer.expand = 'down'
184
- customer.rowParent = { row: 1, col: 0 }
185
- bind(sheet, { row: 2, col: 0 }, customer)
186
-
187
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
188
- detail.aggregate = 'list'
189
- detail.expand = 'down'
190
- detail.rowParent = { row: 2, col: 0 }
191
- bind(sheet, { row: 2, col: 1 }, detail)
192
-
193
- const layout = layoutOf(sheet, data)
194
- // 华东(甲2+乙1) + 华南(丙1) = 3 组块 × 各自明细 = 4 明细行
195
- expect(layout.rowCount).toBe(4)
196
-
197
- const eastCustomer = placementsAt(layout, { row: 2, col: 0 }).find(
198
- (p) => p.rowPath[0] === 0 && p.rowPath[1] === 0
199
- )!
200
- expect(physicalRowSpan(eastCustomer)).toBe(2)
201
- })
202
-
203
- it('单级横向:列分组向右延展', () => {
204
- const sheet = new Sheet()
205
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '品类' } }])
206
-
207
- const category = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'category')
208
- category.aggregate = 'group'
209
- category.expand = 'right'
210
- bind(sheet, { row: 0, col: 1 }, category)
211
-
212
- const layout = layoutOf(sheet, MATRIX)
213
- expect(layout.colCount).toBe(3) // 标题 + A/B 两列
214
- const headers = placementsAt(layout, { row: 0, col: 1 })
215
- expect(headers).toHaveLength(2)
216
- expect(headers[0]!.physical.start.col).toBe(1)
217
- expect(headers[1]!.physical.start.col).toBe(2)
218
- expect(physicalColSpan(headers[0]!)).toBe(1)
219
- })
220
-
221
- it('多级列头:colParent 链逐级向右嵌套', () => {
222
- const sheet = new Sheet()
223
- const data: DatasetRecords = {
224
- sales: [
225
- { year: '2024', quarter: 'Q1', amount: 1 },
226
- { year: '2024', quarter: 'Q2', amount: 2 },
227
- { year: '2025', quarter: 'Q1', amount: 3 }
228
- ]
229
- }
230
-
231
- const year = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'year')
232
- year.aggregate = 'group'
233
- year.expand = 'right'
234
- bind(sheet, { row: 0, col: 0 }, year)
235
-
236
- const quarter = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'quarter')
237
- quarter.aggregate = 'group'
238
- quarter.expand = 'right'
239
- quarter.colParent = { row: 0, col: 0 }
240
- bind(sheet, { row: 1, col: 0 }, quarter)
241
-
242
- const layout = layoutOf(sheet, data)
243
- const yearPlacements = placementsAt(layout, { row: 0, col: 0 })
244
- expect(yearPlacements).toHaveLength(2)
245
- expect(physicalColSpan(yearPlacements[0]!)).toBe(2)
246
- expect(physicalColSpan(yearPlacements[1]!)).toBe(1)
247
-
248
- const quarterPlacements = placementsAt(layout, { row: 1, col: 0 })
249
- expect(quarterPlacements).toHaveLength(3)
250
- })
251
-
252
- it('行列同时展开:交叉格按双父实例落位', () => {
253
- const sheet = new Sheet()
254
- sheet.setCells([
255
- { addr: { row: 0, col: 0 }, data: { v: '标题' } },
256
- { addr: { row: 2, col: 0 }, data: { v: '合计' } }
257
- ])
258
-
259
- const category = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'category')
260
- category.aggregate = 'group'
261
- category.expand = 'right'
262
- bind(sheet, { row: 0, col: 1 }, category)
263
-
264
- const region = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'region')
265
- region.aggregate = 'group'
266
- region.expand = 'down'
267
- bind(sheet, { row: 1, col: 0 }, region)
268
-
269
- const cross = createReportBinding({ id: 'sales', label: 's', fields: [] }, 'amount')
270
- cross.aggregate = 'sum'
271
- cross.expand = 'none'
272
- cross.rowParent = { row: 1, col: 0 }
273
- cross.colParent = { row: 0, col: 1 }
274
- bind(sheet, { row: 1, col: 1 }, cross)
275
-
276
- const layout = layoutOf(sheet, MATRIX)
277
- expect(layout.rowCount).toBe(4) // 表头 + 2 地区 + 合计
278
- expect(layout.colCount).toBe(3) // 角 + 2 品类
279
-
280
- const crossCells = placementsAt(layout, { row: 1, col: 1 })
281
- expect(crossCells).toHaveLength(4)
282
- expect(crossCells.map((p) => p.physical.start)).toEqual(
283
- expect.arrayContaining([
284
- { row: 1, col: 1 },
285
- { row: 1, col: 2 },
286
- { row: 2, col: 1 },
287
- { row: 2, col: 2 }
288
- ])
289
- )
290
- })
291
-
292
- it('mergeSpan: false 时扩展格不合并,逐格占位', () => {
293
- const sheet = new Sheet()
294
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
295
- group.aggregate = 'group'
296
- group.expand = 'down'
297
- group.mergeSpan = false
298
- bind(sheet, { row: 1, col: 0 }, group)
299
-
300
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
301
- detail.aggregate = 'list'
302
- detail.expand = 'down'
303
- detail.rowParent = { row: 1, col: 0 }
304
- bind(sheet, { row: 1, col: 1 }, detail)
305
-
306
- const layout = layoutOf(sheet)
307
- const groupCells = placementsAt(layout, { row: 1, col: 0 })
308
- expect(groupCells.every((p) => !p.mergeSpan)).toBe(true)
309
- expect(groupCells.every((p) => physicalRowSpan(p) === 1)).toBe(true)
310
- expect(groupCells).toHaveLength(4)
311
- })
312
-
313
- it('静态格在扩展带内随明细重复占位', () => {
314
- const sheet = new Sheet()
315
- sheet.setCells([{ addr: { row: 1, col: 2 }, data: { v: '备注' } }])
316
-
317
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
318
- group.aggregate = 'group'
319
- group.expand = 'down'
320
- bind(sheet, { row: 1, col: 0 }, group)
321
-
322
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
323
- detail.aggregate = 'list'
324
- detail.expand = 'down'
325
- detail.rowParent = { row: 1, col: 0 }
326
- bind(sheet, { row: 1, col: 1 }, detail)
327
-
328
- const layout = layoutOf(sheet)
329
- const notes = placementsAt(layout, { row: 1, col: 2 })
330
- expect(notes).toHaveLength(4)
331
- expect(notes.map((p) => p.physical.start.row)).toEqual([0, 1, 2, 3])
332
- })
333
-
334
- it('空展开:扩展格 0 跨度,子树不占位,静态表头仍映射', () => {
335
- const sheet = new Sheet()
336
- sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '客户' } }])
337
-
338
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
339
- group.aggregate = 'group'
340
- group.expand = 'down'
341
- bind(sheet, { row: 1, col: 0 }, group)
342
-
343
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
344
- detail.aggregate = 'list'
345
- detail.expand = 'down'
346
- detail.rowParent = { row: 1, col: 0 }
347
- bind(sheet, { row: 1, col: 1 }, detail)
348
-
349
- const layout = layoutOf(sheet, { orders: [] })
350
- expect(layout.rowCount).toBe(1)
351
- expect(placementsAt(layout, { row: 0, col: 0 })).toHaveLength(1)
352
- expect(placementsAt(layout, { row: 1, col: 0 })).toHaveLength(0)
353
- })
354
-
355
- it('跨数据集:父格过滤不同数据集时不串联', () => {
356
- const sheet = new Sheet()
357
- sheet.setCells([{ addr: { row: 0, col: 5 }, data: { v: '客户ID' } }])
358
-
359
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
360
- group.aggregate = 'group'
361
- group.expand = 'down'
362
- bind(sheet, { row: 1, col: 0 }, group)
363
-
364
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
365
- detail.aggregate = 'list'
366
- detail.expand = 'down'
367
- detail.rowParent = { row: 1, col: 0 }
368
- bind(sheet, { row: 1, col: 1 }, detail)
369
-
370
- const customers = createReportBinding({ id: 'customers', label: 'c', fields: [] }, 'id')
371
- customers.aggregate = 'list'
372
- customers.expand = 'down'
373
- bind(sheet, { row: 4, col: 5 }, customers)
374
-
375
- const layout = layoutOf(sheet, MULTI_DS)
376
- // 订单 4 明细行 + 表头 1 + 客户列表 2 行
377
- expect(layout.rowCount).toBe(4 + 1 + 2)
378
- const customerIds = placementsAt(layout, { row: 4, col: 5 })
379
- expect(customerIds).toHaveLength(2)
380
- expect(customerIds[0]!.physical.start.row).toBe(5)
381
- })
382
-
383
- it('模板合并区域映射到物理表头行', () => {
384
- const sheet = new Sheet()
385
- sheet.setCells([
386
- { addr: { row: 0, col: 0 }, data: { v: '客户' } },
387
- { addr: { row: 0, col: 1 }, data: { v: '订单号' } }
388
- ])
389
- sheet.mergeCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 1 } })
390
-
391
- const group = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'customer')
392
- group.aggregate = 'group'
393
- group.expand = 'down'
394
- bind(sheet, { row: 1, col: 0 }, group)
395
-
396
- const detail = createReportBinding({ id: 'orders', label: 'o', fields: [] }, 'orderNo')
397
- detail.aggregate = 'list'
398
- detail.expand = 'down'
399
- detail.rowParent = { row: 1, col: 0 }
400
- bind(sheet, { row: 1, col: 1 }, detail)
401
-
402
- const layout = layoutOf(sheet)
403
- expect(layout.mappedMerges).toEqual([{ start: { row: 0, col: 0 }, end: { row: 0, col: 1 } }])
404
- })
405
- })