@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.
- package/dist/components/report/binding-badge.js +45 -21
- package/dist/components/report/designer/col-widths.js +18 -4
- package/dist/components/report/designer/conditional-rules/helpers.js +3 -3
- package/dist/components/report/designer/float-panel.js +73 -36
- package/dist/components/report/designer/rule-row.js +111 -127
- package/dist/components/report/designer/rules-dialog.js +5 -22
- package/dist/components/report/preview-grid-size.js +32 -0
- package/dist/components/report/report-designer.js +75 -25
- package/dist/components/report/report-viewer.js +38 -25
- package/dist/components/report/style2.css +44 -39
- package/dist/components/report/use-report-designer.js +34 -12
- package/dist/components/report/use-report-viewer.js +6 -1
- package/dist/components/sheet/popups/border-popup.js +30 -2
- package/dist/components/sheet/popups/fill-color-popup.js +2 -2
- package/dist/components/sheet/popups/font-color-popup.js +2 -2
- package/dist/components/sheet/popups/font-size-popup.js +2 -2
- package/dist/components/sheet/sheet-context-menu.js +6 -3
- package/dist/components/sheet/sheet.js +8 -0
- package/dist/components/sheet/use-sheet-grid.js +14 -4
- package/dist/components/sheet/use-sheet-state.js +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/report/binding.d.ts +49 -11
- package/dist/report/binding.js +186 -29
- package/dist/report/connector.js +1 -1
- package/dist/report/export-xlsx.d.ts +3 -5
- package/dist/report/export-xlsx.js +7 -10
- package/dist/report/index.d.ts +2 -2
- package/dist/report/render/aggregate.js +20 -8
- package/dist/report/template.d.ts +8 -4
- package/dist/report/template.js +5 -2
- package/dist/tools/apply-style.js +29 -0
- package/dist/tools/builtin.js +17 -17
- package/dist/tools/context.d.ts +38 -5
- package/dist/tools/context.js +38 -1
- package/dist/types/report-designer.d.ts +2 -0
- package/dist/types/report-viewer.d.ts +2 -2
- package/dist/types/sheet.d.ts +4 -0
- package/package.json +6 -6
- package/src/components/report/__test__/conditional-rules-helpers.test.ts +1 -0
- package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
- package/src/components/report/__test__/report-designer.test.ts +76 -0
- package/src/components/report/__test__/report-viewer.test.ts +24 -1
- package/src/components/report/__test__/use-report-designer.test.ts +142 -10
- package/src/components/report/binding-badge.ts +39 -20
- package/src/components/report/designer/col-widths.ts +34 -11
- package/src/components/report/designer/conditional-rules/helpers.ts +6 -3
- package/src/components/report/designer/float-panel.vue +71 -27
- package/src/components/report/designer/rule-row.vue +67 -67
- package/src/components/report/designer/rules-dialog.vue +3 -19
- package/src/components/report/preview-grid-size.ts +33 -0
- package/src/components/report/report-designer.vue +79 -15
- package/src/components/report/report-viewer.vue +37 -29
- package/src/components/report/style-designer.scss +45 -41
- package/src/components/report/use-report-designer.ts +63 -14
- package/src/components/report/use-report-viewer.ts +11 -2
- package/src/components/sheet/__test__/sheet-component.test.ts +37 -0
- package/src/components/sheet/__test__/sheet-context-menu.test.ts +3 -2
- package/src/components/sheet/popups/border-popup.vue +31 -3
- package/src/components/sheet/popups/fill-color-popup.vue +2 -2
- package/src/components/sheet/popups/font-color-popup.vue +2 -2
- package/src/components/sheet/popups/font-size-popup.vue +2 -2
- package/src/components/sheet/sheet-context-menu.ts +8 -2
- package/src/components/sheet/sheet.vue +2 -0
- package/src/components/sheet/use-sheet-grid.ts +17 -3
- package/src/components/sheet/use-sheet-state.ts +8 -3
- package/src/report/__test__/binding.test.ts +103 -1
- package/src/report/__test__/connector.test.ts +21 -0
- package/src/report/__test__/export-xlsx.test.ts +4 -4
- package/src/report/__test__/render.test.ts +102 -0
- package/src/report/__test__/template.test.ts +11 -0
- package/src/report/binding.ts +278 -47
- package/src/report/connector.ts +3 -2
- package/src/report/export-xlsx.ts +9 -14
- package/src/report/render/aggregate.ts +29 -8
- package/src/report/template.ts +8 -4
- package/src/tools/__test__/apply-style.test.ts +129 -0
- package/src/tools/apply-style.ts +41 -0
- package/src/tools/builtin.ts +18 -18
- package/src/tools/context.ts +67 -4
- package/src/tools/index.ts +1 -0
- package/src/types/report-designer.ts +2 -0
- package/src/types/report-viewer.ts +2 -2
- package/src/types/sheet.ts +4 -0
package/src/report/binding.ts
CHANGED
|
@@ -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:
|
|
86
|
-
scanCols =
|
|
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:
|
|
169
|
+
getBindingAt: GetBindingAt
|
|
100
170
|
): ReportPreset {
|
|
101
|
-
if (fieldType
|
|
102
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* 列方向父格推断(与行方向对称):
|
|
315
|
+
* 1. 同列向上:先找列分组头,否则最近横向扩展格
|
|
316
|
+
* 2. 左侧各列整列扫描:每列先找列分组头
|
|
317
|
+
* 列小计(preferGroup)禁止把交叉格/明细当成列父格
|
|
318
|
+
*/
|
|
150
319
|
export function inferColParentCandidate(
|
|
151
320
|
addr: CellAddress,
|
|
152
|
-
getBindingAt:
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
169
|
-
binding.
|
|
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 =
|
|
242
|
-
return `${tag} · ${
|
|
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
|
}
|
package/src/report/connector.ts
CHANGED
|
@@ -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
|
-
/** 列宽条目:模型列索引 →
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
...(
|
|
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) =>
|
|
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) =>
|
|
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++) {
|
package/src/report/template.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
} from './types'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 由工作簿快照构造带版本段的 Report Template。
|
|
71
|
+
* `colWidths` 随 `snapshot` 展开(已在 `SheetSnapshot.colWidths`)。
|
|
72
|
+
*/
|
|
69
73
|
export function createReportTemplate(
|
|
70
74
|
snapshot: SheetSnapshot,
|
|
71
75
|
datasets?: ReportDatasetDef[]
|