@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
@@ -35,7 +35,7 @@
35
35
 
36
36
  <span :class="cls.e('hint')">
37
37
  在数据中枢配置连接与 SQL
38
- 数据集,再将左侧字段点击或拖拽到单元格完成绑定;选中已绑定格可通过悬浮卡片调整角色、聚合与条件样式。
38
+ 数据集,再将左侧字段点击或拖拽到单元格完成绑定;选中已绑定格可通过悬浮卡片调整预设与条件样式。
39
39
  </span>
40
40
  </div>
41
41
 
@@ -59,8 +59,8 @@
59
59
  ref="sheetRef"
60
60
  :class="cls.e('sheet')"
61
61
  :workbook="workbook"
62
- :rows="DESIGN_ROWS"
63
- :cols="DESIGN_COLS"
62
+ :rows="gridRows"
63
+ :cols="gridCols"
64
64
  :show-tabs="false"
65
65
  :resolve-cell-style="resolveCellStyle"
66
66
  :resolve-cell-renderer="resolveCellRenderer"
@@ -84,6 +84,20 @@
84
84
  :fallback-label="selectionLabel"
85
85
  />
86
86
 
87
+ <u-report-drop-highlight-overlay
88
+ v-for="addr in pickCandidateCells"
89
+ :key="`pick-${addr.row}-${addr.col}`"
90
+ :cell="addr"
91
+ :host-el="gridHostEl"
92
+ :get-grid="getDesignGrid"
93
+ :dragging="false"
94
+ fallback-label=""
95
+ />
96
+
97
+ <div v-if="parentPick" :class="cls.e('pick-hint')" role="status">
98
+ {{ parentPick.mode === 'row' ? '点击分组头作为行方向父格' : '点击分组头作为列方向父格' }}
99
+ </div>
100
+
87
101
  <u-report-float-panel
88
102
  :cell="activeCell"
89
103
  :binding="activeBinding ?? null"
@@ -93,6 +107,7 @@
93
107
  :row-parent-candidates="rowParentCandidates"
94
108
  :col-parent-candidates="colParentCandidates"
95
109
  :resolve-field-label="resolveFieldLabel"
110
+ :get-binding-at="getBindingAt"
96
111
  :host-el="gridHostEl"
97
112
  :get-grid="getDesignGrid"
98
113
  @patch="patchActiveBinding"
@@ -111,12 +126,11 @@
111
126
  :connector="props.connector"
112
127
  :template="previewTemplate"
113
128
  :workbook="previewWorkbook"
114
- :col-widths="designColWidths"
115
129
  />
116
130
  </div>
117
131
 
118
132
  <u-drawer v-model="hubVisible" :class="cls.e('hub-drawer')" show-close>
119
- <u-report-dataset-hub :hub="designer" @close="hubVisible = false" />
133
+ <u-report-dataset-hub :hub="designer" @close="onHubClose" />
120
134
  </u-drawer>
121
135
 
122
136
  <u-report-rules-dialog
@@ -135,7 +149,7 @@ import { UButton, UDrawer } from '@veltra/desktop'
135
149
  import type { CellAddress, SheetGrid } from '@veltra/sheet-core'
136
150
  import { Workbook } from '@veltra/sheet-core'
137
151
  import { bem } from '@veltra/utils'
138
- import { computed, nextTick, ref, shallowRef, useTemplateRef, watch } from 'vue'
152
+ import { computed, nextTick, onScopeDispose, ref, shallowRef, useTemplateRef, watch } from 'vue'
139
153
 
140
154
  import type { ReportTemplate } from '../../report/template'
141
155
  import type { ConditionalRule } from '../../report/types'
