@veltra/sheet 2.3.3 → 2.4.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 +23 -5
- package/dist/components/report/designer/drill-dialog.js +258 -0
- package/dist/components/report/designer/float-panel.js +26 -14
- package/dist/components/report/print.js +39 -0
- package/dist/components/report/report-designer.js +56 -21
- package/dist/components/report/report-viewer.js +173 -44
- package/dist/components/report/style2.css +91 -0
- package/dist/components/report/use-report-designer.js +2 -1
- package/dist/components/report/use-report-viewer.js +88 -3
- package/dist/components/sheet/insert-image.js +1 -1
- package/dist/components/sheet/popups/import.worker.js +2 -3
- package/dist/components/sheet/use-sheet-state.js +2 -2
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/report/drill.d.ts +48 -0
- package/dist/report/drill.js +78 -0
- package/dist/report/export-xlsx.d.ts +3 -7
- package/dist/report/export-xlsx.js +6 -85
- package/dist/report/index.d.ts +5 -3
- package/dist/report/print.d.ts +23 -0
- package/dist/report/print.js +197 -0
- package/dist/report/render/aggregate.js +25 -1
- package/dist/report/types.d.ts +19 -1
- package/dist/tools/download.js +2 -2
- package/dist/types/report-designer.d.ts +13 -0
- package/dist/types/report-viewer.d.ts +19 -0
- package/package.json +6 -9
- package/src/components/report/__test__/binding-badge.test.ts +92 -0
- package/src/components/report/__test__/report-designer.test.ts +98 -3
- package/src/components/report/__test__/report-drill.test.ts +692 -0
- package/src/components/report/__test__/report-viewer.test.ts +43 -0
- package/src/components/report/__test__/use-report-designer.test.ts +108 -1
- package/src/components/report/binding-badge.ts +35 -5
- package/src/components/report/designer/drill-dialog.vue +245 -0
- package/src/components/report/designer/float-panel.vue +12 -0
- package/src/components/report/print.ts +44 -0
- package/src/components/report/report-designer.vue +35 -2
- package/src/components/report/report-viewer.vue +171 -18
- package/src/components/report/style-designer.scss +66 -0
- package/src/components/report/style-viewer.scss +40 -0
- package/src/components/report/use-report-designer.ts +8 -1
- package/src/components/report/use-report-viewer.ts +150 -11
- package/src/components/sheet/insert-image.ts +1 -1
- package/src/components/sheet/popups/export-popup.vue +1 -1
- package/src/components/sheet/popups/import.worker.ts +7 -10
- package/src/components/sheet/use-sheet-state.ts +2 -2
- package/src/report/__test__/drill.test.ts +194 -0
- package/src/report/__test__/export-xlsx.test.ts +3 -25
- package/src/report/__test__/print.test.ts +170 -0
- package/src/report/__test__/render.test.ts +26 -0
- package/src/report/drill.ts +112 -0
- package/src/report/export-xlsx.ts +5 -103
- package/src/report/index.ts +4 -1
- package/src/report/print.ts +234 -0
- package/src/report/render/aggregate.ts +28 -0
- package/src/report/types.ts +20 -0
- package/src/tools/__test__/builtin.test.ts +1 -1
- package/src/tools/download.ts +2 -2
- package/src/tools/export.worker.ts +2 -2
- package/src/types/report-designer.ts +18 -1
- package/src/types/report-viewer.ts +18 -1
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
@update:values="setValues"
|
|
8
8
|
/>
|
|
9
9
|
|
|
10
|
+
<div v-if="canDrillBack" :class="cls.e('drill-bar')">
|
|
11
|
+
<u-button size="small" plain @click="drillBack">返回上一层</u-button>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
10
14
|
<div :class="cls.e('body')">
|
|
11
15
|
<div v-if="loading" :class="cls.e('loading')" aria-live="polite">
|
|
12
16
|
<div :class="cls.e('loading-spinner')" />
|
|
@@ -18,7 +22,7 @@
|
|
|
18
22
|
|
|
19
23
|
<u-sheet
|
|
20
24
|
ref="sheetRef"
|
|
21
|
-
:class="cls.e('sheet')"
|
|
25
|
+
:class="[cls.e('sheet'), drillHover ? cls.em('sheet', 'drill-hover') : '']"
|
|
22
26
|
:workbook="workbook"
|
|
23
27
|
:rows="renderSize.rows"
|
|
24
28
|
:cols="renderSize.cols"
|
|
@@ -27,31 +31,61 @@
|
|
|
27
31
|
:show-tabs="false"
|
|
28
32
|
:show-row-header="false"
|
|
29
33
|
:show-col-header="false"
|
|
34
|
+
:resolve-cell-style="resolveViewerCellStyle"
|
|
30
35
|
readonly
|
|
36
|
+
@pointerdown="handleGridPointerDown"
|
|
37
|
+
@pointermove="handleGridPointerMove"
|
|
38
|
+
@pointerleave="handleGridPointerLeave"
|
|
39
|
+
@click="handleGridClick"
|
|
31
40
|
/>
|
|
32
41
|
</div>
|
|
42
|
+
|
|
43
|
+
<u-dialog
|
|
44
|
+
v-if="drillDialog"
|
|
45
|
+
v-model="drillDialogVisible"
|
|
46
|
+
title="详情报表"
|
|
47
|
+
:class="cls.e('drill-dialog')"
|
|
48
|
+
>
|
|
49
|
+
<u-report-viewer
|
|
50
|
+
:connector="connector"
|
|
51
|
+
:template="drillDialog.template"
|
|
52
|
+
:initial-values="drillDialog.params"
|
|
53
|
+
:resolve-template="resolveTemplate"
|
|
54
|
+
/>
|
|
55
|
+
</u-dialog>
|
|
33
56
|
</div>
|
|
34
57
|
</template>
|
|
35
58
|
|
|
36
59
|
<script lang="ts" setup>
|
|
37
60
|
import { saveBlob } from '@cat-kit/fe'
|
|
38
|
-
import
|
|
61
|
+
import { UButton, UDialog } from '@veltra/desktop'
|
|
62
|
+
import type { CellAddress, SheetSnapshot } from '@veltra/sheet-core'
|
|
39
63
|
import { Workbook } from '@veltra/sheet-core'
|
|
64
|
+
import type { ResolveCellStyleHook } from '@veltra/sheet-core/grid'
|
|
40
65
|
import { bem } from '@veltra/utils'
|
|
41
|
-
import { computed, nextTick, useTemplateRef, watch } from 'vue'
|
|
66
|
+
import { computed, nextTick, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
|
42
67
|
|
|
43
68
|
import { exportFilledReportXlsx } from '../../report/export-xlsx'
|
|
44
|
-
import
|
|
45
|
-
import type {
|
|
69
|
+
import { buildFilledReportPrintHtml, type ReportPrintOptions } from '../../report/print'
|
|
70
|
+
import type { ReportTemplate } from '../../report/template'
|
|
71
|
+
import type { ParamValues, ReportDrillConfig } from '../../report/types'
|
|
72
|
+
import type { ReportViewerProps, SheetExposed, _ReportViewerExposed } from '../../types'
|
|
46
73
|
import { USheet } from '../sheet'
|
|
47
74
|
import { applyGridColWidths, applySheetColWidths } from './designer/col-widths'
|
|
48
75
|
import UReportFilterBar from './filter-bar.vue'
|
|
49
76
|
import { previewGridSize, type PreviewGridSizeMode } from './preview-grid-size'
|
|
77
|
+
import { printHtmlDocument } from './print'
|
|
50
78
|
import { useReportViewer } from './use-report-viewer'
|
|
51
79
|
|
|
52
80
|
defineOptions({ name: 'UReportViewer' })
|
|
53
81
|
|
|
54
|
-
|
|
82
|
+
/** 与 `ReportViewerProps` 交叉显式列出下钻 props,避免 SFC 对跨文件 interface 漏抽 runtime 声明 */
|
|
83
|
+
type ViewerRuntimeProps = ReportViewerProps & {
|
|
84
|
+
resolveTemplate?: ReportViewerProps['resolveTemplate']
|
|
85
|
+
initialValues?: ReportViewerProps['initialValues']
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const props = defineProps<ViewerRuntimeProps>()
|
|
55
89
|
|
|
56
90
|
const cls = bem('report-viewer')
|
|
57
91
|
|
|
@@ -59,26 +93,69 @@ const cls = bem('report-viewer')
|
|
|
59
93
|
const EXPORT_NOT_READY_MESSAGE = '报表数据尚未就绪,请等待取数完成后再导出'
|
|
60
94
|
const EXPORT_LOADING_MESSAGE = '报表数据加载中,请稍后再导出'
|
|
61
95
|
|
|
96
|
+
/** 取数未完成时拒绝打印的可读错误 */
|
|
97
|
+
const PRINT_NOT_READY_MESSAGE = '报表数据尚未就绪,请等待取数完成后再打印'
|
|
98
|
+
const PRINT_LOADING_MESSAGE = '报表数据加载中,请稍后再打印'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 单击 / 拖拽框选判定位移阈值(px):pointerdown 与 click 位移超过它视为拖选,不触发下钻。
|
|
102
|
+
* (VTable 拖选后 DOM click 仍会冒泡,按位移自行抑制。)
|
|
103
|
+
*/
|
|
104
|
+
const DRAG_CLICK_THRESHOLD_PX = 4
|
|
105
|
+
|
|
62
106
|
/** 查看器工作簿:宿主可注入(同 USheet `workbook?` 先例),缺省内部自建;模板 / 填充结果都 restore 进活动 sheet 只读展示 */
|
|
63
107
|
const internalWorkbook = new Workbook()
|
|
64
108
|
const workbook = computed(() => props.workbook ?? internalWorkbook)
|
|
65
109
|
|
|
66
110
|
const sheetRef = useTemplateRef<SheetExposed>('sheetRef')
|
|
67
111
|
|
|
68
|
-
const {
|
|
69
|
-
|
|
112
|
+
const {
|
|
113
|
+
params,
|
|
114
|
+
values,
|
|
115
|
+
loading,
|
|
116
|
+
error,
|
|
117
|
+
filledSnapshot,
|
|
118
|
+
filledColWidths,
|
|
119
|
+
currentTemplate,
|
|
120
|
+
canDrillBack,
|
|
121
|
+
resolveDrillHit,
|
|
122
|
+
resolveDrillTarget,
|
|
123
|
+
drillInto,
|
|
124
|
+
drillBack,
|
|
125
|
+
refresh,
|
|
126
|
+
setValues
|
|
127
|
+
} = useReportViewer(props)
|
|
128
|
+
|
|
129
|
+
/** 配了下钻的单元格在查看态展示可点击链接样式(蓝字 + 下划线),便于用户直观感知 */
|
|
130
|
+
const resolveViewerCellStyle: ResolveCellStyleHook = (addr, base) => {
|
|
131
|
+
if (!props.resolveTemplate) return base
|
|
132
|
+
const hit = resolveDrillHit(addr)
|
|
133
|
+
if (!hit) return base
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
font: {
|
|
137
|
+
...base?.font,
|
|
138
|
+
color: base?.font?.color ?? '#2f54eb',
|
|
139
|
+
underline: base?.font?.underline ?? true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 弹框下钻:先挂 UDialog 再打开,避免 modelValue 初始为 true 时 overlay 不进入 */
|
|
145
|
+
const drillDialogVisible = ref(false)
|
|
146
|
+
const drillDialog = shallowRef<{ template: ReportTemplate; params: ParamValues } | null>(null)
|
|
70
147
|
|
|
71
148
|
const renderSize = computed(() => {
|
|
72
149
|
const filled = filledSnapshot.value
|
|
73
150
|
if (filled) return previewGridSize(filled, 'filled')
|
|
74
|
-
return previewGridSize(
|
|
151
|
+
return previewGridSize(currentTemplate.value, 'template')
|
|
75
152
|
})
|
|
76
153
|
|
|
77
|
-
/**
|
|
154
|
+
/** 宿主显式传入优先;否则取数完成后用展开映射列宽;再回落当前层模板设计态列宽 */
|
|
78
155
|
const effectiveColWidths = computed(() => {
|
|
79
156
|
if (props.colWidths?.length) return props.colWidths
|
|
80
157
|
if (filledColWidths.value?.length) return filledColWidths.value
|
|
81
|
-
return
|
|
158
|
+
return currentTemplate.value.colWidths
|
|
82
159
|
})
|
|
83
160
|
|
|
84
161
|
/** 将运行时列宽写入模型与 VTable(模型为单一事实源;grid 构造/content-reset 也会回放) */
|
|
@@ -103,17 +180,79 @@ function applySnapshot(snapshot: SheetSnapshot, mode: PreviewGridSizeMode): void
|
|
|
103
180
|
void nextTick(applyRuntimeColWidths)
|
|
104
181
|
}
|
|
105
182
|
|
|
106
|
-
//
|
|
107
|
-
watch(
|
|
108
|
-
() => props.template,
|
|
109
|
-
(template) => applySnapshot(template, 'template'),
|
|
110
|
-
{ immediate: true }
|
|
111
|
-
)
|
|
183
|
+
// 先铺当前层模板静态结构(取数期间 / 取数失败时可见),取数成功后替换为 Filled Report
|
|
184
|
+
watch(currentTemplate, (template) => applySnapshot(template, 'template'), { immediate: true })
|
|
112
185
|
watch(filledSnapshot, (filled) => {
|
|
113
186
|
if (filled) applySnapshot(filled, 'filled')
|
|
114
187
|
})
|
|
115
188
|
watch(effectiveColWidths, applyRuntimeColWidths)
|
|
116
189
|
|
|
190
|
+
// ─── 下钻点击与可点视觉提示 ─────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/** 悬停命中下钻格时给网格加 cursor 提示 */
|
|
193
|
+
const drillHover = ref(false)
|
|
194
|
+
let pointerStart: { x: number; y: number } | null = null
|
|
195
|
+
|
|
196
|
+
watch(drillDialogVisible, (visible) => {
|
|
197
|
+
if (!visible) drillDialog.value = null
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
async function openDrillDialog(
|
|
201
|
+
config: ReportDrillConfig,
|
|
202
|
+
record: Record<string, unknown>
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const next = await resolveDrillTarget(config, record)
|
|
205
|
+
if (!next) return
|
|
206
|
+
drillDialog.value = next
|
|
207
|
+
await nextTick()
|
|
208
|
+
drillDialogVisible.value = true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 事件坐标 → 模型地址(经当前 SheetGrid 命中测试;网格随内容尺寸重建,实例按事件时刻取) */
|
|
212
|
+
function resolveEventAddr(event: MouseEvent): CellAddress | null {
|
|
213
|
+
const host = event.currentTarget as HTMLElement
|
|
214
|
+
const gridEl = host.querySelector('.u-sheet__grid-instance') as HTMLElement | null
|
|
215
|
+
const grid = sheetRef.value?.getGrid()
|
|
216
|
+
if (!gridEl || !grid) return null
|
|
217
|
+
const rect = gridEl.getBoundingClientRect()
|
|
218
|
+
return grid.hitTestSheetAddr(event.clientX - rect.left, event.clientY - rect.top)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function handleGridPointerDown(event: PointerEvent): void {
|
|
222
|
+
pointerStart = { x: event.clientX, y: event.clientY }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function handleGridClick(event: MouseEvent): void {
|
|
226
|
+
if (!props.resolveTemplate) return
|
|
227
|
+
const start = pointerStart
|
|
228
|
+
pointerStart = null
|
|
229
|
+
if (start) {
|
|
230
|
+
const dx = event.clientX - start.x
|
|
231
|
+
const dy = event.clientY - start.y
|
|
232
|
+
if (dx * dx + dy * dy > DRAG_CLICK_THRESHOLD_PX * DRAG_CLICK_THRESHOLD_PX) return
|
|
233
|
+
}
|
|
234
|
+
const addr = resolveEventAddr(event)
|
|
235
|
+
if (!addr) return
|
|
236
|
+
const hit = resolveDrillHit(addr)
|
|
237
|
+
if (!hit) return
|
|
238
|
+
if (hit.config.openMode === 'dialog') {
|
|
239
|
+
void openDrillDialog(hit.config, hit.record)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
void drillInto(hit.config, hit.record)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleGridPointerMove(event: PointerEvent): void {
|
|
246
|
+
const addr = props.resolveTemplate ? resolveEventAddr(event) : null
|
|
247
|
+
const hit = addr ? resolveDrillHit(addr) : null
|
|
248
|
+
const next = !!hit
|
|
249
|
+
if (next !== drillHover.value) drillHover.value = next
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function handleGridPointerLeave(): void {
|
|
253
|
+
drillHover.value = false
|
|
254
|
+
}
|
|
255
|
+
|
|
117
256
|
async function exportXlsx(): Promise<void> {
|
|
118
257
|
if (loading.value) {
|
|
119
258
|
throw new Error(EXPORT_LOADING_MESSAGE)
|
|
@@ -132,5 +271,19 @@ async function exportXlsx(): Promise<void> {
|
|
|
132
271
|
)
|
|
133
272
|
}
|
|
134
273
|
|
|
135
|
-
|
|
274
|
+
/**
|
|
275
|
+
* 打印填充报表(纯前端):填充 sheet 模型 → 打印专用 HTML(buildFilledReportPrintHtml)
|
|
276
|
+
* → 隐藏 iframe 调起浏览器打印。守卫语义与 exportXlsx 一致(取数完成前拒绝)。
|
|
277
|
+
*/
|
|
278
|
+
function print(options?: ReportPrintOptions): void {
|
|
279
|
+
if (loading.value) {
|
|
280
|
+
throw new Error(PRINT_LOADING_MESSAGE)
|
|
281
|
+
}
|
|
282
|
+
if (!filledSnapshot.value) {
|
|
283
|
+
throw new Error(PRINT_NOT_READY_MESSAGE)
|
|
284
|
+
}
|
|
285
|
+
printHtmlDocument(buildFilledReportPrintHtml(workbook.value.activeSheet, options))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
defineExpose<_ReportViewerExposed>({ refresh, exportXlsx, print })
|
|
136
289
|
</script>
|
|
@@ -291,6 +291,72 @@
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/* 下钻配置对话框内容 */
|
|
295
|
+
@include m.b(report-drill-dialog) {
|
|
296
|
+
display: flex;
|
|
297
|
+
flex-direction: column;
|
|
298
|
+
gap: 12px;
|
|
299
|
+
|
|
300
|
+
@include m.e(section) {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
gap: 6px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@include m.e(label) {
|
|
307
|
+
font-size: 12px;
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
color: fn.use-var(text-color, second);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@include m.e(hint) {
|
|
313
|
+
margin: 0;
|
|
314
|
+
font-size: 12px;
|
|
315
|
+
line-height: 1.5;
|
|
316
|
+
color: fn.use-var(text-color, second);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@include m.e(error) {
|
|
320
|
+
margin: 0;
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
line-height: 1.5;
|
|
323
|
+
color: fn.use-var(color, danger);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@include m.e(list) {
|
|
327
|
+
margin: 0;
|
|
328
|
+
padding: 0;
|
|
329
|
+
list-style: none;
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-direction: column;
|
|
332
|
+
gap: 8px;
|
|
333
|
+
max-height: 320px;
|
|
334
|
+
overflow: auto;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@include m.e(mapping) {
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 6px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@include m.e(field) {
|
|
344
|
+
width: 160px;
|
|
345
|
+
flex: none;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@include m.e(param) {
|
|
349
|
+
width: 200px;
|
|
350
|
+
flex: none;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@include m.e(arrow) {
|
|
354
|
+
flex: none;
|
|
355
|
+
font-size: 12px;
|
|
356
|
+
color: fn.use-var(text-color, second);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
294
360
|
@include m.b(report-rule-row) {
|
|
295
361
|
display: flex;
|
|
296
362
|
align-items: flex-start;
|
|
@@ -22,6 +22,46 @@
|
|
|
22
22
|
flex: 1;
|
|
23
23
|
min-height: 0;
|
|
24
24
|
min-width: 0;
|
|
25
|
+
|
|
26
|
+
/* 下钻格可点提示:VTable pointermove 会把容器内联 cursor 重置为 default,需 !important 覆盖 */
|
|
27
|
+
@include m.m(drill-hover) {
|
|
28
|
+
&,
|
|
29
|
+
.u-sheet__grid-instance,
|
|
30
|
+
.u-sheet__grid-instance canvas,
|
|
31
|
+
.u-sheet__grid-instance * {
|
|
32
|
+
cursor: pointer !important;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@include m.e(drill-bar) {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: fn.use-var(gap, small);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* 下钻弹框挂在 UDialog 根(Teleport 到 body),给内嵌查看器明确高度 */
|
|
44
|
+
@include m.e(drill-dialog) {
|
|
45
|
+
width: min(960px, 92vw);
|
|
46
|
+
height: min(80vh, 720px);
|
|
47
|
+
|
|
48
|
+
.u-dialog__body {
|
|
49
|
+
flex: 1;
|
|
50
|
+
min-height: 0;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.u-scroll__container,
|
|
56
|
+
.u-scroll__content {
|
|
57
|
+
height: 100%;
|
|
58
|
+
min-height: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.u-report-viewer {
|
|
62
|
+
height: 100%;
|
|
63
|
+
min-height: 0;
|
|
64
|
+
}
|
|
25
65
|
}
|
|
26
66
|
|
|
27
67
|
@include m.e(loading) {
|
|
@@ -316,6 +316,7 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
|
|
|
316
316
|
const next: ReportBinding = { ...binding, ...patch }
|
|
317
317
|
if ('rowParent' in patch && patch.rowParent === undefined) delete next.rowParent
|
|
318
318
|
if ('colParent' in patch && patch.colParent === undefined) delete next.colParent
|
|
319
|
+
if ('drill' in patch && patch.drill === undefined) delete next.drill
|
|
319
320
|
if (
|
|
320
321
|
('aggregate' in patch || 'expand' in patch) &&
|
|
321
322
|
!('preset' in patch) &&
|
|
@@ -483,7 +484,13 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
|
|
|
483
484
|
// ---- 渲染与模板 ----
|
|
484
485
|
|
|
485
486
|
const resolveCellStyle = createBindingBadgeStyleResolver(getBindingAt)
|
|
486
|
-
|
|
487
|
+
// 对齐访问器:hook 为长生命周期闭包而 activeSheet 可变(template 载入 / 宿主换 workbook),
|
|
488
|
+
// 每次调用动态取当前 sheet 的有效样式(getEffectiveStyle 为 O(1) 列→行→格叠加)
|
|
489
|
+
const resolveCellRenderer = createBindingBadgeRenderer(
|
|
490
|
+
getBindingAt,
|
|
491
|
+
resolveFieldLabel,
|
|
492
|
+
(addr) => workbook.value.activeSheet.getEffectiveStyle(addr)?.align
|
|
493
|
+
)
|
|
487
494
|
|
|
488
495
|
function getTemplate(): ReportTemplate {
|
|
489
496
|
const resolved: ReportDatasetDef[] = datasets.value.flatMap((dataset) => {
|
|
@@ -1,30 +1,65 @@
|
|
|
1
|
-
import type { SheetSnapshot } from '@veltra/sheet-core'
|
|
1
|
+
import type { CellAddress, SheetSnapshot } from '@veltra/sheet-core'
|
|
2
|
+
import { cellKey } from '@veltra/sheet-core'
|
|
2
3
|
import { computed, ref, shallowRef, watch, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
|
3
4
|
|
|
4
5
|
import type { ConnectorError } from '../../report/connector'
|
|
6
|
+
import {
|
|
7
|
+
buildDrillHitMap,
|
|
8
|
+
createDrillStack,
|
|
9
|
+
currentDrillLayer,
|
|
10
|
+
popDrillLayer,
|
|
11
|
+
pushDrillLayer,
|
|
12
|
+
resolveDrillParams,
|
|
13
|
+
type DrillHit,
|
|
14
|
+
type DrillStack
|
|
15
|
+
} from '../../report/drill'
|
|
5
16
|
import type { ReportColWidthEntry } from '../../report/export-xlsx'
|
|
6
17
|
import { renderReport, resolveFilledColWidths } from '../../report/render'
|
|
7
18
|
import {
|
|
8
19
|
fetchTemplateRecords,
|
|
9
20
|
resolveParamDefaults,
|
|
10
|
-
resolveTemplateParams
|
|
21
|
+
resolveTemplateParams,
|
|
22
|
+
type ReportTemplate
|
|
11
23
|
} from '../../report/template'
|
|
12
|
-
import type { ParamValues, QueryParamDef } from '../../report/types'
|
|
24
|
+
import type { ParamValues, QueryParamDef, ReportDrillConfig } from '../../report/types'
|
|
13
25
|
import type { ReportViewerProps } from '../../types'
|
|
14
26
|
|
|
15
27
|
export interface UseReportViewerReturn {
|
|
16
|
-
/** Filter Bar
|
|
28
|
+
/** Filter Bar 参数(当前层模板实际绑定的数据集的查询参数并集) */
|
|
17
29
|
params: ComputedRef<QueryParamDef[]>
|
|
18
|
-
/**
|
|
30
|
+
/** 当前运行时参数值(初始为参数默认值;下钻时写入映射值) */
|
|
19
31
|
values: Ref<ParamValues>
|
|
20
32
|
/** 取数中 */
|
|
21
33
|
loading: Ref<boolean>
|
|
22
|
-
/**
|
|
34
|
+
/** 最近一次取数的业务错误或下钻解析错误(可读提示;成功或重新取数时清空) */
|
|
23
35
|
error: ShallowRef<ConnectorError | null>
|
|
24
36
|
/** 最近一次成功展开渲染的 Filled Report 快照(组件壳据此替换网格内容) */
|
|
25
37
|
filledSnapshot: ShallowRef<SheetSnapshot | null>
|
|
26
38
|
/** 展开后物理列宽(由模板 colWidths 映射;无模板列宽时为 undefined) */
|
|
27
39
|
filledColWidths: ShallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>
|
|
40
|
+
/** 当前展示模板:下钻栈顶;未下钻时为 props.template */
|
|
41
|
+
currentTemplate: ComputedRef<ReportTemplate>
|
|
42
|
+
/** 下钻栈深(0 = 根层,未下钻) */
|
|
43
|
+
drillDepth: ComputedRef<number>
|
|
44
|
+
/** 可回退(栈深 > 0) */
|
|
45
|
+
canDrillBack: ComputedRef<boolean>
|
|
46
|
+
/** 物理格下钻命中查询(无命中返回 null);供组件壳点击 / 悬停判定 */
|
|
47
|
+
resolveDrillHit: (addr: CellAddress) => DrillHit | null
|
|
48
|
+
/**
|
|
49
|
+
* 解析下钻目标:宿主契约取模板 + 默认值叠加映射参数。
|
|
50
|
+
* 失败设可读错误、返回 null(栈不变)。成功时清除先前的下钻解析错误。
|
|
51
|
+
*/
|
|
52
|
+
resolveDrillTarget: (
|
|
53
|
+
config: ReportDrillConfig,
|
|
54
|
+
record: Record<string, unknown>
|
|
55
|
+
) => Promise<{ template: ReportTemplate; params: ParamValues } | null>
|
|
56
|
+
/**
|
|
57
|
+
* 下钻进入(openMode 'switch'):解析目标模板 → 映射参数带入 Filter Bar → 压栈切换并取数。
|
|
58
|
+
* 解析失败设可读错误提示、停留当前报、栈不变。
|
|
59
|
+
*/
|
|
60
|
+
drillInto: (config: ReportDrillConfig, record: Record<string, unknown>) => Promise<void>
|
|
61
|
+
/** 回退上一层:恢复该层模板与当时 Filter Bar 参数并重新取数渲染 */
|
|
62
|
+
drillBack: () => Promise<void>
|
|
28
63
|
/** 重新取数并展开渲染 */
|
|
29
64
|
refresh: () => Promise<void>
|
|
30
65
|
/** Filter Bar 改值:写入新参数值并重新取数 */
|
|
@@ -34,6 +69,8 @@ export interface UseReportViewerReturn {
|
|
|
34
69
|
/**
|
|
35
70
|
* UReportViewer 的 headless 编排:参数提取 → 取数 → renderReport 展开。
|
|
36
71
|
* 不持有 DOM / 网格引用;填充快照经 `filledSnapshot` 交给组件壳应用。
|
|
72
|
+
* 下钻:openMode 'switch' 栈式切换当前层;'dialog' 由组件壳解析目标后嵌套独立查看器。
|
|
73
|
+
* 导出 / 打印始终作用于当前可见层。
|
|
37
74
|
*/
|
|
38
75
|
export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn {
|
|
39
76
|
const loading = ref(false)
|
|
@@ -41,7 +78,18 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
|
|
|
41
78
|
const filledSnapshot = shallowRef<SheetSnapshot | null>(null)
|
|
42
79
|
const filledColWidths = shallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>(undefined)
|
|
43
80
|
|
|
44
|
-
|
|
81
|
+
// 下钻栈:null = 未下钻(当前层即 props.template);命中索引随每次成功取数重建
|
|
82
|
+
const drillStack = shallowRef<DrillStack | null>(null)
|
|
83
|
+
const drillHits = shallowRef<Map<number, DrillHit>>(new Map())
|
|
84
|
+
|
|
85
|
+
const currentTemplate = computed(() => {
|
|
86
|
+
const stack = drillStack.value
|
|
87
|
+
return stack ? currentDrillLayer(stack).template : props.template
|
|
88
|
+
})
|
|
89
|
+
const drillDepth = computed(() => (drillStack.value?.length ?? 1) - 1)
|
|
90
|
+
const canDrillBack = computed(() => drillDepth.value > 0)
|
|
91
|
+
|
|
92
|
+
const params = computed(() => resolveTemplateParams(currentTemplate.value))
|
|
45
93
|
// 初始值由下方 immediate watch 播种(参数默认值)
|
|
46
94
|
const values = shallowRef<ParamValues>({})
|
|
47
95
|
|
|
@@ -50,7 +98,7 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
|
|
|
50
98
|
|
|
51
99
|
async function refresh(): Promise<void> {
|
|
52
100
|
const current = ++fetchSeq
|
|
53
|
-
const template =
|
|
101
|
+
const template = currentTemplate.value
|
|
54
102
|
loading.value = true
|
|
55
103
|
error.value = null
|
|
56
104
|
const result = await fetchTemplateRecords(props.connector, template, values.value)
|
|
@@ -65,6 +113,79 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
|
|
|
65
113
|
filledColWidths.value = template.colWidths?.length
|
|
66
114
|
? resolveFilledColWidths(template, result.data, template.colWidths)
|
|
67
115
|
: undefined
|
|
116
|
+
drillHits.value = props.resolveTemplate ? buildDrillHitMap(template, result.data) : new Map()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 切层共有收尾:清填充结果与命中索引,写入新层参数并重新取数 */
|
|
120
|
+
function transitionTo(layerParams: ParamValues): Promise<void> {
|
|
121
|
+
filledSnapshot.value = null
|
|
122
|
+
filledColWidths.value = undefined
|
|
123
|
+
drillHits.value = new Map()
|
|
124
|
+
values.value = layerParams
|
|
125
|
+
return refresh()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function resolveDrillTarget(
|
|
129
|
+
config: ReportDrillConfig,
|
|
130
|
+
record: Record<string, unknown>
|
|
131
|
+
): Promise<{ template: ReportTemplate; params: ParamValues } | null> {
|
|
132
|
+
const resolve = props.resolveTemplate
|
|
133
|
+
if (!resolve) return null
|
|
134
|
+
|
|
135
|
+
let target: ReportTemplate | null | undefined
|
|
136
|
+
try {
|
|
137
|
+
target = await resolve(config.target)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
error.value = {
|
|
140
|
+
code: 'DRILL_RESOLVE_FAILED',
|
|
141
|
+
message: err instanceof Error ? err.message : String(err)
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
if (!target) {
|
|
146
|
+
error.value = {
|
|
147
|
+
code: 'DRILL_RESOLVE_FAILED',
|
|
148
|
+
message: `下钻目标模板解析失败:${config.target}`
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
error.value = null
|
|
154
|
+
return {
|
|
155
|
+
template: target,
|
|
156
|
+
params: {
|
|
157
|
+
...resolveParamDefaults(resolveTemplateParams(target)),
|
|
158
|
+
...resolveDrillParams(config, record)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function drillInto(
|
|
164
|
+
config: ReportDrillConfig,
|
|
165
|
+
record: Record<string, unknown>
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
const next = await resolveDrillTarget(config, record)
|
|
168
|
+
if (!next) return
|
|
169
|
+
const base =
|
|
170
|
+
drillStack.value ?? createDrillStack({ template: props.template, params: values.value })
|
|
171
|
+
drillStack.value = pushDrillLayer(
|
|
172
|
+
base,
|
|
173
|
+
{ template: next.template, params: next.params },
|
|
174
|
+
values.value
|
|
175
|
+
)
|
|
176
|
+
await transitionTo(next.params)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function drillBack(): Promise<void> {
|
|
180
|
+
const stack = drillStack.value
|
|
181
|
+
if (!stack || stack.length <= 1) return
|
|
182
|
+
const next = popDrillLayer(stack)
|
|
183
|
+
drillStack.value = next
|
|
184
|
+
await transitionTo({ ...currentDrillLayer(next).params })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveDrillHit(addr: CellAddress): DrillHit | null {
|
|
188
|
+
return drillHits.value.get(cellKey(addr)) ?? null
|
|
68
189
|
}
|
|
69
190
|
|
|
70
191
|
function setValues(next: ParamValues): void {
|
|
@@ -72,17 +193,35 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
|
|
|
72
193
|
void refresh()
|
|
73
194
|
}
|
|
74
195
|
|
|
75
|
-
//
|
|
196
|
+
// 宿主更换根模板:下钻栈作废;Filter Bar 播种为默认值叠加 initialValues
|
|
76
197
|
watch(
|
|
77
198
|
() => props.template,
|
|
78
199
|
() => {
|
|
200
|
+
drillStack.value = null
|
|
201
|
+
drillHits.value = new Map()
|
|
79
202
|
filledSnapshot.value = null
|
|
80
203
|
filledColWidths.value = undefined
|
|
81
|
-
values.value = resolveParamDefaults(params.value)
|
|
204
|
+
values.value = { ...resolveParamDefaults(params.value), ...props.initialValues }
|
|
82
205
|
void refresh()
|
|
83
206
|
},
|
|
84
207
|
{ immediate: true }
|
|
85
208
|
)
|
|
86
209
|
|
|
87
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
params,
|
|
212
|
+
values,
|
|
213
|
+
loading,
|
|
214
|
+
error,
|
|
215
|
+
filledSnapshot,
|
|
216
|
+
filledColWidths,
|
|
217
|
+
currentTemplate,
|
|
218
|
+
drillDepth,
|
|
219
|
+
canDrillBack,
|
|
220
|
+
resolveDrillHit,
|
|
221
|
+
resolveDrillTarget,
|
|
222
|
+
drillInto,
|
|
223
|
+
drillBack,
|
|
224
|
+
refresh,
|
|
225
|
+
setValues
|
|
226
|
+
}
|
|
88
227
|
}
|
|
@@ -24,7 +24,7 @@ const EXT_TO_TYPE: Record<string, SheetImageType> = {
|
|
|
24
24
|
webp: 'webp'
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** MIME →
|
|
27
|
+
/** MIME → sheet-core 图片类型(core/image 的 SheetImageType);MIME 为空时回落扩展名 */
|
|
28
28
|
export function resolveImageType(file: File): SheetImageType | undefined {
|
|
29
29
|
const mime = file.type.trim().toLowerCase()
|
|
30
30
|
if (mime && MIME_TO_TYPE[mime]) return MIME_TO_TYPE[mime]
|
|
@@ -30,7 +30,7 @@ const cls = bem('sheet')
|
|
|
30
30
|
function exportXlsx(): void {
|
|
31
31
|
emit('close')
|
|
32
32
|
// 大工作簿序列化在 worker 中进行(数秒):给常驻反馈,失败(如 sheet 名含
|
|
33
|
-
// Excel 非法字符,
|
|
33
|
+
// Excel 非法字符,sheet-core 写入校验抛 InvalidArgumentError)明确报错,
|
|
34
34
|
// 绝不静默失败
|
|
35
35
|
const loading = message({ message: '正在导出…', duration: 0 })
|
|
36
36
|
exportWorkbookFile(props.context)
|