@veltra/sheet 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/index.d.ts +2 -19
  2. package/dist/index.js +2 -15
  3. package/dist/types/index.d.ts +1 -3
  4. package/package.json +2 -3
  5. package/src/index.ts +1 -27
  6. package/src/types/index.ts +0 -2
  7. package/dist/components/report/binding-badge.js +0 -118
  8. package/dist/components/report/designer/cell-coords.js +0 -46
  9. package/dist/components/report/designer/col-widths.js +0 -29
  10. package/dist/components/report/designer/conditional-rules/helpers.js +0 -139
  11. package/dist/components/report/designer/drill-dialog.js +0 -258
  12. package/dist/components/report/designer/drop-highlight-overlay.js +0 -76
  13. package/dist/components/report/designer/float-panel.js +0 -434
  14. package/dist/components/report/designer/role.js +0 -17
  15. package/dist/components/report/designer/rule-preview.js +0 -33
  16. package/dist/components/report/designer/rule-row.js +0 -253
  17. package/dist/components/report/designer/rules-dialog.js +0 -118
  18. package/dist/components/report/designer/topology-overlay.js +0 -122
  19. package/dist/components/report/designer/topology.js +0 -64
  20. package/dist/components/report/designer/use-grid-overlay.js +0 -75
  21. package/dist/components/report/designer-hub.js +0 -177
  22. package/dist/components/report/field-panel-helpers.js +0 -33
  23. package/dist/components/report/field-panel.js +0 -132
  24. package/dist/components/report/filter-bar.js +0 -74
  25. package/dist/components/report/hub-connection-form.js +0 -145
  26. package/dist/components/report/hub-dataset-editor.js +0 -409
  27. package/dist/components/report/preview-grid-size.js +0 -32
  28. package/dist/components/report/print.js +0 -39
  29. package/dist/components/report/report-designer.js +0 -428
  30. package/dist/components/report/report-viewer.js +0 -253
  31. package/dist/components/report/style.d.ts +0 -17
  32. package/dist/components/report/style.js +0 -18
  33. package/dist/components/report/style2.css +0 -965
  34. package/dist/components/report/use-report-designer.js +0 -387
  35. package/dist/components/report/use-report-viewer.js +0 -140
  36. package/dist/report/binding.d.ts +0 -83
  37. package/dist/report/binding.js +0 -334
  38. package/dist/report/connector.d.ts +0 -66
  39. package/dist/report/connector.js +0 -159
  40. package/dist/report/drill.d.ts +0 -48
  41. package/dist/report/drill.js +0 -78
  42. package/dist/report/export-xlsx.d.ts +0 -13
  43. package/dist/report/export-xlsx.js +0 -13
  44. package/dist/report/filter-bar.d.ts +0 -10
  45. package/dist/report/filter-bar.js +0 -31
  46. package/dist/report/index.d.ts +0 -14
  47. package/dist/report/params.d.ts +0 -13
  48. package/dist/report/params.js +0 -61
  49. package/dist/report/print.d.ts +0 -23
  50. package/dist/report/print.js +0 -197
  51. package/dist/report/render/aggregate.js +0 -193
  52. package/dist/report/render/builder.d.ts +0 -12
  53. package/dist/report/render/builder.js +0 -139
  54. package/dist/report/render/coordinate.d.ts +0 -25
  55. package/dist/report/render/coordinate.js +0 -733
  56. package/dist/report/render/index.d.ts +0 -14
  57. package/dist/report/render/index.js +0 -21
  58. package/dist/report/render/style-resolver.js +0 -50
  59. package/dist/report/render/template-index.d.ts +0 -24
  60. package/dist/report/render/template-index.js +0 -137
  61. package/dist/report/rules/evaluate.d.ts +0 -25
  62. package/dist/report/rules/evaluate.js +0 -82
  63. package/dist/report/rules/index.d.ts +0 -2
  64. package/dist/report/template.d.ts +0 -63
  65. package/dist/report/template.js +0 -83
  66. package/dist/report/types.d.ts +0 -103
  67. package/dist/types/report-designer.d.ts +0 -51
  68. package/dist/types/report-viewer.d.ts +0 -57
  69. package/src/components/report/__test__/binding-badge.test.ts +0 -92
  70. package/src/components/report/__test__/conditional-rules-helpers.test.ts +0 -88
  71. package/src/components/report/__test__/field-panel-helpers.test.ts +0 -65
  72. package/src/components/report/__test__/float-panel-position.test.ts +0 -45
  73. package/src/components/report/__test__/preview-grid-size.test.ts +0 -63
  74. package/src/components/report/__test__/report-designer.test.ts +0 -559
  75. package/src/components/report/__test__/report-drill.test.ts +0 -692
  76. package/src/components/report/__test__/report-viewer.test.ts +0 -400
  77. package/src/components/report/__test__/topology.test.ts +0 -143
  78. package/src/components/report/__test__/use-report-designer.test.ts +0 -804
  79. package/src/components/report/binding-badge.ts +0 -122
  80. package/src/components/report/designer/cell-coords.ts +0 -95
  81. package/src/components/report/designer/col-widths.ts +0 -56
  82. package/src/components/report/designer/conditional-rules/helpers.ts +0 -189
  83. package/src/components/report/designer/drill-dialog.vue +0 -245
  84. package/src/components/report/designer/drop-highlight-overlay.vue +0 -75
  85. package/src/components/report/designer/float-panel.vue +0 -444
  86. package/src/components/report/designer/role.ts +0 -32
  87. package/src/components/report/designer/rule-preview.vue +0 -40
  88. package/src/components/report/designer/rule-row.vue +0 -252
  89. package/src/components/report/designer/rules-dialog.vue +0 -110
  90. package/src/components/report/designer/topology-overlay.vue +0 -128
  91. package/src/components/report/designer/topology.ts +0 -124
  92. package/src/components/report/designer/use-grid-overlay.ts +0 -114
  93. package/src/components/report/designer-hub.vue +0 -198
  94. package/src/components/report/field-panel-helpers.ts +0 -44
  95. package/src/components/report/field-panel.vue +0 -136
  96. package/src/components/report/filter-bar.vue +0 -77
  97. package/src/components/report/hub-connection-form.vue +0 -124
  98. package/src/components/report/hub-dataset-editor.vue +0 -387
  99. package/src/components/report/index.ts +0 -2
  100. package/src/components/report/preview-grid-size.ts +0 -33
  101. package/src/components/report/print.ts +0 -44
  102. package/src/components/report/report-designer.vue +0 -454
  103. package/src/components/report/report-viewer.vue +0 -289
  104. package/src/components/report/style-designer.scss +0 -653
  105. package/src/components/report/style-hub.scss +0 -292
  106. package/src/components/report/style-viewer.scss +0 -144
  107. package/src/components/report/style.scss +0 -3
  108. package/src/components/report/style.ts +0 -20
  109. package/src/components/report/use-report-designer.ts +0 -603
  110. package/src/components/report/use-report-viewer.ts +0 -227
  111. package/src/report/__test__/binding.test.ts +0 -320
  112. package/src/report/__test__/connector.test.ts +0 -239
  113. package/src/report/__test__/drill.test.ts +0 -194
  114. package/src/report/__test__/export-xlsx.test.ts +0 -96
  115. package/src/report/__test__/filter-bar.test.ts +0 -58
  116. package/src/report/__test__/params.test.ts +0 -156
  117. package/src/report/__test__/print.test.ts +0 -170
  118. package/src/report/__test__/render.test.ts +0 -856
  119. package/src/report/__test__/rules.test.ts +0 -131
  120. package/src/report/__test__/template.test.ts +0 -366
  121. package/src/report/binding.ts +0 -474
  122. package/src/report/connector.ts +0 -206
  123. package/src/report/drill.ts +0 -112
  124. package/src/report/export-xlsx.ts +0 -15
  125. package/src/report/filter-bar.ts +0 -36
  126. package/src/report/index.ts +0 -16
  127. package/src/report/params.ts +0 -96
  128. package/src/report/print.ts +0 -234
  129. package/src/report/render/__test__/aggregate.test.ts +0 -16
  130. package/src/report/render/__test__/coordinate.test.ts +0 -405
  131. package/src/report/render/aggregate.ts +0 -289
  132. package/src/report/render/builder.ts +0 -211
  133. package/src/report/render/coordinate.ts +0 -1239
  134. package/src/report/render/index.ts +0 -30
  135. package/src/report/render/style-resolver.ts +0 -68
  136. package/src/report/render/template-index.ts +0 -200
  137. package/src/report/rules/evaluate.ts +0 -113
  138. package/src/report/rules/index.ts +0 -7
  139. package/src/report/template.ts +0 -145
  140. package/src/report/types.ts +0 -116
  141. package/src/types/report-designer.ts +0 -55
  142. package/src/types/report-viewer.ts +0 -54
