@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.
Files changed (86) hide show
  1. package/dist/components/report/binding-badge.js +45 -21
  2. package/dist/components/report/designer/col-widths.js +18 -4
  3. package/dist/components/report/designer/conditional-rules/helpers.js +3 -3
  4. package/dist/components/report/designer/float-panel.js +73 -36
  5. package/dist/components/report/designer/rule-row.js +111 -127
  6. package/dist/components/report/designer/rules-dialog.js +5 -22
  7. package/dist/components/report/hub-dataset-editor.js +16 -1
  8. package/dist/components/report/preview-grid-size.js +32 -0
  9. package/dist/components/report/report-designer.js +75 -25
  10. package/dist/components/report/report-viewer.js +38 -25
  11. package/dist/components/report/style2.css +44 -39
  12. package/dist/components/report/use-report-designer.js +34 -12
  13. package/dist/components/report/use-report-viewer.js +6 -1
  14. package/dist/components/sheet/popups/border-popup.js +30 -2
  15. package/dist/components/sheet/popups/fill-color-popup.js +4 -3
  16. package/dist/components/sheet/popups/font-color-popup.js +2 -2
  17. package/dist/components/sheet/popups/font-size-popup.js +2 -2
  18. package/dist/components/sheet/sheet-context-menu.js +6 -3
  19. package/dist/components/sheet/sheet.js +8 -0
  20. package/dist/components/sheet/use-sheet-grid.js +14 -4
  21. package/dist/components/sheet/use-sheet-state.js +3 -2
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +2 -2
  24. package/dist/report/binding.d.ts +49 -11
  25. package/dist/report/binding.js +186 -29
  26. package/dist/report/connector.js +1 -1
  27. package/dist/report/export-xlsx.d.ts +3 -5
  28. package/dist/report/export-xlsx.js +7 -10
  29. package/dist/report/index.d.ts +2 -2
  30. package/dist/report/render/aggregate.js +20 -8
  31. package/dist/report/template.d.ts +8 -4
  32. package/dist/report/template.js +5 -2
  33. package/dist/tools/apply-style.js +29 -0
  34. package/dist/tools/builtin.js +17 -17
  35. package/dist/tools/context.d.ts +38 -5
  36. package/dist/tools/context.js +38 -1
  37. package/dist/types/report-designer.d.ts +2 -0
  38. package/dist/types/report-viewer.d.ts +2 -2
  39. package/dist/types/sheet.d.ts +4 -0
  40. package/package.json +15 -6
  41. package/src/components/report/__test__/conditional-rules-helpers.test.ts +1 -0
  42. package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
  43. package/src/components/report/__test__/report-designer.test.ts +76 -0
  44. package/src/components/report/__test__/report-viewer.test.ts +24 -1
  45. package/src/components/report/__test__/use-report-designer.test.ts +142 -10
  46. package/src/components/report/binding-badge.ts +39 -20
  47. package/src/components/report/designer/col-widths.ts +34 -11
  48. package/src/components/report/designer/conditional-rules/helpers.ts +6 -3
  49. package/src/components/report/designer/float-panel.vue +71 -27
  50. package/src/components/report/designer/rule-row.vue +67 -67
  51. package/src/components/report/designer/rules-dialog.vue +3 -19
  52. package/src/components/report/hub-dataset-editor.vue +19 -1
  53. package/src/components/report/preview-grid-size.ts +33 -0
  54. package/src/components/report/report-designer.vue +79 -15
  55. package/src/components/report/report-viewer.vue +37 -29
  56. package/src/components/report/style-designer.scss +45 -41
  57. package/src/components/report/use-report-designer.ts +63 -14
  58. package/src/components/report/use-report-viewer.ts +11 -2
  59. package/src/components/sheet/__test__/sheet-component.test.ts +37 -0
  60. package/src/components/sheet/__test__/sheet-context-menu.test.ts +3 -2
  61. package/src/components/sheet/popups/border-popup.vue +31 -3
  62. package/src/components/sheet/popups/fill-color-popup.vue +5 -4
  63. package/src/components/sheet/popups/font-color-popup.vue +2 -2
  64. package/src/components/sheet/popups/font-size-popup.vue +2 -2
  65. package/src/components/sheet/sheet-context-menu.ts +8 -2
  66. package/src/components/sheet/sheet.vue +2 -0
  67. package/src/components/sheet/use-sheet-grid.ts +17 -3
  68. package/src/components/sheet/use-sheet-state.ts +8 -3
  69. package/src/report/__test__/binding.test.ts +103 -1
  70. package/src/report/__test__/connector.test.ts +21 -0
  71. package/src/report/__test__/export-xlsx.test.ts +4 -4
  72. package/src/report/__test__/render.test.ts +102 -0
  73. package/src/report/__test__/template.test.ts +11 -0
  74. package/src/report/binding.ts +278 -47
  75. package/src/report/connector.ts +3 -2
  76. package/src/report/export-xlsx.ts +9 -14
  77. package/src/report/render/aggregate.ts +29 -8
  78. package/src/report/template.ts +8 -4
  79. package/src/tools/__test__/apply-style.test.ts +129 -0
  80. package/src/tools/apply-style.ts +41 -0
  81. package/src/tools/builtin.ts +18 -18
  82. package/src/tools/context.ts +67 -4
  83. package/src/tools/index.ts +1 -0
  84. package/src/types/report-designer.ts +2 -0
  85. package/src/types/report-viewer.ts +2 -2
  86. package/src/types/sheet.ts +4 -0
