@veltra/sheet 2.3.4 → 2.4.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 (44) hide show
  1. package/dist/components/report/binding-badge.js +6 -0
  2. package/dist/components/report/designer/drill-dialog.js +258 -0
  3. package/dist/components/report/designer/float-panel.js +26 -14
  4. package/dist/components/report/print.js +39 -0
  5. package/dist/components/report/report-designer.js +56 -21
  6. package/dist/components/report/report-viewer.js +173 -44
  7. package/dist/components/report/style2.css +91 -0
  8. package/dist/components/report/use-report-designer.js +1 -0
  9. package/dist/components/report/use-report-viewer.js +88 -3
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.js +3 -1
  12. package/dist/report/drill.d.ts +48 -0
  13. package/dist/report/drill.js +78 -0
  14. package/dist/report/index.d.ts +4 -2
  15. package/dist/report/print.d.ts +23 -0
  16. package/dist/report/print.js +197 -0
  17. package/dist/report/render/aggregate.js +25 -1
  18. package/dist/report/types.d.ts +19 -1
  19. package/dist/types/report-designer.d.ts +13 -0
  20. package/dist/types/report-viewer.d.ts +19 -0
  21. package/package.json +6 -6
  22. package/src/components/report/__test__/report-designer.test.ts +98 -3
  23. package/src/components/report/__test__/report-drill.test.ts +692 -0
  24. package/src/components/report/__test__/report-viewer.test.ts +43 -0
  25. package/src/components/report/__test__/use-report-designer.test.ts +108 -1
  26. package/src/components/report/binding-badge.ts +5 -0
  27. package/src/components/report/designer/drill-dialog.vue +245 -0
  28. package/src/components/report/designer/float-panel.vue +12 -0
  29. package/src/components/report/print.ts +44 -0
  30. package/src/components/report/report-designer.vue +35 -2
  31. package/src/components/report/report-viewer.vue +171 -18
  32. package/src/components/report/style-designer.scss +66 -0
  33. package/src/components/report/style-viewer.scss +40 -0
  34. package/src/components/report/use-report-designer.ts +1 -0
  35. package/src/components/report/use-report-viewer.ts +150 -11
  36. package/src/report/__test__/drill.test.ts +194 -0
  37. package/src/report/__test__/print.test.ts +170 -0
  38. package/src/report/drill.ts +112 -0
  39. package/src/report/index.ts +4 -1
  40. package/src/report/print.ts +234 -0
  41. package/src/report/render/aggregate.ts +28 -0
  42. package/src/report/types.ts +20 -0
  43. package/src/types/report-designer.ts +18 -1
  44. package/src/types/report-viewer.ts +18 -1