@@ -149,7 +163,7 @@ import type {
149
163
  import { USheet } from '../sheet'
150
164
  import UReportDatasetHub from './designer-hub.vue'
151
165
  import { resolveGridDropAddress } from './designer/cell-coords'
152
- import { applyGridColWidths, readGridColWidths } from './designer/col-widths'
166
+ import { applyGridColWidths, applySheetColWidths, readGridColWidths } from './designer/col-widths'
153
167
  import UReportDropHighlightOverlay from './designer/drop-highlight-overlay.vue'
154
168
  import UReportFloatPanel from './designer/float-panel.vue'
155
169
  import UReportRulesDialog from './designer/rules-dialog.vue'
@@ -166,7 +180,7 @@ const emit = defineEmits<ReportDesignerEmits>()
166
180
 
167
181
  const cls = bem('report-designer')
168
182
 
169
- /** 设计态网格尺寸(列宽捕获范围与之一致) */
183
+ /** 设计态网格默认尺寸(新建模板;已声明尺寸后以模型为准,避免删行后被 props 撑回) */
170
184
  const DESIGN_ROWS = 24
171
185
  const DESIGN_COLS = 10
172
186
 
@@ -210,9 +224,38 @@ const {
210
224
  getTemplate
211
225
  } = designer
212
226
 
227
+ /** 渲染行列数跟随模型(删行后不再被固定 props 经 ensureTableSize 撑回) */
228
+ const gridRows = computed(() => {
229
+ const rows = workbook.value.activeSheet.rows
230
+ return rows > 0 ? rows : DESIGN_ROWS
231
+ })
232
+ const gridCols = computed(() => {
233
+ const cols = workbook.value.activeSheet.cols
234
+ return cols > 0 ? cols : DESIGN_COLS
235
+ })
236
+
213
237
  const hubVisible = ref(false)
214
238
  const rulesDialogVisible = ref(false)
215
239
 
240
+ const pickCandidateCells = computed((): CellAddress[] => {
241
+ if (!parentPick.value) return []
242
+ return parentPick.value.mode === 'row' ? rowParentCandidates.value : colParentCandidates.value
243
+ })
244
+
245
+ function onHubClose(): void {
246
+ hubVisible.value = false
247
+ }
248
+
249
+ watch(hubVisible, (open) => {
250
+ if (!open) emit('datasets-change')
251
+ })
252
+
253
+ watch(
254
+ () => designer.datasets.value,
255
+ () => emit('datasets-change'),
256
+ { deep: true }
257
+ )
258
+
216
259
  const sheetRef = useTemplateRef<SheetExposed>('sheetRef')
217
260
  const previewViewerRef = useTemplateRef<ReportViewerExposed>('previewViewerRef')
218
261
  const gridHostRef = useTemplateRef<HTMLElement>('gridHostRef')
@@ -237,17 +280,21 @@ const isPreview = computed(() => viewMode.value === 'preview')
237
280
  const previewTemplate = shallowRef<ReportTemplate | null>(null)
238
281
  /** 预览工作簿自持(XLSX 导出需要拿到填充后的 sheet,05 票 workbook? prop 先例) */
239
282
  const previewWorkbook = new Workbook()
240
- /** 当前设计态列宽(sheet-core 列宽未进快照,经 VTable 运行时捕获;导出随快照写宽) */
283
+ /** 当前设计态列宽(与 Sheet 模型同步;getTemplate 经 snapshot.colWidths 吐出) */
241
284
  const designColWidths = ref<Array<[number, number]>>([])
242
285
  const exporting = ref(false)
243
286
 
244
- /** 从当前设计网格捕获列宽(网格未就绪则保留旧值) */
287
+ /** 从当前设计网格捕获列宽并写回 Sheet 模型(网格未就绪则保留旧值) */
245
288
  function captureDesignColWidths(): void {
246
289
  const widths = readGridColWidths(
247
290
  getDesignGrid(),
248
- Array.from({ length: DESIGN_COLS }, (_, col) => col)
291
+ Array.from({ length: gridCols.value }, (_, col) => col)
249
292
  )
250
- if (widths) designColWidths.value = widths
293
+ if (!widths) return
294
+ // 仅保留相对默认列宽有变化的条目,避免快照膨胀
295
+ const custom = widths.filter(([, width]) => width > 0)
296
+ designColWidths.value = custom
297
+ applySheetColWidths(workbook.value.activeSheet, custom)
251
298
  }
252
299
 
253
300
  /** 设计网格重建后补刷新与列宽(restore 不发 content-reset 的场景由 grid 自行订阅,这里只管列宽) */
@@ -268,11 +315,18 @@ function setViewMode(mode: ViewMode): void {
268
315
  if (mode === 'design') void nextTick(syncDesignGridView)
269
316
  }
270
317
 
271
- // 模板 prop 更换:组合式函数内完成载入;组件层切回设计态(预览内容随之作废)
318
+ // 模板更换:组合式函数内完成载入;组件层切回设计态并恢复列宽
272
319
  watch(
273
320
  () => props.template,
274
- () => {
321
+ (template) => {
275
322
  viewMode.value = 'design'
323
+ designColWidths.value = template?.colWidths?.length
324
+ ? template.colWidths.map(([col, width]) => [col, width] as [number, number])
325
+ : []
326
+ if (designColWidths.value.length) {
327
+ applySheetColWidths(workbook.value.activeSheet, designColWidths.value)
328
+ }
329
+ void nextTick(syncDesignGridView)
276
330
  }
277
331
  )
278
332
 
@@ -286,6 +340,11 @@ watch(catalog, () => {
286
340
  void nextTick(() => getDesignGrid()?.refresh())
287
341
  })
288
342
 
343
+ const offStructureChange = workbook.value.activeSheet.on('structure-change', () => {
344
+ void nextTick(syncDesignGridView)
345
+ })
346
+ onScopeDispose(offStructureChange)
347
+
289
348
  async function exportPreviewXlsx(): Promise<void> {
290
349
  if (!isPreview.value || exporting.value) return
291
350
  const viewer = previewViewerRef.value
@@ -352,5 +411,10 @@ function onGridDrop(event: DragEvent): void {
352
411
  }
353
412
  }
354
413
 
355
- defineExpose<_ReportDesignerExposed>({ getTemplate })
414
+ defineExpose<_ReportDesignerExposed>({
415
+ getTemplate: () => {
416
+ captureDesignColWidths()
417
+ return getTemplate()
418
+ }
419
+ })
356
420
  </script>
@@ -20,11 +20,13 @@
20
20
  ref="sheetRef"
21
21
  :class="cls.e('sheet')"
22
22
  :workbook="workbook"
23
- :rows="renderRows"
24
- :cols="renderCols"
23
+ :rows="renderSize.rows"
24
+ :cols="renderSize.cols"
25
25
  :show-toolbar="false"
26
26
  :show-formula-bar="false"
27
27
  :show-tabs="false"
28
+ :show-row-header="false"
29
+ :show-col-header="false"
28
30
  readonly
29
31
  />
30
32
  </div>
@@ -42,8 +44,9 @@ import { exportFilledReportXlsx } from '../../report/export-xlsx'
42
44
  import type { ReportViewerProps, _ReportViewerExposed } from '../../types'
43
45
  import type { SheetExposed } from '../../types'
44
46
  import { USheet } from '../sheet'
45
- import { applyGridColWidths, readGridColWidths } from './designer/col-widths'
47
+ import { applyGridColWidths, applySheetColWidths } from './designer/col-widths'
46
48
  import UReportFilterBar from './filter-bar.vue'
49
+ import { previewGridSize, type PreviewGridSizeMode } from './preview-grid-size'
47
50
  import { useReportViewer } from './use-report-viewer'
48
51
 
49
52
  defineOptions({ name: 'UReportViewer' })
@@ -52,10 +55,6 @@ const props = defineProps<ReportViewerProps>()
52
55
 
53
56
  const cls = bem('report-viewer')
54
57
 
55
- /** 网格最小渲染尺寸(与 playground 预览态一致);Filled Report 超出时按快照尺寸放大 */
56
- const MIN_RENDER_ROWS = 50
57
- const MIN_RENDER_COLS = 10
58
-
59
58
  /** 取数未完成时拒绝导出的可读错误 */
60
59
  const EXPORT_NOT_READY_MESSAGE = '报表数据尚未就绪,请等待取数完成后再导出'
61
60
  const EXPORT_LOADING_MESSAGE = '报表数据加载中,请稍后再导出'
@@ -66,44 +65,54 @@ const workbook = computed(() => props.workbook ?? internalWorkbook)
66
65
 
67
66
  const sheetRef = useTemplateRef<SheetExposed>('sheetRef')
68
67
 
69
- const { params, values, loading, error, filledSnapshot, refresh, setValues } =
68
+ const { params, values, loading, error, filledSnapshot, filledColWidths, refresh, setValues } =
70
69
  useReportViewer(props)
71
70
 
72
- const renderRows = computed(() =>
73
- Math.max(MIN_RENDER_ROWS, filledSnapshot.value?.rows ?? 0, props.template.rows)
74
- )
75
- const renderCols = computed(() =>
76
- Math.max(MIN_RENDER_COLS, filledSnapshot.value?.cols ?? 0, props.template.cols)
77
- )
71
+ const renderSize = computed(() => {
72
+ const filled = filledSnapshot.value
73
+ if (filled) return previewGridSize(filled, 'filled')
74
+ return previewGridSize(props.template, 'template')
75
+ })
78
76
 
79
- function exportColIndexes(): number[] {
80
- return Array.from({ length: renderCols.value }, (_, col) => col)
81
- }
77
+ /** 宿主显式传入优先;否则取数完成后用展开映射列宽;再回落模板设计态列宽 */
78
+ const effectiveColWidths = computed(() => {
79
+ if (props.colWidths?.length) return props.colWidths
80
+ if (filledColWidths.value?.length) return filledColWidths.value
81
+ return props.template.colWidths
82
+ })
82
83
 
83
- /** 将宿主传入的列宽写入 VTable 运行时(sheet-core 列宽未进快照) */
84
+ /** 将运行时列宽写入模型与 VTable(模型为单一事实源;grid 构造/content-reset 也会回放) */
84
85
  function applyRuntimeColWidths(): void {
85
- if (!props.colWidths?.length) return
86
- applyGridColWidths(sheetRef.value?.getGrid(), props.colWidths)
86
+ if (!effectiveColWidths.value?.length) return
87
+ const sheet = workbook.value.activeSheet
88
+ applySheetColWidths(sheet, effectiveColWidths.value)
89
+ applyGridColWidths(sheetRef.value?.getGrid(), effectiveColWidths.value)
87
90
  }
88
91
 
89
92
  /**
90
- * restore 负责尺寸/冻结/行高/选区(静默),restoreContent 发 content-reset
93
+ * restore 负责尺寸/冻结/行高/列宽/选区(静默),restoreContent 发 content-reset
91
94
  * 触发网格全量刷新(grid 层订阅直刷,无需手动 flush);history.clear 使填充不进 undo
92
95
  */
93
- function applySnapshot(snapshot: SheetSnapshot): void {
96
+ function applySnapshot(snapshot: SheetSnapshot, mode: PreviewGridSizeMode): void {
97
+ const size = previewGridSize(snapshot, mode)
98
+ const trimmed: SheetSnapshot = { ...snapshot, rows: size.rows, cols: size.cols }
94
99
  const sheet = workbook.value.activeSheet
95
- sheet.restore(snapshot)
96
- sheet.restoreContent(snapshot)
100
+ sheet.restore(trimmed)
101
+ sheet.restoreContent(trimmed)
97
102
  sheet.history.clear()
98
103
  void nextTick(applyRuntimeColWidths)
99
104
  }
100
105
 
101
106
  // 先铺模板静态结构(取数期间 / 取数失败时可见),取数成功后替换为 Filled Report
102
- watch(() => props.template, applySnapshot, { immediate: true })
107
+ watch(
108
+ () => props.template,
109
+ (template) => applySnapshot(template, 'template'),
110
+ { immediate: true }
111
+ )
103
112
  watch(filledSnapshot, (filled) => {
104
- if (filled) applySnapshot(filled)
113
+ if (filled) applySnapshot(filled, 'filled')
105
114
  })
106
- watch(() => props.colWidths, applyRuntimeColWidths)
115
+ watch(effectiveColWidths, applyRuntimeColWidths)
107
116
 
108
117
  async function exportXlsx(): Promise<void> {
109
118
  if (loading.value) {
@@ -114,8 +123,7 @@ async function exportXlsx(): Promise<void> {
114
123
  }
115
124
 
116
125
  const sheet = workbook.value.activeSheet
117
- const colWidths = readGridColWidths(sheetRef.value?.getGrid(), exportColIndexes()) ?? []
118
- const buffer = await exportFilledReportXlsx(sheet, colWidths)
126
+ const buffer = await exportFilledReportXlsx(sheet)
119
127
  saveBlob(
120
128
  new Blob([buffer as unknown as BlobPart], {
121
129
  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@@ -62,6 +62,22 @@
62
62
  min-width: 0;
63
63
  }
64
64
 
65
+ @include m.e(pick-hint) {
66
+ position: absolute;
67
+ left: 50%;
68
+ bottom: fn.use-var(gap, small);
69
+ z-index: 17;
70
+ transform: translateX(-50%);
71
+ padding: 4px fn.use-var(gap, small);
72
+ border-radius: fn.use-var(radius, default);
73
+ background: color-mix(in srgb, fn.use-var(bg-color, top) 92%, transparent);
74
+ border: 1px dashed fn.use-var(color, primary);
75
+ font-size: fn.use-var(font-size-main, small);
76
+ color: fn.use-var(text-color, second);
77
+ pointer-events: none;
78
+ white-space: nowrap;
79
+ }
80
+
65
81
  /* 数据中枢 drawer 面板宽度(class 经 $attrs 落到 .u-drawer 面板;须压过 .u-drawer.is-right 的 320px) */
66
82
  @include m.e(hub-drawer) {
67
83
  &.u-drawer {
@@ -123,7 +139,7 @@
123
139
  }
124
140
  }
125
141
 
126
- /* Action Pill:选中绑定格上方的悬浮编辑卡(角色 / 聚合 / 条件样式 / 排序 / 清除绑定) */
142
+ /* Action Pill:选中绑定格上方的悬浮编辑卡(预设 + 互补的展开/聚合 / 条件样式 / 排序 / 清除绑定) */
127
143
  @include m.b(report-float-panel) {
128
144
  position: absolute;
129
145
  z-index: 18;
@@ -223,7 +239,7 @@
223
239
  }
224
240
 
225
241
  @include m.e(parent-select) {
226
- width: 88px;
242
+ width: 180px;
227
243
  flex: none;
228
244
  }
229
245
 
@@ -244,11 +260,12 @@
244
260
  @include m.b(report-rules-dialog) {
245
261
  display: flex;
246
262
  flex-direction: column;
247
- gap: 12px;
263
+ gap: 10px;
248
264
 
249
265
  @include m.e(hint) {
250
266
  margin: 0;
251
267
  font-size: 12px;
268
+ line-height: 1.5;
252
269
  color: fn.use-var(text-color, second);
253
270
  }
254
271
 
@@ -278,17 +295,18 @@
278
295
  display: flex;
279
296
  align-items: flex-start;
280
297
  gap: 8px;
281
- padding: 10px;
298
+ padding: 10px 10px 10px 6px;
282
299
  border: fn.use-var(border-muted);
283
300
  border-radius: 8px;
284
- background: fn.use-var(bg-color, top);
301
+ background: fn.use-var(bg-color, middle);
285
302
 
286
303
  @include m.e(handle) {
287
304
  flex: none;
288
- width: 20px;
289
- margin-top: 4px;
290
- color: fn.use-var(text-color, second);
291
- font-size: 16px;
305
+ width: 16px;
306
+ margin-top: 6px;
307
+ color: fn.use-var(text-color, placeholder);
308
+ font-size: 12px;
309
+ letter-spacing: -1px;
292
310
  line-height: 1;
293
311
  cursor: grab;
294
312
  user-select: none;
@@ -307,32 +325,33 @@
307
325
  gap: 8px;
308
326
  }
309
327
 
310
- @include m.e(meta) {
328
+ @include m.e(when) {
311
329
  display: flex;
312
330
  align-items: center;
313
- gap: 8px;
331
+ gap: 6px;
314
332
  flex-wrap: wrap;
315
333
  }
316
334
 
317
- @include m.e(field-select) {
318
- width: 120px;
319
- flex: none;
335
+ @include m.e(then) {
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 6px;
339
+ flex-wrap: wrap;
320
340
  }
321
341
 
322
- @include m.e(scope-select) {
323
- width: 72px;
342
+ @include m.e(lead) {
324
343
  flex: none;
344
+ font-size: 12px;
345
+ color: fn.use-var(text-color, second);
325
346
  }
326
347
 
327
- @include m.e(condition) {
328
- display: grid;
329
- grid-template-columns: 72px minmax(0, 1fr) auto;
330
- align-items: center;
331
- gap: 8px;
348
+ @include m.e(field-select) {
349
+ width: 128px;
350
+ flex: none;
332
351
  }
333
352
 
334
353
  @include m.e(operator) {
335
- width: 72px;
354
+ width: 96px;
336
355
  flex: none;
337
356
  }
338
357
 
@@ -345,12 +364,12 @@
345
364
  }
346
365
 
347
366
  @include m.e(scalar-input) {
348
- width: 112px;
367
+ width: 120px;
349
368
  flex: none;
350
369
  }
351
370
 
352
371
  @include m.e(between-input) {
353
- width: 96px;
372
+ width: 88px;
354
373
  flex: none;
355
374
  }
356
375
 
@@ -359,18 +378,6 @@
359
378
  color: fn.use-var(text-color, second);
360
379
  }
361
380
 
362
- @include m.e(style) {
363
- display: flex;
364
- align-items: center;
365
- gap: 8px;
366
- flex-wrap: wrap;
367
- }
368
-
369
- @include m.e(style-label) {
370
- font-size: 12px;
371
- color: fn.use-var(text-color, second);
372
- }
373
-
374
381
  @include m.e(toggle) {
375
382
  min-width: 28px;
376
383
  padding-inline: 6px;
@@ -378,14 +385,11 @@
378
385
 
379
386
  @include m.e(preview) {
380
387
  flex: none;
381
- margin-top: 2px;
382
388
  }
383
389
 
384
- @include m.e(actions) {
390
+ @include m.e(remove) {
385
391
  flex: none;
386
- display: flex;
387
- flex-direction: column;
388
- gap: 2px;
392
+ margin-top: 2px;
389
393
  }
390
394
  }
391
395
 
@@ -8,13 +8,16 @@ import {
8
8
  applyReportPreset,
9
9
  createReportBinding,
10
10
  formatCellAddress,
11
- inferColParentCandidate,
12
11
  inferDropPreset,
13
- inferRowParentCandidate,
12
+ inferParentsForPreset,
13
+ isColGroupBinding,
14
14
  isHorizontalExpandBinding,
15
+ isRowGroupBinding,
15
16
  isVerticalExpandBinding,
16
17
  listColParentCandidates,
17
- listRowParentCandidates
18
+ listRowParentCandidates,
19
+ prefersGroupParent,
20
+ presetStillApplies
18
21
  } from '../../report/binding'
19
22
  import type { DataConnection, QueryResult, Result } from '../../report/connector'
20
23
  import { buildParamDefs } from '../../report/params'
@@ -227,10 +230,9 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
227
230
  let binding = createReportBinding(dataset, fieldName)
228
231
  binding = applyReportPreset(binding, preset)
229
232
 
230
- const rowParent = inferRowParentCandidate(target, getBindingAt)
231
- if (rowParent) binding.rowParent = rowParent
232
- const colParent = inferColParentCandidate(target, getBindingAt)
233
- if (colParent) binding.colParent = colParent
233
+ const parents = inferParentsForPreset(target, preset, getBindingAt)
234
+ if (parents.rowParent) binding.rowParent = parents.rowParent
235
+ if (parents.colParent) binding.colParent = parents.colParent
234
236
 
235
237
  workbook.value.activeSheet.setCellMeta(target, REPORT_META_NAMESPACE, binding)
236
238
  }
@@ -276,14 +278,18 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
276
278
  metaTick.value
277
279
  const binding = activeBinding.value
278
280
  if (!binding) return []
279
- return listRowParentCandidates(binding, bindingEntries.value)
281
+ return listRowParentCandidates(binding, bindingEntries.value, {
282
+ preferGroup: prefersGroupParent(binding)
283
+ })
280
284
  })
281
285
 
282
286
  const colParentCandidates = computed((): CellAddress[] => {
283
287
  metaTick.value
284
288
  const binding = activeBinding.value
285
289
  if (!binding) return []
286
- return listColParentCandidates(binding, bindingEntries.value)
290
+ return listColParentCandidates(binding, bindingEntries.value, {
291
+ preferGroup: prefersGroupParent(binding)
292
+ })
287
293
  })
288
294
 
289
295
  const bindingEntries = computed((): TopologyBindingEntry[] => {
@@ -296,6 +302,12 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
296
302
  return entries
297
303
  })
298
304
 
305
+ function parentNeedsCorrection(addr: CellAddress | undefined): boolean {
306
+ if (!addr) return true
307
+ const parent = getBindingAt(addr)
308
+ return !parent || parent.aggregate === 'list'
309
+ }
310
+
299
311
  function patchActiveBinding(patch: Partial<ReportBinding>): void {
300
312
  const cell = activeCell.value
301
313
  const binding = activeBinding.value
@@ -304,7 +316,25 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
304
316
  const next: ReportBinding = { ...binding, ...patch }
305
317
  if ('rowParent' in patch && patch.rowParent === undefined) delete next.rowParent
306
318
  if ('colParent' in patch && patch.colParent === undefined) delete next.colParent
307
- if (('aggregate' in patch || 'expand' in patch) && !('preset' in patch)) delete next.preset
319
+ if (
320
+ ('aggregate' in patch || 'expand' in patch) &&
321
+ !('preset' in patch) &&
322
+ !presetStillApplies(next)
323
+ ) {
324
+ delete next.preset
325
+ }
326
+
327
+ if (patch.preset === 'subtotal') {
328
+ const inferred = inferParentsForPreset(cell, 'subtotal', getBindingAt)
329
+ if (parentNeedsCorrection(next.rowParent)) {
330
+ if (inferred.rowParent) next.rowParent = inferred.rowParent
331
+ else delete next.rowParent
332
+ }
333
+ if (parentNeedsCorrection(next.colParent)) {
334
+ if (inferred.colParent) next.colParent = inferred.colParent
335
+ else delete next.colParent
336
+ }
337
+ }
308
338
 
309
339
  workbook.value.activeSheet.setCellMeta(cell, REPORT_META_NAMESPACE, next)
310
340
  }
@@ -319,17 +349,36 @@ export function useReportDesigner(options: UseReportDesignerOptions): UseReportD
319
349
  parentPick.value = null
320
350
  }
321
351
 
352
+ function isValidParentTarget(
353
+ mode: ParentPickMode,
354
+ source: ReportBinding,
355
+ target: ReportBinding | undefined
356
+ ): boolean {
357
+ if (!target) return false
358
+ if (target.dataset !== source.dataset) return false
359
+ const groupOnly = prefersGroupParent(source)
360
+ if (mode === 'row') {
361
+ return groupOnly ? isRowGroupBinding(target) : isVerticalExpandBinding(target)
362
+ }
363
+ return groupOnly ? isColGroupBinding(target) : isHorizontalExpandBinding(target)
364
+ }
365
+
322
366
  function pickParentAt(target: CellAddress): void {
323
367
  const pick = parentPick.value
324
368
  if (!pick) return
325
369
  if (target.row === pick.source.row && target.col === pick.source.col) return
326
370
 
327
371
  const sourceBinding = getBindingAt(pick.source)
372
+ if (!sourceBinding) {
373
+ parentPick.value = null
374
+ return
375
+ }
376
+
328
377
  const targetBinding = getBindingAt(target)
329
- if (!sourceBinding || !targetBinding) return
330
- if (targetBinding.dataset !== sourceBinding.dataset) return
331
- if (pick.mode === 'row' && !isVerticalExpandBinding(targetBinding)) return
332
- if (pick.mode === 'col' && !isHorizontalExpandBinding(targetBinding)) return
378
+ if (!isValidParentTarget(pick.mode, sourceBinding, targetBinding)) {
379
+ workbook.value.activeSheet.selectCell(pick.source)
380
+ return
381
+ }
333
382
 
334
383
  const patch = pick.mode === 'row' ? { rowParent: { ...target } } : { colParent: { ...target } }
335
384
  const next: ReportBinding = { ...sourceBinding, ...patch }
@@ -2,7 +2,8 @@ import type { SheetSnapshot } from '@veltra/sheet-core'
2
2
  import { computed, ref, shallowRef, watch, type ComputedRef, type Ref, type ShallowRef } from 'vue'
3
3
 
4
4
  import type { ConnectorError } from '../../report/connector'
5
- import { renderReport } from '../../report/render'
5
+ import type { ReportColWidthEntry } from '../../report/export-xlsx'
6
+ import { renderReport, resolveFilledColWidths } from '../../report/render'
6
7
  import {
7
8
  fetchTemplateRecords,
8
9
  resolveParamDefaults,
@@ -22,6 +23,8 @@ export interface UseReportViewerReturn {
22
23
  error: ShallowRef<ConnectorError | null>
23
24
  /** 最近一次成功展开渲染的 Filled Report 快照(组件壳据此替换网格内容) */
24
25
  filledSnapshot: ShallowRef<SheetSnapshot | null>
26
+ /** 展开后物理列宽(由模板 colWidths 映射;无模板列宽时为 undefined) */
27
+ filledColWidths: ShallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>
25
28
  /** 重新取数并展开渲染 */
26
29
  refresh: () => Promise<void>
27
30
  /** Filter Bar 改值:写入新参数值并重新取数 */
@@ -36,6 +39,7 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
36
39
  const loading = ref(false)
37
40
  const error = shallowRef<ConnectorError | null>(null)
38
41
  const filledSnapshot = shallowRef<SheetSnapshot | null>(null)
42
+ const filledColWidths = shallowRef<ReadonlyArray<ReportColWidthEntry> | undefined>(undefined)
39
43
 
40
44
  const params = computed(() => resolveTemplateParams(props.template))
41
45
  // 初始值由下方 immediate watch 播种(参数默认值)
@@ -54,9 +58,13 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
54
58
  loading.value = false
55
59
  if (!result.ok) {
56
60
  error.value = result.error
61
+ filledColWidths.value = undefined
57
62
  return
58
63
  }
59
64
  filledSnapshot.value = renderReport(template, result.data)
65
+ filledColWidths.value = template.colWidths?.length
66
+ ? resolveFilledColWidths(template, result.data, template.colWidths)
67
+ : undefined
60
68
  }
61
69
 
62
70
  function setValues(next: ParamValues): void {
@@ -69,11 +77,12 @@ export function useReportViewer(props: ReportViewerProps): UseReportViewerReturn
69
77
  () => props.template,
70
78
  () => {
71
79
  filledSnapshot.value = null
80
+ filledColWidths.value = undefined
72
81
  values.value = resolveParamDefaults(params.value)
73
82
  void refresh()
74
83
  },
75
84
  { immediate: true }
76
85
  )
77
86
 
78
- return { params, values, loading, error, filledSnapshot, refresh, setValues }
87
+ return { params, values, loading, error, filledSnapshot, filledColWidths, refresh, setValues }
79
88
  }