如何实现自定义报表视图库前端功能

205 阅读5分钟

产品需求

产品需求希望统计报表改为自定义报表,并且内置数据的配置也是根据自定义配置而来。所以梳理下是如何开发,并且实现的,自定义报表可分为两大功能,报表配置和视图库,重点说下视图库。 ​ 视图库可分为为7个类型,分别是列表、指标、趋势图、组合图、柱状图、TOP图、环形图,数据是通过度量和维度生成的。

产品展示

1740552911524.jpg

前端开发:

设计报表配置界面:
使用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)
    }