n20-common-lib 3.1.4 → 3.1.5

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.
@@ -2,65 +2,17 @@
2
2
  <Dialog class="n20-pivot-dialog" width="95%" max-dialog :visible.sync="visibleC" title="数据透视分析中心">
3
3
  <div class="n20-pivot-container">
4
4
  <!-- 左侧报表清单 -->
5
- <div class="pivot-sidebar-left" :class="{ collapsed: isLeftSidebarCollapsed }">
6
- <div class="sidebar-header">
7
- <span class="sidebar-title">我的报表清单</span>
8
- <div class="add-report-btn" @click="handleAddReport" title="新建">
9
- <i class="v3-icon-plus"></i>
10
- </div>
11
- </div>
12
-
13
- <div class="report-list">
14
- <div
15
- v-for="(report, index) in reportList"
16
- :key="report.id"
17
- class="report-item"
18
- :class="{ active: currentReportIndex === index, editing: editingReportId === report.id }"
19
- @click="handleSelectReport(index)"
20
- @mouseenter="hoveredReportId = report.id"
21
- @mouseleave="hoveredReportId = null"
22
- >
23
- <i :class="getChartTypeIcon(report.chartType)" class="report-icon"></i>
24
- <span v-if="editingReportId !== report.id" class="report-name">{{ report.name }}</span>
25
- <input
26
- v-else
27
- v-model="editingReportName"
28
- class="report-name-input"
29
- type="text"
30
- @click.stop
31
- @blur="handleSaveRename"
32
- @keyup.enter="handleSaveRename"
33
- @keyup.esc="handleCancelRename"
34
- />
35
- <div
36
- v-if="editingReportId !== report.id"
37
- class="report-more"
38
- :class="{ visible: hoveredReportId === report.id || activeMenuReportId === report.id }"
39
- @click.stop="handleToggleMenu(report.id)"
40
- >
41
- <i class="v3-icon-more"></i>
42
- <div v-if="activeMenuReportId === report.id" class="report-menu">
43
- <div class="report-menu-item" @click.stop="handleStartRename(report)">
44
- <i class="v3-icon-edit"></i>
45
- <span>重命名</span>
46
- </div>
47
- <div class="report-menu-item delete" @click.stop="handleDeleteReport(report)">
48
- <i class="v3-icon-delete"></i>
49
- <span>删除</span>
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
- </div>
55
-
56
- <div
57
- class="scroll-bar-indicator"
58
- :class="{ collapsed: isLeftSidebarCollapsed }"
59
- @click="handleScrollBarIndicator"
60
- >
61
- <i :class="isLeftSidebarCollapsed ? 'v3-icon-right' : 'v3-icon-left'"></i>
62
- </div>
63
- </div>
5
+ <ReportSidebar
6
+ ref="reportSidebar"
7
+ :report-list="reportList"
8
+ :current-index="currentReportIndex"
9
+ :is-collapsed="isLeftSidebarCollapsed"
10
+ @select="handleSelectReport"
11
+ @add="handleAddReport"
12
+ @toggle-collapse="handleToggleLeftSidebar"
13
+ @rename="handleRenameReport"
14
+ @delete="handleDeleteReport"
15
+ />
64
16
 
65
17
  <!-- 主内容区域 -->
66
18
  <div
@@ -68,336 +20,135 @@
68
20
  :class="{ 'left-collapsed': isLeftSidebarCollapsed, 'right-collapsed': isRightSidebarCollapsed }"
69
21
  >
70
22
  <!-- 内容头部工具栏 -->
71
- <div class="main-toolbar">
72
- <div class="toolbar-left">
73
- <div class="toolbar-left-top">
74
- <h2 class="report-title">{{ currentReport.name }}</h2>
75
- <div class="add-to-home-btn" @click="handleAddToHome">
76
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
77
- <path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
78
- </svg>
79
- <span>添加到首页</span>
80
- </div>
81
- </div>
82
- <div class="report-stats">
83
- <span class="stat-item">计算耗时:{{ calculateTime }} ms</span>
84
- <span class="stat-divider"></span>
85
- <span class="stat-item">数据条数:{{ dataCount }}</span>
86
- </div>
87
- </div>
88
-
89
- <div class="toolbar-right">
90
- <ViewToggle
91
- v-model="viewMode"
92
- :options="viewModeOptions"
93
- :disabled="isCrossMode"
94
- @change="handleViewModeChange"
95
- />
96
-
97
- <div class="action-buttons">
98
- <div class="action-btn" @click="handleRefresh" title="刷新">
99
- <i class="n20-icon-shuaxin"></i>
100
- </div>
101
- <div class="action-btn" @click="handleMore" title="更多">
102
- <i class="v3-icon-more"></i>
103
- </div>
104
- </div>
105
- </div>
106
- </div>
23
+ <MainToolbar
24
+ ref="mainToolbar"
25
+ :report-name="currentReport.name"
26
+ :calculate-time="calculateTime"
27
+ :data-count="dataCount"
28
+ :view-mode="viewMode"
29
+ :view-mode-options="viewModeOptions"
30
+ :is-cross-mode="isCrossMode"
31
+ :is-right-sidebar-collapsed="isRightSidebarCollapsed"
32
+ @view-mode-change="handleViewModeChange"
33
+ @refresh="handleRefresh"
34
+ @config="handleConfig"
35
+ @export="handleExport"
36
+ @add-to-home="handleAddToHome"
37
+ @delete-report="handleDeleteCurrentReport"
38
+ />
107
39
 
108
40
  <!-- 内容区域 -->
109
41
  <div class="main-content">
110
42
  <!-- 图表视图 -->
111
- <div v-if="viewMode === 'chart'" class="chart-view">
112
- <div v-if="hasData" ref="chartContainer" class="chart-container"></div>
113
- <div v-else class="empty-state">
114
- <div class="empty-icon">
115
- <svg width="100" height="100" viewBox="0 0 100 100" fill="none">
116
- <circle cx="50" cy="50" r="45" stroke="#E5E6EB" stroke-width="2" />
117
- <path
118
- d="M30 60V45L40 55M45 50H55M55 50H65M55 50V40"
119
- stroke="#E5E6EB"
120
- stroke-width="2"
121
- stroke-linecap="round"
122
- stroke-linejoin="round"
123
- />
124
- <circle cx="55" cy="30" r="8" stroke="#E5E6EB" stroke-width="2" />
125
- <path d="M55 38V42M55 48V50" stroke="#E5E6EB" stroke-width="2" stroke-linecap="round" />
126
- </svg>
127
- </div>
128
- <p class="empty-text">暂无数据</p>
129
- </div>
130
- </div>
43
+ <ChartView
44
+ v-if="viewMode === 'chart'"
45
+ ref="chartView"
46
+ :has-data="hasData"
47
+ :data-count="dataCount"
48
+ :dimension-label="getFirstDimensionLabel"
49
+ :unit-label="getChartUnitLabel"
50
+ :chart-option="chartOption"
51
+ />
131
52
 
132
53
  <!-- 表格视图 -->
133
- <div v-else class="table-view">
134
- <div v-if="hasData" class="table-container">
135
- <table class="pivot-table">
136
- <thead>
137
- <tr>
138
- <th v-for="col in tableColumns" :key="col.key">{{ col.title }}</th>
139
- </tr>
140
- </thead>
141
- <tbody>
142
- <tr v-for="(row, index) in tableData" :key="index">
143
- <td v-for="col in tableColumns" :key="col.key">
144
- {{ row[col.key] }}
145
- </td>
146
- </tr>
147
- </tbody>
148
- </table>
149
- </div>
150
- <div v-else class="empty-state">
151
- <div class="empty-icon">
152
- <svg width="100" height="100" viewBox="0 0 100 100" fill="none">
153
- <rect x="15" y="20" width="70" height="60" rx="4" stroke="#E5E6EB" stroke-width="2" />
154
- <path d="M15 35H85" stroke="#E5E6EB" stroke-width="2" />
155
- <path d="M40 35V80" stroke="#E5E6EB" stroke-width="2" />
156
- </svg>
157
- </div>
158
- <p class="empty-text">暂无数据</p>
159
- </div>
160
- </div>
54
+ <TableView
55
+ v-else
56
+ ref="tableView"
57
+ :has-data="hasData"
58
+ :data-count="dataCount"
59
+ :dimension-label="getFirstDimensionLabel"
60
+ :is-cross-mode="isCrossMode"
61
+ :table-columns="tableColumns"
62
+ :cross-table-columns="crossTableColumns"
63
+ :table-data="paginatedTableData"
64
+ :total-count="dataCount"
65
+ :current-page="currentPage"
66
+ :page-size="pageSize"
67
+ :page-size-options="pageSizeOptions"
68
+ :total-pages="totalPages"
69
+ @prev-page="handlePrevPage"
70
+ @next-page="handleNextPage"
71
+ @jump-page="handleJumpToPage"
72
+ @page-size-change="handlePageSizeChange"
73
+ />
161
74
  </div>
162
75
  </div>
163
76
 
164
77
  <!-- 右侧维度配置 -->
