@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
@@ -76,14 +76,71 @@ export function isVerticalExpandBinding(binding: ReportBinding): boolean {
76
76
  }
77
77
 
78
78
  export function isHorizontalExpandBinding(binding: ReportBinding): boolean {
79
+ return (
80
+ binding.expand === 'right' && (binding.aggregate === 'group' || binding.aggregate === 'list')
81
+ )
82
+ }
83
+
84
+ export function isRowGroupBinding(binding: ReportBinding): boolean {
85
+ return binding.expand === 'down' && binding.aggregate === 'group'
86
+ }
87
+
88
+ export function isColGroupBinding(binding: ReportBinding): boolean {
79
89
  return binding.expand === 'right' && binding.aggregate === 'group'
80
90
  }
81
91
 
92
+ export function isReductionAggregate(aggregate: ReportAggregate): boolean {
93
+ return (
94
+ aggregate === 'sum' ||
95
+ aggregate === 'avg' ||
96
+ aggregate === 'count' ||
97
+ aggregate === 'max' ||
98
+ aggregate === 'min'
99
+ )
100
+ }
101
+
102
+ /** 小计 / 汇总 / 交叉格的父格候选只列分组头 */
103
+ export function prefersGroupParent(binding: ReportBinding): boolean {
104
+ if (
105
+ binding.preset === 'subtotal' ||
106
+ binding.preset === 'grandTotal' ||
107
+ binding.preset === 'cross'
108
+ ) {
109
+ return true
110
+ }
111
+ return binding.expand === 'none' && isReductionAggregate(binding.aggregate)
112
+ }
113
+
114
+ /** 当前 expand / aggregate 是否仍属于已写入的预设族(同族内改聚合不掉到「自定义」) */
115
+ export function presetStillApplies(binding: ReportBinding): boolean {
116
+ const preset = binding.preset
117
+ if (!preset) return false
118
+ if (preset === 'groupHeader') {
119
+ return (
120
+ binding.aggregate === 'group' && (binding.expand === 'down' || binding.expand === 'right')
121
+ )
122
+ }
123
+ if (preset === 'detail') {
124
+ return binding.aggregate === 'list' && (binding.expand === 'down' || binding.expand === 'right')
125
+ }
126
+ return binding.expand === 'none' && isReductionAggregate(binding.aggregate)
127
+ }
128
+
129
+ const DEFAULT_SCAN_COLS = 64
130
+ const DEFAULT_SCAN_ROWS = 64
131
+
132
+ type GetBindingAt = (addr: CellAddress) => ReportBinding | undefined
133
+
134
+ export interface InferParentOptions {
135
+ /** 小计落格:必须优先分组头,禁止把同列/同行明细当成父格 */
136
+ preferGroup?: boolean
137
+ }
138
+
82
139
  /** 指定模板行是否含纵向扩展绑定(展开带行) */
