《金融行业前端探索》五、金融行业表格业务场景介绍和增强实践

988 阅读9分钟

申明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

《金融行业前端探索》专题系列文章将构建现代化金融应用的技术与实践,提供理论知识和技术原理,还通过丰富的案例和实例,带大家一步步实现一个简易的金融终端系统,帮助读者将所学知识应用于实际项目中。此系列文章,适用于对金融行业前端感兴趣的同学,以及对前端跨端、可视化、AI技术、大数据等方面感兴趣的同学。需要提前了解技术:electron、vite、vue,Echart和其他前端基础知识等。

本文为《金融行业前端探索》第4篇文章 :金融行业表格业务场景介绍和增强实践,包含以下内容:
【1】 金融行业表格业务场景介绍
【2】 基于 Vue 和 vxe-table表格组件封装
【3】 表单页面组件封装
【4】 表格业务场景增强实践

image.png

一、金融行业表格业务场景介绍

在金融行业,表格业务是非常常见的,涉及到大量的数据展示、分析和处理。下面是一些金融行业常见的表格业务场景的介绍:

金融市场数据监控

金融机构和投资者需要实时监控金融市场的行情数据,例如股票行情报价(报价、自选股、综合排名、综合屏)、债券市场(经纪商报价,债券综合屏、交易所债券今日行情、CFETS债券作市报价),指数变动、汇率变动等。这些数据以表格的形式展示,此业务场景对表格性能要求很高,实时大量的更新表格数据,以及一些表格闪动效果交互,方便用户快速查看和分析市场趋势,以便进行投资决策。

金融指标报表类

金融行业数据指标非常复杂,比如F9深度资料,包含债券的基本资料,发行信息,债券估值,信用分析,相关证券。需要以表格的形式进行展示和分析,以便进行财务指标计算、趋势分析和业务决策。此业务场景报表需求可谓海量,这些报表包含大量的数据,通常我们以报表业务页面组件和低码相结合。

金融工具类表格

数据浏览器,债券计算器是一种用于计算债券相关指标的工具,此业务场景是对表格上进行大量的编辑,帮助投资者评估债券的回报率、到期收益、价格和利息支付等。

金融excel表格插件

金融Excel插件具备强大的Excel数据链接功能,方便用户动态获取实时行情、财务数据、宏观行业等数据,构建各种灵活的分析模板,并可编辑。适用于金融工程、研究、投资、理财等多场景,是广大券商、基金管理公司、保险公司、投资机构、咨询机构、监管机构和专业投资者的理想工具。

二、基于 Vue3 和 vxe-table表格组件封装

el-table和vxe-table是两个常用的Vue表格解决方案,对于复杂表格,我们认为vxe-table在一下几点更为优秀:

  1. 功能丰富:VXE-Table 提供了更多丰富的功能和扩展性。它支持表格的分页、排序、筛选、合并单元格、自定义模板等常见的表格操作和定制需求。VXE-Table 还支持树形表格、虚拟滚动、可编辑表格等高级功能,满足更多复杂场景的需求。
  2. 性能优化:VXE-Table 在性能方面进行了优化,特别是在处理大数据量和复杂表格时表现出色。它支持虚拟滚动和懒加载,可以减少渲染的数据量,提高页面加载和滚动性能。
  3. 扩展性强:VXE-Table 提供了丰富的插槽和 API,方便开发者进行自定义扩展。开发者可以通过自定义单元格模板、自定义表头、自定义筛选等方式来满足具体的业务需求。
  4. 文档和社区支持:VXE-Table 提供了完善的官方文档和示例,以及活跃的社区支持。开发者可以轻松查阅文档,了解各种功能和使用方法,并在社区中获取问题解答和交流。

实现表格内容的完全自定义

1. 对于表格封装的目标

减少在表格使用中的复杂度,简化前端开发表格方式,如下使用,我们只需要传入表头信息,和表格数据,就可以渲染出想要的表格组件

      <c-table
         :headData="headData"
         :tableData="tableData"
        >
      </c-table>

