n20-common-lib 3.0.40 → 3.0.42

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