@veltra/sheet 2.1.1 → 2.2.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 (202) hide show
  1. package/dist/components/report/binding-badge.js +76 -0
  2. package/dist/components/report/designer/cell-coords.js +46 -0
  3. package/dist/components/report/designer/col-widths.js +15 -0
  4. package/dist/components/report/designer/conditional-rules/helpers.js +139 -0
  5. package/dist/components/report/designer/drop-highlight-overlay.js +76 -0
  6. package/dist/components/report/designer/float-panel.js +385 -0
  7. package/dist/components/report/designer/role.js +17 -0
  8. package/dist/components/report/designer/rule-preview.js +33 -0
  9. package/dist/components/report/designer/rule-row.js +269 -0
  10. package/dist/components/report/designer/rules-dialog.js +135 -0
  11. package/dist/components/report/designer/topology-overlay.js +122 -0
  12. package/dist/components/report/designer/topology.js +64 -0
  13. package/dist/components/report/designer/use-grid-overlay.js +75 -0
  14. package/dist/components/report/designer-hub.js +177 -0
  15. package/dist/components/report/field-panel-helpers.js +33 -0
  16. package/dist/components/report/field-panel.js +132 -0
  17. package/dist/components/report/filter-bar.js +74 -0
  18. package/dist/components/report/hub-connection-form.js +145 -0
  19. package/dist/components/report/hub-dataset-editor.js +394 -0
  20. package/dist/components/report/report-designer.js +343 -0
  21. package/dist/components/report/report-viewer.js +111 -0
  22. package/dist/components/report/style.d.ts +17 -0
  23. package/dist/components/report/style.js +18 -0
  24. package/dist/components/report/style2.css +869 -0
  25. package/dist/components/report/use-report-designer.js +360 -0
  26. package/dist/components/report/use-report-viewer.js +50 -0
  27. package/dist/{vue → components/sheet}/formula-bar.js +1 -1
  28. package/dist/{vue → components/sheet}/formula-suggest-list.js +1 -1
  29. package/dist/{vue → components/sheet}/import-file.js +1 -1
  30. package/dist/{vue → components/sheet}/insert-count-menu-item.js +1 -1
  31. package/dist/{vue → components/sheet}/insert-image.js +1 -1
  32. package/dist/{vue → components/sheet}/popup-helpers.js +1 -1
  33. package/dist/{vue → components/sheet}/popups/border-popup.js +1 -1
  34. package/dist/{vue → components/sheet}/popups/border-preset-glyph.js +1 -1
  35. package/dist/{vue → components/sheet}/popups/export-popup.js +2 -2
  36. package/dist/{vue → components/sheet}/popups/fill-color-popup.js +1 -1
  37. package/dist/{vue → components/sheet}/popups/find-popup.js +1 -1
  38. package/dist/{vue → components/sheet}/popups/font-color-popup.js +1 -1
  39. package/dist/{vue → components/sheet}/popups/font-size-popup.js +1 -1
  40. package/dist/{vue → components/sheet}/popups/import.worker.d.ts +1 -1
  41. package/dist/{vue → components/sheet}/popups/import.worker.js +1 -1
  42. package/dist/{vue → components/sheet}/popups/insert-image-popup.js +1 -1
  43. package/dist/{vue → components/sheet}/sheet-context-menu.js +1 -1
  44. package/dist/{vue → components/sheet}/sheet-tabs.js +1 -1
  45. package/dist/{vue → components/sheet}/sheet-toolbar.js +1 -1
  46. package/dist/{vue → components/sheet}/sheet.js +8 -1
  47. package/dist/{vue → components/sheet}/use-find-replace.js +1 -1
  48. package/dist/{vue → components/sheet}/use-formula-ref-select.js +1 -1
  49. package/dist/{vue → components/sheet}/use-sheet-grid.js +10 -2
  50. package/dist/{vue → components/sheet}/use-sheet-state.js +3 -3
  51. package/dist/{vue → components/sheet}/use-sheet-tabs-bar.js +1 -1
  52. package/dist/{vue → components/sheet}/use-tool-groups.js +2 -2
  53. package/dist/{vue → components/sheet}/use-tool-popup.js +2 -2
  54. package/dist/{vue → components/sheet}/use-toolbar-scroll.js +1 -1
  55. package/dist/index.d.ts +17 -3
  56. package/dist/index.js +13 -3
  57. package/dist/report/binding.d.ts +45 -0
  58. package/dist/report/binding.js +175 -0
  59. package/dist/report/connector.d.ts +66 -0
  60. package/dist/report/connector.js +159 -0
  61. package/dist/report/export-xlsx.d.ts +19 -0
  62. package/dist/report/export-xlsx.js +95 -0
  63. package/dist/report/filter-bar.d.ts +10 -0
  64. package/dist/report/filter-bar.js +31 -0
  65. package/dist/report/index.d.ts +12 -0
  66. package/dist/report/params.d.ts +13 -0
  67. package/dist/report/params.js +61 -0
  68. package/dist/report/render/aggregate.js +157 -0
  69. package/dist/report/render/builder.d.ts +12 -0
  70. package/dist/report/render/builder.js +139 -0
  71. package/dist/report/render/coordinate.d.ts +25 -0
  72. package/dist/report/render/coordinate.js +733 -0
  73. package/dist/report/render/index.d.ts +14 -0
  74. package/dist/report/render/index.js +21 -0
  75. package/dist/report/render/style-resolver.js +50 -0
  76. package/dist/report/render/template-index.d.ts +24 -0
  77. package/dist/report/render/template-index.js +137 -0
  78. package/dist/report/rules/evaluate.d.ts +25 -0
  79. package/dist/report/rules/evaluate.js +82 -0
  80. package/dist/report/rules/index.d.ts +2 -0
  81. package/dist/report/template.d.ts +59 -0
  82. package/dist/report/template.js +80 -0
  83. package/dist/report/types.d.ts +85 -0
  84. package/dist/tools/context.d.ts +1 -1
  85. package/dist/types/index.d.ts +3 -2
  86. package/dist/types/report-designer.d.ts +36 -0
  87. package/dist/types/report-viewer.d.ts +38 -0
  88. package/dist/types/sheet.d.ts +17 -1
  89. package/package.json +3 -5
  90. package/src/components/report/__test__/conditional-rules-helpers.test.ts +87 -0
  91. package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
  92. package/src/components/report/__test__/float-panel-position.test.ts +45 -0
  93. package/src/components/report/__test__/report-designer.test.ts +388 -0
  94. package/src/components/report/__test__/report-viewer.test.ts +334 -0
  95. package/src/components/report/__test__/topology.test.ts +143 -0
  96. package/src/components/report/__test__/use-report-designer.test.ts +565 -0
  97. package/src/components/report/binding-badge.ts +73 -0
  98. package/src/components/report/designer/cell-coords.ts +94 -0
  99. package/src/components/report/designer/col-widths.ts +33 -0
  100. package/src/components/report/designer/conditional-rules/helpers.ts +186 -0
  101. package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
  102. package/src/components/report/designer/float-panel.vue +387 -0
  103. package/src/components/report/designer/role.ts +32 -0
  104. package/src/components/report/designer/rule-preview.vue +40 -0
  105. package/src/components/report/designer/rule-row.vue +252 -0
  106. package/src/components/report/designer/rules-dialog.vue +126 -0
  107. package/src/components/report/designer/topology-overlay.vue +127 -0
  108. package/src/components/report/designer/topology.ts +124 -0
  109. package/src/components/report/designer/use-grid-overlay.ts +113 -0
  110. package/src/components/report/designer-hub.vue +198 -0
  111. package/src/components/report/field-panel-helpers.ts +44 -0
  112. package/src/components/report/field-panel.vue +136 -0
  113. package/src/components/report/filter-bar.vue +77 -0
  114. package/src/components/report/hub-connection-form.vue +124 -0
  115. package/src/components/report/hub-dataset-editor.vue +369 -0
  116. package/src/components/report/index.ts +2 -0
  117. package/src/components/report/report-designer.vue +356 -0
  118. package/src/components/report/report-viewer.vue +128 -0
  119. package/src/components/report/style-designer.scss +583 -0
  120. package/src/components/report/style-hub.scss +292 -0
  121. package/src/components/report/style-viewer.scss +104 -0
  122. package/src/components/report/style.scss +3 -0
  123. package/src/components/report/style.ts +20 -0
  124. package/src/components/report/use-report-designer.ts +547 -0
  125. package/src/components/report/use-report-viewer.ts +79 -0
  126. package/src/{vue → components/sheet}/__test__/formula-bar.test.ts +37 -18
  127. package/src/{vue → components/sheet}/__test__/insert-image-popup.test.ts +1 -1
  128. package/src/{vue → components/sheet}/__test__/insert-image.test.ts +1 -1
  129. package/src/{vue → components/sheet}/__test__/sheet-component.test.ts +40 -3
  130. package/src/{vue → components/sheet}/__test__/sheet-context-menu.test.ts +1 -1
  131. package/src/{vue → components/sheet}/formula-bar.vue +1 -1
  132. package/src/{vue → components/sheet}/insert-image.ts +1 -1
  133. package/src/{vue → components/sheet}/popup-helpers.ts +1 -1
  134. package/src/{vue → components/sheet}/popups/border-popup.vue +1 -1
  135. package/src/{vue → components/sheet}/popups/export-popup.vue +2 -2
  136. package/src/{vue → components/sheet}/popups/fill-color-popup.vue +1 -1
  137. package/src/{vue → components/sheet}/popups/find-popup.vue +1 -1
  138. package/src/{vue → components/sheet}/popups/font-color-popup.vue +1 -1
  139. package/src/{vue → components/sheet}/popups/font-size-popup.vue +1 -1
  140. package/src/{vue → components/sheet}/popups/insert-image-popup.vue +1 -1
  141. package/src/{vue → components/sheet}/sheet-context-menu.ts +2 -2
  142. package/src/{vue → components/sheet}/sheet-toolbar.vue +2 -2
  143. package/src/{vue → components/sheet}/sheet.vue +4 -3
  144. package/src/{vue → components/sheet}/use-find-replace.ts +1 -1
  145. package/src/{vue → components/sheet}/use-sheet-grid.ts +7 -3
  146. package/src/{vue → components/sheet}/use-sheet-state.ts +3 -3
  147. package/src/{vue → components/sheet}/use-tool-groups.ts +2 -2
  148. package/src/{vue → components/sheet}/use-tool-popup.ts +2 -2
  149. package/src/index.ts +24 -188
  150. package/src/report/__test__/binding.test.ts +218 -0
  151. package/src/report/__test__/connector.test.ts +218 -0
  152. package/src/report/__test__/export-xlsx.test.ts +118 -0
  153. package/src/report/__test__/filter-bar.test.ts +58 -0
  154. package/src/report/__test__/params.test.ts +156 -0
  155. package/src/report/__test__/render.test.ts +728 -0
  156. package/src/report/__test__/rules.test.ts +131 -0
  157. package/src/report/__test__/template.test.ts +355 -0
  158. package/src/report/binding.ts +243 -0
  159. package/src/report/connector.ts +205 -0
  160. package/src/report/export-xlsx.ts +118 -0
  161. package/src/report/filter-bar.ts +36 -0
  162. package/src/report/index.ts +13 -0
  163. package/src/report/params.ts +96 -0
  164. package/src/report/render/__test__/aggregate.test.ts +16 -0
  165. package/src/report/render/__test__/coordinate.test.ts +405 -0
  166. package/src/report/render/aggregate.ts +240 -0
  167. package/src/report/render/builder.ts +211 -0
  168. package/src/report/render/coordinate.ts +1239 -0
  169. package/src/report/render/index.ts +30 -0
  170. package/src/report/render/style-resolver.ts +68 -0
  171. package/src/report/render/template-index.ts +200 -0
  172. package/src/report/rules/evaluate.ts +113 -0
  173. package/src/report/rules/index.ts +7 -0
  174. package/src/report/template.ts +141 -0
  175. package/src/report/types.ts +96 -0
  176. package/src/tools/builtin.ts +2 -2
  177. package/src/types/index.ts +2 -6
  178. package/src/types/report-designer.ts +36 -0
  179. package/src/types/report-viewer.ts +37 -0
  180. package/src/types/sheet.ts +22 -1
  181. /package/dist/{vue → components/sheet}/style.d.ts +0 -0
  182. /package/dist/{vue → components/sheet}/style.js +0 -0
  183. /package/dist/{vue → components/sheet}/style2.css +0 -0
  184. /package/dist/{vue → components/sheet}/use-formula-suggest.js +0 -0
  185. /package/src/{vue → components/sheet}/__test__/import-file.test.ts +0 -0
  186. /package/src/{vue → components/sheet}/__test__/insert-count-menu-item.test.ts +0 -0
  187. /package/src/{vue → components/sheet}/__test__/use-formula-ref-select.test.ts +0 -0
  188. /package/src/{vue → components/sheet}/__test__/use-formula-suggest.test.ts +0 -0
  189. /package/src/{vue → components/sheet}/formula-suggest-list.vue +0 -0
  190. /package/src/{vue → components/sheet}/import-file.ts +0 -0
  191. /package/src/{vue → components/sheet}/index.ts +0 -0
  192. /package/src/{vue → components/sheet}/insert-count-menu-item.vue +0 -0
  193. /package/src/{vue → components/sheet}/parsing.ts +0 -0
  194. /package/src/{vue → components/sheet}/popups/border-preset-glyph.vue +0 -0
  195. /package/src/{vue → components/sheet}/popups/import.worker.ts +0 -0
  196. /package/src/{vue → components/sheet}/sheet-tabs.vue +0 -0
  197. /package/src/{vue → components/sheet}/style.scss +0 -0
  198. /package/src/{vue → components/sheet}/style.ts +0 -0
  199. /package/src/{vue → components/sheet}/use-formula-ref-select.ts +0 -0
  200. /package/src/{vue → components/sheet}/use-formula-suggest.ts +0 -0
  201. /package/src/{vue → components/sheet}/use-sheet-tabs-bar.ts +0 -0
  202. /package/src/{vue → components/sheet}/use-toolbar-scroll.ts +0 -0