2. 定义 Props 参数

通过 Props 参数将需要的配置项传递给 VXE-Table 组件。这些配置项可以包括表格的数据源、列定义、分页配置等。这里直接同VXE-Table 的Props 参数

image.png

3. 定义插槽

VXE-Table 提供了多个插槽,用于自定义表格的不同部分。在自定义组件中使用这些插槽,以便进行表格内容的完全自定义 表头插槽、 操作列插槽

我们这边需要考虑当数据结构比较复杂的时候,多级表头的情况的插槽,所以我们建一个列组件 c-column.vue分别来处理层级比较深的表头和列,递归组件来实现,并在插槽中赋予单元格需要的数据属性

<template>
  <vxe-colgroup
    v-if="column.column"
    :title="column.title"
    :align="column.align"
  >
    <template v-for="item in column.column" :key="item.field">
      <c-column :column="item" :treeField="treeField">
        <!-- 头部插槽数据 -->
        <template v-if="item.slotHeader" #[item.slotHeader]>
          <slot :name="item.slotHeader"></slot>
        </template>
        <template
          v-for="slotHeader in getSlotLists(item.column, 'slotHeader')"
          #[slotHeader]
        >
          <slot :name="slotHeader"> </slot>
        </template>
        <!-- 插槽数据 -->
        <template v-if="item.slot" #[item.slot]="scope">
          <slot
            :name="item.slot"
            :row="scope.row"
            :rowIndex="scope.rowIndex"
            :columnIndex="scope.columnIndex"
          ></slot>
        </template>
        <template v-for="sub in getSlotLists(item.column)" #[sub]="scope">
          <slot
            :name="sub"
            :row="scope.row"
            :rowIndex="scope.rowIndex"
            :columnIndex="scope.columnIndex"
          >
          </slot>
        </template>
      </c-column>
    </template>
  </vxe-colgroup>
  <vxe-column
    v-else
    :field="column.field"
    :title="column.title"
    :sortable="column.sortable"
    :width="column.width"
    :class-name="column.className"
    :min-width="column.minWidth"
    :align="column.align"
    :fixed="column.fixed"
    :type="column.type"
    :show-header-overflow="!column.showNormal"
    :tree-node="column.field === treeField"
     show-overflow="tooltip"
  >
    <!-- 表头部 插槽 -->
    <template v-if="column.slotHeader" #header>
      <slot :name="column.slotHeader"></slot>
    </template>
    <template v-if="column.slot" #default="slotProps">
      <!-- 插槽真正显示数据 -->
      <slot
        :name="column.slot"
        :row="slotProps.row"
        :rowIndex="slotProps.rowIndex"
        :columnIndex="slotProps.columnIndex"
      ></slot>
    </template>
  </vxe-column>
</template>

对应的table里我们也是遍历表头数据 c-table.vue,一层层传递直到最后一层

        <!-- 动态渲染表格数据 -->
        <template v-for="item in headData" :key="item.field">
          <c-olumn :column="item" :treeField="treeField">
            <!-- 头部插槽数据 -->
            <template v-if="item.slotHeader" #[item.slotHeader]>
              <slot :name="item.slotHeader"></slot>
            </template>

            <template
              v-for="slotHeader in getSlotLists(item.column, 'slotHeader')"
              #[slotHeader]
            >
              <slot :name="slotHeader"> </slot>
            </template>

            <!-- 插槽数据 -->
            <template v-if="item.slot" #[item.slot]="scope">
              <slot
                :name="item.slot"
                :row="scope.row"
                :rowIndex="scope.rowIndex"
                :columnIndex="scope.columnIndex"
              ></slot>
            </template>
            <template v-for="sub in getSlotLists(item.column)" #[sub]="scope">
              <slot
                :name="sub"
                :row="scope.row"
                :rowIndex="scope.rowIndex"
                :columnIndex="scope.columnIndex"
              >
              </slot>
            </template>
          </c-column>
        </template>

4 插槽的使用

我们在使用中只需要对需要插槽的地方进行 同名插入,对应的表头或者单元格即可同步渲染出来了~