@@ -1,387 +0,0 @@
1
- <template>
2
- <section v-if="dataset" :class="cls.b">
3
- <div :class="cls.e('head')">
4
- <div :class="cls.e('rename')">
5
- <span :class="cls.e('label')">数据集名称</span>
6
- <u-input
7
- :model-value="dataset.label"
8
- size="small"
9
- @update:model-value="(v) => patchDataset({ label: String(v ?? '') })"
10
- />
11
- </div>
12
- <u-pop-confirm title="确定删除该数据集?" @confirm="emit('remove')">
13
- <template #reference>
14
- <u-button size="small" type="danger" plain>删除数据集</u-button>
15
- </template>
16
- </u-pop-confirm>
17
- </div>
18
-
19
- <div :class="cls.e('sql')">
20
- <p :class="cls.e('section-title')">SQL 查询</p>
21
- <u-code-editor
22
- v-model="sqlDraft"
23
- lang="sql"
24
- :langs="['sql']"
25
- :default-lines="6"
26
- @change="onSqlChange"
27
- />
28
- <p v-if="describeError" :class="cls.e('sql-error')" role="alert">{{ describeError }}</p>
29
- </div>
30
-
31
- <div v-if="paramRows.length" :class="cls.e('params')">
32
- <p :class="cls.e('section-title')">查询参数(从 SQL 自动提取)</p>
33
- <u-table
34
- :class="cls.e('param-table')"
35
- :data="paramRows"
36
- :columns="paramColumns"
37
- border
38
- text-ellipsis
39
- >
40
- <template #column:label="{ rowData }">
41
- <u-input
42
- :model-value="rowData.label"
43
- size="small"
44
- @update:model-value="(v) => patchParam(rowData.id, { label: String(v ?? '') })"
45
- />
46
- </template>
47
- <template #column:type="{ rowData }">
48
- <u-select
49
- :model-value="rowData.type"
50
- size="small"
51
- :options="paramTypeOptions"
52
- @update:model-value="(v) => patchParam(rowData.id, { type: v as QueryParamType })"
53
- />
54
- </template>
55
- <template #column:defaultValue="{ rowData }">
56
- <u-input
57
- v-if="rowData.type === 'number'"
58
- :model-value="String(rowData.defaultValue ?? '')"
59
- size="small"
60
- type="number"
61
- @update:model-value="(v) => patchParam(rowData.id, { defaultValue: Number(v) })"
62
- />
63
- <u-input
64
- v-else-if="rowData.type === 'date'"
65
- :model-value="String(rowData.defaultValue ?? '')"
66
- size="small"
67
- type="date"
68
- @update:model-value="(v) => patchParam(rowData.id, { defaultValue: v })"
69
- />
70
- <u-select
71
- v-else-if="rowData.type === 'select'"
72
- :model-value="String(rowData.defaultValue ?? '')"
73
- size="small"
74
- :options="rowData.options ?? []"
75
- @update:model-value="(v) => patchParam(rowData.id, { defaultValue: v })"
76
- />
77
- <u-input
78
- v-else
79
- :model-value="formatDefaultValue(rowData.defaultValue)"
80
- size="small"
81
- @update:model-value="(v) => patchParam(rowData.id, { defaultValue: v })"
82
- />
83
- </template>
84
- <template #column:options="{ rowData }">
85
- <u-input
86
- v-if="rowData.type === 'select'"
87
- :model-value="formatOptions(rowData.options)"
88
- size="small"
89
- placeholder="华东|华东,华南|华南"
90
- @update:model-value="
91
- (v) => patchParam(rowData.id, { options: parseOptions(String(v ?? '')) })
92
- "
93
- />
94
- <span v-else :class="cls.e('muted')">—</span>
95
- </template>
96
- </u-table>
97
- </div>
98
-
99
- <u-tabs v-model="detailTab" :items="detailTabItems" :class="cls.e('tabs')">
100
- <template #schema>
101
- <p v-if="describing" :class="cls.e('muted')">正在解析字段…</p>
102
- <u-table
103
- v-else
104
- :class="cls.e('schema-table')"
105
- :data="schemaRows"
106
- :columns="schemaColumns"
107
- border
108
- text-ellipsis
109
- >
110
- <template #column:label="{ rowData }">
111
- <u-input
112
- :model-value="rowData.label"
113
- size="small"
114
- placeholder="中文显示名"
115
- @update:model-value="(v) => patchFieldLabel(rowData.name, String(v ?? ''))"
116
- />
117
- </template>
118
- <template #column:type="{ rowData }">
119
- <u-tag size="small" :type="fieldTypeColor(rowData.type)">{{ rowData.type }}</u-tag>
120
- </template>
121
- </u-table>
122
- </template>
123
- <template #preview>
124
- <div :class="cls.e('preview-actions')">
125
- <u-button size="small" plain :loading="previewing" @click="runPreview">
126
- 刷新预览(按参数默认值取数)
127
- </u-button>
128
- </div>
129
- <u-table
130
- :class="cls.e('preview-table')"
131
- :data="previewRows"
132
- :columns="previewColumns"
133
- show-index
134
- border
135
- text-ellipsis
136
- />
137
- <p v-if="previewError" :class="cls.e('preview-error')" role="alert">{{ previewError }}</p>
138
- </template>
139
- </u-tabs>
140
- </section>
141
- </template>
142
-
143
- <script lang="ts" setup>
144
- import {
145
- UButton,
146
- UCodeEditor,
147
- UInput,
148
- UPopConfirm,
149
- USelect,
150
- UTable,
151
- UTabs,
152
- UTag,
153
- defineTableColumns
154
- } from '@veltra/desktop'
155
- import type { ColorType } from '@veltra/utils'
156
- import { bem } from '@veltra/utils'
157
- import { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
158
-
159
- import { buildParamDefs } from '../../report/params'
160
- import type { DatasetField, QueryParamDef, QueryParamType } from '../../report/types'
161
- import type { DatasetHubController, DesignerDataset } from './use-report-designer'
162
-
163
- defineOptions({ name: 'UReportHubDatasetEditor' })
164
-
165
- const props = defineProps<{ hub: DatasetHubController; datasetId: string }>()
166
-
167
- const emit = defineEmits<{ remove: [] }>()
168
-
169
- const cls = bem('report-hub-dataset-editor')
170
-
171
- const detailTab = ref('schema')
172
- const detailTabItems = [
173
- { key: 'schema', name: '字段 Schema' },
174
- { key: 'preview', name: '数据预览' }
175
- ]
176
-
177
- const paramTypeOptions = [
178
- { label: '文本', value: 'text' },
179
- { label: '数字', value: 'number' },
180
- { label: '日期', value: 'date' },
181
- { label: '日期范围', value: 'date-range' },
182
- { label: '下拉', value: 'select' }
183
- ]
184
-
185
- const dataset = computed((): DesignerDataset | undefined =>
186
- props.hub.datasets.value.find((item) => item.id === props.datasetId)
187
- )
188
-
189
- // ---- describe / 预览状态(声明先于下方 immediate watcher 使用) ----
190
-
191
- const describing = ref(false)
192
- const describeError = shallowRef('')
193
- let describeSeq = 0
194
-
195
- const previewing = ref(false)
196
- const previewError = shallowRef('')
197
- const previewRows = shallowRef<Record<string, unknown>[]>([])
198
- const previewFields = shallowRef<DatasetField[]>([])
199
-
200
- function resetPreview(): void {
201
- previewing.value = false
202
- previewError.value = ''
203
- previewRows.value = []
204
- previewFields.value = []
205
- }
206
-
207
- // ---- SQL 编辑:debounce 写回数据集并触发 describe ----
208
-
209
- const sqlDraft = ref('')
210
- let sqlTimer: ReturnType<typeof setTimeout> | undefined
211
-
212
- watch(
213
- () => props.datasetId,
214
- () => {
215
- sqlDraft.value = dataset.value?.sql ?? ''
216
- resetPreview()
217
- void runDescribe()
218
- },
219
- { immediate: true }
220
- )
221
-
222
- /** 立即把草稿 SQL 落入数据集(v-model 实时、change 失焦才发且提交有 debounce),供预览同步拿到最新 SQL */
223
- function flushSqlDraft(datasetId: string): void {
224
- if (sqlTimer) {
225
- clearTimeout(sqlTimer)
226
- sqlTimer = undefined
227
- }
228
- const current = dataset.value
229
- if (!current || current.sql === sqlDraft.value) return
230
- props.hub.updateDataset(datasetId, { sql: sqlDraft.value })
231
- void runDescribe()
232
- }
233
-
234
- function onSqlChange(value: string): void {
235
- sqlDraft.value = value
236
- const datasetId = props.datasetId
237
- if (sqlTimer) clearTimeout(sqlTimer)
238
- sqlTimer = setTimeout(() => {
239
- sqlTimer = undefined
240
- // 防抖窗口内切换了数据集则丢弃,避免把旧草稿写进新数据集
241
- if (props.datasetId !== datasetId) return
242
- props.hub.updateDataset(datasetId, { sql: value })
243
- void runDescribe()
244
- }, 280)
245
- }
246
-
247
- onBeforeUnmount(() => {
248
- if (sqlTimer) clearTimeout(sqlTimer)
249
- })
250
-
251
- // ---- describe 字段解析(经连接器;结果写入数据集字段缓存) ----
252
-
253
- async function runDescribe(): Promise<void> {
254
- const seq = ++describeSeq
255
- if (!sqlDraft.value.trim()) {
256
- describeError.value = ''
257
- // SQL 清空 = 无查询:清掉字段缓存,字段面板不再残留旧字段
258
- props.hub.updateDataset(props.datasetId, { fields: undefined })
259
- return
260
- }
261
- describing.value = true
262
- const result = await props.hub.describeDataset(props.datasetId)
263
- if (seq !== describeSeq) return
264
- describing.value = false
265
- describeError.value = result.ok ? '' : result.error.message
266
- }
267
-
268
- // ---- 查询参数(`${param}` 提取 + 元数据覆盖) ----
269
-
270
- const paramRows = computed(() => {
271
- const current = dataset.value
272
- if (!current || !current.sql.trim()) return []
273
- return buildParamDefs(current.sql, current.paramOverrides)
274
- })
275
-
276
- // ---- 字段 schema(describe 缓存 + fieldOverrides) ----
277
-
278
- const schemaRows = computed(() => {
279
- const current = dataset.value
280
- if (!current) return []
281
- return (current.fields ?? []).map((field) => ({
282
- ...field,
283
- label: current.fieldOverrides?.[field.name]?.label ?? field.label
284
- }))
285
- })
286
-
287
- // ---- 记录预览(按参数默认值经连接器取数) ----
288
-
289
- async function runPreview(): Promise<void> {
290
- // 编辑器 change 仅在失焦时触发且提交有 debounce,先同步落草稿 SQL,避免用旧 SQL 取数
291
- flushSqlDraft(props.datasetId)
292
- previewing.value = true
293
- previewError.value = ''
294
- const result = await props.hub.previewDataset(props.datasetId)
295
- previewing.value = false
296
- if (result.ok) {
297
- previewRows.value = result.data.rows
298
- previewFields.value = result.data.fields
299
- } else {
300
- previewRows.value = []
301
- previewError.value = result.error.message
302
- }
303
- }
304
-
305
- watch(detailTab, (tab) => {
306
- if (tab === 'preview') void runPreview()
307
- })
308
-
309
- // ---- 表格列与编辑 ----
310
-
311
- const paramColumns = defineTableColumns([
312
- { key: 'id', name: '参数名', minWidth: 100 },
313
- { key: 'label', name: '显示名', minWidth: 120 },
314
- { key: 'type', name: '类型', width: 120 },
315
- { key: 'defaultValue', name: '默认值', minWidth: 140 },
316
- { key: 'options', name: '选项(select)', minWidth: 160 }
317
- ])
318
-
319
- const schemaColumns = defineTableColumns([
320
- { key: 'name', name: '字段名(name)', minWidth: 140 },
321
- { key: 'label', name: '中文描述(label)', minWidth: 160 },
322
- { key: 'type', name: '类型', width: 88 }
323
- ])
324
-
325
- const previewColumns = computed(() =>
326
- defineTableColumns(
327
- previewFields.value.map((field) => ({
328
- key: field.name,
329
- name: field.label,
330
- minWidth: field.type === 'number' ? 88 : 112
331
- }))
332
- )
333
- )
334
-
335
- function fieldTypeColor(type: DatasetField['type']): ColorType {
336
- if (type === 'number') return 'success'
337
- if (type === 'date') return 'warning'
338
- return 'info'
339
- }
340
-
341
- function formatDefaultValue(value: unknown): string {
342
- if (Array.isArray(value)) return value.join(', ')
343
- return String(value ?? '')
344
- }
345
-
346
- function formatOptions(options?: { label: string; value: string }[]): string {
347
- if (!options?.length) return ''
348
- return options.map((item) => `${item.label}|${item.value}`).join(',')
349
- }
350
-
351
- function parseOptions(raw: string): { label: string; value: string }[] {
352
- return raw
353
- .split(',')
354
- .map((part) => part.trim())
355
- .filter(Boolean)
356
- .map((part) => {
357
- const [label, value] = part.split('|')
358
- const text = (label ?? '').trim()
359
- const val = (value ?? text).trim()
360
- return { label: text, value: val }
361
- })
362
- }
363
-
364
- function patchDataset(patch: Partial<Omit<DesignerDataset, 'id'>>): void {
365
- props.hub.updateDataset(props.datasetId, patch)
366
- }
367
-
368
- function patchParam(paramId: string, patch: Partial<Omit<QueryParamDef, 'id'>>): void {
369
- const current = dataset.value
370
- if (!current) return
371
- const nextOverrides = {
372
- ...current.paramOverrides,
373
- [paramId]: { ...current.paramOverrides?.[paramId], ...patch }
374
- }
375
- patchDataset({ paramOverrides: nextOverrides })
376
- }
377
-
378
- function patchFieldLabel(fieldName: string, label: string): void {
379
- const current = dataset.value
380
- if (!current) return
381
- const nextOverrides = {
382
- ...current.fieldOverrides,
383
- [fieldName]: { ...current.fieldOverrides?.[fieldName], label }
384
- }
385
- patchDataset({ fieldOverrides: nextOverrides })
386
- }
387
- </script>
@@ -1,2 +0,0 @@
1
- export { default as UReportViewer } from './report-viewer.vue'
2
- export { default as UReportDesigner } from './report-designer.vue'
@@ -1,33 +0,0 @@
1
- import type { SheetSnapshot } from '@veltra/sheet-core'
2
-
3
- export type PreviewGridSizeMode = 'template' | 'filled'
4
-
5
- /**
6
- * 报表预览网格尺寸:只铺到实际内容,不用设计态 canvas / 50×10 下限。
7
- *
8
- * - `template`:取数前的静态结构,按 cells/merges/meta 包围盒(忽略 snapshot.rows/cols,
9
- * 那是设计网格声明尺寸,会带出空白格)
10
- * - `filled`:行保留展开布局的 `snapshot.rows`(可能含无存值的空扩展行);列按包围盒收敛
11
- * (Filled Report 的 `cols` 常被 `max(layout, template.cols)` 撑到设计态列数)
12
- */
13
- export function previewGridSize(
14
- snapshot: Pick<SheetSnapshot, 'cells' | 'merges' | 'meta' | 'rows'>,
15
- mode: PreviewGridSizeMode
16
- ): { rows: number; cols: number } {
17
- let maxRow = -1
18
- let maxCol = -1
19
- const grow = (row: number, col: number): void => {
20
- if (row > maxRow) maxRow = row
21
- if (col > maxCol) maxCol = col
22
- }
23
- for (const cell of snapshot.cells) grow(cell.row, cell.col)
24
- for (const merge of snapshot.merges) grow(merge.end.row, merge.end.col)
25
- for (const item of snapshot.meta ?? []) grow(item.row, item.col)
26
-
27
- const boundRows = maxRow + 1
28
- const boundCols = maxCol + 1
29
- if (mode === 'filled') {
30
- return { rows: Math.max(boundRows, snapshot.rows, 1), cols: Math.max(boundCols, 1) }
31
- }
32
- return { rows: Math.max(boundRows, 1), cols: Math.max(boundCols, 1) }
33
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * 打印 HTML 文档:隐藏 iframe 装载后调起浏览器打印,结束(或兜底超时)销毁。
3
- *
4
- * 路线取舍:相对 `window.open` 不被弹窗拦截;相对 `@media print` 直打宿主页,
5
- * 不侵入应用布局,也不受网格 canvas 虚拟滚动(只绘制视口)限制。
6
- * iframe 用 0×0 定位隐藏而非 `display:none`(部分浏览器不打印 none 的 iframe)。
7
- */
8
- export function printHtmlDocument(html: string): void {
9
- const iframe = document.createElement('iframe')
10
- iframe.setAttribute('aria-hidden', 'true')
11
- iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;'
12
- document.body.appendChild(iframe)
13
-
14
- const win = iframe.contentWindow
15
- const doc = iframe.contentDocument
16
- if (!win || !doc || typeof win.print !== 'function') {
17
- iframe.remove()
18
- throw new Error('当前环境不支持打印')
19
- }
20
-
21
- let cleaned = false
22
- const cleanup = (): void => {
23
- if (cleaned) return
24
- cleaned = true
25
- iframe.remove()
26
- }
27
- win.addEventListener('afterprint', cleanup)
28
-
29
- doc.open()
30
- doc.write(html)
31
- doc.close()
32
-
33
- // 等内嵌图片(data URL)解码完再打印,避免打印快照取到未解码图片
34
- const decoding = Array.from(doc.images).map((img) =>
35
- typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve()
36
- )
37
- void Promise.all(decoding).then(() => {
38
- if (cleaned) return
39
- win.focus()
40
- win.print()
41
- // 兜底回收:afterprint 在部分环境不触发,或用户长期挂着打印对话框
42
- setTimeout(cleanup, 60_000)
43
- })
44
- }