@@ -0,0 +1,334 @@
1
+ import { Sheet, Workbook } from '@veltra/sheet-core'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { createApp, h, nextTick, type App } from 'vue'
4
+
5
+ import { UReportViewer } from '../../../index'
6
+ import { REPORT_META_NAMESPACE } from '../../../report/binding'
7
+ import type { DataConnection, DataConnector } from '../../../report/connector'
8
+ import { createReportTemplate } from '../../../report/template'
9
+ import type { ReportDatasetDef, ReportTemplate } from '../../../report/template'
10
+ import type { ParamValues, ReportBinding } from '../../../report/types'
11
+ import type { ReportViewerExposed } from '../../../types'
12
+
13
+ // ---- 内联 fixtures:stub connector(实现 DataConnector 接口的内存测试夹具)----
14
+
15
+ const MYSQL: DataConnection = {
16
+ id: 'c1',
17
+ label: 'stub MySQL',
18
+ type: 'mysql',
19
+ host: '127.0.0.1',
20
+ port: 3306,
21
+ database: 'demo',
22
+ username: 'root',
23
+ password: ''
24
+ }
25
+
26
+ // 覆盖全部 5 种参数控件类型:LIKE→text、>=→number、覆盖→select、
27
+ // BETWEEN ${a} AND ${b} 首参→date、BETWEEN 单参(SQL 末尾)→date-range
28
+ const ORDERS_SQL =
29
+ 'SELECT customer, amount FROM orders ' +
30
+ 'WHERE customer LIKE ${keyword} AND amount >= ${minAmount} AND region = ${region} ' +
31
+ 'AND orderDate BETWEEN ${dateFrom} AND ${dateTo} AND createdAt BETWEEN ${dateRange}'
32
+
33
+ const ORDERS_DATASET: ReportDatasetDef = {
34
+ id: 'orders',
35
+ label: '销售明细',
36
+ connection: MYSQL,
37
+ sql: ORDERS_SQL,
38
+ paramOverrides: {
39
+ region: {
40
+ label: '地区',
41
+ type: 'select',
42
+ defaultValue: '华东',
43
+ options: [
44
+ { label: '华东', value: '华东' },
45
+ { label: '华南', value: '华南' }
46
+ ]
47
+ }
48
+ }
49
+ }
50
+
51
+ /** 参数默认值(keyword/dateFrom/dateTo 文本 ''、minAmount 数值 0、region 覆盖默认、dateRange 元组) */
52
+ const DEFAULT_VALUES: ParamValues = {
53
+ keyword: '',
54
+ minAmount: 0,
55
+ region: '华东',
56
+ dateFrom: '',
57
+ dateTo: '',
58
+ dateRange: ['', '']
59
+ }
60
+
61
+ const ORDER_ROWS: Record<string, unknown>[] = [
62
+ { customer: '甲公司', amount: 100 },
63
+ { customer: '甲公司', amount: 200 },
64
+ { customer: '乙公司', amount: 300 }
65
+ ]
66
+
67
+ /** 带绑定的模板:表头 + 同一扩展带(客户分组 + 金额明细) */
68
+ function createViewerTemplate(datasets: ReportDatasetDef[] = [ORDERS_DATASET]): ReportTemplate {
69
+ const sheet = new Sheet()
70
+ sheet.setCells([
71
+ { addr: { row: 0, col: 0 }, data: { v: '客户' } },
72
+ { addr: { row: 0, col: 1 }, data: { v: '金额' } }
73
+ ])
74
+
75
+ const group: ReportBinding = {
76
+ dataset: 'orders',
77
+ field: 'customer',
78
+ preset: 'groupHeader',
79
+ aggregate: 'group',
80
+ expand: 'down'
81
+ }
82
+ sheet.setCellMeta({ row: 1, col: 0 }, REPORT_META_NAMESPACE, group)
83
+
84
+ const amount: ReportBinding = {
85
+ dataset: 'orders',
86
+ field: 'amount',
87
+ preset: 'detail',
88
+ aggregate: 'list',
89
+ expand: 'down',
90
+ rowParent: { row: 1, col: 0 }
91
+ }
92
+ sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, amount)
93
+
94
+ return createReportTemplate(sheet.snapshot(), datasets)
95
+ }
96
+
97
+ interface QueryCall {
98
+ connection: DataConnection
99
+ sql: string
100
+ values: ParamValues
101
+ }
102
+
103
+ type QueryOutcome =
104
+ | { rows: Record<string, unknown>[] }
105
+ | { error: { code: string; message: string } }
106
+
107
+ /** stub connector:记录 query 调用;outcome 可为立即值或手动闸门(loading 测试) */
108
+ function createStubConnector(outcome: QueryOutcome | (() => Promise<QueryOutcome>)) {
109
+ const calls: QueryCall[] = []
110
+ const connector: DataConnector = {
111
+ test: () => Promise.resolve({ ok: true, data: undefined }),
112
+ describe: () => Promise.resolve({ ok: true, data: [] }),
113
+ query: async (connection, sql, values) => {
114
+ calls.push({ connection, sql, values })
115
+ const resolved = typeof outcome === 'function' ? await outcome() : outcome
116
+ if ('error' in resolved) return { ok: false, error: resolved.error }
117
+ return { ok: true, data: { fields: [], rows: resolved.rows } }
118
+ }
119
+ }
120
+ return { connector, calls }
121
+ }
122
+
123
+ function deferred<T>() {
124
+ let resolve!: (value: T | PromiseLike<T>) => void
125
+ const promise = new Promise<T>((r) => {
126
+ resolve = r
127
+ })
128
+ return { promise, resolve }
129
+ }
130
+
131
+ // ---- 挂载与等待 ----
132
+
133
+ const apps: App[] = []
134
+ const containers: HTMLElement[] = []
135
+
136
+ function mountViewer(props: {
137
+ connector: DataConnector
138
+ template: ReportTemplate
139
+ workbook?: Workbook
140
+ }) {
141
+ const el = document.createElement('div')
142
+ el.style.width = '800px'
143
+ el.style.height = '600px'
144
+ document.body.appendChild(el)
145
+ containers.push(el)
146
+ const exposedRef = { value: undefined as ReportViewerExposed | undefined }
147
+ const app = createApp({
148
+ render: () =>
149
+ h(UReportViewer, {
150
+ ...props,
151
+ ref: (value: unknown) => {
152
+ exposedRef.value = value as ReportViewerExposed | undefined
153
+ }
154
+ })
155
+ })
156
+ app.mount(el)
157
+ apps.push(app)
158
+ return { app, el, exposedRef }
159
+ }
160
+
161
+ /** 等待取数(宏/微任务)+ 填充快照应用(watch + nextTick)落定 */
162
+ async function flushViewer(): Promise<void> {
163
+ await nextTick()
164
+ await new Promise((resolve) => setTimeout(resolve, 0))
165
+ await nextTick()
166
+ await nextTick()
167
+ }
168
+
169
+ afterEach(() => {
170
+ while (apps.length) apps.pop()!.unmount()
171
+ while (containers.length) containers.pop()!.remove()
172
+ })
173
+
174
+ describe('UReportViewer', () => {
175
+ it('全流程:带绑定的模板 + stub connector → 参数提取 → Filter Bar → 取数 → 展开渲染', async () => {
176
+ const { connector, calls } = createStubConnector({ rows: ORDER_ROWS })
177
+ const workbook = new Workbook()
178
+ const { el } = mountViewer({ connector, template: createViewerTemplate(), workbook })
179
+ await flushViewer()
180
+
181
+ // 经连接器取数:连接对象 / SQL / 参数默认值原样透传
182
+ expect(calls).toHaveLength(1)
183
+ expect(calls[0]).toEqual({ connection: MYSQL, sql: ORDERS_SQL, values: DEFAULT_VALUES })
184
+
185
+ // Filter Bar 按参数类型映射控件(text/number/select/date/date-range)
186
+ const filterBar = el.querySelector('.u-report-filter-bar')!
187
+ expect(filterBar).toBeTruthy()
188
+ expect(filterBar.querySelector('.u-input')).toBeTruthy()
189
+ expect(filterBar.querySelector('.u-number-input')).toBeTruthy()
190
+ expect(filterBar.querySelector('.u-select')).toBeTruthy()
191
+ expect(filterBar.querySelector('.u-date-picker')).toBeTruthy()
192
+ expect(filterBar.querySelector('.u-date-range-picker')).toBeTruthy()
193
+ // 覆盖元数据的 label 生效
194
+ expect(filterBar.textContent).toContain('地区')
195
+
196
+ // 展开结果落到只读网格(模型断言):表头 + 甲(2 明细) + 乙(1 明细)
197
+ const sheet = workbook.activeSheet
198
+ expect(sheet.getDisplayValue({ row: 0, col: 0 })).toBe('客户')
199
+ expect(sheet.getDisplayValue({ row: 1, col: 0 })).toBe('甲公司')
200
+ expect(sheet.getCellData({ row: 1, col: 1 })?.v).toBe(100)
201
+ expect(sheet.getCellData({ row: 2, col: 1 })?.v).toBe(200)
202
+ expect(sheet.getDisplayValue({ row: 3, col: 0 })).toBe('乙公司')
203
+ expect(sheet.getCellData({ row: 3, col: 1 })?.v).toBe(300)
204
+ })
205
+
206
+ it('Filter Bar 改值重新取数,其余参数值保留', async () => {
207
+ const { connector, calls } = createStubConnector({ rows: ORDER_ROWS })
208
+ const workbook = new Workbook()
209
+ const { el } = mountViewer({ connector, template: createViewerTemplate(), workbook })
210
+ await flushViewer()
211
+ expect(calls).toHaveLength(1)
212
+
213
+ // keyword(text 控件)输入新值
214
+ const input = el.querySelector<HTMLInputElement>('.u-report-filter-bar .u-input input')!
215
+ input.value = '甲公司'
216
+ input.dispatchEvent(
217
+ new InputEvent('input', { bubbles: true, data: '甲公司', inputType: 'insertText' })
218
+ )
219
+ await flushViewer()
220
+
221
+ expect(calls).toHaveLength(2)
222
+ expect(calls[1]!.values).toEqual({ ...DEFAULT_VALUES, keyword: '甲公司' })
223
+
224
+ // minAmount(number 控件)输入新值
225
+ const numberInput = el.querySelector<HTMLInputElement>(
226
+ '.u-report-filter-bar .u-number-input input'
227
+ )!
228
+ numberInput.value = '500'
229
+ numberInput.dispatchEvent(
230
+ new InputEvent('input', { bubbles: true, data: '500', inputType: 'insertText' })
231
+ )
232
+ await flushViewer()
233
+
234
+ expect(calls).toHaveLength(3)
235
+ expect(calls[2]!.values).toEqual({ ...DEFAULT_VALUES, keyword: '甲公司', minAmount: 500 })
236
+ })
237
+
238
+ it('取数中有 loading;业务错误(ok:false)有可读提示;refresh() 触发重新取数', async () => {
239
+ let gate = deferred<QueryOutcome>()
240
+ // 首次取数返回业务错误,refresh 后返回数据
241
+ let outcome: QueryOutcome = { error: { code: 'CONNECTION_FAILED', message: '数据库连接失败' } }
242
+ const { connector, calls } = createStubConnector(() => gate.promise)
243
+ const workbook = new Workbook()
244
+ const { el, exposedRef } = mountViewer({
245
+ connector,
246
+ template: createViewerTemplate(),
247
+ workbook
248
+ })
249
+ await nextTick()
250
+
251
+ // 取数中:loading 遮罩可见
252
+ expect(el.querySelector('.u-report-viewer__loading')).toBeTruthy()
253
+
254
+ // 业务错误:可读错误提示可见,网格保留模板静态结构
255
+ gate.resolve(outcome)
256
+ await flushViewer()
257
+ expect(el.querySelector('.u-report-viewer__loading')).toBeNull()
258
+ const errorBanner = el.querySelector('.u-report-viewer__error')!
259
+ expect(errorBanner.textContent).toContain('数据库连接失败')
260
+ expect(workbook.activeSheet.getDisplayValue({ row: 0, col: 0 })).toBe('客户')
261
+ expect(workbook.activeSheet.getDisplayValue({ row: 1, col: 0 })).toBeUndefined()
262
+
263
+ // refresh() 重新取数:成功后错误消失、展开结果渲染
264
+ outcome = { rows: ORDER_ROWS }
265
+ gate = deferred<QueryOutcome>()
266
+ const refreshed = exposedRef.value!.refresh()
267
+ expect(calls).toHaveLength(2)
268
+ await nextTick()
269
+ expect(el.querySelector('.u-report-viewer__loading')).toBeTruthy()
270
+ gate.resolve(outcome)
271
+ await refreshed
272
+ await flushViewer()
273
+ expect(el.querySelector('.u-report-viewer__error')).toBeNull()
274
+ expect(workbook.activeSheet.getDisplayValue({ row: 1, col: 0 })).toBe('甲公司')
275
+ expect(workbook.activeSheet.getCellData({ row: 3, col: 1 })?.v).toBe(300)
276
+ })
277
+
278
+ it('模板无查询参数时不渲染 Filter Bar', async () => {
279
+ const dataset: ReportDatasetDef = {
280
+ id: 'orders',
281
+ label: '销售明细',
282
+ connection: MYSQL,
283
+ sql: 'SELECT customer, amount FROM orders'
284
+ }
285
+ const { connector } = createStubConnector({ rows: ORDER_ROWS })
286
+ const { el } = mountViewer({
287
+ connector,
288
+ template: createViewerTemplate([dataset]),
289
+ workbook: new Workbook()
290
+ })
291
+ await flushViewer()
292
+ expect(el.querySelector('.u-report-filter-bar')).toBeNull()
293
+ })
294
+
295
+ it('取数前 exportXlsx() 拒绝;取数失败或未就绪时同样拒绝', async () => {
296
+ let gate = deferred<QueryOutcome>()
297
+ let outcome: QueryOutcome = { error: { code: 'CONNECTION_FAILED', message: '数据库连接失败' } }
298
+ const { connector } = createStubConnector(() => gate.promise)
299
+ const { exposedRef } = mountViewer({
300
+ connector,
301
+ template: createViewerTemplate(),
302
+ workbook: new Workbook()
303
+ })
304
+ await nextTick()
305
+
306
+ await expect(exposedRef.value!.exportXlsx()).rejects.toThrow('报表数据加载中')
307
+
308
+ gate.resolve(outcome)
309
+ await flushViewer()
310
+ await expect(exposedRef.value!.exportXlsx()).rejects.toThrow('报表数据尚未就绪')
311
+ })
312
+
313
+ it('取数后 exportXlsx() 成功产出 blob', async () => {
314
+ const { connector } = createStubConnector({ rows: ORDER_ROWS })
315
+ const workbook = new Workbook()
316
+ const { exposedRef } = mountViewer({ connector, template: createViewerTemplate(), workbook })
317
+ await flushViewer()
318
+
319
+ const exportModule = await import('../../../report/export-xlsx')
320
+ const catKitFe = await import('@cat-kit/fe')
321
+ const exportSpy = vi
322
+ .spyOn(exportModule, 'exportFilledReportXlsx')
323
+ .mockResolvedValue(new Uint8Array([80, 75, 3, 4]))
324
+ const saveBlobSpy = vi.spyOn(catKitFe, 'saveBlob').mockImplementation(() => {})
325
+
326
+ await exposedRef.value!.exportXlsx()
327
+
328
+ expect(exportSpy).toHaveBeenCalled()
329
+ expect(saveBlobSpy).toHaveBeenCalledWith(expect.any(Blob), expect.stringMatching(/\.xlsx$/))
330
+
331
+ exportSpy.mockRestore()
332
+ saveBlobSpy.mockRestore()
333
+ })
334
+ })
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { createReportBinding } from '../../../report/binding'
4
+ import type { DatasetCatalogItem, ReportBinding } from '../../../report/types'
5
+ import {
6
+ buildTopologyArcPath,
7
+ collectTopologyLinks,
8
+ findCellsWithColParent,
9
+ findCellsWithRowParent
10
+ } from '../designer/topology'
11
+
12
+ // ---- 内联 fixtures ----
13
+
14
+ const ORDERS_DATASET: DatasetCatalogItem = {
15
+ id: 'orders',
16
+ label: '销售明细',
17
+ fields: [
18
+ { name: 'customer', label: '客户', type: 'string' },
19
+ { name: 'orderNo', label: '订单号', type: 'string' },
20
+ { name: 'region', label: '地区', type: 'string' },
21
+ { name: 'amount', label: '金额', type: 'number' }
22
+ ]
23
+ }
24
+
25
+ function bindingMap(cells: Array<{ addr: { row: number; col: number }; binding: ReportBinding }>) {
26
+ const map = new Map<string, ReportBinding>()
27
+ for (const { addr, binding } of cells) {
28
+ map.set(`${addr.row},${addr.col}`, binding)
29
+ }
30
+ return (addr: { row: number; col: number }) => map.get(`${addr.row},${addr.col}`)
31
+ }
32
+
33
+ describe('report topology', () => {
34
+ it('collectTopologyLinks 沿 rowParent 链上行并包含直接子格', () => {
35
+ const parentAddr = { row: 1, col: 0 }
36
+ const parent = createReportBinding(ORDERS_DATASET, 'customer')
37
+ parent.aggregate = 'group'
38
+ parent.preset = 'groupHeader'
39
+
40
+ const mid = createReportBinding(ORDERS_DATASET, 'orderNo')
41
+ mid.rowParent = parentAddr
42
+
43
+ const detail = createReportBinding(ORDERS_DATASET, 'region')
44
+ detail.rowParent = { row: 1, col: 1 }
45
+
46
+ const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
47
+ subtotal.aggregate = 'sum'
48
+ subtotal.expand = 'none'
49
+ subtotal.rowParent = parentAddr
50
+
51
+ const entries = [
52
+ { addr: parentAddr, binding: parent },
53
+ { addr: { row: 1, col: 1 }, binding: mid },
54
+ { addr: { row: 1, col: 2 }, binding: detail },
55
+ { addr: { row: 2, col: 3 }, binding: subtotal }
56
+ ]
57
+ const getBindingAt = bindingMap(entries)
58
+
59
+ expect(collectTopologyLinks({ row: 1, col: 2 }, detail, entries, getBindingAt)).toEqual([
60
+ { from: { row: 1, col: 2 }, to: { row: 1, col: 1 }, direction: 'row' },
61
+ { from: { row: 1, col: 1 }, to: parentAddr, direction: 'row' }
62
+ ])
63
+
64
+ expect(collectTopologyLinks(parentAddr, parent, entries, getBindingAt)).toEqual([
65
+ { from: { row: 1, col: 1 }, to: parentAddr, direction: 'row' },
66
+ { from: { row: 2, col: 3 }, to: parentAddr, direction: 'row' }
67
+ ])
68
+ })
69
+
70
+ it('collectTopologyLinks 沿 colParent 链上行并包含列方向子格', () => {
71
+ const rowParentAddr = { row: 1, col: 0 }
72
+ const colParentAddr = { row: 0, col: 1 }
73
+
74
+ const rowGroup = createReportBinding(ORDERS_DATASET, 'customer')
75
+ rowGroup.aggregate = 'group'
76
+ rowGroup.expand = 'down'
77
+
78
+ const colGroup = createReportBinding(ORDERS_DATASET, 'region')
79
+ colGroup.aggregate = 'group'
80
+ colGroup.expand = 'right'
81
+
82
+ const cross = createReportBinding(ORDERS_DATASET, 'amount')
83
+ cross.aggregate = 'sum'
84
+ cross.expand = 'none'
85
+ cross.rowParent = rowParentAddr
86
+ cross.colParent = colParentAddr
87
+
88
+ const entries = [
89
+ { addr: rowParentAddr, binding: rowGroup },
90
+ { addr: colParentAddr, binding: colGroup },
91
+ { addr: { row: 1, col: 2 }, binding: cross }
92
+ ]
93
+ const getBindingAt = bindingMap(entries)
94
+
95
+ expect(collectTopologyLinks({ row: 1, col: 2 }, cross, entries, getBindingAt)).toEqual([
96
+ { from: { row: 1, col: 2 }, to: rowParentAddr, direction: 'row' },
97
+ { from: { row: 1, col: 2 }, to: colParentAddr, direction: 'col' }
98
+ ])
99
+ })
100
+
101
+ it('findCellsWithColParent 仅返回 colParent 指向目标父格的子格', () => {
102
+ const parentAddr = { row: 0, col: 1 }
103
+ const parent = createReportBinding(ORDERS_DATASET, 'region')
104
+ parent.aggregate = 'group'
105
+ parent.expand = 'right'
106
+
107
+ const child = createReportBinding(ORDERS_DATASET, 'amount')
108
+ child.colParent = parentAddr
109
+
110
+ const entries = [
111
+ { addr: parentAddr, binding: parent },
112
+ { addr: { row: 1, col: 2 }, binding: child }
113
+ ]
114
+
115
+ expect(findCellsWithColParent(parentAddr, entries)).toEqual([{ row: 1, col: 2 }])
116
+ })
117
+
118
+ it('findCellsWithRowParent 仅返回 rowParent 指向目标父格的子格', () => {
119
+ const parentAddr = { row: 1, col: 0 }
120
+ const parent = createReportBinding(ORDERS_DATASET, 'customer')
121
+ parent.aggregate = 'group'
122
+ parent.preset = 'groupHeader'
123
+
124
+ const child = createReportBinding(ORDERS_DATASET, 'orderNo')
125
+ child.rowParent = parentAddr
126
+
127
+ const other = createReportBinding(ORDERS_DATASET, 'region')
128
+
129
+ const entries = [
130
+ { addr: parentAddr, binding: parent },
131
+ { addr: { row: 1, col: 1 }, binding: child },
132
+ { addr: { row: 1, col: 2 }, binding: other }
133
+ ]
134
+
135
+ expect(findCellsWithRowParent(parentAddr, entries)).toEqual([{ row: 1, col: 1 }])
136
+ })
137
+
138
+ it('buildTopologyArcPath 输出合法 SVG 路径', () => {
139
+ const path = buildTopologyArcPath({ x: 10, y: 20 }, { x: 100, y: 40 })
140
+ expect(path.startsWith('M 10 20 Q')).toBe(true)
141
+ expect(path.endsWith('100 40')).toBe(true)
142
+ })
143
+ })