83
140
  export function isExpansionBandRow(
84
141
  row: number,
85
- getBindingAt: (addr: CellAddress) => ReportBinding | undefined,
86
- scanCols = 64
142
+ getBindingAt: GetBindingAt,
143
+ scanCols = DEFAULT_SCAN_COLS
87
144
  ): boolean {
88
145
  for (let col = 0; col < scanCols; col++) {
89
146
  const binding = getBindingAt({ row, col })
@@ -92,27 +149,50 @@ export function isExpansionBandRow(
92
149
  return false
93
150
  }
94
151
 
95
- /** 落格时推断预设:默认明细;数值字段且位于展开带正下方相邻行 → 小计 */
152
+ /** 指定模板列是否含横向扩展绑定(展开带列) */
153
+ export function isExpansionBandCol(
154
+ col: number,
155
+ getBindingAt: GetBindingAt,
156
+ scanRows = DEFAULT_SCAN_ROWS
157
+ ): boolean {
158
+ for (let row = 0; row < scanRows; row++) {
159
+ const binding = getBindingAt({ row, col })
160
+ if (binding && isHorizontalExpandBinding(binding)) return true
161
+ }
162
+ return false
163
+ }
164
+
165
+ /** 落格时推断预设:默认明细;数值字段且位于展开带正下方或正右相邻 → 小计 */
96
166
  export function inferDropPreset(
97
167
  addr: CellAddress,
98
168
  fieldType: DatasetCatalogItem['fields'][number]['type'],
99
- getBindingAt: (addr: CellAddress) => ReportBinding | undefined
169
+ getBindingAt: GetBindingAt
100
170
  ): ReportPreset {
101
- if (fieldType === 'number' && addr.row > 0 && isExpansionBandRow(addr.row - 1, getBindingAt)) {
102
- return 'subtotal'
103
- }
171
+ if (fieldType !== 'number') return 'detail'
172
+ if (addr.row > 0 && isExpansionBandRow(addr.row - 1, getBindingAt)) return 'subtotal'
173
+ if (addr.col > 0 && isExpansionBandCol(addr.col - 1, getBindingAt)) return 'subtotal'
104
174
  return 'detail'
105
175
  }
106
176
 
177
+ export interface ListParentCandidateOptions {
178
+ preferGroup?: boolean
179
+ }
180
+
107
181
  /** 同数据集纵向扩展绑定格(行方向父格下拉候选) */
108
182
  export function listRowParentCandidates(
109
183
  binding: ReportBinding,
110
- entries: ReadonlyArray<{ addr: CellAddress; binding: ReportBinding }>
184
+ entries: ReadonlyArray<{ addr: CellAddress; binding: ReportBinding }>,
185
+ options?: ListParentCandidateOptions
111
186
  ): CellAddress[] {
187
+ const preferGroup = options?.preferGroup === true
112
188
  const candidates: CellAddress[] = []
113
189
  for (const entry of entries) {
114
190
  if (entry.binding.dataset !== binding.dataset) continue
115
- if (!isVerticalExpandBinding(entry.binding)) continue
191
+ if (preferGroup) {
192
+ if (!isRowGroupBinding(entry.binding)) continue
193
+ } else if (!isVerticalExpandBinding(entry.binding)) {
194
+ continue
195
+ }
116
196
  candidates.push(entry.addr)
117
197
  }
118
198
  return candidates
@@ -121,73 +201,204 @@ export function listRowParentCandidates(
121
201
  /** 同数据集横向扩展绑定格(列方向父格下拉候选) */
122
202
  export function listColParentCandidates(
123
203
  binding: ReportBinding,
124
- entries: ReadonlyArray<{ addr: CellAddress; binding: ReportBinding }>
204
+ entries: ReadonlyArray<{ addr: CellAddress; binding: ReportBinding }>,
205
+ options?: ListParentCandidateOptions
125
206
  ): CellAddress[] {
207
+ const preferGroup = options?.preferGroup === true
126
208
  const candidates: CellAddress[] = []
127
209
  for (const entry of entries) {
128
210
  if (entry.binding.dataset !== binding.dataset) continue
129
- if (!isHorizontalExpandBinding(entry.binding)) continue
211
+ if (preferGroup) {
212
+ if (!isColGroupBinding(entry.binding)) continue
213
+ } else if (!isHorizontalExpandBinding(entry.binding)) {
214
+ continue
215
+ }
130
216
  candidates.push(entry.addr)
131
217
  }
132
218
  return candidates
133
219
  }
134
220
 
135
- /** 同列向上找最近的纵向扩展绑定,作为 rowParent 候选 */
221
+ interface ScanHit {
222
+ addr: CellAddress
223
+ group: boolean
224
+ distance: number
225
+ }
226
+
227
+ function betterHit(current: ScanHit | null, next: ScanHit): boolean {
228
+ if (!current) return true
229
+ if (next.distance !== current.distance) return next.distance < current.distance
230
+ return next.addr.col + next.addr.row < current.addr.col + current.addr.row
231
+ }
232
+
233
+ function pickExpandHit(hits: ScanHit[], preferGroup: boolean): CellAddress | null {
234
+ let group: ScanHit | null = null
235
+ let expand: ScanHit | null = null
236
+ for (const hit of hits) {
237
+ if (hit.group && betterHit(group, hit)) group = hit
238
+ if (betterHit(expand, hit)) expand = hit
239
+ }
240
+ if (preferGroup) return group?.addr ?? null
241
+ return group?.addr ?? expand?.addr ?? null
242
+ }
243
+
244
+ function scanVerticalOnRow(
245
+ row: number,
246
+ originCol: number,
247
+ colStart: number,
248
+ colEnd: number,
249
+ getBindingAt: GetBindingAt
250
+ ): ScanHit[] {
251
+ const hits: ScanHit[] = []
252
+ for (let col = colStart; col < colEnd; col++) {
253
+ const binding = getBindingAt({ row, col })
254
+ if (!binding || !isVerticalExpandBinding(binding)) continue
255
+ hits.push({
256
+ addr: { row, col },
257
+ group: isRowGroupBinding(binding),
258
+ distance: Math.abs(col - originCol)
259
+ })
260
+ }
261
+ return hits
262
+ }
263
+
264
+ function scanHorizontalOnCol(
265
+ col: number,
266
+ originRow: number,
267
+ rowStart: number,
268
+ rowEnd: number,
269
+ getBindingAt: GetBindingAt
270
+ ): ScanHit[] {
271
+ const hits: ScanHit[] = []
272
+ for (let row = rowStart; row < rowEnd; row++) {
273
+ const binding = getBindingAt({ row, col })
274
+ if (!binding || !isHorizontalExpandBinding(binding)) continue
275
+ hits.push({
276
+ addr: { row, col },
277
+ group: isColGroupBinding(binding),
278
+ distance: Math.abs(row - originRow)
279
+ })
280
+ }
281
+ return hits
282
+ }
283
+
284
+ /**
285
+ * 行方向父格推断:
286
+ * 1. 同行向左:先找分组头,否则最近纵向扩展格
287
+ * 2. 上方各行整行扫描:每行先找分组头,再退回任意纵向扩展格
288
+ * 小计(preferGroup)禁止把明细当成父格
289
+ */
136
290
  export function inferRowParentCandidate(
137
291
  addr: CellAddress,
138
- getBindingAt: (addr: CellAddress) => ReportBinding | undefined
292
+ getBindingAt: GetBindingAt,
293
+ options?: InferParentOptions
139
294
  ): CellAddress | null {
295
+ const preferGroup = options?.preferGroup === true
296
+
297
+ const sameRow = pickExpandHit(
298
+ scanVerticalOnRow(addr.row, addr.col, 0, addr.col, getBindingAt),
299
+ preferGroup
300
+ )
301
+ if (sameRow) return sameRow
302
+
140
303
  for (let row = addr.row - 1; row >= 0; row--) {
141
- const binding = getBindingAt({ row, col: addr.col })
142
- if (binding && isVerticalExpandBinding(binding)) {
143
- return { row, col: addr.col }
144
- }
304
+ const found = pickExpandHit(
305
+ scanVerticalOnRow(row, addr.col, 0, DEFAULT_SCAN_COLS, getBindingAt),
306
+ preferGroup
307
+ )
308
+ if (found) return found
145
309
  }
146
310
  return null
147
311
  }
148
312
 
149
- /** 同行向左找最近的横向扩展绑定,作为 colParent 候选 */
313
+ /**
314
+ * 列方向父格推断(与行方向对称):
315
+ * 1. 同列向上:先找列分组头,否则最近横向扩展格
316
+ * 2. 左侧各列整列扫描:每列先找列分组头
317
+ * 列小计(preferGroup)禁止把交叉格/明细当成列父格
318
+ */
150
319
  export function inferColParentCandidate(
151
320
  addr: CellAddress,
152
- getBindingAt: (addr: CellAddress) => ReportBinding | undefined
321
+ getBindingAt: GetBindingAt,
322
+ options?: InferParentOptions
153
323
  ): CellAddress | null {
324
+ const preferGroup = options?.preferGroup === true
325
+
326
+ const sameCol = pickExpandHit(
327
+ scanHorizontalOnCol(addr.col, addr.row, 0, addr.row, getBindingAt),
328
+ preferGroup
329
+ )
330
+ if (sameCol) return sameCol
331
+
154
332
  for (let col = addr.col - 1; col >= 0; col--) {
155
- const binding = getBindingAt({ row: addr.row, col })
156
- if (binding && isHorizontalExpandBinding(binding)) {
157
- return { row: addr.row, col }
158
- }
333
+ const found = pickExpandHit(
334
+ scanHorizontalOnCol(col, addr.row, 0, DEFAULT_SCAN_ROWS, getBindingAt),
335
+ preferGroup
336
+ )
337
+ if (found) return found
159
338
  }
160
339
  return null
161
340
  }
162
341
 
342
+ function suggestsColumnSubtotal(above: ReportBinding | undefined): boolean {
343
+ if (!above) return false
344
+ return isHorizontalExpandBinding(above) || !!above.colParent || above.preset === 'cross'
345
+ }
346
+
347
+ function suggestsRowSubtotal(left: ReportBinding | undefined): boolean {
348
+ if (!left) return false
349
+ return isVerticalExpandBinding(left) || !!left.rowParent || left.preset === 'cross'
350
+ }
351
+
352
+ /**
353
+ * 按预设写入落格父格:明细/分组/交叉双方向推断;小计按「下方行小计 / 右侧列小计」只挂对应轴。
354
+ */
355
+ export function inferParentsForPreset(
356
+ addr: CellAddress,
357
+ preset: ReportPreset,
358
+ getBindingAt: GetBindingAt
359
+ ): Pick<ReportBinding, 'rowParent' | 'colParent'> {
360
+ if (preset === 'grandTotal') return {}
361
+
362
+ const preferGroup = preset === 'subtotal' || preset === 'cross'
363
+ const rowParent = inferRowParentCandidate(addr, getBindingAt, { preferGroup }) ?? undefined
364
+ const colParent = inferColParentCandidate(addr, getBindingAt, { preferGroup }) ?? undefined
365
+
366
+ if (preset !== 'subtotal') {
367
+ return { ...(rowParent ? { rowParent } : {}), ...(colParent ? { colParent } : {}) }
368
+ }
369
+
370
+ const above = addr.row > 0 ? getBindingAt({ row: addr.row - 1, col: addr.col }) : undefined
371
+ const left = addr.col > 0 ? getBindingAt({ row: addr.row, col: addr.col - 1 }) : undefined
372
+ const belowBand = addr.row > 0 && isExpansionBandRow(addr.row - 1, getBindingAt)
373
+ const rightOfBand = addr.col > 0 && isExpansionBandCol(addr.col - 1, getBindingAt)
374
+
375
+ const columnSubtotal =
376
+ suggestsColumnSubtotal(above) || (rightOfBand && !suggestsRowSubtotal(left))
377
+ const rowSubtotal = suggestsRowSubtotal(left) || (belowBand && !columnSubtotal)
378
+
379
+ const result: Pick<ReportBinding, 'rowParent' | 'colParent'> = {}
380
+ if (rowSubtotal && rowParent) result.rowParent = rowParent
381
+ if (columnSubtotal && colParent) result.colParent = colParent
382
+ if (!rowSubtotal && !columnSubtotal) {
383
+ if (belowBand && rowParent) result.rowParent = rowParent
384
+ else if (rightOfBand && colParent) result.colParent = colParent
385
+ else {
386
+ if (rowParent) result.rowParent = rowParent
387
+ if (colParent) result.colParent = colParent
388
+ }
389
+ }
390
+ return result
391
+ }
392
+
163
393
  /** 从绑定字段推断预设;不匹配任何预设时返回 null(自定义) */
164
394
  export function inferReportPreset(binding: ReportBinding): ReportPreset | null {
165
395
  if (binding.preset) return binding.preset
166
396
 
167
- if (
168
- binding.expand === 'none' &&
169
- binding.aggregate === 'sum' &&
170
- binding.rowParent &&
171
- binding.colParent
172
- ) {
173
- return 'cross'
174
- }
175
- if (
176
- binding.expand === 'none' &&
177
- binding.aggregate === 'sum' &&
178
- !binding.rowParent &&
179
- !binding.colParent
180
- ) {
397
+ if (binding.expand === 'none' && isReductionAggregate(binding.aggregate)) {
398
+ if (binding.rowParent && binding.colParent) return 'cross'
399
+ if (binding.rowParent || binding.colParent) return 'subtotal'
181
400
  return 'grandTotal'
182
401
  }
183
- if (
184
- binding.expand === 'none' &&
185
- binding.aggregate === 'sum' &&
186
- binding.rowParent &&
187
- !binding.colParent
188
- ) {
189
- return 'subtotal'
190
- }
191
402
  if (binding.aggregate === 'group' && (binding.expand === 'down' || binding.expand === 'right')) {
192
403
  return 'groupHeader'
193
404
  }
@@ -233,11 +444,31 @@ function resolveFieldLabel(
233
444
  return field?.label ?? fieldName
234
445
  }
235
446
 
447
+ /** Binding Placeholder 分段文案(聚合标签 + 字段标签),供徽章分色渲染等场景使用 */
448
+ export function formatBindingPlaceholderParts(
449
+ binding: ReportBinding,
450
+ resolveLabel?: (datasetId: string, fieldName: string) => string
451
+ ): { tag: string; label: string } {
452
+ return {
453
+ tag: AGGREGATE_PLACEHOLDER_TAG[binding.aggregate],
454
+ label: resolveFieldLabel(binding.dataset, binding.field, resolveLabel)
455
+ }
456
+ }
457
+
236
458
  /** Binding Placeholder 中文可读文案(如「分组 · 客户」) */
237
459
  export function formatBindingPlaceholder(
238
460
  binding: ReportBinding,
239
461
  resolveLabel?: (datasetId: string, fieldName: string) => string
240
462
  ): string {
241
- const tag = AGGREGATE_PLACEHOLDER_TAG[binding.aggregate]
242
- return `${tag} · ${resolveFieldLabel(binding.dataset, binding.field, resolveLabel)}`
463
+ const { tag, label } = formatBindingPlaceholderParts(binding, resolveLabel)
464
+ return `${tag} · ${label}`
465
+ }
466
+
467
+ /** 父格下拉候选文案(如「分组 · 客户 (A2)」) */
468
+ export function formatParentCandidateLabel(
469
+ addr: CellAddress,
470
+ binding: ReportBinding,
471
+ resolveLabel?: (datasetId: string, fieldName: string) => string
472
+ ): string {
473
+ return `${formatBindingPlaceholder(binding, resolveLabel)} (${formatCellAddress(addr)})`
243
474
  }
@@ -73,9 +73,10 @@ export interface CreateHttpConnectorOptions {
73
73
  // ---- HTTP 契约响应形状(ADR-0003 决策 3)----
74
74
  // 传输层错误用 HTTP 状态码;业务错误一律 `200 + { ok: false, error }`。
75
75
 
76
- /** 契约字段 schema:`{ name, type? }`(type 缺省视为 string) */
76
+ /** 契约字段 schema:`{ name, label?, type? }`(label 缺省视为 name,type 缺省视为 string) */
77
77
  interface ContractField {
78
78
  name: string
79
+ label?: string
79
80
  type?: DatasetField['type']
80
81
  }
81
82
 
@@ -113,7 +114,7 @@ function toConnectorError(value: unknown): ConnectorError {
113
114
 
114
115
  /** 契约字段 → 内核 DatasetField(label 缺省回退 name,type 缺省 string) */
115
116
  function normalizeField(field: ContractField): DatasetField {
116
- return { name: field.name, label: field.name, type: field.type ?? 'string' }
117
+ return { name: field.name, label: field.label ?? field.name, type: field.type ?? 'string' }
117
118
  }
118
119
 
119
120
  /** 校验并归一化契约 fields;形状不合法返回 null */
@@ -3,7 +3,7 @@ import { rangeToHucre, styleToHucre } from '@veltra/sheet-core/core/io/export'
3
3
  import type { Cell, CellValue, ColumnDef, RowDef, WriteSheet } from 'hucre'
4
4
  import { writeXlsx } from 'hucre/xlsx'
5
5
 
6
- /** 列宽条目:模型列索引 → 像素宽(sheet-core 列宽未进 SheetSnapshot,导出时由调用方显式传入) */
6
+ /** 列宽条目:模型列索引 → 像素宽(与 SheetSnapshot.colWidths 同构) */
7
7
  export type ReportColWidthEntry = readonly [number, number]
8
8
 
9
9
  /** 像素 → Excel points */
@@ -30,14 +30,11 @@ export function buildColumnDefs(colWidths: ReadonlyArray<ReportColWidthEntry>):
30
30
  /**
31
31
  * 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽)。
32
32
  * 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
33
- *
34
- * 与 `exportWorkbookXlsx`(sheet-core)的差异:列宽未进 SheetSnapshot,
35
- * 只能经 VTable 运行时读取,故这里接收显式列宽并写入 columns 定义。
33
+ * 列宽取自 sheet 模型(`getColWidths`)。
36
34
  */
37
- export async function exportFilledReportXlsx(
38
- sheet: Sheet,
39
- colWidths: ReadonlyArray<ReportColWidthEntry>
40
- ): Promise<Uint8Array> {
35
+ export async function exportFilledReportXlsx(sheet: Sheet): Promise<Uint8Array> {
36
+ const widths: ReportColWidthEntry[] = [...sheet.getColWidths()]
37
+
41
38
  let maxRow = -1
42
39
  let maxCol = -1
43
40
  for (const [addr] of sheet.store.entries()) {
@@ -47,7 +44,7 @@ export async function exportFilledReportXlsx(
47
44
  for (const [row] of sheet.getRowHeights()) {
48
45
  if (row > maxRow) maxRow = row
49
46
  }
50
- for (const [col] of colWidths) {
47
+ for (const [col] of widths) {
51
48
  if (col > maxCol) maxCol = col
52
49
  }
53
50
 
@@ -63,10 +60,8 @@ export async function exportFilledReportXlsx(
63
60
  const row = rows[addr.row]!
64
61
  const key = `${addr.row},${addr.col}`
65
62
  const cell: Partial<Cell> = {}
66
- if (data.s != null) {
67
- const style = sheet.stylePool.get(data.s)
68
- if (style) cell.style = styleToHucre(style)
69
- }
63
+ const style = sheet.getEffectiveStyle(addr)
64
+ if (style) cell.style = styleToHucre(style)
70
65
  if (data.f != null && data.f !== '') {
71
66
  cell.formula = data.f
72
67
  if (data.v != null) cell.formulaResult = data.v as CellValue
@@ -100,7 +95,7 @@ export async function exportFilledReportXlsx(
100
95
  ...(sheet.merges.size > 0
101
96
  ? { merges: sheet.merges.getMerges().map((range) => rangeToHucre(range)) }
102
97
  : {}),
103
- ...(colWidths.length > 0 ? { columns: buildColumnDefs(colWidths) } : {})
98
+ ...(widths.length > 0 ? { columns: buildColumnDefs(widths) } : {})
104
99
  }
105
100
 
106
101
  const frozen = sheet.frozen
@@ -55,9 +55,20 @@ function isColExpanding(binding: ReportBinding): boolean {
55
55
  )
56
56
  }
57
57
 
58
+ function isReductionAggregate(aggregate: ReportAggregate): boolean {
59
+ return (
60
+ aggregate === 'sum' ||
61
+ aggregate === 'avg' ||
62
+ aggregate === 'count' ||
63
+ aggregate === 'max' ||
64
+ aggregate === 'min'
65
+ )
66
+ }
67
+
58
68
  function rowExpandingAncestors(
59
69
  binding: ReportBinding,
60
- index: TemplateIndex
70
+ index: TemplateIndex,
71
+ groupsOnly: boolean
61
72
  ): Array<{ addr: CellAddress; binding: ReportBinding }> {
62
73
  const chain: Array<{ addr: CellAddress; binding: ReportBinding }> = []
63
74
  let current = binding.rowParent
@@ -66,12 +77,17 @@ function rowExpandingAncestors(
66
77
  if (parentBinding) chain.unshift({ addr: current, binding: parentBinding })
67
78
  current = parentBinding?.rowParent
68
79
  }
69
- return chain.filter((item) => isRowExpanding(item.binding))
80
+ return chain.filter((item) => {
81
+ if (!isRowExpanding(item.binding)) return false
82
+ if (groupsOnly && item.binding.aggregate !== 'group') return false
83
+ return true
84
+ })
70
85
  }
71
86
 
72
87
  function colExpandingAncestors(
73
88
  binding: ReportBinding,
74
- index: TemplateIndex
89
+ index: TemplateIndex,
90
+ groupsOnly: boolean
75
91
  ): Array<{ addr: CellAddress; binding: ReportBinding }> {
76
92
  const chain: Array<{ addr: CellAddress; binding: ReportBinding }> = []
77
93
  let current = binding.colParent
@@ -80,7 +96,11 @@ function colExpandingAncestors(
80
96
  if (parentBinding) chain.unshift({ addr: current, binding: parentBinding })
81
97
  current = parentBinding?.colParent
82
98
  }
83
- return chain.filter((item) => isColExpanding(item.binding))
99
+ return chain.filter((item) => {
100
+ if (!isColExpanding(item.binding)) return false
101
+ if (groupsOnly && item.binding.aggregate !== 'group') return false
102
+ return true
103
+ })
84
104
  }
85
105
 
86
106
  function mergeFilters(
@@ -99,8 +119,9 @@ function resolveAncestorFilters(
99
119
  const rowFilter: Record<string, unknown> = {}
100
120
  const colFilter: Record<string, unknown> = {}
101
121
  let parentFilter: Record<string, unknown> = {}
122
+ const groupsOnly = isReductionAggregate(binding.aggregate)
102
123
 
103
- const rowAncestors = rowExpandingAncestors(binding, index)
124
+ const rowAncestors = rowExpandingAncestors(binding, index, groupsOnly)
104
125
  for (let i = 0; i < rowAncestors.length; i++) {
105
126
  const ancestor = rowAncestors[i]!
106
127
  if (ancestor.binding.dataset !== binding.dataset) continue
@@ -114,7 +135,7 @@ function resolveAncestorFilters(
114
135
  }
115
136
 
116
137
  parentFilter = {}
117
- const colAncestors = colExpandingAncestors(binding, index)
138
+ const colAncestors = colExpandingAncestors(binding, index, groupsOnly)
118
139
  for (let i = 0; i < colAncestors.length; i++) {
119
140
  const ancestor = colAncestors[i]!
120
141
  if (ancestor.binding.dataset !== binding.dataset) continue
@@ -136,8 +157,8 @@ function resolveOwnGroupValue(
136
157
  index: TemplateIndex,
137
158
  data: DatasetRecords
138
159
  ): unknown {
139
- const rowAncestors = rowExpandingAncestors(binding, index)
140
- const colAncestors = colExpandingAncestors(binding, index)
160
+ const rowAncestors = rowExpandingAncestors(binding, index, false)
161
+ const colAncestors = colExpandingAncestors(binding, index, false)
141
162
  let parentFilter: Record<string, unknown> = {}
142
163
 
143
164
  for (let i = 0; i < rowAncestors.length; i++) {
@@ -12,7 +12,7 @@ import type {
12
12
  } from './types'
13
13
 
14
14
  /**
15
- * 模板内嵌的数据集定义(Dataset = Data Connection + SQL,ADR-0002 决策 2)。
15
+ * 模板内嵌的数据集定义
16
16
  * 与 playground DatasetDef 同构,connectionId 替换为内嵌的完整连接对象。
17
17
  */
18
18
  export interface ReportDatasetDef {
@@ -26,7 +26,7 @@ export interface ReportDatasetDef {
26
26
  fieldOverrides?: Record<string, Partial<Pick<DatasetField, 'label'>>>
27
27
  }
28
28
 
29
- /** 当前支持的报表模板版本(ADR-0005 决策 4) */
29
+ /** 当前支持的报表模板版本 */
30
30
  export const REPORT_TEMPLATE_VERSION = 1
31
31
 
32
32
  /** 模板版本不兼容时抛出 */
@@ -40,8 +40,9 @@ export class IncompatibleTemplateVersionError extends Error {
40
40
  /**
41
41
  * 自包含 Report Template:SheetSnapshot + 内嵌数据集定义 + 版本段。
42
42
  * 查看器仅凭 `template` + `connector` 即可完成「参数提取 → 取数 → 展开渲染」闭环;
43
- * 模板可 JSON 序列化流转(凭据随之流转,持久化与安全存储由下游负责,ADR-0003 决策 4)。
43
+ * 模板可 JSON 序列化流转。
44
44
  *
45
+ * `colWidths` 继承自 `SheetSnapshot.colWidths`(模型列索引 → 像素)。
45
46
  * 注意:`Sheet.snapshot()` 不产生 `datasets` / `version` 字段,经 `restore()`/`snapshot()` 往返会丢失,
46
47
  * 由设计器 `getTemplate()` 在吐出快照时重新附加。
47
48
  */
@@ -65,7 +66,10 @@ export function assertCompatibleTemplateVersion(template: ReportTemplate): void
65
66
  }
66
67
  }
67
68
 
68
- /** 由工作簿快照构造带版本段的 Report Template */
69
+ /**
70
+ * 由工作簿快照构造带版本段的 Report Template。
71
+ * `colWidths` 随 `snapshot` 展开(已在 `SheetSnapshot.colWidths`)。
72
+ */
69
73
  export function createReportTemplate(
70
74
  snapshot: SheetSnapshot,
71
75
  datasets?: ReportDatasetDef[]