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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n20-common-lib",
3
- "version": "3.1.5",
3
+ "version": "3.1.6",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -74,6 +74,7 @@
74
74
  "devDependencies": {
75
75
  "@babel/plugin-proposal-optional-chaining": "^7.14.5",
76
76
  "@babel/plugin-transform-flow-comments": "^7.14.5",
77
+ "@trivago/prettier-plugin-sort-imports": "^4.3.0",
77
78
  "@vue/cli-plugin-babel": "~4.5.0",
78
79
  "@vue/cli-plugin-eslint": "~4.5.0",
79
80
  "@vue/cli-plugin-unit-jest": "^5.0.8",
@@ -33,6 +33,8 @@
33
33
  /* 左侧边栏 */
34
34
  .pivot-sidebar-left {
35
35
  width: 280px;
36
+ min-width: 280px;
37
+ flex-shrink: 0;
36
38
  background: #f2f3f5;
37
39
  border-right: 1px solid #e5e6eb;
38
40
  display: flex;
@@ -257,6 +259,7 @@
257
259
  /* 主内容区域 */
258
260
  .pivot-main {
259
261
  flex: 1;
262
+ min-width: 0;
260
263
  display: flex;
261
264
  flex-direction: column;
262
265
  position: relative;
@@ -484,6 +487,7 @@
484
487
  width: 100%;
485
488
  height: 100%;
486
489
  position: relative;
490
+ overflow: hidden;
487
491
  }
488
492
 
489
493
  .chart-wrapper {
@@ -528,81 +532,6 @@
528
532
  min-height: 0;
529
533
  }
530
534
 
531
- .table-container {
532
- width: 100%;
533
- height: 100%;
534
- overflow: auto;
535
- }
536
-
537
- .pivot-table {
538
- width: 100%;
539
- min-width: 769px;
540
- border-collapse: collapse;
541
- table-layout: fixed;
542
- font-size: 14px;
543
-
544
- th,
545
- td {
546
- padding: 10px 16px;
547
- border-right: 1px solid #e5e6eb;
548
- border-bottom: 1px solid #e5e6eb;
549
- height: 57px;
550
- box-sizing: border-box;
551
- }
552
-
553
- th {
554
- background: #f2f3f5;
555
- font-weight: 500;
556
- color: #1d2129;
557
- position: sticky;
558
- top: 0;
559
- z-index: 1;
560
- text-align: left;
561
- height: 40px;
562
- }
563
-
564
- th.metric-header {
565
- text-align: right;
566
- }
567
-
568
- td.dimension-cell {
569
- text-align: left;
570
- color: #1d2129;
571
- vertical-align: middle;
572
- }
573
-
574
- td.metric-cell {
575
- text-align: right;
576
- vertical-align: middle;
577
-
578
- .cell-content {
579
- display: flex;
580
- flex-direction: column;
581
- align-items: flex-end;
582
- justify-content: center;
583
- height: 100%;
584
- }
585
-
586
- .cell-value {
587
- font-family: 'Open Sans', sans-serif;
588
- font-size: 14px;
589
- line-height: 22px;
590
- color: #1d2129;
591
- }
592
-
593
- .cell-count {
594
- font-family: 'PingFang SC', sans-serif;
595
- font-size: 12px;
596
- line-height: 20px;
597
- color: #86909c;
598
- }
599
- }
600
-
601
- tbody tr:hover {
602
- background: #f7f8fa;
603
- }
604
- }
605
-
606
535
  .empty-state {
607
536
  width: 100%;
608
537
  height: 100%;
@@ -615,6 +544,8 @@
615
544
  /* 右侧边栏 */
616
545
  .pivot-sidebar-right {
617
546
  width: 280px;
547
+ min-width: 280px;
548
+ flex-shrink: 0;
618
549
  background: #fff;
619
550
  border-left: 1px solid #e5e6eb;
620
551
  display: flex;
@@ -1067,6 +998,8 @@
1067
998
  display: flex;
1068
999
  flex-direction: column;
1069
1000
  height: 100%;
1001
+ width: 100%;
1002
+ overflow: hidden;
1070
1003
  }
1071
1004
 
1072
1005
  .table-header {
@@ -1088,88 +1021,76 @@
1088
1021
  font-family: 'PingFang SC', sans-serif;
1089
1022
  }
1090
1023
 
1091
- /* 交叉统计表格样式 */
1092
- .cross-table-container {
1024
+ /* vxe-table 表格样式 */
1025
+ .table-container {
1093
1026
  flex: 1;
1094
- overflow: auto;
1095
- }
1096
-
1097
- .cross-pivot-table {
1027
+ min-height: 0;
1028
+ overflow: hidden;
1098
1029
  width: 100%;
1099
- min-width: 100%;
1100
- border-collapse: collapse;
1101
- table-layout: fixed;
1102
1030
  }
1103
1031
 
1104
- .cross-header-row th {
1105
- background: #f2f3f5;
1106
- border-right: 1px solid #e5e6eb;
1107
- border-bottom: 1px solid #e5e6eb;
1108
- padding: 9px 16px;
1109
- font-size: 14px;
1110
- font-weight: 500;
1111
- color: #1d2129;
1112
- text-align: center;
1113
- height: 40px;
1114
- box-sizing: border-box;
1115
- }
1032
+ .pivot-vxe-table {
1033
+ font-family: 'PingFang SC', sans-serif;
1116
1034
 
1117
- .cross-dimension-header {
1118
- width: 177px;
1119
- min-width: 177px;
1120
- text-align: left !important;
1121
- }
1035
+ /* 表头样式 */
1036
+ .vxe-table--header-wrapper {
1037
+ .vxe-header--column {
1038
+ background: #f2f3f5;
1039
+ font-weight: 500;
1040
+ color: #1d2129;
1041
+ font-size: 14px;
1042
+ border-right: 1px solid #e5e6eb;
1043
+ }
1122
1044
 
1123
- .cross-metric-header {
1124
- width: 148px;
1125
- min-width: 148px;
1126
- text-align: right !important;
1127
- }
1045
+ /* 多级表头样式 */
1046
+ .vxe-header--row {
1047
+ .vxe-header--column {
1048
+ border-bottom: 1px solid #e5e6eb;
1049
+ }
1050
+ }
1128
1051
 
1129
- .cross-data-row td {
1130
- padding: 10px 16px;
1131
- border-right: 1px solid #e5e6eb;
1132
- border-bottom: 1px solid #e5e6eb;
1133
- font-size: 14px;
1134
- color: #1d2129;
1135
- vertical-align: middle;
1136
- height: 57px;
1137
- box-sizing: border-box;
1138
- }
1052
+ /* 多级表头父级样式 */
1053
+ .vxe-header--column.col--group {
1054
+ background: #f7f8fa;
1055
+ text-align: center;
1056
+ border-bottom: 1px solid #e5e6eb;
1057
+ }
1058
+ }
1139
1059
 
1140
- .cross-dimension-cell {
1141
- background: #fff;
1142
- text-align: left !important;
1143
- width: 177px;
1144
- min-width: 177px;
1060
+ /* 单元格样式 */
1061
+ .vxe-body--column {
1062
+ font-size: 14px;
1063
+ color: #1d2129;
1064
+ border-right: 1px solid #e5e6eb;
1065
+ }
1066
+
1067
+ /* 悬停行样式 */
1068
+ .vxe-body--row.row--hover {
1069
+ background: #f7f8fa;
1070
+ }
1145
1071
  }
1146
1072
 
1147
- .cross-metric-cell {
1148
- background: #fff;
1149
- text-align: right !important;
1150
- width: 148px;
1151
- min-width: 148px;
1073
+ /* 指标单元格内容 */
1074
+ .metric-cell-content {
1075
+ display: flex;
1076
+ flex-direction: column;
1077
+ align-items: flex-end;
1078
+ justify-content: center;
1079
+ height: 100%;
1152
1080
  }
1153
1081
 
1154
- .cross-metric-value {
1155
- display: block;
1082
+ .metric-value {
1156
1083
  font-family: 'Open Sans', sans-serif;
1157
1084
  font-size: 14px;
1158
- font-weight: 400;
1159
- color: #1d2129;
1160
1085
  line-height: 22px;
1086
+ color: #1d2129;
1161
1087
  }
1162
1088
 
1163
- .cross-metric-count {
1164
- display: block;
1089
+ .metric-count {
1165
1090
  font-family: 'PingFang SC', sans-serif;
1166
1091
  font-size: 12px;
1167
- color: #86909c;
1168
1092
  line-height: 20px;
1169
- }
1170
-
1171
- .cross-data-row:hover td {
1172
- background: #f7f8fa;
1093
+ color: #86909c;
1173
1094
  }
1174
1095
 
1175
1096
  /* 分页组件 */
@@ -109,13 +109,15 @@
109
109
  <script>
110
110
  // import formItemInput from './form-item-input.vue'
111
111
  import XEUtils from 'xe-utils'
112
- import axios from '../../utils/axios.js'
113
- import ClDialog from '../Dialog/index.vue'
114
- import InputSearch from '../InputSearch/index.vue'
112
+
115
113
  import filterItem from './filterItem.vue'
116
114
  import formItemRender from './formItemRender.vue'
117
115
  import { getOnlyKey } from './utils'
118
116
 
117
+ import axios from '../../utils/axios.js'
118
+ import ClDialog from '../Dialog/index.vue'
119
+ import InputSearch from '../InputSearch/index.vue'
120
+
119
121
  const prefixCls = 'n20-advanced-filter'
120
122
  export default {
121
123
  name: 'AdvancedFilter',
@@ -334,6 +336,9 @@ export default {
334
336
  if (rangeTypes.includes(item.type)) {
335
337
  return hasRange(item.startDate, item.endDate) ? this.prefixCls + '-active' : ''
336
338
  }
339
+ if (item.slotFields && item.slotFields.length > 0) {
340
+ return item.slotFields.some((field) => hasValue(this.model[field])) ? this.prefixCls + '-active' : ''
341
+ }
337
342
  return hasValue(this.model[item.value]) ? this.prefixCls + '-active' : ''
338
343
  },
339
344
  handleClose(item) {
@@ -397,34 +402,41 @@ export default {
397
402
  this.$emit('clear')
398
403
  },
399
404
  setModelData(list) {
400
- let arr = []
405
+ // 收集当前选中列表中的所有字段
406
+ const selectedFields = new Set()
401
407
  list.forEach((res) => {
402
- // 收集普通字段的 value
403
- if (res.value) {
404
- arr.push(res.value)
405
- }
406
- // 收集日期范围类型的字段
407
- if (res.startDate && res.endDate) {
408
- arr.push(res.startDate)
409
- arr.push(res.endDate)
410
- }
411
- // 收集数字范围类型的字段
412
- if (res.startValue && res.endValue) {
413
- arr.push(res.startValue)
414
- arr.push(res.endValue)
415
- }
416
- // 收集插槽类型可能的自定义字段(slotFields 数组)
408
+ if (res.value) selectedFields.add(res.value)
409
+ if (res.startDate) selectedFields.add(res.startDate)
410
+ if (res.endDate) selectedFields.add(res.endDate)
411
+ if (res.startValue) selectedFields.add(res.startValue)
412
+ if (res.endValue) selectedFields.add(res.endValue)
417
413
  if (res.type === 'slot' && Array.isArray(res?.slotFields)) {
418
414
  res?.slotFields.forEach((field) => {
419
- if (field && !arr.includes(field)) {
420
- arr.push(field)
421
- }
415
+ if (field) selectedFields.add(field)
422
416
  })
423
417
  }
424
418
  })
419
+
420
+ // 收集 filterList 中定义的所有字段
421
+ const allDefinedFields = new Set()
422
+ this.filterList.forEach((item) => {
423
+ if (item.value) allDefinedFields.add(item.value)
424
+ if (item.startDate) allDefinedFields.add(item.startDate)
425
+ if (item.endDate) allDefinedFields.add(item.endDate)
426
+ if (item.startValue) allDefinedFields.add(item.startValue)
427
+ if (item.endValue) allDefinedFields.add(item.endValue)
428
+ if (item.type === 'slot' && Array.isArray(item?.slotFields)) {
429
+ item?.slotFields.forEach((field) => {
430
+ if (field) allDefinedFields.add(field)
431
+ })
432
+ }
433
+ })
434
+
435
+ // 只清除:在 filterList 中定义但不在选中列表中的字段
425
436
  for (const key in this.model) {
426
437
  if (Object.hasOwnProperty.call(this.model, key)) {
427
- if (!arr.includes(key)) {
438
+ // 仅当该字段在 filterList 中定义且未被选中时才清除
439
+ if (allDefinedFields.has(key) && !selectedFields.has(key)) {
428
440
  delete this.model[key]
429
441
  }
430
442
  }
@@ -2,10 +2,6 @@
2
2
  <!-- 图表视图 -->
3
3
  <div class="chart-view">
4
4
  <div v-if="hasData" class="chart-wrapper">
5
- <div class="chart-header">
6
- <div class="chart-title">数据概览</div>
7
- <div class="chart-desc">按维度「{{ dimensionLabel }}」聚合后的结果,共 {{ dataCount }} 条</div>
8
- </div>
9
5
  <div v-if="unitLabel" class="chart-unit-label">{{ unitLabel }}</div>
10
6
  <div ref="chartContainer" class="chart-container"></div>
11
7
  </div>
@@ -16,8 +12,8 @@
16
12
  </template>
17
13
 
18
14
  <script>
19
- import Empty from '../Empty/index.vue'
20
15
  import importG from '../../utils/importGlobal.js'
16
+ import Empty from '../Empty/index.vue'
21
17
 
22
18
  export default {
23
19
  name: 'ChartView',
@@ -106,4 +102,4 @@ export default {
106
102
  this.destroyChart()
107
103
  }
108
104
  }
109
- </script>
105
+ </script>
@@ -3,96 +3,96 @@
3
3
  <div class="table-view">
4
4
  <div v-if="hasData" class="table-wrapper">
5
5
  <!-- 数据概览头部 -->
6
- <div class="table-header">
6
+ <!-- <div class="table-header">
7
7
  <div class="table-title">数据概览</div>
8
- <div class="table-desc">按维度「{{ dimensionLabel }}」聚合后的结果,共 {{ dataCount }} 条</div>
9
- </div>
10
-
11
- <!-- 分组统计表格 -->
12
- <template v-if="!isCrossMode">
13
- <div class="table-container">
14
- <table class="pivot-table">
15
- <thead>
16
- <tr>
17
- <th
18
- v-for="col in tableColumns"
19
- :key="col.key"
20
- :class="col.type === 'metric' ? 'metric-header' : 'dimension-header'"
21
- :style="col.width ? { width: col.width } : null"
22
- >
23
- {{ col.title }}
24
- </th>
25
- </tr>
26
- </thead>
27
- <tbody>
28
- <tr v-for="(row, index) in tableData" :key="index">
29
- <td
30
- v-for="col in tableColumns"
31
- :key="col.key"
32
- :class="col.type === 'metric' ? 'metric-cell' : 'dimension-cell'"
33
- :style="col.width ? { width: col.width } : null"
34
- >
35
- <template v-if="col.type === 'metric'">
36
- <div class="cell-content">
37
- <span v-if="col.aggregateType === 'count'" class="cell-count"
38
- >{{ formatCount(row._groupCount) }} 笔</span
39
- >
40
- <span v-else class="cell-value">{{ formatNumber(row[col.key]) }}</span>
41
- </div>
42
- </template>
43
- <template v-else>
44
- {{ row[col.key] }}
45
- </template>
46
- </td>
47
- </tr>
48
- </tbody>
49
- </table>
8
+ <div class="table-desc">
9
+ <template v-if="isCrossMode">
10
+ 按行维度「{{ rowDimensionLabel }}」、列维度「{{ columnDimensionLabel }}」交叉分析后的结果,共 {{ dataCount }} 条
11
+ </template>
12
+ <template v-else>
13
+ 按维度「{{ dimensionLabel }}」聚合后的结果,共 {{ dataCount }} 条
14
+ </template>
50
15
  </div>
51
- </template>
16
+ </div> -->
52
17
 
53
- <!-- 交叉统计表格 -->
54
- <template v-else>
55
- <div class="cross-table-container">
56
- <table class="cross-pivot-table">
57
- <thead>
58
- <!-- 主表头行 -->
59
- <tr class="cross-header-row">
60
- <!-- 显示第一个维度列 -->
61
- <th class="cross-dimension-header">
62
- {{ crossTableColumns.find((c) => c.type === 'dimension')?.title }}
63
- </th>
64
- <!-- 指标列 -->
65
- <th
66
- v-for="col in crossTableColumns.filter((c) => c.type === 'metric')"
67
- :key="'header-' + col.key"
68
- class="cross-metric-header"
69
- >
70
- {{ col.title }}
71
- </th>
72
- </tr>
73
- </thead>
74
- <tbody>
75
- <tr v-for="(row, index) in tableData" :key="index" class="cross-data-row">
76
- <!-- 第一个维度值 -->
77
- <td class="cross-dimension-cell">
78
- {{ row[crossTableColumns.find((c) => c.type === 'dimension')?.key] }}
79
- </td>
80
- <!-- 指标列 -->
81
- <td
82
- v-for="col in crossTableColumns.filter((c) => c.type === 'metric')"
83
- :key="'cell-' + row._groupKey + '-' + col.key"
84
- class="cross-metric-cell"
85
- >
86
- <span v-if="col.aggregateType === 'count'" class="cross-metric-count"
87
- >{{ formatCount(row._groupCount) }} 笔</span
88
- >
89
- <span v-else class="cross-metric-value">{{ formatNumber(row[col.key]) }}</span>
90
- </td>
91
- </tr>
92
- </tbody>
93
- </table>
94
- </div>
95
- </template>
18
+ <!-- 表格容器 -->
19
+ <div class="table-container">
20
+ <vxe-table
21
+ ref="vxeTable"
22
+ :data="tableData"
23
+ :height="'100%'"
24
+ :scroll-y="{ enabled: true, gt: 0 }"
25
+ :scroll-x="{ enabled: true, gt: 0 }"
26
+ :row-config="{ isHover: true }"
27
+ :column-config="{ resizable: true }"
28
+ show-header-overflow
29
+ show-overflow
30
+ auto-resize
31
+ class="pivot-vxe-table"
32
+ >
33
+ <!-- 动态列渲染 - 支持多级表头 -->
34
+ <template v-for="col in vxeColumns">
35
+ <!-- 分组列(多级表头) -->
36
+ <vxe-colgroup
37
+ v-if="col.children && col.children.length > 0"
38
+ :key="col.key"
39
+ :title="col.title"
40
+ :header-align="col.headerAlign || 'center'"
41
+ >
42
+ <vxe-column
43
+ v-for="childCol in col.children"
44
+ :key="childCol.key"
45
+ :field="childCol.key"
46
+ :title="childCol.title"
47
+ :min-width="childCol.minWidth"
48
+ :align="childCol.align"
49
+ :header-align="childCol.headerAlign"
50
+ >
51
+ <template #default="{ row }">
52
+ <div class="metric-cell-content">
53
+ <span v-if="childCol.aggregateType === 'count'" class="metric-count">
54
+ {{ formatCount(row._groupCount) }}
55
+ </span>
56
+ <span v-else class="metric-value">{{ formatNumber(row[childCol.key]) }}</span>
57
+ </div>
58
+ </template>
59
+ </vxe-column>
60
+ </vxe-colgroup>
61
+
62
+ <!-- 普通列 -->
63
+ <vxe-column
64
+ v-else
65
+ :key="col.field"
66
+ :field="col.field"
67
+ :title="col.title"
68
+ :min-width="col.minWidth"
69
+ :align="col.align"
70
+ :header-align="col.headerAlign"
71
+ >
72
+ <template #default="{ row }">
73
+ <!-- 维度列:直接显示值 -->
74
+ <template v-if="col.type === 'dimension'">
75
+ {{ row[col.field] }}
76
+ </template>
77
+ <!-- 指标列:根据聚合类型显示 -->
78
+ <template v-else-if="col.type === 'metric'">
79
+ <div class="metric-cell-content">
80
+ <span v-if="col.aggregateType === 'count'" class="metric-count">
81
+ {{ formatCount(row._groupCount) }} 笔
82
+ </span>
83
+ <span v-else class="metric-value">{{ formatNumber(row[col.field]) }}</span>
84
+ </div>
85
+ </template>
86
+ </template>
87
+ </vxe-column>
88
+ </template>
89
+
90
+ <!-- 空状态 -->
91
+ <template #empty>
92
+ <Empty type="noData2" :height="200" :width="200" content="暂无数据" />
93
+ </template>
94
+ </vxe-table>
95
+ </div>
96
96
 
97
97
  <!-- 分页组件 -->
98
98
  <div class="cross-pagination">
@@ -165,6 +165,16 @@ export default {
165
165
  type: Boolean,
166
166
  default: false
167
167
  },
168
+ // 行维度标签(交叉统计用)
169
+ rowDimensionLabel: {
170
+ type: String,
171
+ default: ''
172
+ },
173
+ // 列维度标签(交叉统计用)
174
+ columnDimensionLabel: {
175
+ type: String,
176
+ default: ''
177
+ },
168
178
  // 表格列定义
169
179
  tableColumns: {
170
180
  type: Array,
@@ -211,6 +221,67 @@ export default {
211
221
  showPageSizeDropdown: false
212
222
  }
213
223
  },
224
+ computed: {
225
+ // 转换为 vxe-table 列格式(支持多级表头)
226
+ vxeColumns() {
227
+ // 计算列最小宽度的辅助函数
228
+ const calcMinWidth = (title, isDimension) => {
229
+ // 基础宽度:标题字符数 * 14px + 左右 padding 32px
230
+ const titleWidth = (title?.length || 0) * 14 + 32
231
+ // 维度列最小 120px,指标列最小 100px
232
+ const baseMin = isDimension ? 120 : 100
233
+ return Math.max(titleWidth, baseMin)
234
+ }
235
+
236
+ if (this.isCrossMode) {
237
+ return this.crossTableColumns.map((col) => {
238
+ // 分组列(多级表头)
239
+ if (col.type === 'group' && col.children) {
240
+ return {
241
+ key: col.key,
242
+ title: col.title,
243
+ type: 'group',
244
+ headerAlign: 'center',
245
+ // 递归处理子列
246
+ children: col.children.map((childCol) => ({
247
+ field: childCol.key,
248
+ title: childCol.title,
249
+ minWidth: calcMinWidth(childCol.title, false),
250
+ align: 'right',
251
+ headerAlign: 'right',
252
+ type: childCol.type,
253
+ aggregateType: childCol.aggregateType
254
+ }))
255
+ }
256
+ }
257
+ // 普通列
258
+ const isDimension = col.type === 'dimension'
259
+ return {
260
+ field: col.key,
261
+ title: col.title,
262
+ minWidth: calcMinWidth(col.title, isDimension),
263
+ align: isDimension ? 'left' : 'right',
264
+ headerAlign: isDimension ? 'left' : 'right',
265
+ type: col.type,
266
+ aggregateType: col.aggregateType
267
+ }
268
+ })
269
+ } else {
270
+ return this.tableColumns.map((col) => {
271
+ const isDimension = col.type === 'dimension'
272
+ return {
273
+ field: col.key,
274
+ title: col.title,
275
+ minWidth: calcMinWidth(col.title, isDimension),
276
+ align: isDimension ? 'left' : 'right',
277
+ headerAlign: isDimension ? 'left' : 'right',
278
+ type: col.type,
279
+ aggregateType: col.aggregateType
280
+ }
281
+ })
282
+ }
283
+ }
284
+ },
214
285
  methods: {
215
286
  // 格式化数值显示
216
287
  formatNumber(val) {
@@ -257,6 +328,13 @@ export default {
257
328
  // 关闭下拉菜单(供外部调用)
258
329
  closeDropdown() {
259
330
  this.showPageSizeDropdown = false
331
+ },
332
+
333
+ // 刷新表格(供外部调用)
334
+ refreshTable() {
335
+ if (this.$refs.vxeTable) {
336
+ this.$refs.vxeTable.refreshColumn()
337
+ }
260
338
  }
261
339
  }
262
340
  }