申明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
前言
《金融行业前端探索》专题系列文章将构建现代化金融应用的技术与实践,提供理论知识和技术原理,还通过丰富的案例和实例,带大家一步步实现一个简易的金融终端系统,帮助读者将所学知识应用于实际项目中。此系列文章,适用于对金融行业前端感兴趣的同学,以及对前端跨端、可视化、AI技术、大数据等方面感兴趣的同学。需要提前了解技术:electron、vite、vue,Echart和其他前端基础知识等。
本文为《金融行业前端探索》第4篇文章 :金融行业表格业务场景介绍和增强实践,包含以下内容:
【1】 金融行业表格业务场景介绍
【2】 基于 Vue 和 vxe-table表格组件封装
【3】 表单页面组件封装
【4】 表格业务场景增强实践
一、金融行业表格业务场景介绍
在金融行业,表格业务是非常常见的,涉及到大量的数据展示、分析和处理。下面是一些金融行业常见的表格业务场景的介绍:
金融市场数据监控
金融机构和投资者需要实时监控金融市场的行情数据,例如股票行情报价(报价、自选股、综合排名、综合屏)、债券市场(经纪商报价,债券综合屏、交易所债券今日行情、CFETS债券作市报价),指数变动、汇率变动等。这些数据以表格的形式展示,此业务场景对表格性能要求很高,实时大量的更新表格数据,以及一些表格闪动效果交互,方便用户快速查看和分析市场趋势,以便进行投资决策。
金融指标报表类
金融行业数据指标非常复杂,比如F9深度资料,包含债券的基本资料,发行信息,债券估值,信用分析,相关证券。需要以表格的形式进行展示和分析,以便进行财务指标计算、趋势分析和业务决策。此业务场景报表需求可谓海量,这些报表包含大量的数据,通常我们以报表业务页面组件和低码相结合。
金融工具类表格
数据浏览器,债券计算器是一种用于计算债券相关指标的工具,此业务场景是对表格上进行大量的编辑,帮助投资者评估债券的回报率、到期收益、价格和利息支付等。
金融excel表格插件
金融Excel插件具备强大的Excel数据链接功能,方便用户动态获取实时行情、财务数据、宏观行业等数据,构建各种灵活的分析模板,并可编辑。适用于金融工程、研究、投资、理财等多场景,是广大券商、基金管理公司、保险公司、投资机构、咨询机构、监管机构和专业投资者的理想工具。
二、基于 Vue3 和 vxe-table表格组件封装
el-table和vxe-table是两个常用的Vue表格解决方案,对于复杂表格,我们认为vxe-table在一下几点更为优秀:
- 功能丰富:VXE-Table 提供了更多丰富的功能和扩展性。它支持表格的分页、排序、筛选、合并单元格、自定义模板等常见的表格操作和定制需求。VXE-Table 还支持树形表格、虚拟滚动、可编辑表格等高级功能,满足更多复杂场景的需求。
- 性能优化:VXE-Table 在性能方面进行了优化,特别是在处理大数据量和复杂表格时表现出色。它支持虚拟滚动和懒加载,可以减少渲染的数据量,提高页面加载和滚动性能。
- 扩展性强:VXE-Table 提供了丰富的插槽和 API,方便开发者进行自定义扩展。开发者可以通过自定义单元格模板、自定义表头、自定义筛选等方式来满足具体的业务需求。
- 文档和社区支持:VXE-Table 提供了完善的官方文档和示例,以及活跃的社区支持。开发者可以轻松查阅文档,了解各种功能和使用方法,并在社区中获取问题解答和交流。
实现表格内容的完全自定义
1. 对于表格封装的目标
减少在表格使用中的复杂度,简化前端开发表格方式,如下使用,我们只需要传入表头信息,和表格数据,就可以渲染出想要的表格组件
<c-table
:headData="headData"
:tableData="tableData"
>
</c-table>
2. 定义 Props 参数
通过 Props 参数将需要的配置项传递给 VXE-Table 组件。这些配置项可以包括表格的数据源、列定义、分页配置等。这里直接同VXE-Table 的Props 参数
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>
三、表单页面组件封装
表单页面在金融行业中非常常见,此类业务数量是非常巨大的,他们通常分为三部分:查询区,操作区,展示区。如下图:
查询区: 查询区放置查询条件、重置按钮(可选)、查询按钮。当查询条件过多时可使用高级搜索,频率较少的通常会放在高级筛选中,可以使用“展开”、“收起”这种方式。
操作区: 操作区通常放置全局按钮和批量操作按钮,左右均可放置,统一规范即可。
展示区: 展示区放置表头与表格内容,可根据需要增加单条操作按钮,数据过多可分页。
我们建一个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 表格数据变化闪动
方法一: 纯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 监听表格数据的变化,并在数据变化发生时触发相应的操作:
-
创建
MutationObserver实例:通过new MutationObserver(callback)创建一个MutationObserver实例,其中callback是一个回调函数,用于处理数据变化时的操作。 -
配置观察选项:定义了一个
option对象,用于配置观察的选项。具体配置包括:childList: true:观察目标元素的直接子节点的变化。subtree: true:观察目标元素的所有后代节点的变化。characterDataOldValue: true:将旧的数据传递给回调函数。
-
定义回调函数:回调函数接收一个参数
records,其中包含了发生变化的记录。通过对记录进行遍历,可以对每个变化进行处理。 -
执行操作:在回调函数中,根据记录的变化进行相应的操作。在这段代码中,如果不是第一次加载页面(
this.resTime !== 1),则给目标元素的父节点的 offsetParent 添加 "lighting" 类名,以触发相应的样式变化。然后通过setTimeout在 800 毫秒后移除 "lighting" 类名,以实现一个闪烁的效果。 -
获取监听的对象:执行
this.getMutationElements()方法,获取需要监听的对象,并将其存储在this.elementList数组中。 -
开始观察:通过遍历
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 技术栈来实现这些功能,但都是提供了一些思路和关键点的技术实现,在实际应用中,可以根据具体的业务需求和设计准则进一步完善和定制这些组件。同时,也要注意前端性能优化、数据安全和用户体验等方面的考量,以确保金融大表格应用的稳定性和可靠性。希望本文对在金融领域开发表格应用时有所帮助。