n20-common-lib 3.1.4 → 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.
@@ -109,12 +109,14 @@
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 InputSearch from '../InputSearch/index.vue'
112
+
114
113
  import filterItem from './filterItem.vue'
115
114
  import formItemRender from './formItemRender.vue'
116
- import { getOnlyKey, getOptionsMap, setOptionsMap } from './utils'
115
+ import { getOnlyKey } from './utils'
116
+
117
+ import axios from '../../utils/axios.js'
117
118
  import ClDialog from '../Dialog/index.vue'
119
+ import InputSearch from '../InputSearch/index.vue'
118
120
 
119
121
  const prefixCls = 'n20-advanced-filter'
120
122
  export default {
@@ -194,7 +196,8 @@ export default {
194
196
  pageNo: this.filterId,
195
197
  name: ''
196
198
  },
197
- stVisible: false
199
+ stVisible: false,
200
+ optionsMap: {}
198
201
  }
199
202
  },
200
203
  computed: {
@@ -247,6 +250,9 @@ export default {
247
250
  this.getFilterList()
248
251
  this.getDefaultCheck()
249
252
  },
253
+ beforeDestroy() {
254
+ this.optionsMap = {}
255
+ },
250
256
  methods: {
251
257
  // 保存视图
252
258
  saveSt() {
@@ -295,7 +301,7 @@ export default {
295
301
  return getOnlyKey(this.onlyKey, data)
296
302
  },
297
303
  setOptions(id, opts) {
298
- setOptionsMap(id, opts)
304
+ this.optionsMap[id] = opts
299
305
  this.setChoices(id, opts)
300
306
  },
301
307
  setChoices(id, data) {
@@ -330,9 +336,17 @@ export default {
330
336
  if (rangeTypes.includes(item.type)) {
331
337
  return hasRange(item.startDate, item.endDate) ? this.prefixCls + '-active' : ''
332
338
  }
339
+ if (item.slotFields && item.slotFields.length > 0) {
340
+ return item.slotFields.some((field) => hasValue(this.model[field])) ? this.prefixCls + '-active' : ''
341
+ }
333
342
  return hasValue(this.model[item.value]) ? this.prefixCls + '-active' : ''
334
343
  },
335
344
  handleClose(item) {
345
+ const key = item[this.onlyKey] || item.value
346
+ if (this.optionsMap[key]) {
347
+ delete this.optionsMap[key]
348
+ }
349
+
336
350
  switch (item.type) {
337
351
  case 'select':
338
352
  this.$set(this.model, item.value, item.multiple ? [] : null)
@@ -359,12 +373,18 @@ export default {
359
373
  break
360
374
  case 'daterange':
361
375
  case 'datetimerange':
362
- case ' monthrange':
376
+ case 'monthrange':
363
377
  this.$set(this.model, item.startDate, null)
364
378
  this.$set(this.model, item.endDate, null)
365
379
  break
366
380
  case 'slot': {
367
381
  this.$set(this.model, item.value, null)
382
+ // 清空 slotFields 中定义的所有字段
383
+ if (item.slotFields?.length) {
384
+ item.slotFields.forEach((field) => {
385
+ this.$set(this.model, field, null)
386
+ })
387
+ }
368
388
  break
369
389
  }
370
390
  }
@@ -382,21 +402,41 @@ export default {
382
402
  this.$emit('clear')
383
403
  },
384
404
  setModelData(list) {
385
- let arr = []
405
+ // 收集当前选中列表中的所有字段
406
+ const selectedFields = new Set()
386
407
  list.forEach((res) => {
387
- if (res.value) {
388
- arr.push(res.value)
389
- } else if (res.startDate && res.endDate) {
390
- arr.push(res.startDate)
391
- arr.push(res.endDate)
392
- } else if (res.startValue && res.endValue) {
393
- arr.push(res.startValue)
394
- arr.push(res.endValue)
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)
413
+ if (res.type === 'slot' && Array.isArray(res?.slotFields)) {
414
+ res?.slotFields.forEach((field) => {
415
+ if (field) selectedFields.add(field)
416
+ })
395
417
  }
396
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 中定义但不在选中列表中的字段
397
436
  for (const key in this.model) {
398
437
  if (Object.hasOwnProperty.call(this.model, key)) {
399
- if (!arr.includes(key)) {
438
+ // 仅当该字段在 filterList 中定义且未被选中时才清除
439
+ if (allDefinedFields.has(key) && !selectedFields.has(key)) {
400
440
  delete this.model[key]
401
441
  }
402
442
  }
@@ -523,9 +563,9 @@ export default {
523
563
  this.mackData(_data)
524
564
  })
525
565
  .finally(() => {
526
- for (const key in getOptionsMap()) {
527
- if (Object.hasOwnProperty.call(getOptionsMap(), key)) {
528
- const data = getOptionsMap()[key]
566
+ for (const key in this.optionsMap) {
567
+ if (Object.hasOwnProperty.call(this.optionsMap, key)) {
568
+ const data = this.optionsMap[key]
529
569
  this.setChoices(key, data)
530
570
  }
531
571
  }
@@ -1,10 +1,2 @@
1
1
  export const getOnlyKey = (key, data) =>
2
2
  key ? data[key] : Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
3
- let options = {}
4
- export const setOptionsMap = (key, value) => {
5
- options[key] = value
6
- }
7
-
8
- export const getOptionsMap = () => {
9
- return options
10
- }
@@ -0,0 +1,117 @@
1
+ # Pivot 数据透视组件
2
+
3
+ ## 组件作用描述
4
+
5
+ Pivot 是一个基于 Vue 2.6 和 vxe-table 的数据透视组件,通过 Dialog 弹窗呈现。核心功能是将外部传入的表格数据进行多维度聚合分析,支持分组统计和交叉统计两种模式,可视化方式包括图表视图和表格视图。
6
+
7
+ ## 依赖说明
8
+
9
+ | 依赖 | 版本 | 用途 |
10
+ | ---------- | ---- | ------------------------------ |
11
+ | vxe-table | \* | 表格核心,支持大数据虚拟滚动 |
12
+ | xe-utils | \* | vxe-table 工具库,数据聚合计算 |
13
+ | echarts | \* | 图表渲染(仅分组统计模式可用) |
14
+ | Dialog | 内置 | 弹窗容器 |
15
+ | ViewToggle | 内置 | 视图切换控件 |
16
+
17
+ ## Props 接口
18
+
19
+ ```typescript
20
+ {
21
+ visible: Boolean // 控制弹窗显示,默认 false
22
+ dataSource: Array // 数据源,外部传入的原始表格数据
23
+ columns: Array // 列定义,格式: { key, label, type }
24
+ dimensionsField: Array // 维度字段,未指定默认columns中string类型的字段
25
+ metricsField: Array // 聚合计算字段,未指定默认columns中number类型的字段
26
+ }
27
+ ```
28
+
29
+ ### columns 列定义
30
+
31
+ ```javascript
32
+ // 维度字段 typeof dataSource[index].region === 'string'
33
+ { prop: 'region', label: '地区' }
34
+ // 指标字段 typeof dataSource[index].amount === 'number'
35
+ { prop: 'amount', label: '金额' }
36
+ ```
37
+
38
+ > 分组维度 (dimensions) 和计算指标 (metrics) 的可选项由外部通过 `columns` 配置传入,组件根据 dataSource 数据源中对应的 columns 中的 prop 自动区分:
39
+ >
40
+ > - `'string' | 'date'` → 作为维度字段
41
+ > - `'number'` → 作为指标字段(支持聚合)
42
+
43
+ ## 核心功能
44
+
45
+ ### 1. 统计模式
46
+
47
+ | 模式 | 说明 | 可视化 |
48
+ | ---------------- | -------------------------------- | ------------------- |
49
+ | 分组统计 (group) | 按维度字段分组,聚合计算每个指标 | 图表视图 + 表格视图 |
50
+ | 交叉统计 (cross) | 行列交叉分析 | 仅表格视图 |
51
+
52
+ ### 2. 聚合方式
53
+
54
+ 每项指标支持独立设置聚合类型:
55
+
56
+ | 类型 | 说明 |
57
+ | ----- | ------ |
58
+ | sum | 求和 |
59
+ | count | 计数 |
60
+ | avg | 平均值 |
61
+ | max | 最大值 |
62
+ | min | 最小值 |
63
+
64
+ ### 3. 图表类型(仅分组统计模式)
65
+
66
+ - 柱状图
67
+ - 折线图
68
+ - 饼图
69
+ - 条形图
70
+
71
+ > 交叉统计模式仅支持表格展示,不渲染图表。
72
+
73
+ ### 4. 多报表管理
74
+
75
+ - 新建报表
76
+ - 重命名报表
77
+ - 删除报表
78
+ - 报表配置本地持久化 (localStorage)
79
+
80
+ ## 布局结构
81
+
82
+ ```
83
+ ┌──────────────────────────────────────────────────────────────┐
84
+ │ Dialog │
85
+ │ width: 1360px (固定) │
86
+ ├──────────────┬─────────────────────────────┬─────────────────┤
87
+ │ 左侧边栏 │ 主内容区域 │ 右侧边栏 │
88
+ │ 280px │ flex: 1 │ 280px │
89
+ │ │ │ │
90
+ │ 我的报表清单 │ 工具栏 (标题/统计信息) │ 维度配置 │
91
+ │ - 报表1 ├─────────────────────────────┤ │
92
+ │ - 报表2 │ │ 报表标题 │
93
+ │ - 报表3 │ 图表视图 / 表格视图 │ 统计类型 │
94
+ │ │ │ 展示形式 │
95
+ │ │ │ 分组维度 │
96
+ │ │ │ 计算指标 │
97
+ └──────────────┴─────────────────────────────┴─────────────────┘
98
+ ```
99
+
100
+ ## 开发进度
101
+
102
+ | 功能 | 状态 | 说明 |
103
+ | ---------- | --------------- | -------------------------- |
104
+ | 分组统计 | ✅ 完成,待优化 | 支持图表和表格双视图 |
105
+ | 交叉统计 | ✅ 完成,待优化 | 仅表格视图,无图表 |
106
+ | 图表渲染 | ✅ 完成,待优化 | echarts 实现,分组模式可用 |
107
+ | 虚拟滚动 | ⬜ 待实现 | 依赖 vxe-table,大数据优化 |
108
+ | 指标聚合 | ✅ 完成,待优化 | 支持每指标独立设置聚合类型 |
109
+ | 多报表管理 | ✅ 完成,待优化 | CRUD + localStorage 持久化 |
110
+ | 分页 | ✅ 完成,待优化 | 表格统计支持 |
111
+
112
+ ## 注意事项
113
+
114
+ 1. **组件宽度固定为 1360px**,不接受动态调整
115
+ 2. **dimensions 和 metrics 由外部传入**,如未配置,组件根据 columns 配置自动派生可用选项
116
+ 3. **交叉统计模式不渲染图表**,切换到该模式时会自动切换为表格视图
117
+ 4. **虚拟滚动待实现**,当前版本在大数据量场景下可能存在性能问题
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <!-- 图表视图 -->
3
+ <div class="chart-view">
4
+ <div v-if="hasData" class="chart-wrapper">
5
+ <div v-if="unitLabel" class="chart-unit-label">{{ unitLabel }}</div>
6
+ <div ref="chartContainer" class="chart-container"></div>
7
+ </div>
8
+ <div v-else class="empty-state">
9
+ <Empty type="noData2" :height="200" :width="200" content="暂无数据" />
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script>
15
+ import importG from '../../utils/importGlobal.js'
16
+ import Empty from '../Empty/index.vue'
17
+
18
+ export default {
19
+ name: 'ChartView',
20
+ components: {
21
+ Empty
22
+ },
23
+ props: {
24
+ // 是否有数据
25
+ hasData: {
26
+ type: Boolean,
27
+ default: false
28
+ },
29
+ // 数据条数
30
+ dataCount: {
31
+ type: Number,
32
+ default: 0
33
+ },
34
+ // 维度标签
35
+ dimensionLabel: {
36
+ type: String,
37
+ default: ''
38
+ },
39
+ // 单位标签
40
+ unitLabel: {
41
+ type: String,
42
+ default: ''
43
+ },
44
+ // 图表配置
45
+ chartOption: {
46
+ type: Object,
47
+ default: () => ({})
48
+ }
49
+ },
50
+ data() {
51
+ return {
52
+ chartInstance: null
53
+ }
54
+ },
55
+ watch: {
56
+ // 监听图表配置变化
57
+ chartOption: {
58
+ deep: true,
59
+ handler() {
60
+ if (this.chartInstance) {
61
+ this.chartInstance.setOption(this.chartOption, true)
62
+ }
63
+ }
64
+ }
65
+ },
66
+ methods: {
67
+ // 初始化图表
68
+ async initChart() {
69
+ if (!this.$refs.chartContainer) return
70
+ if (!this.hasData) return
71
+
72
+ const echarts = await importG('echarts', () => import(/*webpackChunkName "echarts"*/ 'echarts'))
73
+
74
+ if (!this.chartInstance) {
75
+ this.chartInstance = echarts.init(this.$refs.chartContainer)
76
+ }
77
+
78
+ if (this.chartOption) {
79
+ this.chartInstance.setOption(this.chartOption, true)
80
+ }
81
+ },
82
+
83
+ // 调整图表大小
84
+ resizeChart() {
85
+ if (this.chartInstance) {
86
+ this.chartInstance.resize()
87
+ }
88
+ },
89
+
90
+ // 销毁图表
91
+ destroyChart() {
92
+ if (this.chartInstance) {
93
+ this.chartInstance.dispose()
94
+ this.chartInstance = null
95
+ }
96
+ }
97
+ },
98
+ mounted() {
99
+ this.initChart()
100
+ },
101
+ beforeDestroy() {
102
+ this.destroyChart()
103
+ }
104
+ }
105
+ </script>