@veltra/sheet 2.2.0 → 2.3.1
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/hub-dataset-editor.js +16 -1
- 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 +4 -3
- 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 +15 -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/hub-dataset-editor.vue +19 -1
- 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 +5 -4
- 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
|
@@ -258,6 +258,43 @@ describe('USheet 组件', () => {
|
|
|
258
258
|
expect(gridAfter).not.toBe(gridBefore)
|
|
259
259
|
})
|
|
260
260
|
|
|
261
|
+
it('showRowHeader/showColHeader 为 false 时隐藏行号与列头', async () => {
|
|
262
|
+
const workbook = createWorkbook()
|
|
263
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
264
|
+
mount(
|
|
265
|
+
() => ({ workbook, rows: 8, cols: 4, showRowHeader: false, showColHeader: false }),
|
|
266
|
+
exposed
|
|
267
|
+
)
|
|
268
|
+
await nextTick()
|
|
269
|
+
|
|
270
|
+
workbook.activeSheet.setCellValue({ row: 0, col: 0 }, 'A1')
|
|
271
|
+
await nextTick()
|
|
272
|
+
const table = exposed.value!.getGrid()!.getTable()
|
|
273
|
+
expect(table.options.showHeader).toBe(false)
|
|
274
|
+
expect(table.options.rowSeriesNumber).toBeUndefined()
|
|
275
|
+
expect(table.columnHeaderLevelCount).toBe(0)
|
|
276
|
+
expect(table.rowCount).toBe(8)
|
|
277
|
+
expect(table.getCellValue(0, 0)).toBe('A1')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('删除行后渲染行数跟随模型收缩(不被 props.rows 撑回)', async () => {
|
|
281
|
+
const workbook = createWorkbook()
|
|
282
|
+
const exposed: { value: SheetExposed | undefined } = { value: undefined }
|
|
283
|
+
mount(() => ({ workbook, rows: 10, cols: 6 }), exposed)
|
|
284
|
+
await nextTick()
|
|
285
|
+
|
|
286
|
+
const sheet = exposed.value!.getActiveSheet()
|
|
287
|
+
expect(sheet.rows).toBe(10)
|
|
288
|
+
expect(exposed.value!.getGrid()!.getTable().rowCount).toBe(11) // 列头 + 10
|
|
289
|
+
|
|
290
|
+
sheet.deleteRows(0, 2)
|
|
291
|
+
await nextTick()
|
|
292
|
+
await nextTick()
|
|
293
|
+
|
|
294
|
+
expect(sheet.rows).toBe(8)
|
|
295
|
+
expect(exposed.value!.getGrid()!.getTable().rowCount).toBe(9) // 列头 + 8
|
|
296
|
+
})
|
|
297
|
+
|
|
261
298
|
it('resolveCellStyle prop 传入 SheetGrid 视口渲染', async () => {
|
|
262
299
|
const workbook = createWorkbook()
|
|
263
300
|
const sheet = workbook.activeSheet
|
|
@@ -36,8 +36,9 @@ describe('sheet-context-menu helpers', () => {
|
|
|
36
36
|
expect(isColInSelection(ranges, 6)).toBe(false)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it('resolveRenderSize
|
|
40
|
-
expect(resolveRenderSize(100, 26, 120, 10)).toEqual({ rows: 120, cols:
|
|
39
|
+
it('resolveRenderSize:模型已声明时以模型为准(可低于 props)', () => {
|
|
40
|
+
expect(resolveRenderSize(100, 26, 120, 10)).toEqual({ rows: 120, cols: 10 })
|
|
41
|
+
expect(resolveRenderSize(100, 26, 29, 26)).toEqual({ rows: 29, cols: 26 })
|
|
41
42
|
expect(resolveRenderSize(undefined, undefined, 0, 0)).toEqual({ rows: 100, cols: 26 })
|
|
42
43
|
})
|
|
43
44
|
})
|
|
@@ -40,7 +40,8 @@ import { buildBorderPresetItems } from '@veltra/sheet-core/core/style/border-pre
|
|
|
40
40
|
import {
|
|
41
41
|
BORDER_STYLE_WIDTH,
|
|
42
42
|
type BorderEdge,
|
|
43
|
-
type BorderLineStyle
|
|
43
|
+
type BorderLineStyle,
|
|
44
|
+
type CellStylePatch
|
|
44
45
|
} from '@veltra/sheet-core/core/style/types'
|
|
45
46
|
import { bem } from '@veltra/utils'
|
|
46
47
|
import { ref } from 'vue'
|
|
@@ -63,6 +64,7 @@ defineOptions({ name: 'USheetBorderPopup' })
|
|
|
63
64
|
* 预设补丁由 core/style/border-presets 生成(含邻居格共享边同步,对齐
|
|
64
65
|
* Excel「写入时同步、最近设置生效」);面板打开期间的写入由
|
|
65
66
|
* use-tool-popup 事务包裹为一个 undo 单元。
|
|
67
|
+
* 整行/整列选区下,均匀预设(all/none/单侧)走 applyStyle → 行列默认样式。
|
|
66
68
|
*/
|
|
67
69
|
const props = defineProps<{
|
|
68
70
|
/** 工具上下文(写入走命令系统,天然可撤销) */
|
|
@@ -75,7 +77,27 @@ const cls = bem('sheet')
|
|
|
75
77
|
const borderLineStyle = ref<BorderLineStyle>('thin')
|
|
76
78
|
const borderColor = ref('#000000')
|
|
77
79
|
|
|
78
|
-
/**
|
|
80
|
+
/** 均匀边框补丁(可写进行/列默认样式);outer/inner 几何相关 → null */
|
|
81
|
+
function uniformBorderPatch(preset: BorderPresetId, edge: BorderEdge): CellStylePatch | null {
|
|
82
|
+
switch (preset) {
|
|
83
|
+
case 'all':
|
|
84
|
+
return { border: { top: edge, right: edge, bottom: edge, left: edge } }
|
|
85
|
+
case 'none':
|
|
86
|
+
return { border: { top: null, right: null, bottom: null, left: null } }
|
|
87
|
+
case 'top':
|
|
88
|
+
return { border: { top: edge } }
|
|
89
|
+
case 'bottom':
|
|
90
|
+
return { border: { bottom: edge } }
|
|
91
|
+
case 'left':
|
|
92
|
+
return { border: { left: edge } }
|
|
93
|
+
case 'right':
|
|
94
|
+
return { border: { right: edge } }
|
|
95
|
+
default:
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 应用边框预设:整行/整列均匀预设走 applyStyle;其余逐格批量(含邻居同步) */
|
|
79
101
|
function applyBorderPreset(preset: BorderPresetId): void {
|
|
80
102
|
const range = currentRange(props.context)
|
|
81
103
|
if (!range) return
|
|
@@ -84,8 +106,14 @@ function applyBorderPreset(preset: BorderPresetId): void {
|
|
|
84
106
|
width: BORDER_STYLE_WIDTH[borderLineStyle.value],
|
|
85
107
|
color: borderColor.value
|
|
86
108
|
}
|
|
109
|
+
const target = props.context.resolveStyleTarget(range)
|
|
110
|
+
const uniform = target !== 'cell' ? uniformBorderPatch(preset, edge) : null
|
|
111
|
+
if (uniform) {
|
|
112
|
+
props.context.applyStyle(range, uniform)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
87
115
|
const items: SetCellStyleItem[] = buildBorderPresetItems(range, preset, edge, (addr) =>
|
|
88
|
-
props.context.
|
|
116
|
+
props.context.getEffectiveStyle(addr)
|
|
89
117
|
).map(({ addr, patch }) => ({ addr, partial: patch }))
|
|
90
118
|
if (items.length === 0) return
|
|
91
119
|
props.context.executeCommand('sheet.command.set-cell-style', { items })
|
|
@@ -21,19 +21,20 @@ const props = defineProps<{
|
|
|
21
21
|
}>()
|
|
22
22
|
|
|
23
23
|
/** 填充颜色('' = 无填充) */
|
|
24
|
-
const fillColor = ref(
|
|
24
|
+
const fillColor = ref<string>()
|
|
25
25
|
|
|
26
26
|
// 打开时初始化为活动格当前填充色
|
|
27
27
|
onMounted(() => {
|
|
28
28
|
const active = props.context.getSelection().activeCell
|
|
29
|
-
const style = active ? props.context.
|
|
29
|
+
const style = active ? props.context.getEffectiveStyle(active) : undefined
|
|
30
30
|
fillColor.value = style?.fill?.color ?? ''
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
/** 填充颜色变化('' = 无填充:清除 fill 保留边框) */
|
|
34
|
-
function applyFillColor(color
|
|
34
|
+
function applyFillColor(color?: string): void {
|
|
35
|
+
fillColor.value = color
|
|
35
36
|
const range = currentRange(props.context)
|
|
36
37
|
if (!range) return
|
|
37
|
-
props.context.
|
|
38
|
+
props.context.applyStyle(range, color ? { fill: { color } } : { fill: {} })
|
|
38
39
|
}
|
|
39
40
|
</script>
|
|
@@ -26,7 +26,7 @@ const fontColor = ref('')
|
|
|
26
26
|
// 打开时初始化为活动格当前字体色
|
|
27
27
|
onMounted(() => {
|
|
28
28
|
const active = props.context.getSelection().activeCell
|
|
29
|
-
const style = active ? props.context.
|
|
29
|
+
const style = active ? props.context.getEffectiveStyle(active) : undefined
|
|
30
30
|
fontColor.value = style?.font?.color ?? ''
|
|
31
31
|
})
|
|
32
32
|
|
|
@@ -34,6 +34,6 @@ onMounted(() => {
|
|
|
34
34
|
function applyFontColor(color: string): void {
|
|
35
35
|
const range = currentRange(props.context)
|
|
36
36
|
if (!range) return
|
|
37
|
-
props.context.
|
|
37
|
+
props.context.applyStyle(range, color ? { font: { color } } : { font: { color: null } })
|
|
38
38
|
}
|
|
39
39
|
</script>
|
|
@@ -39,7 +39,7 @@ const currentSize = ref<number | undefined>(undefined)
|
|
|
39
39
|
|
|
40
40
|
onMounted(() => {
|
|
41
41
|
const active = props.context.getSelection().activeCell
|
|
42
|
-
const style = active ? props.context.
|
|
42
|
+
const style = active ? props.context.getEffectiveStyle(active) : undefined
|
|
43
43
|
currentSize.value = style?.font?.size
|
|
44
44
|
})
|
|
45
45
|
|
|
@@ -47,6 +47,6 @@ function applyFontSize(size: number): void {
|
|
|
47
47
|
const range = currentRange(props.context)
|
|
48
48
|
if (!range) return
|
|
49
49
|
currentSize.value = size
|
|
50
|
-
props.context.
|
|
50
|
+
props.context.applyStyle(range, { font: { size } })
|
|
51
51
|
}
|
|
52
52
|
</script>
|
|
@@ -39,14 +39,20 @@ export function wholeColRange(col: number, rowCount: number): CellRange {
|
|
|
39
39
|
return createRange({ row: 0, col }, { row: Math.max(rowCount - 1, 0), col })
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* 渲染尺寸:模型已声明时以模型为准(删行可低于 props);未声明时回落 props。
|
|
44
|
+
* 与 SheetGrid / useSheetGrid 一致——避免 ensureTableSize(props) 把删行结果撑回。
|
|
45
|
+
*/
|
|
43
46
|
export function resolveRenderSize(
|
|
44
47
|
propsRows: number | undefined,
|
|
45
48
|
propsCols: number | undefined,
|
|
46
49
|
sheetRows: number,
|
|
47
50
|
sheetCols: number
|
|
48
51
|
): { rows: number; cols: number } {
|
|
49
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
rows: sheetRows > 0 ? sheetRows : (propsRows ?? 100),
|
|
54
|
+
cols: sheetCols > 0 ? sheetCols : (propsCols ?? 26)
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
function insertCountRender(options: {
|
|
@@ -109,12 +109,16 @@ export function useSheetGrid(options: UseSheetGridOptions) {
|
|
|
109
109
|
const el = document.createElement('div')
|
|
110
110
|
el.className = 'u-sheet__grid-instance'
|
|
111
111
|
host.appendChild(el)
|
|
112
|
+
// 模型已声明尺寸时传入模型值,避免删行后被 props 经 ensureTableSize 撑回
|
|
113
|
+
const { rows, cols } = resolveRenderSize(props.rows, props.cols, sheet.rows, sheet.cols)
|
|
112
114
|
const grid = new SheetGrid({
|
|
113
115
|
container: el,
|
|
114
116
|
sheet,
|
|
115
|
-
rows
|
|
116
|
-
cols
|
|
117
|
+
rows,
|
|
118
|
+
cols,
|
|
117
119
|
readonly: props.readonly,
|
|
120
|
+
showRowHeader: props.showRowHeader,
|
|
121
|
+
showColHeader: props.showColHeader,
|
|
118
122
|
resolveDisplayValue: props.resolveDisplayValue,
|
|
119
123
|
resolveCellStyle: props.resolveCellStyle,
|
|
120
124
|
resolveCellRenderer: props.resolveCellRenderer,
|
|
@@ -237,7 +241,17 @@ export function useSheetGrid(options: UseSheetGridOptions) {
|
|
|
237
241
|
|
|
238
242
|
onMounted(activateGrid)
|
|
239
243
|
|
|
240
|
-
|
|
244
|
+
// props 尺寸显式增大时先扩张模型,再重建(删行缩小不走这条路径)
|
|
245
|
+
watch(
|
|
246
|
+
() =>
|
|
247
|
+
[props.rows, props.cols, props.readonly, props.showRowHeader, props.showColHeader] as const,
|
|
248
|
+
([nextRows, nextCols]) => {
|
|
249
|
+
const sheet = getActiveSheet()
|
|
250
|
+
if (nextRows != null) sheet.ensureTableSize(nextRows, 0)
|
|
251
|
+
if (nextCols != null) sheet.ensureTableSize(0, nextCols)
|
|
252
|
+
rebuildGrid()
|
|
253
|
+
}
|
|
254
|
+
)
|
|
241
255
|
|
|
242
256
|
onBeforeUnmount(() => {
|
|
243
257
|
invalidateAll()
|
|
@@ -5,6 +5,7 @@ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vu
|
|
|
5
5
|
import { createSheetContext } from '../../tools/context'
|
|
6
6
|
import { defaultToolRegistry } from '../../tools/registry'
|
|
7
7
|
import type { SheetProps } from '../../types'
|
|
8
|
+
import { resolveRenderSize } from './sheet-context-menu'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* useSheetState 与宿主组件的协作钩子。
|
|
@@ -44,10 +45,14 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
|
|
|
44
45
|
() => sheetList.value[activeIndex.value] ?? workbook.value.activeSheet
|
|
45
46
|
)
|
|
46
47
|
|
|
47
|
-
/** 工具上下文:动态解析活动 sheet
|
|
48
|
+
/** 工具上下文:动态解析活动 sheet / 工作簿 / 渲染尺寸,tab 切换后自动指向最新值 */
|
|
48
49
|
const context = createSheetContext(
|
|
49
50
|
() => activeSheet.value,
|
|
50
|
-
() => workbook.value
|
|
51
|
+
() => workbook.value,
|
|
52
|
+
{
|
|
53
|
+
resolveGridSize: () =>
|
|
54
|
+
resolveRenderSize(props.rows, props.cols, activeSheet.value.rows, activeSheet.value.cols)
|
|
55
|
+
}
|
|
51
56
|
)
|
|
52
57
|
|
|
53
58
|
/** 工具栏状态版本号:visible/disabled/active 是 (ctx) => boolean 纯函数,状态源变化时 bump 触发重算 */
|
|
@@ -80,7 +85,7 @@ export function useSheetState(props: SheetProps, emit: Emit, hooks: SheetStateHo
|
|
|
80
85
|
// 整表内容替换(导入 / undo/redo 回放):restore 静默不发 cell-change,
|
|
81
86
|
// 状态源需 bump(grid 层自行订阅 content-reset 全量刷新,此处不重建)
|
|
82
87
|
sheet.on('content-reset', scheduleBump),
|
|
83
|
-
// 行列插入/删除 →
|
|
88
|
+
// 行列插入/删除 → 重建网格(渲染行列数跟随模型 sheet.rows/cols,
|
|
84
89
|
// 数据/选区/冻结/行高随重建恢复;低频操作直接重建)
|
|
85
90
|
sheet.on('structure-change', hooks.rebuildGrid)
|
|
86
91
|
]
|
|
@@ -6,11 +6,15 @@ import {
|
|
|
6
6
|
createReportBinding,
|
|
7
7
|
formatBindingPlaceholder,
|
|
8
8
|
formatCellAddress,
|
|
9
|
+
inferColParentCandidate,
|
|
9
10
|
inferDropPreset,
|
|
11
|
+
inferParentsForPreset,
|
|
10
12
|
inferReportPreset,
|
|
13
|
+
inferRowParentCandidate,
|
|
11
14
|
isExpansionBandRow,
|
|
12
15
|
parseCellAddress,
|
|
13
16
|
presetBindingPatch,
|
|
17
|
+
presetStillApplies,
|
|
14
18
|
setBindingCatalog
|
|
15
19
|
} from '../binding'
|
|
16
20
|
import type { DatasetCatalogItem, ReportBinding } from '../types'
|
|
@@ -180,7 +184,32 @@ describe('report binding', () => {
|
|
|
180
184
|
expand: 'none',
|
|
181
185
|
rowParent: { row: 1, col: 0 }
|
|
182
186
|
})
|
|
183
|
-
).
|
|
187
|
+
).toBe('subtotal')
|
|
188
|
+
|
|
189
|
+
expect(
|
|
190
|
+
inferReportPreset({
|
|
191
|
+
dataset: 'orders',
|
|
192
|
+
field: 'amount',
|
|
193
|
+
aggregate: 'sum',
|
|
194
|
+
expand: 'none',
|
|
195
|
+
colParent: { row: 0, col: 1 }
|
|
196
|
+
})
|
|
197
|
+
).toBe('subtotal')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('presetStillApplies 同族内改聚合仍成立', () => {
|
|
201
|
+
const subtotal: ReportBinding = {
|
|
202
|
+
dataset: 'orders',
|
|
203
|
+
field: 'amount',
|
|
204
|
+
preset: 'subtotal',
|
|
205
|
+
aggregate: 'avg',
|
|
206
|
+
expand: 'none',
|
|
207
|
+
rowParent: { row: 1, col: 0 }
|
|
208
|
+
}
|
|
209
|
+
expect(presetStillApplies(subtotal)).toBe(true)
|
|
210
|
+
|
|
211
|
+
const broken = { ...subtotal, aggregate: 'list' as const, expand: 'down' as const }
|
|
212
|
+
expect(presetStillApplies(broken)).toBe(false)
|
|
184
213
|
})
|
|
185
214
|
|
|
186
215
|
it('inferDropPreset / isExpansionBandRow 落格推断', () => {
|
|
@@ -200,6 +229,79 @@ describe('report binding', () => {
|
|
|
200
229
|
expect(inferDropPreset({ row: 2, col: 0 }, 'string', getBindingAt)).toBe('detail')
|
|
201
230
|
})
|
|
202
231
|
|
|
232
|
+
it('inferDropPreset:数值字段落在横向展开带正右相邻列也推断为小计', () => {
|
|
233
|
+
const colGroup = createReportBinding(ORDERS_DATASET, 'region')
|
|
234
|
+
colGroup.aggregate = 'group'
|
|
235
|
+
colGroup.expand = 'right'
|
|
236
|
+
|
|
237
|
+
const getBindingAt = (addr: CellAddress) => {
|
|
238
|
+
if (addr.row === 0 && addr.col === 1) return colGroup
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
expect(inferDropPreset({ row: 0, col: 2 }, 'number', getBindingAt)).toBe('subtotal')
|
|
243
|
+
expect(inferDropPreset({ row: 0, col: 2 }, 'string', getBindingAt)).toBe('detail')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('inferRowParentCandidate:同行向左优先分组头', () => {
|
|
247
|
+
const group = createReportBinding(ORDERS_DATASET, 'customer')
|
|
248
|
+
group.aggregate = 'group'
|
|
249
|
+
group.expand = 'down'
|
|
250
|
+
const detail = createReportBinding(ORDERS_DATASET, 'orderNo')
|
|
251
|
+
|
|
252
|
+
const getBindingAt = (addr: CellAddress) => {
|
|
253
|
+
if (addr.row === 1 && addr.col === 0) return group
|
|
254
|
+
if (addr.row === 1 && addr.col === 1) return detail
|
|
255
|
+
return undefined
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
expect(inferRowParentCandidate({ row: 1, col: 2 }, getBindingAt)).toEqual({ row: 1, col: 0 })
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('inferRowParentCandidate:下方跨列小计推到行分组头而不是同列 list', () => {
|
|
262
|
+
const group = createReportBinding(ORDERS_DATASET, 'customer')
|
|
263
|
+
group.aggregate = 'group'
|
|
264
|
+
group.expand = 'down'
|
|
265
|
+
const detail = createReportBinding(ORDERS_DATASET, 'amount')
|
|
266
|
+
|
|
267
|
+
const getBindingAt = (addr: CellAddress) => {
|
|
268
|
+
if (addr.row === 1 && addr.col === 0) return group
|
|
269
|
+
if (addr.row === 1 && addr.col === 2) return detail
|
|
270
|
+
return undefined
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expect(
|
|
274
|
+
inferRowParentCandidate({ row: 2, col: 2 }, getBindingAt, { preferGroup: true })
|
|
275
|
+
).toEqual({ row: 1, col: 0 })
|
|
276
|
+
expect(inferParentsForPreset({ row: 2, col: 2 }, 'subtotal', getBindingAt)).toEqual({
|
|
277
|
+
rowParent: { row: 1, col: 0 }
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('inferColParentCandidate:右侧列小计同列向上推到列分组头', () => {
|
|
282
|
+
const colGroup = createReportBinding(ORDERS_DATASET, 'region')
|
|
283
|
+
colGroup.aggregate = 'group'
|
|
284
|
+
colGroup.expand = 'right'
|
|
285
|
+
const cross = createReportBinding(ORDERS_DATASET, 'amount')
|
|
286
|
+
cross.preset = 'cross'
|
|
287
|
+
cross.aggregate = 'sum'
|
|
288
|
+
cross.expand = 'none'
|
|
289
|
+
cross.colParent = { row: 0, col: 1 }
|
|
290
|
+
|
|
291
|
+
const getBindingAt = (addr: CellAddress) => {
|
|
292
|
+
if (addr.row === 0 && addr.col === 1) return colGroup
|
|
293
|
+
if (addr.row === 1 && addr.col === 1) return cross
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
expect(
|
|
298
|
+
inferColParentCandidate({ row: 2, col: 1 }, getBindingAt, { preferGroup: true })
|
|
299
|
+
).toEqual({ row: 0, col: 1 })
|
|
300
|
+
expect(inferParentsForPreset({ row: 2, col: 1 }, 'subtotal', getBindingAt)).toEqual({
|
|
301
|
+
colParent: { row: 0, col: 1 }
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
203
305
|
it('formatCellAddress / parseCellAddress 支持多字母列互逆', () => {
|
|
204
306
|
const single = { row: 2, col: 1 }
|
|
205
307
|
expect(formatCellAddress(single)).toBe('B3')
|
|
@@ -84,6 +84,27 @@ describe('createHttpConnector', () => {
|
|
|
84
84
|
expect(calls[0]!.body).toEqual({ connection: CONNECTION, sql: SQL })
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
+
it('describe:服务端返回 label 时保留中文显示名', async () => {
|
|
88
|
+
stubFetch(async () => ({
|
|
89
|
+
ok: true,
|
|
90
|
+
fields: [
|
|
91
|
+
{ name: 'customer', label: '客户名称', type: 'string' },
|
|
92
|
+
{ name: 'amount', label: '金额', type: 'number' }
|
|
93
|
+
]
|
|
94
|
+
}))
|
|
95
|
+
const connector = createHttpConnector({ endpoint: ENDPOINT })
|
|
96
|
+
|
|
97
|
+
const result = await connector.describe(CONNECTION, SQL)
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
ok: true,
|
|
101
|
+
data: [
|
|
102
|
+
{ name: 'customer', label: '客户名称', type: 'string' },
|
|
103
|
+
{ name: 'amount', label: '金额', type: 'number' }
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
87
108
|
it('query:POST {endpoint}/query,请求体 { connection, sql, values },返回 fields + rows', async () => {
|
|
88
109
|
const calls = stubFetch(async () => ({
|
|
89
110
|
ok: true,
|
|
@@ -60,10 +60,10 @@ function buildFilledSheet(): Sheet {
|
|
|
60
60
|
|
|
61
61
|
describe('exportFilledReportXlsx', () => {
|
|
62
62
|
it('导出保真 XLSX:值 / 表头样式 / 条件样式打平后随快照导出', async () => {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const filled = buildFilledSheet()
|
|
64
|
+
filled.setColWidth(0, 120)
|
|
65
|
+
filled.setColWidth(1, 140)
|
|
66
|
+
const buffer = await exportFilledReportXlsx(filled)
|
|
67
67
|
|
|
68
68
|
// 合法 ZIP(xlsx)字节
|
|
69
69
|
expect(buffer[0]).toBe(0x50)
|
|
@@ -725,4 +725,106 @@ describe('renderReport', () => {
|
|
|
725
725
|
])
|
|
726
726
|
)
|
|
727
727
|
})
|
|
728
|
+
|
|
729
|
+
it('跨列行小计按组求和,不是全表合计', () => {
|
|
730
|
+
const sheet = new Sheet()
|
|
731
|
+
sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '客户' } }])
|
|
732
|
+
const groupParent = { row: 1, col: 0 }
|
|
733
|
+
|
|
734
|
+
const customerGroup = createReportBinding(ORDERS_DATASET, 'customer')
|
|
735
|
+
customerGroup.preset = 'groupHeader'
|
|
736
|
+
customerGroup.aggregate = 'group'
|
|
737
|
+
customerGroup.expand = 'down'
|
|
738
|
+
sheet.setCellMeta(groupParent, REPORT_META_NAMESPACE, customerGroup)
|
|
739
|
+
|
|
740
|
+
const orderNo = createReportBinding(ORDERS_DATASET, 'orderNo')
|
|
741
|
+
orderNo.rowParent = groupParent
|
|
742
|
+
sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, orderNo)
|
|
743
|
+
|
|
744
|
+
const amount = createReportBinding(ORDERS_DATASET, 'amount')
|
|
745
|
+
amount.rowParent = groupParent
|
|
746
|
+
sheet.setCellMeta({ row: 1, col: 2 }, REPORT_META_NAMESPACE, amount)
|
|
747
|
+
|
|
748
|
+
const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
|
|
749
|
+
subtotal.preset = 'subtotal'
|
|
750
|
+
subtotal.aggregate = 'sum'
|
|
751
|
+
subtotal.expand = 'none'
|
|
752
|
+
subtotal.rowParent = groupParent
|
|
753
|
+
sheet.setCellMeta({ row: 2, col: 2 }, REPORT_META_NAMESPACE, subtotal)
|
|
754
|
+
|
|
755
|
+
const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
|
|
756
|
+
expect(cellValue(filled, 5, 2)).toBe(630)
|
|
757
|
+
expect(cellValue(filled, 10, 2)).toBe(1050)
|
|
758
|
+
expect(cellValue(filled, 5, 2)).not.toBe(4180)
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
it('跨列行小计误挂明细时仍按分组祖先过滤', () => {
|
|
762
|
+
const sheet = new Sheet()
|
|
763
|
+
sheet.setCells([{ addr: { row: 0, col: 0 }, data: { v: '客户' } }])
|
|
764
|
+
const groupParent = { row: 1, col: 0 }
|
|
765
|
+
const listAddr = { row: 1, col: 2 }
|
|
766
|
+
|
|
767
|
+
const customerGroup = createReportBinding(ORDERS_DATASET, 'customer')
|
|
768
|
+
customerGroup.preset = 'groupHeader'
|
|
769
|
+
customerGroup.aggregate = 'group'
|
|
770
|
+
customerGroup.expand = 'down'
|
|
771
|
+
sheet.setCellMeta(groupParent, REPORT_META_NAMESPACE, customerGroup)
|
|
772
|
+
|
|
773
|
+
const amount = createReportBinding(ORDERS_DATASET, 'amount')
|
|
774
|
+
amount.rowParent = groupParent
|
|
775
|
+
sheet.setCellMeta(listAddr, REPORT_META_NAMESPACE, amount)
|
|
776
|
+
|
|
777
|
+
const subtotal = createReportBinding(ORDERS_DATASET, 'amount')
|
|
778
|
+
subtotal.preset = 'subtotal'
|
|
779
|
+
subtotal.aggregate = 'sum'
|
|
780
|
+
subtotal.expand = 'none'
|
|
781
|
+
subtotal.rowParent = listAddr
|
|
782
|
+
sheet.setCellMeta({ row: 2, col: 2 }, REPORT_META_NAMESPACE, subtotal)
|
|
783
|
+
|
|
784
|
+
const filled = renderReport(sheet.snapshot(), MOCK_DATA_RECORDS)
|
|
785
|
+
expect(cellValue(filled, 5, 2)).toBe(630)
|
|
786
|
+
expect(cellValue(filled, 10, 2)).toBe(1050)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('交叉列小计仅 colParent 时按列分组求和,不是全表合计', () => {
|
|
790
|
+
const sheet = new Sheet()
|
|
791
|
+
const colGroupAddr = { row: 0, col: 1 }
|
|
792
|
+
const rowGroupAddr = { row: 1, col: 0 }
|
|
793
|
+
|
|
794
|
+
const categoryGroup = createReportBinding(SALES_MATRIX_DATASET, 'category')
|
|
795
|
+
categoryGroup.preset = 'groupHeader'
|
|
796
|
+
categoryGroup.aggregate = 'group'
|
|
797
|
+
categoryGroup.expand = 'right'
|
|
798
|
+
sheet.setCellMeta(colGroupAddr, REPORT_META_NAMESPACE, categoryGroup)
|
|
799
|
+
|
|
800
|
+
const regionGroup = createReportBinding(SALES_MATRIX_DATASET, 'region')
|
|
801
|
+
regionGroup.preset = 'groupHeader'
|
|
802
|
+
regionGroup.aggregate = 'group'
|
|
803
|
+
regionGroup.expand = 'down'
|
|
804
|
+
sheet.setCellMeta(rowGroupAddr, REPORT_META_NAMESPACE, regionGroup)
|
|
805
|
+
|
|
806
|
+
const cross = createReportBinding(SALES_MATRIX_DATASET, 'amount')
|
|
807
|
+
cross.preset = 'cross'
|
|
808
|
+
cross.aggregate = 'sum'
|
|
809
|
+
cross.expand = 'none'
|
|
810
|
+
cross.rowParent = rowGroupAddr
|
|
811
|
+
cross.colParent = colGroupAddr
|
|
812
|
+
sheet.setCellMeta({ row: 1, col: 1 }, REPORT_META_NAMESPACE, cross)
|
|
813
|
+
|
|
814
|
+
const colSubtotal = createReportBinding(SALES_MATRIX_DATASET, 'amount')
|
|
815
|
+
colSubtotal.preset = 'subtotal'
|
|
816
|
+
colSubtotal.aggregate = 'sum'
|
|
817
|
+
colSubtotal.expand = 'none'
|
|
818
|
+
colSubtotal.colParent = colGroupAddr
|
|
819
|
+
sheet.setCellMeta({ row: 2, col: 1 }, REPORT_META_NAMESPACE, colSubtotal)
|
|
820
|
+
|
|
821
|
+
const filled = renderReport(sheet.snapshot(), { 'sales-matrix': SALES_MATRIX_ROWS })
|
|
822
|
+
const officeTotal = SALES_MATRIX_ROWS.filter((row) => row.category === '办公设备').reduce(
|
|
823
|
+
(sum, row) => sum + (row.amount as number),
|
|
824
|
+
0
|
|
825
|
+
)
|
|
826
|
+
const grandTotal = SALES_MATRIX_ROWS.reduce((sum, row) => sum + (row.amount as number), 0)
|
|
827
|
+
expect(cellValue(filled, 5, 1)).toBe(officeTotal)
|
|
828
|
+
expect(cellValue(filled, 5, 1)).not.toBe(grandTotal)
|
|
829
|
+
})
|
|
728
830
|
})
|
|
@@ -105,6 +105,17 @@ describe('ReportTemplate version', () => {
|
|
|
105
105
|
expect(template.version).toBe(REPORT_TEMPLATE_VERSION)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
it('createReportTemplate 继承 snapshot.colWidths', () => {
|
|
109
|
+
const sheet = new Sheet()
|
|
110
|
+
sheet.setColWidth(0, 120)
|
|
111
|
+
sheet.setColWidth(2, 90)
|
|
112
|
+
const template = createReportTemplate(sheet.snapshot(), [ORDERS_DATASET])
|
|
113
|
+
expect(template.colWidths).toEqual([
|
|
114
|
+
[0, 120],
|
|
115
|
+
[2, 90]
|
|
116
|
+
])
|
|
117
|
+
})
|
|
118
|
+
|
|
108
119
|
it('version 缺失时 assertCompatibleTemplateVersion 抛可读错误', () => {
|
|
109
120
|
const sheet = new Sheet()
|
|
110
121
|
const template = sheet.snapshot() as ReportTemplate
|