产品需求:
产品需求希望统计报表改为自定义报表,并且内置数据的配置也是根据自定义配置而来。所以梳理下是如何开发,并且实现的,自定义报表可分为两大功能,报表配置和视图库,重点说下视图库。 视图库可分为为7个类型,分别是列表、指标、趋势图、组合图、柱状图、TOP图、环形图,数据是通过度量和维度生成的。
产品展示
前端开发:
设计报表配置界面:
使用Vue组件创建一个用户界面,允许用户选择报表的字段、排序和过滤条件等,因为涉及筛选、排序、列表动态列设置、echart图表的视图变化,数据变化都是前端来控制的。
管理配置数据:
在Vue实例的数据模型中定义报表配置对象,用于存储用户的配置选择。考虑到应用环境不同,此处数据生成,采用触发方式,而不是watch监听触发,各个类型图表配置都不相同,配置存储采用json字符串方式传给后端。
处理配置变化:
监听用户界面上的变化,并相应地更新配置数据,echart图表修改配置,然后视图变化,此处需要注意,配置echart图表的option不能放在vue生命周期data中,不然修改配置是不能变化的。而且涉及echart图表的重绘问题(控制台报错),所以最好更新当前组件。
生成报表:
根据用户的配置数据,使用计算属性或whatch监听等方法来改变视图变化,此处数据生成,采用触发方式,因为要考虑不同场景的应用。
展示报表结果:
将生成的报表数据绑定到报表展示组件上,如表格或图表,同时要考虑视图可缩放大小,在编写时要做好自适应。
具体步骤:
A页面布局:
1、可分为左中(上下)右,公共区域和类型区域
2、左侧为维度和度量选择区域(公共区域)
3、右侧为各个类型配置区域
4、中上为类型和所选维度度量区域(公共区域)
5、中下为视图预览区域
B文件划分:
因为不同类型的视图和配置功能大为不同,为了代码的可读、维护性和降低耦合性,此处前端分别写了7个配置页面和7个视 图页面 RightConfig.vue 和 ResultPreview.vue
c代码实现:
1、因为涉及筛选、排序、列表动态列设置、echart图表的视图变化,数据变化都是前端来控制的。
2、echart图表修改配置,然后视图变化,此处需要注意,配置echart图表的option不能放在vue生命周期data中,不然修 改配置是不能变化的。而且涉及echart图表的重绘问题(控制台报错),所以最好更新当前组件。
3、考虑到应用环境不同,此处数据生成,采用触发方式,而不是watch监听触发。
4、各个类型图表配置都不相同,配置存储采用json字符串方式。
5、每个图例的配置参数不同,需要单独写配置页面和生成页面。
注意事项:
1、为满足自定义报表功能需求,此次开发修改了列表和echart组件,但不会影响其他地方的使用。
2、因为数据源dataSource和根据维度、度量返回的结果数据格式是一样的,需要前端处理数据格式以应对每个类型需要的数据格式是关键。
3、修改配置让echart图表丝滑变化也是重要用户体验,包括自适应每个卡片宽高,前端需要注意。
代码举例:
// 环形图配置参数
rightConfigForm: {
viewType: 7, // 预览类型(环形图)
viewUuid: null, // 查看uuid
cardTitleFlag: true, // 卡片标题开关
cardTitle: null, // 卡片标题
remarkFlag: false, // 备注开关
remark: null, // 备注内容
rightMoreFlag: false, // 更多开关
toolTipFlag: true, // 显示工具提示
legendFlag: true, // 显示图例
colorList: [], // 颜色选择
statisticsFlag: true, // 显示统计计总
targetSelect: null, // 计总选中值
selectFilterVal: null, // 选中的筛选
targetSelectFlag: true, // 显示指标筛选
checkAllTarget: true, // 指标全选
fliterTarget: [], // 指标筛选选中值
isIndeterminate: false, // 全选状态
radius: ['60%', '40%'], // 扇形大小
activeChartType: 2, // 饼图类型
mergeDataOtherFlag: false, // 合并数据为其他
pieSelectType: 1, // 最大扇形数
pieSelectCount: 1, // 扇形选择数
pieSelectVal: [], // 选择数据
pieSelectList: [], // 选择数据
jumpFlag: false,
resChartData: [], // 接口返回数据
dataSourceUuid: null, // 数据源uuid
dimensionCol: [], // 维度
measureCol: [] // 度量
}
// 更新echart图表
updateChart () {
this.chart.data = cloneDeep(this.cloneChartData)
let selectRow = null;
selectRow = this.rightConfigForm.resChartData?.find(
(itx) => itx.legendUuid === this.rightConfigForm.targetSelect
);
if (selectRow) { // 显示计总
this.targetSelectName = selectRow.legendName
let total = 0;
selectRow.dataList.forEach(v => {
total += v;
})
this.chartTotal = total
}
this.fliterTargetOpt = [] // 筛选条件
if (this.rightConfigForm.fliterTarget && this.rightConfigForm.fliterTarget.length) {
this.rightConfigForm.resChartData.forEach(v => {
this.rightConfigForm.fliterTarget.forEach(m => {
if (v.legendUuid === m) {
this.fliterTargetOpt.push(v)
}
})
})
}
if (this.rightConfigForm.mergeDataOtherFlag) { // 合并数据为其他
if (this.rightConfigForm.pieSelectType === 1) { // 最大扇形数
let otherNum = 0
this.cloneChartData.forEach((v, i) => {
if (i >= (this.rightConfigForm.pieSelectCount - 1)) {
otherNum += v.value;
}
})
const otherData = { name: window.SunflowerConfig.$t('STATISTICSREPORT.TEXT_QI_TA'), value: otherNum }
const newData = cloneDeep(this.cloneChartData)
newData.splice(this.rightConfigForm.pieSelectCount - 1, (this.cloneChartData.length - (this.rightConfigForm.pieSelectCount - 1)))
this.chart.data = [...newData, otherData]
if (this.rightConfigForm.pieSelectCount === 1) {
this.chart.data = [otherData]
}
}
if (this.rightConfigForm.pieSelectType === 2) { // 选择数据
let chartDataTemp = []
let residueDataTemp = []
chartDataTemp = this.cloneChartData.filter(item => { // 过滤出相同的数据
return this.rightConfigForm.pieSelectVal.indexOf(item.name) !== -1
})
residueDataTemp = this.cloneChartData.filter(item => { // 过滤出不相同的数据
return this.rightConfigForm.pieSelectVal.indexOf(item.name) === -1
})
let otherNum = 0
chartDataTemp.forEach((v) => {
otherNum += v.value;
})
const otherData = { name: window.SunflowerConfig.$t('STATISTICSREPORT.TEXT_QI_TA'), value: otherNum }
this.chart.data = [...residueDataTemp, otherData]
}
}
if (this.rightConfigForm.targetSelectFlag) { // 显示筛选打开
const isHaveSelect = this.fliterTargetOpt.find(item => item.legendUuid === this.rightConfigForm.selectFilterVal)
if (!isHaveSelect) { // 勾选指标筛选判断下拉是否存在
this.rightConfigForm.selectFilterVal = null
this.chart.data = []
}
}
this.chart.option = {
title: {
text: '',
subtext: '',
x: '24%',
y: '80',
textStyle: {
fontSize: 12,
lineHeight: 20
},
subtextStyle: {
fontSize: 20,
fontWeight: 600
}
},
tooltip: {
show: this.rightConfigForm.toolTipFlag,
trigger: 'item',
formatter: function (param) {
return `${param.name}<br>
${param.marker}${param.percent.toFixed(2)}% (${param.value})`;
}
}
}
const mainTheme = localStorage.getItem('theme');
this.getTheme(mainTheme);
this.updateChartKey = Math.random() * 1000
}
// 更新视图
refreshResult (hand) {
let flag = false
if (this.activeChartType === 1) { // 表格
if (this.dimensionCol.length === 0) {
flag = true
}
} else {
if (this.dimensionCol.length === 0 || this.measureCol.length === 0) {
flag = true
}
}
if (flag) {
this.$message.warning('维度、度量不能为空')
return
}
this.customSearchForm = {
chartType: this.activeChartType,
viewUuid: null,
dataSourceUuid: this.dataSourceUuid,
dimension: this.activeChartType === 1 ? this.dimensionCol : this.dimensionCol[0],
metricColumnList: this.measureCol,
lineMetricColumnList: this.measureColLine,
sMetricsList: [],
conditionList: []
}
this.rightConfigForm.dimensionColumnList = this.dimensionColumnList
this.rightConfigForm.dataSourceUuid = this.dataSourceUuid
this.rightConfigForm.dimensionCol = this.dimensionCol
this.rightConfigForm.measureCol = this.measureCol
this.rightConfigForm.measureColLine = this.measureColLine
if (hand) { // 手动更新
this.rightConfigForm.selectFilterVal = null
}
// this.rightConfigForm.updateKey = Math.random()
setTimeout(() => {
this.$refs[this.resultType].getCustomChart()
}, 300)
}