n20-common-lib 3.1.5 → 3.1.6

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.
@@ -24,7 +24,7 @@
24
24
  ref="mainToolbar"
25
25
  :report-name="currentReport.name"
26
26
  :calculate-time="calculateTime"
27
- :data-count="dataCount"
27
+ :data-count="rawDataCount"
28
28
  :view-mode="viewMode"
29
29
  :view-mode-options="viewModeOptions"
30
30
  :is-cross-mode="isCrossMode"
@@ -58,6 +58,8 @@
58
58
  :data-count="dataCount"
59
59
  :dimension-label="getFirstDimensionLabel"
60
60
  :is-cross-mode="isCrossMode"
61
+ :row-dimension-label="getRowDimensionLabel"
62
+ :column-dimension-label="getColumnDimensionLabel"
61
63
  :table-columns="tableColumns"
62
64
  :cross-table-columns="crossTableColumns"
63
65
  :table-data="paginatedTableData"
@@ -190,8 +192,6 @@ export default {
190
192
  reportList: [],
191
193
  // 当前选中的报表索引
192
194
  currentReportIndex: 0,
193
- // 视图模式: chart / table
194
- viewMode: 'chart',
195
195
  // 计算耗时
196
196
  calculateTime: 0,
197
197
  // 数据条数
@@ -237,6 +237,11 @@ export default {
237
237
  return this.data || []
238
238
  },
239
239
 
240
+ // 原始数据条数(用于工具栏显示)
241
+ rawDataCount() {
242
+ return this.actualData.length
243
+ },
244
+
240
245
  visibleC: {
241
246
  get() {
242
247
  return this.visible
@@ -260,6 +265,16 @@ export default {
260
265
  return this.currentReport.statType === 'cross'
261
266
  },
262
267
 
268
+ // 视图模式:从报表配置中读取,交叉统计强制为 table
269
+ viewMode() {
270
+ // 交叉统计模式强制使用表格视图
271
+ if (this.isCrossMode) {
272
+ return 'table'
273
+ }
274
+ // 从报表配置中读取,默认为 chart
275
+ return this.currentReport.viewMode || 'chart'
276
+ },
277
+
263
278
  // 是否有数据
264
279
  hasData() {
265
280
  return this.processedData.length > 0
@@ -488,7 +503,7 @@ export default {
488
503
  return cols
489
504
  },
490
505
 
491
- // 交叉表列定义
506
+ // 交叉表列定义(支持多级表头)
492
507
  crossTableColumns() {
493
508
  const { rowDimension, columnDimension, metrics: rawMetrics } = this.currentReport
494
509
  const metrics = (rawMetrics || []).filter((m) => m && m.prop)
@@ -496,6 +511,7 @@ export default {
496
511
 
497
512
  const cols = []
498
513
 
514
+ // 行维度列(第一列)
499
515
  const rowDimDef = this.columns.find((c) => c.prop === rowDimension)
500
516
  cols.push({
501
517
  key: rowDimension,
@@ -504,22 +520,55 @@ export default {
504
520
  isPrimary: true
505
521
  })
506
522
 
507
- const uniqueColumnValues = [...new Set(this.actualData.map((item) => item[columnDimension]))]
523
+ // 获取列维度的唯一值,使用 getEnumDisplayValue 处理枚举和对象类型
524
+ const uniqueColumnValues = [
525
+ ...new Set(
526
+ this.actualData.map((item) => this.getEnumDisplayValue(item, columnDimension))
527
+ )
528
+ ]
508
529
 
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}`
530
+ // 如果有多个指标,使用多级表头结构
531
+ if (metrics.length > 1) {
532
+ // 每个列维度值作为一个父列
533
+ uniqueColumnValues.forEach((colValue) => {
534
+ const safeColValue = String(colValue)
535
+ const children = metrics.map((metric) => {
536
+ const metricDef = this.columns.find((c) => c.prop === metric.prop)
537
+ const colMetricKey = `${safeColValue}_${metric.prop}`
538
+ return {
539
+ key: colMetricKey,
540
+ title: metricDef ? metricDef.label : metric.prop,
541
+ type: 'metric',
542
+ aggregateType: metric.aggregateType,
543
+ columnValue: colValue,
544
+ metricKey: metric.prop
545
+ }
546
+ })
513
547
  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
548
+ key: `group_${safeColValue}`,
549
+ title: colValue,
550
+ type: 'group',
551
+ children
520
552
  })
521
553
  })
522
- })
554
+ } else {
555
+ // 单个指标时,使用原来的扁平结构
556
+ uniqueColumnValues.forEach((colValue) => {
557
+ metrics.forEach((metric) => {
558
+ const metricDef = this.columns.find((c) => c.prop === metric.prop)
559
+ const safeColValue = String(colValue)
560
+ const colMetricKey = `${safeColValue}_${metric.prop}`
561
+ cols.push({
562
+ key: colMetricKey,
563
+ title: `${colValue} - ${metricDef ? metricDef.label : metric.prop}`,
564
+ type: 'metric',
565
+ aggregateType: metric.aggregateType,
566
+ columnValue: colValue,
567
+ metricKey: metric.prop
568
+ })
569
+ })
570
+ })
571
+ }
523
572
 
524
573
  return cols
525
574
  },
@@ -557,6 +606,22 @@ export default {
557
606
  return dimDef ? dimDef.label : dim
558
607
  },
559
608
 
609
+ // 获取行维度标签(交叉统计用)
610
+ getRowDimensionLabel() {
611
+ const dim = this.currentReport.rowDimension
612
+ if (!dim) return '未选择'
613
+ const dimDef = this.columns.find((c) => c.prop === dim)
614
+ return dimDef ? dimDef.label : dim
615
+ },
616
+
617
+ // 获取列维度标签(交叉统计用)
618
+ getColumnDimensionLabel() {
619
+ const dim = this.currentReport.columnDimension
620
+ if (!dim) return '未选择'
621
+ const dimDef = this.columns.find((c) => c.prop === dim)
622
+ return dimDef ? dimDef.label : dim
623
+ },
624
+
560
625
  // 图表配置
561
626
  chartOption() {
562
627
  if (!this.hasData) return {}
@@ -736,6 +801,27 @@ export default {
736
801
  },
737
802
 
738
803
  watch: {
804
+ // 弹窗打开时重新加载数据
805
+ visible(newVal) {
806
+ if (newVal) {
807
+ // 弹窗打开时重新加载持久化数据
808
+ this.loadFromStorage()
809
+ // 如果没有报表则新建
810
+ if (this.reportList.length === 0) {
811
+ this.addNewReport()
812
+ }
813
+ // 初始化图表
814
+ if (this.viewMode === 'chart') {
815
+ this.$nextTick(() => {
816
+ this.initChart()
817
+ })
818
+ }
819
+ } else {
820
+ // 弹窗关闭时保存数据
821
+ this.saveToStorage()
822
+ }
823
+ },
824
+
739
825
  actualData: {
740
826
  handler(newData) {
741
827
  if (newData && newData.length > 0) {
@@ -754,7 +840,10 @@ export default {
754
840
  columns: {
755
841
  handler(newColumns) {
756
842
  if (newColumns && newColumns.length > 0) {
757
- this.initDefaultConfig()
843
+ // 仅在首次初始化或没有配置时调用
844
+ if (!this.currentReport.selectedDimension && !this.currentReport.rowDimension) {
845
+ this.initDefaultConfig()
846
+ }
758
847
  }
759
848
  },
760
849
  immediate: true
@@ -804,6 +893,7 @@ export default {
804
893
  immediate: true
805
894
  },
806
895
 
896
+ // 视图模式变化时初始化图表
807
897
  viewMode(newVal) {
808
898
  if (newVal === 'chart') {
809
899
  this.$nextTick(() => {
@@ -811,6 +901,7 @@ export default {
811
901
  })
812
902
  }
813
903
  },
904
+
814
905
  chartOption: {
815
906
  deep: true,
816
907
  handler() {
@@ -917,17 +1008,30 @@ export default {
917
1008
  // 初始化默认配置
918
1009
  initDefaultConfig() {
919
1010
  if (!this.currentReport) return
920
- if (this.currentReport.selectedDimension) {
921
- return
922
- }
923
1011
 
924
1012
  const allColumns = this.availableDimensions
925
1013
  const numericCols = this.availableMetrics
926
1014
 
927
- if (allColumns.length > 0 && !this.currentReport.selectedDimension) {
1015
+ // 分组统计模式:初始化分组维度
1016
+ if (
1017
+ this.currentReport.statType === 'group' &&
1018
+ !this.currentReport.selectedDimension &&
1019
+ allColumns.length > 0
1020
+ ) {
928
1021
  this.currentReport.selectedDimension = allColumns[0].prop
929
1022
  }
930
1023
 
1024
+ // 交叉统计模式:初始化行列维度
1025
+ if (this.currentReport.statType === 'cross') {
1026
+ if (!this.currentReport.rowDimension && allColumns.length > 0) {
1027
+ this.currentReport.rowDimension = allColumns[0].prop
1028
+ }
1029
+ if (!this.currentReport.columnDimension && allColumns.length > 1) {
1030
+ this.currentReport.columnDimension = allColumns[1].prop
1031
+ }
1032
+ }
1033
+
1034
+ // 初始化指标(仅当没有任何指标时)
931
1035
  if (numericCols.length > 0 && this.currentReport.metrics.length === 0) {
932
1036
  this.currentReport.metrics = [
933
1037
  {
@@ -948,12 +1052,21 @@ export default {
948
1052
  selectedDimension: null,
949
1053
  rowDimension: null,
950
1054
  columnDimension: null,
951
- metrics: []
1055
+ metrics: [],
1056
+ // 添加视图模式持久化
1057
+ viewMode: 'chart'
952
1058
  }
953
1059
  },
954
1060
 
955
1061
  // 迁移旧格式报表数据
956
1062
  migrateReportFormat(report) {
1063
+ if (!report) return this.getDefaultReport()
1064
+
1065
+ // 确保 statType 存在,默认为 'group'
1066
+ if (!report.statType) {
1067
+ report.statType = 'group'
1068
+ }
1069
+
957
1070
  if (!report.metrics) {
958
1071
  report.metrics = []
959
1072
  }
@@ -963,7 +1076,7 @@ export default {
963
1076
  if (typeof m === 'string') {
964
1077
  return { prop: m, aggregateType: report.aggregateType || 'sum' }
965
1078
  }
966
- if (m.key && !m.prop) {
1079
+ if (m && m.key && !m.prop) {
967
1080
  return { prop: m.key, aggregateType: m.aggregateType || 'sum' }
968
1081
  }
969
1082
  return m
@@ -981,6 +1094,14 @@ export default {
981
1094
  if (!report.selectedDimension) {
982
1095
  report.selectedDimension = null
983
1096
  }
1097
+ // 确保图表类型存在
1098
+ if (!report.chartType) {
1099
+ report.chartType = 'bar'
1100
+ }
1101
+ // 确保 viewMode 存在,交叉统计默认为 table,分组统计默认为 chart
1102
+ if (!report.viewMode) {
1103
+ report.viewMode = report.statType === 'cross' ? 'table' : 'chart'
1104
+ }
984
1105
  delete report.aggregateType
985
1106
  delete report.dimensions
986
1107
  return report
@@ -1051,7 +1172,15 @@ export default {
1051
1172
 
1052
1173
  // ========== MainToolbar 事件处理 ==========
1053
1174
  handleViewModeChange(val) {
1054
- this.viewMode = val
1175
+ // 将视图模式保存到报表配置中
1176
+ this.currentReport.viewMode = val
1177
+ this.saveToStorage()
1178
+ // 切换到图表视图时初始化图表
1179
+ if (val === 'chart') {
1180
+ this.$nextTick(() => {
1181
+ this.initChart()
1182
+ })
1183
+ }
1055
1184
  },
1056
1185
 
1057
1186
  handleRefresh() {
@@ -1143,13 +1272,17 @@ export default {
1143
1272
  handleStatTypeChange(type) {
1144
1273
  this.currentReport.statType = type
1145
1274
  if (type === 'cross') {
1146
- // 切换到交叉统计时清除分组维度
1275
+ // 切换到交叉统计时清除分组维度,并强制使用表格视图
1147
1276
  this.currentReport.selectedDimension = null
1148
- this.viewMode = 'table'
1277
+ this.currentReport.viewMode = 'table'
1149
1278
  } else {
1150
1279
  // 切换到分组统计时清除行列维度
1151
1280
  this.currentReport.rowDimension = null
1152
1281
  this.currentReport.columnDimension = null
1282
+ // 如果之前是表格视图,可以保持;否则默认图表视图
1283
+ if (!this.currentReport.viewMode || this.currentReport.viewMode === 'table') {
1284
+ this.currentReport.viewMode = 'chart'
1285
+ }
1153
1286
  }
1154
1287
  this.saveToStorage()
1155
1288
  },
@@ -1234,11 +1367,18 @@ export default {
1234
1367
  if (saved) {
1235
1368
  const configs = JSON.parse(saved)
1236
1369
  if (Array.isArray(configs) && configs.length > 0) {
1370
+ // 迁移格式并加载报表列表
1237
1371
  this.reportList = configs.map((report) => this.migrateReportFormat(report))
1372
+ // 确保 currentReportIndex 有效
1373
+ if (this.currentReportIndex >= this.reportList.length) {
1374
+ this.currentReportIndex = 0
1375
+ }
1238
1376
  }
1239
1377
  }
1240
1378
  } catch (e) {
1241
1379
  console.warn('Failed to load pivot configs from storage:', e)
1380
+ // 加载失败时清空报表列表
1381
+ this.reportList = []
1242
1382
  }
1243
1383
  },
1244
1384
 
@@ -213,11 +213,12 @@
213
213
 
214
214
  <script>
215
215
  import advancedQuery from './advancedQuery.vue'
216
+
217
+ import axios from '../../utils/axios.js'
216
218
  import ClAdvancedFilter from '../AdvancedFilter/index.vue'
217
- import ClInputSearch from '../InputSearch/index.vue'
218
- import ClDragList from '../DragList/index.vue'
219
219
  import ClDialog from '../Dialog/index.vue'
220
- import axios from '../../utils/axios.js'
220
+ import ClDragList from '../DragList/index.vue'
221
+ import ClInputSearch from '../InputSearch/index.vue'
221
222
 
222
223
  const VIEW_TYPE = { ADVANCED: '0', BASIC: '1' }
223
224
 
@@ -299,13 +300,21 @@ export default {
299
300
 
300
301
  getInitialSearchValue() {
301
302
  const obj = {}
302
- // 遍历 filterList,为每个 type 为 slot 且有 value 字段的项初始化值
303
- // 注意:跳过无 value 的 slot(如 startDate/endDate 多值绑定),这些由父组件在 slot 内部管理
304
- this.filterList.forEach((item) => {
305
- if (item.type === 'slot' && item.value) {
306
- obj[item.value] = undefined
307
- }
308
- })
303
+ // 为 slot 类型筛选项初始化值
304
+ this.filterList
305
+ .filter((item) => item.type === 'slot')
306
+ .forEach((item) => {
307
+ // 单值绑定
308
+ if (item.value) {
309
+ obj[item.value] = undefined
310
+ }
311
+ // 多值绑定(slotFields 数组)
312
+ if (item.slotFields?.length) {
313
+ item.slotFields.forEach((field) => {
314
+ obj[field] = undefined
315
+ })
316
+ }
317
+ })
309
318
  return obj
310
319
  },
311
320
  filterObj() {
@@ -411,27 +420,38 @@ export default {
411
420
  handleClear() {
412
421
  // 收集 required 项的当前值,清空时保留
413
422
  const preserved = {}
414
- this.filterList.forEach((item) => {
415
- if (!item.required) return
416
- if (item.type === 'daterange' || item.type === 'datetimerange' || item.type === 'monthrange') {
423
+ this.filterList
424
+ .filter((item) => item.required)
425
+ .forEach((item) => {
426
+ // 日期范围类型
417
427
  if (item.startDate && this.searchValue[item.startDate] !== undefined) {
418
428
  preserved[item.startDate] = this.searchValue[item.startDate]
419
429
  }
420
430
  if (item.endDate && this.searchValue[item.endDate] !== undefined) {
421
431
  preserved[item.endDate] = this.searchValue[item.endDate]
422
432
  }
423
- } else if (item.type === 'numberrange') {
433
+ // 数字范围类型
424
434
  if (item.startValue && this.searchValue[item.startValue] !== undefined) {
425
435
  preserved[item.startValue] = this.searchValue[item.startValue]
426
436
  }
427
437
  if (item.endValue && this.searchValue[item.endValue] !== undefined) {
428
438
  preserved[item.endValue] = this.searchValue[item.endValue]
429
439
  }
430
- } else if (item.value && this.searchValue[item.value] !== undefined) {
431
- preserved[item.value] = this.searchValue[item.value]
432
- }
433
- })
434
- this.searchValue = { ...this.getInitialSearchValue, ...preserved }
440
+ // 普通单值
441
+ if (item.value && this.searchValue[item.value] !== undefined) {
442
+ preserved[item.value] = this.searchValue[item.value]
443
+ }
444
+ // slot 多值绑定
445
+ if (item.slotFields?.length) {
446
+ item.slotFields.forEach((field) => {
447
+ if (this.searchValue[field] !== undefined) {
448
+ preserved[field] = this.searchValue[field]
449
+ }
450
+ })
451
+ }
452
+ })
453
+
454
+ this.searchValue = { ...this.initialValue, ...this.getInitialSearchValue, ...preserved }
435
455
  // 直接构建 payload,绕过 filterObj 避免 initialValue 重新注入
436
456
  this.$emit(
437
457
  'filter',