@veltra/sheet 2.4.0 → 2.5.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/sheet/formula-bar.js +50 -7
- package/dist/components/sheet/style2.css +2 -2
- package/dist/index.d.ts +2 -19
- package/dist/index.js +2 -15
- package/dist/types/index.d.ts +1 -3
- package/package.json +5 -6
- package/src/components/sheet/__test__/formula-bar.test.ts +29 -0
- package/src/components/sheet/formula-bar.vue +67 -6
- package/src/components/sheet/style.scss +1 -1
- package/src/index.ts +1 -27
- package/src/types/index.ts +0 -2
- package/dist/components/report/binding-badge.js +0 -118
- package/dist/components/report/designer/cell-coords.js +0 -46
- package/dist/components/report/designer/col-widths.js +0 -29
- package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
- package/dist/components/report/designer/drill-dialog.js +0 -258
- package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
- package/dist/components/report/designer/float-panel.js +0 -434
- package/dist/components/report/designer/role.js +0 -17
- package/dist/components/report/designer/rule-preview.js +0 -33
- package/dist/components/report/designer/rule-row.js +0 -253
- package/dist/components/report/designer/rules-dialog.js +0 -118
- package/dist/components/report/designer/topology-overlay.js +0 -122
- package/dist/components/report/designer/topology.js +0 -64
- package/dist/components/report/designer/use-grid-overlay.js +0 -75
- package/dist/components/report/designer-hub.js +0 -177
- package/dist/components/report/field-panel-helpers.js +0 -33
- package/dist/components/report/field-panel.js +0 -132
- package/dist/components/report/filter-bar.js +0 -74
- package/dist/components/report/hub-connection-form.js +0 -145
- package/dist/components/report/hub-dataset-editor.js +0 -409
- package/dist/components/report/preview-grid-size.js +0 -32
- package/dist/components/report/print.js +0 -39
- package/dist/components/report/report-designer.js +0 -428
- package/dist/components/report/report-viewer.js +0 -253
- package/dist/components/report/style.d.ts +0 -17
- package/dist/components/report/style.js +0 -18
- package/dist/components/report/style2.css +0 -965
- package/dist/components/report/use-report-designer.js +0 -387
- package/dist/components/report/use-report-viewer.js +0 -140
- package/dist/report/binding.d.ts +0 -83
- package/dist/report/binding.js +0 -334
- package/dist/report/connector.d.ts +0 -66
- package/dist/report/connector.js +0 -159
- package/dist/report/drill.d.ts +0 -48
- package/dist/report/drill.js +0 -78
- package/dist/report/export-xlsx.d.ts +0 -13
- package/dist/report/export-xlsx.js +0 -13
- package/dist/report/filter-bar.d.ts +0 -10
- package/dist/report/filter-bar.js +0 -31
- package/dist/report/index.d.ts +0 -14
- package/dist/report/params.d.ts +0 -13
- package/dist/report/params.js +0 -61
- package/dist/report/print.d.ts +0 -23
- package/dist/report/print.js +0 -197
- package/dist/report/render/aggregate.js +0 -193
- package/dist/report/render/builder.d.ts +0 -12
- package/dist/report/render/builder.js +0 -139
- package/dist/report/render/coordinate.d.ts +0 -25
- package/dist/report/render/coordinate.js +0 -733
- package/dist/report/render/index.d.ts +0 -14
- package/dist/report/render/index.js +0 -21
- package/dist/report/render/style-resolver.js +0 -50
- package/dist/report/render/template-index.d.ts +0 -24
- package/dist/report/render/template-index.js +0 -137
- package/dist/report/rules/evaluate.d.ts +0 -25
- package/dist/report/rules/evaluate.js +0 -82
- package/dist/report/rules/index.d.ts +0 -2
- package/dist/report/template.d.ts +0 -63
- package/dist/report/template.js +0 -83
- package/dist/report/types.d.ts +0 -103
- package/dist/types/report-designer.d.ts +0 -51
- package/dist/types/report-viewer.d.ts +0 -57
- package/src/components/report/__test__/binding-badge.test.ts +0 -92
- package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
- package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
- package/src/components/report/__test__/float-panel-position.test.ts +0 -45
- package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
- package/src/components/report/__test__/report-designer.test.ts +0 -559
- package/src/components/report/__test__/report-drill.test.ts +0 -692
- package/src/components/report/__test__/report-viewer.test.ts +0 -400
- package/src/components/report/__test__/topology.test.ts +0 -143
- package/src/components/report/__test__/use-report-designer.test.ts +0 -804
- package/src/components/report/binding-badge.ts +0 -122
- package/src/components/report/designer/cell-coords.ts +0 -95
- package/src/components/report/designer/col-widths.ts +0 -56
- package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
- package/src/components/report/designer/drill-dialog.vue +0 -245
- package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
- package/src/components/report/designer/float-panel.vue +0 -444
- package/src/components/report/designer/role.ts +0 -32
- package/src/components/report/designer/rule-preview.vue +0 -40
- package/src/components/report/designer/rule-row.vue +0 -252
- package/src/components/report/designer/rules-dialog.vue +0 -110
- package/src/components/report/designer/topology-overlay.vue +0 -128
- package/src/components/report/designer/topology.ts +0 -124
- package/src/components/report/designer/use-grid-overlay.ts +0 -114
- package/src/components/report/designer-hub.vue +0 -198
- package/src/components/report/field-panel-helpers.ts +0 -44
- package/src/components/report/field-panel.vue +0 -136
- package/src/components/report/filter-bar.vue +0 -77
- package/src/components/report/hub-connection-form.vue +0 -124
- package/src/components/report/hub-dataset-editor.vue +0 -387
- package/src/components/report/index.ts +0 -2
- package/src/components/report/preview-grid-size.ts +0 -33
- package/src/components/report/print.ts +0 -44
- package/src/components/report/report-designer.vue +0 -454
- package/src/components/report/report-viewer.vue +0 -289
- package/src/components/report/style-designer.scss +0 -653
- package/src/components/report/style-hub.scss +0 -292
- package/src/components/report/style-viewer.scss +0 -144
- package/src/components/report/style.scss +0 -3
- package/src/components/report/style.ts +0 -20
- package/src/components/report/use-report-designer.ts +0 -603
- package/src/components/report/use-report-viewer.ts +0 -227
- package/src/report/__test__/binding.test.ts +0 -320
- package/src/report/__test__/connector.test.ts +0 -239
- package/src/report/__test__/drill.test.ts +0 -194
- package/src/report/__test__/export-xlsx.test.ts +0 -96
- package/src/report/__test__/filter-bar.test.ts +0 -58
- package/src/report/__test__/params.test.ts +0 -156
- package/src/report/__test__/print.test.ts +0 -170
- package/src/report/__test__/render.test.ts +0 -856
- package/src/report/__test__/rules.test.ts +0 -131
- package/src/report/__test__/template.test.ts +0 -366
- package/src/report/binding.ts +0 -474
- package/src/report/connector.ts +0 -206
- package/src/report/drill.ts +0 -112
- package/src/report/export-xlsx.ts +0 -15
- package/src/report/filter-bar.ts +0 -36
- package/src/report/index.ts +0 -16
- package/src/report/params.ts +0 -96
- package/src/report/print.ts +0 -234
- package/src/report/render/__test__/aggregate.test.ts +0 -16
- package/src/report/render/__test__/coordinate.test.ts +0 -405
- package/src/report/render/aggregate.ts +0 -289
- package/src/report/render/builder.ts +0 -211
- package/src/report/render/coordinate.ts +0 -1239
- package/src/report/render/index.ts +0 -30
- package/src/report/render/style-resolver.ts +0 -68
- package/src/report/render/template-index.ts +0 -200
- package/src/report/rules/evaluate.ts +0 -113
- package/src/report/rules/index.ts +0 -7
- package/src/report/template.ts +0 -145
- package/src/report/types.ts +0 -116
- package/src/types/report-designer.ts +0 -55
- package/src/types/report-viewer.ts +0 -54
package/src/report/drill.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import type { SheetSnapshot } from '@veltra/sheet-core'
|
|
2
|
-
import { cellKey } from '@veltra/sheet-core'
|
|
3
|
-
|
|
4
|
-
import { AggregateIndex, resolvePlacementContext } from './render/aggregate'
|
|
5
|
-
import { computeExpansionLayout } from './render/coordinate'
|
|
6
|
-
import { buildTemplateIndex } from './render/template-index'
|
|
7
|
-
import type { ReportTemplate } from './template'
|
|
8
|
-
import type { DatasetRecords, ParamValues, ReportDrillConfig } from './types'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 宿主模板解析契约:按下钻 target 引用取回目标模板(允许同步/异步)。
|
|
12
|
-
* 失败抛错——查看器提示可读错误并停留当前报;设计器配置对话框据此解析目标模板查询参数。
|
|
13
|
-
*/
|
|
14
|
-
export type ResolveReportTemplate = (ref: string) => ReportTemplate | Promise<ReportTemplate>
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 按下钻映射从该格对应记录取值,生成详情报 Filter Bar 参数值。
|
|
18
|
-
* 记录缺字段(值为 `undefined`)时跳过对应参数,由详情报回退默认值;空映射返回空参数。
|
|
19
|
-
*/
|
|
20
|
-
export function resolveDrillParams(
|
|
21
|
-
config: ReportDrillConfig,
|
|
22
|
-
record: Record<string, unknown>
|
|
23
|
-
): ParamValues {
|
|
24
|
-
const values: ParamValues = {}
|
|
25
|
-
for (const [field, paramId] of Object.entries(config.mapping)) {
|
|
26
|
-
const value = record[field]
|
|
27
|
-
if (value === undefined) continue
|
|
28
|
-
values[paramId] = value
|
|
29
|
-
}
|
|
30
|
-
return values
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 下钻栈层:一张报表模板 + 当时 Filter Bar 参数值(回退时据此恢复并重新取数) */
|
|
34
|
-
export interface DrillStackLayer {
|
|
35
|
-
template: ReportTemplate
|
|
36
|
-
params: ParamValues
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 下钻栈:自根(初始报表)到顶的层序列,至少一层。
|
|
41
|
-
* 只经下方纯函数做不可变更新(返回新栈),便于组件层直接挂响应式。
|
|
42
|
-
*/
|
|
43
|
-
export type DrillStack = DrillStackLayer[]
|
|
44
|
-
|
|
45
|
-
/** 以初始报表为根层创建下钻栈 */
|
|
46
|
-
export function createDrillStack(root: DrillStackLayer): DrillStack {
|
|
47
|
-
return [{ template: root.template, params: { ...root.params } }]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 压入详情报新层并使其成为当前层;`currentParams` 把当前层参数定格为离开时的值
|
|
52
|
-
* (缺省保留原值)。指向自身或成环不做检测,照常压栈,靠 pop 逐级退出。
|
|
53
|
-
*/
|
|
54
|
-
export function pushDrillLayer(
|
|
55
|
-
stack: DrillStack,
|
|
56
|
-
next: DrillStackLayer,
|
|
57
|
-
currentParams?: ParamValues
|
|
58
|
-
): DrillStack {
|
|
59
|
-
const top = stack[stack.length - 1]!
|
|
60
|
-
const layers = currentParams
|
|
61
|
-
? [...stack.slice(0, -1), { template: top.template, params: { ...currentParams } }]
|
|
62
|
-
: [...stack]
|
|
63
|
-
layers.push({ template: next.template, params: { ...next.params } })
|
|
64
|
-
return layers
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 弹出当前层返回上一层;仅剩根层时原样返回(查看器始终保留一张报表) */
|
|
68
|
-
export function popDrillLayer(stack: DrillStack): DrillStack {
|
|
69
|
-
if (stack.length <= 1) return stack
|
|
70
|
-
return stack.slice(0, -1)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** 当前层(栈顶) */
|
|
74
|
-
export function currentDrillLayer(stack: DrillStack): DrillStackLayer {
|
|
75
|
-
return stack[stack.length - 1]!
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 下钻命中:物理格对应的下钻配置与该格记录 */
|
|
79
|
-
export interface DrillHit {
|
|
80
|
-
config: ReportDrillConfig
|
|
81
|
-
record: Record<string, unknown>
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 构建填充报表的物理格 → 下钻命中索引(每次成功取数后重建)。
|
|
86
|
-
* 仅含配了下钻的绑定格落点(含分组/汇总格与展开后的实例格,合并跨度内每格都命中);
|
|
87
|
-
* 记录由 `resolvePlacementContext` 解析:list 明细取源数据行,分组/汇总取分组上下文。
|
|
88
|
-
*/
|
|
89
|
-
export function buildDrillHitMap(
|
|
90
|
-
template: SheetSnapshot,
|
|
91
|
-
data: DatasetRecords
|
|
92
|
-
): Map<number, DrillHit> {
|
|
93
|
-
const hits = new Map<number, DrillHit>()
|
|
94
|
-
const index = buildTemplateIndex(template)
|
|
95
|
-
const layout = computeExpansionLayout(index, data)
|
|
96
|
-
const aggregateIndex = new AggregateIndex(data)
|
|
97
|
-
|
|
98
|
-
for (const placement of layout.placements) {
|
|
99
|
-
const config = placement.binding?.drill
|
|
100
|
-
if (!config) continue
|
|
101
|
-
const record = resolvePlacementContext(placement, index, data, aggregateIndex)
|
|
102
|
-
if (!record) continue
|
|
103
|
-
|
|
104
|
-
const hit: DrillHit = { config, record }
|
|
105
|
-
for (let row = placement.physical.start.row; row <= placement.physical.end.row; row++) {
|
|
106
|
-
for (let col = placement.physical.start.col; col <= placement.physical.end.col; col++) {
|
|
107
|
-
hits.set(cellKey({ row, col }), hit)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return hits
|
|
112
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Sheet } from '@veltra/sheet-core'
|
|
2
|
-
import { exportSheetXlsx } from '@veltra/sheet-core/core/io/export'
|
|
3
|
-
|
|
4
|
-
/** 列宽条目:模型列索引 → 像素宽(与 SheetSnapshot.colWidths 同构) */
|
|
5
|
-
export type ReportColWidthEntry = readonly [number, number]
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 将已填充的 Sheet(Filled Report)导出为保真 XLSX(合并 / 样式 / 行高 / 列宽 / 浮动图)。
|
|
9
|
-
* 组装与序列化委托 sheet-core `exportSheetXlsx`(与 `exportWorkbookXlsx` 同一套单表逻辑);
|
|
10
|
-
* 条件样式颜色已在 renderReport 展开阶段打平进 StylePool(ADR-0001 决策 2)。
|
|
11
|
-
* 列宽取自 sheet 模型(`getColWidths`)。
|
|
12
|
-
*/
|
|
13
|
-
export function exportFilledReportXlsx(sheet: Sheet): Promise<Uint8Array> {
|
|
14
|
-
return exportSheetXlsx(sheet, { fallbackName: '报表' })
|
|
15
|
-
}
|
package/src/report/filter-bar.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ParamValues } from './types'
|
|
2
|
-
|
|
3
|
-
// Filter Bar 控件值规范化(自 playground filter-bar-helpers 平移,行为不变)
|
|
4
|
-
|
|
5
|
-
function toParamString(value: unknown): string {
|
|
6
|
-
if (value == null || value === '') return ''
|
|
7
|
-
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
8
|
-
return String(value)
|
|
9
|
-
}
|
|
10
|
-
return ''
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** 将运行时 date-range 参数值规范为 [from, to] 字符串元组 */
|
|
14
|
-
export function parseDateRangeValue(raw: unknown): [string, string] {
|
|
15
|
-
if (Array.isArray(raw)) {
|
|
16
|
-
return [toParamString(raw[0]), toParamString(raw[1])]
|
|
17
|
-
}
|
|
18
|
-
if (raw && typeof raw === 'object') {
|
|
19
|
-
const obj = raw as Record<string, unknown>
|
|
20
|
-
return [toParamString(obj.from ?? obj.start), toParamString(obj.to ?? obj.end)]
|
|
21
|
-
}
|
|
22
|
-
return ['', '']
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 将运行时 number 参数值规范为 UNumberInput 可接受的 number | undefined */
|
|
26
|
-
export function resolveNumberParamValue(raw: unknown): number | undefined {
|
|
27
|
-
if (raw === '' || raw == null) return undefined
|
|
28
|
-
if (typeof raw === 'number' && Number.isFinite(raw)) return raw
|
|
29
|
-
const parsed = Number(raw)
|
|
30
|
-
return Number.isFinite(parsed) ? parsed : undefined
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 合并单参数变更,保持其余参数值不变 */
|
|
34
|
-
export function patchParamValues(values: ParamValues, id: string, value: unknown): ParamValues {
|
|
35
|
-
return { ...values, [id]: value }
|
|
36
|
-
}
|
package/src/report/index.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// 报表纯 TS 内核(ADR-0003 决策 1):渲染引擎 / 绑定 / 条件规则 / 查询参数 / 数据连接器 /
|
|
2
|
-
// Report Template(自包含模板与取数)/ Filter Bar 值规范化 / 下钻配置与下钻栈 /
|
|
3
|
-
// Filled Report XLSX 导出与打印 HTML。
|
|
4
|
-
// 框架无关、无 DOM 依赖;组件壳(UReportDesigner / UReportViewer)落 components/report/。
|
|
5
|
-
|
|
6
|
-
export * from './types'
|
|
7
|
-
export * from './binding'
|
|
8
|
-
export * from './rules'
|
|
9
|
-
export * from './params'
|
|
10
|
-
export * from './render'
|
|
11
|
-
export * from './connector'
|
|
12
|
-
export * from './template'
|
|
13
|
-
export * from './filter-bar'
|
|
14
|
-
export * from './drill'
|
|
15
|
-
export * from './export-xlsx'
|
|
16
|
-
export * from './print'
|
package/src/report/params.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { QueryParamDef, QueryParamType } from './types'
|
|
2
|
-
|
|
3
|
-
/** 从 SQL 提取 `${param}` 占位符 id(ADR-0002 决策 2:前端纯函数) */
|
|
4
|
-
export function extractParamIds(sql: string): string[] {
|
|
5
|
-
const ids: string[] = []
|
|
6
|
-
const re = /\$\{([^}]+)\}/g
|
|
7
|
-
let match: RegExpExecArray | null
|
|
8
|
-
while ((match = re.exec(sql)) !== null) {
|
|
9
|
-
const id = match[1]!.trim()
|
|
10
|
-
if (id && !ids.includes(id)) ids.push(id)
|
|
11
|
-
}
|
|
12
|
-
return ids
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function escapeRegExp(value: string): string {
|
|
16
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 从 SQL 上下文推断参数控件类型(BETWEEN/LIKE/数值比较) */
|
|
20
|
-
function inferParamTypeFromSql(sql: string, paramId: string): QueryParamType {
|
|
21
|
-
const betweenSingle = new RegExp(
|
|
22
|
-
`\\bBETWEEN\\s+\\$\\{${escapeRegExp(paramId)}\\}(?!\\s+AND)`,
|
|
23
|
-
'i'
|
|
24
|
-
)
|
|
25
|
-
if (betweenSingle.test(sql)) return 'date-range'
|
|
26
|
-
|
|
27
|
-
const betweenPair = new RegExp(
|
|
28
|
-
`\\bBETWEEN\\s+\\$\\{${escapeRegExp(paramId)}\\}\\s+AND\\s+\\$\\{([^}]+)\\}`,
|
|
29
|
-
'i'
|
|
30
|
-
)
|
|
31
|
-
const pairMatch = betweenPair.exec(sql)
|
|
32
|
-
if (pairMatch && pairMatch[1]!.trim() !== paramId) return 'date'
|
|
33
|
-
|
|
34
|
-
const like = new RegExp(`\\bLIKE\\s+\\$\\{${escapeRegExp(paramId)}\\}`, 'i')
|
|
35
|
-
if (like.test(sql)) return 'text'
|
|
36
|
-
|
|
37
|
-
const numericCompare = new RegExp(
|
|
38
|
-
`[<>=]+\\s+\\$\\{${escapeRegExp(paramId)}\\}|\\$\\{${escapeRegExp(paramId)}\\}\\s*[<>=]+`,
|
|
39
|
-
'i'
|
|
40
|
-
)
|
|
41
|
-
if (numericCompare.test(sql)) return 'number'
|
|
42
|
-
|
|
43
|
-
return 'text'
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function defaultValueForType(type: QueryParamType): unknown {
|
|
47
|
-
switch (type) {
|
|
48
|
-
case 'number':
|
|
49
|
-
return 0
|
|
50
|
-
case 'date-range':
|
|
51
|
-
return ['', '']
|
|
52
|
-
case 'select':
|
|
53
|
-
return ''
|
|
54
|
-
default:
|
|
55
|
-
return ''
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 从 SQL 构建查询参数定义:提取 `${param}` 占位符 + 上下文类型推断,
|
|
61
|
-
* 再叠加数据集编辑器的元数据覆盖(label / 类型 / 默认值 / 选项)。
|
|
62
|
-
*/
|
|
63
|
-
export function buildParamDefs(
|
|
64
|
-
sql: string,
|
|
65
|
-
overrides?: Record<string, Partial<Omit<QueryParamDef, 'id'>>>
|
|
66
|
-
): QueryParamDef[] {
|
|
67
|
-
const ids = extractParamIds(sql)
|
|
68
|
-
return ids.map((id) => {
|
|
69
|
-
const inferred = inferParamTypeFromSql(sql, id)
|
|
70
|
-
const override = overrides?.[id]
|
|
71
|
-
return {
|
|
72
|
-
id,
|
|
73
|
-
label: override?.label ?? id,
|
|
74
|
-
type: override?.type ?? inferred,
|
|
75
|
-
defaultValue: override?.defaultValue ?? defaultValueForType(override?.type ?? inferred),
|
|
76
|
-
options: override?.options
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** 按绑定的数据集 id 合并查询参数(同名参数先见为准) */
|
|
82
|
-
export function resolveBoundDatasetParams(
|
|
83
|
-
queryParams: QueryParamDef[],
|
|
84
|
-
boundDatasetIds: readonly string[],
|
|
85
|
-
getDatasetParams: (datasetId: string) => QueryParamDef[]
|
|
86
|
-
): QueryParamDef[] {
|
|
87
|
-
const map = new Map<string, QueryParamDef>()
|
|
88
|
-
for (const datasetId of boundDatasetIds) {
|
|
89
|
-
const params = getDatasetParams(datasetId)
|
|
90
|
-
for (const param of params) {
|
|
91
|
-
if (!map.has(param.id)) map.set(param.id, param)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (map.size > 0) return Array.from(map.values())
|
|
95
|
-
return queryParams
|
|
96
|
-
}
|
package/src/report/print.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BorderLineStyle,
|
|
3
|
-
CellStyle,
|
|
4
|
-
Sheet,
|
|
5
|
-
SheetImage,
|
|
6
|
-
SheetImageType
|
|
7
|
-
} from '@veltra/sheet-core'
|
|
8
|
-
import { BORDER_SIDES } from '@veltra/sheet-core'
|
|
9
|
-
|
|
10
|
-
/** 报表打印选项 */
|
|
11
|
-
export interface ReportPrintOptions {
|
|
12
|
-
/** 打印文档标题(打印对话框标题 / 另存 PDF 文件名);缺省取 sheet 名,再回落「报表」 */
|
|
13
|
-
title?: string
|
|
14
|
-
/** 纸张方向(@page size),默认 portrait */
|
|
15
|
-
orientation?: 'portrait' | 'landscape'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 默认列宽 / 行高(px):与 grid 层 vtable-theme 的
|
|
20
|
-
* SHEET_DEFAULT_COL_WIDTH / SHEET_DEFAULT_ROW_HEIGHT 对齐;
|
|
21
|
-
* report 内核保持 headless,不引 grid 入口。
|
|
22
|
-
*/
|
|
23
|
-
const PRINT_DEFAULT_COL_WIDTH = 80
|
|
24
|
-
const PRINT_DEFAULT_ROW_HEIGHT = 28
|
|
25
|
-
|
|
26
|
-
const IMAGE_MIME: Record<SheetImageType, string> = {
|
|
27
|
-
png: 'image/png',
|
|
28
|
-
jpeg: 'image/jpeg',
|
|
29
|
-
gif: 'image/gif',
|
|
30
|
-
svg: 'image/svg+xml',
|
|
31
|
-
webp: 'image/webp'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 模型边框线型 → CSS 线型(thin/medium/thick 为实线粗细分级,宽度由 edge.width 表达) */
|
|
35
|
-
const BORDER_CSS_LINE: Record<BorderLineStyle, string> = {
|
|
36
|
-
thin: 'solid',
|
|
37
|
-
medium: 'solid',
|
|
38
|
-
thick: 'solid',
|
|
39
|
-
dashed: 'dashed',
|
|
40
|
-
dotted: 'dotted'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function escapeHtml(text: string): string {
|
|
44
|
-
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function escapeAttr(text: string): string {
|
|
48
|
-
return escapeHtml(text).replace(/"/g, '"')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Uint8Array → base64(分块避免栈溢出;btoa 在浏览器 / Node >=16 / Bun / happy-dom 均可用) */
|
|
52
|
-
function bytesToBase64(data: Uint8Array): string {
|
|
53
|
-
let binary = ''
|
|
54
|
-
const CHUNK = 0x8000
|
|
55
|
-
for (let i = 0; i < data.length; i += CHUNK) {
|
|
56
|
-
binary += String.fromCharCode(...data.subarray(i, i + CHUNK))
|
|
57
|
-
}
|
|
58
|
-
return btoa(binary)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 模型有效样式 → td 内联 CSS(字号 pt 直出,与屏幕 px ×4/3 物理尺寸一致) */
|
|
62
|
-
function styleToCss(style: CellStyle): string {
|
|
63
|
-
const decls: string[] = []
|
|
64
|
-
if (style.fill?.color) decls.push(`background-color:${style.fill.color}`)
|
|
65
|
-
for (const side of BORDER_SIDES) {
|
|
66
|
-
const edge = style.border?.[side]
|
|
67
|
-
if (edge)
|
|
68
|
-
decls.push(`border-${side}:${edge.width}px ${BORDER_CSS_LINE[edge.style]} ${edge.color}`)
|
|
69
|
-
}
|
|
70
|
-
const font = style.font
|
|
71
|
-
if (font) {
|
|
72
|
-
if (font.color) decls.push(`color:${font.color}`)
|
|
73
|
-
if (font.bold) decls.push('font-weight:bold')
|
|
74
|
-
if (font.italic) decls.push('font-style:italic')
|
|
75
|
-
const decoration = [font.underline ? 'underline' : '', font.strikethrough ? 'line-through' : '']
|
|
76
|
-
.filter(Boolean)
|
|
77
|
-
.join(' ')
|
|
78
|
-
if (decoration) decls.push(`text-decoration:${decoration}`)
|
|
79
|
-
if (typeof font.size === 'number') decls.push(`font-size:${font.size}pt`)
|
|
80
|
-
}
|
|
81
|
-
const align = style.align
|
|
82
|
-
if (align) {
|
|
83
|
-
if (align.horizontal) decls.push(`text-align:${align.horizontal}`)
|
|
84
|
-
if (align.vertical) decls.push(`vertical-align:${align.vertical}`)
|
|
85
|
-
if (align.wrap) decls.push('white-space:normal;word-break:break-all')
|
|
86
|
-
}
|
|
87
|
-
return decls.join(';')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface PrintImageRect {
|
|
91
|
-
left: number
|
|
92
|
-
top: number
|
|
93
|
-
width?: number
|
|
94
|
-
height?: number
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 浮动图打印定位(镜像 grid image-layer `computeImageRect`):
|
|
99
|
-
* left/top = from 格左上 + 格内像素偏移;宽高优先 image.width/height(两者都有才用),
|
|
100
|
-
* 缺失且有 to 时按 from→to 跨度兜底,都缺失则省略尺寸(浏览器取自然尺寸)。
|
|
101
|
-
*/
|
|
102
|
-
function computePrintImageRect(
|
|
103
|
-
image: SheetImage,
|
|
104
|
-
colWidths: readonly number[],
|
|
105
|
-
rowHeights: readonly number[]
|
|
106
|
-
): PrintImageRect {
|
|
107
|
-
const { from, to } = image.anchor
|
|
108
|
-
let left = from.offsetX ?? 0
|
|
109
|
-
for (let c = 0; c < from.col; c++) left += colWidths[c] ?? PRINT_DEFAULT_COL_WIDTH
|
|
110
|
-
let top = from.offsetY ?? 0
|
|
111
|
-
for (let r = 0; r < from.row; r++) top += rowHeights[r] ?? PRINT_DEFAULT_ROW_HEIGHT
|
|
112
|
-
|
|
113
|
-
if (image.width != null && image.height != null) {
|
|
114
|
-
return { left, top, width: image.width, height: image.height }
|
|
115
|
-
}
|
|
116
|
-
if (to) {
|
|
117
|
-
let width = 0
|
|
118
|
-
for (let c = from.col; c <= to.col; c++) width += colWidths[c] ?? PRINT_DEFAULT_COL_WIDTH
|
|
119
|
-
let height = 0
|
|
120
|
-
for (let r = from.row; r <= to.row; r++) height += rowHeights[r] ?? PRINT_DEFAULT_ROW_HEIGHT
|
|
121
|
-
return { left, top, width, height }
|
|
122
|
-
}
|
|
123
|
-
return { left, top }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* 将已填充的 Sheet(Filled Report)构建为打印专用 HTML 文档(纯 TS、headless、无 DOM)。
|
|
128
|
-
*
|
|
129
|
-
* 路线说明:网格渲染层是 canvas + 虚拟滚动(视口外不绘制),无法直接 window.print;
|
|
130
|
-
* 这里从模型单一事实源(与 exportFilledReportXlsx 同一读取路径)重建 `<table>`——
|
|
131
|
-
* 矢量文字清晰、浏览器原生分页、合并格天然映射 rowspan/colspan。
|
|
132
|
-
*
|
|
133
|
-
* 保真范围:值 / 合并 / 有效样式(fill + border + font + align/wrap,条件样式已在
|
|
134
|
-
* 展开阶段打平进 StylePool)/ 行高 / 列宽 / 浮动图(data URL 内嵌,跨页被裁剪属预期)。
|
|
135
|
-
* 与 Excel 默认一致:不打印屏幕网格线(仅模型显式边框)。
|
|
136
|
-
*/
|
|
137
|
-
export function buildFilledReportPrintHtml(sheet: Sheet, options?: ReportPrintOptions): string {
|
|
138
|
-
const title = options?.title ?? sheet.name ?? ''
|
|
139
|
-
const orientation = options?.orientation ?? 'portrait'
|
|
140
|
-
|
|
141
|
-
// 打印范围 = 数据 ∪ 行高 ∪ 列宽 ∪ 合并 ∪ 图片锚点 包围盒(与 xlsx 导出收敛逻辑一致 + 合并/图片)
|
|
142
|
-
let maxRow = -1
|
|
143
|
-
let maxCol = -1
|
|
144
|
-
const grow = (row: number, col: number): void => {
|
|
145
|
-
if (row > maxRow) maxRow = row
|
|
146
|
-
if (col > maxCol) maxCol = col
|
|
147
|
-
}
|
|
148
|
-
for (const [addr] of sheet.store.peekEntries()) grow(addr.row, addr.col)
|
|
149
|
-
for (const [row] of sheet.getRowHeights()) grow(row, 0)
|
|
150
|
-
for (const [col] of sheet.getColWidths()) grow(0, col)
|
|
151
|
-
const merges = sheet.merges.getMerges()
|
|
152
|
-
for (const range of merges) grow(range.end.row, range.end.col)
|
|
153
|
-
const images = sheet.getImages()
|
|
154
|
-
for (const image of images) {
|
|
155
|
-
grow(image.anchor.from.row, image.anchor.from.col)
|
|
156
|
-
if (image.anchor.to) grow(image.anchor.to.row, image.anchor.to.col)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const colWidths: number[] = []
|
|
160
|
-
for (let c = 0; c <= maxCol; c++) colWidths.push(sheet.getColWidth(c) ?? PRINT_DEFAULT_COL_WIDTH)
|
|
161
|
-
const rowHeights: number[] = []
|
|
162
|
-
for (let r = 0; r <= maxRow; r++)
|
|
163
|
-
rowHeights.push(sheet.getRowHeight(r) ?? PRINT_DEFAULT_ROW_HEIGHT)
|
|
164
|
-
|
|
165
|
-
// 合并:锚点 → 跨度;被覆盖格集合(跳过客格)
|
|
166
|
-
const spanByAnchor = new Map<string, { rowSpan: number; colSpan: number }>()
|
|
167
|
-
const covered = new Set<string>()
|
|
168
|
-
for (const range of merges) {
|
|
169
|
-
const rowSpan = range.end.row - range.start.row + 1
|
|
170
|
-
const colSpan = range.end.col - range.start.col + 1
|
|
171
|
-
spanByAnchor.set(`${range.start.row},${range.start.col}`, { rowSpan, colSpan })
|
|
172
|
-
for (let r = range.start.row; r <= range.end.row; r++) {
|
|
173
|
-
for (let c = range.start.col; c <= range.end.col; c++) {
|
|
174
|
-
if (r !== range.start.row || c !== range.start.col) covered.add(`${r},${c}`)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const css = [
|
|
180
|
-
`@page { size: A4 ${orientation}; margin: 12mm; }`,
|
|
181
|
-
'html, body { margin: 0; padding: 0; }',
|
|
182
|
-
"body { font-family: Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; font-size: 12px; color: #1f2329; -webkit-print-color-adjust: exact; print-color-adjust: exact; }",
|
|
183
|
-
'.sheet-print-root { position: relative; }',
|
|
184
|
-
'table { border-collapse: collapse; table-layout: fixed; }',
|
|
185
|
-
'tr { break-inside: avoid; }',
|
|
186
|
-
'td { box-sizing: border-box; padding: 2px 6px; overflow: hidden; white-space: nowrap; vertical-align: middle; text-align: left; }',
|
|
187
|
-
'.sheet-print-root img { position: absolute; }'
|
|
188
|
-
].join('\n')
|
|
189
|
-
|
|
190
|
-
const parts: string[] = []
|
|
191
|
-
parts.push('<!doctype html><html><head><meta charset="utf-8">')
|
|
192
|
-
parts.push(`<title>${escapeHtml(title || '报表')}</title>`)
|
|
193
|
-
parts.push(`<style>\n${css}\n</style></head><body>`)
|
|
194
|
-
parts.push('<div class="sheet-print-root">')
|
|
195
|
-
parts.push('<table>')
|
|
196
|
-
if (colWidths.length > 0) {
|
|
197
|
-
parts.push('<colgroup>')
|
|
198
|
-
for (const width of colWidths) parts.push(`<col style="width:${width}px">`)
|
|
199
|
-
parts.push('</colgroup>')
|
|
200
|
-
}
|
|
201
|
-
for (let r = 0; r <= maxRow; r++) {
|
|
202
|
-
parts.push(`<tr style="height:${rowHeights[r]}px">`)
|
|
203
|
-
for (let c = 0; c <= maxCol; c++) {
|
|
204
|
-
if (covered.has(`${r},${c}`)) continue
|
|
205
|
-
const attrs: string[] = []
|
|
206
|
-
const span = spanByAnchor.get(`${r},${c}`)
|
|
207
|
-
if (span && span.colSpan > 1) attrs.push(`colspan="${span.colSpan}"`)
|
|
208
|
-
if (span && span.rowSpan > 1) attrs.push(`rowspan="${span.rowSpan}"`)
|
|
209
|
-
const style = sheet.getEffectiveStyle({ row: r, col: c })
|
|
210
|
-
const cellCss = style ? styleToCss(style) : ''
|
|
211
|
-
if (cellCss) attrs.push(`style="${escapeAttr(cellCss)}"`)
|
|
212
|
-
const value = sheet.getDisplayValue({ row: r, col: c })
|
|
213
|
-
const text = value == null ? '' : escapeHtml(String(value))
|
|
214
|
-
parts.push(`<td${attrs.length > 0 ? ` ${attrs.join(' ')}` : ''}>${text}</td>`)
|
|
215
|
-
}
|
|
216
|
-
parts.push('</tr>')
|
|
217
|
-
}
|
|
218
|
-
parts.push('</table>')
|
|
219
|
-
|
|
220
|
-
for (const image of images) {
|
|
221
|
-
const rect = computePrintImageRect(image, colWidths, rowHeights)
|
|
222
|
-
const size =
|
|
223
|
-
rect.width != null && rect.height != null
|
|
224
|
-
? `width:${rect.width}px;height:${rect.height}px;`
|
|
225
|
-
: ''
|
|
226
|
-
const src = `data:${IMAGE_MIME[image.type]};base64,${bytesToBase64(image.data)}`
|
|
227
|
-
parts.push(
|
|
228
|
-
`<img src="${src}" style="left:${rect.left}px;top:${rect.top}px;${size}" alt="${escapeAttr(image.altText ?? '')}">`
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
parts.push('</div></body></html>')
|
|
233
|
-
return parts.join('')
|
|
234
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { aggregateField } from '../aggregate'
|
|
4
|
-
|
|
5
|
-
describe('aggregateField', () => {
|
|
6
|
-
it('avg 空集返回 undefined,sum 空集仍为 0', () => {
|
|
7
|
-
expect(aggregateField([], 'amount', 'avg')).toBeUndefined()
|
|
8
|
-
expect(aggregateField([], 'amount', 'sum')).toBe(0)
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('max / min 对数值集求值', () => {
|
|
12
|
-
const rows = [{ amount: 10 }, { amount: 30 }, { amount: 20 }]
|
|
13
|
-
expect(aggregateField(rows, 'amount', 'max')).toBe(30)
|
|
14
|
-
expect(aggregateField(rows, 'amount', 'min')).toBe(10)
|
|
15
|
-
})
|
|
16
|
-
})
|