@veltra/sheet 2.2.0 → 2.3.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 (84) 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/preview-grid-size.js +32 -0
  8. package/dist/components/report/report-designer.js +75 -25
  9. package/dist/components/report/report-viewer.js +38 -25
  10. package/dist/components/report/style2.css +44 -39
  11. package/dist/components/report/use-report-designer.js +34 -12
  12. package/dist/components/report/use-report-viewer.js +6 -1
  13. package/dist/components/sheet/popups/border-popup.js +30 -2
  14. package/dist/components/sheet/popups/fill-color-popup.js +2 -2
  15. package/dist/components/sheet/popups/font-color-popup.js +2 -2
  16. package/dist/components/sheet/popups/font-size-popup.js +2 -2
  17. package/dist/components/sheet/sheet-context-menu.js +6 -3
  18. package/dist/components/sheet/sheet.js +8 -0
  19. package/dist/components/sheet/use-sheet-grid.js +14 -4
  20. package/dist/components/sheet/use-sheet-state.js +3 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +2 -2
  23. package/dist/report/binding.d.ts +49 -11
  24. package/dist/report/binding.js +186 -29
  25. package/dist/report/connector.js +1 -1
  26. package/dist/report/export-xlsx.d.ts +3 -5
  27. package/dist/report/export-xlsx.js +7 -10
  28. package/dist/report/index.d.ts +2 -2
  29. package/dist/report/render/aggregate.js +20 -8
  30. package/dist/report/template.d.ts +8 -4
  31. package/dist/report/template.js +5 -2
  32. package/dist/tools/apply-style.js +29 -0
  33. package/dist/tools/builtin.js +17 -17
  34. package/dist/tools/context.d.ts +38 -5
  35. package/dist/tools/context.js +38 -1
  36. package/dist/types/report-designer.d.ts +2 -0
  37. package/dist/types/report-viewer.d.ts +2 -2
  38. package/dist/types/sheet.d.ts +4 -0
  39. package/package.json +6 -6
  40. package/src/components/report/__test__/conditional-rules-helpers.test.ts +1 -0
  41. package/src/components/report/__test__/preview-grid-size.test.ts +63 -0
  42. package/src/components/report/__test__/report-designer.test.ts +76 -0
  43. package/src/components/report/__test__/report-viewer.test.ts +24 -1
  44. package/src/components/report/__test__/use-report-designer.test.ts +142 -10
  45. package/src/components/report/binding-badge.ts +39 -20
  46. package/src/components/report/designer/col-widths.ts +34 -11
  47. package/src/components/report/designer/conditional-rules/helpers.ts +6 -3
  48. package/src/components/report/designer/float-panel.vue +71 -27
  49. package/src/components/report/designer/rule-row.vue +67 -67
  50. package/src/components/report/designer/rules-dialog.vue +3 -19
  51. package/src/components/report/preview-grid-size.ts +33 -0
  52. package/src/components/report/report-designer.vue +79 -15
  53. package/src/components/report/report-viewer.vue +37 -29
  54. package/src/components/report/style-designer.scss +45 -41
  55. package/src/components/report/use-report-designer.ts +63 -14
  56. package/src/components/report/use-report-viewer.ts +11 -2
  57. package/src/components/sheet/__test__/sheet-component.test.ts +37 -0
  58. package/src/components/sheet/__test__/sheet-context-menu.test.ts +3 -2
  59. package/src/components/sheet/popups/border-popup.vue +31 -3
  60. package/src/components/sheet/popups/fill-color-popup.vue +2 -2
  61. package/src/components/sheet/popups/font-color-popup.vue +2 -2
  62. package/src/components/sheet/popups/font-size-popup.vue +2 -2
  63. package/src/components/sheet/sheet-context-menu.ts +8 -2
  64. package/src/components/sheet/sheet.vue +2 -0
  65. package/src/components/sheet/use-sheet-grid.ts +17 -3
  66. package/src/components/sheet/use-sheet-state.ts +8 -3
  67. package/src/report/__test__/binding.test.ts +103 -1
  68. package/src/report/__test__/connector.test.ts +21 -0
  69. package/src/report/__test__/export-xlsx.test.ts +4 -4
  70. package/src/report/__test__/render.test.ts +102 -0
  71. package/src/report/__test__/template.test.ts +11 -0
  72. package/src/report/binding.ts +278 -47
  73. package/src/report/connector.ts +3 -2
  74. package/src/report/export-xlsx.ts +9 -14
  75. package/src/report/render/aggregate.ts +29 -8
  76. package/src/report/template.ts +8 -4
  77. package/src/tools/__test__/apply-style.test.ts +129 -0
  78. package/src/tools/apply-style.ts +41 -0
  79. package/src/tools/builtin.ts +18 -18
  80. package/src/tools/context.ts +67 -4
  81. package/src/tools/index.ts +1 -0
  82. package/src/types/report-designer.ts +2 -0
  83. package/src/types/report-viewer.ts +2 -2
  84. package/src/types/sheet.ts +4 -0
@@ -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
@@ -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
+ }
@@ -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'