@@ -0,0 +1,194 @@
1
+ import { Sheet, cellKey } from '@veltra/sheet-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { REPORT_META_NAMESPACE } from '../binding'
5
+ import {
6
+ buildDrillHitMap,
7
+ createDrillStack,
8
+ currentDrillLayer,
9
+ popDrillLayer,
10
+ pushDrillLayer,
11
+ resolveDrillParams
12
+ } from '../drill'
13
+ import { createReportTemplate, type ReportTemplate } from '../template'
14
+ import type { ReportBinding, ReportDrillConfig } from '../types'
15
+
16
+ // ---- 内联 fixtures ----
17
+
18
+ /** 最小 Report Template:下钻栈只关心引用与参数,不消费快照内容 */
19
+ function makeTemplate(rows: number): ReportTemplate {
20
+ return {
21
+ version: 1,
22
+ cells: [],
23
+ styles: [],
24
+ merges: [],
25
+ frozen: { rows: 0, cols: 0 },
26
+ rows,
27
+ cols: 0
28
+ }
29
+ }
30
+
31
+ const TPL_A = makeTemplate(10)
32
+ const TPL_B = makeTemplate(20)
33
+ const TPL_C = makeTemplate(30)
34
+
35
+ const DRILL: ReportDrillConfig = {
36
+ target: 'tpl-detail',
37
+ mapping: { region: 'p_region', amount: 'p_amount' },
38
+ openMode: 'switch'
39
+ }
40
+
41
+ describe('resolveDrillParams', () => {
42
+ it('按映射从记录取值为详情报参数', () => {
43
+ const values = resolveDrillParams(DRILL, { region: '华东', amount: 100 })
44
+ expect(values).toEqual({ p_region: '华东', p_amount: 100 })
45
+ })
46
+
47
+ it('记录缺字段时跳过对应参数,null 值保留', () => {
48
+ const values = resolveDrillParams(DRILL, { region: '华东', amount: null })
49
+ expect(values).toEqual({ p_region: '华东', p_amount: null })
50
+ expect(
51
+ 'p_missing' in resolveDrillParams({ ...DRILL, mapping: { missing: 'p_missing' } }, {})
52
+ ).toBe(false)
53
+ })
54
+
55
+ it('空映射返回空参数', () => {
56
+ expect(resolveDrillParams({ ...DRILL, mapping: {} }, { region: '华东' })).toEqual({})
57
+ })
58
+ })
59
+
60
+ describe('下钻栈', () => {
61
+ it('根层即当前层', () => {
62
+ const stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
63
+ expect(stack).toHaveLength(1)
64
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a0' } })
65
+ })
66
+
67
+ it('push 后当前层为新层,前层参数定格为离开时的值', () => {
68
+ const stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
69
+ const next = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b1' } }, { p: 'a1' })
70
+ expect(next).toHaveLength(2)
71
+ expect(currentDrillLayer(next)).toEqual({ template: TPL_B, params: { p: 'b1' } })
72
+ expect(next[0]).toEqual({ template: TPL_A, params: { p: 'a1' } })
73
+ // 不可变更新:原栈不受影响
74
+ expect(stack).toHaveLength(1)
75
+ })
76
+
77
+ it('push 缺省 currentParams 时保留前层原参数', () => {
78
+ const stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
79
+ const next = pushDrillLayer(stack, { template: TPL_B, params: {} })
80
+ expect(next[0]).toEqual({ template: TPL_A, params: { p: 'a0' } })
81
+ })
82
+
83
+ it('pop 逐级回退并保留各层当时参数', () => {
84
+ let stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
85
+ stack = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b1' } }, { p: 'a1' })
86
+ stack = pushDrillLayer(stack, { template: TPL_C, params: { p: 'c1' } }, { p: 'b2' })
87
+
88
+ stack = popDrillLayer(stack)
89
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_B, params: { p: 'b2' } })
90
+ stack = popDrillLayer(stack)
91
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a1' } })
92
+ })
93
+
94
+ it('仅剩根层时 pop 原样返回', () => {
95
+ const stack = createDrillStack({ template: TPL_A, params: {} })
96
+ expect(popDrillLayer(stack)).toBe(stack)
97
+ })
98
+
99
+ it('指向自身照常压栈,靠 pop 逐级退出', () => {
100
+ let stack = createDrillStack({ template: TPL_A, params: { p: 'a0' } })
101
+ stack = pushDrillLayer(stack, { template: TPL_A, params: { p: 'a1' } })
102
+ stack = pushDrillLayer(stack, { template: TPL_A, params: { p: 'a2' } })
103
+ expect(stack).toHaveLength(3)
104
+
105
+ stack = popDrillLayer(stack)
106
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a1' } })
107
+ stack = popDrillLayer(stack)
108
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a0' } })
109
+ })
110
+
111
+ it('成环照常压栈,回退沿压栈路径逐级退出', () => {
112
+ let stack = createDrillStack({ template: TPL_A, params: { p: 'root' } })
113
+ stack = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b1' } })
114
+ stack = pushDrillLayer(stack, { template: TPL_A, params: { p: 'a-from-b' } })
115
+ stack = pushDrillLayer(stack, { template: TPL_B, params: { p: 'b2' } })
116
+ expect(stack.map((layer) => layer.template)).toEqual([TPL_A, TPL_B, TPL_A, TPL_B])
117
+
118
+ stack = popDrillLayer(stack)
119
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'a-from-b' } })
120
+ stack = popDrillLayer(stack)
121
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_B, params: { p: 'b1' } })
122
+ stack = popDrillLayer(stack)
123
+ expect(currentDrillLayer(stack)).toEqual({ template: TPL_A, params: { p: 'root' } })
124
+ })
125
+ })
126
+
127
+ describe('buildDrillHitMap 与上下文解析', () => {
128
+ it('分组格与汇总聚合格均生成正确命中上下文(含汇总格自身聚合值)', () => {
129
+ const sheet = new Sheet()
130
+ sheet.setCellMeta({ row: 0, col: 0 }, REPORT_META_NAMESPACE, {
131
+ dataset: 'ds',
132
+ field: 'region',
133
+ aggregate: 'group',
134
+ expand: 'down',
135
+ drill: { target: 't2', mapping: { region: 'p_region' }, openMode: 'switch' }
136
+ })
137
+ sheet.setCellMeta({ row: 0, col: 1 }, REPORT_META_NAMESPACE, {
138
+ dataset: 'ds',
139
+ field: 'amount',
140
+ aggregate: 'sum',
141
+ expand: 'none',
142
+ rowParent: { row: 0, col: 0 },
143
+ drill: {
144
+ target: 't2',
145
+ mapping: { region: 'p_region', amount: 'p_amount' },
146
+ openMode: 'dialog'
147
+ }
148
+ })
149
+
150
+ const template = sheet.snapshot()
151
+ const data = {
152
+ ds: [
153
+ { region: '华东', amount: 100 },
154
+ { region: '华东', amount: 200 },
155
+ { region: '华南', amount: 50 }
156
+ ]
157
+ }
158
+
159
+ const hitMap = buildDrillHitMap(template, data)
160
+ // 华东分组格 (0,0)
161
+ const hitGroup0 = hitMap.get(cellKey({ row: 0, col: 0 }))
162
+ expect(hitGroup0?.record).toEqual({ region: '华东' })
163
+
164
+ // 华东汇总格 (0,1) 拥有祖先分组 region 与自身的求和计算值 300
165
+ const hitSum0 = hitMap.get(cellKey({ row: 0, col: 1 }))
166
+ expect(hitSum0?.record).toEqual({ region: '华东', amount: 300 })
167
+ expect(resolveDrillParams(hitSum0!.config, hitSum0!.record)).toEqual({
168
+ p_region: '华东',
169
+ p_amount: 300
170
+ })
171
+
172
+ // 华南汇总格 (1,1)
173
+ const hitSum1 = hitMap.get(cellKey({ row: 1, col: 1 }))
174
+ expect(hitSum1?.record).toEqual({ region: '华南', amount: 50 })
175
+ expect(resolveDrillParams(hitSum1!.config, hitSum1!.record)).toEqual({
176
+ p_region: '华南',
177
+ p_amount: 50
178
+ })
179
+ })
180
+ })
181
+
182
+ describe('旧绑定兼容', () => {
183
+ it('无 drill 字段的旧绑定序列化往返后不出现 drill 键', () => {
184
+ const binding: ReportBinding = {
185
+ dataset: 'orders',
186
+ field: 'region',
187
+ expand: 'down',
188
+ aggregate: 'list'
189
+ }
190
+ const restored = JSON.parse(JSON.stringify(binding)) as ReportBinding
191
+ expect('drill' in restored).toBe(false)
192
+ expect(restored.drill).toBeUndefined()
193
+ })
194
+ })
@@ -0,0 +1,170 @@
1
+ import { Sheet } from '@veltra/sheet-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { buildFilledReportPrintHtml } from '../print'
5
+
6
+ /** 基础填充表:表头 + 两列数据 */
7
+ function createFilledSheet(name = '销售报表'): Sheet {
8
+ const sheet = new Sheet(name)
9
+ sheet.setCells([
10
+ { addr: { row: 0, col: 0 }, data: { v: '客户' } },
11
+ { addr: { row: 0, col: 1 }, data: { v: '金额' } },
12
+ { addr: { row: 1, col: 0 }, data: { v: '甲公司' } },
13
+ { addr: { row: 1, col: 1 }, data: { v: 100 } }
14
+ ])
15
+ return sheet
16
+ }
17
+
18
+ describe('buildFilledReportPrintHtml', () => {
19
+ it('输出完整打印文档:值为文本、默认 A4 纵向、缺省标题取 sheet 名', () => {
20
+ const html = buildFilledReportPrintHtml(createFilledSheet())
21
+
22
+ expect(html).toContain('<!doctype html>')
23
+ expect(html).toContain('<title>销售报表</title>')
24
+ expect(html).toContain('@page { size: A4 portrait;')
25
+ expect(html).toContain('<td>客户</td>')
26
+ expect(html).toContain('<td>100</td>')
27
+ // 默认列宽 80 / 行高 28(对齐 grid 默认)
28
+ expect(html).toContain('<col style="width:80px">')
29
+ expect(html).toContain('<tr style="height:28px">')
30
+ // 背景色打印保色
31
+ expect(html).toContain('print-color-adjust: exact')
32
+ })
33
+
34
+ it('选项:title 覆盖、landscape 方向;空名 sheet 回落「报表」', () => {
35
+ const html = buildFilledReportPrintHtml(createFilledSheet(), {
36
+ title: '月度报表',
37
+ orientation: 'landscape'
38
+ })
39
+ expect(html).toContain('<title>月度报表</title>')
40
+ expect(html).toContain('@page { size: A4 landscape;')
41
+
42
+ const fallback = buildFilledReportPrintHtml(new Sheet(''))
43
+ expect(fallback).toContain('<title>报表</title>')
44
+ })
45
+
46
+ it('合并格映射 rowspan/colspan,被覆盖格不输出', () => {
47
+ const sheet = createFilledSheet()
48
+ sheet.mergeCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 1 } })
49
+
50
+ const html = buildFilledReportPrintHtml(sheet)
51
+ expect(html).toContain('<td colspan="2" rowspan="2">客户</td>')
52
+ // 2×2 合并后只剩 1 个 td(锚点),覆盖格跳过
53
+ const tdCount = (html.match(/<td/g) ?? []).length
54
+ expect(tdCount).toBe(1)
55
+ })
56
+
57
+ it('有效样式序列化为内联 CSS(fill/border/font/align/wrap)', () => {
58
+ const sheet = createFilledSheet()
59
+ sheet.setCellStyle(
60
+ { start: { row: 0, col: 0 }, end: { row: 0, col: 0 } },
61
+ {
62
+ fill: { color: '#ff0000' },
63
+ border: { top: { style: 'thin', width: 1, color: '#000000' } },
64
+ font: {
65
+ bold: true,
66
+ italic: true,
67
+ underline: true,
68
+ strikethrough: true,
69
+ size: 14,
70
+ color: '#111111'
71
+ },
72
+ align: { horizontal: 'center', vertical: 'middle', wrap: true }
73
+ }
74
+ )
75
+ sheet.setCellStyle(
76
+ { start: { row: 1, col: 1 }, end: { row: 1, col: 1 } },
77
+ { align: { horizontal: 'right' } }
78
+ )
79
+
80
+ const html = buildFilledReportPrintHtml(sheet)
81
+ expect(html).toContain('background-color:#ff0000')
82
+ expect(html).toContain('border-top:1px solid #000000')
83
+ expect(html).toContain('font-weight:bold')
84
+ expect(html).toContain('font-style:italic')
85
+ expect(html).toContain('text-decoration:underline line-through')
86
+ expect(html).toContain('font-size:14pt')
87
+ expect(html).toContain('color:#111111')
88
+ expect(html).toContain('text-align:center')
89
+ expect(html).toContain('vertical-align:middle')
90
+ expect(html).toContain('white-space:normal;word-break:break-all')
91
+ expect(html).toContain('text-align:right')
92
+ })
93
+
94
+ it('虚线/点线边框线型映射 CSS dashed/dotted', () => {
95
+ const sheet = createFilledSheet()
96
+ sheet.setCellStyle(
97
+ { start: { row: 0, col: 0 }, end: { row: 0, col: 0 } },
98
+ {
99
+ border: {
100
+ left: { style: 'dashed', width: 1, color: '#000000' },
101
+ right: { style: 'dotted', width: 2, color: '#00ff00' }
102
+ }
103
+ }
104
+ )
105
+ const html = buildFilledReportPrintHtml(sheet)
106
+ expect(html).toContain('border-left:1px dashed #000000')
107
+ expect(html).toContain('border-right:2px dotted #00ff00')
108
+ })
109
+
110
+ it('自定义列宽 / 行高写入 colgroup 与 tr', () => {
111
+ const sheet = createFilledSheet()
112
+ sheet.setColWidth(1, 160)
113
+ sheet.setRowHeight(1, 40)
114
+
115
+ const html = buildFilledReportPrintHtml(sheet)
116
+ expect(html).toContain('<col style="width:160px">')
117
+ expect(html).toContain('<tr style="height:40px">')
118
+ })
119
+
120
+ it('文本与标题 HTML 转义', () => {
121
+ const sheet = new Sheet('报表<Q>')
122
+ sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '<b>"x"</b> & y' } }])
123
+
124
+ const html = buildFilledReportPrintHtml(sheet)
125
+ expect(html).toContain('<title>报表&lt;Q&gt;</title>')
126
+ expect(html).toContain('<td>&lt;b&gt;"x"&lt;/b&gt; &amp; y</td>')
127
+ expect(html).not.toContain('<td><b>')
128
+ })
129
+
130
+ it('浮动图:data URL 内嵌,锚点格左上 + 格内偏移定位,显式宽高直出', () => {
131
+ const sheet = createFilledSheet()
132
+ sheet.setColWidth(0, 100)
133
+ sheet.insertImage({
134
+ data: new Uint8Array([1, 2, 3]),
135
+ type: 'png',
136
+ anchor: { from: { row: 1, col: 1, offsetX: 10, offsetY: 5 } },
137
+ width: 48,
138
+ height: 24,
139
+ altText: '图章'
140
+ })
141
+
142
+ const html = buildFilledReportPrintHtml(sheet)
143
+ // left = col0 宽 100 + 默认列宽 80(col1 前)→ 取 col1 左缘 100... colWidths=[100,80],col1 左缘=100,+offsetX=110
144
+ // top = row0 高 28,+offsetY=5 → 33
145
+ expect(html).toContain(`<img src="data:image/png;base64,AQID"`)
146
+ expect(html).toContain('left:110px;top:33px;width:48px;height:24px;')
147
+ expect(html).toContain('alt="图章"')
148
+ })
149
+
150
+ it('浮动图:宽高缺失且有 to 时按 from→to 跨度兜底', () => {
151
+ const sheet = createFilledSheet()
152
+ sheet.insertImage({
153
+ data: new Uint8Array([0]),
154
+ type: 'jpeg',
155
+ anchor: { from: { row: 0, col: 0 }, to: { row: 1, col: 1 } }
156
+ })
157
+
158
+ const html = buildFilledReportPrintHtml(sheet)
159
+ // 跨度:col0+col1 = 80+80 = 160;row0+row1 = 28+28 = 56
160
+ expect(html).toContain('left:0px;top:0px;width:160px;height:56px;')
161
+ expect(html).toContain('data:image/jpeg;base64,')
162
+ })
163
+
164
+ it('空 sheet 输出合法空表文档', () => {
165
+ const html = buildFilledReportPrintHtml(new Sheet('空白'))
166
+ expect(html).toContain('<title>空白</title>')
167
+ expect(html).toContain('<table>')
168
+ expect(html).not.toContain('<tr')
169
+ })
170
+ })
@@ -0,0 +1,112 @@
1
+ import type { SheetSnapshot } from '@veltra/sheet-core'
2
+ import { cellKey } from '@veltra/sheet-core'
3
+
4
+ import { AggregateIndex, resolvePlacementContext } from './render/aggregate'
5
+ import { computeExpansionLayout } from './render/coordinate'
6
+ import { buildTemplateIndex } from './render/template-index'
7
+ import type { ReportTemplate } from './template'
8
+ import type { DatasetRecords, ParamValues, ReportDrillConfig } from './types'
9
+
10
+ /**
11
+ * 宿主模板解析契约:按下钻 target 引用取回目标模板(允许同步/异步)。
12
+ * 失败抛错——查看器提示可读错误并停留当前报;设计器配置对话框据此解析目标模板查询参数。
13
+ */
14
+ export type ResolveReportTemplate = (ref: string) => ReportTemplate | Promise<ReportTemplate>
15
+
16
+ /**
17
+ * 按下钻映射从该格对应记录取值,生成详情报 Filter Bar 参数值。
18
+ * 记录缺字段(值为 `undefined`)时跳过对应参数,由详情报回退默认值;空映射返回空参数。
19
+ */
20
+ export function resolveDrillParams(
21
+ config: ReportDrillConfig,
22
+ record: Record<string, unknown>
23
+ ): ParamValues {
24
+ const values: ParamValues = {}
25
+ for (const [field, paramId] of Object.entries(config.mapping)) {
26
+ const value = record[field]
27
+ if (value === undefined) continue
28
+ values[paramId] = value
29
+ }
30
+ return values
31
+ }
32
+
33
+ /** 下钻栈层:一张报表模板 + 当时 Filter Bar 参数值(回退时据此恢复并重新取数) */
34
+ export interface DrillStackLayer {
35
+ template: ReportTemplate
36
+ params: ParamValues
37
+ }
38
+
39
+ /**
40
+ * 下钻栈:自根(初始报表)到顶的层序列,至少一层。
41
+ * 只经下方纯函数做不可变更新(返回新栈),便于组件层直接挂响应式。
42
+ */
43
+ export type DrillStack = DrillStackLayer[]
44
+
45
+ /** 以初始报表为根层创建下钻栈 */
46
+ export function createDrillStack(root: DrillStackLayer): DrillStack {
47
+ return [{ template: root.template, params: { ...root.params } }]
48
+ }
49
+
50
+ /**
51
+ * 压入详情报新层并使其成为当前层;`currentParams` 把当前层参数定格为离开时的值
52
+ * (缺省保留原值)。指向自身或成环不做检测,照常压栈,靠 pop 逐级退出。
53
+ */
54
+ export function pushDrillLayer(
55
+ stack: DrillStack,
56
+ next: DrillStackLayer,
57
+ currentParams?: ParamValues
58
+ ): DrillStack {
59
+ const top = stack[stack.length - 1]!
60
+ const layers = currentParams
61
+ ? [...stack.slice(0, -1), { template: top.template, params: { ...currentParams } }]
62
+ : [...stack]
63
+ layers.push({ template: next.template, params: { ...next.params } })
64
+ return layers
65
+ }
66
+
67
+ /** 弹出当前层返回上一层;仅剩根层时原样返回(查看器始终保留一张报表) */
68
+ export function popDrillLayer(stack: DrillStack): DrillStack {
69
+ if (stack.length <= 1) return stack
70
+ return stack.slice(0, -1)
71
+ }
72
+
73
+ /** 当前层(栈顶) */
74
+ export function currentDrillLayer(stack: DrillStack): DrillStackLayer {
75
+ return stack[stack.length - 1]!
76
+ }
77
+
78
+ /** 下钻命中:物理格对应的下钻配置与该格记录 */
79
+ export interface DrillHit {
80
+ config: ReportDrillConfig
81
+ record: Record<string, unknown>
82
+ }
83
+
84
+ /**
85
+ * 构建填充报表的物理格 → 下钻命中索引(每次成功取数后重建)。
86
+ * 仅含配了下钻的绑定格落点(含分组/汇总格与展开后的实例格,合并跨度内每格都命中);
87
+ * 记录由 `resolvePlacementContext` 解析:list 明细取源数据行,分组/汇总取分组上下文。
88
+ */
89
+ export function buildDrillHitMap(
90
+ template: SheetSnapshot,
91
+ data: DatasetRecords
92
+ ): Map<number, DrillHit> {
93
+ const hits = new Map<number, DrillHit>()
94
+ const index = buildTemplateIndex(template)
95
+ const layout = computeExpansionLayout(index, data)
96
+ const aggregateIndex = new AggregateIndex(data)
97
+
98
+ for (const placement of layout.placements) {
99
+ const config = placement.binding?.drill
100
+ if (!config) continue
101
+ const record = resolvePlacementContext(placement, index, data, aggregateIndex)
102
+ if (!record) continue
103
+
104
+ const hit: DrillHit = { config, record }
105
+ for (let row = placement.physical.start.row; row <= placement.physical.end.row; row++) {
106
+ for (let col = placement.physical.start.col; col <= placement.physical.end.col; col++) {
107
+ hits.set(cellKey({ row, col }), hit)
108
+ }
109
+ }
110
+ }
111
+ return hits
112
+ }
@@ -1,5 +1,6 @@
1
1
  // 报表纯 TS 内核(ADR-0003 决策 1):渲染引擎 / 绑定 / 条件规则 / 查询参数 / 数据连接器 /
2
- // Report Template(自包含模板与取数)/ Filter Bar 值规范化。
2
+ // Report Template(自包含模板与取数)/ Filter Bar 值规范化 / 下钻配置与下钻栈 /
3
+ // Filled Report XLSX 导出与打印 HTML。
3
4
  // 框架无关、无 DOM 依赖;组件壳(UReportDesigner / UReportViewer)落 components/report/。
4
5
 
5
6
  export * from './types'
@@ -10,4 +11,6 @@ export * from './render'
10
11
  export * from './connector'
11
12
  export * from './template'
12
13
  export * from './filter-bar'
14
+ export * from './drill'
13
15
  export * from './export-xlsx'
16
+ export * from './print'