@@ -21,6 +21,7 @@
21
21
  @update:model-value="onPreset"
22
22
  />
23
23
  <u-select
24
+ v-if="showAggregateSelect"
24
25
  size="small"
25
26
  :class="[cls.e('select'), cls.em('select', 'aggregate')]"
26
27
  :model-value="binding.aggregate"
@@ -28,6 +29,7 @@
28
29
  @update:model-value="onAggregate"
29
30
  />
30
31
  <u-select
32
+ v-if="showExpandSelect"
31
33
  size="small"
32
34
  :class="[cls.e('select'), cls.em('select', 'expand')]"
33
35
  :model-value="binding.expand"
@@ -35,6 +37,7 @@
35
37
  @update:model-value="onExpand"
36
38
  />
37
39
  <u-button size="small" plain @click="emit('open-rules')">条件样式</u-button>
40
+ <u-button size="small" text type="danger" @click="emit('remove')">删除绑定</u-button>
38
41
  <u-button
39
42
  size="small"
40
43
  text
@@ -138,11 +141,8 @@
138
141
  </div>
139
142
  </section>
140
143
 
141
- <footer :class="cls.e('footer')">
142
- <u-button v-if="parentPickMode" size="small" plain @click="emit('cancel-parent-pick')">
143
- 取消点选
144
- </u-button>
145
- <u-button size="small" type="danger" plain @click="emit('remove')">删除绑定</u-button>
144
+ <footer v-if="parentPickMode" :class="cls.e('footer')">
145
+ <u-button size="small" plain @click="emit('cancel-parent-pick')">取消点选</u-button>
146
146
  </footer>
147
147
  </div>
148
148
  </div>
@@ -157,7 +157,9 @@ import { computed, nextTick, ref, toRef, watch } from 'vue'
157
157
  import {
158
158
  formatBindingPlaceholder,
159
159
  formatCellAddress,
160
- inferReportPreset
160
+ formatParentCandidateLabel,
161
+ inferReportPreset,
162
+ isReductionAggregate
161
163
  } from '../../../report/binding'