165
- <div class="pivot-sidebar-right" :class="{ collapsed: isRightSidebarCollapsed }">
166
- <div class="sidebar-header">
167
- <template v-if="!isRightSidebarCollapsed">
168
- <i class="v3-icon-move-right sidebar-header-collapse" @click="handleToggleRightSidebar"></i>
169
- <span class="sidebar-title">维度配置</span>
170
- </template>
171
- </div>
172
-
173
- <div class="config-form">
174
- <!-- 报表名称 -->
175
- <div class="form-item">
176
- <label class="form-label">报表标题</label>
177
-
178
- <div class="input-wrapper">
179
- <input class="form-input" type="text" v-model="currentReport.name" placeholder="请输入报表名称" />
180
- </div>
181
- </div>
182
- <!-- 统计类型 -->
183
- <div class="form-item">
184
- <label class="form-label">统计类型</label>
185
-
186
- <div class="radio-group">
187
- <ViewToggle
188
- v-model="currentReport.statType"
189
- :itemWidth="120"
190
- :options="totalModeOptions"
191
- @change="handleStatTypeChange"
192
- />
193
- </div>
194
- </div>
195
-
196
- <!-- 展示形式 -->
197
- <div v-if="!isCrossMode" class="form-item">
198
- <label class="form-label">展示形式</label>
199
- <div class="select-wrapper" @click="showChartTypeDropdown = !showChartTypeDropdown">
200
- <div class="select-value">
201
- <i :class="getChartTypeIcon(currentReport.chartType)" class="report-icon"></i>
202
- <span>{{ getChartTypeLabel(currentReport.chartType) }}</span>
203
- </div>
204
- <svg class="select-arrow" width="10" height="10" viewBox="0 0 10 10" fill="none">
205
- <path
206
- d="M2 3L5 7L8 3"
207
- stroke="currentColor"
208
- stroke-width="1.5"
209
- stroke-linecap="round"
210
- stroke-linejoin="round"
211
- />
212
- </svg>
213
- <div v-if="showChartTypeDropdown" class="dropdown-menu">
214
- <div
215
- v-for="type in chartTypes"
216
- :key="type.value"
217
- class="dropdown-item"
218
- :class="{ active: currentReport.chartType === type.value }"
219
- @click.stop="handleChartTypeChange(type.value)"
220
- >
221
- {{ type.label }}
222
- </div>
223
- </div>
224
- </div>
225
- </div>
226
-
227
- <div class="form-divider"></div>
228
-
229
- <!-- 分组维度 -->
230
- <div class="form-item">
231
- <label class="form-label">
232
- 分组维度
233
- <svg class="label-tip" width="12" height="12" viewBox="0 0 12 12" fill="none">
234
- <circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1" />
235
- <path d="M6 5V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
236
- <circle cx="6" cy="3.5" r="0.5" fill="currentColor" />
237
- </svg>
238
- </label>
239
- <div class="select-wrapper" @click="showDimensionDropdown = !showDimensionDropdown">
240
- <div class="select-value">
241
- <span v-if="currentReport.dimensions.length === 0" class="placeholder">请选择</span>
242
- <span v-else>{{ currentReport.dimensions.join(', ') }}</span>
243
- </div>
244
- <svg class="select-arrow" width="10" height="10" viewBox="0 0 10 10" fill="none">
245
- <path
246
- d="M2 3L5 7L8 3"
247
- stroke="currentColor"
248
- stroke-width="1.5"
249
- stroke-linecap="round"
250
- stroke-linejoin="round"
251
- />
252
- </svg>
253
- <div v-if="showDimensionDropdown" class="dropdown-menu dimension-dropdown">
254
- <div
255
- v-for="dim in availableDimensions"
256
- :key="dim.key"
257
- class="dropdown-item checkbox-item"
258
- @click.stop="handleDimensionToggle(dim.key)"
259
- >
260
- <div class="checkbox" :class="{ checked: currentReport.dimensions.includes(dim.key) }">
261
- <svg
262
- v-if="currentReport.dimensions.includes(dim.key)"
263
- width="10"
264
- height="10"
265
- viewBox="0 0 10 10"
266
- fill="none"
267
- >
268
- <path
269
- d="M2 5L4 7L8 3"
270
- stroke="currentColor"
271
- stroke-width="1.5"
272
- stroke-linecap="round"
273
- stroke-linejoin="round"
274
- />
275
- </svg>
276
- </div>
277
- <span>{{ dim.label }}</span>
278
- </div>
279
- </div>
280
- </div>
281
- </div>
282
-
283
- <div class="form-divider"></div>
284
-
285
- <!-- 计算指标 -->
286
- <div class="form-item">
287
- <div class="form-label-row">
288
- <label class="form-label">
289
- 计算指标
290
- <svg class="label-tip" width="12" height="12" viewBox="0 0 12 12" fill="none">
291
- <circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1" />
292
- <path d="M6 5V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
293
- <circle cx="6" cy="3.5" r="0.5" fill="currentColor" />
294
- </svg>
295
- </label>
296
- <div class="aggregate-dropdown" @click="showAggregateDropdown = !showAggregateDropdown">
297
- <span>{{ getAggregateLabel(currentReport.aggregateType) }}</span>
298
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
299
- <path
300
- d="M2 3L5 7L8 3"
301
- stroke="currentColor"
302
- stroke-width="1.5"
303
- stroke-linecap="round"
304
- stroke-linejoin="round"
305
- />
306
- </svg>
307
- <div v-if="showAggregateDropdown" class="dropdown-menu aggregate-dropdown-menu">
308
- <div
309
- v-for="agg in aggregateTypes"
310
- :key="agg.value"
311
- class="dropdown-item"
312
- :class="{ active: currentReport.aggregateType === agg.value }"
313
- @click.stop="handleAggregateChange(agg.value)"
314
- >
315
- {{ agg.label }}
316
- </div>
317
- </div>
318
- </div>
319
- </div>
320
-
321
- <div class="select-wrapper" @click="showMetricDropdown = !showMetricDropdown">
322
- <div class="select-value">
323
- <span v-if="currentReport.metrics.length === 0" class="placeholder">请选择</span>
324
- <span v-else>{{ currentReport.metrics.join(', ') }}</span>
325
- </div>
326
- <div class="select-tag" v-if="currentReport.metrics.length > 0">
327
- {{ getAggregateLabel(currentReport.aggregateType) }}
328
- </div>
329
- <svg class="select-arrow" width="10" height="10" viewBox="0 0 10 10" fill="none">
330
- <path
331
- d="M2 3L5 7L8 3"
332
- stroke="currentColor"
333
- stroke-width="1.5"
334
- stroke-linecap="round"
335
- stroke-linejoin="round"
336
- />
337
- </svg>
338
- <div v-if="showMetricDropdown" class="dropdown-menu">
339
- <div
340
- v-for="metric in availableMetrics"
341
- :key="metric.key"
342
- class="dropdown-item checkbox-item"
343
- @click.stop="handleMetricToggle(metric.key)"
344
- >
345
- <div class="checkbox" :class="{ checked: currentReport.metrics.includes(metric.key) }">
346
- <svg
347
- v-if="currentReport.metrics.includes(metric.key)"
348
- width="10"
349
- height="10"
350
- viewBox="0 0 10 10"
351
- fill="none"
352
- >
353
- <path
354
- d="M2 5L4 7L8 3"
355
- stroke="currentColor"
356
- stroke-width="1.5"
357
- stroke-linecap="round"
358
- stroke-linejoin="round"
359
- />
360
- </svg>
361
- </div>
362
- <span>{{ metric.label }}</span>
363
- </div>
364
- </div>
365
- </div>
366
-
367
- <div class="add-field-btn" @click="handleAddMetric">
368
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
369
- <path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
370
- </svg>
371
- <span>添加字段</span>
372
- </div>
373
- </div>
374
- </div>
375
- </div>
78
+ <ConfigSidebar
79
+ ref="configSidebar"
80
+ :is-collapsed="isRightSidebarCollapsed"
81
+ :report-name="currentReport.name"
82
+ :stat-type="currentReport.statType"
83
+ :stat-type-options="totalModeOptions"
84
+ :chart-type="currentReport.chartType"
85
+ :chart-types="chartTypes"
86
+ :is-cross-mode="isCrossMode"
87
+ :view-mode="viewMode"
88
+ :selected-dimension="currentReport.selectedDimension"
89
+ :row-dimension="currentReport.rowDimension"
90
+ :column-dimension="currentReport.columnDimension"
91
+ :metrics="validMetrics"
92
+ :available-dimensions="availableDimensions"
93
+ :available-metrics="availableMetrics"
94
+ :available-metrics-for-add="availableMetricsForAdd"
95
+ :global-aggregate-type="getGlobalAggregateType()"
96
+ :aggregate-types="aggregateTypes"
97
+ @toggle-collapse="handleToggleRightSidebar"
98
+ @name-change="handleNameChange"
99
+ @stat-type-change="handleStatTypeChange"
100
+ @chart-type-change="handleChartTypeChange"
101
+ @dimension-select="handleDimensionSelect"
102
+ @row-dimension-change="handleRowDimensionChange"
103
+ @column-dimension-change="handleColumnDimensionChange"
104
+ @metric-aggregate-change="handleMetricAggregateChange"
105
+ @global-aggregate-change="handleGlobalAggregateChange"
106
+ @add-metric="handleAddSpecificMetric"
107
+ @remove-metric="handleRemoveMetric"
108
+ />
376
109
  </div>
377
110
  </Dialog>
378
111
  </template>
379
112
 
380
113
  <script>
381
- import Dialog from '../Dialog/index.vue'
382
- import ViewToggle from '../ViewToggle/index.vue'
383
114
  import XEUtils from 'xe-utils'
384
- import importG from '../../utils/importGlobal.js'
115
+ import Dialog from '../Dialog/index.vue'
116
+ import ReportSidebar from './ReportSidebar.vue'
117
+ import MainToolbar from './MainToolbar.vue'
118
+ import ChartView from './ChartView.vue'
119
+ import TableView from './TableView.vue'
120
+ import ConfigSidebar from './ConfigSidebar.vue'
385
121
 
386
122
  const STORAGE_KEY = 'pivot_report_configs'
123
+ const DEBOUNCE_DELAY = 300
124
+
125
+ // 防抖函数
126
+ function debounce(func, wait) {
127
+ let timeout
128
+ return function (...args) {
129
+ clearTimeout(timeout)
130
+ timeout = setTimeout(() => func.apply(this, args), wait)
131
+ }
132
+ }
387
133
 