<c-table>
 <template #test="slotProps">
  <!-- 插槽内容 -->
  </template>
</c-table>

三、表单页面组件封装

表单页面在金融行业中非常常见,此类业务数量是非常巨大的,他们通常分为三部分:查询区,操作区,展示区。如下图:

1716357826973.png

查询区: 查询区放置查询条件、重置按钮(可选)、查询按钮。当查询条件过多时可使用高级搜索,频率较少的通常会放在高级筛选中,可以使用“展开”、“收起”这种方式。

操作区: 操作区通常放置全局按钮和批量操作按钮,左右均可放置,统一规范即可。

展示区: 展示区放置表头与表格内容,可根据需要增加单条操作按钮,数据过多可分页。

我们建一个c-table-search.vue,这里主要放一些查询类型的组件比如 日期、输入框、下拉选项,多选...,例如:(此处只展示部分样例)

      <el-form :inline="true" :model="queryVal">
        <el-form-item
          v-for="item in filterItems"
          :label="item.label"
          :key="item.key"
        >
          <template v-if="item.type?.toLowerCase() === 'select'">
            <el-select v-model="queryVal[item.key]">
              <el-option
                :label="option.label"
                :value="option.value"
                v-for="(option, index) in item.data"
                :key="index"
              ></el-option>
            </el-select>
          </template>

          <template v-if="item.type?.toLowerCase() === 'Input'">
            <el-input
              v-model="queryVal[item.key]"
              :placeholder="item.props?.placeholder"
            ></el-input>
          </template>
        </el-form-item>
        ....
        <el-form-item>
          <el-button type="primary" @click="$_onSearch">查询</el-button>
        </el-form-item>
      </el-form>

数据部分我们双向绑定即可, 监听搜索项的变化,并在用户进行搜索操作时触发数据查询。

<script>
export default {
  name: 'c-table-search',
  props: {
    filterItems: Array
  },
  data() {
    let queryVal = {}
    this.filterItems.forEach(({ key }) => {
      queryVal[key] = ''
    })

    return {
      queryVal
    }
  },
  methods: {
    $_onSearch() {
      let params = this.queryVal
      this.$emit('doSearch', params)
    }
  }
}
</script>

通过封装报表页面组件,我们可以实现页面的模块化和可复用性,减少重复开发工作,并提供一致的用户体验。

五、表格业务场景增强实践

5.1 滚动出现白屏情况

1、数据量过大,快速滚动

2、虚拟滚动、动态合并、固定栏,以上任意组合 + 快速滚动

问题分析:chrome版本在90以下,未出现白屏现象;对比了当前最新版本发现浏览器对滚动频率做了改变导致

解决办法:调整滚动频率,scroll-y="{ enabled: true, mode: 'wheel' }" 也可以 手动调优,对于低性能的浏览器可以通过设置 oSize 偏移量来缓解渲染次数,偏移量越大渲染次数就越少,但是每次渲染的耗时就越久

5.2 滚动加载

在滚动到底部时触发自定义事件。通过节流函数 debounce,可以控制事件的频率,避免过于频繁地触发事件。在滚动事件处理函数中,还进行了一些计算和判断,以确定是否满足触发条件

   // 滚动加载分页
    scroll: debounce(function ({ scrollTop, scrollHeight }) {
      // 横向滚动return
      if (!scrollTop) return;
      const scrollDistance = scrollHeight - scrollTop;
      // 获取当前table 内容高度
      const tableHeight =
        this.$refs.table.parentHeight - this.$refs.table.headerHeight;
      if (0 <= scrollDistance && scrollDistance <= tableHeight) {
        this.$emit("scrollEvent");
      }
    }, 300)

5.3 表格数据变化闪动

image.png

方法一: 纯CSS方案

