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.
- package/package.json +2 -1
- package/src/assets/css/_coreLib.scss +1 -0
- package/src/assets/css/pivot.scss +1251 -0
- package/src/components/AdvancedFilter/index.vue +59 -19
- package/src/components/AdvancedFilter/utils.js +0 -8
- package/src/components/Pivot/CLAUDE.md +117 -0
- package/src/components/Pivot/ChartView.vue +105 -0
- package/src/components/Pivot/ConfigSidebar.vue +518 -0
- package/src/components/Pivot/MainToolbar.vue +160 -0
- package/src/components/Pivot/ReportSidebar.vue +172 -0
- package/src/components/Pivot/TableView.vue +341 -0
- package/src/components/Pivot/index.vue +1026 -1462
- package/src/components/Pivot//344/275/277/347/224/250/350/257/264/346/230/216.md +240 -0
- package/src/components/Pivot//350/257/264/346/230/216/346/226/207/346/241/243.md +171 -0
- package/src/components/ProFilterView/index.vue +39 -19
- package/src/components/v3/TablePro/index.vue +3 -5
- package/src/index.js +93 -91
- package/style/index.css +1 -1
- package/theme/blue.css +1 -1
- package/theme/cctcRed.css +1 -1
- package/theme/green.css +1 -1
- package/theme/lightBlue.css +1 -1
- package/theme/orange.css +1 -1
- package/theme/purple.css +1 -1
- package/theme/red.css +1 -1
- package/theme/yellow.css +1 -1
|
@@ -109,12 +109,14 @@
|
|
|
109
109
|
<script>
|
|
110
110
|
// import formItemInput from './form-item-input.vue'
|
|
111
111
|
import XEUtils from 'xe-utils'
|
|
112
|
-
|
|
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
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
405
|
+
// 收集当前选中列表中的所有字段
|
|
406
|
+
const selectedFields = new Set()
|
|
386
407
|
list.forEach((res) => {
|
|
387
|
-
if (res.value)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
|
527
|
-
if (Object.hasOwnProperty.call(
|
|
528
|
-
const data =
|
|
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>
|