388
134
  export default {
389
135
  name: 'Pivot',
390
136
  components: {
391
137
  Dialog,
392
- ViewToggle
138
+ ReportSidebar,
139
+ MainToolbar,
140
+ ChartView,
141
+ TableView,
142
+ ConfigSidebar
393
143
  },
394
144
  props: {
145
+ // 对话框可见性
395
146
  visible: {
396
147
  type: Boolean,
397
148
  default: false
398
149
  },
399
150
  // 数据源
400
- dataSource: {
151
+ data: {
401
152
  type: Array,
402
153
  default: () => []
403
154
  },
@@ -406,20 +157,35 @@ export default {
406
157
  type: Array,
407
158
  default: () => []
408
159
  },
409
- // 组件宽度
410
- width: {
411
- type: [Number, String],
412
- default: 1360
160
+ // 行维度字段
161
+ rowFields: {
162
+ type: Array,
163
+ default: () => []
164
+ },
165
+ // 列维度字段
166
+ colFields: {
167
+ type: Array,
168
+ default: () => []
169
+ },
170
+ // 计算指标字段
171
+ valueFields: {
172
+ type: Array,
173
+ default: () => []
413
174
  },
414
- // 组件高度
415
- height: {
416
- type: [Number, String],
417
- default: 820
175
+ // 图表类型
176
+ chartType: {
177
+ type: String,
178
+ default: 'bar',
179
+ validator: (val) => ['bar', 'line', 'pie', 'area'].includes(val)
418
180
  }
419
181
  },
420
182
 
421
183
  data() {
422
184
  return {
185
+ // 错误状态
186
+ error: null,
187
+ // 加载状态
188
+ isProcessing: false,
423
189
  // 报表列表
424
190
  reportList: [],
425
191
  // 当前选中的报表索引
@@ -430,22 +196,14 @@ export default {
430
196
  calculateTime: 0,
431
197
  // 数据条数
432
198
  dataCount: 0,
433
- // 图表实例
434
- chartInstance: null,
435
- // 下拉框显示状态
436
- showCustomAggregateDropdown: false,
437
199
  // 左侧边栏是否折叠
438
200
  isLeftSidebarCollapsed: false,
439
201
  // 右侧边栏是否折叠
440
202
  isRightSidebarCollapsed: false,
441
- // 报表项悬停ID
442
- hoveredReportId: null,
443
- // 当前显示菜单的报表ID
444
- activeMenuReportId: null,
445
- // 正在编辑的报表ID
446
- editingReportId: null,
447
- // 编辑中的报表名称
448
- editingReportName: '',
203
+ // 分页状态
204
+ currentPage: 1,
205
+ pageSize: 10,
206
+ pageSizeOptions: [10, 20, 50, 100],
449
207
  // 视图模式选项
450
208
  viewModeOptions: [
451
209
  { label: '图表视图', value: 'chart' },
@@ -474,6 +232,11 @@ export default {
474
232
  },
475
233
 
476
234
  computed: {
235
+ // 数据源兼容性处理
236
+ actualData() {
237
+ return this.data || []
238
+ },
239
+
477
240
  visibleC: {
478
241
  get() {
479
242
  return this.visible
@@ -483,6 +246,7 @@ export default {
483
246
  return v
484
247
  }
485
248
  },
249
+
486
250
  // 当前报表配置
487
251
  currentReport() {
488
252
  if (this.reportList.length === 0) {
@@ -501,138 +265,325 @@ export default {
501
265
  return this.processedData.length > 0
502
266
  },
503
267
 
504
- // 可用维度
268
+ // 当前报表有效的指标列表
269
+ validMetrics() {
270
+ return (this.currentReport.metrics || []).filter((m) => m && m.prop)
271
+ },
272
+
273
+ // 可用维度(根据数据源值类型判断,过滤掉 checkbox 和无 prop 的列)
505
274
  availableDimensions() {
506
- return this.columns.filter((col) => col.type !== 'number' && col.type !== 'date')
275
+ return this.columns.filter((col) => {
276
+ // 过滤掉 checkbox 类型和没有 prop 的列
277
+ if (col.type === 'checkbox' || !col.prop) return false
278
+ // 数值类型不作为维度
279
+ return this.getColumnType(col.prop) !== 'number'
280
+ })
507
281
  },
508
282
 
509
- // 可用指标
283
+ // 可用指标(根据数据源值类型判断,过滤掉 checkbox 和无 prop 的列)
510
284
  availableMetrics() {
511
- return this.columns.filter((col) => col.type === 'number')
285
+ return this.columns.filter((col) => {
286
+ // 过滤掉 checkbox 类型和没有 prop 的列
287
+ if (col.type === 'checkbox' || !col.prop) return false
288
+ // 仅数值类型作为指标
289
+ return this.getColumnType(col.prop) === 'number'
290
+ })
291
+ },
292
+
293
+ // 可添加的指标
294
+ availableMetricsForAdd() {
295
+ const selectedProps = (this.currentReport.metrics || []).filter((m) => m && m.prop).map((m) => m.prop)
296
+ return this.availableMetrics.filter((m) => !selectedProps.includes(m.prop))
512
297
  },
513
298
 
514
299
  // 处理后的数据
515
300
  processedData() {
516
301
  const startTime = performance.now()
517
- const data = this.dataSource
518
- const result = []
302
+ this.error = null
303
+ this.isProcessing = true
519
304
 
520
- if (!data || data.length === 0) {
521
- this.calculateTime = 0
522
- this.dataCount = 0
523
- return []
524
- }
305
+ try {
306
+ const data = this.actualData
307
+ const result = []
308
+
309
+ if (!data || data.length === 0) {
310
+ this.calculateTime = 0
311
+ this.dataCount = 0
312
+ this.isProcessing = false
313
+ return []
314
+ }
525
315
 
526
- const { dimensions, metrics, aggregateType, chartType } = this.currentReport
316
+ // 交叉统计模式
317
+ if (this.isCrossMode) {
318
+ const { rowDimension, columnDimension, metrics: rawMetrics } = this.currentReport
319
+ const metrics = (rawMetrics || []).filter((m) => m && m.prop)
527
320
 
528
- if (dimensions.length === 0 || metrics.length === 0) {
529
- this.calculateTime = Math.round(performance.now() - startTime)
530
- this.dataCount = data.length
531
- return []
532
- }
321
+ if (!rowDimension || !columnDimension || metrics.length === 0) {
322
+ this.calculateTime = Math.round(performance.now() - startTime)
323
+ this.dataCount = data.length
324
+ this.isProcessing = false
325
+ return []
326
+ }
533
327
 
534
- // 按维度分组
535
- const groupedData = XEUtils.groupBy(data, dimensions)
536
-
537
- // 计算聚合值
538
- for (const key in groupedData) {
539
- const group = groupedData[key]
540
- const row = {}
541
-
542
- // 设置维度值
543
- if (dimensions.length === 1) {
544
- row[dimensions[0]] = key
545
- } else {
546
- const keyParts = key.split(',')
547
- dimensions.forEach((dim, i) => {
548
- row[dim] = keyParts[i]
549
- })
550
- }
328
+ // 使用枚举显示值进行分组
329
+ const groupedByRow = XEUtils.groupBy(data, (item) => this.getEnumDisplayValue(item, rowDimension))
330
+
331
+ for (const rowKey in groupedByRow) {
332
+ const rowGroup = groupedByRow[rowKey]
333
+ const row = {}
334
+
335
+ row[rowDimension] = rowKey
336
+ row._rowKey = rowKey
337
+
338
+ const groupedByColumn = XEUtils.groupBy(rowGroup, (item) => this.getEnumDisplayValue(item, columnDimension))
339
+
340
+ for (const colKey in groupedByColumn) {
341
+ const colGroup = groupedByColumn[colKey]
342
+
343
+ metrics.forEach((metric) => {
344
+ const metricProp = metric.prop
345
+ const values = colGroup.map((item) => parseFloat(item[metricProp]) || 0)
346
+ const colMetricKey = `${colKey}_${metricProp}`
347
+
348
+ switch (metric.aggregateType) {
349
+ case 'sum':
350
+ row[colMetricKey] = XEUtils.sum(values)
351
+ break
352
+ case 'count':
353
+ row[colMetricKey] = values.length
354
+ break
355
+ case 'avg':
356
+ row[colMetricKey] = XEUtils.mean(values)
357
+ break
358
+ case 'max':
359
+ row[colMetricKey] = XEUtils.max(values)
360
+ break
361
+ case 'min':
362
+ row[colMetricKey] = XEUtils.min(values)
363
+ break
364
+ default:
365
+ row[colMetricKey] = XEUtils.sum(values)
366
+ }
367
+ })
368
+ }
551
369
 
552
- // 计算指标值
553
- metrics.forEach((metric) => {
554
- const values = group.map((item) => parseFloat(item[metric]) || 0)
555
- switch (aggregateType) {
556
- case 'sum':
557
- row[metric] = XEUtils.sum(values)
558
- break
559
- case 'count':
560
- row[metric] = values.length
561
- break
562
- case 'avg':
563
- row[metric] = XEUtils.mean(values)
564
- break
565
- case 'max':
566
- row[metric] = XEUtils.max(values)
567
- break
568
- case 'min':
569
- row[metric] = XEUtils.min(values)
570
- break
571
- default:
572
- row[metric] = XEUtils.sum(values)
370
+ row._groupCount = rowGroup.length
371
+ row._groupKey = rowKey
372
+
373
+ result.push(row)
573
374
  }
375
+
376
+ this.calculateTime = Math.round(performance.now() - startTime)
377
+ this.dataCount = result.length
378
+ this.isProcessing = false
379
+
380
+ return result
381
+ }
382
+
383
+ // 分组统计模式
384
+ const { selectedDimension, metrics: rawMetrics } = this.currentReport
385
+ const metrics = (rawMetrics || []).filter((m) => m && m.prop)
386
+
387
+ if (!selectedDimension || metrics.length === 0) {
388
+ this.calculateTime = Math.round(performance.now() - startTime)
389
+ this.dataCount = data.length
390
+ this.isProcessing = false
391
+ return []
392
+ }
393
+
394
+ // 使用枚举显示值进行分组(单维度)
395
+ const groupedData = XEUtils.groupBy(data, (item) => {
396
+ return this.getEnumDisplayValue(item, selectedDimension)
574
397
  })
575
398
 
576
- result.push(row)
577
- }
399
+ for (const key in groupedData) {
400
+ const group = groupedData[key]
401
+ const row = {}
402
+
403
+ row[selectedDimension] = key
404
+
405
+ metrics.forEach((metric) => {
406
+ const metricProp = metric.prop
407
+ const values = group.map((item) => parseFloat(item[metricProp]) || 0)
408
+ switch (metric.aggregateType) {
409
+ case 'sum':
410
+ row[metricProp] = XEUtils.sum(values)
411
+ break
412
+ case 'count':
413
+ row[metricProp] = values.length
414
+ break
415
+ case 'avg':
416
+ row[metricProp] = XEUtils.mean(values)
417
+ break
418
+ case 'max':
419
+ row[metricProp] = XEUtils.max(values)
420
+ break
421
+ case 'min':
422
+ row[metricProp] = XEUtils.min(values)
423
+ break
424
+ default:
425
+ row[metricProp] = XEUtils.sum(values)
426
+ }
427
+ })
578
428
 
579
- this.calculateTime = Math.round(performance.now() - startTime)
580
- this.dataCount = result.length
429
+ row._groupCount = group.length
430
+ row._groupKey = key
431
+
432
+ result.push(row)
433
+ }
581
434
 
582
- return result
435
+ this.calculateTime = Math.round(performance.now() - startTime)
436
+ this.dataCount = result.length
437
+ this.isProcessing = false
438
+
439
+ return result
440
+ } catch (err) {
441
+ console.error('数据透视计算失败:', err)
442
+ this.error = {
443
+ code: 'AGGREGATION_ERROR',
444
+ message: err.message || '数据计算过程中发生错误',
445
+ stack: err.stack
446
+ }
447
+ this.calculateTime = 0
448
+ this.dataCount = 0
449
+ this.isProcessing = false
450
+ return []
451
+ }
583
452
  },
584
453
 
585
454
  // 表格列定义
586
455
  tableColumns() {
456
+ if (this.isCrossMode) {
457
+ return this.crossTableColumns
458
+ }
587
459
  const cols = []
588
- const { dimensions, metrics } = this.currentReport
460
+ const { selectedDimension, metrics: rawMetrics } = this.currentReport
461
+ const metrics = (rawMetrics || []).filter((m) => m && m.prop)
589
462
 
590
- dimensions.forEach((dim) => {
591
- const colDef = this.columns.find((c) => c.key === dim)
463
+ // 维度列
464
+ if (selectedDimension) {
465
+ const colDef = this.columns.find((c) => c.prop === selectedDimension)
592
466
  cols.push({
593
- key: dim,
594
- title: colDef ? colDef.label : dim
467
+ key: selectedDimension,
468
+ title: colDef ? colDef.label : selectedDimension,
469
+ type: 'dimension',
470
+ width: '37.5%'
595
471
  })
596
- })
472
+ }
597
473
 
474
+ // 指标列
475
+ const totalMetricWidth = 62.5
476
+ const metricWidth = metrics.length > 0 ? totalMetricWidth / metrics.length + '%' : null
598
477
  metrics.forEach((metric) => {
599
- const colDef = this.columns.find((c) => c.key === metric)
478
+ const colDef = this.columns.find((c) => c.prop === metric.prop)
600
479
  cols.push({
601
- key: metric,
602
- title: colDef ? colDef.label : metric
480
+ key: metric.prop,
481
+ title: colDef ? colDef.label : metric.prop,
482
+ type: 'metric',
483
+ width: metricWidth,
484
+ aggregateType: metric.aggregateType
485
+ })
486
+ })
487
+
488
+ return cols
489
+ },
490
+
491
+ // 交叉表列定义
492
+ crossTableColumns() {
493
+ const { rowDimension, columnDimension, metrics: rawMetrics } = this.currentReport
494
+ const metrics = (rawMetrics || []).filter((m) => m && m.prop)
495
+ if (!rowDimension || !columnDimension || metrics.length === 0) return []
496
+
497
+ const cols = []
498
+
499
+ const rowDimDef = this.columns.find((c) => c.prop === rowDimension)
500
+ cols.push({
501
+ key: rowDimension,
502
+ title: rowDimDef ? rowDimDef.label : rowDimension,
503
+ type: 'dimension',
504
+ isPrimary: true
505
+ })
506
+
507
+ const uniqueColumnValues = [...new Set(this.actualData.map((item) => item[columnDimension]))]
508
+
509
+ uniqueColumnValues.forEach((colValue) => {
510
+ metrics.forEach((metric) => {
511
+ const metricDef = this.columns.find((c) => c.prop === metric.prop)
512
+ const colMetricKey = `${colValue}_${metric.prop}`
513
+ cols.push({
514
+ key: colMetricKey,
515
+ title: `${colValue} - ${metricDef ? metricDef.label : metric.prop}`,
516
+ type: 'metric',
517
+ aggregateType: metric.aggregateType,
518
+ columnValue: colValue,
519
+ metricKey: metric.prop
520
+ })
603
521
  })
604
522
  })
605
523
 
606
524
  return cols
607
525
  },
608
526
 
609
- // 表格数据
610
- tableData() {
611
- return this.processedData
527
+ // 分页后的表格数据
528
+ paginatedTableData() {
529
+ const allData = this.processedData
530
+ if (!allData || allData.length === 0) return []
531
+
532
+ const start = (this.currentPage - 1) * this.pageSize
533
+ const end = start + this.pageSize
534
+ return allData.slice(start, end)
535
+ },
536
+
537
+ // 总页数
538
+ totalPages() {
539
+ const total = this.dataCount
540
+ if (!total || total === 0) return 1
541
+ return Math.ceil(total / this.pageSize)
542
+ },
543
+
544
+ // 图表单位标签
545
+ getChartUnitLabel() {
546
+ const metrics = (this.currentReport.metrics || []).filter((m) => m && m.prop)
547
+ if (metrics.length === 0) return ''
548
+ const firstMetric = this.columns.find((c) => c.prop === metrics[0].prop)
549
+ return firstMetric ? `${firstMetric.label}(人民币)` : ''
550
+ },
551
+
552
+ // 获取第一个维度标签
553
+ getFirstDimensionLabel() {
554
+ const dim = this.currentReport.selectedDimension
555
+ if (!dim) return '未选择'
556
+ const dimDef = this.columns.find((c) => c.prop === dim)
557
+ return dimDef ? dimDef.label : dim
612
558
  },
613
559
 
614
560
  // 图表配置
615
561
  chartOption() {
616
562
  if (!this.hasData) return {}
617
563
 
618
- const { chartType, dimensions, metrics, aggregateType } = this.currentReport
564
+ const { chartType, selectedDimension, metrics: rawMetrics } = this.currentReport
565
+ const metrics = (rawMetrics || []).filter((m) => m && m.prop)
619
566
  const data = this.processedData
620
567
 
621
- // 获取标签和数值
622
- const labels = data.map((row) => row[dimensions[0]] || '')
623
- const labelCol = this.columns.find((c) => c.key === dimensions[0])
624
- const labelName = labelCol ? labelCol.label : dimensions[0]
568
+ const labels = data.map((row) => row[selectedDimension] || '')
569
+
570
+ const gridConfig = {
571
+ left: '3%',
572
+ right: '4%',
573
+ bottom: '15%',
574
+ top: '10%',
575
+ containLabel: true
576
+ }
625
577
 
626
578
  if (chartType === 'pie') {
627
- // 饼图配置
628
- const metric = metrics[0] || ''
629
- const metricCol = this.columns.find((c) => c.key === metric)
630
- const metricName = metricCol ? metricCol.label : metric
579
+ const metricProp = metrics[0]?.prop || ''
580
+ const metricCol = this.columns.find((c) => c.prop === metricProp)
581
+ const metricName = metricCol ? metricCol.label : metricProp
631
582
 
632
583
  return {
633
584
  tooltip: {
634
585
  trigger: 'item',
635
- formatter: `{b}: {c} ({d}%)`
586
+ formatter: '{b}: {c} ({d}%)'
636
587
  },
637
588
  legend: {
638
589
  orient: 'horizontal',
@@ -662,25 +613,24 @@ export default {
662
613
  }
663
614
  },
664
615
  data: data.map((row) => ({
665
- name: row[dimensions[0]],
666
- value: row[metric]
616
+ name: row[selectedDimension],
617
+ value: row[metricProp]
667
618
  }))
668
619
  }
669
620
  ],
670
- color: ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#5856D6', '#AF52DE', '#00C7BE', '#FF2D55']
621
+ color: ['#165DFF', '#14C9C9', '#F7BA1E', '#F53F3F', '#86909C', '#00B42A', '#FF7D00', '#F5319D']
671
622
  }
672
623
  }
673
624
 
674
- // 柱状图/折线图配置
675
- const series = metrics.map((metric, index) => {
676
- const metricCol = this.columns.find((c) => c.key === metric)
677
- const metricName = metricCol ? metricCol.label : metric
625
+ const series = metrics.map((metric) => {
626
+ const metricCol = this.columns.find((c) => c.prop === metric.prop)
627
+ const metricName = metricCol ? metricCol.label : metric.prop
678
628
 
679
629
  return {
680
630
  name: metricName,
681
631
  type: chartType === 'line' ? 'line' : 'bar',
682
- data: data.map((row) => row[metric]),
683
- barMaxWidth: 32,
632
+ data: data.map((row) => row[metric.prop]),
633
+ barMaxWidth: 24,
684
634
  smooth: true
685
635
  }
686
636
  })
@@ -688,40 +638,172 @@ export default {
688
638
  return {
689
639
  tooltip: {
690
640
  trigger: 'axis',
691
- axisPointer: {
692
- type: 'shadow'
641
+ backgroundColor: '#fff',
642
+ borderColor: '#e5e6eb',
643
+ borderWidth: 1,
644
+ padding: [12, 12],
645
+ textStyle: {
646
+ color: '#1d2129'
647
+ },
648
+ extraCssText: 'box-shadow: 0px 8px 10px rgba(0,0,0,0.1); border-radius: 4px;',
649
+ formatter: (params) => {
650
+ const title = params[0].axisValue
651
+ const dataIndex = params[0].dataIndex
652
+ const rowData = this.processedData[dataIndex] || {}
653
+ const groupCount = rowData._groupCount || 0
654
+ let html = `<div style="font-size:14px;font-weight:500;margin-bottom:8px;color:#1d2129;">${title}</div>`
655
+ params.forEach((p) => {
656
+ const color = p.color
657
+ const value =
658
+ typeof p.value === 'number' ? p.value.toLocaleString('en-US', { minimumFractionDigits: 2 }) : p.value
659
+ html += `<div style="display:flex;align-items:center;justify-content:space-between;gap:24px;margin-top:4px;">`
660
+ html += `<div style="display:flex;align-items:center;gap:4px;">`
661
+ html += `<span style="display:inline-block;width:8px;height:3px;background:${color};"></span>`
662
+ html += `<span style="font-size:12px;color:#4e5969;font-family:'PingFang SC',sans-serif;">${p.seriesName}</span>`
663
+ html += `</div>`
664
+ html += `<div style="display:flex;align-items:center;gap:8px;">`
665
+ html += `<span style="font-size:12px;color:#1d2129;font-weight:600;font-family:'Open Sans',sans-serif;">${value}</span>`
666
+ html += `<span style="font-size:12px;color:#4e5969;font-family:'PingFang SC',sans-serif;">${groupCount} 笔</span>`
667
+ html += `</div>`
668
+ html += `</div>`
669
+ })
670
+ return html
693
671
  }
694
672
  },
695
673
  legend: {
696
674
  data: series.map((s) => s.name),
697
- bottom: 10,
698
- left: 'center'
699
- },
700
- grid: {
701
- left: '3%',
702
- right: '4%',
703
- bottom: '15%',
704
- top: '10%',
705
- containLabel: true
675
+ top: 0,
676
+ right: 0,
677
+ itemWidth: 8,
678
+ itemHeight: 3,
679
+ textStyle: {
680
+ fontSize: 12,
681
+ color: '#4e5969',
682
+ fontFamily: "'PingFang SC', sans-serif"
683
+ }
706
684
  },
685
+ grid: gridConfig,
707
686
  xAxis: {
708
687
  type: 'category',
709
688
  data: labels,
710
689
  axisLabel: {
711
690
  interval: 0,
712
- rotate: labels.length > 6 ? 30 : 0
691
+ rotate: labels.length > 6 ? 30 : 0,
692
+ color: '#86909c',
693
+ fontSize: 12
694
+ },
695
+ axisLine: {
696
+ lineStyle: {
697
+ color: '#e5e6eb'
698
+ }
699
+ },
700
+ axisTick: {
701
+ alignWithLabel: true,
702
+ lineStyle: {
703
+ color: '#e5e6eb'
704
+ }
713
705
  }
714
706
  },
715
707
  yAxis: {
716
- type: 'value'
708
+ type: 'value',
709
+ axisLabel: {
710
+ color: '#86909c',
711
+ fontSize: 12,
712
+ formatter: (value) => {
713
+ if (value >= 1000000) {
714
+ return (value / 1000000).toFixed(0) + ',000,000.00'
715
+ }
716
+ return value.toLocaleString('en-US', { minimumFractionDigits: 2 })
717
+ }
718
+ },
719
+ splitLine: {
720
+ lineStyle: {
721
+ color: '#e5e6eb',
722
+ type: [4, 4]
723
+ }
724
+ },
725
+ axisLine: {
726
+ show: false
727
+ },
728
+ axisTick: {
729
+ show: false
730
+ }
717
731
  },
718
732
  series,
719
- color: ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#5856D6']
733
+ color: ['#165DFF', '#14C9C9', '#F7BA1E', '#F53F3F', '#86909C']
720
734
  }
721
735
  }
722
736
  },
723
737
 
724
738
  watch: {
739
+ actualData: {
740
+ handler(newData) {
741
+ if (newData && newData.length > 0) {
742
+ this.currentPage = 1
743
+ this.error = null
744
+ if (this.viewMode === 'chart') {
745
+ this.$nextTick(() => {
746
+ this.initChart()
747
+ })
748
+ }
749
+ }
750
+ },
751
+ deep: true
752
+ },
753
+
754
+ columns: {
755
+ handler(newColumns) {
756
+ if (newColumns && newColumns.length > 0) {
757
+ this.initDefaultConfig()
758
+ }
759
+ },
760
+ immediate: true
761
+ },
762
+
763
+ rowFields: {
764
+ handler(fields) {
765
+ if (fields && fields.length > 0 && this.currentReport) {
766
+ this.currentReport.selectedDimension = fields[0]
767
+ }
768
+ },
769
+ immediate: true
770
+ },
771
+
772
+ colFields: {
773
+ handler(fields) {
774
+ if (fields && fields.length > 0 && this.currentReport) {
775
+ if (fields.length >= 1) {
776
+ this.currentReport.rowDimension = fields[0]
777
+ }
778
+ if (fields.length >= 2) {
779
+ this.currentReport.columnDimension = fields[1]
780
+ }
781
+ }
782
+ },
783
+ immediate: true
784
+ },
785
+
786
+ valueFields: {
787
+ handler(fields) {
788
+ if (fields && fields.length > 0 && this.currentReport) {
789
+ this.currentReport.metrics = fields.map((prop) => ({
790
+ prop,
791
+ aggregateType: 'sum'
792
+ }))
793
+ }
794
+ },
795
+ immediate: true
796
+ },
797
+
798
+ chartType: {
799
+ handler(type) {
800
+ if (type && this.currentReport) {
801
+ this.currentReport.chartType = type
802
+ }
803
+ },
804
+ immediate: true
805
+ },
806
+
725
807
  viewMode(newVal) {
726
808
  if (newVal === 'chart') {
727
809
  this.$nextTick(() => {
@@ -732,8 +814,10 @@ export default {
732
814
  chartOption: {
733
815
  deep: true,
734
816
  handler() {
735
- if (this.viewMode === 'chart' && this.chartInstance) {
736
- this.chartInstance.setOption(this.chartOption, true)
817
+ if (this.viewMode === 'chart') {
818
+ this.$nextTick(() => {
819
+ this.initChart()
820
+ })
737
821
  }
738
822
  }
739
823
  },
@@ -743,6 +827,9 @@ export default {
743
827
  this.initChart()
744
828
  })
745
829
  }
830
+ },
831
+ 'currentReport.statType'() {
832
+ this.currentPage = 1
746
833
  }
747
834
  },
748
835
 
@@ -752,43 +839,156 @@ export default {
752
839
  this.addNewReport()
753
840
  }
754
841
  document.addEventListener('click', this.handleOutsideClick)
842
+
843
+ this.handleResize = debounce(() => {
844
+ if (this.$refs.chartView) {
845
+ this.$refs.chartView.resizeChart()
846
+ }
847
+ }, DEBOUNCE_DELAY)
848
+ window.addEventListener('resize', this.handleResize)
755
849
  },
756
850
 
757
851
  beforeDestroy() {
758
852
  document.removeEventListener('click', this.handleOutsideClick)
853
+ window.removeEventListener('resize', this.handleResize)
759
854
  this.saveToStorage()
760
- if (this.chartInstance) {
761
- this.chartInstance.dispose()
762
- this.chartInstance = null
763
- }
764
855
  },
765
856
 
766
857
  methods: {
858
+ // 获取列的数据类型(遍历数据找到第一个有效值判断)
859
+ getColumnType(prop) {
860
+ if (!this.actualData || this.actualData.length === 0) return 'unknown'
861
+ // 遍历数据找到第一个有效值
862
+ for (const row of this.actualData) {
863
+ const value = row[prop]
864
+ if (value !== null && value !== undefined && value !== '') {
865
+ if (typeof value === 'number') return 'number'
866
+ return 'string'
867
+ }
868
+ }
869
+ return 'unknown'
870
+ },
871
+
872
+ // 获取字段的枚举选项(filters 或 options)
873
+ getEnumOptions(prop) {
874
+ const col = this.columns.find((c) => c.prop === prop)
875
+ if (!col) return null
876
+ // 优先使用 filters,其次 options
877
+ if (col.filters && col.filters.length > 0) return col.filters
878
+ if (col.options && col.options.length > 0) return col.options
879
+ return null
880
+ },
881
+
882
+ // 获取枚举字段的显示值(用于分组)
883
+ getEnumDisplayValue(row, prop) {
884
+ const enumOptions = this.getEnumOptions(prop)
885
+ const rawValue = row[prop]
886
+
887
+ // 如果没有枚举配置,直接返回原始值
888
+ if (!enumOptions) {
889
+ // 处理对象类型的值(如 txType.name)
890
+ if (rawValue && typeof rawValue === 'object') {
891
+ return rawValue.name || rawValue.label || JSON.stringify(rawValue)
892
+ }
893
+ return rawValue
894
+ }
895
+
896
+ // 根据 rawValue 查找对应的枚举标签
897
+ // rawValue 可能是对象 { code, name } 或直接是值
898
+ const codeValue = rawValue && typeof rawValue === 'object' ? rawValue.code || rawValue.value : rawValue
899
+
900
+ // 在枚举选项中查找匹配项
901
+ const matchedOption = enumOptions.find((opt) => {
902
+ const optValue = opt.value !== undefined ? opt.value : opt.code
903
+ return optValue === codeValue || optValue === rawValue
904
+ })
905
+
906
+ if (matchedOption) {
907
+ return matchedOption.label || matchedOption.name || matchedOption.text || codeValue
908
+ }
909
+
910
+ // 未匹配到,返回原始值的显示形式
911
+ if (rawValue && typeof rawValue === 'object') {
912
+ return rawValue.name || rawValue.label || JSON.stringify(rawValue)
913
+ }
914
+ return rawValue
915
+ },
916
+
917
+ // 初始化默认配置
918
+ initDefaultConfig() {
919
+ if (!this.currentReport) return
920
+ if (this.currentReport.selectedDimension) {
921
+ return
922
+ }
923
+
924
+ const allColumns = this.availableDimensions
925
+ const numericCols = this.availableMetrics
926
+
927
+ if (allColumns.length > 0 && !this.currentReport.selectedDimension) {
928
+ this.currentReport.selectedDimension = allColumns[0].prop
929
+ }
930
+
931
+ if (numericCols.length > 0 && this.currentReport.metrics.length === 0) {
932
+ this.currentReport.metrics = [
933
+ {
934
+ prop: numericCols[0].prop,
935
+ aggregateType: 'sum'
936
+ }
937
+ ]
938
+ }
939
+ },
940
+
767
941
  // 获取默认报表配置
768
942
  getDefaultReport() {
769
943
  return {
770
944
  id: this.generateId(),
771
945
  name: '报表清单',
772
946
  statType: 'group',
773
- chartType: 'bar',
774
- dimensions: [],
775
- metrics: [] // 每项格式: { key: '字段名', aggregateType: 'sum' }
947
+ chartType: this.chartType || 'bar',
948
+ selectedDimension: null,
949
+ rowDimension: null,
950
+ columnDimension: null,
951
+ metrics: []
776
952
  }
777
953
  },
778
954
 
779
- // 迁移旧格式报表数据(metrics 从 string[] 迁移为 { key, aggregateType }[])
955
+ // 迁移旧格式报表数据
780
956
  migrateReportFormat(report) {
781
- if (report.metrics && report.metrics.length > 0 && typeof report.metrics[0] === 'string') {
782
- const aggregateType = report.aggregateType || 'sum'
783
- report.metrics = report.metrics.map((key) => ({ key, aggregateType }))
957
+ if (!report.metrics) {
958
+ report.metrics = []
959
+ }
960
+ report.metrics = report.metrics
961
+ .filter((m) => m !== null && m !== undefined)
962
+ .map((m) => {
963
+ if (typeof m === 'string') {
964
+ return { prop: m, aggregateType: report.aggregateType || 'sum' }
965
+ }
966
+ if (m.key && !m.prop) {
967
+ return { prop: m.key, aggregateType: m.aggregateType || 'sum' }
968
+ }
969
+ return m
970
+ })
971
+ if (!report.rowDimension) {
972
+ report.rowDimension = null
973
+ }
974
+ if (!report.columnDimension) {
975
+ report.columnDimension = null
976
+ }
977
+ // 兼容旧的 dimensions 数组,迁移为 selectedDimension
978
+ if (report.dimensions && report.dimensions.length > 0 && !report.selectedDimension) {
979
+ report.selectedDimension = report.dimensions[0]
980
+ }
981
+ if (!report.selectedDimension) {
982
+ report.selectedDimension = null
784
983
  }
785
984
  delete report.aggregateType
985
+ delete report.dimensions
786
986
  return report
787
987
  },
788
988
 
789
989
  // 生成唯一ID
790
990
  generateId() {
791
- return 'report_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
991
+ return 'report_' + Date.now() + '_' + Math.random().toString(36).substring(2, 11)
792
992
  },
793
993
 
794
994
  // 添加新报表
@@ -800,299 +1000,282 @@ export default {
800
1000
  this.saveToStorage()
801
1001
  },
802
1002
 
803
- // 选择报表
1003
+ // ========== ReportSidebar 事件处理 ==========
804
1004
  handleSelectReport(index) {
805
1005
  this.currentReportIndex = index
806
1006
  },
807
1007
 
808
- // 添加报表
809
1008
  handleAddReport() {
810
1009
  this.addNewReport()
811
1010
  },
812
1011
 
813
- // 统计类型变更
814
- handleStatTypeChange(type) {
815
- this.currentReport.statType = type
816
- // 交叉统计模式下强制切换到表格视图
817
- if (type === 'cross') {
818
- this.viewMode = 'table'
1012
+ handleToggleLeftSidebar() {
1013
+ this.isLeftSidebarCollapsed = !this.isLeftSidebarCollapsed
1014
+ this.$nextTick(() => {
1015
+ if (this.viewMode === 'chart' && this.$refs.chartView) {
1016
+ this.$refs.chartView.resizeChart()
1017
+ }
1018
+ })
1019
+ },
1020
+
1021
+ handleRenameReport({ id, name }) {
1022
+ const report = this.reportList.find((r) => r.id === id)
1023
+ if (report) {
1024
+ report.name = name
1025
+ this.saveToStorage()
819
1026
  }
820
- this.saveToStorage()
821
1027
  },
822
1028
 
823
- // 图表类型变更
824
- handleChartTypeChange(type) {
825
- this.currentReport.chartType = type
826
- this.showChartTypeDropdown = false
827
- this.saveToStorage()
1029
+ handleDeleteReport(report) {
1030
+ this.$confirm(`确定要删除报表"${report.name}"吗?`, '提示', {
1031
+ confirmButtonText: '确定',
1032
+ cancelButtonText: '取消',
1033
+ type: 'warning'
1034
+ })
1035
+ .then(() => {
1036
+ const index = this.reportList.findIndex((r) => r.id === report.id)
1037
+ if (index > -1) {
1038
+ this.reportList.splice(index, 1)
1039
+ if (this.currentReportIndex >= index && this.currentReportIndex > 0) {
1040
+ this.currentReportIndex--
1041
+ }
1042
+ if (this.reportList.length === 0) {
1043
+ this.addNewReport()
1044
+ }
1045
+ this.saveToStorage()
1046
+ this.$message.success('删除成功')
1047
+ }
1048
+ })
1049
+ .catch(() => {})
828
1050
  },
829
1051
 
830
- // 维度选择变更
831
- handleDimensionToggle(key) {
832
- const index = this.currentReport.dimensions.indexOf(key)
833
- if (index > -1) {
834
- this.currentReport.dimensions.splice(index, 1)
835
- } else {
836
- this.currentReport.dimensions.push(key)
837
- }
838
- this.saveToStorage()
1052
+ // ========== MainToolbar 事件处理 ==========
1053
+ handleViewModeChange(val) {
1054
+ this.viewMode = val
839
1055
  },
840
1056
 
841
- // 指标选择变更
842
- handleMetricToggle(key) {
843
- const index = this.currentReport.metrics.indexOf(key)
844
- if (index > -1) {
845
- this.currentReport.metrics.splice(index, 1)
846
- } else {
847
- this.currentReport.metrics.push(key)
1057
+ handleRefresh() {
1058
+ if (this.viewMode === 'chart') {
1059
+ this.$nextTick(() => {
1060
+ this.initChart()
1061
+ })
848
1062
  }
849
- this.saveToStorage()
1063
+ this.$emit('refresh')
850
1064
  },
851
1065
 
852
- // 聚合类型变更
853
- handleAggregateChange(type) {
854
- this.currentReport.aggregateType = type
855
- this.showAggregateDropdown = false
856
- this.saveToStorage()
1066
+ handleConfig() {
1067
+ this.handleToggleRightSidebar()
857
1068
  },
858
1069
 
859
- // 添加指标字段
860
- handleAddMetric() {
861
- // 默认选中第一个可用指标
862
- if (this.availableMetrics.length > 0 && !this.currentReport.metrics.includes(this.availableMetrics[0].key)) {
863
- this.currentReport.metrics.push(this.availableMetrics[0].key)
864
- this.saveToStorage()
1070
+ handleExport() {
1071
+ const data = this.processedData
1072
+ if (data.length === 0) {
1073
+ this.$message.warning('暂无数据可导出')
1074
+ return
865
1075
  }
1076
+
1077
+ const headers = this.tableColumns.map((col) => col.title).join(',')
1078
+ const rows = data.map((row) => {
1079
+ return this.tableColumns
1080
+ .map((col) => {
1081
+ const val = row[col.key]
1082
+ return typeof val === 'string' ? `"${val}"` : val
1083
+ })
1084
+ .join(',')
1085
+ })
1086
+
1087
+ const csv = [headers, ...rows].join('\n')
1088
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
1089
+ const link = document.createElement('a')
1090
+ link.href = URL.createObjectURL(blob)
1091
+ link.download = `${this.currentReport.name}_${Date.now()}.csv`
1092
+ link.click()
1093
+
1094
+ this.$emit('export', data)
866
1095
  },
867
1096
 
868
- // 配置变更
869
- handleConfigChange() {
870
- this.saveToStorage()
1097
+ handleAddToHome() {
1098
+ this.$emit('add-to-home', this.currentReport)
1099
+ this.$message.success('已添加到首页')
871
1100
  },
872
1101
 
873
- // 获取图表类型标签
874
- getChartTypeLabel(type) {
875
- const chart = this.chartTypes.find((t) => t.value === type)
876
- return chart ? chart.label : '柱状图'
1102
+ handleDeleteCurrentReport() {
1103
+ this.handleDeleteReport(this.currentReport)
877
1104
  },
878
1105
 
879
- // 获取图表类型图标
880
- getChartTypeIcon(type) {
881
- const iconMap = {
882
- bar: 'v3-icon-chart-histogram',
883
- line: 'v3-icon-chart-line',
884
- pie: 'v3-icon-chart-pie',
885
- horizontalBar: 'v3-icon-histogram'
1106
+ // ========== TableView 事件处理 ==========
1107
+ handlePrevPage() {
1108
+ if (this.currentPage > 1) {
1109
+ this.currentPage--
886
1110
  }
887
- return iconMap[type] || 'v3-icon-chart-histogram'
888
1111
  },
889
1112
 
890
- // 获取聚合类型标签
891
- getAggregateLabel(type) {
892
- const agg = this.aggregateTypes.find((t) => t.value === type)
893
- return agg ? agg.label : '求和'
1113
+ handleNextPage() {
1114
+ if (this.currentPage < this.totalPages) {
1115
+ this.currentPage++
1116
+ }
894
1117
  },
895
1118
 
896
- // 初始化图表
897
- async initChart() {
898
- if (!this.$refs.chartContainer) return
899
- if (!this.hasData) return
900
-
901
- const echarts = await importG('echarts', () => import(/*webpackChunkName "echarts"*/ 'echarts'))
902
-
903
- if (!this.chartInstance) {
904
- this.chartInstance = echarts.init(this.$refs.chartContainer)
905
- }
906
-
907
- if (this.chartOption) {
908
- this.chartInstance.setOption(this.chartOption, true)
909
- }
1119
+ handleJumpToPage(page) {
1120
+ this.currentPage = page
910
1121
  },
911
1122
 
912
- // 从本地存储加载
913
- loadFromStorage() {
914
- try {
915
- const saved = localStorage.getItem(STORAGE_KEY)
916
- if (saved) {
917
- const configs = JSON.parse(saved)
918
- if (Array.isArray(configs) && configs.length > 0) {
919
- // 迁移旧格式数据
920
- this.reportList = configs.map((report) => this.migrateReportFormat(report))
921
- }
922
- }
923
- } catch (e) {
924
- console.warn('Failed to load pivot configs from storage:', e)
925
- }
1123
+ handlePageSizeChange(size) {
1124
+ this.pageSize = size
1125
+ this.currentPage = 1
926
1126
  },
927
1127
 
928
- // 保存到本地存储
929
- saveToStorage() {
930
- try {
931
- localStorage.setItem(STORAGE_KEY, JSON.stringify(this.reportList))
932
- } catch (e) {
933
- console.warn('Failed to save pivot configs to storage:', e)
934
- }
1128
+ // ========== ConfigSidebar 事件处理 ==========
1129
+ handleToggleRightSidebar() {
1130
+ this.isRightSidebarCollapsed = !this.isRightSidebarCollapsed
1131
+ this.$nextTick(() => {
1132
+ if (this.viewMode === 'chart' && this.$refs.chartView) {
1133
+ this.$refs.chartView.resizeChart()
1134
+ }
1135
+ })
935
1136
  },
936
1137
 
937
- // 刷新数据
938
- handleRefresh() {
939
- if (this.viewMode === 'chart') {
940
- this.$nextTick(() => {
941
- this.initChart()
942
- })
943
- }
944
- this.$emit('refresh')
1138
+ handleNameChange(value) {
1139
+ this.currentReport.name = value
1140
+ this.saveToStorage()
945
1141
  },
946
1142
 
947
- // 导出数据
948
- handleExport() {
949
- const data = this.processedData
950
- if (data.length === 0) {
951
- this.$message.warning('暂无数据可导出')
952
- return
1143
+ handleStatTypeChange(type) {
1144
+ this.currentReport.statType = type
1145
+ if (type === 'cross') {
1146
+ // 切换到交叉统计时清除分组维度
1147
+ this.currentReport.selectedDimension = null
1148
+ this.viewMode = 'table'
1149
+ } else {
1150
+ // 切换到分组统计时清除行列维度
1151
+ this.currentReport.rowDimension = null
1152
+ this.currentReport.columnDimension = null
953
1153
  }
954
-
955
- // 生成CSV
956
- const headers = this.tableColumns.map((col) => col.title).join(',')
957
- const rows = data.map((row) => {
958
- return this.tableColumns
959
- .map((col) => {
960
- const val = row[col.key]
961
- return typeof val === 'string' ? `"${val}"` : val
962
- })
963
- .join(',')
964
- })
965
-
966
- const csv = [headers, ...rows].join('\n')
967
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
968
- const link = document.createElement('a')
969
- link.href = URL.createObjectURL(blob)
970
- link.download = `${this.currentReport.name}_${Date.now()}.csv`
971
- link.click()
972
-
973
- this.$emit('export', data)
1154
+ this.saveToStorage()
974
1155
  },
975
1156
 
976
- // 添加到首页
977
- handleAddToHome() {
978
- this.$emit('add-to-home', this.currentReport)
979
- this.$message.success('已添加到首页')
1157
+ handleChartTypeChange(type) {
1158
+ this.currentReport.chartType = type
1159
+ this.saveToStorage()
980
1160
  },
981
1161
 
982
- // 新建报表
983
- handleNewReport() {
984
- this.addNewReport()
1162
+ handleDimensionSelect(dimKey) {
1163
+ this.currentReport.selectedDimension = dimKey
1164
+ this.saveToStorage()
985
1165
  },
986
1166
 
987
- // 关闭
988
- handleClose() {
989
- this.$emit('close')
1167
+ handleRowDimensionChange(dimKey) {
1168
+ if (this.currentReport.columnDimension === dimKey) {
1169
+ this.currentReport.columnDimension = null
1170
+ }
1171
+ this.currentReport.rowDimension = dimKey
1172
+ this.saveToStorage()
990
1173
  },
991
1174
 
992
- // 点击外部关闭下拉框
993
- handleOutsideClick(e) {
994
- if (
995
- !e.target.closest('.select-wrapper') &&
996
- !e.target.closest('.aggregate-dropdown') &&
997
- !e.target.closest('.report-more')
998
- ) {
999
- this.showChartTypeDropdown = false
1000
- this.showDimensionDropdown = false
1001
- this.showMetricDropdown = false
1002
- this.showAggregateDropdown = false
1003
- this.activeMenuReportId = null
1175
+ handleColumnDimensionChange(dimKey) {
1176
+ if (this.currentReport.rowDimension === dimKey) {
1177
+ this.currentReport.rowDimension = null
1004
1178
  }
1179
+ this.currentReport.columnDimension = dimKey
1180
+ this.saveToStorage()
1005
1181
  },
1006
1182
 
1007
- // 处理左侧边栏折叠/展开
1008
- handleScrollBarIndicator() {
1009
- this.isLeftSidebarCollapsed = !this.isLeftSidebarCollapsed
1010
- this.$nextTick(() => {
1011
- if (this.viewMode === 'chart' && this.chartInstance) {
1012
- this.chartInstance.resize()
1013
- }
1014
- })
1183
+ handleMetricAggregateChange({ prop, aggregateType }) {
1184
+ const index = this.currentReport.metrics.findIndex((m) => m && m.prop === prop)
1185
+ if (index > -1) {
1186
+ this.$set(this.currentReport.metrics, index, {
1187
+ ...this.currentReport.metrics[index],
1188
+ aggregateType
1189
+ })
1190
+ this.saveToStorage()
1191
+ }
1015
1192
  },
1016
1193
 
1017
- // 处理右侧边栏折叠/展开
1018
- handleToggleRightSidebar() {
1019
- this.isRightSidebarCollapsed = !this.isRightSidebarCollapsed
1020
- this.$nextTick(() => {
1021
- if (this.viewMode === 'chart' && this.chartInstance) {
1022
- this.chartInstance.resize()
1194
+ handleGlobalAggregateChange(type) {
1195
+ this.currentReport.metrics.forEach((metric, index) => {
1196
+ if (metric && metric.prop) {
1197
+ this.$set(this.currentReport.metrics, index, { ...metric, aggregateType: type })
1023
1198
  }
1024
1199
  })
1200
+ this.saveToStorage()
1025
1201
  },
1026
1202
 
1027
- // 切换报表菜单显示
1028
- handleToggleMenu(reportId) {
1029
- this.activeMenuReportId = this.activeMenuReportId === reportId ? null : reportId
1203
+ getGlobalAggregateType() {
1204
+ const firstValid = (this.currentReport.metrics || []).find((m) => m && m.prop)
1205
+ return firstValid ? firstValid.aggregateType : 'sum'
1030
1206
  },
1031
1207
 
1032
- // 开始重命名
1033
- handleStartRename(report) {
1034
- this.editingReportId = report.id
1035
- this.editingReportName = report.name
1036
- this.activeMenuReportId = null
1037
- this.$nextTick(() => {
1038
- const input = this.$el.querySelector('.report-name-input')
1039
- if (input) {
1040
- input.focus()
1041
- input.select()
1042
- }
1208
+ handleAddSpecificMetric(metricProp) {
1209
+ this.currentReport.metrics.push({
1210
+ prop: metricProp,
1211
+ aggregateType: this.getGlobalAggregateType()
1043
1212
  })
1213
+ this.saveToStorage()
1044
1214
  },
1045
1215
 
1046
- // 保存重命名
1047
- handleSaveRename() {
1048
- if (this.editingReportId && this.editingReportName.trim()) {
1049
- const report = this.reportList.find((r) => r.id === this.editingReportId)
1050
- if (report) {
1051
- report.name = this.editingReportName.trim()
1052
- this.saveToStorage()
1053
- }
1216
+ handleRemoveMetric(metricProp) {
1217
+ const index = this.currentReport.metrics.findIndex((m) => m && m.prop === metricProp)
1218
+ if (index > -1) {
1219
+ this.currentReport.metrics.splice(index, 1)
1220
+ this.saveToStorage()
1054
1221
  }
1055
- this.editingReportId = null
1056
- this.editingReportName = ''
1057
1222
  },
1058
1223
 
1059
- // 取消重命名
1060
- handleCancelRename() {
1061
- this.editingReportId = null
1062
- this.editingReportName = ''
1224
+ // ========== 其他方法 ==========
1225
+ initChart() {
1226
+ if (this.$refs.chartView) {
1227
+ this.$refs.chartView.initChart()
1228
+ }
1063
1229
  },
1064
1230
 
1065
- // 删除报表
1066
- handleDeleteReport(report) {
1067
- this.activeMenuReportId = null
1068
- this.$confirm(`确定要删除报表"${report.name}"吗?`, '提示', {
1069
- confirmButtonText: '确定',
1070
- cancelButtonText: '取消',
1071
- type: 'warning'
1072
- })
1073
- .then(() => {
1074
- const index = this.reportList.findIndex((r) => r.id === report.id)
1075
- if (index > -1) {
1076
- this.reportList.splice(index, 1)
1077
- if (this.currentReportIndex >= index && this.currentReportIndex > 0) {
1078
- this.currentReportIndex--
1079
- }
1080
- if (this.reportList.length === 0) {
1081
- this.addNewReport()
1082
- }
1083
- this.saveToStorage()
1084
- this.$message.success('删除成功')
1231
+ loadFromStorage() {
1232
+ try {
1233
+ const saved = localStorage.getItem(STORAGE_KEY)
1234
+ if (saved) {
1235
+ const configs = JSON.parse(saved)
1236
+ if (Array.isArray(configs) && configs.length > 0) {
1237
+ this.reportList = configs.map((report) => this.migrateReportFormat(report))
1085
1238
  }
1086
- })
1087
- .catch(() => {})
1239
+ }
1240
+ } catch (e) {
1241
+ console.warn('Failed to load pivot configs from storage:', e)
1242
+ }
1088
1243
  },
1089
1244
 
1090
- // 处理视图模式切换
1091
- handleViewModeChange(val) {
1092
- if (val === 'chart') {
1093
- this.$nextTick(() => {
1094
- this.initChart()
1095
- })
1245
+ saveToStorage() {
1246
+ try {
1247
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.reportList))
1248
+ } catch (e) {
1249
+ console.warn('Failed to save pivot configs to storage:', e)
1250
+ }
1251
+ },
1252
+
1253
+ handleOutsideClick(e) {
1254
+ const dropdownSelectors = [
1255
+ '.select-wrapper',
1256
+ '.aggregate-dropdown',
1257
+ '.add-field-btn',
1258
+ '.metric-add-dropdown',
1259
+ '.metric-select-tag',
1260
+ '.report-more',
1261
+ '.more-btn',
1262
+ '.pagination-size-select'
1263
+ ]
1264
+ const isInsideDropdown = dropdownSelectors.some((sel) => e.target.closest(sel))
1265
+ if (!isInsideDropdown) {
1266
+ // 关闭所有子组件的下拉菜单
1267
+ if (this.$refs.reportSidebar) {
1268
+ this.$refs.reportSidebar.closeMenu()
1269
+ }
1270
+ if (this.$refs.mainToolbar) {
1271
+ this.$refs.mainToolbar.closeMenu()
1272
+ }
1273
+ if (this.$refs.tableView) {
1274
+ this.$refs.tableView.closeDropdown()
1275
+ }
1276
+ if (this.$refs.configSidebar) {
1277
+ this.$refs.configSidebar.closeAllDropdowns()
1278
+ }
1096
1279
  }
1097
1280
  }
1098
1281
  }
@@ -1100,764 +1283,5 @@ export default {
1100
1283
  </script>
1101
1284
 
1102
1285
  <style lang="scss">
1103
- .n20-pivot-dialog {
1104
- .el-dialog {
1105
- box-sizing: border-box;
1106
- margin-top: 3vh !important;
1107
- max-height: 94vh;
1108
- display: flex;
1109
- flex-direction: column;
1110
- .el-dialog__body {
1111
- padding: 0;
1112
- flex: 1;
1113
- min-height: 0;
1114
- overflow: hidden;
1115
- }
1116
- }
1117
- }
1118
- .n20-pivot-container {
1119
- display: flex;
1120
- background: #fff;
1121
- height: 100%;
1122
- border-radius: 8px;
1123
- overflow: hidden;
1124
- font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1125
- position: relative;
1126
- }
1127
-
1128
- /* 左侧边栏 */
1129
- .pivot-sidebar-left {
1130
- width: 280px;
1131
- background: #f2f3f5;
1132
- border-right: 1px solid #e5e6eb;
1133
- display: flex;
1134
- flex-direction: column;
1135
- position: relative;
1136
- transition: width 0.3s ease;
1137
- }
1138
-
1139
- .pivot-sidebar-left.collapsed {
1140
- width: 8px;
1141
- }
1142
-
1143
- .pivot-sidebar-left.collapsed .sidebar-header,
1144
- .pivot-sidebar-left.collapsed .report-list {
1145
- opacity: 0;
1146
- pointer-events: none;
1147
- }
1148
-
1149
- .pivot-sidebar-left .sidebar-header {
1150
- height: 48px;
1151
- padding: 0 16px;
1152
- display: flex;
1153
- align-items: center;
1154
- justify-content: space-between;
1155
- border-bottom: 1px solid #e5e6eb;
1156
- background: #f2f3f5;
1157
- }
1158
-
1159
- .pivot-sidebar-left .sidebar-title {
1160
- color: var(--text-2, #4e5969);
1161
- /* 13/CN-Regular */
1162
- font-family: 'PingFang SC';
1163
- font-size: 13px;
1164
- font-style: normal;
1165
- font-weight: 400;
1166
- line-height: 22px; /* 169.231% */
1167
- }
1168
-
1169
- .add-report-btn {
1170
- padding: 9px;
1171
- display: flex;
1172
- align-items: center;
1173
- justify-content: center;
1174
- border-radius: 4px;
1175
- border: 1px solid var(--border-2, #e5e6eb);
1176
- background: var(--bg-white, #fff);
1177
- cursor: pointer;
1178
- color: var(--text-primary, #4e5969);
1179
- transition: all 0.2s;
1180
- i {
1181
- font-size: 14px;
1182
- line-height: 14px;
1183
- }
1184
- }
1185
-
1186
- .add-report-btn:hover {
1187
- background: #fff;
1188
- color: #007aff;
1189
- }
1190
-
1191
- .report-list {
1192
- flex: 1;
1193
- overflow-y: auto;
1194
- padding: 8px;
1195
- }
1196
-
1197
- .report-item {
1198
- display: flex;
1199
- align-items: center;
1200
- padding: 8px;
1201
- border-radius: 4px;
1202
- cursor: pointer;
1203
- transition: all 0.2s;
1204
- }
1205
-
1206
- .report-item:hover {
1207
- background: rgba(0, 0, 0, 0.04);
1208
- }
1209
-
1210
- .report-item.active {
1211
- border-radius: 4px;
1212
- background: var(--bg-white, #fff);
1213
- }
1214
-
1215
- .report-item.editing {
1216
- background: var(--bg-white, #fff);
1217
- }
1218
-
1219
- .report-name-input {
1220
- flex: 1;
1221
- height: 24px;
1222
- padding: 0 8px;
1223
- border: 1px solid #007aff;
1224
- border-radius: 2px;
1225
- font-size: 14px;
1226
- color: #1d2129;
1227
- outline: none;
1228
- }
1229
-
1230
- .report-more {
1231
- display: none;
1232
- align-items: center;
1233
- justify-content: center;
1234
- width: 24px;
1235
- height: 24px;
1236
- margin-left: 4px;
1237
- border-radius: 4px;
1238
- cursor: pointer;
1239
- color: #86909c;
1240
- position: relative;
1241
- }
1242
-
1243
- .report-more.visible {
1244
- display: flex;
1245
- }
1246
-
1247
- .report-more:hover {
1248
- background: rgba(0, 0, 0, 0.04);
1249
- color: #4e5969;
1250
- }
1251
-
1252
- .report-menu {
1253
- position: absolute;
1254
- top: 100%;
1255
- right: 0;
1256
- margin-top: 4px;
1257
- background: #fff;
1258
- border: 1px solid #e5e6eb;
1259
- border-radius: 4px;
1260
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1261
- z-index: 100;
1262
- min-width: 100px;
1263
- padding: 4px 0;
1264
- }
1265
-
1266
- .report-menu-item {
1267
- display: flex;
1268
- align-items: center;
1269
- gap: 8px;
1270
- padding: 8px 12px;
1271
- font-size: 14px;
1272
- color: #1d2129;
1273
- cursor: pointer;
1274
- transition: background 0.2s;
1275
- white-space: nowrap;
1276
- }
1277
-
1278
- .report-menu-item:hover {
1279
- background: #f7f8fa;
1280
- }
1281
-
1282
- .report-menu-item.delete {
1283
- color: #f53f3f;
1284
- }
1285
-
1286
- .report-menu-item.delete:hover {
1287
- background: #fff2f0;
1288
- }
1289
-
1290
- .report-icon {
1291
- width: 16px;
1292
- height: 16px;
1293
- margin-right: 8px;
1294
- color: #4e5969;
1295
- }
1296
-
1297
- .report-item.active .report-icon {
1298
- color: #007aff;
1299
- }
1300
-
1301
- .report-name {
1302
- font-size: 14px;
1303
- color: #1d2129;
1304
- flex: 1;
1305
- overflow: hidden;
1306
- text-overflow: ellipsis;
1307
- white-space: nowrap;
1308
- }
1309
-
1310
- .scroll-bar-indicator {
1311
- position: absolute;
1312
- right: -8px;
1313
- top: 50%;
1314
- transform: translateY(-50%);
1315
- width: 16px;
1316
- height: 80px;
1317
- display: flex;
1318
- align-items: center;
1319
- justify-content: center;
1320
- border-radius: 8px;
1321
- border: 1px solid var(--border-2, #e5e6eb);
1322
- background: var(--bg-white, #fff);
1323
- color: #4e5969;
1324
- cursor: pointer;
1325
- // transition: right 0.3s ease, color 0.2s;
1326
- // z-index: 10;
1327
- }
1328
-
1329
- .scroll-bar-indicator:hover {
1330
- color: #007aff;
1331
- }
1332
-
1333
- .scroll-bar-indicator.collapsed {
1334
- right: -8px;
1335
- }
1336
-
1337
- /* 主内容区域 */
1338
- .pivot-main {
1339
- flex: 1;
1340
- display: flex;
1341
- flex-direction: column;
1342
- position: relative;
1343
- padding: 16px;
1344
- transition: margin-left 0.3s ease, margin-right 0.3s ease;
1345
- }
1346
-
1347
- .pivot-main.left-collapsed {
1348
- margin-left: 8px;
1349
- }
1350
-
1351
- .pivot-main.right-collapsed {
1352
- margin-right: 8px;
1353
- }
1354
-
1355
- .main-toolbar {
1356
- display: flex;
1357
- align-items: center;
1358
- justify-content: space-between;
1359
- padding: 6px 16px;
1360
- border-bottom: 1px solid #e5e6eb;
1361
- }
1362
-
1363
- .toolbar-left {
1364
- display: flex;
1365
- flex-direction: column;
1366
- justify-content: center;
1367
- gap: 4px;
1368
- }
1369
-
1370
- .toolbar-left-top {
1371
- display: flex;
1372
- align-items: center;
1373
- gap: 8px;
1374
- }
1375
-
1376
- .report-title {
1377
- color: var(--text-1, #1d2129);
1378
- /* 18/CN-Medium */
1379
- font-family: 'PingFang SC';
1380
- font-size: 18px;
1381
- font-style: normal;
1382
- font-weight: 500;
1383
- line-height: 26px; /* 144.444% */
1384
- }
1385
-
1386
- .add-to-home-btn {
1387
- display: flex;
1388
- align-items: center;
1389
- gap: 4px;
1390
- padding: 4px 8px;
1391
- border-radius: 2px;
1392
- cursor: pointer;
1393
- color: #007aff;
1394
- font-size: 14px;
1395
- transition: all 0.2s;
1396
- }
1397
-
1398
- .add-to-home-btn:hover {
1399
- background: rgba(0, 122, 255, 0.08);
1400
- }
1401
-
1402
- .report-stats {
1403
- display: flex;
1404
- align-items: center;
1405
- gap: 8px;
1406
- }
1407
-
1408
- .stat-item {
1409
- font-size: 12px;
1410
- color: #4e5969;
1411
- }
1412
-
1413
- .stat-divider {
1414
- width: 0;
1415
- height: 12px;
1416
- border-left: 1px solid #e5e6eb;
1417
- }
1418
-
1419
- .toolbar-right {
1420
- display: flex;
1421
- align-items: center;
1422
- gap: 8px;
1423
- }
1424
-
1425
- .action-buttons {
1426
- display: flex;
1427
- gap: 8px;
1428
- }
1429
-
1430
- .action-btn {
1431
- width: 32px;
1432
- height: 32px;
1433
- display: flex;
1434
- align-items: center;
1435
- justify-content: center;
1436
- background: #fff;
1437
- border: 1px solid #e5e6eb;
1438
- border-radius: 4px;
1439
- cursor: pointer;
1440
- color: #4e5969;
1441
- transition: all 0.2s;
1442
- }
1443
-
1444
- .action-btn:hover {
1445
- color: #007aff;
1446
- border-color: #007aff;
1447
- }
1448
-
1449
- /* 主内容 */
1450
- .main-content {
1451
- flex: 1;
1452
- padding: 16px;
1453
- overflow: hidden;
1454
- border: 1px solid #e5e6eb;
1455
- border-radius: 4px;
1456
- }
1457
-
1458
- .chart-view,
1459
- .table-view {
1460
- width: 100%;
1461
- height: 100%;
1462
- position: relative;
1463
- }
1464
-
1465
- .chart-container {
1466
- width: 100%;
1467
- height: 100%;
1468
- }
1469
-
1470
- .table-container {
1471
- width: 100%;
1472
- height: 100%;
1473
- overflow: auto;
1474
- }
1475
-
1476
- .pivot-table {
1477
- width: 100%;
1478
- border-collapse: collapse;
1479
- font-size: 14px;
1480
- }
1481
-
1482
- .pivot-table th,
1483
- .pivot-table td {
1484
- padding: 12px 16px;
1485
- text-align: left;
1486
- border: 1px solid #e5e6eb;
1487
- }
1488
-
1489
- .pivot-table th {
1490
- background: #f2f3f5;
1491
- font-weight: 500;
1492
- color: #1d2129;
1493
- position: sticky;
1494
- top: 0;
1495
- z-index: 1;
1496
- }
1497
-
1498
- .pivot-table tbody tr:hover {
1499
- background: #f7f8fa;
1500
- }
1501
-
1502
- .empty-state {
1503
- width: 100%;
1504
- height: 100%;
1505
- display: flex;
1506
- flex-direction: column;
1507
- align-items: center;
1508
- justify-content: center;
1509
- }
1510
-
1511
- .empty-icon {
1512
- opacity: 0.5;
1513
- }
1514
-
1515
- .empty-text {
1516
- margin-top: 16px;
1517
- font-size: 12px;
1518
- color: #4e5969;
1519
- }
1520
-
1521
- /* 新建按钮 */
1522
- .new-report-btn {
1523
- position: absolute;
1524
- top: 56px;
1525
- right: 16px;
1526
- display: flex;
1527
- flex-direction: column;
1528
- align-items: center;
1529
- cursor: pointer;
1530
- }
1531
-
1532
- .new-report-btn > span {
1533
- background: #1d2129;
1534
- color: #fff;
1535
- padding: 8px 16px;
1536
- border-radius: 4px;
1537
- font-size: 14px;
1538
- }
1539
-
1540
- .btn-arrow {
1541
- color: #1d2129;
1542
- }
1543
-
1544
- /* 右侧边栏 */
1545
- .pivot-sidebar-right {
1546
- width: 280px;
1547
- background: #fff;
1548
- border-left: 1px solid #e5e6eb;
1549
- display: flex;
1550
- flex-direction: column;
1551
- transition: width 0.3s ease;
1552
- }
1553
-
1554
- .pivot-sidebar-right.collapsed {
1555
- width: 8px;
1556
- }
1557
-
1558
- .pivot-sidebar-right.collapsed .sidebar-header,
1559
- .pivot-sidebar-right.collapsed .config-form {
1560
- opacity: 0;
1561
- pointer-events: none;
1562
- }
1563
-
1564
- .pivot-sidebar-right .sidebar-header {
1565
- height: 48px;
1566
- padding: 0 16px;
1567
- display: flex;
1568
- align-items: center;
1569
- gap: 8px;
1570
- border-bottom: 1px solid #e5e6eb;
1571
- white-space: nowrap;
1572
- }
1573
-
1574
- .pivot-sidebar-right .sidebar-title {
1575
- color: var(--text-1, #1d2129);
1576
-
1577
- /* 14/CN-Medium */
1578
- font-family: 'PingFang SC';
1579
- font-size: 14px;
1580
- font-style: normal;
1581
- font-weight: 500;
1582
- line-height: 22px; /* 157.143% */
1583
- }
1584
-
1585
- .pivot-sidebar-right .sidebar-header-collapse {
1586
- font-size: 14px;
1587
- color: #86909c;
1588
- cursor: pointer;
1589
- transition: color 0.2s;
1590
- &:hover {
1591
- color: #1d2129;
1592
- }
1593
- }
1594
-
1595
- .config-form {
1596
- flex: 1;
1597
- padding: 0 16px;
1598
- overflow-y: auto;
1599
-
1600
- .form-item:first-child {
1601
- margin-top: 16px;
1602
- }
1603
- }
1604
-
1605
- .form-item {
1606
- margin-bottom: 16px;
1607
- }
1608
-
1609
- .form-divider {
1610
- height: 1px;
1611
- background-color: #e5e6eb;
1612
- margin: 16px 0;
1613
- }
1614
-
1615
- .form-label {
1616
- display: flex;
1617
- align-items: center;
1618
- gap: 4px;
1619
- font-size: 14px;
1620
- color: #4e5969;
1621
- margin-bottom: 8px;
1622
- }
1623
-
1624
- .label-tip {
1625
- color: #86909c;
1626
- }
1627
-
1628
- .form-label-row {
1629
- display: flex;
1630
- align-items: center;
1631
- justify-content: space-between;
1632
- margin-bottom: 8px;
1633
- }
1634
-
1635
- .form-label-row .form-label {
1636
- margin-bottom: 0;
1637
- }
1638
-
1639
- .input-wrapper {
1640
- width: 100%;
1641
- }
1642
-
1643
- .form-input {
1644
- width: 224px;
1645
- height: 32px;
1646
- padding: 0 12px;
1647
- border: 1px solid #e5e6eb;
1648
- border-radius: 4px;
1649
- font-size: 14px;
1650
- color: #1d2129;
1651
- outline: none;
1652
- transition: border-color 0.2s, box-shadow 0.2s;
1653
- }
1654
-
1655
- .form-input:focus {
1656
- border-color: #007aff;
1657
- box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
1658
- }
1659
-
1660
- .form-input::placeholder {
1661
- color: #86909c;
1662
- }
1663
-
1664
- /* 单选组 */
1665
- .radio-group {
1666
- display: flex;
1667
- background: #f2f3f5;
1668
- border-radius: 4px;
1669
- padding: 3px;
1670
- }
1671
-
1672
- .radio-item {
1673
- flex: 1;
1674
- height: 26px;
1675
- display: flex;
1676
- align-items: center;
1677
- justify-content: center;
1678
- font-size: 14px;
1679
- color: #4e5969;
1680
- cursor: pointer;
1681
- border-radius: 2px;
1682
- transition: all 0.2s;
1683
- }
1684
-
1685
- .radio-item.active {
1686
- background: #fff;
1687
- color: #007aff;
1688
- font-weight: 500;
1689
- box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08);
1690
- }
1691
-
1692
- /* 选择框 */
1693
- .select-wrapper {
1694
- position: relative;
1695
- display: flex;
1696
- align-items: center;
1697
- gap: 4px;
1698
- padding: 4px 12px;
1699
- background: #fff;
1700
- border: 1px solid #e5e6eb;
1701
- border-radius: 4px;
1702
- cursor: pointer;
1703
- transition: border-color 0.2s;
1704
- }
1705
-
1706
- .select-wrapper:hover {
1707
- border-color: #007aff;
1708
- }
1709
-
1710
- .select-value {
1711
- flex: 1;
1712
- display: flex;
1713
- align-items: center;
1714
- gap: 8px;
1715
- font-size: 14px;
1716
- color: #1d2129;
1717
- .report-icon {
1718
- width: 16px;
1719
- height: 16px;
1720
- font-size: 16px;
1721
- line-height: 16px;
1722
- display: inline-flex;
1723
- align-items: center;
1724
- justify-content: center;
1725
- margin-right: 0;
1726
- color: #007aff;
1727
- }
1728
- }
1729
-
1730
- .select-value .placeholder {
1731
- color: #86909c;
1732
- }
1733
-
1734
- .select-arrow {
1735
- color: #4e5969;
1736
- transition: transform 0.2s;
1737
- }
1738
-
1739
- .select-tag {
1740
- padding: 2px 8px;
1741
- background: rgba(78, 89, 105, 0.08);
1742
- border-radius: 2px;
1743
- font-size: 12px;
1744
- color: #4e5969;
1745
- margin-right: 8px;
1746
- }
1747
-
1748
- /* 下拉菜单 */
1749
- .dropdown-menu {
1750
- position: absolute;
1751
- top: 100%;
1752
- left: 0;
1753
- right: 0;
1754
- margin-top: 4px;
1755
- background: #fff;
1756
- border: 1px solid #e5e6eb;
1757
- border-radius: 4px;
1758
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1759
- z-index: 100;
1760
- max-height: 200px;
1761
- overflow-y: auto;
1762
- }
1763
-
1764
- .dropdown-item {
1765
- padding: 8px 12px;
1766
- font-size: 14px;
1767
- color: #1d2129;
1768
- cursor: pointer;
1769
- transition: background 0.2s;
1770
- }
1771
-
1772
- .dropdown-item:hover {
1773
- background: #f7f8fa;
1774
- }
1775
-
1776
- .dropdown-item.active {
1777
- color: #007aff;
1778
- }
1779
-
1780
- .checkbox-item {
1781
- display: flex;
1782
- align-items: center;
1783
- gap: 8px;
1784
- }
1785
-
1786
- .checkbox {
1787
- width: 16px;
1788
- height: 16px;
1789
- border: 1px solid #e5e6eb;
1790
- border-radius: 2px;
1791
- display: flex;
1792
- align-items: center;
1793
- justify-content: center;
1794
- transition: all 0.2s;
1795
- }
1796
-
1797
- .checkbox.checked {
1798
- background: #007aff;
1799
- border-color: #007aff;
1800
- color: #fff;
1801
- }
1802
-
1803
- /* 聚合类型下拉 */
1804
- .aggregate-dropdown {
1805
- position: relative;
1806
- display: flex;
1807
- align-items: center;
1808
- gap: 4px;
1809
- padding: 4px 8px;
1810
- border-radius: 2px;
1811
- cursor: pointer;
1812
- color: #007aff;
1813
- font-size: 12px;
1814
- transition: background 0.2s;
1815
- }
1816
-
1817
- .aggregate-dropdown:hover {
1818
- background: rgba(0, 122, 255, 0.08);
1819
- }
1820
-
1821
- .aggregate-dropdown-menu {
1822
- right: 0;
1823
- left: auto;
1824
- min-width: 80px;
1825
- }
1826
-
1827
- /* 添加字段按钮 */
1828
- .add-field-btn {
1829
- display: flex;
1830
- align-items: center;
1831
- gap: 4px;
1832
- margin-top: 8px;
1833
- padding: 4px 8px;
1834
- font-size: 14px;
1835
- color: #007aff;
1836
- cursor: pointer;
1837
- border-radius: 2px;
1838
- transition: background 0.2s;
1839
- }
1840
-
1841
- .add-field-btn:hover {
1842
- background: rgba(0, 122, 255, 0.08);
1843
- }
1844
-
1845
- /* 滚动条样式 */
1846
- ::-webkit-scrollbar {
1847
- width: 6px;
1848
- height: 6px;
1849
- }
1850
-
1851
- ::-webkit-scrollbar-thumb {
1852
- background: #c9cdd4;
1853
- border-radius: 3px;
1854
- }
1855
-
1856
- ::-webkit-scrollbar-track {
1857
- background: transparent;
1858
- }
1859
-
1860
- ::-webkit-scrollbar-thumb:hover {
1861
- background: #a1a7b3;
1862
- }
1863
- </style>
1286
+ @import '../../assets/css/pivot.scss';
1287
+ </style>