这里我们通过数据变化实时计算表格的样式涨跌颜色判断,并 CSS 中用于设置元素过渡效果的属性,1. 淡入淡出效果:通过设置 opacity 属性的过渡,可以实现元素的淡入淡出效果。当元素的 opacity 值从 0(完全透明)过渡到 1(完全不透明)时,会在 1 秒的时间内渐变显示,从而产生淡入的效果。同样地,当 opacity 值从 1 过渡到 0 时,会在 1 秒的时间内渐变隐藏,产生淡出的效果。当然也可以通过

        <template #default="{ row }">
          <div :class="getFontColor(row.px_change_rate, row)">
            <i
              :class="getIcon(row.px_change_rate)"
            ></i>
            {{ row.px_change_rate === "" ? "--" : row.px_change_rate + "%" }}
          </div>
        </template>
        ...
        
   // 涨跌颜色判断
    getFontColor(val, row) {
      if (row && (row.rateStyle || row.newStyle) && !this.countFisrt) {
        return val > 0 ? "red lighting" : val < 0 ? "green lighting" : "";
      } else {
        return val > 0 ? "red" : val < 0 ? "green" : "";
      }
    },

css

  .red {
    color: #ff4d4f;
    i {
      line-height: 22px;
    }
    &.lighting {
      background-color: #fff2f0;
      border-radius: 4px;
      opacity: 0.8;
      transition: opacity 1s;
    }
  }

方法二: new MutationObserver(callback)方案

还有一种方法是使用 MutationObserver 监听表格数据的变化,并在数据变化发生时触发相应的操作:

  1. 创建 MutationObserver 实例:通过 new MutationObserver(callback) 创建一个 MutationObserver 实例,其中 callback 是一个回调函数,用于处理数据变化时的操作。

  2. 配置观察选项:定义了一个 option 对象,用于配置观察的选项。具体配置包括:

    • childList: true:观察目标元素的直接子节点的变化。
    • subtree: true:观察目标元素的所有后代节点的变化。
    • characterDataOldValue: true:将旧的数据传递给回调函数。
  3. 定义回调函数:回调函数接收一个参数 records,其中包含了发生变化的记录。通过对记录进行遍历,可以对每个变化进行处理。

  4. 执行操作:在回调函数中,根据记录的变化进行相应的操作。在这段代码中,如果不是第一次加载页面(this.resTime !== 1),则给目标元素的父节点的 offsetParent 添加 "lighting" 类名,以触发相应的样式变化。然后通过 setTimeout 在 800 毫秒后移除 "lighting" 类名,以实现一个闪烁的效果。

  5. 获取监听的对象:执行 this.getMutationElements() 方法,获取需要监听的对象,并将其存储在 this.elementList 数组中。

  6. 开始观察:通过遍历 this.elementList,对每个元素调用 mo.observe(item, option) 方法,开始观察其变化

    observeTableData() {
      const option = {
        childList: true, // 观察直接子节点
        subtree: true, // 及其更低的后代节点
        characterDataOldValue: true, // 将旧的数据传递给回调
      };
      const mo = new MutationObserver((records) => {
        records.map((record) => {
          // 不是第一次加载页面
          if (this.resTime !== 1) {
            record.target.parentNode.offsetParent.classList.add("lighting");
            setTimeout(() => {
              record.target.parentNode.offsetParent.classList.remove(
                "lighting"
              );
            }, 800);
          }
        });
      });
      // 获取监听的对象
      this.getMutationElements();
      this.elementList.map((item) => {
        mo.observe(item, option);
      });
    },

结论

本文介绍了面对金融大表格业务时的技术分析和增强实践。从业务场景介绍开始,我们了解了金融大表格应用的需求和挑战。然后,我们讨论了如何封装表格组件和报表页面组件,以提供高性能、可扩展且用户友好的解决方案。

最后,我们使用 Vue 和 Element UI 技术栈来实现这些功能,但都是提供了一些思路和关键点的技术实现,在实际应用中,可以根据具体的业务需求和设计准则进一步完善和定制这些组件。同时,也要注意前端性能优化、数据安全和用户体验等方面的考量,以确保金融大表格应用的稳定性和可靠性。希望本文对在金融领域开发表格应用时有所帮助。