162
164
  import type {
163
165
  ReportAggregate,
@@ -188,6 +190,7 @@ const props = defineProps<{
188
190
  rowParentCandidates: CellAddress[]
189
191
  colParentCandidates: CellAddress[]
190
192
  resolveFieldLabel?: (datasetId: string, fieldName: string) => string
193
+ getBindingAt?: (addr: CellAddress) => ReportBinding | undefined
191
194
  hostEl: HTMLElement | null
192
195
  getGrid: () => SheetGrid | undefined
193
196
  }>()
@@ -216,7 +219,7 @@ const presetOptions = computed(() => {
216
219
  return options
217
220
  })
218
221
 
219
- const aggregateOptions = [
222
+ const ALL_AGGREGATE_OPTIONS = [
220
223
  { value: 'list' as const, label: '明细' },
221
224
  { value: 'group' as const, label: '分组' },
222
225
  { value: 'sum' as const, label: '求和' },
@@ -226,6 +229,10 @@ const aggregateOptions = [
226
229
  { value: 'min' as const, label: '最小' }
227
230
  ]
228
231
 
232
+ const REDUCTION_AGGREGATE_OPTIONS = ALL_AGGREGATE_OPTIONS.filter((item) =>
233
+ isReductionAggregate(item.value)
234
+ )
235
+
229
236
  const expandOptions = [
230
237
  { value: 'down' as const, label: '向下' },
231
238
  { value: 'right' as const, label: '向右' },
@@ -247,24 +254,59 @@ const currentPresetValue = computed(() => {
247
254
  return inferReportPreset(props.binding) ?? CUSTOM_PRESET_VALUE
248
255
  })
249
256
 
257
+ const showAggregateSelect = computed(() => {
258
+ const preset = currentPresetValue.value
259
+ return (
260
+ preset === CUSTOM_PRESET_VALUE ||
261
+ preset === 'subtotal' ||
262
+ preset === 'grandTotal' ||
263
+ preset === 'cross'
264
+ )
265
+ })
266
+
267
+ const showExpandSelect = computed(() => {
268
+ const preset = currentPresetValue.value
269
+ return preset === CUSTOM_PRESET_VALUE || preset === 'groupHeader' || preset === 'detail'
270
+ })
271
+
272
+ const aggregateOptions = computed(() =>
273
+ currentPresetValue.value === CUSTOM_PRESET_VALUE
274
+ ? ALL_AGGREGATE_OPTIONS
275
+ : REDUCTION_AGGREGATE_OPTIONS
276
+ )
277
+
250
278
  const summaryText = computed(() => {
251
279
  if (!props.binding) return ''
252
- return formatBindingPlaceholder(props.binding, props.resolveFieldLabel)
280
+ const parts = [formatBindingPlaceholder(props.binding, props.resolveFieldLabel)]
281
+ if (props.binding.rowParent && props.resolvedRowParentLabel !== '—') {
282
+ const parent = props.getBindingAt?.(props.binding.rowParent)
283
+ const name = parent
284
+ ? (props.resolveFieldLabel?.(parent.dataset, parent.field) ?? parent.field)
285
+ : props.resolvedRowParentLabel
286
+ parts.push(`行父格 ${name} ${props.resolvedRowParentLabel}`)
287
+ }
288
+ if (props.binding.colParent && props.resolvedColParentLabel !== '—') {
289
+ const parent = props.getBindingAt?.(props.binding.colParent)
290
+ const name = parent
291
+ ? (props.resolveFieldLabel?.(parent.dataset, parent.field) ?? parent.field)
292
+ : props.resolvedColParentLabel
293
+ parts.push(`列父格 ${name} ${props.resolvedColParentLabel}`)
294
+ }
295
+ return parts.join(' · ')
253
296
  })
254
297
 
255
- const rowParentOptions = computed(() =>
256
- props.rowParentCandidates.map((addr) => ({
257
- value: formatCellAddress(addr),
258
- label: formatCellAddress(addr)
259
- }))
260
- )
298
+ function candidateOption(addr: CellAddress): { value: string; label: string } {
299
+ const value = formatCellAddress(addr)
300
+ const binding = props.getBindingAt?.(addr)
301
+ return {
302
+ value,
303
+ label: binding ? formatParentCandidateLabel(addr, binding, props.resolveFieldLabel) : value
304
+ }
305
+ }
261
306
 
262
- const colParentOptions = computed(() =>
263
- props.colParentCandidates.map((addr) => ({
264
- value: formatCellAddress(addr),
265
- label: formatCellAddress(addr)
266
- }))
267
- )
307
+ const rowParentOptions = computed(() => props.rowParentCandidates.map(candidateOption))
308
+
309
+ const colParentOptions = computed(() => props.colParentCandidates.map(candidateOption))
268
310
 
269
311
  const rowParentSelectValue = computed(() =>
270
312
  props.binding?.rowParent ? formatCellAddress(props.binding.rowParent) : undefined
@@ -281,10 +323,18 @@ const panelStyle = computed(() => ({ left: `${panelLeft.value}px`, top: `${panel
281
323
  watch(
282
324
  () => [props.cell?.row, props.cell?.col] as const,
283
325
  () => {
326
+ if (props.parentPickMode) return
284
327
  expanded.value = false
285
328
  }
286
329
  )
287
330
 
331
+ watch(
332
+ () => props.parentPickMode,
333
+ (mode) => {
334
+ if (mode) expanded.value = true
335
+ }
336
+ )
337
+
288
338
  watch(expanded, () => {
289
339
  nextTick(() => updatePosition())
290
340
  })
@@ -300,13 +350,7 @@ function onPreset(preset: ReportPreset | typeof CUSTOM_PRESET_VALUE): void {
300
350
  }
301
351
 
302
352
  function aggregateDefaultExpand(aggregate: ReportAggregate): ReportExpand {
303
- return aggregate === 'sum' ||
304
- aggregate === 'avg' ||
305
- aggregate === 'count' ||
306
- aggregate === 'max' ||
307
- aggregate === 'min'
308
- ? 'none'
309
- : 'down'
353
+ return isReductionAggregate(aggregate) ? 'none' : 'down'
310
354
  }
311
355
 
312
356
  function onAggregate(aggregate: ReportAggregate): void {
@@ -1,27 +1,17 @@
1
1
  <template>
2
2
  <div :class="cls.b">
3
- <span :class="cls.e('handle')" title="拖拽排序">≡</span>
3
+ <span :class="cls.e('handle')" title="拖拽排序">⋮⋮</span>
4
4
 
5
5
  <div :class="cls.e('main')">
6
- <div :class="cls.e('meta')">
6
+ <div :class="cls.e('when')">
7
+ <span :class="cls.e('lead')">当</span>
7
8
  <u-select
8
9
  size="small"
9
10
  :class="cls.e('field-select')"
10
- :model-value="rule.field"
11
+ :model-value="rule.field ?? SELF_FIELD_VALUE"
11
12
  :options="fieldOptions"
12
- placeholder="求值字段"
13
13
  @update:model-value="onEvalField"
14
14
  />
15
- <u-select
16
- size="small"
17
- :class="cls.e('scope-select')"
18
- :model-value="rule.scope ?? 'cell'"
19
- :options="scopeOptions"
20
- @update:model-value="onScope"
21
- />
22
- </div>
23
-
24
- <div :class="cls.e('condition')">
25
15
  <u-select
26
16
  size="small"
27
17
  :class="cls.e('operator')"
@@ -29,7 +19,6 @@
29
19
  :options="operatorOptions"
30
20
  @update:model-value="onOperator"
31
21
  />
32
-
33
22
  <div :class="cls.e('value')">
34
23
  <template v-if="rule.operator === 'between'">
35
24
  <u-number-input
@@ -80,56 +69,76 @@
80
69
  @update:model-value="(v) => patchValue(v ?? 0)"
81
70
  />
82
71
  </div>
72
+ <span :class="cls.e('lead')">时</span>
73
+ </div>
83
74
 
75
+ <div :class="cls.e('then')">
76
+ <span :class="cls.e('lead')">样式</span>
77
+ <span title="背景">
78
+ <u-palette
79
+ size="small"
80
+ :model-value="rule.style.fill?.color ?? ''"
81
+ @update:model-value="patchFillColor"
82
+ />
83
+ </span>
84
+ <span title="文字">
85
+ <u-palette
86
+ size="small"
87
+ :model-value="rule.style.font?.color ?? ''"
88
+ @update:model-value="patchFontColor"
89
+ />
90
+ </span>
91
+ <u-button-group>
92
+ <u-button
93
+ size="small"
94
+ :class="cls.e('toggle')"
95
+ :type="rule.style.font?.bold ? 'primary' : undefined"
96
+ plain
97
+ title="加粗"
98
+ @click="toggleBold"
99
+ >
100
+ B
101
+ </u-button>
102
+ <u-button
103
+ size="small"
104
+ :class="cls.e('toggle')"
105
+ :type="rule.style.font?.italic ? 'primary' : undefined"
106
+ plain
107
+ title="斜体"
108
+ @click="toggleItalic"
109
+ >
110
+ I
111
+ </u-button>
112
+ </u-button-group>
84
113
  <u-report-rule-preview :class="cls.e('preview')" :rule="rule" />
85
- </div>
86
114
 
87
- <div :class="cls.e('style')">
88
- <span :class="cls.e('style-label')">背景</span>
89
- <u-palette
90
- size="small"
91
- :model-value="rule.style.fill?.color ?? ''"
92
- @update:model-value="patchFillColor"
93
- />
94
- <span :class="cls.e('style-label')">字体</span>
95
- <u-palette
96
- size="small"
97
- :model-value="rule.style.font?.color ?? ''"
98
- @update:model-value="patchFontColor"
99
- />
100
- <u-button
101
- size="small"
102
- :class="cls.e('toggle')"
103
- :type="rule.style.font?.bold ? 'primary' : undefined"
104
- plain
105
- @click="toggleBold"
106
- >
107
- B
108
- </u-button>
109
- <u-button
110
- size="small"
111
- :class="cls.e('toggle')"
112
- :type="rule.style.font?.italic ? 'primary' : undefined"
113
- plain
114
- @click="toggleItalic"
115
- >
116
- I
117
- </u-button>
115
+ <span :class="cls.e('lead')">应用到</span>
116
+ <u-button-group>
117
+ <u-button
118
+ v-for="option in RULE_SCOPE_OPTIONS"
119
+ :key="option.value"
120
+ size="small"
121
+ plain
122
+ :type="(rule.scope ?? 'cell') === option.value ? 'primary' : undefined"
123
+ :title="
124
+ option.value === 'row' ? '染满物理输出行(交叉表会盖住同行所有列)' : '只改本格'
125
+ "
126
+ @click="onScope(option.value)"
127
+ >
128
+ {{ option.label }}
129
+ </u-button>
130
+ </u-button-group>
118
131
  </div>
119
132
  </div>
120
133
 
121
- <div :class="cls.e('actions')">
122
- <u-button size="small" text :disabled="index === 0" @click="emit('move-up')">↑</u-button>
123
- <u-button size="small" text :disabled="index >= total - 1" @click="emit('move-down')">
124
- ↓
125
- </u-button>
126
- <u-button size="small" text type="danger" @click="emit('remove')">删除</u-button>
127
- </div>
134
+ <u-button size="small" text type="danger" :class="cls.e('remove')" @click="emit('remove')">
135
+ 删除
136
+ </u-button>
128
137
  </div>
129
138
  </template>
130
139
 
131
140
  <script lang="ts" setup>
132
- import { UButton, UInput, UNumberInput, UPalette, USelect } from '@veltra/desktop'
141
+ import { UButton, UButtonGroup, UInput, UNumberInput, UPalette, USelect } from '@veltra/desktop'
133
142
  import { bem } from '@veltra/utils'
134
143
  import { computed } from 'vue'
135
144
 
@@ -142,6 +151,7 @@ import {
142
151
  resolveEvalFieldType,
143
152
  RULE_SCOPE_OPTIONS,
144
153
  ruleEvalFieldOptions,
154
+ SELF_FIELD_VALUE,
145
155
  writeBetweenValue
146
156
  } from './conditional-rules/helpers'
147
157
  import UReportRulePreview from './rule-preview.vue'
@@ -152,16 +162,9 @@ const props = defineProps<{
152
162
  rule: ConditionalRule
153
163
  bindingField: string
154
164
  datasetFields: readonly DatasetField[]
155
- index: number
156
- total: number
157
165
  }>()
158
166
 
159
- const emit = defineEmits<{
160
- 'update:rule': [rule: ConditionalRule]
161
- remove: []
162
- 'move-up': []
163
- 'move-down': []
164
- }>()
167
+ const emit = defineEmits<{ 'update:rule': [rule: ConditionalRule]; remove: [] }>()
165
168
 
166
169
  const cls = bem('report-rule-row')
167
170
 
@@ -171,9 +174,6 @@ const evalFieldType = computed(() =>
171
174
  const operatorOptions = computed(() => operatorsForFieldType(evalFieldType.value))
172
175
  const numericField = computed(() => isNumericFieldType(evalFieldType.value))
173
176
  const fieldOptions = computed(() => ruleEvalFieldOptions(props.bindingField, props.datasetFields))
174
- const scopeOptions = computed(() =>
175
- RULE_SCOPE_OPTIONS.map((option) => ({ value: option.value, label: option.label }))
176
- )
177
177
 
178
178
  const betweenStart = computed(() => readBetweenValue(props.rule.value, 0, evalFieldType.value))
179
179
  const betweenEnd = computed(() => readBetweenValue(props.rule.value, 1, evalFieldType.value))
@@ -191,7 +191,7 @@ function patchRule(patch: Partial<ConditionalRule>): void {
191
191
 
192
192
  function onEvalField(field: string | undefined): void {
193
193
  const next: Partial<ConditionalRule> = {}
194
- if (field === undefined) {
194
+ if (!field) {
195
195
  next.field = undefined
196
196
  } else {
197
197
  next.field = field
@@ -2,13 +2,11 @@
2
2
  <u-dialog
3
3
  v-model="visible"
4
4
  title="条件样式"
5
- style="width: min(960px, 92vw); max-height: 85vh"
5
+ style="width: min(600px, 92vw); max-height: 85vh"
6
6
  @close="onClose"
7
7
  >
8
8
  <div :class="cls.b">
9
- <p :class="cls.e('hint')">
10
- 按列表顺序依次求值并合并样式。可指定求值字段(默认同绑定格字段);作用范围为「整行」时染满物理输出行(含横向展开列与静态格)。交叉表下整行高亮会覆盖同行所有列,明细行报表更合适。
11
- </p>
9
+ <p :class="cls.e('hint')">规则按顺序叠加。整行高亮适合明细表,交叉表会染满整行。</p>
12
10
 
13
11
  <ul v-if="draftItems.length" ref="parentRef" :class="cls.e('list')">
14
12
  <li v-for="(item, index) in draftItems" :key="item.id" :class="cls.e('item')">
@@ -16,16 +14,12 @@
16
14
  :rule="item.rule"
17
15
  :binding-field="bindingField"
18
16
  :dataset-fields="datasetFields"
19
- :index="index"
20
- :total="draftItems.length"
21
17
  @update:rule="item.rule = $event"
22
18
  @remove="removeRule(index)"
23
- @move-up="moveRule(index, -1)"
24
- @move-down="moveRule(index, 1)"
25
19
  />
26
20
  </li>
27
21
  </ul>
28
- <p v-else :class="cls.e('empty')">暂无规则,点击下方按钮添加。</p>
22
+ <p v-else :class="cls.e('empty')">还没有规则。</p>
29
23
 
30
24
  <u-button size="small" plain @click="addRule">添加规则</u-button>
31
25
  </div>
@@ -105,16 +99,6 @@ function removeRule(index: number): void {
105
99
  draftItems.value = draftItems.value.filter((_, i) => i !== index)
106
100
  }
107
101
 
108
- function moveRule(index: number, delta: -1 | 1): void {
109
- const target = index + delta
110
- if (target < 0 || target >= draftItems.value.length) return
111
- const next = [...draftItems.value]
112
- const [item] = next.splice(index, 1)
113
- if (!item) return
114
- next.splice(target, 0, item)
115
- draftItems.value = next
116
- }
117
-
118
102
  function confirm(): void {
119
103
  emit('save', cloneRulesFromDraft(draftItems.value))
120
104
  visible.value = false
@@ -219,11 +219,27 @@ watch(
219
219
  { immediate: true }
220
220
  )
221
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
+
222
234
  function onSqlChange(value: string): void {
223
235
  sqlDraft.value = value
236
+ const datasetId = props.datasetId
224
237
  if (sqlTimer) clearTimeout(sqlTimer)
225
238
  sqlTimer = setTimeout(() => {
226
- props.hub.updateDataset(props.datasetId, { sql: value })
239
+ sqlTimer = undefined
240
+ // 防抖窗口内切换了数据集则丢弃,避免把旧草稿写进新数据集
241
+ if (props.datasetId !== datasetId) return
242
+ props.hub.updateDataset(datasetId, { sql: value })
227
243
  void runDescribe()
228
244
  }, 280)
229
245
  }
@@ -271,6 +287,8 @@ const schemaRows = computed(() => {
271
287
  // ---- 记录预览(按参数默认值经连接器取数) ----
272
288
 
273
289
  async function runPreview(): Promise<void> {
290
+ // 编辑器 change 仅在失焦时触发且提交有 debounce,先同步落草稿 SQL,避免用旧 SQL 取数
291
+ flushSqlDraft(props.datasetId)
274
292
  previewing.value = true
275
293
  previewError.value = ''
276
294
  const result = await props.hub.previewDataset(props.datasetId)
@@ -0,0 +1,33 @@
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
+ }